Custom Plan (Custom Contract)

Mô Tả

Tài liệu này mô tả flow custom plan (giá tùy chỉnh theo hợp đồng) sử dụng Stripe Checkout với price_data. Custom plan tách biệt khỏi base plan mapping để tránh ảnh hưởng đến catalog price và flow đổi gói tiêu chuẩn.

Data Model Chính

subscriptions

  • pricing_type: custom
  • custom_contract_id: liên kết hợp đồng
  • payment_provider_subscription_id: Stripe sub_...

custom_contracts

  • Điều khoản: amount, currency, billing_interval, starts_at, ends_at, các giới hạn
  • Stripe neo dữ liệu:
    • provider_checkout_session_id (Stripe cs_...)
    • provider_price_id (Stripe price_...)
    • provider_subscription_item_id (Stripe si_...)

subscription_histories

Snapshot theo kỳ thanh toán:

  • custom_contract_id
  • provider_price_id
  • provider_subscription_item_id
  • invoice_id / payment_intent_id

Flow Tổng Quan

1) Tạo Custom Contract (Admin)

API:

  • POST /api/v1/admin/custom-contracts

Logic:

  • Tạo hợp đồng ở trạng thái draft
  • Gắn subscription_id nếu đã có, hoặc tạo subscription mới với pricing_type = custom

2) Gửi Payment Link (Admin)

API:

  • POST /api/v1/admin/custom-contracts/{id}/send-payment-link

Logic:

  • Tạo Stripe Checkout Session với price_data
  • Gắn metadata:
    • custom_contract_id
    • subscription_slug
  • Update custom_contracts.provider_checkout_session_id
  • Chuyển trạng thái hợp đồng: draftoffered

3) Stripe Webhook (Checkout + Subscription)

customer.subscription.created

  • Map subscription với custom contract:
    • subscriptions.pricing_type = custom
    • subscriptions.custom_contract_id = <id>
  • Cập nhật custom_contracts:
    • provider_price_id
    • provider_subscription_item_id
    • subscription_id

invoice.paid

  • Tạo hoặc cập nhật subscription_histories cho kỳ thanh toán
  • Update custom_contracts.provider_price_id / provider_subscription_item_id (fallback theo invoice line)
  • Nếu thanh toán thành công: custom_contracts.status = active

customer.subscription.updated

  • Đồng bộ trạng thái subscription
  • Tạo history định kỳ (renew/change) cho custom contract nếu cần

customer.subscription.deleted

  • Hủy subscription và cập nhật contract:
    • cancelled nếu bị hủy sớm
    • expired nếu ends_at đã qua

Guardrails

  • Custom plan không dùng package_plan_to_providers
  • Các thay đổi giá/phạm vi phải thao tác qua Custom Contract + webhook
  • Stripe metadata là khóa để map đúng hợp đồng

Ghi Chú

  • Custom plan có thể nhận webhook theo thứ tự khác nhau; logic đã thiết kế idempotent để xử lý out-of-order.
  • Trường hợp miễn phí (amount = 0) vẫn cần theo dõi invoice.paid để kích hoạt chính xác.