【Stripe】サブスクリプションの支払いタイミングが特定日時においてズレる問題について

f:id:kuracux:20200625113934p:plain スタメンでエンジニアをしている田中です。 今回は決済プラットフォームであるStripeのサブスクリプションを扱う際に遭遇した問題について、発生した事象とその原因、および対策方法についてご紹介します。

なお、本記事ではStripeのサブスクリプションについての詳細は説明いたしません。また、対策方法についてはRubyのコードで記載します。RubyでStripeのサブスクリプションを扱う場合については、以下の記事にて紹介しているのでよろしければご参照ください。

【Ruby on Rails】Stripeのサブスクリプションで試したことをまとめてみた

前提

  • 本記事で扱うサブスクリプションは請求期間が月次のものです
  • サブスクリプションの支払い日について、通常、翌月に同じ日が存在しない場合は自動的にその前の日を指定してくれます
    • 5/31 → 6/30
    • 8/31 → 9/30

参考 https://stripe.com/docs/billing/subscriptions/billing-cycle

発生した事象

以下の画像のように同じ日付でサブスクリプションを開始しましたが、2回目の支払いのタイミングがズレてしまうということがありました。そのため、ともに5月31日開始のサブスクリプションですが、前者については現在の期間の開始日が1日ズレてしまっています。

  • 2回目の支払いが7/1になっているケース
    f:id:kuracux:20200925172519p:plain f:id:kuracux:20200925172539p:plain

  • 2回目の支払いが6/30になっているケース
    f:id:kuracux:20200925172552p:plain f:id:kuracux:20200925172602p:plain

そのため、例えば支払い成功時のWebhookにて何かしらの処理をする場合に、このズレによって影響が発生する可能性が大いに考えられます。

発生原因

Stripeのサポートに問い合わせたところ、「billing_cycle_anchorとタイムゾーンの関係による可能性が高い」とのことでした。

ここで、billing_cycle_anchorについて説明します。billing_cycle_anchorとは支払い開始の起点となる日時のことです。たとえば、毎月1日に決済したい場合、サブスクリプション作成時にbilling_cycle_anchorに翌月の1日を指定することで、毎月1日払いを実現することが出来ます。特に指定をしなければ、サブスクリプション作成時刻 = billing_cycle_anchorとなります。

参考 https://stripe.com/docs/billing/subscriptions/billing-cycle

発生原因についての詳細は下記の通りです。

  • Stripeのシステムは、UTC基準で動作する
  • 日本時間(JST)でサブスクリプションを作成する場合に、UTCの時刻から9時間の差がある
  • そのため、UTC基準では月末だが、日本時間だと翌月と判定されてしまうため今回の問題が発生する

これだけだとよく分からないので、具体例を挙げて説明します。

具体例

(1)午前9時より前にサブスクリプションを作成した場合 ・日本時間「2020-05-31 08:00:00」にbilling_cycle_anchorを指定

支払回数 ダッシュボード上の挙動(JST) 実際の挙動(UTC)
1回目 2020-05-31 08:00:00 2020-05-30 23:00:00
2回目 2020-07-01 08:00:00 2020-06-30 23:00:00
3回目 2020-07-31 08:00:00 2020-07-30 23:00:00
4回目 2020-08-31 08:00:00 2020-08-30 23:00:00

(2)午前9時以降にサブスクリプションを作成した場合 ・日本時間「2020-05-31 10:00:00」にbilling_cycle_anchorを指定

支払回数 ダッシュボード上の挙動(JST) 実際の挙動(UTC)
1回目 2020-05-31 10:00:00 2020-05-31 01:00:00
2回目 2020-06-30 10:00:00 2020-06-30 01:00:00
3回目 2020-07-31 10:00:00 2020-07-31 01:00:00
4回目 2020-08-31 10:00:00 2020-08-31 01:00:00

どちらに関してもUTC基準だと翌月の支払いは6/30となっていますが、JSTに変換されると支払日に1日のズレが生じていることが分かります。これが今回発生した問題でした。

上記のことから、日本時間において以下の日時にサブスクリプションが作成されると今回の問題が発生すると考えられます。

  • 毎月29, 30, 31日
  • 午前0時から午前9時の間

たとえば、以下のようなケースです。

  • 12/29 午前2時にサブスクリプションを作成
    • 2月の支払い予定日は本来であれば2/28だが、3/1となる
  • 12/30 午前2時にサブスクリプションを作成
    • 2月の支払い予定日は本来であれば2/28だが、3/1となる
  • 12/31 午前2時にサブスクリプションを作成
    • 2月の支払い予定日は本来であれば2/28だが、3/1となる
    • 4月の支払い予定日は本来であれば4/30だが、5/1となる
    • 以降、31日がない月は1日のズレが発生する

対策方法

方針

今回の問題を解消するための方針として、以下の2つがあります。

  • 特定日時(毎月29, 30, 31日の0時から9時の間)でサブスクリプションを作成できないようにする
  • 特定日時でサブスクリプションを作成した場合、次回以降の支払い日時をずらす

前者に関しては、特定日時ではサブスクリプション契約させないという方法なので、あまり現実的な方法ではありません。そこで、後者に関して説明します。(Stripeのサポートの方にオススメいただいた方法です)

なお、設定にてUTC基準からJST基準に変更出来ないかと問い合わせをしましたが、そのような方法は存在しないため、現状は下記の方法で対応するしかなさそうです。

方法

次回以降の支払い日時を変更する方法としてtrial_endを使用します。 trial_endは本来であればトライアル期間を設定するために使用するパラメータですが、支払い日時を変更する用途でも使用できます。

参考: https://stripe.com/docs/api/subscriptions/update

今回は即時で初回決済する場合とトライアル期間を経て決済する場合の2種類について説明します。

即時決済

  1. サブスクリプションを作成する(Stripe::Subscription.create)
  2. サブスクリプションの開始日時を取得する(StripeのSubscription or Invoiceから取得する)
  3. 2で取得した日時が特定日時に該当する場合、サブスクリプションを以下のように更新する
# 次回の請求書作成日時 + n時間 = 特定日時を避けた時刻
next_payment_date = period_end + diff_hour.hours

# proration_behavior: 'none'で日割り計算を無効にする
Stripe::Subscription.update('該当するサブスクリプションID', { trial_end: next_payment_date.to_i, proration_behavior: 'none' })
注意点(即時決済のみ)
  • trial_endは本来トライアルを設定するために使用されるので、Stripeのダッシュボードのサブスクリプションのステータスが「トライアル」になります。
  • trial_endによるアップデートで、以下のイベントが発生します。ステータスがトライアルに変わり、そのタイミングで0円の請求書が作成されるためです。
    • invoice.finalized
    • invoice.paid
    • invoice.payment_succeeded

トライアル

  1. トライアルオプション付きでサブスクリプションを作成する(Stripe::Subscription.create)
  2. トライアルオプションで指定した日付が特定日時に該当する場合、即時決済と同様の方法で、サブスクリプションをアップデートする

おわりに

今回はStripeのサブスクリプションを扱う際に遭遇した問題についてご紹介しました。この問題を発見できたのは偶然で、発生条件もかなり限られており、最初は何が原因か分かりませんでしたが、Stripeのサポートの方に助けられつつ原因の特定と対応することが出来ました。Stripeをシステムに組み込む際の参考になればと思います。

最後に、株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひエンジニア採用サイトをご覧ください。