Stripe大冒险:利用 Subscription Schedule 更新订阅
Utilizing Subscription Schedules in Stripe: A Journey of Discovery
使用 Stripe API 更新 Subscription 算是一个常规操作,其 API 使用也不困难;但有时由于账单日期或产品订阅类型的原因,我们无法立即更新一个Subscription,这就需要利用 Subscription Schedule 的功能来在未来的某个时间创建或更新订阅。
Subscription Schedule是一个非常强大的功能。然而,就我个人的开发体验而言,最初我在使用这个功能时遇到很多迷惑之处。虽然 Stripe 提供了详细的 API 文档 和 Use Cases 来描述Subscription Schedule,但由于 Stripe 的 API 不断发展,一些新的对象被引入,一些字段会被弃用,而最新文档并不会告诉我们它的演化过程。另外,Use Case 与 API 文档存在一些小的差距,比如 Price
取代了 Plan
,或者在 Phase Items 中弃用的 coupon
字段。
在本文中,我将分享一个我自己实际使用的案例——在未来的某个时间安排更新单个 Subscription Item 的 quantity,同时保持本身 coupon 的 duration。我还将讨论我在过程中遇到的挑战和解决方案。
场景设定
我们当前这样一个 Subscription,里面有一个 Subscription Item - SaaS Member Fee,单价为 $10,数量为5;同时它附带一个三个月的免费 coupon。我们需要在9月1日将其数量更新为10,即实现如下的效果:
TL; DR 可以直接跳到 解决方案 这一节看代码。
Stripe 自己是怎么做的
纵使有 API 文档 和 Use Cases,第一次对接这个 API 时还是有些困惑。我知道我需要创建一个 Subscription Schedule,可是参数怎么传还是比较疑惑。
针对创建 Schedule 的API POST /v1/subscription_schedules
,文档中给出的用例是这样的:
Stripe::SubscriptionSchedule.create({
customer: 'cus_NcI8FsMbh0OeFs',
start_date: 1680716828,
end_behavior: 'release',
phases: [
{
items: [
{
price: 'price_1Mr3YcLkdIwHu7ixYCFhXHNb',
quantity: 1,
},
],
iterations: 12,
},
],
})
但是,总觉并不适用我的 case ,因为我需要修改一个已经存在的 Subscription,那么我至少应该把 Subscription ID 传过去吧。读了下文档,有一个参数叫 from_subscription
,其接受一个 existing subscription ID,文档中写着:
When using this parameter, other parameters (such as phase values) cannot be set. To create a subscription schedule with other modifications, we recommend making two separate API calls.
这意思是说,如果我设置了 from_subscription
那么其他的参数就不能设置了,而是要进行两次 API 调用,那该怎么办?
我们也知道手动在 Stripe 里也是可以进行 Schedule Update,这个操作也是调用的 Stripe 自己的 API。让我打开 Chrome 的 dev tool,看看 Stripe 自己是怎么进行的。Stripe 进行了两次 API 调用(省略其他参数):
第一次 API 请求:
POST /v1/subscription_schedules
Request Body:
{
from_subscription: "sub_1PeCNNLiQSlGYPd5Wpmvb1n6"
}
Response:
{
id: "sub_sched_1PeH6LLiQSlGYPd599aOVdoW",
# ... other values omitted
}
第二次 API 请求:
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 请求是创建一个 Subscription Schedule,只传递 from_subscription 这个参数;在拿到创建出的 Schedule ID之后,第二个 API 对这个 Schedule 进行修改,并且传入了一大批参数。这其中涉及了很多概念,比如 Phase,Proration,我们来具体看看他们的含义。
什么是 Phase
Phase 代表 Subscription Schedule 中的一段特定时间,在这段时间内会应用我们所指定的 Price、Charge、Tax等参数。也就是说,我们可以指定一组phases,每个phase有自己的计费参参数。一个 Phase 具有以下关键参数:
- start_date 和 end_date:标识每个 phase 的开始日期和结束日期,一个 schedule 可以有多个 phase,但是它们的 start_date 和 end_date 要能保持首尾相连
- price 和 quantity:我们可以在每个阶段为订阅项目指定不同的价格和数量,这样就可以实现这个场景中定期更改 quantity 的目标
- discounts:在这个phase使用的特定的折扣和优惠券
- tax_rates 和 automatic_tax:税率相关参数
- collection_method 等
从上述的第二个请求来看,Stripe 设定了3个Phase:
- Phase#0,start_date 为原先subscription的起始时间,end_date是
now
,同时 discounts 包含了原先的 coupon 代码,以及 items 中有一个对象,其中有 price 和 quantity 参数,值和当前 subscription 一致。这个phase的意义在于立即终止当前的订阅。 - Phase#1,start_date 为
now
,end_date 是我们预期的时间,items 和 Phase#0保持一致; - Phase#2,start_date 是我们预期的时间,end_date为空,items[0] 中的 quantity 更新为10。
不是很明白为什么要用3个Phases,2个行不行?其实两个也是可以的。
什么是 proration 和 proration_behavior
当订阅的计划发生变化,比如我们这里对 quantity 进行了修改,这个修改可能是发生在一个账单周期之中,那就可能需要按照比例来调整账单的金额,也是 proration。
Stripe 中的 proration
我们对 Subscription 进行更改时,默认会自动处理按比例折算,即 proration 会发生。我们可以使用 proration_behavior 来指定它的行为,这个参数有以下三个值
create_prorations
: 默认值,在计费周期发生变化时创建按比例调整none
: 计费周期发生变化时,不进行 proration。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 日升级,不进行 proration
- 下一张账单(下月 1 日)仅需要支付高级计划费用 200
什么是 end_behavior
end_behavior 指的是 Schedule 结束(即到达最后一个phase的end_date)时,相关联的 Subscription 应该进行什么操作。end_behavior
的枚举值是 release
或 cancel
,它们的区别在于:
release
:默认值,当 Schedule 结束时,Subscription 保持那一刻的状态继续存续cancel
:当 Schedule 结束时,Subscription 直接结束(取消)
同时,Subscription Schedule 也提供2个对应的 API,其含义是一致的:
POST /v1/subscription_schedules/:id/cancel
POST /v1/subscription_schedules/:id/release
这一点十分重要,因为 Stripe 只允许一个 Subscription 最多有一个激活状态的 Schedule。如果你在调试代码时,创建 schedule遇到了这个错误 “You cannot migrate a subscription that is already attached to a schedule”,这意味着当前的 Subscription 已经有一个 Schedule。对于这个 Schedule,我们要么 update,要么 release/cancel。如果此时选择 cancel,你会发现与之关联的 Subscription 直接消失了。不过是从业务方面还是从调试代码的方面来看,这都很不妙,需要避免。
为什么 item 里是 price 而不是 plan
如果仔细观察创建 SubscriptionSchedule 返回的结果,我们会注意到 item 是这样的一个呈现方式:
{
:billing_thresholds => false,
:discounts => [],
:metadata => {},
:plan => "price_1PU2ARLiQSlGYPd5Agu2y15G",
:price => "price_1PU2ARLiQSlGYPd5Agu2y15G",
:quantity => 5,
:tax_rates => []
}
看到这里不免会有疑惑,为什么会同时有 price
和 plan
两个字段,而且他们的值都是一样的 price_xxx
?
Plan 是早期版本的 Stripe Subscription系统中的核心概念,它用于定义Subscription的基本参数;而 Price 是 Stripe 新版中的改进概念,提供了更灵活和细粒度的控制,用于替代Plan。所以,在使用 Subscription Schedule功能时,我们更倾向于使用价格 Price 而不是计划 Plan。
解决方案
给定一个 subscription
,想要在 time
修改其 item 的 quantity
。我们只使用2个phase,代码如下:
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 = [
# Phase 0 - 修改现有订阅阶段,结束日期为 `time`
{
start_date: phase0.start_date,
end_date: time.to_i,
proration_behavior: 'none',
discounts: discounts,
items: [
{
price: price_id,
quantity: original_quantity,
}
]
},
# Phase 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'
解释一下之前没覆盖到的部分:
- 更新SubscriptionSchedule时,如果没有明确传递
discounts
参数,现有 coupon 将被取消。要保留任何当前discount 或 coupon,需要将它们包含在更新的phase中。 - Item 对象中的
coupon
参数已被弃用,取而代之的是在外层传入 discounts 数组。 - 最好为最后一个阶段设置一个结束日期,以确保Schedule自动release。这种做法可以避免触及只能有一个active Schedule的数量限制
结语
利用 Stripe 的 Subscription Schedule 功能可以大大简化订阅管理过程。虽然最初可能会遇到一些困惑,但通过仔细阅读文档和实践,我们可以充分发挥这个强大工具的潜力。希望本文对大家理解和使用 Subscription Schedule 有所帮助。如果有任何问题或需要进一步的解释,请随时联系我。