7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

朝日新聞社Advent Calendar 2022

Day 1

[Stripe] Webhookを利用して、自動生成されるサブスク請求書を自動整形してみた

Last updated at Posted at 2022-11-30

この記事は朝日新聞社 Advent Calendar 2022の第1日目の記事です。
これから毎日、朝日新聞社のエンジニアがバラエティに富んだテック記事をお届けします!

一部記事はnoteで運営中のテックブログ記事となります。
合わせてこちらもフォローいただけると幸いです。

初日はメディア研究開発センターの倉井が、Stripeのサブスク請求書を自動で整形する方法についてお届けします!


みなさん、Stripe使っていますか?

Auth0、CircleCIに並び、サブスクリプションビジネス開発における「3種の神器」と呼ばれることもあるStripeですが、実際に使ったことのあるという人はあまり多くないのかな、という印象があります。
私個人は、仕事でも個人開発でもStripeにお世話になっており、日々その使いやすさを実感してます。

例えば顧客にサブスクリプションを紐付けると、毎月の契約更新日に先月分の使用量から金額を自動的に算出して請求書を発行してくれるので、煩雑な事務作業から解放されます。
ただし自動生成がゆえに、細かな項目の文言は基本的に固定で発行されてしまいます。
例えば「xx月分請求書」という注釈を入れたい場合、請求書が自動作成されてから1時間以内にWebコンソール上でその変更を入力しなければなりません。(1時間後に自動で本発行されてしまうため)

今回は、それを自動で修正するフローを構築することがあったので、その知見を共有したいと思います。

Stripeのサブスクリプションの仕組み

Stripeでは1ヶ月もしくは1年ごとに料金を回収するサブスクリプションを作成することができます。
サブスクリプションの作成から、請求書が作成されるまでのフローは大まかに以下のようなイメージです。

  1. サブスクリプションプランと顧客を定義する
  2. 顧客にサブスクリプションを紐付ける、このとき次回の課金サイクルの開始日や請求書のメール送付の要否、支払い方法などを設定する
  3. 次回の課金サイクルの開始日になった時、従量課金の場合その日までの利用量から料金を算出して請求する。請求書メールを送付するようにした場合、サイクルの更新のタイミングで請求書が仮に発行され、1時間後に顧客に請求書メールが送信(本発行)される。この後、請求書の内容を修正することはできない

ここで先述したように請求書に細かな修正を加えたい場合は、仮発行からの1時間の間に修正を行わなければなりません。
せっかく請求を自動化できたのに、その修正をしたい場合は待機&手作業が発生するなんて残念...と思う必要はありません!

この作業を自動化できるように、Stripeではいくつかのイベントが起きた時に通知を送ってくれるWebhookが用意されています。

StripeのWebhook

StripeではWebhookエンドポイントを定義することで、いくつかのイベントに対して通知を受け取る機能が用意されています。
Stripeの公式Docsからその概要文を引用します。

イベントの概要
Stripe は、お客様のアカウントで何らかの変化が発生すると、イベントを使用してお知らせします。イベントが発生すると、Stripe は新しい Event オブジェクトを作成します。そのイベントを受信するために Webhook エンドポイントが登録されている場合は、POST リクエストの一部としてエンドポイントに送信します。

イベントの種類はこちらで列挙されています。

何らかの自動化したい作業があった時には次のような流れでWebhookエンドポイントを構築すればOKです。

  1. 自動化したい作業に関係するイベントが必ずStripe上で発生している(Eventオブジェクトが生成される)はずなので、そのイベントの種類を調べる
  2. そのEventオブジェクトの中身をもとに何らかの処理を行うWebhookエンドポイントを用意する

ここまではStripeの仕組みを紹介してきましたが、ここからはエンドポイントの作り方とそれをStripeに連携させる方法をお伝えしたいと思います。

エンドポイントをAWS Chaliceでデプロイする

エンドポイントはどういう形で用意していただいても良いのですが、サーバレスな形式かつPythonで記述したかったので、今回はAWS Chaliceを採用してみることにしました。

Chaliceの説明はclassmethodさんの記事が詳しいので、そちらの説明を引用させていただきます。

フレームワークの設計が秀逸でAPI Gatewayなどのインフラのことをあまり意識せず、Flaskでサーバーを書いているかのように開発が可能です。

具体的にはAWS Lambdaを中心としたアプリケーションの開発を支援するようなフレームワークで、Pythonで書かれた関数をデコレータでラップすると、それをLambda関数としてデプロイしてくれます。

さらに便利なのが、API Gatewayとの連携やS3やSQSなどのイベントからのトリガーなど、AWS Lambdaを使用するいくつかのユースケースでの設定を自動でやってくれます。

デコレータでこれらの設定をかけるので、まるでFlaskでエンドポイントを書くかのようにAPI Gatewayなどの設定が可能です。

PythonのコードをLambdaにデプロイしてくれるだけでなく、API Gatewayと結びつけるところまでやってくれるので、簡単にエンドポイントを作成することができます。

今回の記事はAWS Chaliceでのデプロイ方法がメインテーマではないので、詳しい使い方は省略して実際のコード内容の説明に移ります。
コード自体はこちらの記事を参考にして作りました。

さて、今回の目的を思い出すと「自動生成される請求書を手直しする」ことが目標でした。
Stripeの請求書にはdescriptionというプロパティがあり、こちらにその請求書の概要を記すことができるのですが、ここに「xx月分請求書」という文字列を表示させることを今回のゴールとしましょう。

それを実現するためのコードが以下になります。

import datetime
import os
import stripe
from chalice import Chalice
from chalice import BadRequestError, UnauthorizedError


# 環境変数
STRIPE_API_KEY = os.environ.get("STRIPE_API_KEY")
STRIPE_ENDPOINT_SECRET_INVOICE = os.environ.get("STRIPE_ENDPOINT_SECRET")


app = Chalice(app_name='stripe-webhook-lambda')


@app.route("/webhook_handler", methods=["POST"])
def webhook_handler():
    event = None
    # リクエストパラメータの解析
    payload = app.current_request.raw_body
    sig_header = app.current_request.headers["stripe-signature"]

    # イベントの解析
    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, STRIPE_ENDPOINT_SECRET
        )
    except ValueError:
        raise BadRequestError("")
    except stripe.error.SignatureVerificationError:
        raise UnauthorizedError("")

    # 請求期間の終わりの瞬間のタイムスタンプ
    period_end = event["data"]["object"]["period_end"]
    # 請求書のID
    id = event["data"]["object"]["id"]

    dt = datetime.datetime.fromtimestamp(int(period_end))

    # 今回は日本時間の1日 00:00に契約が更新されるとしてその前日の年月を計算
    before_dt = dt - datetime.timedelta(days=1)
    before_year = before_dt.strftime('%Y')
    before_month = before_dt.strftime('%m')

    stripe.api_key = STRIPE_API_KEY
    stripe.Invoice.modify(
        id,
        # 自動で規定時間後に請求書が本発行されるのをやめる
        auto_advance=False,
        # descriptionの上書き(今回の目的)
        description=f"{before_year}{before_month}月分 API利用料金",
    )

    return {
        "statusCode": 200,
        "body": "invoice updated successfully!"
    }

いくつか要点を説明します。

# イベントの解析というところで、送られてきた内容が本当にStripeからのものなのかの検証を行なっています。
事前に取り決めた署名シークレットを元に計算されるstripe-signatureが適切な値になっているかを比較しています。

先月の年月の判定方法ですが、今回は契約が日本時間の1日 00:00に更新される想定なので、プログラムが動いている瞬間の前日の年月を計算することで求められます。

最後に請求書の更新指示についてですが、descriptionの更新とともにauto_advance=Falseとするように指示しています。
先述の通り、デフォルトでは請求書の更新から1時間後に本発行としてユーザーに送信されてしまうのですが、これを無効にしています。
こうすることで、更新された請求書の形式が本当に意図したものなのか、人間が時間に迫られることなくチェックすることができるようになります。

上のコードをChaliceを利用してデプロイすると、エンドポイントがコマンドに表示されます。
(デプロイの仕方は先ほど紹介したこちらの記事が参考になります。)
次のステップでこのエンドポイントをStripeと連携させていきましょう。

StripeのWebhookとデプロイしたエンドポイントを連携させる

ここまできたらあとは連携作業だけです。
連携作業はWebのコンソールから行っていきます。

Stripeコンソールにログインした後に、右上に開発者という表示があるのでそれを押します。
その後下の画像のように左の方にWebhookという表示が見えるのでそれを押します。
すると画像のような表示がされると思います。
ここで+ エンドポイントを追加を押します。
スクリーンショット 2022-11-24 16.44.50.png
その後下の画像のような画面が表示されると思うので、作成したエンドポイントURLを入力し、リッスンするイベントにinvoice.createdを選択したのち、イベントを追加を押します。
スクリーンショット 2022-11-24 17.01.40.png
すると作成されたWebhookの詳細画面に飛ぶので、その画面で署名シークレットをコピーし、Chaliceでconfig.jsonに環境変数(上のコードでいうところの、STRIPE_ENDPOINT_SECRET)として登録し再度デプロイを行えば、すべての準備が完了です。
これで自動生成されるサブスク請求書を自動整形するエンドポイントの完成です!

Stripeを是非使ってみてください!

今回は触れませんでしたが、Webhookのイベントをローカルで擬似的に発火させて、処理が正しく行われるかテストすることも可能です。
このようにさまざまな機能が盛りだくさんなStripeですが、実は社内でも利用事例はほとんどなく、色々と手探りで実装していました。

しかし便利なAPIやWebhook、しっかりとしたDocs(英語)など開発者にとってありがたい、そして使いやすい機能がたくさん用意されていますので、ぜひオンライン決済サービスの手段として検討してみてください!
(手数料もそこまで高くないですよー)


朝日新聞社では、技術職の中途採用を強化しています。
ご興味のある方は下記リンクから希望職種の募集ページに進んでください。
皆様からのご応募、お待ちしております!

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?