Braintree 是否支持分期付款?
结论
Braintree 通常不提供独立的“分期付款计划(installment plan)”功能。它的 Recurring Billing 主要用于订阅,也可以通过限定订阅的计费次数,模拟固定期数的分期扣款。
例如,业务要求“首期立即支付,之后每月扣款,共 12 期”时,通常需要:
- 创建按月计费的 Braintree Plan。
- 使用该 Plan 创建 Subscription。
- 在应用数据库中记录总期数和已支付期数。
- 达到约定期数后主动取消 Subscription。
Braintree 负责保存支付方式、定期发起交易并处理支付结果。分期合同、剩余本金、期数终止等业务规则,通常仍由商户系统管理。
为什么 Recurring Billing 不等于完整的分期付款
订阅和分期付款都可以表现为周期性收费,但两者的业务含义并不相同:
- 订阅通常会持续提供服务,直到用户或商户取消。
- 分期付款有固定总金额、固定期数和明确的结束条件。
- 分期业务还可能涉及首付款、利息、提前结清、退款、逾期和监管要求。
即使使用 Subscription 完成周期扣款,也不能只依赖 Braintree 的订阅状态管理分期计划。商户系统还应保存并校验以下信息:
- 分期订单 ID
- 总金额
- 每期金额
- 总期数
- 已成功支付期数
- 下一扣款日期
- 剩余应付金额
- 分期状态
- 对应的 Braintree Subscription ID
实现方式一:用 Subscription 模拟固定期数分期
先在 Braintree 控制台配置按月计费的 Plan,再使用客户的 paymentMethodToken 创建订阅:
const result = await gateway.subscription.create({
paymentMethodToken: paymentMethodToken,
planId: "installment-monthly-plan",
price: "100.00"
});
if (!result.success) {
throw new Error(result.message);
}
const subscriptionId = result.subscription.id;
假设订单总额为 1200.00,每月支付 100.00,共 12 期,应用端应同时保存类似的数据:
const installment = {
orderId: "ORDER-10001",
subscriptionId,
totalAmount: "1200.00",
installmentAmount: "100.00",
totalInstallments: 12,
paidInstallments: 0,
status: "ACTIVE"
};
每次收到 Braintree Webhook 后,再根据实际事件更新付款记录。只有确认对应交易成功,才能增加已支付期数。
下面是简化后的示意代码。具体事件类型和字段应以当前 Braintree SDK 及官方文档为准:
const webhookNotification = await gateway.webhookNotification.parse(
req.body.bt_signature,
req.body.bt_payload
);
const subscriptionId = webhookNotification.subscription?.id;
if (isSuccessfulBillingEvent(webhookNotification.kind)) {
const installment = await findInstallmentBySubscriptionId(subscriptionId);
if (installment && !await eventAlreadyProcessed(webhookNotification)) {
installment.paidInstallments += 1;
if (installment.paidInstallments >= installment.totalInstallments) {
const cancelResult = await gateway.subscription.cancel(subscriptionId);
if (!cancelResult.success) {
throw new Error(cancelResult.message);
}
installment.status = "COMPLETED";
}
await saveInstallment(installment);
await markEventProcessed(webhookNotification);
}
}
isSuccessfulBillingEvent 和幂等记录都属于业务层逻辑,这段示意不能直接当作完整的生产代码使用。尤其要先确认 Webhook 表示的是“已成功结算”“已提交结算”还是其他状态,再判断何时将该期标记为已支付。
实现方式二:由商户系统逐期创建 Transaction
如果每期金额或扣款日期可能变化,或者需要更精确地控制扣款次数,可以不使用 Subscription。商户系统可以保存客户的支付方式 Token,再通过定时任务逐期创建交易:
const result = await gateway.transaction.sale({
amount: "100.00",
paymentMethodToken: paymentMethodToken,
orderId: "ORDER-10001-INSTALLMENT-03",
options: {
submitForSettlement: true
}
});
if (!result.success) {
throw new Error(result.message);
}
这种方式更灵活,但以下工作需要由商户自行完成:
- 扣款调度
- 重试策略
- 失败通知
- 期数计算
- 幂等控制
- 对账
- 计划终止
- 支付方式失效处理
不要保存原始卡号、CVV 等敏感卡数据。应使用 Braintree Vault 返回的 paymentMethodToken,并遵守适用的 PCI DSS 要求。
金额不能整除时的处理
总金额不一定能被期数整除。例如,将 100.00 分成 3 期时,如果每期都收取 33.33,最终合计只有 99.99。
金额计算应使用最小货币单位,以免出现浮点误差:
function splitInstallments(totalMinorUnits, count) {
const base = Math.floor(totalMinorUnits / count);
const remainder = totalMinorUnits % count;
return Array.from({ length: count }, (_, index) =>
base + (index < remainder ? 1 : 0)
);
}
console.log(splitInstallments(10000, 3));
// [3334, 3333, 3333]
对于人民币,10000 表示 100.00 元,即 10000 分。不同货币的小数位规则可能不同,应按具体货币处理。
注意事项
不要只按时间自动增加已支付期数
进入下一个月,并不代表上一期已经支付成功。已支付期数应以实际交易结果为准,同时还要处理扣款失败、重试、拒付和退款。
Webhook 必须做幂等处理
同一通知可能因重试而多次送达。系统应使用能够稳定识别事件或交易的标识,避免重复记账,不能收到通知后直接执行 paidInstallments += 1。
完成最后一期后及时停止订阅
使用 Subscription 模拟分期时,必须设置可靠的终止机制,否则订阅可能在约定期数结束后继续扣款。取消失败时,还应触发告警和人工处理流程。
退款不等于减少一期
全额退款、部分退款和拒付都会影响剩余应付金额,但不能简单地折算成“少付一期”。订单账本应分别记录扣款、退款和争议交易。
分期能力可能因支付方式和地区而不同
部分地区或支付方式可能支持由发卡行、钱包或支付服务商管理的“先买后付”或卡分期。这类产品与商户通过 Recurring Billing 模拟分期并不相同。是否可用,取决于商户所在地区、账户配置、支付方式以及 Braintree 当前支持的产品。上线前,应查看最新官方文档,并向 Braintree 支持团队确认具体商户账户的支持情况。
选择建议
如果金额和周期固定,业务规则也比较简单,可以使用 Subscription,由应用记录分期期数,并在最后一期支付成功后取消订阅。
如果扣款日期、每期金额、重试方式或提前结清规则较复杂,更适合由应用逐期创建 Transaction。如果需要金融意义上的信用分期、利息计算或“先买后付”,应先确认 Braintree 在目标市场是否提供相应的支付方式,不要把普通 Subscription 当作完整的分期产品。
备注:内容仅供参考。
