VietBilling
Tất cả hướng dẫn
VietBilling Engineering Guides

Thiết kế Product, Price, Subscription và entitlement đúng từ đầu

Một data model thực dụng cho SaaS VND: version giá, snapshot lịch sử, trạng thái subscription và quyền lợi tách khỏi payment.

VietBilling EngineeringKiểm chứng 2026-08-3112 phút đọc

Tách catalog, commerce và access

Billing dễ bảo trì hơn khi ba mối quan tâm không bị gộp chung:

Product 1─* ProductPrice
Product *─* Benefit
Customer 1─* Checkout 1─0..1 Order 1─* Payment
Customer 1─* Subscription 1─* BenefitGrant
Subscription *─1 Product

ProductPrice phải bất biến theo lịch sử

Khi thay giá 99.000đ thành 149.000đ, đừng update row giá cũ. Archive giá đó và tạo ProductPrice mới. Order và Subscription giữ snapshot hoặc tham chiếu tới version đã mua, nên báo cáo lịch sử không đổi theo catalog hiện tại.

type ProductPrice = {
  id: string
  productId: string
  amount: number
  currency: "VND"
  interval: "month" | "year"
  archivedAt: string | null
}

Amount nên là integer VND, không dùng floating point. Currency và interval là dữ liệu bắt buộc dù hiện tại chỉ hỗ trợ VND; điều này làm invariant rõ hơn.

Checkout không phải Payment

Checkout là orchestration record: idempotency key, provider reference, redirect URLs và state tạo link. Payment là kết quả tiền tệ có thể được ghi nhận sau. Tách hai thực thể giúp biểu diễn timeout sau khi provider đã tạo link nhưng ứng dụng chưa nhận response.

Order lưu snapshot sản phẩm, giá và period được mua. Transaction là sổ ghi bất biến của thay đổi tiền; đừng sửa transaction cũ để phản ánh trạng thái mới.

Subscription là state machine

Một enum rõ ràng tốt hơn nhiều boolean:

incomplete → active → past_due → canceled
                    ↘ active after renewal
active + cancel_at_period_end → canceled at period end

Mọi transition phải đi qua billing engine, kiểm tra state hiện tại và chạy trong transaction. Controller, webhook và cron không nên tự update status riêng.

Benefit và entitlement

Product có thể gắn nhiều Benefit như feature access hoặc customer limit. Khi subscription active, hệ thống materialize BenefitGrant với thời gian hiệu lực. Ứng dụng của merchant đọc grant hoặc đồng bộ nó qua signed webhook.

Tách Benefit khỏi Subscription giúp thay cấu trúc plan mà không nhét từng cột feature vào subscription. Đồng thời, quyền truy cập không phụ thuộc trực tiếp vào row payment cuối cùng.

Tenant boundary và khóa

Mọi business entity phải thuộc đúng Organization. Public API key xác định Organization; URL public không nhận organization ID từ client. Unique constraint nên bao gồm tenant khi identifier chỉ unique trong tenant.

Các khóa quan trọng:

Migration và kiểm thử

Thêm constraint sau khi đã query dữ liệu trùng và có kế hoạch backfill. Test ít nhất: thay giá không sửa lịch sử, retry không nhân bản, hai tenant không đọc chéo, payment muộn không revive subscription sai và grant bị revoke đúng lúc.