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
``` 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で作った商品の詳細に飛んで、価格コードの部分をコピーします。

![](https://storage.googleapis.com/zenn-user-upload/7baaf01175a1-20220312.png)

続いて、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