1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Stripe Webhook でトランザクションを自動化 🌐💼

Posted at

Stripe のウェブフックを実装し、取引の各ステップを自動化する方法を探求します。このチャプターでは、ウェブフックの設定から、取引イベントのリアルタイムモニタリング、そして自動応答の仕組みまでを解説。Stripe とサーバーレスアプリケーションの連携を最大限に活かす技術を提供します。

Stripe CLI のインストール

Stripe CLI は Stripe の開発を効率的に行うためのコマンドラインツールです。以下のコマンドでインストールできます。

Stripe CLI のインストール

Mac の場合

brew install stripe/stripe-cli/stripe

Stripe Webhook のテスト

skeet s でローカルサーバーを起動し、Stripe Webhook のテストを行います。

skeet s

別ウィンドウで Stripe CLI を使って、Stripe Webhook のテストを行います。
新しく追加した stripeRouter のエンドポイントを指定します。

stripe listen --forward-to http://127.0.0.1:5001/<your-project-id>/<your-region>/stripeRouter

そしてさらに別ウィンドウで、Stripe CLI でトリガーを送信します。

stripe trigger checkout.session.completed

すると、ローカルサーバーにリクエストが送信され、
payment アクションが実行されました。

無事に stripeRouter のエンドポイントにリクエストが送信されました 🎉

Firestore との連携、StripeUser モデルの作成

それでは、Stripe と Firestore を連携させるために、
StripeUser モデルを作成します。

functions/skeet/src/models/StripeUser.ts

import { Timestamp, FieldValue } from '@skeet-framework/firestore'

// CollectionId: StripeUser
// DocumentId: StripeUserId
// Path: StripeUser
export const StripeUserCN = 'StripeUser'
export type StripeUser = {
  id?: string
  customerId: string | null
  username: string
  email: string
  isActivated: boolean
  expirationDate: Timestamp | FieldValue | null
  createdAt?: Timestamp | FieldValue
  updatedAt?: Timestamp | FieldValue
}

トランザクション発生時に StripeUser を作成する

サブスクリプションの購入が完了したら、Webhook を受信して、
StripeUser を作成します。

functions/skeet/src/lib/stripe/webhook/payment.ts

import { StripeUser, StripeUserCN } from '@/models/stripeUserModels'
import { add } from '@skeet-framework/firestore'
import Stripe from 'stripe'

export const subscription = async (
  db: FirebaseFirestore.Firestore,
  eventObject: Stripe.Checkout.Session
) => {
  console.log('subscription event handler')
  const stripeUserParams: StripeUser = {
    customerId: String(eventObject.customer),
    username: eventObject.customer_details?.name || '',
    email: eventObject.customer_details?.email || '',
    isActivated: true,
    expirationDate: null,
  }
  await add<StripeUser>(db, StripeUserCN, stripeUserParams)
  return true
}

それでは subscription イベントを起こすために、
テスト用のペイメントリンクから支払いを行います。

テスト用のクレジットカードは

4242 4242 4242 4242

期限とCVCは任意のものでテスト用として動作します。

そして Firebase エミュレーターの Firestore を確認します。

http://localhost:4000/

無事に StripeUser が作成されました 🎉

メンバー限定のボタンに変更する

前の章で作成した、bonusAction.ts を会員限定のコンテンツへ変更します。

functions/skeet/src/lib/discord/actions/bonusAction.ts

import { DiscordUser, DiscordUserCN } from '@/models/discordUserModels'
import { StripeUser, StripeUserCN } from '@/models/stripeUserModels'
import {
  DiscordRouterParams,
  deferResponse,
  updateResponse,
} from '@skeet-framework/discord-utils'
import { get, update } from '@skeet-framework/firestore'
import { Response } from 'firebase-functions/v1'

export const bonusAction = async (
  res: Response,
  db: FirebaseFirestore.Firestore,
  discordToken: string,
  body: DiscordRouterParams
) => {
  console.log('bonusAction')
  await deferResponse(discordToken, body.id, body.token)

  const memberId = body.member.user.id
  const user = await get<DiscordUser>(db, DiscordUserCN, memberId)
  if (!user) {
    await updateResponse(discordToken, body.application_id, body.token, {
      content: `⚠️ Failed to get user.Please try again in a few seconds.`,
      flags: 64,
    })
    return false
  }

  const bonus = Math.floor(Math.random() * 100)
  const newExp = user.exp + bonus
  let lv = user.lv
  const expToNextLevel = 100 * lv

  if (expToNextLevel >= 300) {
    const stripeUser = await get<StripeUser>(db, StripeUserCN, memberId)
    if (!stripeUser || !stripeUser.isActivated) {
      await updateResponse(discordToken, body.application_id, body.token, {
        content: `⚠️ この先はメンバーシップが必要です。`,
        flags: 64,
      })
      return false
    }
  }

  if (newExp >= expToNextLevel) {
    lv++
  }

  await update<DiscordUser>(db, DiscordUserCN, body.member.user.id, {
    exp: newExp,
    lv,
  })

  let description = `あなたは${bonus}の EXP を獲得しました!`
  if (lv > user.lv) {
    description += `\nレベルが${lv}に上がりました!`
  }
  // Discord Embed Message
  const emdbed = {
    description,
    color: 0x0099ff,
  }

  await updateResponse(discordToken, body.application_id, body.token, {
    embeds: [emdbed],
    flags: 64,
  })
  return true
}

これで、さきほどのボタンを EXP が 300 を超えるまでクリックしてみます。

メンバー限定のコンテンツに変更されました 🎉

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?