![見出し画像](https://assets.st-note.com/production/uploads/images/74159288/rectangle_large_type_2_25d807c9c8dfc1e4900d8eec6a4ad381.png?width=1200)
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
![](https://assets.st-note.com/img/1645017525910-4WQ5fz8J1g.png?width=1200)
![](https://assets.st-note.com/img/1645017562930-6rYaKN4VOL.png)
Stripeをインストール
pip install stripe
stripe_keyの取得をし、setiing.pyに追加します。
stripeのアカウント作成はこちら↓ (公式)
アカウントを作成したらダッシュボードに入り、テストモードにします。
![](https://assets.st-note.com/img/1645016094591-1228Buc6eb.png?width=1200)
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)
![](https://assets.st-note.com/img/1645017107352-s5KdZVK5Vf.png?width=1200)
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 :勝手に生成される)ができます。
![](https://assets.st-note.com/img/1645187088289-cq1L87Yd0P.png)
Enterキーを押すとブラウザーが開かれ、アクセス許可を求められるので、許可します。
![](https://assets.st-note.com/img/1645187234169-Smzx8fb6dL.png?width=1200)
許可をすると下記の文章が出ます。
> 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があると、その通信イベントで失敗があったということになります。
![](https://assets.st-note.com/img/1646954113139-pRGpF0mt1h.png?width=1200)
ターミナルにjust subscribed.のメッセージが表示されます。
adminの画面に遷移するときちんとcustomerIDとsubscriptionIDが入っていますね。
![](https://assets.st-note.com/img/1646953785358-6QE2PAGOnm.png?width=1200)
あとは.html上に商品を設定し、サブスクの状態を表示します。
html上に商品ステータスを表示する
![](https://assets.st-note.com/img/1647078157130-YqaNRCUDB7.png?width=1200)
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
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
この記事が参加している募集
ご覧いただきありがとうございました。 サポートしていただいたお金は開発費にかけさせていただきます。