billing.js 3.85 KB
/**
 * `billing.js` —— `createBilling`:装配各能力 + 暴露 `asBillingPort`(§3 唯一编排入口)。
 *
 * 只做装配:注入 `billingRepo` → 组出 ledger(deduct/grant)、paywall、products、port。
 * 装配时即自检(少注入 billingRepo 当场 assert,不拖到运行某行——架构 §10.3)。
 * 不读 env、不建连接。
 *
 * @typedef {import('./repo/index.js').BillingRepo} BillingRepo
 * @typedef {import('./port.js').BillingPort} BillingPort
 */

import { createLedger } from './ledger.js'
import { createActionPolicies } from './action-policies.js'
import { createPaywall } from './paywall.js'
import { createProducts } from './products.js'
import { asBillingPort } from './port.js'
import { createModelBilling } from './model-billing.js'
import { createDailyClaim } from './daily-claim.js'
import { createCommerceOperations } from './commerce-operations.js'
import { createSignupReward } from './signup-reward.js'

/**
 * 工厂:唯一入口(§3)。
 * @param {object} [deps]
 * @param {BillingRepo} [deps.billingRepo]  注入真实数据库仓库(createBillingRepo)
 * @returns {{
 *   deduct:Function, grant:Function, reversePayment:Function, balance:Function, ledger:Function, findLedgerByIdempotencyKey:Function,
 *   settleModelBilling:Function,
 *   paywall:{ check:Function, unlock:Function, listUnlocked:Function },
 *   products:{ list:Function, get:Function, save:Function },
 *   withTransaction?:Function,
 *   asBillingPort:() => BillingPort
 * }}
 */
export function createBilling({ billingRepo, onDeductCommitted = null } = {}) {
  if (!billingRepo || typeof billingRepo.applyDelta !== 'function') {
    throw new Error('[billing] createBilling 需要注入 billingRepo')
  }
  /** @type {BillingRepo} */
  const repo = billingRepo

  const ledgerOperations = createLedger(repo)
  async function deduct(input) {
    const result = await ledgerOperations.deduct(input)
    if (!result.replayed && onDeductCommitted !== null) {
      repo.afterCommit(() => onDeductCommitted({
        user_id: input.user_id,
        balance_after: result.balance_after,
        ledger_id: result.ledger_id,
      }))
    }
    return result
  }
  const { grant, reversePayment } = ledgerOperations
  const actionPolicies = createActionPolicies(repo)
  const settleModelBilling = createModelBilling({ deduct })
  const paywall = createPaywall({ billingRepo: repo, deduct })
  const products = createProducts(repo)

  /**
   * 当前余额。
   * @param {{ user_id:string }} p
   * @returns {Promise<{ points:number }>}
   */
  async function balance({ user_id }) {
    const points = await repo.getBalance(user_id)
    return { points: Number(points) }
  }

  /**
   * 可审计流水(按时间倒序,支持类型/时间过滤与分页)。
   * @param {object} p
   * @param {string} p.user_id
   * @param {{ type?:string, since?:string, until?:string, limit?:number, cursor?:string }} [p.filter]
   * @returns {Promise<{ items:any[], next_cursor:string|null }>}
   */
  async function ledger({ user_id, filter }) {
    return repo.listLedger(user_id, filter ?? {})
  }

  async function findLedgerByIdempotencyKey(key) {
    return repo.findLedgerByIdempotencyKey(key)
  }

  function withTransaction(trx) {
    if (typeof repo.forTransaction !== 'function') {
      throw new Error('[billing] 当前 billingRepo 不支持事务绑定')
    }
    return createBilling({ billingRepo: repo.forTransaction(trx), onDeductCommitted })
  }

  const billing = {
    deduct,
    grant,
    reversePayment,
    balance,
    ledger,
    findLedgerByIdempotencyKey,
    settleModelBilling,
    paywall,
    products,
    actionPolicies,
    withTransaction,
    asBillingPort: () => asBillingPort({ deduct }),
  }
  billing.dailyClaim = createDailyClaim(billing)
  billing.signupReward = createSignupReward(billing)
  billing.commerce = createCommerceOperations(billing)
  return billing
}