[Elixir] Stripeで従量課金を実現する

この記事は Elixir Advent Calendar 2024 シリーズ4の23日目です。昨日は @piacerex さんでした！

https://qiita.com/piacerex/items/3a19a3a43f3d49bf3f63

---

執筆時点ではまだベータテストという建付けですが、[imagepix](https://imagepix.app) というサービスを開発・運営しています。

https://www.docswell.com/s/s12m/ZXY7XP-imagepix

imagepixの管理コンソールElixirのWebアプリケーションフレームワーク [Phoenix](https://www.phoenixframework.org/) で開発していて、従量課金を実現するために言わずと知れた **Stripe** を使っています。

公式ドキュメントが充実しているので困ることはないのですが、従量課金の解説記事はあまり見かけません。そこで、今回はElixirでStripeのAPIを使って従量課金を実現する流れを解説してみます :money_with_wings:  

# Stripeダッシュボードでの事前準備

アカウントを持っていない場合は、以下を参考に作っておきましょう。

https://docs.stripe.com/get-started/account

私はもちろんアカウントを持っていますが、テスト環境を汚したくないこともあり、今回たまたま発見した **サンドボックス** を使うことにします。もちろんテスト環境を使っても構いません :ok_hand: 

https://docs.stripe.com/sandboxes

## メーターの作成

[https://dashboard.stripe.com/test/meters](https://dashboard.stripe.com/test/meters)

以下のようにメーターを作成します。メーター名は後からでも変えられますが、イベント名やその他の項目は一度決めると変えられないので要注意です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28249/e78dc671-29a7-0ea4-b453-654b51c062aa.png)

## 商品の作成

[https://dashboard.stripe.com/test/products](https://dashboard.stripe.com/test/products)

以下のように商品を作成します。今回は1ユニットあたり100円という商品にしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28249/2e2de4f1-8eac-7f72-c1c1-d01329eb62f9.png)

「その他の料金体系オプション」をクリックすると従量課金の料金体系を設定できます。

- 料金モデル
- 金額
- メーター

をそれぞれ以下のように入力しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28249/29ecbae2-1de5-ec88-3ba2-9b87edab4b3e.png)

# LivebookでのElixir実行環境の準備

本題ではないので、Dockerでサクッと起動しましょう。

```bash
docker run --env LIVEBOOK_TOKEN_ENABLED=false -p 8080:8080 -p 8081:8081 --pull always ghcr.io/livebook-dev/livebook
```

[http://localhost:8080](http://localhost:8080)

:::note info
本記事の内容をとりあえず動かしたい方は、以下の方法を試してみてください :information_desk_person: 

<details><summary>Live markdown</summary>

以下の内容を http://localhost:8080/open/source の「Notebook source」にコピペしてインポートするだけで動かせます。

    # Stripeで従量課金を実現する
    
    ```elixir
    Mix.install([
      {:stripity_stripe, "~> 3.2"},
      {:kino, "~> 0.14.2"}
    ])
    ```
    
    ## Stripe Checkout Sessionでのサブスクリプションの作成
    
    ```elixir
    secret_key_input = Kino.Input.password("SECRET_KEY")
    ```
    
    ```elixir
    price_input = Kino.Input.password("PRICE")
    ```
    
    ```elixir
    secret_key = Kino.Input.read(secret_key_input)
    price = Kino.Input.read(price_input)
    
    {:ok, session} =
      Stripe.Checkout.Session.create(
        %{
          line_items: [
            %{price: price}
          ],
          mode: :subscription,
          success_url: "https://dashboard.stripe.com/test/subscriptions"
        },
        api_key: secret_key
      )
    
    IO.puts(session.url)
    ```
    
    ## Stripe Meter Eventでの使用量の記録
    
    ```elixir
    {:ok, %{data: [subscription | _]}} = Stripe.Subscription.list(%{}, api_key: secret_key)
    customer = subscription.customer
    ```
    
    ```elixir
    path = Stripe.OpenApi.Path.replace_path_params("/v1/billing/meter_events", [], [])
    
    Stripe.Request.new_request(api_key: secret_key)
    |> Stripe.Request.put_endpoint(path)
    |> Stripe.Request.put_params(%{
      event_name: "sample",
      payload: %{
        value: 10,
        stripe_customer_id: customer
      }
    })
    |> Stripe.Request.put_method(:post)
    |> Stripe.Request.make_request()
    ```
</details>
:::

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

Livebookの「Notebook dependencies and setup」に以下のコードを入力して実行し、ライブラリをインストールします。

```elixir
Mix.install([
  {:stripity_stripe, "~> 3.2"},
  {:kino, "~> 0.14.2"}
])
```

[stripity_stripe](https://hexdocs.pm/stripity_stripe/3.2.0/readme.html) は、StripeのAPIをElixirで実行するためのサードパーティライブラリです。公式ライブラリは本記事の執筆時点では存在しません…

[kino](https://hexdocs.pm/kino/0.14.2/Kino.html) は、Livebook上で便利なウィジェットを使えるようにするライブラリです。本記事では秘匿情報の入力フォームのために使います。

## 秘匿情報の入力

[Kino.Input.password/2](https://hexdocs.pm/kino/0.14.2/Kino.Input.html#password/2) を使って秘匿情報の入力フォームを用意します。

### Stripeシークレットキー

Livebookで以下のコードを実行すると入力フォームが表示されるので、シークレットキーを入力しましょう。

```elixir
secret_key_input = Kino.Input.password("SECRET_KEY")
```

シークレットキーは [開発者ページ](https://dashboard.stripe.com/test/apikeys) で確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28249/21354612-6bee-eaf9-f8f9-8654a9fd0d06.png)

### Stripe価格ID

Livebookで以下のコードを実行すると入力フォームが表示されるので、価格IDを入力しましょう。

```elixir
price_input = Kino.Input.password("PRICE")
```

価格IDは [商品カタログページ](https://dashboard.stripe.com/test/products?active=true) から確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28249/9a51109d-4ab3-7bf2-c8bd-a3cf5d2851e1.png)

# Stripe Checkoutでのサブスクリプションの作成

Stripeでは、**Checkout** を使ってStripeが提供してくれる決済画面で手続きさせることができます。

https://docs.stripe.com/payments/checkout/how-checkout-works?locale=ja-JP

[Stripe.Checkout.Session/create/2](https://hexdocs.pm/stripity_stripe/3.2.0/Stripe.Checkout.Session.html#create/2) を使って決済画面のURLを発行します。秘匿情報は [Kino.Input.read/1](https://hexdocs.pm/kino/0.14.2/Kino.Input.html#read/1) で取得できます。

```elixir
secret_key = Kino.Input.read(secret_key_input)
price = Kino.Input.read(price_input)

{:ok, session} =
  Stripe.Checkout.Session.create(
    %{
      line_items: [
        %{price: price}
      ],
      mode: :subscription,
      success_url: "https://dashboard.stripe.com/test/subscriptions"
    },
    api_key: secret_key
  )

IO.puts(session.url)
```

表示されるURLにアクセスすると以下のような決済画面が表示されるので、カード情報（[テストカード](https://docs.stripe.com/testing?locale=ja-JP) を使います）など諸々入力して申し込みましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28249/0c633c4a-1e5d-e950-be6b-a512a799854e.png)

申し込みが完了すると、[サブスクリプションページ](https://dashboard.stripe.com/test/subscriptions) から内容を確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28249/2c1245eb-f6e7-9ae6-420b-8ccebd6f7a62.png)

この時点では使用量を記録していないので、次回請求は0円になっています。

# Stripe Meter Eventでの使用量の記録

使用量を記録すると、次回請求に反映されます。

https://docs.stripe.com/billing/subscriptions/usage-based/recording-usage-api

使用量を記録するには顧客IDが必要です。今回は、作成済みのサブスクリプションから顧客IDを取得します。

```elixir
{:ok, %{data: [subscription | _]}} = Stripe.Subscription.list(%{}, api_key: secret_key)
customer = subscription.customer
```

ここで、使用量を記録しますが、残念ながらstripity_stripeは執筆時点でMeter Events APIに対応していないため、[顧客作成APIのコード](https://github.com/beam-community/stripity-stripe/blob/v3.2.0/lib/generated/customer.ex#L330-L338) を参考に自前で実装しましょう。

https://docs.stripe.com/api/billing/meter-event/create

今回は10ユニット使用したと想定します。`event_name` にはメーターを作る際に入力した「sample」を指定します。

```elixir
path = Stripe.OpenApi.Path.replace_path_params("/v1/billing/meter_events", [], [])

Stripe.Request.new_request(api_key: secret_key)
|> Stripe.Request.put_endpoint(path)
|> Stripe.Request.put_params(%{
  event_name: "sample",
  payload: %{
    value: 10,
    stripe_customer_id: customer
  }
})
|> Stripe.Request.put_method(:post)
|> Stripe.Request.make_request()
```

少し待ってからサブスクリプションページを見ると、次回請求が1,000円になっています :tada: 100円の商品を10ユニット使用した想定なので、計算も合っていますね :sparkles: 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28249/71322a94-d70b-2e39-c106-ed2186be22e6.png)

# 最後に

今回はLivebookで完結させられるようにシークレットキーを都度指定していますが、Elixirアプリケーションを開発する際は `config/*.exs` で設定するのが良いでしょう。

https://hexdocs.pm/stripity_stripe/3.2.0/readme.html#configuration

StripeではAPIのバージョンが更新されていき、挙動が変わってしまうこともあるので、明示的にバージョンを指定しておくとより安心です :blush:

```elixir
config :stripity_stripe,
  api_key: System.get_env("STRIPE_SECRET"),
  api_version: "2024-11-20.acacia"
```

今回はかなり単純な例でしたが、Stripeでは段階的な価格にしたり複数の商品を組み合わせたりといった複雑な料金体系も簡単に実現できるので、ぜひ試してみてください :moneybag: 

それにしても、一昔前と比べて決済を組み込むの楽になりましたねぇ（遠い目）。まあ、法律やセキュリティ基準もだいぶ厳しくなりましたが…

---

明日の Elixir Advent Calendar 2024 シリーズ4の24日目は @RyoWakabayashi さんです！
お楽しみに :rocket: