StripeとFastAPIとvue.jsによる決済できるウェブアプリの実装。テスト環境で動かす

この記事は**Stripe**によって決済するウェブアプリをゼロから作るサンプルを説明します。フロントエンドはTypeScriptのvue.jsで、バックエンドはPythonのFastAPIで書きます。

vue.jsとFastAPIを使っている読者の場合はすぐここに載っている手順で実行できるはずですが、そうでない場合でもStripeを実装する大体の流れの参考になれるでしょう。


# ここでやることと目標

## Stripeとは

**Stripe**（ストライプ）とはウェブアプリ上で決済を実装するためのサービスです。ネットでお金のやり取りをする時によく使われて、ウェブエンジニアの間で結構人気です。

Stripe決済をウェブサイトに入れるには普段フロントエンド側もバックエンド側もコードを書く必要があって、かなり複雑でわかりにくくて挫折してしまいそうです。ただStripeのいいところはテスト環境で開発できることだと思います。

## テスト環境

お金に関わることなので、開発の時に気軽に本番のクレジットカードや決済の情報を使うわけにはいきませんよね。だから普段はテスト環境で本番に真似する擬似的な支払いをすることができると開発しやすいです。

それに本番環境を使うには自分や会社の決済情報を入力する必要があって、試してみたくてもすぐにはできないが、テスト環境はただStripeのアカウントを登録したらすぐ使えます。だからStripeを使うかどうか迷っている人もまず簡単に試して上手くいくかどうか確認することができます。

## 作ってみるウェブアプリ

今回の記事では全体の流れを勉強するために、テスト環境で動く簡単なウェブアプリを実装するコードを載せて解説します。

支払い方法としては、1回ごと支払ったら終了するものと、クレジットカード登録したら定期的に払い続けることになる「**サブスクリプション**」（subscription）、長いのでここでは略して「**サブスク**」、という方法に大きく分けられています。やり方は少し違うので、両方の使い方を勉強するために今回はどっちも実装することにします。

纏めてみるとこんな感じのウェブアプリにします。
- ユーザーログインと新規登録の画面がある
- 登録するにはクレジットカード情報も入れてサブスクする必要がある
- 登録した後、ガチャを回すメイン画面に入る
- ガチャを回すのにまず課金する必要がある
- サブスクと支払いの情報はStripeに登録されるから再度ログインする時に確認できる

このように、支払いしかないウェブアプリです。本番なら中身が何かいいサービスがあるはずですが、今回は支払いの勉強するためだけなので支払いに関する部分しか殆ど触れません。だからこんな無駄なウェブアプリになります。

それに、ユーザー登録は普段Stripeとは別としてバックエンドで作成しておいたデータベースやAWSのCognitoなどのサービスを使った方がいいのですが、今回はStripe重視なので、全部省略してユーザー情報の記録もStripeだけ済ませることにします。パスワードも使わずに電子メールだけでユーザー登録と認証を行います。

又、ウェブデザインとかも重視しません。クレジットカード入力とユーザー情報の入力と表示が正しければそれでいいです。簡単にわかりやすい地味な画面にします。

フロントエンドもバックエンドも、一般に使っている言語とフレームワークはどれも実装できますが、今回は私がいつも使っている馴染みの**FastAPI**（バック）と**vue.js**（フロント）を採用します。

以前もこの組み合わせで書いた簡単なウェブアプリのサンプルを載せた記事があったので、これも読んでいただけたらと思います。

https://qiita.com/phyblas/items/d25315a2fdab744117ae

又、vue.jsだけで書いたCognito実装の記事もあります。CognitoもよくStripeと組み合わせられるので、興味があればこの記事も一緒に読んだいただけたらと思います。

https://qiita.com/phyblas/items/0005ccafe19bce5754d8

## 開発の大体の流れ

次はこのような順番で解説していきます。
- 使うパソコンの環境設定を整えておく
- [Stripeのウェブサイト](https://stripe.com/jp)にアカウント登録する
- テスト用の「公開可能キー」と「シークレットキー」を取得する
- 「サブスク」に使う「商品」と「プラン」を登録する
- バックエンドでStripeのサブスク登録のためのコードを書く
- フロントエンドでクレジットカード情報を登録する画面を作る
- 上手く動くかテストして確認する
- 問題がなければ次は本番の実装（*この記事ではそこまで説明しません*）


# 環境設定

## pythonライブラリーのインストール

バックエンドの方はFastAPIを使うのでまずはFastAPIと関連のライブラリーをインストールします。
```
pip install fastapi uvicorn python-multipart
```

Stripeを使うためにPythonではstripeというライブラリーがあります。これもインストールする必要があります。
```
pip install stripe
```

## vueプロジェクト作成

次はプロジェクトの作成です。vuetifyを使うことにします。yarnで実行します。
```
yarn create vuetify
```

```
yarn create v1.22.22
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Installed "create-vuetify@2.2.6" with binaries:
      - create-vuetify
[##########] 10/10
Vuetify.js - Material Component Framework for Vue

✔ Project name: … fastrivue
✔ Which preset would you like to install? › Barebones (Only Vue & Vuetify)
✔ Use TypeScript? … No / Yes
✔ Would you like to install dependencies with yarn, npm, pnpm, or bun? › yarn
✔ Install Dependencies? … No / Yes

◌ Generating scaffold...
◌ Installing dependencies with yarn...

yarn install v1.22.22
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
✨  Done in 16.99s.

fastrivue has been generated at /Users/phyblas/Library/CloudStorage/Dropbox/web/fastrivue

Discord community: https://community.vuetifyjs.com
Github: https://github.com/vuetifyjs/vuetify
Support Vuetify: https://github.com/sponsors/johnleider
✨  Done in 100.72s.
```

作成の途中で出てくる選択肢はこのように選びます。
```
✔ Barebones (Only Vue & Vuetify)
✔ Yes
✔ yarn
✔ Yes
```

プロジェクト名は「fastrivue」にします。fastapi+stripe+vueを合わせた適当な名前です。

作成したプロジェクトフォルダに入って必要なパッケージを追加します。
```
cd fastrivue
```

今回追加する必要があるのは2つだけ。まず必要なのはバックエンド側に接続するためのaxiosです。
```
yarn add axios
```

```
yarn add v1.22.22
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

success Saved lockfile.
success Saved 9 new dependencies.
info Direct dependencies
└─ axios@1.7.5
info All dependencies
├─ asynckit@0.4.0
├─ axios@1.7.5
├─ combined-stream@1.0.8
├─ delayed-stream@1.0.0
├─ follow-redirects@1.15.6
├─ form-data@4.0.0
├─ mime-db@1.52.0
├─ mime-types@2.1.35
└─ proxy-from-env@1.1.0
✨  Done in 1.21s.
```

もう一つは今回の主役である、stripeに接続するパッケージです。
```
yarn add @stripe/stripe-js
```

```
yarn add v1.22.22
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
└─ @stripe/stripe-js@4.3.0
info All dependencies
└─ @stripe/stripe-js@4.3.0
✨  Done in 2.16s.
```

なお、紛らわしいので一応説明しておきますが、これは間違いです。
```
yarn add stripe
```
これだと[別のパッケージ](https://github.com/stripe/stripe-node)になります。このパッケージはバックエンドとしてJavaScript（又はTypeScript）を使うためのパッケージです。

混同しやすいが、今回はフロントエンドとして使うだけで、バックエンドはPythonのFastAPIを使うので、このパッケージは要りません。


## 全体の構造

作成したばかりのfastrivueプロジェクトフォルダ内は最初から色んなファイルが入っているはずですが、特に変更する必要がありません。

書き直すのは「src/App.vue」だけで、その他にこれらを書いて追加します。
- web.py
- src/components/login.vue
- src/components/subsc.vue
- src/components/gacha.vue

「web.py」はバックエンドの実装のコードです。今回は纏めて一つだけのファイルにします。

src/componentsの中はそれぞれのウェブアプリを織り成す部品です。

そうしたらこのような構造になります。
```
├──fastrivue
│   ├── web.py
│   ├── src
│   │   ├── App.vue
│   │   ├── components
│   │   │   ├── login.vue
│   │   │   ├── subsc.vue
│   │   │   └── gacha.vue
│   │   └── (その他）
│   ├── dist
│   └── (その他）

```

実際に書き終わった後のフォルダ。

![qsfv14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/4ee3cce4-e772-f00f-68fd-b82f1a35ada0.png)


# テスト環境で始める

## Stripeアカウント登録

まずはstripeのウェブサイトに入って新規アカウント登録を行います。

https://dashboard.stripe.com/register

登録した後自分の決済などの情報を入力してと言われますが、今すぐする必要がないのです。テスト環境で始めたい場合はこれを省略してダッシュボードの画面に入ることができます。そうしたら「**テスト環境**」という設定になっています。

ダッシュボードのメイン画面の右側に「**公開可能キー**」と「**シークレットキー**」が搭載されています。これをコピーしてすぐ使えるようになります。

![qsfv01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/698a01d0-1836-1516-822d-586154dcb1e3.png)

**公開可能キー**は**フロントエンド側**に使うコードです。「pk_」から始めます。

**シークレットキー**又は**APIキー**は**バックエンド側**に使うコードです。「sk_」から始めます。

なんで2つも必要かというと、恐らく秘密かどうかの違いでしょう。シークレットキーが必要となるのは決済情報を更新したり削除したりする操作なので、フロントエンド側に流して晒されることは許されないでしょう。それに対し公開可能キーが必要とするフロントエンド側の操作はクレジットカード登録など顧客本人に関わる機能に限られています。だからこういう使い分けをしているでしょう。


## 商品とプランの作成

サブスクを行うためにはまず商品と支払いのプランを定義しておく必要があります。これはStripeのダッシュボードで行うこともできますが、APIを通じて操作することもできます。

バックエンドを書く時もこのAPIを使って顧客やサブスクを作成することになるので、最初から商品とプランもAPIで作成しておいた方がしっくり来ると思うので、今回はAPIで行います。

まず商品の定義はPythonでこのように書いて実行します。（ここにメモしておいた**APIキー**を入れる必要があります）
```python
import stripe

stripe.api_key = 'sk_test_*****'
product = stripe.Product.create(name='あぷり')
print(product)
```

これで商品が作成されて、このような情報が見られます。
```json
{
  "active": true,
  "attributes": [],
  "created": 1724675940,
  "default_price": null,
  "description": null,
  "id": "prod_QjVCnjOMg5q4QD",
  "images": [],
  "livemode": false,
  "marketing_features": [],
  "metadata": {},
  "name": "\u3042\u3077\u308a",
  "object": "product",
  "package_dimensions": null,
  "shippable": null,
  "statement_descriptor": null,
  "tax_code": null,
  "type": "service",
  "unit_label": null,
  "updated": 1724675940,
  "url": null
}
```

商品を定義する時に色んな情報を入れることができますが、実際に必要なのは「名前」（name）だけです。

「prod_」から始める商品のIDは自動的に発行されて、これは次に使う必要があるので、控えておきましょう。

APIで作成した後、ダッシュボードの方で見たらその商品は「商品カタログ」に表示されることになっています。これでちゃんと繋がっているとわかりますね。

![qsfv02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/11466b69-80eb-2ac7-5936-827b3175ab2a.png)

ただしまだ値段設定はしていないので、今は「価格がありません」と表示されていますね。

次はプランの定義です。これもAPIを通じて行えます。
```python
import stripe

stripe.api_key = 'sk_test_*****'
plan = stripe.Plan.create(
    product='prod_QjVCnjOMg5q4QD',
    amount=1024,
    currency='jpy',
    interval='month',
    nickname='てきとうぷらん'
)
print(plan)
```
実行の結果。
```json
{
  "active": true,
  "aggregate_usage": null,
  "amount": 1024,
  "amount_decimal": "1024",
  "billing_scheme": "per_unit",
  "created": 1724676082,
  "currency": "jpy",
  "id": "plan_QjVFDfaACOyJJY",
  "interval": "month",
  "interval_count": 1,
  "livemode": false,
  "metadata": {},
  "meter": null,
  "nickname": "\u3066\u304d\u3068\u3046\u3077\u3089\u3093",
  "object": "plan",
  "product": "prod_QjVCnjOMg5q4QD",
  "tiers_mode": null,
  "transform_usage": null,
  "trial_period_days": null,
  "usage_type": "licensed"
}
```

そうしたらもう一度商品カタログを見たら、入れた通りの価格は表示されます。

![qsfv03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/b56bd10e-663f-fa40-efb5-0d12f95f11d7.png)

クリックして商品の詳細を読んでみたら、作成したプランの情報も入っています。

![qsfv04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/9149ebb0-117a-cb6b-595c-e4a0db7ca93c.png)

プランの定義に商品のidと支払い方式などの情報が必要です。`product`は先程作成した商品のidで、`amount`は支払う料金で、`currency`は金貨の種類で、`interval`は`day`、`week`、`month`、`year`などどれくらいの間隔で支払いするを決めるのです。`nickname`はなくてもいいのでここでは一応適当につけました。

尚、商品とプランを予め定義する必要があるのはサブスクで支払う場合だけです。一回きり支払いの場合は特に必要ありません。


# バックエンドでの実装

アカウント作成と商品とプランの定義が終わったら次は早速実装です。まずはバックエンドの方。これはPythonフレームワークであるFastAPIで書きます。そこまで長くはないので、一つだけのファイルに纏めます。

```python:web.py
from fastapi import FastAPI,Request,HTTPException
from fastapi.staticfiles import StaticFiles
import stripe

# Stripeのウェブサイトから取得できたシークレットキー
stripe.api_key = 'sk_test_*****'
# 予め定義しておいた商品のプランのID
plan_id = 'plan_QjVFDfaACOyJJY'

app = FastAPI()

# サブスクを行うAPI
@app.post('/subsc_okonau')
async def subsc_okonau(request: Request):
    try:
        req = await request.json() # ウェブサイトから送られたユーザーとクレジットカードの情報
        email = req['email']
        name = req['name']
        pm_id = req['pm_id'] # 支払い方法、つまりクレジットカードのID
        
        # この電子メールが既に登録されているかどうかの確認
        customer_query = stripe.Customer.search(query=f'email: "{email}"')
        if(customer_query['data']):
            raise HTTPException(status_code=403,detail='この電子メールは既に使われているのだ。')
        
        # 顧客作成
        customer = stripe.Customer.create(
            email=email,
            name=name
        )
        
        # 支払い方法を添付する
        try:
            pm = stripe.PaymentMethod.attach(
                payment_method=pm_id,
                customer=customer.id
            )
        except Exception as err:
            # カードに問題があって添付できない場合はここでエラーが出る
            stripe.Customer.delete(customer.id)
            raise HTTPException(status_code=403,detail=str(err))
        
        # この顧客と支払い方法でサブスクを作成する
        subsc = stripe.Subscription.create(
            customer=customer.id,
            items=[{'plan': plan_id, 'quantity': 1}],
            default_payment_method=pm_id
        )
        
        # サブスクが成功かどうかを確認。失敗したらエラーを出して終了
        if(subsc['status']!='active'):
            stripe.Customer.delete(customer.id)
            raise HTTPException(status_code=403,detail='何か間違いが発生してサブスクは完成できないらしいのだ。')
        
        # サブスクが成功した後、サブスクのIDも顧客情報のメタデータの中に入れて更新する
        customer = stripe.Customer.modify(
            customer.id,
            invoice_settings={'default_payment_method': pm_id},
            metadata={'kakin': 0, 'subsc_id': subsc.id},
        )
        
        # 顧客情報をブラウザの方へ返す
        return customer
    except Exception as err:
        raise HTTPException(status_code=500,detail='エラー：'+str(err))

# ログインするAPI
@app.post('/login_suru')
async def login_suru(request: Request):
    try:
        req = await request.json()
        email = req['email']
        # この電子メールを使っている顧客が登録されているかどうか確認
        customer_query = stripe.Customer.search(query=f'email: "{email}"')
        if(not customer_query['data']):
            raise HTTPException(status_code=403,detail='この電子メールはまだ登録されていないのだ。')
        
        # 顧客情報を取得する
        customer = customer_query['data'][0]
        # サブスク情報を取得する
        subscr = stripe.Subscription.retrieve(customer['metadata']['subsc_id'])
        # サブスクはまだ有効かどうか確認
        if(subscr['status']!='active'):
            stripe.Customer.delete(customer.id)
            raise HTTPException(status_code=403,detail='サブスクはもう無効になっているので、もう一度サブスクするのだ。')
        
        # 問題なければログイン成功。顧客情報をブラウザ側へ返す
        return customer
    except Exception as err:
        raise HTTPException(status_code=500,detail='エラー：'+str(err))

# 課金を行うAPI
@app.post('/kakin_okonau')
async def kakin_okonau(request: Request):
    try:
        req = await request.json()
        ryoukin = int(req['ryoukin']) # 課金する料金
        cid = req['cid'] # 顧客のID
        # 顧客情報取得
        customer = stripe.Customer.retrieve(cid)
        # 一度きりの支払いをする。支払い方法はサブスクの時に登録したのと同じものを流用する
        payment_intent = stripe.PaymentIntent.create(
            amount=ryoukin,
            currency='jpy',
            payment_method=customer.invoice_settings['default_payment_method'],
            customer=cid,
            confirm=True,
            automatic_payment_methods={'enabled': True, 'allow_redirects': 'never'}
        )
        # 支払いがちゃんと成功かどうか確認
        if(payment_intent['status']!='succeeded'):
            raise HTTPException(status_code=403,detail='何か間違いが発生して課金は完成できないらしいのだ。')
        
        # 支払いが成功したから、メタデータの中にある課金の数値も更新する
        customer['metadata']['kakin'] = int(customer['metadata']['kakin']) + ryoukin
        customer = stripe.Customer.modify(cid,metadata=customer['metadata'])
        return customer
    except Exception as err:
        raise HTTPException(status_code=500,detail='エラー：'+str(err))

# ガチャを回す、つまり消費するだけのAPI
@app.post('/gacha_mawasu')
async def gacha_mawasu(request: Request):
    try:
        req = await request.json()
        cid = req['cid']
        customer = stripe.Customer.retrieve(cid)
        # 課金の数値から引くだけ
        kakin = int(customer['metadata']['kakin']) - 300
        if(kakin<0): # 課金したお金が足りなければエラー
            raise HTTPException(status_code=403,detail='お金は足りないのだ。')
        
        # メタデータの中の課金を更新する
        customer['metadata']['kakin'] = kakin
        customer = stripe.Customer.modify(cid,metadata=customer['metadata'])
        return customer
    except Exception as err:
        raise HTTPException(status_code=500,detail='エラー：'+str(err))

# 退会、つまりユーザーを消すAPI
@app.post('/taikai_suru')
async def taikai_suru(request: Request):
    try:
        req = await request.json()
        cid = req['cid']
        return stripe.Customer.delete(cid)
    except Exception as err:
        raise HTTPException(status_code=500,detail='エラー：'+str(err))
        
# 静的ファイルの定義。vue.jsによってプリコンパイルされる
app.mount('/', StaticFiles(directory='dist',html=True))
```

バックエンドのコードは主にStripeに接続するためのAPIだけです。Stripeとの接続は、クレジットカード情報登録以外全部バックエンドでやっています。

クレジットカード情報の処理だけがフロントエンド側で行われるという仕組みは、「顧客のクレジットカード情報がサーバーに流れないように」配慮されているためでしょう。

サーバーで処理される「支払い方法登録」はクレジットカード情報ではなく、フロントエンド側から登録された「支払い方法のID」だけとなっています。

「顧客登録」と「支払い方法登録」と「サブスク登録」は本来別々やってもいいですが、今回は登録したらすぐサブスクさせるということにしています。だから長い一連の処理になりますが、わかりやすいように各段階の説明を記述しています。

今回はサブスクがメインではありますが、1回きりの支払い、つまり課金の処理も入れています。サブスクは`Subscription`クラスで行われるのに対し、課金は`PaymentIntent`クラスで行われます。今回ではサブスクしてから課金できるというようにしているので、課金の時は支払い情報の準備をする必要がなく、ただサブスクの時に使ったデフォルト支払い方法を流用するという形にしています。

:::note info
古い記事を読んだら支払いに`Charge`というクラスが使われることもあります。`Charge`は`PaymentIntent`と大体似て、同じように支払いに使うこともできますが、`PaymentIntent`の方が新しく搭載された機能で、現在`Charge`の代わりに`PaymentIntent`を使った方がいいと言われているので、今使い始める人にとって`Charge`の存在は無視してもいいです。
:::

バックエンド側のコードだけ読んでも具体的にどういうやり取りをするか想像しにくくて漠然とするでしょう。では次にフロントエンド側の実装に入ります。


# フロントエンドでの実装

フロントエンドの方はTypeScriptのvue.jsで書いて、4つの.vueファイルに分けられます。

## App.vue

メインのファイルです。ここから3つのコンポネントが呼び出されます。ログイン処理の関数はここに定義しています。
```vue
<template>
  <v-app>
    <v-main>
      <v-container>
        <v-card class="text-h5 px-2 py-2 mx-1 my-1">
          ただのつまらないウェブアプリ
        </v-card>
        <login v-if="page == 'ログイン'" @login_suru="login_suru" @subsc="page = 'サブスク'" />
        <subsc v-if="page == 'サブスク'" @login_shita="page = 'ガチャ'" />
        <gacha v-if="page == 'ガチャ'" @logout_suru="logout_suru" />
      </v-container>
    </v-main>
  </v-app>
</template>

<script lang="ts">
import axios from "axios";
const root_url: string = "http://127.0.0.1:8000";

export default {
  data() {
    return {
      page: "" // 表示してるページ
    };
  },
  methods: {
    // APIに接続してログインを行う
    login_suru(email: string) {
      axios.post(root_url + "/login_suru", { email })
        .then((res) => {
          sessionStorage.setItem("customer", JSON.stringify(res.data));
          this.page = "ガチャ";
        })
        .catch((err) => {
          alert(err.response.data.detail);
          this.page = "ログイン";
        });
    },
    // ログアウトする処理
    logout_suru() {
      sessionStorage.clear();
      this.page = "ログイン";
    }
  },
  mounted() {
    // アクセスした時にセッションストレージの中のログイン状態を調べる
    let customer = sessionStorage.getItem("customer");
    if (customer && customer != "null") {
      // 前回からログインしているならログイン状態にする
      this.login_suru(JSON.parse(customer).email);
    }
    else {
      // 既にログイン状態でなければログイン画面へ
      this.page = "ログイン";
    }
  }
};
</script>
```


## components/login.vue

ログイン画面を作るためのコンポネントです。実際にログイン処理の定義はApp.vueの方に書いてあるからこの部分は短めです。
```vue
<template>
  <v-card class="px-2 py-2 mx-1 my-1">
    <div>電子メールを入力してログインするのだ。</div>
    <v-text-field class="px-2 py-1" v-model="email" label="電子メール" hide-details />
    <div class="d-flex align-center mx-2 my-2">
      <v-btn class="mx-2" @click="$emit('login_suru', email)" color="primary">
        ログインする
      </v-btn>
      <v-btn @click="$emit('subsc')" color="secondary">まだ登録していない方はこちら</v-btn>
    </div>
  </v-card>
</template>

<script lang="ts">
export default {
  data() {
    return {
      email: "",
    };
  },
};
</script>
```


## components/subsc.vue

顧客登録からサブスクまでの一連の処理を行う画面を構成するコンポネントです。今回で中心とも言える部分です。ここでクレジットカードの情報を入力してStripeに接続して支払い方法を登録してからサブスクを行います。
```vue
<template>
  <v-card class="px-2 py-2 mx-1 my-1">
    <div>電子メールと名前とクレジットカード情報を入力して登録するのだ。</div>
    <v-text-field class="px-2 py-1" v-model="email" label="電子メール" hide-details />
    <v-text-field class="px-2 py-1" v-model="name" label="名前" hide-details />
    <div class="px-2 py-3 mx-2 my-3" id="card-element"></div>
    <v-btn class="mx-2" @click="subsc_okonau" color="primary">登録する</v-btn>
  </v-card>
</template>

<script lang="ts">
import axios from "axios";
import { loadStripe, Stripe, StripeCardElement } from '@stripe/stripe-js';
// Stripeのダッシュボードから取得しておいた公開可能キー
const pk: string = "pk_test_*****"
const root_url: string = "http://127.0.0.1:8000";

export default {
  data() {
    return {
      stripe: null as null | Stripe,
      card: null as null | StripeCardElement,
      email: "",
      name: "",
    };
  },
  computed: {

  },
  methods: {
    // クレジットカード情報を入力する欄を初期化する
    async start_stripe() {
      this.stripe = await loadStripe(pk);
      if (this.stripe) {
        this.card = this.stripe.elements().create("card");
        this.card.mount("#card-element");
      }
      else {
        alert("Stripeの初期化は失敗！");
      }
    },
    // サブスクを行う
    subsc_okonau() {
      if (!this.email) {
        alert("まだ電子メールを入れていないのだ");
        return;
      }
      if (!this.name) {
        alert("まだ名前を入れていないのだ");
        return;
      }
      if (this.stripe && this.card) {
        // 入力したカードを使って支払い方法を定義する
        this.stripe.createPaymentMethod({ type: "card", card: this.card })
          .then(res => {
            if (!res.error) { // エラーがないか確認
              let data = {
                pm_id: res.paymentMethod.id, // 支払い方法のID
                email: this.email,
                name: this.name,
              }
              // 支払い方法の作成にエラーがなければAPIに接続してサブスクを行う
              axios.post(root_url + "/subsc_okonau", data)
                .then((res) => {
                  alert("サブスク完了");
                  // サブスクが成功したらセッションストレージに保存してログインする
                  sessionStorage.setItem("customer", JSON.stringify(res.data));
                  this.$emit("login_shita");
                })
                .catch((err) => {
                  alert(err.response.data.detail);
                });
            }
            else { // クレジットカード情報の間違いなどによってエラーが出る場合
              alert(res.error.message);
            }
          });
      }
    },
  },
  mounted() {
    // 初めてこの画面に入った時まずクレジットカード情報入力の欄を初期化する
    this.start_stripe();
  }
};
</script>

<style scoped>
.StripeElement {
  border: 2px solid #44aa33;
  background-color: #eefff6;
  border-radius: 5px;
}
</style>
```


## components/gacha.vue

ログインした後現れる画面です。本来この部分は何かをするアプリのメイン部分になるばすですが、今回では特に何も実装していません。ただ課金とガチャごっこをするだけにしています。
```vue
<template>
  <v-card v-if="customer" class="px-2 py-2 mx-1 my-1">
    <div class="mx-2 my-1">名前：{{ customer.name }}</div>
    <div class="mx-2 my-1">電子メール：{{ customer.email }}</div>
    <div class="mx-2 my-1">課金：{{ customer.metadata.kakin }}</div>
    <v-text-field class="px-2 py-1" v-model="ryoukin" label="課金したい料金" hide-details />
    <v-btn class="mx-2" @click="kakin_okonau" color="primary">課金する</v-btn>
    <v-btn class="mx-2" @click="gacha_mawasu" color="warning">ガチャする</v-btn>
    <v-btn class="mx-2" @click="$emit('logout_suru')" color="secondary">ログアウトする</v-btn>
    <v-btn class="mx-2" @click="taikai_suru" color="red">退会する</v-btn>
  </v-card>
</template>

<script lang="ts">
import axios from "axios";
const root_url: string = "http://127.0.0.1:8000";

export default {
  data() {
    return {
      // 顧客の情報
      customer: { id: "", name: "", email: "", metadata: { kakin: 0 } },
      ryoukin: 50, // 支払う料金の欄
    };
  },
  methods: {
    // APIに接続して課金処理を行う
    kakin_okonau() {
      let data = {
        cid: this.customer.id,
        ryoukin: this.ryoukin
      }
      axios.post(root_url + "/kakin_okonau", data)
        .then((res) => {
          alert(this.ryoukin + "円課金完了");
          sessionStorage.setItem("customer", JSON.stringify(res.data));
          this.customer = res.data;
        })
        .catch((err) => {
          alert(err.response.data.detail);
        });

    },
    // ガチャを回す（課金を減らすだけ）
    gacha_mawasu() {
      axios.post(root_url + "/gacha_mawasu", { cid: this.customer.id })
        .then((res) => {
          console.log(res);
          alert("ガチャガチャガチャ！");
          sessionStorage.setItem("customer", JSON.stringify(res.data));
          this.customer = res.data;
        })
        .catch((err) => {
          alert(err.response.data.detail);
        });
    },
    // 顧客を賛助する
    taikai_suru() {
      axios.post(root_url + "/taikai_suru", { cid: this.customer.id })
        .then((res) => {
          console.log(res);
          alert("退会したのだ！");
          this.$emit("logout_suru");
        })
        .catch((err) => {
          alert(err.response.data.detail);
        });
    },
  },
  mounted() {
    // セッションストレージの中の顧客情報を取得する
    let customer = sessionStorage.getItem("customer");
    if (customer && customer != "null") {
      Object.assign(this.customer, JSON.parse(customer));
    }
  }
};
</script>
```


# 実行

## 起動する
全部コードを書き終わったら実行して動きを確認しましょう。

まずフロントエンドの方はプリコンパイルを実行します。
```
yarn run build
```

問題がなければこれで「dist」フォルダにhtmlとJavaScriptが作成されて、これをFastAPIの静的ファイルとして扱われます。

次はバックエンドのサーバーを実行します。
```
uvicorn web:app --reload
```

これで`http://127.0.0.1:8000`のURLでブラウザからアクセスできます。

## アクセスしてみる

アクセスしたらこのようなログイン画面になります。

![qsfv05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/7b7f6367-3ea6-6aa8-716b-6a59a827d812.png)

まず登録するための画面に入ります。

![qsfv06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/c3288427-3924-122b-3cea-84cc007c0975.png)

カードの情報に関しては今テストなのでStripe側から準備しておいたダミーのカード番号を入れて擬似的に支払うことができます。

使えるカード番号はここに書いてあります。

https://docs.stripe.com/testing?locale=ja-JP

今回は日本のVISAで支払うという設定にするので「4000003920000003」を入れます。有効期間は未来の月と年を入れたらいいです。CVCは何を入れても通れます。

![qsfv07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/bfbd5485-859f-d791-7ab4-c452ff4b07c6.png)
（ここで入力した電子メールはただ適当で、実在しません）

因みに、残高不足で支払い拒否されるという設定などのカード番号も準備されています。例えば「4000000000009995」を入れたらそれらしいエラーが実際に出てきます。これで本番と同じような仕様を確認することができますね。

![qsfv08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/839299f1-300e-a141-a48c-679625073e2f.png)

問題ないクレジットカードの情報を入れたらサブスクが完成して、自動的にログインして、この画面に入ります。ただ顧客の情報の表示と少し簡単な動作のボタンだけ準備されています。

![qsfv09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/6bd51bd0-0ec9-67d8-b4ed-d7331ee9dc52.png)

料金を入力して「課金する」を押したら課金が実行されて、「課金：」の右の数値は増えます。

![qsfv10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/8b592b75-521b-9a0f-000c-d6b1d2259955.png)

尚、ここまで来てもしダッシュボードの「顧客」を調べてみたらちゃんと登録されているとわかります。

![qsfv11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/e1ebccdd-027a-a586-19a2-54fb18680bec.png)

そしてクリックして中を見たらサブスクと支払いの歴史も入っているはずです。

![qsfv12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/b2186dc5-0e05-67d8-1114-a6fcf34b45fa.png)

最後に「ガチャする」を押したらただメタデータの中の課金のお金が300円減るだけで、これは支払いとは関係ない処理ですが、一応Stripeに記録されている情報を利用してこのようなこともできます。

![qsfv13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/36588600-2bd7-1c7d-d290-01495ddc3b95.png)

こんな感じで、こうやって支払いを実行できるウェブサイトを作ることができます。

ただし今のはあくまでテスト環境で擬似的な支払いなので、本番を使うためにはちゃんとした決済情報をStripeのウェブサイトに登録して本番用の「公開可能キー」と「シークレットキー」を取得する必要があります。


# APIの纏め

APIの実装で色んなクラスのオブジェクトが関わっているのでここではちょっと登場したものの纏めをしておきます。

<table>
<tr><td style="text-align: center;"><strong>クラス</strong></td><td style="text-align: center;"><strong>意味</strong></td><td style="text-align: center;"><strong>IDの接頭辞</strong></td></tr>
<tr><td>Product</td><td>商品</td><td>prod_</td></tr>
<tr><td>Plan</td><td>プラン</td><td>plan_</td></tr>
<tr><td>Customer</td><td>顧客</td><td>cus_</td></tr>
<tr><td>PaymentMethod</td><td>支払い方法</td><td>pm_</td></tr>
<tr><td>Subscription</td><td>サブスク</td><td>sub_</td></tr>
<tr><td>PaymentIntent</td><td>支払い</td><td>pi_</td></tr>
<tr><td><strike>Charge</strike></td><td><strike>古い版の支払い</strike></td><td><strike>ch_</strike></td></tr>
</table>

又、どのクラスでも同じようなクラスメソッドがあります。これを覚えておくと便利です。

<table>
<tr><td style="text-align: center;"><strong>メソッド</strong></td><td style="text-align: center;"><strong>意味</strong></td></tr>
<tr><td>.create()</td><td>新規作成</td></tr>
<tr><td>.retrieve()</td><td>IDを指定してオブジェクトを取得する</td></tr>
<tr><td>.list()</td><td>全部を羅列する</td></tr>
<tr><td>.search()</td><td>一部の情報によって条件に合うものを検索する</td></tr>
<tr><td>.modify()</td><td>情報更新</td></tr>
<tr><td>.delete()</td><td>削除</td></tr>
</table>


# 参考

Qiitaで今までも大量のStripe関連の記事があります。私もこれらの記事をいっぱい読んで参考にしました。全部読んだ関連の記事のリンクをここに載せておきます。

- [【初心者向け】Stripeでクレカ登録・決済・削除を実装する【Python, Go, Javascript】](https://qiita.com/yk__/items/4486256d0da8b63093bb)
- [AWS Amplify（Cognito User Pools）で、ユーザーがアカウントを作成したタイミングでStripeにCustomerデータを登録する](https://qiita.com/hideokamoto/items/347fd937fc747c54fc8c)
- [stripe決済のためのvueプラグイン「vue-stripe」](https://qiita.com/motoki_tamagawa/items/3f05421e849b564a6612)
- [StripeをVueにシンプルに導入してみた](https://qiita.com/reon777/items/486eefb08e0c94042148)
- [Stripe Billing 101](https://qiita.com/y_toku/items/235b5e7ee00792edcbbf)
- [Stripe.js & Elements を利用して決済フローを理解する](https://qiita.com/y_toku/items/7e51ef7e69d7cbbfb3ca)
- [FastAPI × Stripe サブスクリプション登録を試す](https://qiita.com/ryori0925/items/722cd64045e0142122a5)
- [pythonのstripe sdkでやったことまとめ](https://qiita.com/sugo/items/3659e8baee2d053041f5)
- [【Rails 入門】Stripe API](https://qiita.com/tomokazu0112/items/89f69c47761ac782ce13)
- [[Rails]Stripe Checkoutを利用して定期決済機能を実装する](https://qiita.com/TAKO-zhangyu/items/d756c92828f0d356f57e)
- [Stripe初心者のための基本的な使い方（Rails編）](https://qiita.com/ryouzi/items/6ee8f277471aa3b02f7b)
- [Stripeで従量課金、かつ最初の数人は無料というサービスの設計](https://qiita.com/_akira19/items/4688ebaf87b44462baf2)
- [Stripeの各種API（ChargesとPayment Intents）について](https://qiita.com/piggydev/items/7acaa6a7f6db7b56cde2)
- [Stripe Billing を使ってサービスに月額課金のシステムを組み込む際に必要な実装ってめちゃくちゃ少ない](https://qiita.com/phigasui/items/b06c2728c56d1904e3d0)
- [[保存版] Stripe APIチートシート](https://qiita.com/omo_taku/items/6ae95b6904f974a33176)
- [【JSだけで完結】Stripeの決済機能は1時間で実装できる【簡単】](https://qiita.com/Ikko_Kojima/items/9055969cc9ec58c65a5a)
- [Stripeによるサブスクリプション設計](https://qiita.com/asosori/items/b1c2a35e526702220f0c)