B2B向けのSaaSサービスでStripeのSubscriptionsを1年間やってみた

@iwasaki 98views 更新:2017年12月6日

この記事はStripe Advent Calendar 2017 – Adventar6日目の記事です。

StripeにはSubscriptionsという定期支払いのしくみがあります。

この機能をサービスに組み込んだものをリリースしてから今日でちょうど1年が経ちました。

https://www.aipo.com/information/update/2016/12_06_162359.html

1年間運用してみて得た知見をエンジニア視点からまとめてみました。

どんなサービスに組み込んでみたか

サービスのバックグラウンドは

https://www.wantedly.com/companies/towninc/post_articles/70973

Stripeのユーザーコミュニティで、Aipo.comに定期課金を導入した際の話をしてきました

に詳しく書いていますが、3行でまとめると

  • B2B向けSaaSサービス
  • 月額・年額の定期課金に対応
  • 銀行振込からクレジットカード決済に対応を拡大

というような感じになっています。

実装における具体的なポイントについてはSpeaker Deckの

https://speakerdeck.com/yoshiteruiwasaki/aipo-dot-comdefalsesubscriptionli-yong-falseshao-jie

Aipo.comでのSubscription利用の紹介

にもまとめていますが、

  • Webhookを使いましょう
  • Metadataを活用しましょう
  • リトライを設定しましょう

というのがポイントになってきます。

WebhookをJavaのDaemonで受け取る例

あんまりポエムっぽい内容ばかりになるのは本意では無いので、Webhookを受け取ってどう処理するかについてソースコードとその解説もしてみたいと思います。

Webhookを受け取るエンドポイントはAPI Gatewayを使っています。LambdaからSQSを発行し、それをEC2上のJavaのDaemonで受け取っている感じになっています。複雑な処理はJava側に寄せたいため、Lambdaの方のコードは100行ほどしかなく、SQSにeventIdを渡すという程度の処理しかしていません。

WebhookをAPI Gatewayで受け取って処理する箇所は

なんかも参考になるかと思います。

SQSからeventIdを受け取ったあとのJava側の処理を抜粋するとこんな感じになっています。

Event event = Event.retrieve(eventId);
String type = event.getType();
if ("customer.updated".equals(type)) {
  // 決済成功時に他の失敗している請求を再決済する
} else if ("invoice.payment_failed".equals(type)) {
  // 支払い失敗時にユーザーとサービス運営者へメールを送信する
  // ユーザーに表示する情報をMetadataに保存
} else if ("invoice.payment_succeeded".equals(type)) {
  // 支払い成功時にサービス運営者へメールを送信する
  // ユーザーに表示する情報をMetadataに保存
}

Webhookを受けっとってゴニョゴニョする場合は上記の3パターンを押さえておけばほぼOKではないかと思います。

customer.updated

決済成功時に他の失敗している請求を再決済しています。

決済に失敗していたものが、登録しているクレジットカードの変更などにより決済に成功するようになると customer.updated のイベントが発生します。メールアドレスの変更などでもこのイベントが発生するので、決済失敗→決済成功かどうかは delinquent 属性が変わっているかで判定をします。

  private boolean isResume(Customer customer,
      Map<String, Object> previousAttributes) {
    boolean isResume = false;
    if (customer != null
      && !previousAttributes.isEmpty()
      && previousAttributes.containsKey("delinquent")
      && "true".equals(previousAttributes.get("delinquent").toString())
      && !customer.getDelinquent()) {
      isResume = true;
    }
    return isResume;
  }

Stripeでは定期課金の決済に失敗しても次の課金オブジェクトを生成することができるようになっています。そのため決済が成功するようになったら他の失敗している請求を再決済するようにしています。

invoice.payment_failed

決済が失敗した際にメールを送信する処理を行います。

決済失敗時のユーザーへのメール送信はそのうちStripe側で対応されるようです。サービス運営者側にもメールを送信しておくことで、ユーザーへの連絡やユーザーから課金失敗理由の問い合わせがあった際の対応をしやすくしています。

Metadataに決済失敗の日時や理由を保存しています。

invoice.payment_succeeded

決済が成功した際にサービス運営者へメールを送信する処理を行います。

どのプランを何ユーザーで契約期間はいつか、といった内容を自分たちの見やすい形にしたメールで送信しています。

B2Bだと請求担当者が途中で変わることもあり、それぞれの請求とその時点の担当者がわかるようにしたいという要望があるためMetadataに請求時のユーザーのメールアドレスなどを保存しています。

バッチで行っている処理

Stripeの処理は基本的にユーザーによる画面操作とWebhookが起点になりますが、1箇所だけバッチによる定期実行処理もしています。

年額払いの場合の1ヶ月前の通知メールの送信

年額払いだと更新の1ヶ月前に「次回請求のお知らせ」みたいなメールが届くことが多いかと思いますが、それです。

これはWebhookでは実行できないのでバッチとして実行させています。

契約更新30日前になったらメールを送る場合の処理は以下のような感じになります。

  1. Plan一覧を取得
  2. 年額(year)のPlanのみピックアップ
  3. PlanにひもづくSubscription一覧を取得
  4. 契約終了まで30日を切ったSubscriptionのみピックアップ
  5. 次(upcoming)のInvoiceを取得
  6. Invoiceをもとにメールを送信

Calendar cal = Calendar.getInstance(); // 契約更新30日前 cal.add(Calendar.DATE, 30); Long time = cal.getTimeInMillis() / 1000L; Calendar today = Calendar.getInstance(); Long now = today.getTimeInMillis() / 1000L; Iterable<Plan> plans = Plan.list(); for (Plan plan : plans) { if ("year".equals(plan.getInterval()) { Iterable<Subscription> subscriptions = Subscription.list(plan); for (Subscription subscription : subscriptions) { if (subscription.getCurrentPeriodEnd() <= time && subscription.getCurrentPeriodEnd() > now) { Invoice invoice = Invoice.upcoming(subscription.getCustomer()); if (invoice != null) { // メールを送信する処理 } } } } }

定期課金を1年間やってみて

正直なにもやることなかった!

…ということに尽きると思います。

Stripeの設計が美しいため、Subscriptionの対応で必要になる機能を一度作ってしまえばその後の手離れはすさまじくよいです。

日々の運用ではCS(カスタマーサポート)のメンバーが入金関連の処理をしており、クレジットカード決済に失敗した際にはWebhookで飛ばしているメールをフックにStripeのダッシュボードから決済失敗理由の確認とユーザーへのリマインドを行う業務を行います。(ダッシュボードが日本語化されたのでさらに便利に!)

エンジニアへの依頼が発生するのは以下のようなケース。

決済成功メールの再送

決済成功メールがどこかにいってしまったので再送して欲しいというケースですね。これはStripeのダッシュボードから再送すればよいだけです。

返金処理

さまざまな理由により返金が発生するケースですが、こちらもStripeのダッシュボードから返金手続きの操作を行うだけです。

いずれの対応も1年間のうちで数える程度でもっとトラブルや手作業が発生するのではないかと思っていましたが、本当に何もすることがないため、運用ノウハウとかありません。

Stripeは導入が楽、というのがよく言われますが、運用はもっと楽です。

決済失敗の2大理由

せっかく1年運用したので決済失敗の2大理由を見てみましょう。なおこれは個人の主観です。

カード限度額オーバー

特に年額払いだと1回の支払い金額が大きくなるため限度額オーバーになりやすく決済に失敗することが多いようです。

カードの有効期限切れ

登録カードの自動更新が謳われていますが、日本のカード会社だとカードの登録し直しが必要となるケースも多いようです。

【告知】 JP_Stripes (Stripe ユーザーグループ) Tokyo Vol. 5のおしらせ

来週 14日(木) に東京でのユーザーグループの第5回イベントが開催されます。

http://eventregist.com/e/JP_Stripes_vol5

JP_Stripes (Stripe ユーザーグループ) Tokyo Vol. 5

もしこのエントリーを見てStripeへの興味が湧いたならぜひご参加ください!

僕は会社の忘年会の幹事をやっておりまして、その募集をする時に誰にも頼まれてないけど勝手に来年の目標を宣言しているんですが、去年は「来年は社外のイベントで話をする」ということを宣言していました。

その後そんな宣言をしたのはすっかり忘れていた(今年の忘年会の募集告知をする際に「去年はどんなこと書いてたかな」と思って見返してようやく思い出した)のですが、おかげさまで今年はStripeのイベントでLTをする機会に恵まれました。

そのようなご縁もあり、14日のイベントでは進行を務めさせていただくことになりました。

みなさまのご参加お待ちしています。

ログイン / 新規登録してコメントする

このソースコードをストックして後で利用したり、作業に利用したソースコードをまとめることができます。

こちらもお役に立つかもしれません