🦔

StripeのWebhookをAWS Lambda上のPythonで実装する

2022/06/01に公開

(2022年11月25日追記)
私の本が株式会社インプレス R&Dさんより出版されました。この記事の内容も含まれています。イラストは鍋料理さんの作品です。猫のモデルはなんとうちのコです!

https://www.amazon.co.jp/dp/B0BMPZW444/

感想を書いていただけるととても嬉しいです!

(2022年8月3日追記)この記事の内容はこちらの本でも読めます。

https://zenn.dev/sikkim/books/how_to_create_api_sales_service

サブスクリプションの解約機能を作りたい

先日はStripeでサブスクリプション契約を実装する記事[1]を書きました。今度は解約機能を実装します。前回実装した請求ポータルにはサブスクリプションを解約する機能が最初から用意されています。[2]また、StripeにはWebhook機能があります。多彩なイベントを取得できるので、サブスクリプションの解約イベントもWebhookで取得できます。

Webhookをローカルでテストする手順

StripeのWebhookがどのようなタイミングで発生しているのか理解するために、まずはローカルでサンプルコードを動かしてみます。

Stripe CLIの準備

ローカルでStripeのWebhookを受け取るにはStripe CLIをインストールする必要があります。筆者の環境はMacなのでHomebrewでインストールします。

brew install stripe/stripe-cli/stripe

CLIでログインします。

stripe login

ブラウザ側で許可すると90日間有効なキーが発行されます。ついでに入力補完も設定しておきましょう。

$ stripe completion
Detected `zsh`, generating zsh completion file: stripe-completion.zsh

Suggested next steps:
---------------------
1. Move `stripe-completion.zsh` to the correct location:
    mkdir -p ~/.stripe
    mv stripe-completion.zsh ~/.stripe

2. Add the following lines to your `.zshrc` enabling shell completion for Stripe:
    fpath=(~/.stripe $fpath)
    autoload -Uz compinit && compinit -i

3. Source your `.zshrc` or open a new terminal session:
    source ~/.zshrc

stripe completionを実行するとこのように表示され、stripe-completionファイルが生成されます。指示通りにディレクトリを作成してファイルを移動し、.zshrcを修正すればOKです。これでTABキーによる補完が効くようになります。

Stripe CLIについてはこちらの本で詳しく説明されています。

https://zenn.dev/hideokamoto/books/e961b4bad92429

ローカルでイベントを受け取る

StripeのダッシュボードでWebhookを開くと「ローカル環境でテスト」というボタンがあります。ここをクリックするとサンプルコードと使い方が表示されます。[3]

Stripeイベントのリッスン

指示通りにサンプルコードを実行すればローカル環境でイベントを受信できます。イベントはStripe CLIで擬似的に発生させることもできますし、実際に請求ポータルを開いてサブスクリプションを解約してもOKです。[4]APIドキュメントには大量のイベントが記載されています。[5]サブスクリプションの解約時に発生するイベントはcustomer.subscription.deletedです。

解約処理の実装

今回実装したい機能は次のとおりです。

  • サブスクリプション解約イベントをWebhookで受け取る
  • StripeのカスタマーIDに紐づくCognitoのユーザーIDとAPIキーのIDを取得する
  • CognitoのユーザーIDに紐づく有償版のAPIキーを削除する
  • Cognitoのカスタム属性とDynamoDBを更新する

サブスクリプション開始時の処理を修正

現時点ではStripeのカスタマーIDに紐づくCognitoのユーザーIDとAPIキーのIDを取得する手段がありません。そこで先週作ったアップグレード時の処理を修正します。

fm_mail_create_api_key_pro/app.pyの一部
    # DynamoDBにAPIキーを登録
    with api_key_table.batch_writer() as batch:
        batch.put_item(Item={"UserID": user_name, "Type": "PRO", "ApiKey": api_key})

+   # DynamoDBにStripeのカスタマーIDを登録
+   with api_key_table.batch_writer() as batch:
+       batch.put_item(
+           Item={
+               "CustomerID": customer_id,
+               "UserID": user_name,
+               "APIKeyID": api_key_id,
+           }
+       )

    # DynamoDBの支払い済みフラグを更新
    dt_now_jst = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9)))
    stripe_table.update_item(
        Key={"SessionID": session_id},
        ExpressionAttributeNames={"#PaidFlag": "PaidFlag", "#UpdatedAt": "UpdatedAt"},
        ExpressionAttributeValues={
            ":PaidFlag": True,
            ":UpdatedAt": dt_now_jst.strftime("%Y-%m-%d %H:%M:%S"),
        },
        UpdateExpression="SET #PaidFlag = :PaidFlag, #UpdatedAt = :UpdatedAt",
    )

アップグレードするときに、カスタマーIDとユーザーID、APIキーのIDをDynamoDBに登録します。これでカスタマーIDからユーザーIDとAPIキーのIDを取得できるようになりました。

Webhookを実装

それでは今回のメイン機能を実装します。例によってChaliceを使います。

fm_mail_stripe_webhook/app.py
import boto3
import os
import stripe
from chalice import Chalice

app = Chalice(app_name="fm_mail_stripe_webhook")

# 環境変数
USER_POOL_ID = os.environ.get("USER_POOL_ID")
DYNAMODB_API_KEY_TABLE = os.environ.get("DYNAMODB_API_KEY_TABLE")
DYNAMODB_CUSTOMER_TABLE = os.environ.get("DYNAMODB_CUSTOMER_TABLE")
REGION_NAME = os.environ.get("REGION_NAME")
STRIPE_ENDPOINT_SECRET = os.environ["STRIPE_ENDPOINT_SECRET"]

# DynamoDBに接続
dynamodb = boto3.resource("dynamodb", region_name=REGION_NAME)
api_key_table = dynamodb.Table(DYNAMODB_API_KEY_TABLE)
customer_table = dynamodb.Table(DYNAMODB_CUSTOMER_TABLE)

# API Gatewayの設定用クライアント
apigateway_cli = boto3.client("apigateway")

# Cognitoの設定用クライアント
cognito_cli = boto3.client("cognito-idp")


@app.route("/webhook", methods=["POST"])
def webhook():
    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 as e:
        raise e
    except stripe.error.SignatureVerificationError as e:
        raise e

    # サブスクリプションの解約以外は何もしない
    if event["type"] != "customer.subscription.deleted":
        return {"success": True}

    # StripeのカスタマーID
    customer_id = event["data"]["object"]["customer"]

    # DynamoDBからカスタマーIDに紐づくCognitoユーザーIDを取り出す
    result = customer_table.get_item(Key={"CustomerID": customer_id})
    user_id = result["Item"]["UserID"]
    api_key_id = result["Item"]["APIKeyID"]

    # APIキーを削除
    result = apigateway_cli.delete_api_key(apiKey=api_key_id)

    # DynamoDBからPRO版のAPIキーを削除
    api_key_table.delete_item(Key={"UserID": user_id, "Type": "PRO"})

    # ユーザープールのカスタム属性を更新
    cognito_cli.admin_update_user_attributes(
        UserPoolId=USER_POOL_ID,
        Username=user_id,
        UserAttributes=[
            {"Name": "custom:plan_type", "Value": "FREE"},
            {"Name": "custom:stripe_customer_id", "Value": customer_id},
        ],
    )

    return {"success": True}

サンプルコードはFlaskでしたが、Chaliceではリクエストパラメーターの解析部分の書き方が少し変わります。[6]イベントの解析処理はほぼ同じです。StripeのカスタマーIDを取り出したあとはコメントに書いたとおりの処理をしているだけです。とくに難しいところはないので説明は省略します。

ローカルで確認

まずWebhookサーバーを起動します。

chalice local

次にStripe CLIを使ってイベントをストリーミングします。

stripe listen --forward-to http://127.0.0.1:8000/webhook

擬似的にイベントを発生する場合は次のようにします。

stripe trigger customer.subscription.deleted

実際にイベントを発生させる場合は請求ポータルでプランをキャンセルします。

プランをキャンセル

デプロイとStripe側の設定

ローカル環境で問題なく動いたらデプロイします。

chalice deploy

デプロイ先のURLが返るので、Webhookのエンドポイントに追加しましょう。

エンドポイントの追加

エンドポイントを追加すると新しい署名シークレットが発行されます。config.jsonSTRIPE_ENDPOINT_SECRETを書き換えて、もう一度デプロイしましょう。

chalice deploy

IAMに必要なポリシーをアタッチするのも忘れないようにしましょう。DynamoDBとAPI GatewayおよびCognitoの権限が必要です。

ポリシーのアタッチ

今回の修正はバックエンドで完結しているため、React側では何もする必要はありません。

まとめ

Stripeのサブスクリプション解約をWebhookで受けて後続処理を行う方法をご紹介しました。

これで主要機能はほぼ完成です。残タスクは次のように細かいものばかりです。

  • ユーザー情報変更機能の実装
  • 通知機能の実装
  • ユーザー向けドキュメントの作成
  • 利用規約をきちんと書き直す
  • セキュリティーポリシーをきちんと書き直す
  • Stripeを本番環境に変更する

あれ?結構残ってますね。はたして今月中に完成するでしょうか。

本を執筆中です

今回作成したWebアプリの作り方をまとめた本を執筆中です。仮題は「ReactとPythonでAPI販売サービスを作ろう」です。まだ環境を整えただけで1章までしか書けていませんが、[7]なんとか9月の技術書典13に間に合うようがんばります。

脚注
  1. https://zenn.dev/sikkim/articles/cfb1de153943e3 ↩︎

  2. デフォルトでは無効になっています。 ↩︎

  3. StripeのUI/UXは本当に素晴らしいです。他のサービスも追随してもらいたいところです。 ↩︎

  4. もちろんテスト環境で実施します。 ↩︎

  5. https://stripe.com/docs/api/events/types ↩︎

  6. これが地味に難しくて、たった2行を書くために1時間近く試行錯誤を繰り返しました。 ↩︎

  7. https://zenn.dev/sikkim/articles/d5e6d851d100d2 ↩︎

Discussion