Payment 入账
PaymentService 拥有支付订单,确认 paid 后向 Primary Card 发放额度。
const credits = new CreditsService();
const payment = new PaymentService({
providers: [stripePaymentProvider()],
resolve_topup: ({ topup_amount_minor }) => ({
credits: topup_amount_minor * 10_000,
}),
on_paid: async (record) => {
throw new Error("此接入仅支持自由充值");
}
await credits.topup({
card: { kind: "primary", user_id: record.user_id },
credits: record.credits,
source: "payment",
ref: record.payment_id,
idempotency_key: `payment:${record.payment_id}`,
note: record.note,
});
},
});用户直接创建支付订单,无需先在 Credits 中创建 pending Topup:
await city.payment.method("stripe").invoke({
topup_amount_minor: 500,
idempotency_key: "order_123",
note: "购买 Credits",
});用户提交希望支付的金额,resolve_topup 在 Federation 服务端计算并冻结本单 Credits,客户端不能指定兑换比例。支付确认必须依赖验签事件或 Provider API 的可信金额、币种与外部交易标识,并与本地订单快照核对;请求 metadata 不能充当实付证明。税费、折扣和换汇的金额口径必须明确,无法确认或不匹配时阻止发放并转人工核对。所有 paid 事件统一要求可信金额、币种与外部交易标识,缺失或不匹配以 422 转入 review。Stripe 按 subtotal 减 discount 投影本金并核对总额等式;Creem 使用签名订单的 amount_paid(存在时优先)或 amount;Waffo 严格按币种精度转换签名十进制金额;Dodo 当前强制一次性动态金额、同币种、无折扣/PPP 且税含定价,使用签名 total_amount 核对。具体字段和边界见各 Provider 页面;不承诺通用税费、折扣或换汇自动支持。
事件收件箱与恢复
Webhook 验签并规范化后,先持久化可信事件到 inbox,再入队,成功后返回 HTTP 200;这表示已接收,不表示发放完成。入队失败返回错误,但已保存的事件可由恢复扫描重新唤醒。Queue 执行 payment.events/process,重放使用保存的规范化事件,不重新验证旧 HTTP 请求的签名。
宿主必须周期调用 payment.recover_events(),或由管理员调用 POST /v1/payment/events/recover,扫描到期 pending 和租约过期 processing。当前 Edge Worker 每五分钟执行扫描,部署配置通过 federation.json 的 deployment.crons 声明计划。
临时失败从 15 秒开始指数退避,延迟上限一小时,最多尝试 20 次后进入 review。金额校验失败(422)直接进入 review,不无限自动重试。
管理员通过 GET /v1/payment/events/inbox 查看最近 100 条记录、状态、尝试次数与错误诊断;修复原因后调用 POST /v1/payment/events/replay,JSON 为 { "event_id": "provider:event_id" }。重放会再次经过订单校验与幂等发放,不绕过金额校验。on_paid(record) 必须使用稳定 payment_id 及权益快照实现幂等,避免重试重复发放。