見出し画像

DjangoでStripeのSubscription決済を構築【サブスク】

Djangoのサブスクリプションの構築をハンズオン形式で解説します。

この記事では、サイトにログインしたユーザーがサブスク会員になり、マイページで会員表示できるようになるまでを解説しています。

以下のログイン認証の構築記事から引き継いで制作をします。

宣伝

ECサイト構築、Stripeの埋め込み、決済の開発を承っています!
したいことを形にいたします。ぜひのぞいてみてください。


ログイン実装の記事から引用


アプリを2つそろえることになるので、(registrationアプリとsubscriptionアプリ)mypage.htmlを追加し、ログイン直後のページから戻れるようにする

sample/templates/registration/mypage.html

{% extends "base.html" %}
{% block main %}
<h2>マイページ</h2>
<p>{{ user }}さん、こんにちは!</p>

<p><a href="{% url 'logout' %}">ログアウト</a></p>
<p><a href="{% url 'password_change' %}">パスワードの変更</a></p>
<p><a href="{% url 'password_reset' %}">パスワードを忘れた場合</a></p>

<p><a href="{% url 'subscription:home' %}">サブスクリプションページ</a></p>

{% endblock %}


ログイン認証の追加設定は終了です。

スタートアプリ

project/sample/に移動し、新しいアプリを作ります。

sample/subscription

python manage.py startapp subscription

アプリを作ったら、setting.pyに移り、設定

setting.py

    INSTALLED_APPS=[
'subscription', #一番最後の行
]


templateフォルダにhtmlファイルを追加

template/subscription/base.html

{% load static %}
<!doctype HTML>
<html>
    <head>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
        <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
        <script src="https://js.stripe.com/v3/"></script>  
        <script src="{% static 'main.js' %}"></script> 
        
    </head>
    <body>
        <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
            <a class="navbar-brand" href="/">TOP</a>
            <a class="navbar-brand"   href="{% url 'mypage' %}">マイページ</a>

        </nav>
        <div class="container mt-4">
        {% block main %}
        
        {% endblock %}
        
        </div>
    </body>
</html>

プロジェクト直下のurls.py

from django.contrib import admin
from django.urls import path, include
from django.contrib.auth.decorators import login_required

from django.views.generic import TemplateView
from sample import views 


urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('django.contrib.auth.urls')),
    path('registration', login_required(TemplateView.as_view(template_name='registration/index.html'))),
    path("signup/", views.SignUpView.as_view(), name="signup"),
    path("mypage/", views.MypageView.as_view(), name="mypage"),
    path('activate/<uidb64>/<token>/', views.ActivateView.as_view(), name='activate'),
    path('', include('subscription.urls')),    
    ]


subscription/urls.py

from django.urls import path
from . import views

from subscription import views 

app_name ="subscription"

urlpatterns = [
    path('', views.IndexView.as_view(), name='home'),
]


sample/templates/subscription/home.html

{% extends "base.html" %}
{% load static %}

{% block main %}
<h2>サブスクページ</h2>
<p>{{ user }}さん、こんにちは!</p>

<script src="https://js.stripe.com/v3/"></script>  <!-- new -->
<script src="{% static "main.js" %}"></script> <!-- new -->

<div class="container mt-5">
    <button type="submit" class="btn btn-primary" id="submitBtn">特別会員になる</button>
  </div>
{% endblock %}

subscription/views.py

from django.views.generic import TemplateView
from sample_app.settings import AUTH_USER_MODEL
User = AUTH_USER_MODEL
from django.utils.decorators import method_decorator

@method_decorator(login_required,name="dispatch")
class IndexView(TemplateView):
    template_name = "subscription/home.html"
    model = User

method_decoratorは「クラスベースの条件つき実行」を意味します。
name=dispatchとすれば、クラスを実行するまえに必ずこの実行がされます。

関数ベースビューだとdefの上段に@login_requiredとします。

AUTH_USER_MODELはユーザーモデルを呼び出してくれます。
一時的に呼び出したいときにインスタンス化して、使い回すことができます。

関数ベースならget_user_modelを使用します。

ここで一度ルーティングがうまくいっているか確かめます。

python manage.py runserver
トップページ


サブスクページ


Stripeをインストール

pip install stripe

stripe_keyの取得をし、setiing.pyに追加します。

stripeのアカウント作成はこちら↓ (公式)

アカウントを作成したらダッシュボードに入り、テストモードにします。

APIキー画面

STRIPE_PUBLISHABLE_KEY
公開可能キーを設定します。Publicshable Key の略です
STRIPE_SECRET_KEY
シークレットキーを設定します。Secret_Keyの略です


setting.py

STRIPE_PUBLISHABLE_KEY = '<enter your stripe publishable key>' #追加
STRIPE_SECRET_KEY = '<enter your stripe secret key>'#追加


STATICフォルダを作り、その中にmain.jsファイルを入れます。

static/subscripton/js/main.js


base.htmlにSTATICファイルの読み込みを追加

{% load static %}


ユーザーモデルにStripe情報を追加する

ユーザーモデルを拡張します。
stripeに必要な
・customer_id (顧客番号)
・subscription_id(サブスクID)
・regist_date(登録日)

これらを登録して、サブスクの契約をしたと同時にStripeから得る情報をユーザーモデルに書き込みます。

subscription/models.py

from django.db import models
from sample_app.settings import AUTH_USER_MODEL
from django.utils import timezone

User = AUTH_USER_MODEL

class Stripe_Customer(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)

    stripeCustomerId = models.CharField(max_length=255)
    stripeSubscriptionId = models.CharField(max_length=255)
    regist_date = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return self.user.username


ここまできたら、makemigrations、migrateを実行してください。

adminの管理画面で操作できるように登録します。

subscription/admin.py

from django.contrib import admin


from . models import Stripe_Customer

admin.site.register(Stripe_Customer)


admin.pyに登録すると、adminの画面 で確認できます。

サブスクリプション決済を実装する


subscription/views.py

from django.shortcuts import render
from django.views.generic import TemplateView

from sample_app.settings import AUTH_USER_MODEL
import subscription
from subscription.models import Stripe_Customer

from django.contrib.auth import get_user_model


User = AUTH_USER_MODEL


from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator


from django.http.response import JsonResponse, HttpResponse 
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
import stripe 


#-----サブスクメソッド------


@csrf_exempt
def create_checkout_session(request):
    if request.method == 'GET':

        domain_url = 'http://localhost:8000/'
        stripe.api_key = settings.STRIPE_SECRET_KEY
        try:
            checkout_session = stripe.checkout.Session.create(
                client_reference_id=request.user.id if request.user.is_authenticated else None,
                success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}',
                cancel_url=domain_url + 'cancel/',
                payment_method_types=['card'],
                mode='subscription',
                line_items=[
                    {
                        'price': settings.STRIPE_PRICE_ID,
                        'quantity': 1,
                    }
                ]
            )
            return JsonResponse({'sessionId': checkout_session['id']})
        except Exception as e:
            return JsonResponse({'error': str(e)})

@csrf_exempt
def stripe_config(request):
    if request.method == 'GET':
        stripe_config = {'publicKey': settings.STRIPE_PUBLISHABLE_KEY}
        return JsonResponse(stripe_config, safe=False)


@csrf_exempt
def stripe_webhook(request):
    stripe.api_key = settings.STRIPE_SECRET_KEY
    endpoint_secret = settings.STRIPE_ENDPOINT_SECRET
    payload = request.body.decode('utf-8')
    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':

    #jsondataをフォルダ内に書き込みするテスト  ※webhook使用時   
    # if event['type'] == 'invoice.created':
        # with open("request.json", mode='w') as f:
        #     f.write(str(event))

        session = event['data']['object']

        # Fetch all the required data from session
        client_reference_id = session.get('client_reference_id')
        stripe_customer_id = session.get('customer')
        stripe_subscription_id = session.get('subscription')
        print(client_reference_id)
        print(stripe_customer_id)
        print(stripe_subscription_id)
        # Get the user and create a new Stripe_Customer
 
        user = get_user_model().objects.get(id=client_reference_id)
        print(client_reference_id)
        print(user)
        Stripe_Customer.objects.create(
            user=user,
            stripeCustomerId=stripe_customer_id,
            stripeSubscriptionId=stripe_subscription_id,
        )
        print("usercreate")

        print (' just subscribed.')

    return HttpResponse(status=200)

#--------------------------------



#-------index_view-----------
@method_decorator(login_required,name="dispatch")
class IndexView(TemplateView):
    template_name = "subscription/home.html"
    model = Stripe_Customer


#-------Success_view-----------
@method_decorator(login_required,name="dispatch")
class SuccessView(TemplateView):
   template_name = "subscription/success.html"
   
#-------Cancel_view-----------
@method_decorator(login_required,name="dispatch")
class CancelView(TemplateView):
   template_name = "subscription/cancel.html"

#----サブスクメソッド------からstripeのcheckoutセッションを作るのに必要とするメソッドです。
・@csrf exapt はCSRFトークンを無効にします。
・webhook関数 はstripeにurlを文字列として渡して、設定したwebアドレスに返してくれます。
if event['type']はwebhookを使って、Stripeと通信し、eventタイプをPOSTとGETを行います。何回もこの中のメソッドを行ったり来たりし、通信を行います。

Stripe公式にwebhookの説明が載っています。目を通すとやりやすいかもしれません。
https://stripe.com/docs/api/events/retrieve
https://stripe.com/docs/api/subscriptions


main.jsにAJAXリクエストを追加

console.log("Sanity check!");

// Get Stripe publishable key
fetch("/config/")
.then((result) => { return result.json(); })
.then((data) => {
  // Initialize Stripe.js
  const stripe = Stripe(data.publicKey);

  // Event handler
  let submitBtn = document.querySelector("#submitBtn");
  if (submitBtn !== null) {
    submitBtn.addEventListener("click", () => {
    // Get Checkout Session ID
    fetch("/create-checkout-session/")
      .then((result) => { return result.json(); })
      .then((data) => {
        console.log(data);
        // Redirect to Stripe Checkout
        return stripe.redirectToCheckout({sessionId: data.sessionId})
      })
      .then((res) => {
        console.log(res);
      });
    });
  }
});

fetchでurl画面を生成します。url.pyで設定したconfigの関数で画面を呼び出し、stripeのcreate_check_outを呼び出しています。

urls.pyのルーティングを追加

 
  path('', views.IndexView.as_view(), name='home'),
  path('create_checkout_session/', views.create_checkout_session, name='checkout_session'),#追加
  path('success/', views.SubscriptionSuccessView.as_view(), name='success'),#追加
  path('cancel/', views.SubscriptionCancelView.as_view(), name='cancel'),#追加
  path('config/', views.stripe_config),#追加
  path('webhook/', views.stripe_webhook),  # new


ベーステンプレートhtmlファイルを作成

subscription/base.html

{% load static %}
<!doctype HTML>
<html>
    <head>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
        <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
        <script src="https://js.stripe.com/v3/"></script>  <!-- new -->
        <script src="{% static 'main.js' %}"></script> <!-- new -->
        
    </head>
    <body>
        <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
            <a class="navbar-brand" href="/">TOP</a>
            <a class="navbar-brand"   href="{% url 'mypage' %}">マイページ</a>

        </nav>
        <div class="container mt-4">
        {% block main %}
        
        {% endblock %}
        
        </div>
    </body>
</html>

subscription/sucsess.html

{% extends "subscription/base.html" %}

{% block main %}

    <title>成功ページ</title>

    成功しました!

{% endblock %}


subscription/cancel.html

{% extends "subscription/base.html" %}

{% block main %}

    <title>取り消しページ</title>

    取り消ししました!

{% endblock %}


商品のIDを設定


ダッシュボードで商品を作り、price_idをコピーします。

setting.py

STRIPE_PRICE_ID = ""


Webhookのテスト

StripeCLIをダウンロードしてインストールしたら、stripeがインストールされているドライブに移動し、Stripeアカウントにログインします。

windowsコマンドから、コマンドラインを呼び出します。(vscodeエディタではローカルホストのwebブラウザが起動しているため)

stripe login

ランダムにペアリングコード(下記だとpeach-loves-classy-cozy :勝手に生成される)ができます。




Enterキーを押すとブラウザーが開かれ、アクセス許可を求められるので、許可します。

許可をすると下記の文章が出ます。

> Done! The Stripe CLI is configured for Django Test with account id acct_<ACCOUNT_ID>

Please note: this key will expire after 90 days, at which point you'll need to re-authenticate.


下記をコマンドをペーストするとイベントのリッスンが開始されます。それらをエンドポイントに転送できます。

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

これにより、Webhook署名シークレットも生成されます。

> Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (^C to quit)

エンドポイントを登録するには settings.pyファイルにシークレットを追加します。

settings.py

STRIPE_ENDPOINT_SECRET = '<your webhook signing secret here>'

wesecからはじまる数字をコピーして貼り付けしてください。


SStripeは、イベントをエンドポイントに転送します。テストするには、テスト支払いを実行します。
カードNoは4242 4242 4242 4242を入力します。

コマンドラインにstatusの数字が並び、すべて200ならOK、500があると、その通信イベントで失敗があったということになります。


ターミナルにjust subscribed.のメッセージが表示されます。


adminの画面に遷移するときちんとcustomerIDとsubscriptionIDが入っていますね。

あとは.html上に商品を設定し、サブスクの状態を表示します。


html上に商品ステータスを表示する


サブスクリプションを情報をもってログイン


templates/subscription/home.html


{% extends "subscription/base.html" %}

{% block main %}
<style type="text/css">
    .card {
    padding: 0.5em 1em;
    margin: 2em 0;
    color: #00BCD4;
    background: #e4fcff;
    border-top: solid 6px #1dc1d6;
    box-shadow: 0 3px 4px rgba(0, 0, 0, 0.32);

</style>

<h3>サブスクページ</h3>
<p>{{ user }}さん、こんにちは!</p>

  {% if subscription.status == "active" %}
  <div class="container mt-5">
  <h6>{{ user }}様のステータス:</h6>
    <div class="card" style="width: 18rem;">
      <div class="card-body">
        <h5 class="card-title">{{ product.name }}</h5>
        <p class="card-text">
          {{ product.description }}
        </p>
      </div>
       

      {% else %}

      <button type="submit" class="btn btn-primary" id="submitBtn">特別会員になる</button>
      <p><a href="{% url 'logout' %}">ログアウト</a></p>
      {% endif %}
    </div>
{% endblock %}

  </body>
</html>



サブスクリプションのステータス表示

  {% if subscription.status == "active" %}

subscriptionのステータスをアクセスしています。
後述しますが、jsonデータを取得して、さらにstatusキーを取得し、active=trueの状態だと、ログイン時にサブスクリプションが表示されるようになります。


Indexviewに追加

subscription/views.py

"""省略"""

#-------index_view-----------
@method_decorator(login_required,name="dispatch")
class IndexView(TemplateView):
    template_name = "subscription/home.html"
    model = Stripe_Customer

    def get_context_data(self, **kwargs):
        stripe.api_key = settings.STRIPE_SECRET_KEY

        customer = Stripe_Customer.objects.filter(user = self.request.user).first()
        print(f'{customer}'"←現在のログインユーザー")
        client_reference_id=self.request.user.id
        print(f'{client_reference_id}'"←現在のログインID")
        subscription = stripe.Subscription.retrieve(customer.stripeSubscriptionId)
        print("サブスクリプションのjsonデータ")
        print(subscription)
        product = stripe.Product.retrieve(subscription.plan.product)
 

        #htmlに表示
        context = {
            "subscription": subscription,
            "product": product,
        }
        return context


ログイン中のユーザー=顧客情報を取得する


        customer = Stripe_Customer.objects.filter(user = self.request.user).first()
        print(f'{customer}'"←現在のログインユーザー")

(user = self.request.user)で取得します。selfでインスタンス化しています。
コンソール上にログインユーザーが表示されます。

        client_reference_id=self.request.user.id
        print(f'{client_reference_id}'"←現在のログインID")

現在のログイン中のユーザーIDを取得しています。
コンソール上にログインIDが表示されます。
※ここの部分は特にメソッドに関係してありませんが、整合性を取るために取得しています。
try ifでclient_reference_id=self.request.user.idとしてユニーク情報に間違いないかチェックしてもいいかもしれません。

サブスクリプションのデータを取得する

        subscription = stripe.Subscription.retrieve(customer.stripeSubscriptionId)
        print("サブスクリプションのjsonデータ")
        print(subscription)

Stripe上にあるサブスクリプションのデータを取得します。
コンソール上にサブスクリプションのデータが表示されます。


サブスクリプションのjsonデータ
{
~省略~
 "id": "sub_XXXXXXX",
 }

キー"id"から"sub_XXXXXXXXXX"取得しています。

製品データを取得する

     product = stripe.Product.retrieve(subscription.plan.product)

  "plan": {
          "product": "prod_XXXXXX",
}

製品データを"plan","product"で取得します。

サブスクリプションのデータを表示する

subscription/home.html

        <h5 class="card-title">{{ product.name }}</h5>
        <p class="card-text">
          {{ product.description }}

productからさらに、nameで製品名、product.discriptionで製品の説明を取得しています。

これで、サブスクリプションに属しているか否かを分けます。


何か間違いや指摘ありましたらコメントを、
この記事がよかったと思ったらスキ、シェアをお願いします。



宣伝

プログラミング開発を承っています!
したいことを形にいたします。ぜひのぞいてみてください。


参考にしたサイト・URL:
MENTAでお世話になりました。
てつのすけさん
https://menta.work/user/24564

http://harmonizedai.com/article/django%E3%81%A7stripe%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F%E6%9C%88%E9%A1%8D%E8%AA%B2%E9%87%91%E6%96%B9%E6%B3%95/

https://kuma-server.com/create-save/
https://testdriven.io/blog/django-stripe-subscriptions/
https://testdriven.io/blog/flask-stripe-subscriptions/#authentication
https://zenn.dev/var/articles/65785548e340fc

この記事が参加している募集

ご覧いただきありがとうございました。 サポートしていただいたお金は開発費にかけさせていただきます。