サブスクリプションプラン変更比較

概要説明

サブスクリプションプラン変更比較機能により、グループオーナーは新しいサブスクリプションプランへの変更を確認する前に、その影響をプレビューできます。ユーザーがプラン変更(アップグレードまたはダウングレード)をスケジュールすると、新しいプランのリソース制限が既存のグループメンバーとウィッシュリストにどのように影響するかを理解する必要があります。

この機能は、以下の詳細な比較を提供します:

  • 現在のプランと対象プランのリソース制限
  • 新しいメンバー制限を超えるメンバー(ダウングレード時)
  • リソース制約に違反するウィッシュリスト
  • 新しい商品グループ制限を超えるウィッシュリスト

このプロセスでは、グループオーナーが潜在的な影響を確認し、プラン変更を確認する前に、どのメンバーを無効にし、どのウィッシュリストを手動トレーニングステータスに設定するかについて、情報に基づいた決定を行う必要があります。

前提条件:

  • ユーザーが認証されている必要があります
  • ユーザーがグループ作成者(オーナー)である必要があります
  • グループにアクティブなサブスクリプションがある必要があります
  • 保留中のプラン変更(スケジュールされたプラン変更)が必要です

API: サブスクリプション比較変更API


アクティビティ図

---
config:
  theme: base
  layout: dagre
  flowchart:
    curve: linear
    htmlLabels: true
  themeVariables:
    edgeLabelBackground: "transparent"
---
flowchart TB
    %% Main components
    SubscriptionController[SubscriptionController]
    SubscriptionService(SubscriptionService)
    UserService(UserService)
    WishlistToGroupService(WishlistToGroupService)
    SubscriptionModel[[Subscription]]
    SubscriptionHistoryModel[[SubscriptionHistory]]
    GroupModel[[Group]]
    GroupMemberModel[[GroupMember]]
    WishlistToGroupModel[[WishlistToGroup]]
    SubDB[(subscriptions)]
    SubHistDB[(subscription_histories)]
    GroupDB[(groups)]
    GroupMemberDB[(group_members)]
    WishlistDB[(wishlist_to_groups)]
    
    subgraph Controllers
        GetCompareChangeController["GET /subscription/compare-change"]
        ConfirmChangeController["POST /subscription/confirm-change"]
    end
    
    subgraph Services
        SubscriptionService
        UserService
        WishlistToGroupService
    end
    
    subgraph Models
        SubscriptionModel
        SubscriptionHistoryModel
        GroupModel
        GroupMemberModel
        WishlistToGroupModel
    end
    
    subgraph Databases
        SubDB
        SubHistDB
        GroupDB
        GroupMemberDB
        WishlistDB
    end
    
    %% GET compare-change flow
    GetCompareChangeController --- Step1A[
        <div style='text-align: center'>
            <span style='display: inline-block; background-color: #6699cc !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>1A</span>
            <p style='margin-top: 8px'>ユーザー権限の検証</p>
        </div>
    ]
    Step1A --> GetCompareChangeController
    
    GetCompareChangeController --- Step2A[
        <div style='text-align: center'>
            <span style='display: inline-block; background-color: #6699cc !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>2A</span>
            <p style='margin-top: 8px'>アクティブなサブスクリプションを取得</p>
        </div>
    ]
    Step2A --> SubscriptionModel
    
    GetCompareChangeController --- Step3A[
        <div style='text-align: center'>
            <span style='display: inline-block; background-color: #6699cc !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>3A</span>
            <p style='margin-top: 8px'>プラン比較プレビューを取得</p>
        </div>
    ]
    Step3A --> SubscriptionService
    
    GetCompareChangeController --- Step4A[
        <div style='text-align: center'>
            <span style='display: inline-block; background-color: #6699cc !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>4A</span>
            <p style='margin-top: 8px'>メンバー差分を計算</p>
        </div>
    ]
    Step4A --> GroupMemberModel
    
    GetCompareChangeController --- Step5A[
        <div style='text-align: center'>
            <span style='display: inline-block; background-color: #6699cc !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>5A</span>
            <p style='margin-top: 8px'>ウィッシュリスト差分を計算</p>
        </div>
    ]
    Step5A --> WishlistToGroupModel
    
    GetCompareChangeController --- Step6A[
        <div style='text-align: center'>
            <span style='display: inline-block; background-color: #6699cc !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>6A</span>
            <p style='margin-top: 8px'>プレビューレスポンスを返す</p>
        </div>
    ]
    Step6A --> GetCompareChangeController
    
    %% POST confirm-change flow
    ConfirmChangeController --- Step1B[
        <div style='text-align: center'>
            <span style='display: inline-block; background-color: #99cc66 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>1B</span>
            <p style='margin-top: 8px'>ユーザー権限の検証</p>
        </div>
    ]
    Step1B --> ConfirmChangeController
    
    ConfirmChangeController --- Step2B[
        <div style='text-align: center'>
            <span style='display: inline-block; background-color: #99cc66 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>2B</span>
            <p style='margin-top: 8px'>リクエストデータの検証</p>
        </div>
    ]
    Step2B --> ConfirmChangeController
    
    ConfirmChangeController --- Step3B[
        <div style='text-align: center'>
            <span style='display: inline-block; background-color: #99cc66 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>3B</span>
            <p style='margin-top: 8px'>トランザクション開始</p>
        </div>
    ]
    Step3B --> SubscriptionService
    
    ConfirmChangeController --- Step4B[
        <div style='text-align: center'>
            <span style='display: inline-block; background-color: #99cc66 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>4B</span>
            <p style='margin-top: 8px'>メンバーを無効化</p>
        </div>
    ]
    Step4B --> UserService
    
    ConfirmChangeController --- Step5B[
        <div style='text-align: center'>
            <span style='display: inline-block; background-color: #99cc66 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>5B</span>
            <p style='margin-top: 8px'>ウィッシュリストを手動に設定</p>
        </div>
    ]
    Step5B --> WishlistToGroupService
    
    ConfirmChangeController --- Step6B[
        <div style='text-align: center'>
            <span style='display: inline-block; background-color: #99cc66 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>6B</span>
            <p style='margin-top: 8px'>トランザクションをコミット</p>
        </div>
    ]
    Step6B --> SubscriptionService
    
    %% Service to Model relationships
    SubscriptionService -.-> SubscriptionModel
    SubscriptionService -.-> SubscriptionHistoryModel
    SubscriptionService -.-> GroupModel
    UserService -.-> GroupMemberModel
    WishlistToGroupService -.-> WishlistToGroupModel
    
    %% Model to Database relationships
    SubscriptionModel -.-> SubDB
    SubscriptionHistoryModel -.-> SubHistDB
    GroupModel -.-> GroupDB
    GroupMemberModel -.-> GroupMemberDB
    WishlistToGroupModel -.-> WishlistDB
    
    %% Styling
    style SubDB fill:#ffe6cc,stroke:#ff9900,stroke-width:2px
    style SubHistDB fill:#ffe6cc,stroke:#ff9900,stroke-width:2px
    style GroupDB fill:#ffe6cc,stroke:#ff9900,stroke-width:2px
    style GroupMemberDB fill:#ffe6cc,stroke:#ff9900,stroke-width:2px
    style WishlistDB fill:#ffe6cc,stroke:#ff9900,stroke-width:2px
    style Controllers fill:#e6f3ff
    style Services fill:#f0f8e6
    style Models fill:#fff0f5
    style Databases fill:#ffe6cc
    style Step1A fill:transparent,stroke:transparent,stroke-width:1px
    style Step2A fill:transparent,stroke:transparent,stroke-width:1px
    style Step3A fill:transparent,stroke:transparent,stroke-width:1px
    style Step4A fill:transparent,stroke:transparent,stroke-width:1px
    style Step5A fill:transparent,stroke:transparent,stroke-width:1px
    style Step6A fill:transparent,stroke:transparent,stroke-width:1px
    style Step1B fill:transparent,stroke:transparent,stroke-width:1px
    style Step2B fill:transparent,stroke:transparent,stroke-width:1px
    style Step3B fill:transparent,stroke:transparent,stroke-width:1px
    style Step4B fill:transparent,stroke:transparent,stroke-width:1px
    style Step5B fill:transparent,stroke:transparent,stroke-width:1px
    style Step6B fill:transparent,stroke:transparent,stroke-width:1px

ケース1: 比較変更プレビュー取得 - 成功

説明

グループオーナーが現在のサブスクリプションプランと対象プランの変更のプレビューを正常に取得します。システムはメンバー制限とウィッシュリスト制約を分析して、潜在的な影響を特定します。

シーケンス図

sequenceDiagram
    participant Client
    participant Controller as SubscriptionController
    participant Service as SubscriptionService
    participant SubModel as Subscription
    participant HistModel as SubscriptionHistory
    participant GroupModel as Group
    participant MemberModel as GroupMember
    participant WishlistModel as WishlistToGroup
    participant Database
    
    Note over Client,Database: GET /subscription/compare-change
    
    rect rgb(200, 255, 200)
    Note right of Client: 正常ケースフロー
    
    Client->>Controller: GET /subscription/compare-change
    
    rect rgb(255, 255, 200)
    Note right of Controller: 権限チェック
    Controller->>Controller: ユーザーがグループオーナーかチェック
    end
    
    rect rgb(200, 230, 255)
    Note right of Controller: サブスクリプションデータ取得
    Controller->>SubModel: アクティブなサブスクリプションを取得
    SubModel->>Database: SELECT * FROM subscriptions WHERE group_id AND status = 'active'
    Database-->>SubModel: アクティブなサブスクリプションを返す
    SubModel-->>Controller: サブスクリプションオブジェクトを返す
    
    Controller->>HistModel: 現在のプランを取得(最新の支払済み)
    HistModel->>Database: SELECT * FROM subscription_histories WHERE payment_status = 'paid' ORDER BY paid_at DESC
    Database-->>HistModel: 現在のプランを返す
    
    Controller->>HistModel: 対象プランを取得(最新の変更保留中)
    HistModel->>Database: SELECT * FROM subscription_histories WHERE type = 'change' AND payment_status = 'pending'
    Database-->>HistModel: 対象プランを返す
    end
    
    rect rgb(200, 255, 255)
    Note right of Controller: メンバー差分を計算
    Controller->>Service: calculateMemberDifferences(group, currentPlan, targetPlan)
    Service->>MemberModel: 現在のメンバー数を取得
    MemberModel->>Database: SELECT COUNT(*) FROM group_members WHERE group_id
    Database-->>MemberModel: カウントを返す
    Service->>Service: 対象プランのmax_member制限と比較
    Service->>MemberModel: 制限を超える場合、非作成者メンバーを取得
    MemberModel->>Database: SELECT * FROM group_members WHERE group_id AND is_creator = false
    Database-->>MemberModel: メンバーリストを返す
    Service-->>Controller: メンバー差分を返す
    end
    
    rect rgb(230, 200, 255)
    Note right of Controller: ウィッシュリスト差分を計算
    Controller->>Service: calculateWishlistDifferences(group, currentPlan, targetPlan)
    Service->>WishlistModel: 関連データを含むすべてのウィッシュリストを取得
    WishlistModel->>Database: SELECT * FROM wishlist_to_groups WHERE group_id
    Database-->>WishlistModel: ウィッシュリストを返す
    Service->>Service: 各ウィッシュリストを対象制限に対してチェック
    Service->>Service: 強制無効化を特定(リソース制限違反)
    Service->>Service: 任意無効化を特定(max_product_group超過)
    Service-->>Controller: ウィッシュリスト差分を返す
    end
    
    Controller-->>Client: 200 OK (比較データ)
    end
    
    rect rgb(255, 200, 200)
    Note right of Client: エラーシナリオ
    rect rgb(255, 230, 230)
    alt グループオーナーではない
        Controller-->>Client: 403 Forbidden
    else アクティブなサブスクリプションなし
        SubModel-->>Controller: アクティブなサブスクリプションが見つかりません
        Controller-->>Client: 400 Bad Request
    else 対象プランなし
        HistModel-->>Controller: 保留中の変更が見つかりません
        Controller-->>Client: 400 Bad Request
    end
    end
    end

ステップ

ステップ1: 権限チェック

  • 説明: リクエストを行っているユーザーがグループオーナーであることを確認
  • 検証:
    • ユーザーは認証されている必要があります(Bearerトークン)
    • ユーザーのgroupMember.is_creatortrueである必要があります
  • エラー条件: オーナーでない場合、メッセージ「アクセスが拒否されました。」で403 Forbiddenを返す

ステップ2: アクティブなサブスクリプションを取得

  • 説明: グループの現在のアクティブなサブスクリプションを取得
  • アクション: subscriptionsテーブルからstatus = 'active'で一致するgroup_idのサブスクリプションをクエリ
  • エラー条件: アクティブなサブスクリプションが存在しない場合、メッセージ「アクティブなサブスクリプションがありません。」で400を返す

ステップ3: 現在および対象プランを取得

  • 説明: 比較のためにサブスクリプション履歴レコードを取得
  • 現在のプラン: payment_status = 'paid'の最新のsubscription_historyレコード(paid_at DESCでソート)
  • 対象プラン: type = 'change'およびpayment_status = 'pending'の最新のsubscription_historyレコード
  • エラー条件: 対象プランが存在しない場合、メッセージ「変更予定のプランがありません。」で400を返す

ステップ4: メンバー差分を計算

  • 説明: 対象プランのメンバー制限が現在のメンバーにどのように影響するかを分析
  • アクション:
    • group_membersテーブルから現在のメンバー数を取得
    • target_plan.max_memberと比較
    • 制限を超える場合、excess_member_count = current_count - max_memberを計算
    • 無効化できる非作成者メンバー(is_creator = false)のリストを取得
  • レスポンスフィールド:
    • is_over_limit: boolean
    • current_member_count: integer
    • current_member_limit: 現在のプランから
    • new_member_limit: 対象プランから
    • excess_member_count: integer
    • members_to_choose: メンバーオブジェクトの配列(user_id、name、role)

ステップ5: ウィッシュリスト差分を計算

  • 説明: 対象プランのリソース制限がウィッシュリストにどのように影響するかを分析
  • アクション:
    • 関連データを含むグループのすべてのウィッシュリストを取得:
      • wishlistProducts
      • wishlistCategories
      • wishlistSearchQueries
      • wishlistCategoryViewpointReference(ネストされたビューポイントデータを含む)
    • 各ウィッシュリストについて、リソースをカウント:
      • 商品数
      • カテゴリ数
      • 検索クエリ数
      • ビューポイント数
    • ウィッシュリストを分類:
      • 強制無効化: 任意のリソース制限に違反
        • products > max_product
        • categories > max_category
        • search_queries > max_search_query
        • viewpoints > max_viewpoint
      • 任意無効化: 有効なウィッシュリストだがtotal_valid_wishlist > max_product_group
  • レスポンスフィールド:
    • is_over_limit: boolean
    • total_wishlist: 合計数
    • total_valid_wishlist: リソース要件を満たすウィッシュリストの数
    • total_excess: 超過した有効なウィッシュリスト
    • force_deactivation: 理由を含む配列
    • optional_deactivation: 有効なウィッシュリストの配列

ステップ6: 比較レスポンスを返す

  • 説明: 完全な比較データをフォーマットして返す
  • レスポンス構造:
    {
      "status": true,
      "message": "プラン変更のプレビューを取得しました。",
      "data": {
        "current_plan": {
          "slug": "plan_slug",
          "name": "プラン名",
          "limits": {
            "max_member": 5,
            "max_product_group": 10,
            "max_product": 50,
            "max_category": 20,
            "max_search_query": 100,
            "max_viewpoint": 10
          }
        },
        "target_plan": { /* 同じ構造 */ },
        "differences": {
          "members": { /* メンバー差分データ */ },
          "wishlists": { /* ウィッシュリスト差分データ */ }
        }
      }
    }
    

データベース関連テーブルとフィールド

erDiagram
    subscriptions {
        bigint id PK
        bigint package_id FK
        bigint package_plan_id FK
        bigint group_id FK "groupsテーブルへの参照"
        bigint user_id FK
        string status "Active, Canceled, Pending Cancellation, Past Due, Unpaid"
        timestamp created_at
        timestamp updated_at
    }
    subscription_histories {
        bigint id PK
        bigint subscription_id FK "subscriptionsテーブルへの参照"
        bigint package_id FK
        bigint package_plan_id FK
        bigint group_id FK
        bigint user_id FK
        string old_plan_id "プラン変更時の古いパッケージプランデータ"
        string type "サブスクリプションのタイプ"
        integer max_member
        integer max_product_group
        integer max_product
        integer max_category
        integer max_search_query
        integer max_viewpoint
        string payment_status
        tinyint status "0:Inactive, 1: Active"
        timestamp paid_at
        timestamp created_at
    }
    groups {
        bigint id PK
        string name
        bigint created_by FK "usersテーブルへの参照"
        integer status "0: Inactive, 1: Active"
        timestamp created_at
    }
    group_members {
        bigint id PK
        bigint group_id FK "groupsテーブルへの参照"
        bigint user_id FK "usersテーブルへの参照"
        bigint group_role_id FK
        boolean is_creator "メンバーがグループ作成者かどうかを示すフラグ"
        timestamp joined_at
        timestamp created_at
    }
    wishlist_to_groups {
        bigint id PK
        bigint group_id FK "groupsテーブルへの参照"
        bigint created_by FK "usersテーブルへの参照"
        bigint subscription_id FK "subscriptionsテーブルへの参照"
        string name "ウィッシュリストの名前"
        string slug "ウィッシュリストのスラグ"
        integer status "0: Inactive, 1: Active, 3: Canceled"
        timestamp created_at
    }

    subscriptions ||--o{ subscription_histories : has
    groups ||--|| subscriptions : has
    groups ||--o{ group_members : has
    groups ||--o{ wishlist_to_groups : has

エラーハンドリング

ログ:

  • すべてのエラーはlogThrow($th)でアプリケーションログに記録されます
  • 重大なエラーはSlack通知をトリガーする可能性があります

エラー詳細:

ステータスコード エラーメッセージ 説明
401 "未認証です。" ユーザーが認証されていないまたは無効なトークン
403 "アクセスが拒否されました。" ユーザーがグループオーナーではない
400 "アクティブなサブスクリプションがありません。" グループのアクティブなサブスクリプションが見つかりません
400 "変更予定のプランがありません。" 保留中のプラン変更が見つかりません
400 "プラン変更のプレビューに失敗しました。" プレビュー生成中の一般的なエラー

ケース2: 比較変更の確認 - 成功

説明

グループオーナーがプラン変更を確認し、指定されたメンバーの無効化とウィッシュリストのステータス変更を適用します。すべての操作はデータベーストランザクションでラップされ、データの一貫性を確保します。

シーケンス図

sequenceDiagram
    participant Client
    participant Controller as SubscriptionController
    participant Request as ConfirmCompareChangeRequest
    participant Service as SubscriptionService
    participant UserService as UserService
    participant WishlistService as WishlistToGroupService
    participant SubModel as Subscription
    participant HistModel as SubscriptionHistory
    participant GroupMemberModel as GroupMember
    participant WishlistModel as WishlistToGroup
    participant Database
    
    Note over Client,Database: POST /subscription/confirm-change
    
    rect rgb(200, 255, 200)
    Note right of Client: 正常ケースフロー
    
    Client->>Controller: POST /subscription/confirm-change (members_to_inactive, wishlists_to_manual)
    
    rect rgb(255, 255, 200)
    Note right of Controller: 権限チェック
    Controller->>Controller: ユーザーがグループオーナーかチェック
    end
    
    rect rgb(200, 230, 255)
    Note right of Controller: リクエスト検証
    Controller->>Request: validate(data)
    Request->>Request: members_to_inactiveフォーマットを検証
    Request->>GroupMemberModel: メンバーIDがグループに属しているかチェック
    GroupMemberModel->>Database: SELECT user_id FROM group_members WHERE group_id AND user_id IN (...)
    Database-->>GroupMemberModel: 一致するメンバーを返す
    Request->>Request: wishlists_to_manualフォーマットを検証
    Request->>WishlistModel: ウィッシュリストスラグがグループに属しているかチェック
    WishlistModel->>Database: SELECT slug FROM wishlist_to_groups WHERE group_id AND slug IN (...)
    Database-->>WishlistModel: 一致するウィッシュリストを返す
    Request-->>Controller: 検証合格
    end
    
    rect rgb(200, 255, 255)
    Note right of Controller: サブスクリプション状態を確認
    Controller->>SubModel: アクティブなサブスクリプションを取得
    SubModel->>Database: SELECT * FROM subscriptions WHERE group_id AND status = 'active'
    Database-->>SubModel: アクティブなサブスクリプションを返す
    
    Controller->>HistModel: 現在のプランを取得(最新の支払済み)
    HistModel->>Database: SELECT * FROM subscription_histories WHERE subscription_id AND payment_status = 'paid'
    Database-->>HistModel: 現在のプランを返す
    
    Controller->>HistModel: 対象プランを取得(最新の変更保留中)
    HistModel->>Database: SELECT * FROM subscription_histories WHERE subscription_id AND type = 'change'
    Database-->>HistModel: 対象プランを返す
    end
    
    rect rgb(230, 200, 255)
    Note right of Controller: トランザクションで変更を実行
    Controller->>Service: executeCompareChange(members, wishlists)
    Service->>Database: BEGIN TRANSACTION
    
    alt 無効化するメンバー
        Service->>UserService: updateStatusByIds(memberIds, 'inactive')
        UserService->>GroupMemberModel: ユーザーステータスを更新
        GroupMemberModel->>Database: UPDATE users SET status = 'inactive' WHERE id IN (...)
        Database-->>GroupMemberModel: 更新を確認
    end
    
    alt 手動にするウィッシュリスト
        Service->>WishlistService: changeTranningStatusBySlugs(wishlistSlugs, 'manual')
        WishlistService->>WishlistModel: トレーニングステータスを更新
        WishlistModel->>Database: UPDATE wishlist_to_groups SET training_status = 'manual' WHERE slug IN (...)
        Database-->>WishlistModel: 更新を確認
    end
    
    Service->>Database: COMMIT TRANSACTION
    Service-->>Controller: 成功結果を返す
    end
    
    Controller-->>Client: 200 OK (成功メッセージ)
    end
    
    rect rgb(255, 200, 200)
    Note right of Client: エラーシナリオ
    rect rgb(255, 230, 230)
    alt グループオーナーではない
        Controller-->>Client: 403 Forbidden
    else 検証失敗
        Request-->>Controller: 検証エラー
        Controller-->>Client: 400 Bad Request (検証エラー)
    else アクティブなサブスクリプションなし
        SubModel-->>Controller: サブスクリプションが見つかりません
        Controller-->>Client: 400 Bad Request
    else 対象プランなし
        HistModel-->>Controller: 保留中の変更なし
        Controller-->>Client: 400 Bad Request
    else トランザクションエラー
        Database-->>Service: データベースエラー
        Service->>Database: ROLLBACK TRANSACTION
        Service-->>Controller: エラー結果
        Controller-->>Client: 400 Bad Request
    end
    end
    end

ステップ

ステップ1: 権限チェック

  • 説明: リクエストを行っているユーザーがグループオーナーであることを確認
  • ケース1のステップ1と同じ

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

  • 説明: リクエストボディパラメータを検証
  • 検証ルール:
    • members_to_inactive(オプション、文字列):
      • カンマ区切りのユーザーIDである必要があります
      • すべてのユーザーIDは現在のグループに属している必要があります
      • グループ作成者を含めることはできません
    • wishlists_to_manual(オプション、文字列):
      • カンマ区切りのウィッシュリストスラグである必要があります
      • すべてのスラグは現在のグループに属している必要があります
  • 検証クエリ:
    • メンバー用: SELECT user_id FROM group_members WHERE group_id = ? AND user_id IN (?)
    • ウィッシュリスト用: SELECT slug FROM wishlist_to_groups WHERE group_id = ? AND slug IN (?)
  • エラー条件:
    • 無効なメンバーID: メッセージ「次のメンバーはあなたのグループに属していません: {ids}」で400を返す
    • 無効なウィッシュリストスラグ: メッセージ「次のウィッシュリストはあなたのグループに属していません: {slugs}」で400を返す

ステップ3: サブスクリプション状態を確認

  • 説明: サブスクリプションと対象プランが存在することを確認
  • ケース1のステップ2-3と同じ検証

ステップ4: データベーストランザクションを開始

  • 説明: アトミック性のためにデータベーストランザクションを開始
  • アクション: $this->subscriptionRepository->transactionBegin()を呼び出す
  • 目的: すべての変更が一緒に適用されるか、エラー時にロールバックされることを確保

ステップ5: メンバーを無効化(提供されている場合)

  • 説明: 指定されたメンバーのステータスをinactiveに更新
  • アクション:
    • カンマ区切りのメンバーIDを解析: explode(',', $request->input('members_to_inactive'))
    • UserService::updateStatusByIds($memberIds, ActiveOrNotStatus::Inactive)を呼び出す
    • 実行: UPDATE users SET status = 'inactive' WHERE id IN (?)
  • 注意: 現在のグループのメンバーであるユーザーにのみ影響します(ステップ2で検証済み)

ステップ6: ウィッシュリストを手動に設定(提供されている場合)

  • 説明: 指定されたウィッシュリストのtraining_statusをmanualに更新
  • アクション:
    • カンマ区切りのウィッシュリストスラグを解析: explode(',', $request->input('wishlists_to_manual'))
    • WishlistToGroupService::changeTranningStatusBySlugs($wishlistSlugs, WishlistToGroupTraningStatus::Manual)を呼び出す
    • 実行: UPDATE wishlist_to_groups SET training_status = 'manual' WHERE slug IN (?)
  • 目的: これらのウィッシュリストの自動トレーニングを防止

ステップ7: トランザクションをコミット

  • 説明: すべてのデータベース変更をコミット
  • アクション: $this->subscriptionRepository->transactionCommit()を呼び出す
  • 成功条件: すべての更新がエラーなしで完了

ステップ8: 成功レスポンスを返す

  • 説明: クライアントに確認メッセージを返す
  • レスポンス構造:
    {
      "status": true,
      "message": "プラン変更を確認しました。",
      "data": []
    }
    

データベース関連テーブルとフィールド

erDiagram
    users {
        bigint id PK
        string name
        string email
        string status "アカウントステータス: active, inactive, suspended"
        timestamp created_at
        timestamp updated_at
    }
    group_members {
        bigint id PK
        bigint group_id FK
        bigint user_id FK "usersテーブルへの参照"
        boolean is_creator "メンバーがグループ作成者かどうかを示すフラグ"
        timestamp created_at
    }
    wishlist_to_groups {
        bigint id PK
        bigint group_id FK
        string slug "ウィッシュリストのスラグ"
        string training_status "トレーニングステータス: auto, manual"
        integer status "0: Inactive, 1: Active, 3: Canceled"
        timestamp created_at
        timestamp updated_at
    }
    subscriptions {
        bigint id PK
        bigint group_id FK
        string status "Active, Canceled, Pending Cancellation, Past Due, Unpaid"
        timestamp created_at
    }
    subscription_histories {
        bigint id PK
        bigint subscription_id FK
        string type "サブスクリプションのタイプ"
        string payment_status
        timestamp created_at
    }

    users ||--o{ group_members : has
    group_members }o--|| wishlist_to_groups : manages
    subscriptions ||--o{ subscription_histories : tracks

エラーハンドリング

ログ:

  • すべてのエラーはlogThrow($th)で記録されます
  • トランザクションロールバックはデバッグのために記録されます

エラー詳細:

ステータスコード エラーメッセージ 説明
401 "未認証です。" ユーザーが認証されていない
403 "アクセスが拒否されました。" ユーザーがグループオーナーではない
400 "リクエストが正しくありません。" 検証エラー(無効なメンバーIDまたはウィッシュリストスラグ)
400 "アクティブなサブスクリプションがありません。" アクティブなサブスクリプションが見つかりません
400 "変更予定のプランがありません。" 保留中のプラン変更が見つかりません
400 "プラン変更の確認に失敗しました。" トランザクションエラーまたはその他の失敗

追加メモ

ビジネスルール:

  1. グループ作成者(オーナー)のみがこれらのエンドポイントにアクセスできます
  2. メンバーの無効化は非作成者メンバーにのみ影響します
  3. ウィッシュリストの強制無効化は、リソース制限に違反した場合に必要です
  4. ウィッシュリストの任意無効化は、max_product_group制限を超える場合です
  5. すべてのデータベース操作はトランザクションを使用してデータの一貫性を確保します

パフォーマンスの考慮事項:

  • N+1クエリを避けるために関係を積極的にロードします
  • SubscriptionHistoryモデルからgetLimits()ヘルパーメソッドを使用します
  • 可能な場合はプランデータをキャッシュします

セキュリティ:

  • すべてのリクエストで権限チェック
  • 入力検証によりSQLインジェクションを防止
  • グループが所有するリソースのみを変更できます

将来の機能拡張:

  • プラン間のコスト差のプレビューを追加
  • 大規模なメンバーリストの一括操作をサポート
  • ユーザーが気が変わった場合のロールバック機能を追加