stripe 升级循环订单的坏账问题

前言

我们项目有提供一个基础的 vip 服务,但是在这个基础的 vip 服务上,我们还提供了一些增值服务。因为我们的支付都是走循环订单的。无论是用户买基础的 vip 服务,还是直接在基于 vip 服务的含有增值项的服务,都是走循环订单。
但是还有一种情况,就是如果有一个用户刚开始买了一个月的基础的 vip 服务,但是用了半个月之后,想把另外半个月的时间换成含有增值项的 vip 服务,因为后者的价钱更贵,并且为了保证用户的循环付款周期不改变,我们没办法先将基础的 vip 服务的循环取消掉,再重新订阅一个新的循环,因为这样子循环的付款时间就会变。
所以为了保证循环付款周期不变,我们会算接下来这半个月时间,用户需要付新服务的差价,然后在下一个循环周期到来的时候,就换成新的循环订单, 在 stripe 支付上,我们考虑的是直接用 stripe 的循环订单升降级机制。

升降级订单

stripe 的升降级订单机制,可以完美的解决这个问题: 文档

订阅可以通过切换客户订阅的计划或通过更改计划的数量来更改。通常,结果符合升级或降级订阅,具体取决于两个订阅之间的价格差异。

假设客户当前每月订购10美元的标准月度计划,以下代码将客户切换为月薪30美元的专业月度计划:

\Stripe\Stripe::setApiKey("sk_test_BQokikJOvBiI2HlWgH4olfQ2");
$subscription = \Stripe\Subscription::retrieve('sub_49ty4767H20z6a');\Stripe\Subscription::update('sub_49ty4767H20z6a', [
    'items' => [
        [
            'id' => $subscription->items->data[0]->id,
            'plan' => 'plan_CBb6IXqvTLXp3f',
        ],
    ],]);

如果两个计划都具有相同的结算周期(即间隔和间隔计数的组合),则订阅会保留相同的结算日期。如果计划有不同的结算周期,则新计划将在变更当天开始以新的时间间隔结算。例如,将客户从一个月计划转换到另一个月计划不会更改结算日期。但是,将客户从月度计划转换为年度计划会将开票日期转换为交换日期。在推出试用期的同时,将客户从一个月度计划切换到另一个月度计划也会移动开票日期(到试验结束时)。


也就是说,如果循环周期是一样的话,比如都是一个月,那么结算时间是不变的, 只有当结算周期不一样的时候,比如一个月升级为一年,才会变。
简单的来说,就是如果有一个用户是在第一天(4月1号)付款的,并且在这个循环周期的中途,也就是4月15号升级, 升级的订单是 $30一个月,旧订单是 $10 一个月,那么补这半个月的差价大概就是 15 - 5 = 10, 也就是要补$10, 但是这个10块钱不是马上就收的,而是下一个循环的周期一起收,也就是下个月的一号,也就是 5月1号的时候,这个时候,应该扣款是 30 + 10 = 40, 总共是 40 块钱
如果是换成一个月的循环如果要升级成一年的循环的话,那么情况又是不一样的(包括计算方式),加上一个用户还是一样,一个月用了一半,然后升级成一年的,这时候旧的一个月是$30, 新的一年的循环是 $300, 这时候就要补差价 $300 - $15 = $285 元,并且是当天就生效的,不会等到下个月
而如果是新的plan比旧的plan更便宜的时候,其实就是降级操作,假设新的循环是 $10 一个月, 旧的是 $30 一个月, 还是一样月中才降级,这时候逻辑就是差价就是 5 - 15 = -10, 也就是说我们需要给用户返还 10元, 当然这部分钱不是直接打回用户账号,而是存起来,用于用户接下来的付款,也就是下个月(5月1号)付款的时候,本来要付10元, 所以就变成 10 - 10 = 0, 其实就不用付钱了,下个月的循环付款直接就是 0 元, 反正只要这个存起来的钱没有用完的话,下次付款的时候就会优先扣这里面的钱,只有这里面扣完的话,才会真正扣用户的钱。

以上就是补差价的方式,也就是说,如果不想补差价的话,即我升级上来之后,下次就要扣全款,那么在创建plan的时候,就要把 prorate 参数设置为 false

1
2
3
4
5
6
7
8
9
10
\Stripe\Stripe::setApiKey("sk_test_BQokikJOvBiI2HlWgxxxxx");

$subscription = \Stripe\Subscription::retrieve('sub_49ty4767H20zxx');\Stripe\Subscription::update('sub_49ty4767H20zxx', [
'items' => [
[
'id' => $subscription->items->data[0]->id,
'plan' => 'plan_CBb6IXqvTLXp3f',
],
],
'prorate' => false,]);

这样只要是换了新循环的话,就会直接扣全款。
我们后面采取的是升降级订单补差价的方式。而且代码也很好写,而且循环的单号根本不用变。

为什么会产生坏账

当我们用这一套上线了之后,刚开始数据没什么异常,反正如果要升级到含有增值项的服务的时候,下个月差价就会连同新循环一起要扣的钱,一起汇到我们的账号来。但是后面对了一下账单,发现好多这种差价后面都没有汇过来,为什么呢?? 因为用户把循环取消掉了,不继续用我们的服务了,所以新循环不扣钱也就算了,但是连同上一个循环累计的差价也黄了。这就是坏账的产生。 因为用户一旦取消掉循环,那么这个循环的所有的扣款计划就失效了。

解决坏账的方法

当我们意识到通过升降级循环订单会有坏账的时候,我们第一时间就是赶紧止损,所以就换了另一种方式来处理。那就是差价我当下就收了,然后再换循环订单。这样子就算用户取消掉循环,但是他当前享受的服务的钱我们也已经收到了。
那么我们是怎么在用户的一次支付行为中,又让用户付差价,又让用户换订单呢??

使用customer的方式同时进行多次付款

我们知道 stripe 一个 token 只能支付一次,如果一个token想要支付两次的话,那么就会报这个错误:

1
You cannot use a Stripe token more than once: tok_1C5Q9nAP4l9EoBkACxxauZlk. [] []

但是如果是用创建的 customer 来支付的话,就可以在程序里面同时支付多次。所以我们的解决方式,就是通过前端传的这个 token,直接创建一个customer,然后用这个 customer 来进行两次支付,一次是普通支付,一次是循环支付:

1
2
3
4
$customer = Customer::create([
"description" => $this->accountId,
"source" => $token,
]);

这样就可以了。