DjangoでStripe決済

こんなことがしたかった

  • Djangoで作っているアプリに、クレジットカード決済を導入したかった

決済系の機能をつけたかったので、外部サービスを利用しなんとか決済できないかと調査したところ「Stripe」というサービスがどうも使いやすそうだということで、とりあえず入れてみることにした。 ※まだ本番運用はしていない

情報があるようでないようで、結局ドキュメントを読みながら組み立てていったのでそれをもとに備忘録として残す。

こんな感じ

仕組みとしては、

  1. ブラウザ側で画面を読み込む際、Javascriptからサーバのエンドポイントに通信を行う
  2. 通信を受けたサーバ側で、Stripe側に準備されているPaymentIntent APIに通信を行い、で「PaymentIntent」なるものを生成する
  3. その「PaymentIntent」から「clientsecret」という値を取得しブラウザ側に返す
  4. うまくいったらブラウザ側にクレジットカード情報を入力するフォームが作成される
  5. そのフォームにクレジットカード情報を入力し、「支払う」ボタンを押すと、Stripe側が準備したJavascriptの関数で、入力したカード情報とclientsecretを利用し決済処理を行う
  6. 決済に成功したか失敗したかの情報がブラウザ側に返ってくるので、その情報をもとに成功メッセージやエラーメッセージを表示させるなどの処理を行う

という形のようだ。

サンプルコードは色々用意されているので、参考にしながら書けば機能する。

主に実装する必要があるのは決済用の画面以外では「2.のサーバのエンドポイント」「6.の成功/失敗後の処理」くらいではなかろうか。

実装の前に

Python用のStripeパッケージをインストールする必要がある。

pip3 install stripe でOK。

サーバのエンドポイント

サンプルコードのページによるとFlaskでは以下のように実装するようだ(私はFlaskはよくわからないが…)

  • server.py
import json
import os
import stripe
# This is your real test secret API key.
stripe.api_key = "シークレットキー"
from flask import Flask, render_template, jsonify, request

app = Flask(__name__, static_folder=".",
            static_url_path="", template_folder=".")

※中略

@app.route('/create-payment-intent', methods=['POST'])
def create_payment():
    try:
        #①ブラウザから送られたjsonを読み込む
        data = json.loads(request.data)
        #②stripe.PaymentIntent.createでPaymentIntent APIに通信する
        intent = stripe.PaymentIntent.create(
        #③APIに渡す値を記載。最低限「amount」と「currency」をjson形式で渡せば良い
        #ちなみに「calculate_order_amount」は商品名を渡して金額を計算するロジックのようなので、特に気にせずともよい。
            amount=calculate_order_amount(data['items']),
            currency='usd'
        )
        #④APIから返ってきた値のうち、clientsecretをブラウザに返す
        return jsonify({
          'clientSecret': intent['client_secret']
        })
    except Exception as e:
        return jsonify(error=str(e)), 403

if __name__ == '__main__':
    app.run()

こんなことが書いてあるみたいだ。これをDjangoで同じように書けばいい。

①の前段としてブラウザからjsonを送る際は、JavaScriptからPOSTで送るので、Djangoの場合csrf_tokenを一緒に送るかデコレータで回避するかしないと失敗するので注意。

ただ、送っているjsonデータは商品名だけなので、もう価格が決まっているのであればエンドポイントに通信させず、サーバ側で画面描画時にclient_secretを取得するところまでやってしまっても良いかもしれない。あとで試してみよう。

※追記:エンドポイントなしでできた。

また、③で渡せる値は結構多種多様なようで、渡す値によってはStripeのコンソールにも表示が可能だ。例えば「description」。

amount=1600,
currency='jpy'
description='説明'

という感じで投げると、Stripeのコンソール側には以下のように表示される。 f:id:ryori0925:20200614224848p:plain

コンソール側を見やすくするためにも何らかの値は渡してもいいかもしれない。

Stripe API Reference - Create a PaymentIntentに詳細は記載がある。

クライアント側のJS

サンプルコードのページによるとJS側は以下のように実装するようだ。

  • client.js
// パブリックキーを記載。
var stripe = Stripe("パブリックキー");
// PaymentIntentを作成するときにBodyに入れて送るjson。例では商品を送っている。
var purchase = {
  items: [{ id: "xl-tshirt" }]
};
// Stripeの準備ができるまではボタンを無効化
document.querySelector("button").disabled = true;

// ①サーバ側のエンドポイントにPOST通信し、問題なければフォームを作ったりしている
fetch("/create-payment-intent", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify(purchase)
})
  .then(function(result) {
    return result.json();
  })
  .then(function(data) {
    var elements = stripe.elements();
    var style = {
      base: {
        color: "#32325d",
        fontFamily: 'Arial, sans-serif',
        fontSmoothing: "antialiased",
        fontSize: "16px",
        "::placeholder": {
          color: "#32325d"
        }
      },
      invalid: {
        fontFamily: 'Arial, sans-serif',
        color: "#fa755a",
        iconColor: "#fa755a"
      }
    };
    var card = elements.create("card", { style: style });
    card.mount("#card-element");
    card.on("change", function (event) {
      document.querySelector("button").disabled = event.empty;
      document.querySelector("#card-errors").textContent = event.error ? event.error.message : "";
    });

 // ボタンが押されたら決済処理を行う
    var form = document.getElementById("payment-form");
    form.addEventListener("submit", function(event) {
      event.preventDefault();
      payWithCard(stripe, card, data.clientSecret);
    });
  });

// 決済の実処理
var payWithCard = function(stripe, card, clientSecret) {
  loading(true);
  stripe
    .confirmCardPayment(clientSecret, {
      payment_method: {
        card: card
      }
    })
    .then(function(result) {
      if (result.error) {
        // Show error to your customer
        showError(result.error.message);
      } else {
        // The payment succeeded!
        orderComplete(result.paymentIntent.id);
      }
    });
};

// ②決済がうまく行ったときの後処理
var orderComplete = function(paymentIntentId) {
  loading(false);
  document
    .querySelector(".result-message a")
    .setAttribute(
      "href",
      "https://dashboard.stripe.com/test/payments/" + paymentIntentId
    );
  document.querySelector(".result-message").classList.remove("hidden");
  document.querySelector("button").disabled = true;
};

// ③決済が失敗したときの後処理
var showError = function(errorMsgText) {
  loading(false);
  var errorMsg = document.querySelector("#card-errors");
  errorMsg.textContent = errorMsgText;
  setTimeout(function() {
    errorMsg.textContent = "";
  }, 4000);
};

※後略

概ねコピペすればよいのだが、対応が必要な部分は3点。

①については、csrf_tokenをHeaderに入れた上で通信するなどの対応が必要。

②③については、入れるシステムによって好きに対応する形となる。私は完了ページにリダイレクトさせるような形をとった。

その他

決済用ページには少なくとも以下2つを読み込ませる必要がある。

<script src="https://js.stripe.com/v3/"></script>
<script src="/client.js" defer></script>

上記でカスタムするclient.jsはdeferで読み込むこと、別途stripeのjsを読み込むことが必要。

実装してみて

ちょっと厄介だったのが、エンドポイントを作らなければならない点。なるべくなら作りたくない。

決済金額は決済画面にアクセスした時点で決定している場合、contextか何かにclientsecretを入れてしまい、 それを取ってきてフォームを生成する…とかしてしまえばいいのかなどうかな、と考えている。 (clientsecret自体はどうせ通信で返ってきちゃうので、見えても問題ない…?)

参考文献

[Django]Stipeを使ってECサイトを作る │ 機械系エンジニア奮闘記

Accept a card payment | Stripe Payments

Stripe API Reference - Create a PaymentIntent