Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

FirebaseとStripe Billingを組み合わせるとき、stripe.customerのdescriptionとmetadataが便利です

こんにちは、スタディプラスの須藤(id:kurotyann)です。

9/17にFirebaseとStripe Billingを使って新しいサービスをリリースしました。 サービスについては先日投稿したブログを参照してください。

tech.studyplus.co.jp

今日は、タイトルどおり「FirebaseとStripe Billingを組み合わせるとき、stripe.customerのdescriptionとmetadataが便利です」について説明します。

FirebaseとStripe Billingを使ってサービスを開発する予定がある人は、知っておけば作業効率が確実に上がる内容です 💪

Stripe Billingとstripe.customerとは?

Stripe Billingとは、定期支払いのビジネスモデルを構築できるStripeの機能のことです。 詳細は、Stripeの公式サイトを参照してください。

stripe.com

そして、stripe.customerとは、Stripe上で管理される顧客オブジェクトのことです。 stripe.customerには、顧客の連絡先や、クレジットカード情報、定期支払いのプランなど、様々な支払い情報が紐付けられています。

stripe.customerはIDをもち、このIDをFirestoreに保存してFirebase Authenticationのユーザーと紐付けることで、サブスクリプションサービスの構築が可能です。

stripe.customerのdescriptionとmetadataとは?

stripe.customerのdescriptionとは、Stripeのダッシュボードで顧客情報の隣に表示される文字列のことです。

customer_object-description

An arbitrary string attached to the object. Often useful for displaying to users. https://stripe.com/docs/api/customers/object#customer_object-description

そして、stripe.customerのmetadataとは、開発者が顧客情報に追加できるhash型の情報のことです。

customer_object-metadata

Set of key-value pairs that you can attach to an object. This can be useful for storing additional information about the object in a structured format https://stripe.com/docs/api/customers/object#customer_object-metadata

APIドキュメントを見た限りでは、よくあるオブジェクトのプロパティですが、これらを活用すると、FirebaseとStripeの開発効率が上がります。

私が開発をしていて、開発効率が上がった(便利だ)と感じた具体的な事例を3つあげて説明します。

descriptionとmetadataが便利な理由

1. descriptionは、Stripeのテストデータを環境別に見分けやすくする

Stripeには本番とテスト(テストデータ)の2つの環境しか用意されていません。 2つしかないので、本番(production)/ ステージング(staging)/ 開発(development)といった3環境構成のときに困ります。

一方、Firebaseで環境別に3つのプロジェクトを作成するのは簡単です。 そして、アプリ側でリクエスト先のプロジェクトを変えることも、今やデファクトスタンダードになっています。

Stripeのテスト環境にステージング環境と開発環境のユーザーを登録する仕様にした場合、テスト環境に2つの環境のユーザーが混じり合うことになります。 これだと、Stripeのダッシュボードでどの環境の顧客データなのか識別しづらくなります。

そこで、descriptionに環境の識別子をいれて見分けやすくしましょう。 具体的には、Cloud Functions for Firebase で process.env.GCLOUD_PROJECT を stripe.customerの新規作成時に保存するのが一番簡単だと思います。

customer = await stripe.customers.create({
  description: process.env.GCLOUD_PROJECT,
  email: email,
  source: token,
});

これでStripeのダッシュボードでテストデータを見たとき、どの環境で作成された顧客データなのか、ひと目でわかります。

Stripeのダッシュボードでテストデータを見たとき
f:id:kurotyann:20191105012821p:plain

さらに、他にも嬉しいことがあります。

  • ダッシュボードの検索バーに process.env.GCLOUD_PROJECT の値を入力すれば、環境ごとの顧客データを絞り込める
  • 顧客データをcsv形式でエクスポートしたとき、顧客データにdescriptionも同時に付与されて出力される

2. metadataにuidを保存して、uidで顧客情報を検索できるようにする

descriptionには環境の識別子を入れたので、metadataにはFirebase Authenticationのuidを保存します。

customer = await stripe.customers.create({
  description: process.env.GCLOUD_PROJECT,
  email: email,
  source: token,
  metadata: {
    uid: uid,
  },
});

これでdescriptionと同様に、Stripeのダッシュボードでどの環境のユーザーでもuidさえわかれば、顧客情報をすぐに絞り込めます。 また、metadataもcsv形式でエクスポートしたとき、顧客データに付与されて出力されるので、csvで分析したいときも便利です。

3. metadataにも環境識別子を保存して、どの環境のwebhookなのか判別できるようにする

最後に、 metadataにも環境識別子を保存しましょう。

customer = await stripe.customers.create({
  description: process.env.GCLOUD_PROJECT,
  email: email,
  source: token,
  metadata: {
    uid: uid,
    env: process.env.GCLOUD_PROJECT,
  },
});

Stripeのwebhookの環境も、本番とテストの2環境しかありません。

これだと、例えば定期支払いのイベント invoice.payment_failed がテスト環境で発生したとき、ステージングと開発の両方のwebhookが対象になってしまいます。

つまり、特に何も準備しなければ、Stripeのテスト環境で起きたイベントが、ステージングと開発のどちらの環境で発生したイベントなのか識別できません。

そこで、metadataの環境識別子を使います。 私の場合、webhookのStripe-Signatureの処理直後に、下記の環境識別子チェックを走らせるようにしました。

const functions = require('firebase-functions');
const stripe = require('stripe')(functions.config().stripe.token);

/*
 * Stripeのテスト環境に開発とステージングの webhookUrl を定義しているため
 * customerのメタデータでどちらの環境の webhook なのか判定している
 * doc: https://stripe.com/docs/api/customers/retrieve?lang=node
 */
module.exports = async function(data) {
  try {
    const customerId = data['customer'];
    if (customerId === 'cus_00000000000000') {
      // Stripe dash boardからイベントをテスト送信した場合
      return {
        isValid: true,
      };
    }

    const customer = await stripe.customers.retrieve(customerId);
    const metadataEnv = customer.metadata['env'];
    return {
      isValid: metadataEnv === process.env.GCLOUD_PROJECT,
      code: 202,
      message: `different project processEnv: ${process.env.GCLOUD_PROJECT}, metadataEnv: ${metadataEnv}`,
    };
  } catch (e) {
    console.error(`🧨 validateProjectEnv: ${JSON.stringify(e)}`);
    throw e;
  }
};
const validateProjectEnv = require('./util/validateProjectEnv');

/* Stripe-Signature の直後 */

const validate = await validateProjectEnv(event.data.object);
if (!validate.isValid) {
  return res.status(validate.code).send(validate.message);
}

/* 任意の処理が走る(例: Firestoreの更新や、slackへの通知など) */

Stripeの現在の仕様だと、 ダッシュボードからテストでwebhookを起動させるとき顧客IDは cus_00000000000000 です。これは無視するようにしています。

ちなみに、descriptionもwebhookのレスポンスに含まれるのでmetadataに保存するのは二度手間のように思いますが、StripeのAPIドキュメントだとdescriptionはダッシュボードで利用されるプロパティだと明記されています。

今後のStripeの仕様変更でAPIのレスポンスにdescriptionが含まれなくなっても大丈夫なように念の為、metadataに保存しておいた方が無難だと私は思います。

おわりに

stripe.customerのdescriptionとmetadataが便利な理由を具体的な3つの事例を交えて紹介しました。

まだまたFirebaseとStripe Billingを使った内容で紹介したいことがあるのですが、それは今年のアドベントカレンダーのどこかで投稿する予定なのでしばしお待ちください 😄