model-billing.js 1.97 KB
const MICROS_PER_YUAN = 1_000_000

const POLICY_SETTLERS = {
  record_only() {
    return 0
  },
  fixed_points(policy) {
    return policy.points
  },
  cost_ratio(policy, supplierCost) {
    return Math.ceil(supplierCost.supplier_cost_micros * policy.points_per_yuan / MICROS_PER_YUAN)
  },
}

/**
 * 模型供应商成本到用户积分的唯一结算入口。
 * @param {{ deduct:(input:object)=>Promise<{balance_after:number,ledger_id:string}> }} deps
 */
export function createModelBilling({ deduct }) {
  return async function settleModelBilling(input) {
    const settlement = modelSettlement(input.policy, input.supplier_cost)
    if (settlement.charged_points === 0) return { ...settlement, ledger_id: null }
    const ledger = await deduct({
      user_id: input.user_id,
      amount: settlement.charged_points,
      reason: 'model_usage',
      ref_type: input.call_ref.type,
      ref_id: input.call_ref.id,
      idempotency_key: input.idempotency_key,
    })
    return { ...settlement, ledger_id: ledger.ledger_id }
  }
}

/**
 * 结算入口的唯一数据构造点。非法策略和金额在写 ledger 前立即失败。
 * @param {Record<string, any>} input
 */
function modelSettlement(policy, supplierCost) {
  const billingPolicySnapshot = policySnapshot(policy)
  const chargedPoints = POLICY_SETTLERS[policy.mode](billingPolicySnapshot, supplierCost)
  return {
    billing_mode: billingPolicySnapshot.mode,
    billing_policy_snapshot: billingPolicySnapshot,
    charged_points: chargedPoints,
  }
}

/** 校验并构造可审计的精确策略快照;不接受未声明字段。 */
function policySnapshot(policy) {
  switch (policy.mode) {
    case 'record_only':
      return { mode: policy.mode, reason: policy.reason }
    case 'fixed_points':
      return { mode: policy.mode, points: policy.points }
    case 'cost_ratio':
      return { mode: policy.mode, points_per_yuan: policy.points_per_yuan }
    default:
      throw new Error(`Unhandled billing mode: ${policy.mode}`)
  }
}