paywall-repo.js 2.73 KB
import { randomUUID } from 'node:crypto'
import {
  billingFreeQuotaTable,
  billingPaywallRuleTable,
  billingUnlockTable,
} from '../db/index.js'

const QUOTA = billingFreeQuotaTable.tableName
const RULE = billingPaywallRuleTable.tableName
const UNLOCK = billingUnlockTable.tableName

export function createPaywallRepo(db, runner = db) {
  const transactionBound = runner !== db

  async function inTransaction(work) {
    if (transactionBound) return work(runner)
    return db.transaction(work)
  }

  async function findUnlock({ user_id, content_id, target_key }) {
    const row = await runner(UNLOCK).where({ user_id, content_id, target_key }).first()
    if (row === undefined) return null
    return { ledger_id: row.ledger_id }
  }

  async function insertUnlock({ user_id, content_id, target_key, ledger_id }) {
    await runner(UNLOCK).insert({ id: randomUUID(), user_id, content_id, target_key, ledger_id, created_at: new Date() })
  }

  async function listUnlocks(user_id, content_id) {
    const rows = await runner(UNLOCK).where({ user_id, content_id })
    return rows.map(row => ({ target_key: row.target_key, ledger_id: row.ledger_id }))
  }

  async function getFreeQuotaUsage({ user_id, content_id, kind, window }) {
    const row = await runner(QUOTA).where({ user_id, content_id, kind, window_start: window }).first()
    if (row === undefined) return 0
    return Number(row.used)
  }

  async function bumpFreeQuota({ user_id, content_id, kind, window }) {
    return inTransaction(async trx => {
      const existing = await trx(QUOTA).where({ user_id, content_id, kind, window_start: window }).first().forUpdate()
      if (existing === undefined) {
        await trx(QUOTA).insert({ user_id, content_id, kind, window_start: window, used: 1 })
        return
      }
      await trx(QUOTA).where({ user_id, content_id, kind, window_start: window }).update({ used: Number(existing.used) + 1 })
    })
  }

  async function upsertPaywallRule(rule) {
    await runner(RULE).insert({
      content_id: rule.content_id,
      free_episodes: rule.free_episodes,
      free_continue_per_day: rule.free_continue_per_day,
      prices_json: JSON.stringify(rule.prices),
      active: rule.active,
      updated_at: new Date(),
    }).onConflict('content_id').merge()
    return listPaywallRules(rule.content_id)
  }

  async function listPaywallRules(content_id) {
    const row = await runner(RULE).where({ content_id }).first()
    if (row === undefined) return null
    return {
      free_episodes: Number(row.free_episodes),
      free_continue_per_day: Number(row.free_continue_per_day),
      prices: JSON.parse(row.prices_json),
    }
  }

  return { findUnlock, insertUnlock, listUnlocks, getFreeQuotaUsage, bumpFreeQuota, upsertPaywallRule, listPaywallRules }
}