Stripe決済のDjangoでの実装

# 初めに
Stripeを使えば比較的簡単に決済機能を実装できます。この記事ではStripeが用意した決済フォームにページをリダイレクトさせることで、自サイトではクレジットカード情報を保持することなく支払い機能を自サイトに組み込むことができます。

基本は[Stripeのドキュメント](https://stripe.com/docs/payments/checkout/one-time)と[Github上のサンプル](https://github.com/stripe-samples/checkout-one-time-payments)を基にして実装しています。また、Djangoで実装します。~~正直、試行錯誤しながら適当に作ったので実装に関して突っ込みどころ満載です。~~

最終的には[Stripeのサンドボックス](https://0hczv.sse.codesandbox.io)の劣化版が実装されます。また、以下の情報は2020年3月時点のものです。

## 環境設定
- Python==3.8.1
- Django==3.0.4
- stripe==2.43.0
- django-environ==0.4.5

# Step0: 前準備

適当にディレクトリを作成する。

```bash
mkdir stripe-test
cd stripe-test
```

`pipenv`を使って仮想環境を構築します。

```bash
pipenv --python 3.8
pipenv install django stripe django-environ
```

`pipenv shell`で仮想環境を起動する。pipenvについてさらに詳しく知りたい場合は[Pipenvを使ったPython開発まとめ](https://qiita.com/y-tsutsu/items/54c10e0b2c6b565c887a)を参照。

```bash
# shell起動
pipenv shell
```

`Django`プロジェクトを作成する。

```bash
django-admin startproject stripe_test .
django-admin startapp checkout
```

## Stripeにアカウント登録

Stripeにアカウント登録する。アカウント作成やダッシュボードの見方については[RailsのdeviseとStripe(とBootstrap)の組み合わせで、ただただユーザー登録させたユーザーに定期更新購読させるだけのappを作りました②](https://qiita.com/Rafflesia/items/5ce936656c6f99c144ef)に詳しい。

`公開ビジネス情報`を入力する。2020年3月現在だと以下のURL。

- https://dashboard.stripe.com/account

次に開発用の公開キーとシークレットキーを確認する。2020年3月現在だと以下のURL。

- https://dashboard.stripe.com/test/apikeys

## Django Setting

Djangoのsettingを以下のようにする。`.env`ファイルを取り込むのに`django-environ`を使用している。

```python:stripe_test/stripe_test/settings.py

import os
import environ

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_NAME = os.path.basename(BASE_DIR)

# django-environから環境変数を読み込む
env = environ.Env()
env.read_env(os.path.join(BASE_DIR, '.env'))

### 省略 ###

INSTALLED_APPS = [
    ...,
    'stripe',#追加
    'checkout.apps.CheckoutConfig', # 自作 チェックアウト
]

### 省略　###

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')], #デフォルトから変更
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

### 省略　###
# Static files (CSS, JavaScript, Images)

STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

STRIPE_SECRET_KEY = env('STRIPE_SECRET_KEY')
STRIPE_PUBLISHABLE_KEY = env('STRIPE_PUBLISHABLE_KEY')
```

`.env`には例えばこういう設定を行う。使用通貨がアメリカドル(usd)の場合はセント単位で計算するので1234と入力した場合は$12.34となる。

```bash:.env
# Stripe API keys
STRIPE_PUBLISHABLE_KEY=pk_12345
STRIPE_SECRET_KEY=sk_12345

# Checkout settings 
DOMAIN=http://localhost:8000/
BASE_PRICE=1099
CURRENCY=usd
```

# [Step1:サーバー側でチェックアウト・セッションを実装する](https://stripe.com/docs/payments/checkout/one-time#create-checkout-session)

セッションの作成はサーバー側で行う。[Python + Django でJSONの送受信](https://qiita.com/t-iguchi/items/ac9638dbdbe509515148)を参考にして、以下のようにDjangoを使って、Checkout Sessionを作成する。

```python:stripe_test/checkout/views.py
# -*- coding: utf-8 -*-
import logging
import os
import json

from django.shortcuts import render
from django.conf import settings
from django.views import generic

import stripe
from django.http.response import JsonResponse
from django.views.decorators.csrf import csrf_exempt

logger = logging.getLogger(__name__)

@csrf_exempt
def onetime_payment_checkout(request):
    if request.method == 'POST':
        data = json.loads(request.body)
        domain_url = os.getenv('DOMAIN')
        stripe.api_key = settings.STRIPE_SECRET_KEY
        try:
            # Create new Checkout Session for the order
            # ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param
            checkout_session = stripe.checkout.Session.create(
                success_url=domain_url +
                "checkout/success?session_id={CHECKOUT_SESSION_ID}",
                cancel_url=domain_url + "checkout/canceled/",
                payment_method_types=["card"],
                line_items=[
                    {
                        "name": "Pasha photo",
                        "images": ["https://picsum.photos/300/300?random=4"],
                        "quantity": data['quantity'],
                        "currency": os.getenv('CURRENCY'),
                        "amount": os.getenv('BASE_PRICE'),
                    }
                ]
            )
            logger.debug( str(checkout_session))
            return JsonResponse({'sessionId': checkout_session['id']})
        except Exception as e:
            logger.warning( str(e) )
            return JsonResponse({'error':str(e)})
```

# [Step2:チェックアウトにリダイレクト](https://stripe.com/docs/payments/checkout/one-time#redirect-checkout)

基本的にJavascriptを使って実装していく。実現していることは`stripe.js`を使ってStripe側にデータを渡して、Stripeで決済してもらった後に自サイトに戻る。

```html:stripe_test/templates/base.html
{% load i18n static %}
<!DOCTYPE html>{% get_current_language as LANGUAGE_CODE %}
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
        <title>{% block title %}{% endblock %}</title>
        <script src="{% static 'js/jquery-3.3.1.slim.min.js' %}"></script>
        <script src="{% static 'js/bootstrap.min.js' %}"></script>
        {% block additional_script %}
        {% endblock %}
    </head>
    <body>
        <div class="container">
            {% block content %}
            {% endblock %}
        </div>
    </body>
</html>
```

```html:stripe_test/templates/checkout/checkout_test.html
{% extends "base.html" %}

{% load i18n static %}
{% block title %}Checkout_test{% endblock title %}

{% block additional_script %}
<script src="https://js.stripe.com/v3/"></script>
<script src="{% static 'checkout/js/script.js' %}"></script>
{% endblock additional_script%}


{% block content %}
<div>
    <h1 data-i18n="headline"></h1>
    <h4 data-i18n="subline"></h4>
    <div class="pasha-image">
      <img
        src="https://picsum.photos/280/320?random=4"
        width="140"
        height="160"
      />
    </div>
  </div>
  <div class="quantity-setter">
    <input type="number" id="quantity-input" min="1" max="2" value="1" />
  </div>
  <p class="sr-legal-text" data-i18n="sr-legal-text"></p>

  {% csrf_token %}
  <button
    id="submit"
  >PAYMENT</button>
</section>
<div id="error-message"></div>
</div>
{% endblock content %}
```

自作Scriptは以下のサイトを参考にした
- [参考Github](https://github.com/stripe-samples/checkout-one-time-payments/blob/master/client-and-server/client/script.js)
- [Fetch を使う](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch)

```javascript:stripe_test/static/checkout/js/script.js
/* Handle any errors returns from Checkout  */
var handleResult = function(result) {
    if (result.error) {
      var displayError = document.getElementById("error-message");
      displayError.textContent = result.error.message;
    }
  };

  // Create a Checkout Session with the selected quantity
  var createCheckoutSession = function() {
    var inputEl = document.getElementById("quantity-input");
    var quantity = parseInt(inputEl.value);
    return fetch("/checkout/create-checkout-session/", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        quantity: quantity,
      })
    }).then(function(result) {
      return result.json();
    });
  };

  /* Get your Stripe publishable key to initialize Stripe.js */
  fetch("/checkout/config")
    .then(function(result) {
      return result.json();
    })
    .then(function(json) {
      window.config = json;
      var stripe = Stripe(config.publicKey);
      // Setup event handler to create a Checkout Session on submit
      document.querySelector("#submit").addEventListener("click", function(evt) {
        createCheckoutSession().then(function(data) {
          stripe
            .redirectToCheckout({
              sessionId: data.sessionId
            })
            .then(handleResult);
        });
      });
    });
```

HTMLとJavascriptが実装できたら、checkout_test.htmlが見られるように`views.py`を以下を追加する。

```python:stripe_test/checkout/views.py
class IndexView(generic.TemplateView):
    template_name = "checkout/checkout_test.html"
    def get(self, request, *args, **kwargs):
        return render(request, self.template_name )
```

とりあえずは`checkout`アプリのマッピング情報を記述して、最初のページだけ見られるようにする。これは`checkout/urls.py`を以下のように編集することで実現できる。

```python:stripe_test/checkout/urls.py
from . import views
from django.urls import path,include

app_name = 'checkout'

urlpatterns = [
    path('',views.IndexView.as_view(), name="index"),
]
```

Djangoアプリではない、ホームページ側の`urls.py`（この記事では`stripe_test/stripe_test/urls.py`)を以下のように修正する。

```python:stripe_test/stripe_test/urls.py
from django.contrib import admin
from django.urls import path,include
from django.views.generic import RedirectView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', RedirectView.as_view(url='/checkout')),
    path('checkout/', include('checkout.urls')),
]
```

上記自作のjavascript（`stripe_test/static/checkout/js/script.js`）では`fetch("/checkout/config")`を行っているが、これはjsonでstripeAPIの公開キーを受け取っている。サーバー側の実装は以下のとおり。

```python:stripe_test/checkout/views.py
@csrf_exempt
def stripe_config(request):
    if request.method == 'GET':
        stripe_config = {
            'publicKey': settings.STRIPE_PUBLISHABLE_KEY,
            'basePrice': os.getenv('BASE_PRICE'),
            'currency': os.getenv('CURRENCY'),
        }
        return JsonResponse(stripe_config, safe=False)
```

```python:stripe_test/checkout/urls.py
from . import views
from django.urls import path,include

urlpatterns = [
    ...,
    path('config/',views.stripe_config),
    ...,
]
```

`python manage.py runserver`でとりあえずサイトを起動して`http://localhost:8000/`にアクセスすると以下の感じの画面が出るはずだ。

<img width="210" alt="サンプルページ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235984/62bfe8e5-034d-9711-a338-7040fb1be3d2.png">

ただ、これだと`PAYMENT`を押しても、何も起きないはず。なぜなら、ボタン押されたあとの実装を行っていないから。具体的には自作javascriptの`createCheckoutSession`関数はとあるURLにPOSTしているが、Django側で設定していないので、設定を行う必要がある。それには`urls.py`に`create-checkout-session`を追加すればいい。

```python:stripe_test/checkout/urls.py

urlpatterns = [
    ...,
    path('create-checkout-session/',views.onetime_payment_checkout , name='create-checkout'),
    ...,
]
```

以上まで実装を行えば、Stripe側にURLが飛んで決済情報を入力する画面が表示されるはずです（参考：[Stripe Checkout](https://stripe.com/docs/payments/checkout)）。後は、決済情報を入力し終わったリダイレクト先のページ（`success.html`と`canceled.html`）を実装していけば、一通り決済はできるようになっている。

#テスト
テスト用の決済カードは[複数用意されている](https://stripe.com/docs/testing#cards)ので、適宜選ぶ。特にこだわりがないのならば `4242424242424242`を使用。

もし、うまく実装できていれば[ダッシュボード上](https://dashboard.stripe.com/test/payments)に支払いが反映されているので、確認してみてください。

# 参考:Github
上記の実装はGithubに公開しているので、参考にしてください。
- [Github上のサンプル](https://github.com/k-ken-t4g/django_stripe_one-time-checkout)

# 参考サイト
- [Stripe.js & Elements を利用して決済フローを理解する](https://qiita.com/y_toku/items/7e51ef7e69d7cbbfb3ca)