Subscription Plan Change Comparison

Overview Description

The Subscription Plan Change Comparison feature allows group owners to preview the impact of changing to a new subscription plan before confirming the change. When a user schedules a plan change (upgrade or downgrade), they need to understand how the new plan's resource limits will affect their existing group members and wishlists.

This feature provides a detailed comparison showing:

  • Current vs target plan resource limits
  • Members that exceed the new member limit (if downgrading)
  • Wishlists that violate resource constraints
  • Wishlists that exceed the new product group limit

The process requires the group owner to review potential impacts and make informed decisions about which members to deactivate and which wishlists to set to manual training status before the plan change can be confirmed.

Pre-conditions:

  • User must be authenticated
  • User must be the group creator (owner)
  • Group must have an active subscription
  • Must have a pending plan change (scheduled plan change)

API: Subscription Compare Change API


Activity Diagram

---
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'>Validate User Authorization</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'>Get Active Subscription</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'>Get Plan Comparison Preview</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'>Calculate Member Differences</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'>Calculate Wishlist Differences</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'>Return Preview Response</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'>Validate User Authorization</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'>Validate Request Data</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'>Begin Transaction</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'>Deactivate Members</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'>Set Wishlists to Manual</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'>Commit Transaction</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

Case 1: Get Compare Change Preview - Success

Description

Group owner successfully retrieves a preview of the changes between the current subscription plan and the target plan. The system analyzes member limits and wishlist constraints to identify potential impacts.

Sequence Diagram

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: Happy Case Flow
    
    Client->>Controller: GET /subscription/compare-change
    
    rect rgb(255, 255, 200)
    Note right of Controller: Authorization Check
    Controller->>Controller: Check user is group owner
    end
    
    rect rgb(200, 230, 255)
    Note right of Controller: Fetch Subscription Data
    Controller->>SubModel: Get active subscription
    SubModel->>Database: SELECT * FROM subscriptions WHERE group_id AND status = 'active'
    Database-->>SubModel: Return active subscription
    SubModel-->>Controller: Return subscription object
    
    Controller->>HistModel: Get current plan (latest paid)
    HistModel->>Database: SELECT * FROM subscription_histories WHERE payment_status = 'paid' ORDER BY paid_at DESC
    Database-->>HistModel: Return current plan
    
    Controller->>HistModel: Get target plan (latest pending change)
    HistModel->>Database: SELECT * FROM subscription_histories WHERE type = 'change' AND payment_status = 'pending'
    Database-->>HistModel: Return target plan
    end
    
    rect rgb(200, 255, 255)
    Note right of Controller: Calculate Member Differences
    Controller->>Service: calculateMemberDifferences(group, currentPlan, targetPlan)
    Service->>MemberModel: Get current member count
    MemberModel->>Database: SELECT COUNT(*) FROM group_members WHERE group_id
    Database-->>MemberModel: Return count
    Service->>Service: Compare with target plan max_member limit
    Service->>MemberModel: Get non-creator members if over limit
    MemberModel->>Database: SELECT * FROM group_members WHERE group_id AND is_creator = false
    Database-->>MemberModel: Return members list
    Service-->>Controller: Return member differences
    end
    
    rect rgb(230, 200, 255)
    Note right of Controller: Calculate Wishlist Differences
    Controller->>Service: calculateWishlistDifferences(group, currentPlan, targetPlan)
    Service->>WishlistModel: Get all wishlists with related data
    WishlistModel->>Database: SELECT * FROM wishlist_to_groups WHERE group_id
    Database-->>WishlistModel: Return wishlists
    Service->>Service: Check each wishlist against target limits
    Service->>Service: Identify force deactivation (violates resource limits)
    Service->>Service: Identify optional deactivation (exceeds max_product_group)
    Service-->>Controller: Return wishlist differences
    end
    
    Controller-->>Client: 200 OK (comparison data)
    end
    
    rect rgb(255, 200, 200)
    Note right of Client: Error Scenarios
    rect rgb(255, 230, 230)
    alt Not Group Owner
        Controller-->>Client: 403 Forbidden
    else No Active Subscription
        SubModel-->>Controller: No active subscription found
        Controller-->>Client: 400 Bad Request
    else No Target Plan
        HistModel-->>Controller: No pending change found
        Controller-->>Client: 400 Bad Request
    end
    end
    end

Steps

Step 1: Authorization Check

  • Description: Verify that the user making the request is the group owner
  • Validation:
    • User must be authenticated (Bearer token)
    • User's groupMember.is_creator must be true
  • Error condition: If not owner, return 403 Forbidden with message "アクセスが拒否されました。"

Step 2: Fetch Active Subscription

  • Description: Retrieve the group's current active subscription
  • Action: Query subscriptions table for subscription with status = 'active' and matching group_id
  • Error condition: If no active subscription exists, return 400 with message "アクティブなサブスクリプションがありません。"

Step 3: Get Current and Target Plans

  • Description: Retrieve subscription history records for comparison
  • Current Plan: Latest subscription_history record with payment_status = 'paid' (ordered by paid_at DESC)
  • Target Plan: Latest subscription_history record with type = 'change' and payment_status = 'pending'
  • Error condition: If no target plan exists, return 400 with message "変更予定のプランがありません。"

Step 4: Calculate Member Differences

  • Description: Analyze how the target plan's member limit affects current members
  • Actions:
    • Get current member count from group_members table
    • Compare with target_plan.max_member
    • If over limit, calculate excess_member_count = current_count - max_member
    • Fetch list of non-creator members (is_creator = false) that can be deactivated
  • Response fields:
    • is_over_limit: boolean
    • current_member_count: integer
    • current_member_limit: from current plan
    • new_member_limit: from target plan
    • excess_member_count: integer
    • members_to_choose: array of member objects (user_id, name, role)

Step 5: Calculate Wishlist Differences

  • Description: Analyze how the target plan's resource limits affect wishlists
  • Actions:
    • Fetch all wishlists for the group with related data:
      • wishlistProducts
      • wishlistCategories
      • wishlistSearchQueries
      • wishlistCategoryViewpointReference (with nested viewpoint data)
    • For each wishlist, count resources:
      • Product count
      • Category count
      • Search query count
      • Viewpoint count
    • Classify wishlists:
      • Force Deactivation: Violates ANY resource limit
        • products > max_product
        • categories > max_category
        • search_queries > max_search_query
        • viewpoints > max_viewpoint
      • Optional Deactivation: Valid wishlists but total_valid_wishlist > max_product_group
  • Response fields:
    • is_over_limit: boolean
    • total_wishlist: total count
    • total_valid_wishlist: count of wishlists meeting resource requirements
    • total_excess: excess valid wishlists
    • force_deactivation: array with reasons
    • optional_deactivation: array of valid wishlists

Step 6: Return Comparison Response

  • Description: Format and return the complete comparison data
  • Response structure:
    {
      "status": true,
      "message": "プラン変更のプレビューを取得しました。",
      "data": {
        "current_plan": {
          "slug": "plan_slug",
          "name": "Plan Name",
          "limits": {
            "max_member": 5,
            "max_product_group": 10,
            "max_product": 50,
            "max_category": 20,
            "max_search_query": 100,
            "max_viewpoint": 10
          }
        },
        "target_plan": { /* same structure */ },
        "differences": {
          "members": { /* member diff data */ },
          "wishlists": { /* wishlist diff data */ }
        }
      }
    }
    

Database Related Tables & Fields

erDiagram
    subscriptions {
        bigint id PK
        bigint package_id FK
        bigint package_plan_id FK
        bigint group_id FK "Reference to groups table"
        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 "Reference to subscriptions table"
        bigint package_id FK
        bigint package_plan_id FK
        bigint group_id FK
        bigint user_id FK
        string old_plan_id "Old package plan data, has data when plan is changed"
        string type "The type of the subscription"
        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 "Reference to users table"
        integer status "0: Inactive, 1: Active"
        timestamp created_at
    }
    group_members {
        bigint id PK
        bigint group_id FK "Reference to groups table"
        bigint user_id FK "Reference to users table"
        bigint group_role_id FK
        boolean is_creator "Flag indicating if member is the group creator"
        timestamp joined_at
        timestamp created_at
    }
    wishlist_to_groups {
        bigint id PK
        bigint group_id FK "Reference to groups table"
        bigint created_by FK "Reference to users table"
        bigint subscription_id FK "Reference to subscriptions table"
        string name "Name of the wishlist"
        string slug "Slug of the wishlist"
        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

Error Handling

Log:

  • All errors logged to application log with logThrow($th)
  • Critical errors may trigger Slack notifications

Error Detail:

Status Code Error Message Description
401 "未認証です。" User not authenticated or invalid token
403 "アクセスが拒否されました。" User is not the group owner
400 "アクティブなサブスクリプションがありません。" No active subscription found for group
400 "変更予定のプランがありません。" No pending plan change found
400 "プラン変更のプレビューに失敗しました。" General error during preview generation

Case 2: Confirm Compare Change - Success

Description

Group owner confirms the plan change and applies the specified member deactivations and wishlist status changes. All operations are wrapped in a database transaction to ensure data consistency.

Sequence Diagram

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: Happy Case Flow
    
    Client->>Controller: POST /subscription/confirm-change (members_to_inactive, wishlists_to_manual)
    
    rect rgb(255, 255, 200)
    Note right of Controller: Authorization Check
    Controller->>Controller: Check user is group owner
    end
    
    rect rgb(200, 230, 255)
    Note right of Controller: Request Validation
    Controller->>Request: validate(data)
    Request->>Request: Validate members_to_inactive format
    Request->>GroupMemberModel: Check member IDs belong to group
    GroupMemberModel->>Database: SELECT user_id FROM group_members WHERE group_id AND user_id IN (...)
    Database-->>GroupMemberModel: Return matching members
    Request->>Request: Validate wishlists_to_manual format
    Request->>WishlistModel: Check wishlist slugs belong to group
    WishlistModel->>Database: SELECT slug FROM wishlist_to_groups WHERE group_id AND slug IN (...)
    Database-->>WishlistModel: Return matching wishlists
    Request-->>Controller: Validation passed
    end
    
    rect rgb(200, 255, 255)
    Note right of Controller: Verify Subscription State
    Controller->>SubModel: Get active subscription
    SubModel->>Database: SELECT * FROM subscriptions WHERE group_id AND status = 'active'
    Database-->>SubModel: Return active subscription
    
    Controller->>HistModel: Get current plan (latest paid)
    HistModel->>Database: SELECT * FROM subscription_histories WHERE subscription_id AND payment_status = 'paid'
    Database-->>HistModel: Return current plan
    
    Controller->>HistModel: Get target plan (latest change pending)
    HistModel->>Database: SELECT * FROM subscription_histories WHERE subscription_id AND type = 'change'
    Database-->>HistModel: Return target plan
    end
    
    rect rgb(230, 200, 255)
    Note right of Controller: Execute Change with Transaction
    Controller->>Service: executeCompareChange(members, wishlists)
    Service->>Database: BEGIN TRANSACTION
    
    alt Members to Deactivate
        Service->>UserService: updateStatusByIds(memberIds, 'inactive')
        UserService->>GroupMemberModel: Update user status
        GroupMemberModel->>Database: UPDATE users SET status = 'inactive' WHERE id IN (...)
        Database-->>GroupMemberModel: Confirm update
    end
    
    alt Wishlists to Manual
        Service->>WishlistService: changeTranningStatusBySlugs(wishlistSlugs, 'manual')
        WishlistService->>WishlistModel: Update training status
        WishlistModel->>Database: UPDATE wishlist_to_groups SET training_status = 'manual' WHERE slug IN (...)
        Database-->>WishlistModel: Confirm update
    end
    
    Service->>Database: COMMIT TRANSACTION
    Service-->>Controller: Return success result
    end
    
    Controller-->>Client: 200 OK (success message)
    end
    
    rect rgb(255, 200, 200)
    Note right of Client: Error Scenarios
    rect rgb(255, 230, 230)
    alt Not Group Owner
        Controller-->>Client: 403 Forbidden
    else Validation Failed
        Request-->>Controller: Validation errors
        Controller-->>Client: 400 Bad Request (validation errors)
    else No Active Subscription
        SubModel-->>Controller: No subscription found
        Controller-->>Client: 400 Bad Request
    else No Target Plan
        HistModel-->>Controller: No pending change
        Controller-->>Client: 400 Bad Request
    else Transaction Error
        Database-->>Service: Database error
        Service->>Database: ROLLBACK TRANSACTION
        Service-->>Controller: Error result
        Controller-->>Client: 400 Bad Request
    end
    end
    end

Steps

Step 1: Authorization Check

  • Description: Verify that the user making the request is the group owner
  • Same as Case 1, Step 1

Step 2: Request Validation

  • Description: Validate the request body parameters
  • Validation rules:
    • members_to_inactive (optional, string):
      • Must be comma-separated user IDs
      • All user IDs must belong to the current group
      • Cannot include the group creator
    • wishlists_to_manual (optional, string):
      • Must be comma-separated wishlist slugs
      • All slugs must belong to the current group
  • Validation queries:
    • For members: SELECT user_id FROM group_members WHERE group_id = ? AND user_id IN (?)
    • For wishlists: SELECT slug FROM wishlist_to_groups WHERE group_id = ? AND slug IN (?)
  • Error conditions:
    • Invalid member IDs: Return 400 with message "The following members do not belong to your group: {ids}"
    • Invalid wishlist slugs: Return 400 with message "The following wishlists do not belong to your group: {slugs}"

Step 3: Verify Subscription State

  • Description: Ensure subscription and target plan exist
  • Same validation as Case 1, Steps 2-3

Step 4: Begin Database Transaction

  • Description: Start a database transaction for atomicity
  • Action: Call $this->subscriptionRepository->transactionBegin()
  • Purpose: Ensure all changes are applied together or rolled back on error

Step 5: Deactivate Members (if provided)

  • Description: Update status of specified members to inactive
  • Action:
    • Parse comma-separated member IDs: explode(',', $request->input('members_to_inactive'))
    • Call UserService::updateStatusByIds($memberIds, ActiveOrNotStatus::Inactive)
    • Execute: UPDATE users SET status = 'inactive' WHERE id IN (?)
  • Note: Only affects users who are members of the current group (validated in Step 2)

Step 6: Set Wishlists to Manual (if provided)

  • Description: Update training_status of specified wishlists to manual
  • Action:
    • Parse comma-separated wishlist slugs: explode(',', $request->input('wishlists_to_manual'))
    • Call WishlistToGroupService::changeTranningStatusBySlugs($wishlistSlugs, WishlistToGroupTraningStatus::Manual)
    • Execute: UPDATE wishlist_to_groups SET training_status = 'manual' WHERE slug IN (?)
  • Purpose: Prevent automatic training for these wishlists

Step 7: Commit Transaction

  • Description: Commit all database changes
  • Action: Call $this->subscriptionRepository->transactionCommit()
  • Success condition: All updates completed without errors

Step 8: Return Success Response

  • Description: Return confirmation message to client
  • Response structure:
    {
      "status": true,
      "message": "プラン変更を確認しました。",
      "data": []
    }
    

Database Related Tables & Fields

erDiagram
    users {
        bigint id PK
        string name
        string email
        string status "Account status: active, inactive, suspended"
        timestamp created_at
        timestamp updated_at
    }
    group_members {
        bigint id PK
        bigint group_id FK
        bigint user_id FK "Reference to users table"
        boolean is_creator "Flag indicating if member is the group creator"
        timestamp created_at
    }
    wishlist_to_groups {
        bigint id PK
        bigint group_id FK
        string slug "Slug of the wishlist"
        string training_status "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 "The type of the subscription"
        string payment_status
        timestamp created_at
    }

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

Error Handling

Log:

  • All errors logged with logThrow($th)
  • Transaction rollback logged for debugging

Error Detail:

Status Code Error Message Description
401 "未認証です。" User not authenticated
403 "アクセスが拒否されました。" User is not the group owner
400 "リクエストが正しくありません。" Validation errors (invalid member IDs or wishlist slugs)
400 "アクティブなサブスクリプションがありません。" No active subscription found
400 "変更予定のプランがありません。" No pending plan change found
400 "プラン変更の確認に失敗しました。" Transaction error or other failure

Additional Notes

Business Rules:

  1. Only group creators (owners) can access these endpoints
  2. Member deactivation only affects non-creator members
  3. Wishlist force deactivation is required when resource limits are violated
  4. Wishlist optional deactivation is for exceeding max_product_group limit
  5. All database operations use transactions for data consistency

Performance Considerations:

  • Eager load relationships to avoid N+1 queries
  • Use getLimits() helper method from SubscriptionHistory model
  • Index on status columns for faster queries

Security:

  • Authorization check on every request
  • Input validation prevents SQL injection
  • Only group-owned resources can be modified

Future Enhancements:

  • Add preview of cost differences between plans
  • Support bulk operations for large member lists
  • Add rollback capability if user changes mind