Tạo Hợp Đồng Tùy Chỉnh

Mô Tả Tổng Quan

Tính năng Tạo Hợp Đồng Tùy Chỉnh cho phép quản trị viên tạo các thỏa thuận giá tùy chỉnh cho nhóm. Endpoint này tạo một hợp đồng tùy chỉnh mới với các điều khoản được chỉ định bao gồm số tiền thanh toán, khoảng thời gian và giới hạn sử dụng. Hợp đồng có thể được liên kết với một đăng ký hiện có hoặc một đăng ký mới sẽ được tạo tự động.

Điều Kiện Tiên Quyết

  • Người dùng phải được xác thực với vai trò quản trị viên (SuperAdmin hoặc AdminStaff)
  • Nhóm mục tiêu phải tồn tại và đang hoạt động
  • Nếu liên kết với đăng ký hiện có, nó phải:
    • Thuộc về nhóm được chỉ định
    • pricing_type = 'custom' hoặc đã bị hủy
    • Không có đăng ký không tùy chỉnh đang hoạt động

Phụ Thuộc

  • Mô-đun Quản Lý Nhóm (để xác thực nhóm)
  • Mô-đun Đăng Ký (để tạo/liên kết đăng ký)
  • Dịch Vụ Người Dùng (để đồng bộ hóa khách hàng Stripe)

Liên Kết Swagger

API: Tạo Hợp Đồng Tùy Chỉnh

Tài Liệu Trường Hợp

Trường Hợp 1: Tạo Hợp Đồng Thành Công (Đăng Ký Mới)

Mô Tả

Quản trị viên tạo thành công một hợp đồng tùy chỉnh cho một nhóm không có đăng ký đang hoạt động. Một đăng ký mới với pricing_type = 'custom' sẽ được tạo tự động.

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

Các Bước

Bước 1: Xác Thực Yêu Cầu

  • Mô Tả: Hệ thống xác thực tất cả dữ liệu đầu vào theo quy tắc nghiệp vụ
  • Yêu Cầu: POST /api/v1/admin/custom-contracts
  • Ủy Quyền: Người dùng phải có vai trò SuperAdmin hoặc AdminStaff
  • Quy Tắc Xác Thực:
    • 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
    • Lưu Ý: Phải cung cấp ít nhất một trong hai trường subscription_id hoặc package_plan_id
    • 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

Bước 2: Xác Thực Nhóm

  • Mô Tả: Xác minh nhóm mục tiêu tồn tại và kiểm tra ràng buộc đăng ký
  • Hành Động:
    • Truy vấn nhóm theo group_id
    • Kiểm tra xem nhóm có đăng ký đang hoạt động không
    • Nếu có đăng ký đang hoạt động, xác minh nó là:
      • Đã là loại tùy chỉnh (pricing_type = 'custom')
      • Hoặc đã bị hủy (status = 'cancelled')
  • Quy Tắc Nghiệp Vụ:
    • Không thể tạo hợp đồng tùy chỉnh cho nhóm có đăng ký không tùy chỉnh đang hoạt động
    • Nhóm phải tồn tại và đang hoạt động
  • Lỗi Có Thể Xảy Ra:
    • Không tìm thấy nhóm (404)
    • Đăng ký không tùy chỉnh đang hoạt động tồn tại (400)

Bước 3: Tạo Hoặc Xác Thực Đăng Ký

  • Mô Tả: Tạo đăng ký mới hoặc xác thực đăng ký hiện có
  • Hành Động Cho Đăng Ký Mới:
    • Xác minh package_plan_id được cung cấp
    • Lấy chi tiết gói
    • Lấy người dùng (từ user_id hoặc group.created_by)
    • Đồng bộ ID khách hàng Stripe nếu thiếu
    • Tạo bản ghi đăng ký với:
      • pricing_type = 'custom'
      • status = 'unpaid'
      • Liên kết với gói và nhóm
  • Hành Động Cho Đăng Ký Hiện Có:
    • Xác minh đăng ký thuộc về nhóm
    • Kiểm tra pricing_type của đăng ký là 'custom' hoặc status là 'cancelled'
    • Sử dụng package_plan_id của đăng ký nếu không được cung cấp
  • Thao Tác Cơ Sở Dữ Liệu:
    • INSERT INTO subscriptions (cho mới)
    • hoặc SELECT subscription (cho hiện có)

Bước 4: Tạo Hợp Đồng

  • Mô Tả: Tạo bản ghi hợp đồng tùy chỉnh với tất cả các điều khoản
  • Hành Động:
    • Chèn bản ghi hợp đồng với:
      • Liên kết đăng ký (subscription_id)
      • Liên kết nhóm và người dùng
      • Điều khoản thanh toán (code, amount, currency, billing_interval)
      • Phạm vi ngày (starts_at, ends_at)
      • Giới hạn sử dụng (max_member, max_product_group, v.v.)
      • Cài đặt khả năng hiển thị dữ liệu và API
    • Đặt trạng thái ban đầu thành 'draft'
  • Thao Tác Cơ Sở Dữ Liệu:
    • INSERT INTO custom_contracts
    • Đặt timestamps (created_at, updated_at)

Bước 5: Cập Nhật Đăng Ký

  • Mô Tả: Liên kết đăng ký với hợp đồng và đặt loại giá
  • Hành Động:
    • Cập nhật bản ghi đăng ký:
      • Đặt pricing_type = 'custom'
      • Đặt custom_contract_id = contract.id
    • Commit transaction
  • Thao Tác Cơ Sở Dữ Liệu:
    • UPDATE subscriptions SET pricing_type, custom_contract_id

Bước 6: Trả Về Phản Hồi

  • Mô Tả: Trả về hợp đồng đã tạo cùng với các mối quan hệ
  • Dữ Liệu Phản Hồi:
    • ID hợp đồng và chi tiết
    • Dữ liệu đăng ký liên quan
    • Dữ liệu nhóm liên quan
    • Dữ liệu người dùng liên quan (nếu có)
  • Thông Báo Thành Công: "カスタムプランが正常に作成されました" (Đã tạo thành công gói tùy chỉnh)

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

Xử Lý Lỗi

  • Nhật Ký: Tất cả lỗi được ghi nhật ký qua phương thức logThrow() với đầy đủ chi tiết ngoại lệ
  • Hoàn Tác Giao Dịch: Tự động hoàn tác khi có lỗi trong quá trình tạo hợp đồng
  • Chi Tiết Lỗi:
Mã Trạng Thái Thông Báo Lỗi Mô Tả
422 Thông báo lỗi xác thực Khi xác thực đầu vào thất bại (xem quy tắc xác thực ở trên)
400 "事業者が見つかりませんでした" Khi nhóm được chỉ định không tồn tại
400 "サブスクリプションのタイプ切り替えは許可されていません" Khi nhóm có đăng ký không tùy chỉnh đang hoạt động
400 "サブスクリプションが見つかりませんでした" Khi subscription_id được chỉ định không tồn tại
400 "グループとサブスクリプションが一致しません" Khi đăng ký không thuộc về nhóm
400 "アクティブなサブスクリプションが既に存在します" Khi cố gắng tạo đăng ký mới nhưng nhóm đã có đăng ký đang hoạt động
400 "パッケージプランが必要です" Khi package_plan_id thiếu cho đăng ký mới
400 "パッケージプランが見つかりませんでした" Khi gói được chỉ định không tồn tại
400 "ユーザーが見つかりませんでした" Khi user_id được chỉ định không tồn tại
400 Lỗi chung với thông báo ngoại lệ Cho các lỗi cơ sở dữ liệu hoặc hệ thống không mong đợi

Trường Hợp 2: Tạo Hợp Đồng Thành Công (Đăng Ký Hiện Có)

Mô Tả

Quản trị viên tạo một hợp đồng tùy chỉnh được liên kết với một đăng ký hiện có đã có pricing_type = 'custom'. Đăng ký được cập nhật với chi tiết hợp đồng mới.

Sự Khác Biệt Chính So Với Trường Hợp 1

  • Đăng ký đã tồn tại với pricing_type = 'custom'
  • Không cần tạo đăng ký mới
  • Chỉ bản ghi hợp đồng được tạo và liên kết với đăng ký hiện có
  • custom_contract_id của đăng ký được cập nhật thành hợp đồng mới

Các Bước Xác Thực

  1. Xác minh đăng ký tồn tại và thuộc về nhóm
  2. Kiểm tra pricing_type = 'custom' của đăng ký hoặc status là 'cancelled'
  3. Sử dụng package_plan_id của đăng ký nếu không được cung cấp trong yêu cầu
  4. Tất cả các bước khác theo cùng luồng như Trường Hợp 1

Ghi Chú Bổ Sung

Vòng Đời Trạng Thái Hợp Đồng
  • Draft: Trạng thái ban đầu sau khi tạo hợp đồng, trước khi gửi liên kết thanh toán
  • Offered: Trạng thái sau khi gửi liên kết thanh toán cho khách hàng
  • Active: Trạng thái sau khi thanh toán thành công lần đầu (qua Stripe webhook)
  • Expired: Trạng thái khi ngày ends_at của hợp đồng đã qua
  • Cancelled: Trạng thái khi đăng ký bị hủy trước khi hết hạn
Đồng Bộ Hóa Khách Hàng Stripe
  • Nếu người dùng không có payment_provider_customer_id, hệ thống tự động tạo khách hàng Stripe
  • Tạo khách hàng sử dụng email và tên người dùng từ cơ sở dữ liệu
  • ID khách hàng được lưu trong cả bản ghi người dùng và đăng ký
  • Cần thiết cho việc tạo liên kết thanh toán trong tương lai
Giới Hạn Sử Dụng
  • Tất cả các trường giới hạn (max_member, max_product_group, v.v.) là tùy chọn
  • Giá trị null có nghĩa là không giới hạn cho tài nguyên đó
  • Giá trị 0 có nghĩa là tính năng bị vô hiệu hóa
  • Giới hạn được thực thi bởi mô-đun đăng ký trong quá trình sử dụng
Xử Lý Tiền Tệ
  • Tiền tệ mặc định là 'jpy' (Yên Nhật)
  • Số tiền được lưu ở đơn vị nhỏ (ví dụ: đối với JPY: 1000 = ¥1,000)
  • Mã tiền tệ được tự động chuyển thành chữ thường
Quản Lý Giao Dịch
  • Toàn bộ hoạt động được bọc trong giao dịch cơ sở dữ liệu
  • Bất kỳ lỗi nào sẽ kích hoạt hoàn tác tự động
  • Đảm bảo tính nhất quán dữ liệu giữa các bảng subscriptions và contracts
Cân Nhắc Hiệu Suất
  • Một giao dịch cơ sở dữ liệu duy nhất cho tất cả các hoạt động
  • Tối thiểu các lời gọi API bên ngoài (chỉ tạo khách hàng Stripe nếu cần)
  • Truy vấn có chỉ mục trên các trường subscription_id và status