Create Custom Contract

Overview Description

The Create Custom Contract feature allows administrators to create custom pricing agreements for groups. This endpoint creates a new custom contract with specified terms including billing amount, interval, and usage limits. The contract can be linked to an existing subscription or a new subscription will be created automatically.

Pre-conditions

  • User must be authenticated as admin (SuperAdmin or AdminStaff role)
  • Target group must exist and be active
  • If linking to existing subscription, it must:
    • Belong to the specified group
    • Have pricing_type = 'custom' or be cancelled
    • Not have an active non-custom subscription

Dependencies

  • Group Management Module (for group validation)
  • Subscription Module (for subscription creation/linking)
  • User Service (for Stripe customer synchronization)

Swagger Link

API: Create Custom Contract

Case Documentation

Case 1: Successful Contract Creation (New Subscription)

Description

Administrator successfully creates a custom contract for a group that doesn't have an active subscription. A new subscription with pricing_type = 'custom' is automatically created.

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

Steps

Step 1: Request Validation

  • Description: System validates all input data according to business rules
  • Request: POST /api/v1/admin/custom-contracts
  • Authorization: User must have SuperAdmin or AdminStaff role
  • Validation Rules:
    • 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
    • Note: Either subscription_id or package_plan_id must be provided (at least one is required)
    • 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

Step 2: Group Validation

  • Description: Verify target group exists and check subscription constraints
  • Action:
    • Query group by group_id
    • Check if group has active subscription
    • If active subscription exists, verify it's either:
      • Already custom type (pricing_type = 'custom')
      • Or cancelled (status = 'cancelled')
  • Business Rules:
    • Cannot create custom contract for group with active non-custom subscription
    • Group must exist and be active
  • Potential Errors:
    • Group not found (404)
    • Active non-custom subscription exists (400)

Step 3: Subscription Creation or Validation

  • Description: Create new subscription or validate existing one
  • Action for New Subscription:
    • Verify package_plan_id is provided
    • Get package plan details
    • Get user (from user_id or group.created_by)
    • Sync Stripe customer ID if missing
    • Create subscription record with:
      • pricing_type = 'custom'
      • status = 'unpaid'
      • Link to package plan and group
  • Action for Existing Subscription:
    • Verify subscription belongs to the group
    • Check subscription pricing_type is 'custom' or status is 'cancelled'
    • Use subscription's package_plan_id if not provided
  • Database Operations:
    • INSERT INTO subscriptions (for new)
    • or SELECT subscription (for existing)

Step 4: Contract Creation

  • Description: Create custom contract record with all terms
  • Action:
    • Insert contract record with:
      • Subscription link (subscription_id)
      • Group and user links
      • Billing terms (code, amount, currency, billing_interval)
      • Date range (starts_at, ends_at)
      • Usage limits (max_member, max_product_group, etc.)
      • Data visibility and API availability settings
    • Set initial status to 'draft'
  • Database Operations:
    • INSERT INTO custom_contracts
    • Set timestamps (created_at, updated_at)

Step 5: Update Subscription

  • Description: Link subscription to contract and set pricing type
  • Action:
    • Update subscription record:
      • Set pricing_type = 'custom'
      • Set custom_contract_id = contract.id
    • Commit transaction
  • Database Operations:
    • UPDATE subscriptions SET pricing_type, custom_contract_id

Step 6: Return Response

  • Description: Return created contract with relationships
  • Response Data:
    • Contract ID and details
    • Related subscription data
    • Related group data
    • Related user data (if applicable)
  • Success Message: "カスタムプランが正常に作成されました" (Custom plan created successfully)

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

Error Handling

  • Log: All errors are logged via logThrow() method with full exception details
  • Transaction Rollback: Automatic rollback on any error during contract creation
  • Error Detail:
Status Code Error Message Description
422 Validation error messages When input validation fails (see validation rules above)
400 "事業者が見つかりませんでした" When specified group doesn't exist
400 "サブスクリプションのタイプ切り替えは許可されていません" When group has active non-custom subscription
400 "サブスクリプションが見つかりませんでした" When specified subscription_id doesn't exist
400 "グループとサブスクリプションが一致しません" When subscription doesn't belong to the group
400 "アクティブなサブスクリプションが既に存在します" When trying to create new subscription but group already has active one
400 "パッケージプランが必要です" When package_plan_id missing for new subscription
400 "パッケージプランが見つかりませんでした" When specified package_plan doesn't exist
400 "ユーザーが見つかりませんでした" When specified user_id doesn't exist
400 Generic error with exception message For unexpected database or system errors

Case 2: Successful Contract Creation (Existing Subscription)

Description

Administrator creates a custom contract linked to an existing subscription that already has pricing_type = 'custom'. The subscription is updated with the new contract details.

Key Differences from Case 1

  • Subscription already exists with pricing_type = 'custom'
  • No new subscription creation needed
  • Only contract record is created and linked to existing subscription
  • Subscription's custom_contract_id is updated to new contract

Validation Steps

  1. Verify subscription exists and belongs to the group
  2. Check subscription pricing_type = 'custom' or status is 'cancelled'
  3. Use subscription's package_plan_id if not provided in request
  4. All other steps follow same flow as Case 1

Additional Notes

Contract Status Lifecycle
  • Draft: Initial status after contract creation, before payment link is sent
  • Offered: Status after payment link is sent to customer
  • Active: Status after first successful payment (via Stripe webhook)
  • Expired: Status when contract ends_at date has passed
  • Cancelled: Status when subscription is cancelled before expiry
Stripe Customer Synchronization
  • If user doesn't have payment_provider_customer_id, system automatically creates Stripe customer
  • Customer creation uses user's email and name from database
  • Customer ID is stored in both user and subscription records
  • Required for future payment link generation
Usage Limits
  • All limit fields (max_member, max_product_group, etc.) are optional
  • null value means unlimited for that resource
  • 0 value means feature is disabled
  • Limits are enforced by subscription module during usage
Currency Handling
  • Default currency is 'jpy' (Japanese Yen)
  • Amount is stored in minor unit (e.g., for JPY: 1000 = ¥1,000)
  • Currency code is automatically converted to lowercase
Transaction Management
  • Entire operation wrapped in database transaction
  • Any failure triggers automatic rollback
  • Ensures data consistency across subscriptions and contracts tables
Performance Considerations
  • Single database transaction for all operations
  • Minimal external API calls (only Stripe customer creation if needed)
  • Indexed queries on subscription_id and status fields