こんにちは、λ沢です。
microCMS 社では ジャムジャム!!Jamstack というイベントを開催しています。
前回のジャムジャム!!Jamstack では Stripe Developer Adovocate の HideOkamoto さんに 「microCMSとStripe + Next.jsを利用したJamstackなHeadless ECサイトを作る方法(入門編)」というタイトルで LT をしていただきました。
このように、 microCMS と Stripe には親和性があります。
上記の LT と Qiita は大変素晴らしいものですが、 Qiita の Appendix にある通り、より実践的なアーキテクチャにすることができます。
この記事では上記 Qiita の Appendix にある「microCMSのWebhookを利用し、簡易説明文や商品名の変更をStripe側に反映させる」という部分を実践する方法をチュートリアル形式で紹介します。
なぜやるか
上記のようなアーキテクチャでは microCMS と Stripe の両方でデータを管理する必要があります。
これは片方のデータを更新したけどもう片方のデータの更新を忘れてしまった…というような事象が発生して混乱してしまいがちです。
今回はこの問題を解決するために microCMS でデータを作成したら Stripe 上にもデータが作成されるような仕組みを構築していきます。
APIスキーマについて
今回想定するスキーマは以下のようなものです。
繰り返しフィールド部分で使っているカスタムフィールドは以下のようなシンプルな構造です。name
, description
, images
については Stripe の Product に紐づけます。
これは決済完了後にユーザにメールで送られたりことがあるでしょう。priceJPY
は Stripe の Price に紐付けます。
これはその名の通り商品の金額であり、ユーザにこの金額が請求されます。richDescription
は EC サイト上で表示する想定のリッチテキストです。
これは Stripe 上では扱えないタイプのデータですが、 microCMS と Stripe を連携させることでこのような商品を表現する幅が広がるということをイメージしていただければと思います。
この API スキーマをエクスポートしたものを以下のリンクからダウンロード可能です。
microCMS には API スキーマをインポートする機能があるため、こちらをインポートしていただくとお手元の環境でも動作確認がしやすいと思います。
API スキーマをダウンロード
環境構築
データを同期するコードを書く前にまず各種セットアップを終えましょう。
今回は JavaScript で Express を使う例を紹介します。
まず npm
のパッケージを以下のように初期化します。
$ npm init -y
$ npm install express stripe
そして index.js
を以下のように実装して Express や Stripe の API クライアントを初期化します。
// Stripe の API クライアントを初期化
const stripe = require("stripe")("sk_test_XXXX");
// Web アプリケーションフレームワークとして Express を使用する
const express = require("express");
const app = express();
app.use(express.json());
app.post("/", async (req, res) => {
console.log(JSON.stringify(req.body));
// TODO ここに処理を実装していく
return res.json({ status: "OK" });
});
// http://localhost:8000/ でリクエストを受け付ける
app.listen(8000, () => console.log("sync-microcms-to-stripe server start!"));
そして以下のようにしてサーバを起動します。
$ node index.js
このサーバは現在インターネット上に公開されていない状態なので、 まだ Webhook を受け取ることはできません。
ローカルで Webhook (カスタム通知) を受け取れるようにする方法は以下の記事から確認できます。
カスタム通知をローカル環境で受け取って開発をスムーズにする
実装方針
大まかには以下のようなフローの実装を行います。
- 下書きの更新などによるイベントは無視
- Stripe 上に商品が無ければ新規作成の関数を呼び出す
- Stripe 上に商品が存在すれば既存商品を更新する関数を呼び出す
- Stripe の商品 ID と microCMS のコンテンツ ID は共有する
これをコードにすると以下のようになります。
// TODO: microCMS の Webhook で受け取った値を使って Stripe に商品を登録する
async function createStripeProduct(microCMSValue) { /* TODO */ }
// TODO: microCMS の Webhook で受け取った値を使って Stripe に商品を更新する
async function updateStripeProduct(microCMSValue) { /* TODO */ }
app.post("/", async (req, res) => {
// 下書きの編集中は何もせずにスキップ
// see: https://document.microcms.io/manual/webhook-setting#h3e9a323374
const microCMSValue = req.body.contents?.new?.publishValue;
if (!microCMSValue) {
return res.json({ status: "OK" });
}
// Stripe 上から商品を取得(未登録なら null)
const productOrNull =
await stripe.products.retrieve(microCMSValue.id).catch(() => null);
// Stripe 上に商品情報が登録されているか?
const existsProductOnStripe = Boolean(productOrNull);
if (existsProductOnStripe) {
// Stripe 上に既に商品情報が登録済みなら更新する
await updateStripeProduct(microCMSValue);
} else {
// Stripe 上に商品情報が存在しないなら新規登録する
await createStripeProduct(microCMSValue);
}
return res.json({ status: "OK" });
});
次のステップで createStripeProduct
と updateStripeProduct
の詳細について見ていきましょう。
新規作成時の実装
新規作成時の実装は以下のように書けます。
単純に microCMS の Webhook で受け取った値を分解して、それらを利用して Stripe 上に Product と Price を作成するだけとなります。
Stripe 上では Price が Product に依存するような形式となっています。
async function createStripeProduct(microCMSValue) {
const { id, name, description, images, priceJPY } = microCMSValue;
const imageURLs = images.map(({ image }) => image.url);
// 名前、簡易説明文、画像を Product に同期
await stripe.products.create({
id,
name,
description,
images: imageURLs,
});
// 新しい Price を作って Product に紐付け。これによって金額が同期されたことになる。
await stripe.prices.create({
currency: "JPY",
product: id,
unit_amount: priceJPY,
});
}
更新時の実装
更新時の実装は新規作成時より少し複雑になります。
商品の名前、説明文、画像は簡単に Stripe 上の Product に反映することができます。
しかし金額の更新は工夫が必要です。
これは Stripe では既に存在している Price を更新して金額を変えることができないためです。
また、既に誰かがその金額で商品を購入していた場合はその Product を削除することもできません。
そのようなことが出来てしまうと購入履歴を正しく把握することが難しいため、これは仕方が無いでしょう。
ではどのように金額の更新を実現するかというと、Price を新規作成した上で既存の Price をアーカイブすることで対応できます。
これを実現するのが以下のようなコードになります。
async function updateStripeProduct(microCMSValue) {
const { id, name, description, images, priceJPY } = microCMSValue;
const imageURLs = images.map(({ image }) => image.url);
// 名前、簡易説明文、画像を Product に同期
const product = await stripe.products.update(id, {
name,
description,
images: imageURLs,
});
// 利用可能な Price (アーカイブされていない Price) の一覧を取得。通常は1件のみ。
const prices = await stripe.prices.list({ active: true, product: product.id });
// Stripe 上の金額
const priceStripe = prices.data[0].unit_amount;
// microCMS 上の金額
const priceMicroCMS = priceJPY;
// Stripe と microCMS 上の金額が異なる場合のみ Price を更新
if (priceStripe !== priceMicroCMS) {
// 既存の Price を全てアーカイブ
await Promise.all(
prices.data.map((price) => {
return stripe.prices.update(price.id, { active: false });
})
);
// 新しい Price を作って Product に紐付け。これによって金額が更新されたことになる。
await stripe.prices.create({
currency: "JPY",
product: product.id,
unit_amount: priceJPY,
});
}
}
ここまで全てうまく動作していれば microCMS 上でコンテンツを作成すると Stripe 上に商品が作成されることを確認できると思います。
お疲れさまでした!
デプロイ方法について
今回は素朴な Express のサーバとしてデータ同期の仕組みを実装しました。
これを実際に運用する際はこの Express のサーバをどこかにデプロイする必要があります。
Express をデプロイする手段は豊富にあります。
また、リクエストの読み込み部分のみ Next.js の /pages/api 向けに改修して Vercel にデプロイしたり、 itty-router 向けに改修して Cloudflare Workers にデプロイすることも出来ると思います。
この記事には含まれていないこと
今回作成したサーバは金額の変更を担う重要なコンポーネントです。
悪意のある第三者が Webhook のドメインを推測して意図せずアクセスしてくる可能性に備える必要があります。
この問題を防ぐためには以下の記事が参考になります。
Webhook発行元がmicroCMSであることを検証可能になりました
また、今回書いたコードでは説明をシンプルにするために全体的にエラーハンドリングを省略しています。
最後に
無事 microCMS と Stripe のデータを同期できるようになりました。
これによってコンテンツ(商品)の表現は microCMS に任せて、決済の基盤は Stripe に任せて、それぞれの強みを活かせる状態でありながらもシンプルな運用ができるようになりました。