オンライン決済サービスStripeをDjangoで実装する

Django
DjangoWeb

オンライン決済サービスというと、審査が厳しいとか実装が難しいとか何かとハードルが高い印象がある。また、いくつかのオンライン決済サービスがあり、一長一短があるため選ぶのも難しい。
色々悩んだ結果、オンライン決済サービスの1つであるStripeというサービスを使ったところ、非常に良かったので、この記事ではStripeについて紹介する。
また、自身のメモのためにもDjangoでの実装について軽く紹介する。

スポンサーリンク

オンライン決済サービスについて

オンライン決済サービスはとてもたくさんのサービスがあり、手数料の安さや決済手段の多さ、サービスの手厚さなどでそれぞれが差別化を図っている。どのオンライン決済が良いかは自分自身のサービスに合わせて決めると良い。

具体的に挙げると下のようなサービスがある。

  • Stripe
    • 今回紹介するのがこれ。
  • Square
    • オフライン決済もやっている。お店にいくとSquareの端末をたまに見る。
  • Pay.jp
    • 日本発のサービス。VISAやMasterCardの手数料が3.0%と安い。
スポンサーリンク

Stripeについて

Stripeは2021年8月時点で米国最大のユニコーン企業である。IPOしておらず時価総額が10兆円を超えている。
オンラインサービスを手掛ける企業を中心とした顧客を持ち、Shopify, Slack, Zoom, Lyftなど名だたる企業が利用している。

Stripeを利用する上でメリット・デメリットをまとめると以下のようになる。

メリット

  • 決済手数料が一律3.6%でシンプル
    • 他のサービスはJCBだけ3.95%だったりと複雑になっていることが多い。
    • 振込手数料や初期費用などその他の手数料がゼロ
  • 実装が非常に簡単
    • ドキュメントも充実している
    • クレジットカードだけではなく、Google PayとApple Payにもデフォルトで対応。
  • セキュリティが強固
  • UIUXが考えつくされていて、WebコンソールもAPIもとても使いやすい
  • 審査が後からなのですぐに使い始められる
    • 個人事業主でも使える
  • C2Cサービスのような複雑な決済にも対応

デメリット

  • 一律3.6%という手数料が必ずしも安いわけではない

Djangoでの実装

Stripeではドキュメントが非常に丁寧に作られているため、基本的にはそのドキュメントを見るだけで決済を実装することができる。
ただ、DjangoではなくFlaskでの実装が書かれていることが多く、適宜読み替える必要がある。

ここではDjangoで支払いを受け付ける方法から、支払いの処理をするためのWebhookの使い方までをメモしておく。(アカウントの作成は済んでいるものとする。)

基本的には以下のドキュメントをDjango版に置き換え、少しアレンジしたものである。

支払いを受け付ける

支払いページの作成はStripe APIのおかげで非常簡単に実装することができる。

まずはStripeライブラリをインストールする。

pip intall stripe

StripeのマイページからAPIキーを取得し、settings.pyに記述する。(または、.envファイルに記述する。)

STRIPE_PUBLIC_KEY = 'YOUR_PUBLIC_KEY'
STRIPE_SECRET_KEY = 'YOUR_SECRET_KEY'

例として、myapp/models.pyにProductテーブルを作り、すでに商品がいくつか登録されている状態であるとする。
また、注文はOrderテーブルに記録する。(stripeカラムについては後ほど説明する。)

from django.db import models

class Product(models.Model):
    name = models.CharField(verbose_name='商品名', max_length=100)
    description = models.TextField(verbose_name='説明')
    price = models.PositiveIntegerField(verbose_name='価格')
    timestamp = models.DateTimeField(verbose_name='登録日', auto_now_add=True)
    thumbnail = models.ImageField(upload_to='images/', verbose_name='画像')

class Order(models.Model):
    # customer = models.ForeignKey(User, on_delete=models.SET_NULL,  null=True)
    product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True)
    price = models.PositiveIntegerField(verbose_name='価格')
    timestamp = models.DateTimeField(verbose_name='発注日', auto_now_add=True)
    stripe = models.CharField(verbose_name='Stripe Session', max_length=100)

myapp/templates/order.htmlに以下のようなhtmlファイルを作成する。
決済に進むボタンを押すと、チェックした商品の決済ページ(Stripeのページ)に移動する。

<html>
    <head>
        <title>商品の購入</title>
    </head>
    <body>
        <form action="{% url 'create-checkout-session' %}" method="POST">
            {% comment %} ユーザ情報や住所の収集なども行えると良い {% endcomment %}
            {% for p in products %}
            <p>
                <input type="radio" name="product" value="{{ p.id }}" id="id_product_{{ p.id }}">
                <label for="id_product_{{ p.id }}">{{ p.name }}</label>
            </p>
            {% endfor %}
            <button type="submit">決済に進む</button>
        </form>
    </body>
</html>

また、myapp/templates/success.htmlに以下のようなhtmlファイルを作成する。
商品の購入が完了したあとに移動するページである。

<html>
  <head><title>Thanks for your order!</title></head>
  <body>
    <h1>Thanks for your order!</h1>
    <p>
      We appreciate your business!
      If you have any questions, please email
      <a href="mailto:[email protected]">[email protected]</a>.
    </p>
  </body>
</html>

次に、myapp/views.pyに次のように記述。

import stripe
from .models import * 
from django.views.decorators.http import require_POST
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse
from django.conf import settings

stripe.api_key = settings.STRIPE_SECRET_KEY

def order_view(request):
    products = Product.objects.all()
    return render(request, 'order.html', {'products': products})

def checkout-success-view(request):
    return render(request, 'success.html')

@require_POST
def create_checkout_session(request):
    product_id = request.POST['product']
    product = get_object_or_404(Product, id=product_id)
    # customer = request.user

    session = stripe.checkout.Session.create(
        # customer_email= customer.email,
        payment_method_types=['card'],
        line_items=[{
            'price_data': {
                'currency': 'jpy',
                'product_data': {
                    'name': product.name,
                    'images': [product.image.url]
                },
                'unit_amount': product.price,
            },
            'quantity': 1,
        }],
        mode='payment',
        success_url=request.scheme + '://' + request.get_host() + reverse('success'),
        cancel_url=request.scheme + '://' + request.get_host() + reverse('order'),
        metadata = {
            'product_id': product.id,
        }
    )
    return redirect(session.url)

重要なのがstripe.checkout.Session.createという部分。
商品のデータをここで指定できる。(事前に商品データをStripe上に登録することもできる。
また、自身のデータベースに保存するためにproduct.idmetadataに入れておくのが良い。
session.urlにリダイレクトすることでStripeの購入ページに移動することができる。
詳しくは、公式ドキュメントを参照。ドキュメントは熟読するのがおすすめ。

また、上では@require_POSTデコレータを使ってcreate_checkout_sessionではPOSTのみを受け付けるようにしている。このデコレータについて。

次に、myapp/url.pyに次のように記述。

from django.urls import path
from . import views

urlpatterns = [
    path('order', views.order-view, name='order'),
    path('create-checkout-session', views.create-checkout-session, name='create-checkout-session'),
    path('checkout/success', views.checkout-success-view, name='success'),
]

以上で、テストカードとして、4242 4242 4242 4242など(有効期限とセキュリティコードは任意)を入力すれば、決済が完了したことが分かる。

注文の処理(Webhookの利用)

決済が完了すると、上のstripe.checkout.Session.createという部分で記載したsuccess_urlに移動する。
このページのViews関数でOrderの処理(データベースに保存、購入者にメールなど)を行うこともできるが、非推奨となっている。
これは、必ずしもユーザが注文完了後にsuccess_urlに到達するとは限らないためである。

そこでWebhookを利用してStripeから注文が完了した通知を受け取り、注文の処理をする。
そもそもWebhookとは、あるイベントが発生したことを指定したURLに通知する仕組みである。
今回は、決済の完了を指定のURLに通知させ、views関数で処理を行う。

まず、Stripe CLIをインストールする。
Stripe CLIはローカルでStripeの組み込みをテストするものである。
インストールしたら次にようにイベントを指定にURLに転送できるようにしておく。

stripe listen --forward-to localhost:4242/webhook

また、出てきたendpoint_secretsettings.pyなどに書いておく。

Dockerでインストールした場合、次の記事に書いたことを注意する。

次に、myapp/views.pyに次のように追記する。

from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

endpoint_secret = settings.ENDPOINT_SECRET

@csrf_exempt
@require_POST
def checkout-success-webhook(request):
payload = request.body
  sig_header = request.META['HTTP_STRIPE_SIGNATURE']
  event = None

  try:
    event = stripe.Webhook.construct_event(
      payload, sig_header, endpoint_secret
    )
  except ValueError as e:
    # Invalid payload
    return HttpResponse(status=400)
  except stripe.error.SignatureVerificationError as e:
    # Invalid signature
    return HttpResponse(status=400)

  # Handle the checkout.session.completed event
  if event['type'] == 'checkout.session.completed':
    session = event['data']['object']

    # Fulfill the purchase...
    fulfill_order(session)

  # Passed signature verification
  return HttpResponse(status=200)

def fulfill_order(session):
  order = Order.object.create(
        product = Product.objects.get(id=session['metadata']['product_id']),
        price = session['amount_total'],
        stripe = session['id']
  )
  print("Fulfilling order")

@csrf_exemptはDjangoのCSRF認証を無視するものである。このデコレータについてはこちら。
イベントハンドラには誰でもデータを書き込むことができるため、eventが Stripe から送信されていることを確認している。

fulfill_order関数の中で実際の処理を書いている。
Orderテーブルの中にstripeカラムを作ったのは、stripeの決済情報と紐付けるためである。

stripe.checkout.Session.retrieve(order.stripe)

などで、決済情報を参照することができる。
また、myapp/urls.pyに次のように書き込む。

urlpatterns = [
    path('webhook', views.checkout-success-webhook, name='checkout-success-webhook'),
]

これでStripe CLIを動かした状態で、テストカードで商品を購入すると、処理が完了していることが分かる。

おわりに

DjangoでStripeの決済を実装する方法を紹介した。
この記事での実装は導入のほんの一部で、実際に決済を実装するとなると細かな仕様(割引、配送料金、税金などなど)を詰める必要がある。
Stripeはカスタマイズ性に非常に優れているので、ドキュメントを検索すればそれっぽいことが出てくる。
ドキュメントさえ読めば、本当に簡単に実装できてしまうのがStripeのすごさである。

C2Cのような複雑な資金移動をする場合に使う、Stripe Connectについても別記事でまとめる。

スポンサーリンク
H-MEMO

コメント