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 } }