0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

webサービスにpythonで、stripeの定期支払い機能を追加する

Last updated at Posted at 2022-03-12

今回の記事では、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

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

config/local_settings.py
# テスト
STRIPE_SECRET_KEY = 'xxxxxxxxxxxxxxxxxx'
STRIPE_PUBLISHABLE_KEY = 'xxxxxxxxxxxxxxxxxx'

Djangoでstripeの商品を設定する

モデルにstripeで取引したデータを管理できるOrderというテーブルを作ります。
カラムは以下のようにしました。

  • 定期購入をしたユーザー名
  • stripeのsession(解約などするときに使います。)
  • created_at
  • deleted_date(解約した際に、サブスクリプションが切れるタイミングを保存します)

models.py

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をダウンロードしてください。
https://stripe.com/docs/stripe-cli#install
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

settings.py
ENDPOINT_SECRET = 'whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

views.py

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

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を持ってきます。

views.py
        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

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サービスとの連携も取れると思います。

参考

https://stripe.com/docs/payments/checkout/fulfill-orders
https://stripe.com/docs/billing/subscriptions/cancel

0
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?