カスタムプラン作成

概要説明

カスタムプラン作成機能により、管理者はグループ向けのカスタム価格契約を作成できます。このエンドポイントは、請求金額、間隔、使用制限を含む指定された条件で新しいカスタムプランを作成します。契約は既存のサブスクリプションにリンクすることも、新しいサブスクリプションが自動的に作成されることもあります。

前提条件

  • ユーザーは管理者として認証されている必要があります(SuperAdminまたはAdminStaffロール)
  • 対象グループが存在し、アクティブである必要があります
  • 既存のサブスクリプションにリンクする場合、以下を満たす必要があります:
    • 指定されたグループに属している
    • pricing_type = 'custom'であるか、キャンセルされている
    • アクティブな非カスタムサブスクリプションを持っていない

依存関係

  • グループ管理モジュール(グループ検証用)
  • サブスクリプションモジュール(サブスクリプション作成/リンク用)
  • ユーザーサービス(Stripe顧客同期用)

Swaggerリンク

API: カスタムプラン作成

ケースドキュメント

ケース1: 契約作成成功(新規サブスクリプション)

説明

管理者がアクティブなサブスクリプションを持たないグループに対してカスタムプランを正常に作成します。pricing_type = 'custom'の新しいサブスクリプションが自動的に作成されます。

Sequence Diagram

sequenceDiagram
    participant Admin
    participant Controller as CustomContractController
    participant Request as StoreCustomContractRequest
    participant Service as CustomContractService
    participant GroupRepo as GroupRepository
    participant SubscriptionRepo as SubscriptionRepository
    participant UserService as UserService
    participant ContractRepo as CustomContractRepository
    participant Database
    
    Note over Admin,Database: POST /api/v1/admin/custom-contracts
    
    rect rgb(200, 255, 200)
    Note right of Admin: Happy Case Flow - New Subscription
    
    Admin->>Controller: POST request with contract data
    
    rect rgb(200, 230, 255)
    Note right of Controller: Input Validation
    Controller->>Request: validate(data)
    Request->>Request: Check required fields
    Request->>Request: Validate billing_interval
    Request->>Request: Validate amount >= 0
    Request->>Request: Check code uniqueness
    Request-->>Controller: Validation passed
    end
    
    rect rgb(200, 255, 255)
    Note right of Controller: Business Logic Processing
    Controller->>Service: create(validatedData)
    Service->>Service: transactionBegin()
    
    Service->>GroupRepo: findById(group_id)
    GroupRepo->>Database: SELECT * FROM groups WHERE id = ?
    Database-->>GroupRepo: Return group data
    GroupRepo-->>Service: Return group object
    
    Service->>Service: Check active subscription
    Service->>Service: Verify no active non-custom subscription
    
    Service->>SubscriptionRepo: create(subscription_data)
    SubscriptionRepo->>Database: INSERT INTO subscriptions
    Database-->>SubscriptionRepo: Return created subscription
    SubscriptionRepo-->>Service: Return subscription object
    
    Service->>ContractRepo: create(contract_data)
    ContractRepo->>Database: INSERT INTO custom_contracts
    Database-->>ContractRepo: Return created contract
    ContractRepo-->>Service: Return contract object
    
    Service->>Service: Set contract status to 'draft'
    Service->>SubscriptionRepo: update pricing_type and custom_contract_id
    SubscriptionRepo->>Database: UPDATE subscriptions
    Database-->>SubscriptionRepo: Update confirmed
    
    Service->>Service: transactionCommit()
    Service-->>Controller: Return contract with relations
    end
    
    Controller-->>Admin: 200 OK (contract_data, success_message)
    end
    
    rect rgb(255, 200, 200)
    Note right of Admin: Error Scenarios
    rect rgb(255, 230, 230)
    alt Validation Error
        Request-->>Controller: Validation failed
        Controller-->>Admin: 422 Unprocessable Entity
    else Group Not Found
        GroupRepo-->>Service: Group not found
        Service->>Service: transactionRollback()
        Service-->>Controller: Error: Group not found
        Controller-->>Admin: 400 Bad Request
    else Active Non-Custom Subscription Exists
        Service-->>Service: Check fails
        Service->>Service: transactionRollback()
        Service-->>Controller: Error: Cannot switch type
        Controller-->>Admin: 400 Bad Request
    else Database Error
        Database-->>ContractRepo: Database constraint error
        Service->>Service: transactionRollback()
        Service-->>Controller: Error: Database error
        Controller-->>Admin: 400 Bad Request
    end
    end
    end

ステップ

ステップ1: リクエスト検証

  • 説明: システムはビジネスルールに従ってすべての入力データを検証します
  • リクエスト: POST /api/v1/admin/custom-contracts
  • 認証: ユーザーはSuperAdminまたはAdminStaffロールを持つ必要があります
  • 検証ルール:
    • group_id: Required, must exist in groups table
    • code: Required, max 100 characters, must be unique
    • billing_interval: Required, must be 'month' or 'year'
    • amount: Required, integer, minimum 0
    • currency: Required, default 'jpy', max 10 characters
    • package_plan_id: Required if subscription_id not provided, must exist in package_plans table
    • subscription_id: Required if package_plan_id not provided, must exist in subscriptions table
    • 注意: subscription_id または package_plan_id のいずれかを指定する必要があります(少なくとも1つが必須)
    • user_id: Optional, must exist if provided
    • starts_at: Optional, must be valid date
    • ends_at: Optional, but required if starts_at is provided, must be date after or equal to starts_at
    • All limit fields (max_member, max_product_group, etc.): Optional, integer, min 0

ステップ2: グループ検証

  • 説明: 対象グループが存在することを確認し、サブスクリプションの制約をチェックします
  • アクション:
    • group_idでグループをクエリ
    • グループにアクティブなサブスクリプションがあるかチェック
    • アクティブなサブスクリプションが存在する場合、以下を確認:
      • 既にカスタムタイプ(pricing_type = 'custom'
      • またはキャンセルされている(status = 'cancelled'
  • ビジネスルール:
    • アクティブな非カスタムサブスクリプションを持つグループにはカスタムプランを作成できません
    • グループは存在し、アクティブである必要があります
  • 潜在的なエラー:
    • グループが見つかりません(404)
    • アクティブな非カスタムサブスクリプションが存在します(400)

ステップ3: サブスクリプション作成または検証

  • 説明: 新しいサブスクリプションを作成するか、既存のものを検証します
  • 新規サブスクリプションの場合のアクション:
    • package_plan_idが提供されていることを確認
    • パッケージプランの詳細を取得
    • ユーザーを取得(user_idまたはgroup.created_byから)
    • 欠落している場合はStripe顧客IDを同期
    • 以下のサブスクリプションレコードを作成:
      • pricing_type = 'custom'
      • status = 'unpaid'
      • パッケージプランとグループにリンク
  • 既存サブスクリプションの場合のアクション:
    • サブスクリプションがグループに属していることを確認
    • サブスクリプションのpricing_typeが'custom'であるか、statusが'cancelled'であることを確認
    • 提供されていない場合は、サブスクリプションのpackage_plan_idを使用
  • データベース操作:
    • INSERT INTO subscriptions(新規の場合)
    • またはSELECT subscription(既存の場合)

ステップ4: 契約作成

  • 説明: すべての条件を含むカスタムプランレコードを作成します
  • アクション:
    • 以下の契約レコードを挿入:
      • サブスクリプションリンク(subscription_id)
      • グループとユーザーのリンク
      • 請求条件(code, amount, currency, billing_interval)
      • 日付範囲(starts_at, ends_at)
      • 使用制限(max_member, max_product_groupなど)
      • データ可視性とAPI可用性設定
    • 初期ステータスを'draft'に設定
  • データベース操作:
    • INSERT INTO custom_contracts
    • タイムスタンプを設定(created_at, updated_at)

ステップ5: サブスクリプション更新

  • 説明: サブスクリプションを契約にリンクし、価格タイプを設定します
  • アクション:
    • サブスクリプションレコードを更新:
      • pricing_type = 'custom'を設定
      • custom_contract_id = contract.idを設定
    • トランザクションをコミット
  • データベース操作:
    • UPDATE subscriptions SET pricing_type, custom_contract_id

ステップ6: レスポンス返却

  • 説明: 関連データを含む作成された契約を返却します
  • レスポンスデータ:
    • 契約IDと詳細
    • 関連するサブスクリプションデータ
    • 関連するグループデータ
    • 関連するユーザーデータ(該当する場合)
  • 成功メッセージ: "カスタムプランが正常に作成されました"

Database Related Tables & Fields

erDiagram
    custom_contracts {
        bigint id PK
        bigint subscription_id FK "Link to subscriptions table (nullable)"
        bigint package_plan_id FK "Base package plan reference (nullable)"
        bigint group_id FK "Group owning this contract"
        bigint user_id FK "User responsible for contract (nullable)"
        string code "Unique contract identifier (max 100 chars, unique)"
        string billing_interval "month or year"
        string currency "Currency code (default jpy, max 10 chars)"
        integer amount "Billing amount in minor unit"
        string status "draft, offered, active, expired, cancelled (default draft)"
        string provider_checkout_session_id "Stripe checkout session ID (nullable, max 100)"
        string provider_price_id "Stripe price ID (nullable, max 100)"
        string provider_subscription_item_id "Stripe subscription item ID (nullable, max 100)"
        timestamp starts_at "Contract start date (nullable)"
        timestamp ends_at "Contract end date (nullable)"
        integer max_member "Maximum members allowed (nullable)"
        integer max_product_group "Maximum product groups (nullable)"
        integer max_product "Maximum products (nullable)"
        integer max_category "Maximum categories (nullable)"
        integer max_search_query "Maximum search queries (nullable)"
        integer max_viewpoint "Maximum viewpoints (nullable)"
        string data_visible "Data visibility setting (nullable)"
        tinyint api_available "API availability flag (default 1, nullable)"
        timestamp created_at
        timestamp updated_at
    }
    subscriptions {
        bigint id PK
        string slug "Unique subscription identifier"
        bigint package_id FK
        bigint package_plan_id FK
        bigint group_id FK
        bigint user_id FK
        string pricing_type "standard or custom"
        bigint custom_contract_id FK "Link to custom_contracts (nullable)"
        string status "unpaid, active, cancelled, expired"
        string payment_provider_customer_id "Stripe customer ID"
        string email "Subscription owner email"
        timestamp created_at
        timestamp updated_at
    }
    groups {
        bigint id PK
        string name "Group name"
        bigint created_by FK "User who created group"
        integer status "Group status (1=active)"
        timestamp created_at
        timestamp updated_at
    }
    users {
        bigint id PK
        string name "User full name"
        string email "User email (unique)"
        string payment_provider_customer_id "Stripe customer ID (nullable)"
        timestamp created_at
        timestamp updated_at
    }
    package_plans {
        bigint id PK
        bigint package_id FK
        string name "Plan name"
        string billing_interval "month or year"
        integer amount "Plan amount"
        timestamp created_at
        timestamp updated_at
    }
    
    custom_contracts ||--o| subscriptions : has
    custom_contracts ||--|| groups : belongs_to
    custom_contracts ||--o| users : managed_by
    custom_contracts ||--o| package_plans : based_on
    subscriptions ||--|| groups : belongs_to
    subscriptions ||--|| users : owned_by

エラーハンドリング

  • ログ: すべてのエラーはlogThrow()メソッドで完全な例外詳細とともにログに記録されます
  • トランザクションロールバック: 契約作成中のエラー時に自動ロールバック
  • エラー詳細:
ステータスコード エラーメッセージ 説明
422 検証エラーメッセージ 入力検証が失敗した場合(上記の検証ルールを参照)
400 "事業者が見つかりませんでした" 指定されたグループが存在しない場合
400 "サブスクリプションのタイプ切り替えは許可されていません" グループにアクティブな非カスタムサブスクリプションがある場合
400 "サブスクリプションが見つかりませんでした" 指定されたsubscription_idが存在しない場合
400 "グループとサブスクリプションが一致しません" サブスクリプションがグループに属していない場合
400 "アクティブなサブスクリプションが既に存在します" 新しいサブスクリプションを作成しようとしたが、グループに既にアクティブなものがある場合
400 "パッケージプランが必要です" 新しいサブスクリプションにpackage_plan_idが欠落している場合
400 "パッケージプランが見つかりませんでした" 指定されたパッケージプランが存在しない場合
400 "ユーザーが見つかりませんでした" 指定されたuser_idが存在しない場合
400 例外メッセージを含む汎用エラー 予期しないデータベースまたはシステムエラーの場合

ケース2: 契約作成成功(既存サブスクリプション)

説明

管理者が既にpricing_type = 'custom'を持つ既存のサブスクリプションにリンクされたカスタムプランを作成します。サブスクリプションは新しい契約の詳細で更新されます。

ケース1との主な違い

  • サブスクリプションは既にpricing_type = 'custom'で存在
  • 新しいサブスクリプションの作成は不要
  • 契約レコードのみが作成され、既存のサブスクリプションにリンク
  • サブスクリプションのcustom_contract_idが新しい契約に更新

検証ステップ

  1. サブスクリプションが存在し、グループに属していることを確認
  2. サブスクリプションのpricing_type = 'custom'またはstatusが'cancelled'であることを確認
  3. リクエストで提供されていない場合は、サブスクリプションのpackage_plan_idを使用
  4. その他のステップはケース1と同じフローに従う

追加ノート

契約ステータスのライフサイクル
  • Draft: 契約作成後の初期ステータス、支払いリンクが送信される前
  • Offered: 支払いリンクが顧客に送信された後のステータス
  • Active: 最初の支払い成功後(Stripe webhook経由)のステータス
  • Expired: 契約のends_at日付が過ぎた後のステータス
  • Cancelled: 有効期限前にサブスクリプションがキャンセルされたステータス
Stripe顧客同期
  • ユーザーがpayment_provider_customer_idを持っていない場合、システムは自動的にStripe顧客を作成します
  • 顧客作成にはデータベースのユーザーのメールアドレスと名前を使用
  • 顧客IDはユーザーとサブスクリプションの両方のレコードに保存されます
  • 将来の支払いリンク生成に必要
使用制限
  • すべての制限フィールド(max_member、max_product_groupなど)はオプションです
  • null値はそのリソースに対して無制限を意味します
  • 0値は機能が無効になっていることを意味します
  • 制限は使用中にサブスクリプションモジュールによって強制されます
通貨処理
  • デフォルト通貨は'jpy'(日本円)
  • 金額はマイナー単位で保存されます(例:JPYの場合:1000 = ¥1,000)
  • 通貨コードは自動的に小文字に変換されます
トランザクション管理
  • すべての操作がデータベーストランザクションでラップされます
  • 失敗すると自動ロールバックがトリガーされます
  • subscriptionsテーブルとcontractsテーブル間のデータ整合性を保証します
パフォーマンスの考慮事項
  • すべての操作に対して単一のデータベーストランザクション
  • 最小限の外部API呼び出し(必要な場合のみStripe顧客作成)
  • subscription_idとstatusフィールドのインデックス付きクエリ