Braintree 如何按当前周期最高价格执行 proration
结论
Braintree 没有内置配置可以让 proration 按当前 billing cycle 内的最高价格计算。
prorateCharges: false 通常只表示这次修改不会立即生成按比例交易,并不代表 Braintree 会继续以 $100 作为本周期后续 proration 的比较基准。金额从 $100 改为 $80 后,subscription 的当前金额已经是 $80。随后启用 proration 并将金额改为 $90 时,gateway 会把它视为从 $80 涨到 $90,因此可能立即收费。
如果只想在新价格超过本周期最高价格时产生 proration transaction,就需要由业务层维护本周期的价格高水位,并推迟向 Braintree 提交 downgrade。也就是说,在下一个周期开始前,Braintree 中仍保留 $100。这样,调整到 $90 不会被视为 upgrade,调整到 $110 时才会按 $100 到 $110 的差额计算 proration。
为什么关闭 downgrade proration 后仍然会收费
需要区分两件事:
- subscription 的
amount是否更新; - 这次更新是否立即产生 proration charge 或 credit。
执行以下操作时:
$100 → $80,prorateCharges = false
Braintree 可以不生成 downgrade credit,但 subscription 的金额仍可能立即保存为 $80。
之后再执行:
$80 → $90,prorateCharges = true
按照 gateway 当前保存的数据,这是一次 $10 的涨价。Braintree 不知道业务上希望将 $100 保留为本周期最高价格,也不会回溯本周期的历史金额并取最大值。
所以,只切换 prorateCharges 无法实现价格高水位规则。
推荐做法:延迟提交 downgrade
业务系统至少需要为每个 subscription 保存以下状态:
cycleHighAmount 本周期已经计费或用于 proration 的最高金额
pendingNextCycleAmount 下周期准备生效的金额
billingPeriodEnd 当前周期结束时间
价格变更可以按以下规则处理:
-
新价格低于或等于
cycleHighAmount时:- 不修改 Braintree subscription 的
amount; - 将新价格保存到
pendingNextCycleAmount; - 不发起带 proration 的 upgrade;
- 业务权益是否立即降级,由产品规则决定。
- 不修改 Braintree subscription 的
-
新价格高于
cycleHighAmount时:- 将 Braintree subscription 从当前高水位金额更新为新价格;
- 设置
prorateCharges: true; - transaction 成功后,将
cycleHighAmount更新为新价格。
-
下一个 billing cycle 开始前:
- 将 Braintree subscription 更新为
pendingNextCycleAmount; - 是否关闭 proration,取决于实际调度时点;
- 新周期开始后,将该周期的价格保存为新的
cycleHighAmount。
- 将 Braintree subscription 更新为
以题目中的价格变化为例:
周期开始:Braintree amount = $100,cycleHighAmount = $100
请求降到 $80:
不修改 Braintree amount
pendingNextCycleAmount = $80
随后请求升到 $90:
$90 <= $100
仍不修改 Braintree amount
pendingNextCycleAmount = $90
不产生 transaction
随后请求升到 $110:
$110 > $100
Braintree amount 从 $100 更新到 $110,并启用 proration
只对 $100 → $110 的差额进行按比例收费
Node.js 示例
以下代码用于说明业务流程。数据库事务、金额类型和错误恢复仍需根据实际系统补充。
const braintree = require("braintree");
async function changeSubscriptionPrice({
subscriptionId,
newAmount,
pricingState
}) {
// 金额应使用 Decimal 或最小货币单位比较。
// 此处为了突出流程,假设传入值均为规范化的十进制字符串。
const requested = Number(newAmount);
const cycleHigh = Number(pricingState.cycleHighAmount);
if (requested <= cycleHigh) {
// 不要把 downgrade 写入 Braintree,否则 gateway 会丢失本周期高水位。
await savePricingState(subscriptionId, {
pendingNextCycleAmount: newAmount
});
return {
prorated: false,
pendingNextCycleAmount: newAmount
};
}
const result = await gateway.subscription.update(subscriptionId, {
amount: newAmount,
options: {
prorateCharges: true
}
});
if (!result.success) {
throw new Error(
result.message || "Unable to update Braintree subscription"
);
}
await savePricingState(subscriptionId, {
cycleHighAmount: newAmount,
pendingNextCycleAmount: newAmount
});
return {
prorated: true,
subscription: result.subscription
};
}
周期切换时,再应用待生效的价格:
async function applyNextCyclePrice(subscriptionId, pricingState) {
const nextAmount = pricingState.pendingNextCycleAmount;
const result = await gateway.subscription.update(subscriptionId, {
amount: nextAmount,
options: {
prorateCharges: false
}
});
if (!result.success) {
throw new Error(
result.message || "Unable to apply next-cycle subscription price"
);
}
await savePricingState(subscriptionId, {
cycleHighAmount: nextAmount,
pendingNextCycleAmount: nextAmount
});
return result.subscription;
}
gateway 的初始化方式如下:
const gateway = new braintree.BraintreeGateway({
environment: braintree.Environment.Production,
merchantId: process.env.BRAINTREE_MERCHANT_ID,
publicKey: process.env.BRAINTREE_PUBLIC_KEY,
privateKey: process.env.BRAINTREE_PRIVATE_KEY
});
如果必须立即更新 Braintree 中的金额
如果业务要求每次价格变化都立即写入 Braintree,gateway 中的金额就会变成 $80。之后将金额更新为 $110 并启用原生 proration 时,Braintree 可能会按 $80 到 $110 计算,而不是按业务期望的 $100 到 $110 计算。
这种情况下,不应直接使用 Braintree 的自动 proration。可以改为:
- 始终使用
prorateCharges: false更新 subscription; - 在业务层保存本周期最高价格;
- 新价格超过高水位时,自行计算超出部分;
- 使用独立 transaction 收取差额;
- 收款成功后更新高水位。
计算公式可以表示为:
proration charge
= (newAmount - cycleHighAmount)
× remainingBillingPeriodRatio
不过,这种方案需要自行处理计费周期边界、时区、舍入、税费、折扣、失败重试和退款,也容易与 Braintree 的周期计算出现偏差。除非业务确实需要完全自定义计费规则,否则更适合采用延迟提交 downgrade 的方案。
注意事项
- 生产环境中不要用 JavaScript
Number直接处理货币。应使用 Decimal 类型,或者统一换算为 cents 等最小货币单位。 - 更新 subscription 和写入
cycleHighAmount时需要并发控制,否则两个同时到达的 upgrade 请求可能造成重复收费。 - 每次价格变更请求都应携带幂等键或业务操作 ID,避免超时重试产生重复 transaction。
- 必须处理 proration transaction 失败的情况。gateway 更新失败时,不要提前提高本地高水位。
- downgrade 应在续费扣款发生前写入 Braintree。调度过晚时,旧金额可能已经用于下一次扣款。
billingPeriodEnd、webhook 到达时间与实际扣款时间之间可能存在边界差异。上线前应在 Braintree Sandbox 中验证具体的调度时点。- 折扣、add-on、discount、税费或不同 plan 之间的切换,会让 “subscription price” 不再只是一个
amount。此时应根据实际参与比较的计费组成维护高水位,而不是只比较基础金额。 - 如果 Braintree SDK 或账户配置下的实际行为与上述前提不同,应以 Sandbox 返回的 subscription 状态和 transaction 记录为准。Braintree 不会保存业务所需的 “周期最高价格”,这项状态需要由应用自行维护。
备注:内容仅供参考。
