Stripe とのパッケージ同期

説明

このドキュメントでは、Stripe からの Webhook により、パッケージおよびパッケージプランのデータをシステムへ同期する方法を説明します。同期対象のアーティファクトは以下のとおりです。

  • Stripe Product → packagespackage_to_providers
  • Stripe Price → package_planspackage_plan_to_providers

このロジックにより、Stripe のイベントからローカルレコードが作成または更新され、Checkout/Subscription フローで利用するプロバイダ連携が維持されます。

前提条件:

  • Stripe が設定済みで、Webhook エンドポイントがシステムに設定されていること。
  • Stripe の決済プロバイダレコードが存在し、アプリケーションコンテナに app('stripe.payment.provider') として登録されていること。

プロセスフローダイアグラム

---
config:
  theme: base
  layout: dagre
  flowchart:
    curve: linear
    htmlLabels: false
  themeVariables:
    edgeLabelBackground: "transparent"
---
flowchart TD
    %% External Services
    subgraph ExternalServices["External Services"]
        StripeAPI((Stripe API))
    end

    %% API Controller Layer
    subgraph ApiControllerLayer["API Controller Layer"]
        WebhookController[WebhookController]
    end

    %% Business Services
    subgraph BusinessServices["Business Logic Services"]
        PackageService(PackageService)
        PackagePlanService(PackagePlanService)
    end

    %% Repository Layer
    subgraph RepositoryLayer["Repository Layer"]
        PackageRepository(PackageRepository)
        PackagePlanRepository(PackagePlanRepository)
        PackageToProviderRepository(PackageToProviderRepository)
        PackagePlanToProviderRepository(PackagePlanToProviderRepository)
    end

    %% Database Layer
    subgraph DatabaseLayer["Database Layer"]
        PackagesDB[(packages)]
        PackagePlansDB[(package_plans)]
        PackageToProvidersDB[(package_to_providers)]
        PlanToProvidersDB[(package_plan_to_providers)]
        ProvidersDB[(payment_providers)]
    end

    %% Flow
    StripeAPI --- E1["Send webhook (product/price events)"]
    E1 --> WebhookController

    WebhookController --- E2["Verify & dispatch by event type"]

    E2 -->|product.created / product.updated / product.deleted| PackageService
    E2 -->|price.created / price.updated| PackagePlanService

    PackageService --> PackageRepository
    PackageService --> PackageToProviderRepository
    PackageRepository --> PackagesDB
    PackageToProviderRepository --> PackageToProvidersDB

    PackagePlanService --> PackagePlanRepository
    PackagePlanService --> PackagePlanToProviderRepository
    PackagePlanService --> PackageRepository
    PackagePlanRepository --> PackagePlansDB
    PackagePlanToProviderRepository --> PlanToProvidersDB
    PackageRepository --> PackagesDB
    ProvidersDB -. reference .- PackageToProvidersDB
    ProvidersDB -. reference .- PlanToProvidersDB

    %% Styling
    style ExternalServices fill:#fcd9d9,stroke:#cc3333,stroke-width:2px
    style ApiControllerLayer fill:#e6f3ff,stroke:#0066cc,stroke-width:2px
    style BusinessServices fill:#f5f0ff,stroke:#9966cc,stroke-width:2px
    style RepositoryLayer fill:#f0f8e6,stroke:#339933,stroke-width:2px
    style DatabaseLayer fill:#ffe6cc,stroke:#ff9900,stroke-width:2px
    style E1 fill:transparent,stroke:transparent,stroke-width:1px
    style E2 fill:transparent,stroke:transparent,stroke-width:1px

ユースケース

ケース 1: プロダクト同期 (Stripe → Package)

説明

product.created / product.updated を受信し、ローカルの packages および package_to_providers を作成または更新します。

シーケンス図

sequenceDiagram
    participant StripeAPI as Stripe API
    participant Webhook as WebhookController
    participant PkgSvc as PackageService
    participant Repo as Repositories
    participant DB as Database

    Note over StripeAPI,DB: Product Sync

    StripeAPI->>Webhook: POST /api/v1/admin/stripe/webhook (product.*)
    Webhook->>Webhook: Verify signature & construct event
    Webhook->>PkgSvc: syncFromStripe(product)

    rect rgb(230, 255, 255)
    Note right of PkgSvc: Map fields
    PkgSvc->>Repo: findBySlug(product.metadata.slug)
    alt Package exists
        PkgSvc->>Repo: update package fields (name, description, status, limits, visibility)
    else Not found
        PkgSvc->>Repo: create package
    end
    PkgSvc->>Repo: upsert package_to_providers (provider_product_id)
    Repo->>DB: persist changes
    end

    Webhook-->>StripeAPI: 200 OK

手順

手順 1: Product イベントを受信

  • 説明: Stripe から product.created または product.updated が送信されます。
  • 検証: ペイロードに id があることを確認。欠落している場合は無効として拒否。

手順 2: パッケージ項目のマッピング

  • マッピング(PackageService::mapPackageParams より):
    • name, description, status: active
    • slugmetadata.slug から設定
    • 上限: max_member, max_product_group, max_product, max_category, max_search_query, max_viewpoint
    • data_visible, api_available, schedule_id, schedule_priority

手順 3: パッケージの Upsert

  • slug で検索し、存在すれば更新、なければ作成。

手順 4: プロバイダマッピングの Upsert

  • package_to_providers に以下を挿入または更新:
    • provider_id, provider_product_id, status, created_at, updated_at(Stripe のタイムスタンプから)

手順 5: レスポンス

  • 成功時: 200 OK。エラー時: ログ、通知、エラーステータスを返却。

ケース 3: プロダクト削除

説明

product.deleted イベントを受信した場合、アクティブなサブスクリプションの有無に応じて、該当パッケージを非アクティブまたは非推奨としてマークできます(実装詳細は状況により異なります)。

関連データベース構成

erDiagram
    packages {
        bigint id PK "主キー"
        string name "パッケージ名"
        string slug "パッケージスラッグ(一意)"
        text description "説明(null 可)"
        text image "画像パス(null 可)"
        integer schedule_id "スケジュール ID"
        integer schedule_priority "スケジュール優先度"
        integer max_member "メンバー上限(null 可)"
        integer max_product_group "商品グループ上限(null 可)"
        integer max_product "商品上限(null 可)"
        integer max_category "カテゴリ上限(null 可)"
        integer max_search_query "検索クエリ上限(null 可)"
        integer max_viewpoint "ビューポイント上限(null 可)"
        string data_visible "データ可視性"
        tinyint api_available "API 利用可否"
        tinyint status "パッケージステータス"
        timestamp created_at "作成日時"
        timestamp updated_at "更新日時"
    }

    package_plans {
        bigint id PK "主キー"
        string name "プラン名"
        string slug "プランスラッグ(一意)"
        bigint package_id FK "packages テーブルへの外部キー"
        double amount "金額"
        string currency "通貨"
        string type "プラン種別(recurring, one_time)"
        string billing_plan "請求サイクル"
        tinyint status "プランステータス"
        timestamp created_at "作成日時"
        timestamp updated_at "更新日時"
    }

    package_to_providers {
        bigint id PK "主キー"
        bigint package_id FK "packages テーブルへの外部キー"
        bigint provider_id FK "payment_providers テーブルへの外部キー"
        string provider_product_id "プロバイダのプロダクト ID(Stripe)"
        tinyint status "ステータス"
        timestamp created_at "作成日時"
        timestamp updated_at "更新日時"
    }

    package_plan_to_providers {
        bigint id PK "主キー"
        bigint package_plan_id FK "package_plans テーブルへの外部キー"
        bigint provider_id FK "payment_providers テーブルへの外部キー"
        string provider_price_id "プロバイダの価格 ID(Stripe)"
        tinyint status "ステータス"
        timestamp created_at "作成日時"
        timestamp updated_at "更新日時"
    }

    payment_providers {
        bigint id PK "主キー"
        string name "プロバイダ名"
        string slug "プロバイダスラッグ(一意)"
        tinyint status "ステータス"
        timestamp created_at "作成日時"
        timestamp updated_at "更新日時"
    }

    packages ||--o{ package_plans : has
    packages ||--o{ package_to_providers : uses
    package_plans ||--o{ package_plan_to_providers : uses
    payment_providers ||--o{ package_to_providers : linked
    payment_providers ||--o{ package_plan_to_providers : linked

関連 API エンドポイント

メソッド エンドポイント コントローラ 説明
POST /api/v1/admin/stripe/webhook WebhookController@handleWebhook Stripe の product/price イベントを受信
GET /api/v1/admin/groups/packages PackageController@index 利用可能なサービスパッケージ一覧を取得
GET /api/v1/general/package-plan PackagePlanController@index サービスパッケージプラン一覧を取得

エラーハンドリング

  • ログと通知:

    • すべての同期失敗はログに記録。重大なケースは Slack 等へ通知される場合があります。
  • エラー詳細:

ステータスコード エラーメッセージ 説明
400 "Invalid request" Stripe のペイロードに必須識別子が欠落
400 "Product created without slug" Stripe Product の metadata.slug が存在しない
400 "Price created without slug" Stripe Price の lookup_key(プラン slug)が存在しない
404 "Package not found" 参照された Product/Package を解決できない
500 "Failed to create price for plan ..." Stripe 側での Price 作成に失敗

追加メモ

  • パッケージの slug は安定かつ一意であるべきで、Stripe Product のメタデータとローカルレコードの関連付けに使用します。
  • Price の lookup_key は必須で、衝突を避けるためにパッケージの文脈を含めることが望ましいです。
  • 月次プラン作成時、利便性のため Product の default_price を月次価格へ設定する場合があります。
  • すべての upsert ロジックは冪等であり、同じイベントが繰り返されても重複は作成されません。