StripeのWebhookをAWS Lambda上のPythonで実装する
(2022年11月25日追記)
私の本が株式会社インプレス R&Dさんより出版されました。この記事の内容も含まれています。イラストは鍋料理さんの作品です。猫のモデルはなんとうちのコです!
感想を書いていただけるととても嬉しいです!
(2022年8月3日追記)この記事の内容はこちらの本でも読めます。
サブスクリプションの解約機能を作りたい
先日は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についてはこちらの本で詳しく説明されています。
ローカルでイベントを受け取る
StripeのダッシュボードでWebhookを開くと「ローカル環境でテスト」というボタンがあります。ここをクリックするとサンプルコードと使い方が表示されます。[3]
指示通りにサンプルコードを実行すればローカル環境でイベントを受信できます。イベントは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を取得する手段がありません。そこで先週作ったアップグレード時の処理を修正します。
# 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を使います。
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.json
のSTRIPE_ENDPOINT_SECRET
を書き換えて、もう一度デプロイしましょう。
chalice deploy
IAMに必要なポリシーをアタッチするのも忘れないようにしましょう。DynamoDBとAPI GatewayおよびCognitoの権限が必要です。
今回の修正はバックエンドで完結しているため、React側では何もする必要はありません。
まとめ
Stripeのサブスクリプション解約をWebhookで受けて後続処理を行う方法をご紹介しました。
これで主要機能はほぼ完成です。残タスクは次のように細かいものばかりです。
- ユーザー情報変更機能の実装
- 通知機能の実装
- ユーザー向けドキュメントの作成
- 利用規約をきちんと書き直す
- セキュリティーポリシーをきちんと書き直す
- Stripeを本番環境に変更する
あれ?結構残ってますね。はたして今月中に完成するでしょうか。
本を執筆中です
今回作成したWebアプリの作り方をまとめた本を執筆中です。仮題は「ReactとPythonでAPI販売サービスを作ろう」です。まだ環境を整えただけで1章までしか書けていませんが、[7]なんとか9月の技術書典13に間に合うようがんばります。
Discussion