webサービスにpythonで、stripeの定期支払い機能を追加する
今回の記事では、jsなどを使わず、pythonとwebhookを使って、Djangoで作ったwebサービスにstripeの定期商品を組み込み、Django側のモデルにデータを保存する方法について紹介します。
stripeの設定
stripeに登録します。
テスト環境モードにしておき、定期商品をつくっておきます。
Django側の設定
pipfileにstripeを追加します。
pipenv install stripe
pipenv lock -r > requirements.txt
settings.pyでstripeを使えるようにします。
本番環境では、先程heokuに設定した値を読み込んでくれるように設定します。
settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'whitenoise.runserver_nostatic',
'django.contrib.staticfiles',
'cloudinary',
# 3rd Party
'crispy_forms',
'social_django',
'stripe',
...
try:
from .local_settings import *
except ImportError:
pass
if not DEBUG:
STRIPE_SECRET_KEY = os.environ['STRIPE_SECRET_KEY']
STRIPE_PUBLISHABLE_KEY = os.environ['STRIPE_PUBLISHABLE_KEY']
ローカル環境では、直接値を読み込むようにします。
stripeにダッシュボードからキーを持ってきて、記載します。
このlocal_settingsはgitにあげないように注意してください。
config/local_settings.py
# テスト
STRIPE_SECRET_KEY = 'xxxxxxxxxxxxxxxxxx'
STRIPE_PUBLISHABLE_KEY = 'xxxxxxxxxxxxxxxxxx'
Djangoでstripeの商品を設定する
モデルにstripeで取引したデータを管理できるOrderというテーブルを作ります。
カラムは以下のようにしました。
- 定期購入をしたユーザー名
- stripeのsession(解約などするときに使います。)
- created_at
- deleted_date(解約した際に、サブスクリプションが切れるタイミングを保存します)
models.py
class Order(models.Model):
user = models.ForeignKey(
User, on_delete=models.CASCADE, null=True, blank=True
)
stripe = models.CharField(verbose_name='Stripe Session', max_length=100)
created_at = models.DateTimeField(auto_now_add=True)
deleted_date = models.DateField(null=True, blank=True)
続いて、商品をDjango上のHTMLから定期商品を購入できるようにします。
<div class="text-center my-5">
<br>下の定期プランにお申し込み下さい。</small>
</div>
<form action="{% url 'create_checkout_session' %}" method="POST">
<input type="hidden" name="priceId" value="price_xxxxxxxxxxxxxxx" />
<button class="btn btn-outline-info btn-lg" type="submit">無制限試着プランに申し込む</button>
</form>
formタグの中に、name="priceId" value="price_xxxxxxxxxxxxxxx"のinputタグを作成します。
inputタグのprice_xxxxxxxxxxxxxxxは、実際にstripeで作った商品の詳細に飛んで、価格コードの部分をコピーします。
続いて、views.pyに実際に決済をし、そのデータを保存する処理を書いていきます。
stripe.api_key = settings.STRIPE_SECRET_KEY
@csrf_exempt
@require_POST
def create_checkout_session(request):
customer = request.user
price_id = request.POST.get('priceId')
session = stripe.checkout.Session.create(
payment_method_types=['card'],
line_items=[{
'price': price_id,
'quantity': 1,
}],
mode='subscription',
success_url=request.scheme + '://' +
request.get_host() + reverse('checkout_success'),
cancel_url=request.scheme + '://' + request.get_host() + reverse('user_info'),
metadata={
'customer_id': customer.id,
'meta_flag': 'create',
}
)
return redirect(session.url)
def checkout_success(request):
return render(request, 'checkout_success.html')
price_idはinputタグから送られてきた値です。
stripe.checkout.Session.createで、注文内容を作ります。
注文作成後は、checkout_successに飛ぶようにします。
事前にcheckout_success.htmlを作成しておいてください。
metadataの部分は、webhookでdjangoで作ったモデルにデータを入れるのですが、そのときに使うデータを書いておきます。
userのidを保存したいので、user_idと、注文を作成したというフラグをもたせたいので、meta_flagというものを作ります。
webhookで、Djangoにstripeの注文データを保存する
create_checkout_sessionが行われたら、Djangoのモデルに購入されたデータを保存するというふうにしたいと思います。
今回はwebhookを使って、その処理を行っていきたいと思います。
まず、stripe-cliをダウンロードしてください。
stripe cliはstripeが正常に行われているかをテストする仕組みです。ダウンロード後、下記のコマンドをうち、CLI でイベント転送を設定し、すべての Stripe イベントをローカルの Webhook エンドポイントに送信するようにします。
これで、イベントが成功したかどうかを確認することができます。
$ stripe listen --forward-to localhost:8000/webhook
⣽ Getting ready... > Ready! You are using Stripe API Version [2019-03-14]. Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (^C to quit)
Your webhook signing secret isからの文字を、settings.pyに書き込んでください。
settings.py
ENDPOINT_SECRET = 'whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
views.py
endpoint_secret = settings.ENDPOINT_SECRET
@csrf_exempt
def checkout_success_webhook(request):
payload = request.body
sig_header = request.headers.get('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):
print(session['metadata'])
order = Order.objects.create(
user=User.objects.get(id=session['metadata']['customer_id']),
stripe=session['id']
)
print("Fulfilling order")
urls.py
path('webhook', views.checkout_success_webhook,
name='checkout_success_webhook'),
create_checkout_session
で、注文が行われたとき、event['type'] == 'checkout.session.completed'
が動くので、fulfill_order
の処理が行われるようになります。
fulfill_order
では、ユーザー情報、stripeのsessionを保存します。
コマンドラインでも確認できます。
2022-03-07 20:56:59 --> charge.succeeded [evt_3KafBNLXC6TXiqu51xy1q8l0]
2022-03-07 20:56:59 <-- [200] POST http://localhost:8000/webhook [evt_3KafBNLXC6TXiqu51xy1q8l0]
2022-03-07 20:56:59 --> checkout.session.completed [evt_1KafBPLXC6TXiqu5lP0JvLDS]
2022-03-07 20:56:59 <-- [200] POST http://localhost:8000/webhook [evt_1KafBPLXC6TXiqu5lP0JvLDS]
2022-03-07 20:56:59 --> payment_method.attached [evt_1KafBPLXC6TXiqu5OO4O09Ct]
2022-03-07 20:56:59 <-- [200] POST htt
これで、正常にstripeの取引が行われ、djangoのデータも保存されました!
定期商品の解約する機能を追加する
定期商品なので、購入ができても、解約ができなければクレームが来そうです。
そのため、解約できるようにします。
views.pyでorderのsubscription_idを持ってきます。
subscription_id = stripe.checkout.Session.retrieve(order.stripe)[
'subscription']
params['subscription_id'] = subscription_id
その値を、htmlでも表示します。
<form action="{% url 'stop_subscription_session' %}" method="POST">
<input type="hidden" name="subscriptionId" value="{{ subscription_id }}" />
<button class="btn btn-outline-info btn-lg" type="submit">定期利用をキャンセルする</button>
</form>
定期利用をキャンセルするを押すと、キャンセルできるように、views.pyで処理を書いていきます。
views.py
@csrf_exempt
@require_POST
def stop_subscription_session(request):
customer = request.user
subscriptionId = request.POST.get('subscriptionId')
session = stripe.Subscription.modify(
subscriptionId,
cancel_at_period_end=True,
metadata={
'customer_id': customer.id,
'meta_flag': 'cancel_scheduled',
}
)
return redirect('stop_success')
基本的に流れは注文時と同じです。
cancel_at_period_end
とつけると、例えば定期の更新日が3/9で、キャンセル日が3/1だとすると、すぐにキャンセルにならず、3/9でキャンセルになるようになります。
続いて、webhookの処理を書いていきます。
@csrf_exempt
def checkout_success_webhook(request):
payload = request.body
sig_header = request.headers.get('stripe-signature')
event = None
....
if event['type'] == 'customer.subscription.updated':
session = event['data']['object']
cancel_order(session)
# Passed signature verification
return HttpResponse(status=200)
def cancel_order(session):
print(session['metadata'])
if session['metadata'] and session['metadata']['meta_flag'] == 'cancel_scheduled':
order = Order.objects.filter(user=User.objects.get(
id=session['metadata']['customer_id']), deleted_date=None).first()
order.stripe = session['id']
today = make_aware(datetime.now())
ordered_at = order.created_at
if date(today.year, today.month, ordered_at.day) > date(today.year, today.month, today.day):
o_deleted_date = date(today.year, today.month, ordered_at.day)
else:
o_deleted_date = date(today.year, today.month,
ordered_at.day) + relativedelta(months=1)
order.deleted_date = o_deleted_date
order.save()
cancel_orderで、orderモデルの内容を変更します。
注意する点として、customer.subscription.updated'
は、注文作成時にも呼ばれてしまいます。
そのため、session['metadata']['meta_flag']
にcancel_scheduled
という、キャンセルボタンを押したときのみ付与するデータを定義しています。
このおかげで、cancel_order
の処理を、注文時にも行わないようにしています。
orderに、解約日を入れて保存します。
django側で、解約日までは、定期サービスが使え、解約後には使えないようにする処理を書けば、stripeの解約とwebサービスとの連携も取れると思います。
参考
Discussion