利用 Stripe 的订阅计划功能:探索之旅

使用 Stripe API 更新订阅是一项常见操作,API 的使用也相对简单。然而,有时由于账单日期或产品订阅类型的原因,我们无法立即更新一个订阅,这时就需要利用订阅计划(Subscription Schedule)功能,在未来的某个时间创建或更新订阅。

订阅计划的强大功能

订阅计划是一个非常强大的功能。根据我个人的开发经验,最初在使用这个功能时遇到了许多困惑。尽管 Stripe 提供了详细的 API 文档 和 用例 来描述订阅计划,但由于 Stripe 的 API 不断发展,一些新的对象被引入,一些字段被弃用,而最新文档并不会告诉我们它的演化过程。此外,用例与 API 文档之间也存在一些小的差距,例如 Price 取代了 Plan,或者在 Phase Items 中弃用的 coupon 字段。

在本文中,我将分享一个我实际使用的案例——在未来的某个时间安排更新单个订阅项目的数量,同时保持原有优惠券的有效期。我还将讨论在过程中遇到的挑战和解决方案。

场景设定

我们当前有一个订阅,其中包含一个订阅项目 – SaaS 会员费用,单价为 $10,数量为 5;同时它附带一个三个月的免费优惠券。我们需要在 9 月 1 日将其数量更新为 10,具体效果如下:

Subscription Schedule

如果您想直接查看代码,可以跳到解决方案这一节。

Stripe 的实现方式

尽管有 API 文档 和 用例,但第一次对接这个 API 时仍然会感到困惑。我知道需要创建一个订阅计划,但参数如何传递仍然不太清楚。

针对创建计划的 API POST /v1/subscription_schedules,文档中给出的用例如下:

ruby
Stripe::SubscriptionSchedule.create({
customer: ‘cus_NcI8FsMbh0OeFs’,
start_date: 1680716828,
end_behavior: ‘release’,
phases: [
{
items: [
{
price: ‘price_1Mr3YcLkdIwHu7ixYCFhXHNb’,
quantity: 1,
},
],
iterations: 12,
},
],
})

但我觉得这并不适用于我的情况,因为我需要修改一个已经存在的订阅,至少应该传递订阅 ID。查阅文档后发现有一个参数叫 from_subscription,它接受一个现有的订阅 ID,文档中写道:

当使用此参数时,其他参数(如阶段值)不能设置。要创建一个带有其他修改的订阅计划,我们建议进行两次单独的 API 调用。

这意味着如果我设置了 from_subscription,那么其他参数就不能设置了,而是需要进行两次 API 调用。那么该怎么办呢?

我们知道手动在 Stripe 中也可以进行计划更新,这个操作也是调用 Stripe 自己的 API。让我打开 Chrome 的开发者工具,看看 Stripe 是如何进行的。Stripe 进行了两次 API 调用(省略其他参数):

第一次 API 请求:

http
POST /v1/subscription_schedules

Request Body:
{
from_subscription: “sub_1PeCNNLiQSlGYPd5Wpmvb1n6”
}

Response:
{
id: “sub_sched_1PeH6LLiQSlGYPd599aOVdoW”,
# … other values omitted
}

第二次 API 请求:

http
POST /v1/subscription_schedules/sub_sched_1PeH6LLiQSlGYPd599aOVdoW

Request Body:
{
proration_behavior: “none”,
phases: [
{
start_date: 1721378477,
end_date: “now”,
iterations: “”,
discounts: [
{
coupon: “EDHtWtdk”
}
],
default_tax_rates: “”,
automatic_tax: {
enabled: false
},
items: [
{
price: “price_1PeCLRLiQSlGYPd50cFz00Na”,
quantity: 5
}
],
collection_method: “charge_automatically”
},
{
start_date: “now”,
end_date: 1725217200,
discounts: [
{
coupon: “EDHtWtdk”
}
],
default_tax_rates: “”,
items: [
{
quantity: 5,
tax_rates: “”,
price: “price_1PeCLRLiQSlGYPd50cFz00Na”
}
]
},
{
start_date: 1725217200,
discounts: [
{
coupon: “EDHtWtdk”
}
],
default_tax_rates: “”,
items: [
{
quantity: 10,
tax_rates: “”,
price: “price_1PeCLRLiQSlGYPd50cFz00Na”
}
],
proration_behavior: “none”,
collection_method: “charge_automatically”,
invoice_settings: {
description: “Thank you for your business!”
}
}
],
end_behavior: “release”
}

第一个 API 请求是创建一个订阅计划,只传递 from_subscription 这个参数;在获取创建出的计划 ID 后,第二个 API 对该计划进行修改,并传入了一系列参数。这其中涉及了许多概念,比如阶段(Phase)、按比例调整(Proration),我们来具体看看它们的含义。

什么是阶段(Phase)

阶段代表订阅计划中的一段特定时间,在这段时间内会应用我们所指定的价格、费用、税等参数。也就是说,我们可以指定一组阶段,每个阶段有自己的计费参数。一个阶段具有以下关键参数:

  1. start_dateend_date:标识每个阶段的开始和结束日期,一个计划可以有多个阶段,但它们的 start_dateend_date 必须保持首尾相连。
  2. pricequantity:我们可以在每个阶段为订阅项目指定不同的价格和数量,从而实现定期更改数量的目标。
  3. discounts:在该阶段使用的特定折扣和优惠券。
  4. tax_ratesautomatic_tax:税率相关参数。
  5. collection_method 等。

从上述的第二个请求来看,Stripe 设定了 3 个阶段:

  1. 阶段#0start_date 为原先订阅的起始时间,end_datenow,同时 discounts 包含了原先的优惠券代码,items 中有一个对象,其中有 pricequantity 参数,值与当前订阅一致。这个阶段的意义在于立即终止当前的订阅。
  2. 阶段#1start_datenowend_date 是我们预期的时间,items 和阶段#0保持一致。
  3. 阶段#2start_date 是我们预期的时间,end_date 为空,items[0] 中的 quantity 更新为 10。

并不是很明白为什么要用 3 个阶段,2 个行不行?其实两个也是可以的。

什么是按比例调整(Proration)和按比例调整行为(proration_behavior)

当订阅的计划发生变化时,比如我们在这里对数量进行了修改,这个修改可能发生在一个账单周期之中,这时就可能需要按比例调整账单金额,这就是按比例调整(proration)。

Stripe 中的按比例调整

我们对订阅进行更改时,默认会自动处理按比例折算,即按比例调整会发生。我们可以使用 proration_behavior 来指定它的行为,这个参数有以下三个值:

  • create_prorations:默认值,在计费周期发生变化时创建按比例调整。
  • none:计费周期发生变化时,不进行按比例调整。
  • always_invoice:立即创建按比例调整账单。

举个例子来说明一下。假设客户订购了一项每月 100 美元的基础计划服务,预收费,计费周期从每月 1 日开始。而在当月 15 日,客户决定升级到高级计划,每月费用为 200 美元。

如果选择 create_prorations,那么会有如下计算:

  • 初始订购费用 100(本月 1 号收费)
  • 15 日升级,则本月剩余天数:16 天
  • 高级计划每日费用:200 / 30 ≈ 6.67 / day
  • 基本计划每日费用:100 / 30 ≈ 3.33 / day
  • 按比例折算剩余 16 天差价:(6.67 – 3.33) * 16 = 53.44
  • 即下个月 1 号需要付的账单为:200(高级计划费用)+ 53.44 = 253.44

如果选择 none,则是:

  • 初始订购费用:100(本月 1 号收费)
  • 15 日升级,不进行按比例调整
  • 下一张账单(下月 1 日)仅需要支付高级计划费用 200。

什么是结束行为(end_behavior)

end_behavior 指的是计划结束(即到达最后一个阶段的 end_date)时,相关联的订阅应该进行什么操作。end_behavior 的枚举值是 releasecancel,它们的区别在于:

  • release:默认值,当计划结束时,订阅保持那一刻的状态继续存续。
  • cancel:当计划结束时,订阅直接结束(取消)。

同时,订阅计划也提供两个对应的 API,其含义是一致的:

  • POST /v1/subscription_schedules/:id/cancel
  • POST /v1/subscription_schedules/:id/release

这一点十分重要,因为 Stripe 只允许一个订阅最多有一个激活状态的计划。如果在调试代码时,创建计划遇到错误 “You cannot migrate a subscription that is already attached to a schedule”,这意味着当前的订阅已经有一个计划。对于这个计划,我们要么更新,要么释放/取消。如果此时选择取消,您会发现与之关联的订阅直接消失了。这在业务和调试代码方面都是不妙的,需要避免。

为什么项目中是价格(price)而不是计划(plan)

如果仔细观察创建订阅计划返回的结果,我们会注意到项目是这样的一个呈现方式:

ruby
{
:billing_thresholds => false,
:discounts => [],
:metadata => {},
:plan => “price_1PU2ARLiQSlGYPd5Agu2y15G”,
:price => “price_1PU2ARLiQSlGYPd5Agu2y15G”,
:quantity => 5,
:tax_rates => []
}

看到这里不免会有疑惑,为什么会同时有 priceplan 两个字段,而且它们的值都是一样的 price_xxx

Plan 是早期版本的 Stripe 订阅系统中的核心概念,用于定义订阅的基本参数;而 Price 是 Stripe 新版中的改进概念,提供了更灵活和细粒度的控制,用于替代 Plan。因此,在使用订阅计划功能时,我们更倾向于使用价格 Price 而不是计划 Plan

解决方案

给定一个 subscription,想要在 time 修改其项目的 quantity。我们只使用 2 个阶段,代码如下:

ruby
schedule = Stripe::SubscriptionSchedule.create from_subscription: subscription.id

phase0 = schedule.phases[0]
phase0_item = phase0.items[0]

original_quantity = phase0_item.quantity
price_id = phase0_item.price
discounts = phase0.discounts.collect(&:to_hash)

phases = [
# 阶段 0 – 修改现有订阅阶段,结束日期为 time
{
start_date: phase0.start_date,
end_date: time.to_i,
proration_behavior: ‘none’,
discounts: discounts,
items: [
{
price: price_id,
quantity: original_quantity,
}
]
},
# 阶段 1 – 设置一个新的订阅阶段,开始日期为 time,数量为 quantity
{
start_date: time.to_i,
end_date: time.to_i + 60,
proration_behavior: ‘none’,
discounts: discounts,
items: [
{
price: price_id,
quantity: quantity,
}
]
}
]

Stripe::SubscriptionSchedule.update schedule.id, phases: phases, proration_behavior: ‘none’

解释一下之前未覆盖到的部分:

更新订阅计划时,如果没有明确传递 discounts 参数,现有优惠券将被取消。要保留任何当前折扣或优惠券,需要将它们包含在更新的阶段中。

项目对象中的 coupon 参数已被弃用,取而代之的是在外层传入 discounts 数组。

最好为最后一个阶段设置一个结束日期,以确保计划自动释放。这种做法可以避免触及只能有一个激活计划的数量限制。

结语

利用 Stripe 的订阅计划功能可以大大简化订阅管理过程。尽管最初可能会遇到一些困惑,但通过仔细阅读文档和实践,我们可以充分发挥这个强大工具的潜力。希望本文对大家理解和使用订阅计划有所帮助。如果有任何问题或需要进一步的解释,请随时联系我。

👉 野卡 | 一分钟注册,轻松订阅海外线上服务

(0)
上一篇 2025年6月7日
下一篇 2025年6月7日

相关推荐