PAYATHON 2026

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       当前周期结束时间

价格变更可以按以下规则处理:

  1. 新价格低于或等于 cycleHighAmount 时:

    • 不修改 Braintree subscription 的 amount;
    • 将新价格保存到 pendingNextCycleAmount;
    • 不发起带 proration 的 upgrade;
    • 业务权益是否立即降级,由产品规则决定。
  2. 新价格高于 cycleHighAmount 时:

    • 将 Braintree subscription 从当前高水位金额更新为新价格;
    • 设置 prorateCharges: true;
    • transaction 成功后,将 cycleHighAmount 更新为新价格。
  3. 下一个 billing cycle 开始前:

    • 将 Braintree subscription 更新为 pendingNextCycleAmount;
    • 是否关闭 proration,取决于实际调度时点;
    • 新周期开始后,将该周期的价格保存为新的 cycleHighAmount。

以题目中的价格变化为例:

周期开始: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。可以改为:

  1. 始终使用 prorateCharges: false 更新 subscription;
  2. 在业务层保存本周期最高价格;
  3. 新价格超过高水位时,自行计算超出部分;
  4. 使用独立 transaction 收取差额;
  5. 收款成功后更新高水位。

计算公式可以表示为:

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 不会保存业务所需的 “周期最高价格”,这项状态需要由应用自行维护。

备注:内容仅供参考。