2020/10/29

[NuxtJs][Python]StripeのCheckoutを使った支払を実装してみた

stripenuxtjsfastapipythonvuejs

概要

stripeにはカードの支払を簡単に実装する方法が提供されています。

単純な支払・定額課金・請求書の作成と支払など結構複雑な要件でも対応してくれます。

今回は、単純な支払をするパターンを実装したので、そのメモになります。

主に以下のドキュメントを参考にしました

実施事項

流れ

大まかな動作の流れは以下です

  1. ユーザーが購入ボタンを押す
  2. (server側からstripe Session idが発行されそれを元に)stripeの支払画面に遷移する
  3. stripeの支払画面で操作を行う
  4. success_url / cancel_urlにリダイレクトされ、結果を表示する
  5. (並行して)stripeからwebhookが呼ばれ、商品付与処理が実行される

注意点としては、商品購入完了ページに飛ばされたとしても、必ずしも商品付与が完了しているとは限らないので、そのような制約は念頭に入れておく必要があります。

大まかな実装の流れは以下のようになります。

  1. server側でstripe Sessionを作成
  2. client側でstripeにリダイレクト
  3. 成功/キャンセルした場合のページを作成
  4. stripe用のwebhookを実装

server側でstripe Sessionを作成

  • FastAPIを使って実装しています。
  • かなりデフォルメしていますが、以下のようになりました
from fastapi import APIRouter, HTTPException, status
import stripe

stripe.api_key = 'sk_test_your_stripe_secret_key'
router = APIRouter()

@router.post("/create-session", status_code=status.HTTP_201_CREATED)
def create_checkout_session():
    try:
        # 本来ここではpostデータから商品データをデータベースから引っ張ってきて商品・価格の内容を作成しています
        product_data = {
            "name": "あなたの商品名"
        }
        price = 200  # あなたの商品価格
        quantity = 1 # 購入数
        success_url = "https://your-front-domain.com/checkout/result"  # stripeの支払画面で支払が成功した時にリダイレクトされます
        cancel_url = "https://your-front-domain.com/checkout/cancel"   # stripeの支払画面でキャンセルした時にリダイレクトされます

        checkout_session = stripe.checkout.Session.create(
            payment_method_types=["card"],
            line_items=[
                {
                    "price_data": {
                        "currency": "jpy",
                        "unit_amount": price,
                        "product_data": product_data,
                    },
                    "quantity": quantity,
                },
            ],
            mode="payment",
            success_url=success_url,
            cancel_url=cancel_url,
        )
        # ここで Checkout Session id をキーにどの商品を購入しようとしていたか履歴を取っておくと便利です。
        return {"id": checkout_session.id}
    except Exception as e:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail=str(e)
        )

client側でstripeにリダイレクト

clientのconfig

  • NuxtJsでのセットアップは、この記事と同様の設定を行っています。
% yarn add nuxt-stripe-module

.env

STRIPE_PUBLISHABLE_KEY="pk_test_your_stripe_public_key"

nuxt.config.js

  modules: [
    ...
    'nuxt-stripe-module' // この行を追加
  ],
  // 以下のblockを追加
  stripe: {
    version: 'v3',
    publishableKey: process.env.STRIPE_PUBLISHABLE_KEY
  },

画面側の実装

<template>
  <button @click="checkout">購入</button>
</template>
<script>
export default {
  methods: {
    async checkout() {
      // 先ほどのサーバーにリクエストを飛ばす
      const res = await this.$axios
        .$post('/create-session')
        .then((data) => {
          return { data }
        })
        .catch((err) => {
          return { err }
        })
      if (!res.err) {
        // checkout session idが返却されているので、それを使って、redirectさせる
        const sessionId = res.data.id
        const stripe = this.$stripe.import()  // Stripeインスタンスを生成
        const redirectResult = await stripe.redirectToCheckout({
          sessionId,
        })
        if (redirectResult.error) {
          console.log(redirectResult.error.message)
        }
      }
    }
  }
}
</script>

成功/キャンセルした場合のページを作成

server側で指定した success_url / cancel_url に対応したページを作っておきます。

  • pages/checkout/result.vue
  • pages/checkout/cancel.vue

このページでは、サーバー側に最後にチェックアウトした商品を問合せて、どのような商品を購入/キャンセルしたのかを制御できるとユーザビリティが高くなりそうです。

stripe用のwebhookを実装

商品を購入した後にユーザーに対して商品購入処理(商品付与等)をするには、stripeから決済が完了した際に呼ばれるwebhookを使うと便利です。

ちなみに、success_url でしたページ起因で処理をしてしまうとユーザーがブラウザに直接URLを入力してしまった場合に思わぬ処理をしてしまう場合があるので、注意が必要です。

また、(例えば、メンテナンス中やネットワーク障害などで、webhookがリアルタイムに受け取れなかった場合等に有効そうですが)
StripeのAPIを使って、Sessionデータを取得して処理をすることで、webhookでなくても処理はできそうな気がします。
(かなり特殊な要件ですが、incoming webhookが一時的に受け取れないとかネットワーク的にNGの場合など?はそういうこともできそうです。)

stripeにwebhookを設定

import stripe

stripe.api_key = 'sk_test_your_stripe_secret_key'
webhook_url = 'http://fffffffffff.ngrok.io/webhook'  # ngrokを使うとローカルでテストするときに便利です

webhook = stripe.WebhookEndpoint.create(
    url=webhook_url,
    enabled_events=[
        "checkout.session.completed",
    ],
)
print(webhook)  # webhook secretをメモしておきます

webhook実装

  • (こちらも)FastAPIを使って実装しています。
  • (こちらも)かなりデフォルメしていますが、以下のようになりました
import stripe
from fastapi import APIRouter, status, Request

router = APIRouter()


@router.post("/webhook", status_code=status.HTTP_200_OK)
async def stripe_webhook(request: Request):
    data = await request.body()
    stripe_signature = request.headers.get("stripe-signature")
    try:
        event = stripe.Webhook.construct_event(
            payload=data,
            sig_header=stripe_signature,
            secret="your-stripe-webhook-secret",
        )
    except ValueError as e:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid payload"
        )
    except stripe.error.SignatureVerificationError as e:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid signature"
        )
    
    event_type = event["type"]
    if event_type == "checkout.session.completed":
        session = event["data"]["object"]
        if session["payment_status"] == "paid":
            _fulfill_order(session)

    return "OK"


def _fulfill_order(session):
    # TODO: ここで支払成功後の商品付与処理を実施します
    # (念のため)複数回呼ばれることも考慮して、1sessionにつき1回だけ処理するようにします
    print("Fulfilling order")

カードのテスト

支払のテストには以下の番号が使えます。これを使ってテストを実施しました。

カード番号 動作
4242 4242 4242 4242 必ず成功
4000 0025 0000 3155 認証が必要(ポップアップが表示されます)
4000 0000 0000 9995 必ず失敗

以上になります。
慣れていると結構簡単に実装できます。
stripe便利で良いですね