TÀI LIỆU ĐẶC TẢ YÊU CẦU PHẦN MỀM (SRS) ​
HỆ THỐNG: NỀN TẢNG TRUY XUẤT NGUỒN GỐC GOTRACE ​
TXNG-F46 -- Kích hoạt & Vòng đời Dịch vụ Doanh nghiệp ​
| Thông tin | Chi tiết |
|---|---|
| Phiên bản | 1.0 |
| Ngày tạo | 01/07/2026 |
| Ngày cập nhật | 01/07/2026 |
| Tác giả | GoTrace Team |
| Người review | |
| Người phê duyệt |
Lịch sử thay đổi ​
| Ngày | Người cập nhật | Mô tả thay đổi |
|---|---|---|
| 01/07/2026 | GoTrace Team | Khởi tạo SRS v1.0 — Kích hoạt DN + vòng đời dịch vụ theo SERVICE_STATUS (DRAFT → TRIAL → ACTIVE → EXPIRED / TERMINATED); state machine đầy đủ; login gate lazy-expire (chặn đăng nhập EXPIRED/TERMINATED + invalidate cache user_perms); nhật ký subscription_histories append-only immutable; mirror companies.status; upload hồ sơ hợp đồng; tab "Lịch sử sử dụng dịch vụ" cho admin & DN; KHÔNG có CTE/KDE (module vận hành thương mại) |
1. Tổng quan tài liệu ​
1.1. Mục đích tài liệu ​
Tài liệu mô tả chi tiết chức năng Kích hoạt & Vòng đời Dịch vụ Doanh nghiệp (TXNG-F46) trong Module Vận hành Nền tảng (Platform Operations) của hệ thống GoTrace.
Chức năng cho phép Quản trị viên Nền tảng GoTrace (PLATFORM_ADMIN) thực hiện toàn bộ nghiệp vụ thương mại vận hành trên một Doanh nghiệp (DN) khách hàng:
- Tạo yêu cầu kích hoạt dịch vụ cho một DN: chọn DN chưa-active, chọn gói dịch vụ (
service_plans— TXNG-F45), nhập mã hợp đồng, thời hạn (term_months), số ngày dùng thử (trial_days), bonus QR/sản phẩm, upload hồ sơ (hợp đồng/MOU/phụ lục). - Kích hoạt dịch vụ: chuyển
DRAFT → TRIAL(hoặc→ ACTIVEkhitrial_days = 0), chốtplan_snapshot(phí/quota/features tại thời điểm kích hoạt). - Quản lý vòng đời: chuyển trial sang chính thức (
TRIAL → ACTIVE), gia hạn (EXPIRED → ACTIVE, extendpaid_until_date), chấm dứt (→ TERMINATED, bắt buộc lý do), mở lại (REACTIVATE). - Login gate (login gate): chặn đăng nhập của mọi user thuộc DN khi dịch vụ ở trạng thái
EXPIRED/TERMINATED; đánh giá hết hạn theo kiểu lazy khi đăng nhập (không cần cron ở v1).
Điểm cốt lõi khác biệt so với các module nghiệp vụ khác: đây là state machine 5 trạng thái (SERVICE_STATUS) với login gate tác động trực tiếp đến AuthService.login() + JwtStrategy.validate(), và nhật ký append-only immutable (subscription_histories) được ghi trong cùng transaction với mọi thay đổi trạng thái, kèm invalidate cache user_perms:{userId} và mirror companies.status.
Đây là module vận hành thương mại nội bộ GoTrace, KHÔNG phát sinh CTE/KDE truy xuất (nêu rõ trong Section 17 — Ràng buộc). Nguồn sự thật (source of truth) của trạng thái dịch vụ là company_subscriptions; companies.status chỉ là bản mirror phục vụ chặn đăng nhập nhanh.
Tài liệu là cơ sở cho đội ngũ phát triển (Developer), kiểm thử (QA), DBA và Business Analyst để triển khai chức năng phù hợp với BRD tương ứng (docs/brd/BRD_TXNG-F46_Kich_Hoat_Va_Vong_Doi_Dich_Vu_Doanh_Nghiep.md).
1.2. Thông tin chức năng ​
| Thuộc tính | Giá trị |
|---|---|
| Mã chức năng | TXNG-F46 |
| Tên chức năng | Kích hoạt & Vòng đời Dịch vụ Doanh nghiệp |
| Menu | TXNG > Vận hành Nền tảng > Kích hoạt Doanh nghiệp |
| Ưu tiên | Bắt buộc |
| Giai đoạn | v1.0 |
| Pháp lý | Chính sách thương mại nội bộ GoTrace (docs/sale/Sale_Pricing.md, hiệu lực 01/06/2026 — báo giá 19/05/2026); Luật Giao dịch điện tử 2023 (hợp đồng điện tử, chữ ký số hồ sơ); Nghị định 13/2023/NĐ-CP (bảo vệ dữ liệu cá nhân — thông tin liên hệ DN + hồ sơ upload); Bộ luật Dân sự 2015 (hợp đồng dịch vụ). KHÔNG áp dụng TCVN 12850:2019 / CTE / KDE (module vận hành thương mại, không thuộc chuỗi truy xuất) |
| Liên thông | ← TXNG-F45 (Cấu hình Gói Dịch vụ — chọn service_plans.id ACTIVE khi kích hoạt; chốt plan_snapshot) |
← TXNG-F01 (Quản lý Doanh nghiệp — chọn DN, mirror companies.status) |
|
| ← TXNG-F29 (Hồ sơ Doanh nghiệp — chèn tab "Lịch sử sử dụng dịch vụ" vào màn chi tiết DN) | |
→ Module Auth (login gate: sửa AuthService.login() + JwtStrategy.validate() — chặn EXPIRED/TERMINATED + invalidate cache) |
|
→ TXNG-F16 (Cấp mã QR — ràng buộc effective_qr_quota theo năm) |
|
→ TXNG-F03 (Sản phẩm — giới hạn effective_max_products) |
|
| ↔ TXNG-F42 (Loại hình tổ chức — phân loại DN) |
1.3. Tác nhân tham gia (Actors) ​
| Actor | Vai trò | Quyền hạn |
|---|---|---|
| PLATFORM_ADMIN | Quản trị viên Nền tảng GoTrace (impl ROLE.ADMIN = numeric 1; chặn qua AdminGuard; API prefix /api/v1/admin/...) |
Actor duy nhất thao tác: tạo/sửa yêu cầu kích hoạt; kích hoạt (ACTIVATE); chuyển sang ACTIVE; gia hạn (RENEW); chấm dứt (TERMINATE); mở lại (REACTIVATE); upload hồ sơ; xem toàn bộ danh sách/chi tiết/lịch sử của mọi DN (cross-tenant). Sale thao tác qua tài khoản PLATFORM_ADMIN. |
| SYSTEM | Hệ thống tự động — login gate (lazy-evaluate khi đăng nhập) | Khi user đăng nhập: đọc companies.status (mirror) để chặn nhanh; nếu stored TRIAL/ACTIVE nhưng đã quá hạn theo ngày → lazy-expire: flip EXPIRED + ghi subscription_histories {action: EXPIRE} + mirror companies.status + invalidate cache user_perms:{userId} (trong cùng transaction), rồi chặn đăng nhập. KHÔNG có UI, KHÔNG có cron ở v1. |
| ENTERPRISE_ADMIN | Quản trị viên Doanh nghiệp (khách hàng) | CHỈ XEM tab "Lịch sử sử dụng dịch vụ" của chính DN mình (multi-tenant: lọc company_id = current_company_id). KHÔNG được tạo/sửa/kích hoạt/gia hạn/chấm dứt. |
| ENTERPRISE_USER | Nhân viên Doanh nghiệp (khách hàng) | CHỈ XEM tab "Lịch sử sử dụng dịch vụ" của chính DN mình (giống ENTERPRISE_ADMIN, read-only). |
Ghi chú phân quyền:
SALE_STAFF(implROLE.AGENT= numeric2) không là actor trực tiếp của F46; nghiệp vụ onboard/chăm sóc KH nằm ngoài hệ thống, sale thao tác qua tài khoảnPLATFORM_ADMIN. Mọi endpoint ghi/chuyển trạng thái đều chặn quaAdminGuard.
1.4. Tham chiếu UI/UX ​
| Tài liệu | Link |
|---|---|
| Mockup danh sách kích hoạt | docs/html/kich_hoat/activation_list.html (tạo trong Phase implementation) |
| Mockup tạo/sửa yêu cầu kích hoạt | docs/html/kich_hoat/activation_form.html |
| Mockup chi tiết + timeline | docs/html/kich_hoat/activation_detail.html |
| Mockup tab lịch sử dịch vụ (nhúng chi tiết DN) | docs/html/kich_hoat/subscription_history_tab.html |
| Design System | apps/frontend/CLAUDE.md — Section Frontend UI/UX |
| Component tái dùng | components/admin/shared/timeline.tsx (TimelineEvent[]); components/admin/company/list/* (DebouncedInput + CSelect + useFilter + TableWrapper); FormWrapper/FormRow + ImageUploadGroup |
2. Luồng nghiệp vụ (Business Flow) ​
2.1. Luồng nghiệp vụ tổng quan ​
┌────────────────────────────────────────────────────────────────────────────────────────┐
│ LUỒNG KÍCH HOẠT & VÒNG ĐỜI DỊCH VỤ DOANH NGHIỆP (F46) │
├────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ╔══════════════════════════════════════════════════════════════════════════════════╗ │
│ ║ PHẦN A: PLATFORM_ADMIN TẠO & VẬN HÀNH VÒNG ĐỜI DỊCH VỤ ║ │
│ ╚══════════════════════════════════════════════════════════════════════════════════╝ │
│ │
│ ┌────────────────┐ ┌──────────────────────────────┐ │
│ │ DN đã đăng ký │──>│ ADMIN TẠO YÊU CẦU KÍCH HOẠT │ (UC-F46-01) │
│ │ (companies) │ │ • Chọn DN (service_status ∈ │ │
│ │ chưa có sub │ │ {chưa có / DRAFT / EXPIRED │ │
│ │ hoặc DRAFT │ │ / TERMINATED}) │ │
│ └────────────────┘ │ • Chọn plan (F45, ACTIVE) │ │
│ │ • contract_code + term_months│ │
│ │ • trial_days (mặc định 7) │ │
│ │ • bonus_qr / bonus_products │ │
│ │ • upload hồ sơ (CONTRACT...) │ │
│ │ → status = DRAFT │ │
│ │ → history {action: CREATE} │ │
│ └──────────────┬───────────────┘ │
│ │ │
│ ┌─────────────────┤ (UC-F46-02: SỬA — ghi history UPDATE) │
│ │ ▼ │
│ │ ┌────────────────────────────┐ │
│ │ │ ADMIN KÍCH HOẠT (ACTIVATE) │ (UC-F46-03) │
│ │ │ • chốt plan_snapshot │ │
│ │ │ • effective_qr_quota = │ │
│ │ │ plan.qr + bonus_qr │ │
│ │ │ • trial_end = start+trial │ │
│ │ │ • trial_days>0 → TRIAL │ │
│ │ │ trial_days=0 → ACTIVE │ │
│ │ │ ▸ history {ACTIVATE} │ │
│ │ │ ▸ mirror companies.status │ │
│ │ │ ▸ invalidate user_perms │ │
│ │ └───────┬──────────────┬──────┘ │
│ │ │ │ │
│ │ trial_days>0 trial_days=0 │
│ │ ▼ ▼ │
│ │ ┌───────────┐ ┌───────────┐ │
│ │ │ TRIAL │ │ ACTIVE │<───────┐ │
│ │ │ (dùng thử) │ │ (chính │ │ RENEW │
│ │ └─────┬──────┘ │ thức) │ │ (UC-F46-05) │
│ │ │ └─────┬──────┘ │ │
│ │ chuyển ACTIVE (UC-F46-04) │ │ │
│ │ └──────►──────────┘ │ │
│ │ │ hết hạn (lazy) │ hết hạn(lazy) │ │
│ │ ▼ ▼ │ │
│ │ ┌────────────────────────┐ │ │
│ │ │ EXPIRED │─────────┘ │
│ │ │ (hết trial/hết kỳ) │ │
│ │ └───────────┬────────────┘ │
│ │ │ │
│ │ TERMINATE (bắt buộc reason, UC-F46-06) │
│ └───────────────────────┼──────────────►┌────────────────┐ │
│ ▼ │ TERMINATED │ │
│ (từ TRIAL/ACTIVE/EXPIRED) │ (chấm dứt) │ │
│ └───────┬─────────┘ │
│ REACTIVATE (mở lại) ◄────┘ │
│ │
│ ════════════════════════════════════════════════════════════════════════════════════ │
│ ╔══════════════════════════════════════════════════════════════════════════════════╗ │
│ ║ PHẦN B: LOGIN GATE (SYSTEM — lazy-expire khi user DN đăng nhập) ║ │
│ ╚══════════════════════════════════════════════════════════════════════════════════╝ │
│ │
│ ┌──────────────┐ ┌──────────────────────────┐ ┌──────────────────────────────┐ │
│ │ User DN gọi │──>│ AuthService.login() │──>│ Đọc companies.status (mirror) │ │
│ │ POST /login │ │ + JwtStrategy.validate() │ │ + company_subscriptions │ │
│ └──────────────┘ └──────────────────────────┘ └──────────────┬───────────────┘ │
│ │ │
│ ┌───────────────────────────────────────────────────────────┤ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌──────────────────────┐ ┌──────────────────────────┐ │
│ │ EXPIRED │ │ stored TRIAL/ACTIVE │ │ TRIAL/ACTIVE còn hạn │ │
│ │ hoặc │ │ nhưng quá hạn theo │ │ hoặc DRAFT │ │
│ │ TERMINATED │ │ ngày → LAZY-EXPIRE: │ │ → CHO PHÉP đăng nhập │ │
│ │ → 403 chặn │ │ flip EXPIRED + history│ │ (cache user_perms 10') │ │
│ │ (2 message) │ │ + mirror + invalidate │ └──────────────────────────┘ │
│ └─────────────┘ │ → rồi 403 chặn │ │
│ └──────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────┘
2.2. Diễn giải luồng ​
| Bước | Mô tả hành động | Hệ thống thực hiện |
|---|---|---|
| B1 | DN đã tồn tại trong companies (đăng ký tài khoản, TXNG-F01). Chưa có subscription hiện hành hoặc đang DRAFT/EXPIRED/TERMINATED |
ADMIN mở SCR-F46-01 → filter DN đủ điều kiện |
| B2 | ADMIN tạo yêu cầu kích hoạt (UC-F46-01): chọn DN + gói (F45 ACTIVE) + contract_code + term_months + trial_days + bonus + upload hồ sơ |
1. Validate DN đủ điều kiện (BR-F46-02) 2. Validate contract_code unique (partial)3. INSERT company_subscriptions {service_status: DRAFT}4. INSERT subscription_documents (nếu upload)5. INSERT subscription_histories {action: CREATE}6. Mirror companies.status = 0 (DRAFT) |
| B3 | (Tùy chọn) ADMIN sửa yêu cầu ở DRAFT (UC-F46-02) |
UPDATE + INSERT subscription_histories {action: UPDATE, changes_json diff} — cùng transaction |
| B4 | ADMIN kích hoạt (UC-F46-03): DRAFT → TRIAL (hoặc → ACTIVE nếu trial_days=0) |
ATOMIC: chốt plan_snapshot; tính effective_qr_quota/effective_max_products; set trial_end_date/active_start_date/paid_until_date; UPDATE status; INSERT history {ACTIVATE}; mirror companies.status; invalidate cache user_perms:{userId} mọi user DN |
| B5 | ADMIN chuyển trial sang chính thức (UC-F46-04): TRIAL → ACTIVE |
ATOMIC: set active_start_date, tính paid_until_date = active_start + term_months; UPDATE; history {ACTIVATE (chuyển ACTIVE)}; mirror; invalidate cache |
| B6 | Hết hạn (lazy): user DN đăng nhập, stored TRIAL/ACTIVE nhưng trial_end_date/paid_until_date < today (UC-F46-07) |
LAZY-EXPIRE ATOMIC: flip EXPIRED; INSERT history {EXPIRE}; mirror companies.status = 3; invalidate cache; trả 403 chặn login |
| B7 | ADMIN gia hạn (UC-F46-05): EXPIRED → ACTIVE (hoặc extend paid_until_date khi còn ACTIVE) |
ATOMIC: cộng paid_until_date += term_months; áp chiết khấu theo term_months; UPDATE status ACTIVE; history {RENEW}; mirror; invalidate cache |
| B8 | ADMIN chấm dứt (UC-F46-06): → TERMINATED (bắt buộc terminate_reason) |
ATOMIC: UPDATE status TERMINATED, terminated_at, terminated_by, terminate_reason; history {TERMINATE}; mirror companies.status = 4; invalidate cache |
| B9 | ADMIN mở lại (REACTIVATE): TERMINATED → TRIAL/ACTIVE |
ATOMIC: UPDATE status; history {REACTIVATE}; mirror; invalidate cache |
| B10 | Login gate chặn khi EXPIRED/TERMINATED (UC-F46-07) |
AuthService.login() đọc mirror companies.status; nếu EXPIRED (3) → 403 msg "Cần liên hệ sale của gotrace để kích hoạt dịch vụ"; nếu TERMINATED (4) → 403 msg "Cần liên hệ với quản trị viên gotrace" |
| B11 | DN xem lịch sử dịch vụ của chính mình (UC-F46-08) | GET /api/v1/companies/:id/subscription-history (multi-tenant: company_id = current); render tab {CompanySubscriptionHistoryTab} |
3. Phân loại / Trạng thái ​
3.1. Trạng thái dịch vụ (SERVICE_STATUS — Canonical §2.43) ​
Vòng đời sử dụng dịch vụ của một DN (company_subscriptions.service_status; mirror sang companies.status). Đánh giá "hết hạn" theo kiểu lazy khi đăng nhập (không cần cron ở v1).
| Trạng thái | Mã | Mirror companies.status |
Mô tả | Ảnh hưởng đăng nhập | CSS class | Background | Color |
|---|---|---|---|---|---|---|---|
| Nháp | DRAFT |
0 |
DN mới đăng ký, chưa kích hoạt dịch vụ (mặc định) | Cho phép (onboarding, hạn chế) | .bdg.bdg-neutral |
#f0f2f5 | #8c99ab |
| Dùng thử | TRIAL |
1 |
Đã kích hoạt, đang dùng thử miễn phí (trial_days) |
Cho phép (đầy đủ) | .bdg.bdg-info |
#e3f2fd | #0277bd |
| Chính thức | ACTIVE |
2 |
Hết trial, tính ngày dịch vụ theo gói | Cho phép (đầy đủ) | .bdg.bdg-success |
#e8f5e9 | #2e7d32 |
| Hết hạn | EXPIRED |
3 |
Hết trial chưa kích hoạt, hoặc hết kỳ chưa gia hạn | Chặn → "Cần liên hệ sale của gotrace để kích hoạt dịch vụ" | .bdg.bdg-warning |
#fff8e1 | #fb8c00 |
| Chấm dứt | TERMINATED |
4 |
Admin chấm dứt thủ công (bắt buộc lý do) | Chặn → "Cần liên hệ với quản trị viên gotrace" | .bdg.bdg-danger |
#feecec | #e53935 |
Chỉ
PLATFORM_ADMINđược chuyển trạng thái. Mọi transition ghisubscription_histories(§2.46) trong cùng$transaction+ invalidate cacheuser_perms:{userId}của mọi user thuộc DN + mirrorcompanies.status.
3.2. Bảng effective-status (lazy-evaluate theo ngày) ​
Trạng thái hiển thị/hiệu lực được tính tại thời điểm đọc (login hoặc admin mở màn hình) dựa trên stored service_status và ngày hệ thống. Nếu stored ≠ effective → thực hiện flip + ghi history (chỉ với login của user DN; màn admin chỉ hiển thị badge "quá hạn" nhắc admin, không tự flip trừ khi login trigger).
Stored service_status |
Điều kiện ngày | Effective status | Hành động khi user DN login |
|---|---|---|---|
DRAFT |
— | DRAFT |
Cho phép (hạn chế onboarding) |
TRIAL |
today ≤ trial_end_date |
TRIAL |
Cho phép (đầy đủ) |
TRIAL |
today > trial_end_date |
EXPIRED |
Lazy-expire → flip EXPIRED + history + mirror + invalidate → 403 |
ACTIVE |
today ≤ paid_until_date |
ACTIVE |
Cho phép (đầy đủ) |
ACTIVE |
today > paid_until_date |
EXPIRED |
Lazy-expire → flip EXPIRED + history + mirror + invalidate → 403 |
EXPIRED |
— | EXPIRED |
Chặn (E-F46-005) |
TERMINATED |
— | TERMINATED |
Chặn (E-F46-006) |
3.3. Phân loại tài liệu (Subscription Document Type — Canonical §2.47) ​
| Mã | Tên | Mô tả |
|---|---|---|
CONTRACT |
Hợp đồng dịch vụ | File hợp đồng ký kết giữa GoTrace và DN |
MOU |
Biên bản ghi nhớ (MOU) | Biên bản ghi nhớ hợp tác |
APPENDIX |
Phụ lục / báo giá | Phụ lục hợp đồng, báo giá đính kèm |
OTHER |
Tài liệu khác | Tài liệu bổ sung khác |
3.4. Phân loại thao tác nhật ký (Subscription History Action — Canonical §2.46) ​
| Mã | Tên | Mô tả |
|---|---|---|
CREATE |
Tạo yêu cầu | Tạo yêu cầu kích hoạt (trạng thái DRAFT) |
UPDATE |
Sửa | Sửa thông tin đăng ký (ghi diff changes_json bắt buộc) |
ACTIVATE |
Kích hoạt | DRAFT → TRIAL (hoặc → ACTIVE nếu trial_days=0); TRIAL → ACTIVE |
RENEW |
Gia hạn | Cập nhật paid_until_date; EXPIRED → ACTIVE |
EXPIRE |
Hết hạn | TRIAL/ACTIVE → EXPIRED (lazy khi login hoặc admin) |
TERMINATE |
Chấm dứt | → TERMINATED (bắt buộc reason) |
REACTIVATE |
Mở lại | TERMINATED → TRIAL/ACTIVE |
4. Danh sách màn hình ​
| Mã màn hình | Tên màn hình | Chức năng chính | Vai trò sử dụng | Ghi chú |
|---|---|---|---|---|
| SCR-F46-01 | Danh sách yêu cầu kích hoạt | Hiển thị, tìm kiếm, lọc danh sách subscription; summary cards đếm theo trạng thái | PLATFORM_ADMIN | Màn hình chính; filter khoảng thời gian Active (service_start_date/activated_at), trạng thái dịch vụ, search MST/tên DN/SĐT/email |
| SCR-F46-02 | Tạo yêu cầu kích hoạt | Form tạo mới subscription (DRAFT) |
PLATFORM_ADMIN | Chọn DN đủ điều kiện + gói + hồ sơ |
| SCR-F46-03 | Sửa yêu cầu kích hoạt | Sửa subscription ở DRAFT (ghi history bắt buộc) |
PLATFORM_ADMIN | UPDATE ghi changes_json |
| SCR-F46-04 | Chi tiết yêu cầu kích hoạt | Xem chi tiết + timeline lịch sử + nút hành động | PLATFORM_ADMIN | Nút: Kích hoạt / Chuyển Active / Gia hạn / Chấm dứt / Mở lại |
| SCR-F46-05 | Tab "Lịch sử sử dụng dịch vụ" | Tab nhúng vào màn chi tiết DN (TXNG-F01/F29) | PLATFORM_ADMIN, ENTERPRISE_ADMIN, ENTERPRISE_USER | Admin xem mọi DN; DN chỉ xem của chính mình (multi-tenant) |
5. Mô tả chi tiết trường dữ liệu ​
5.1. SCR-F46-01: Danh sách yêu cầu kích hoạt ​
Summary cards (đếm theo trạng thái):
| Card | Nguồn | Mô tả |
|---|---|---|
| Nháp | COUNT service_status = 'DRAFT' |
Số subscription chờ kích hoạt |
| Dùng thử | COUNT service_status = 'TRIAL' |
Số DN đang dùng thử |
| Chính thức | COUNT service_status = 'ACTIVE' |
Số DN chính thức |
| Hết hạn | COUNT service_status = 'EXPIRED' |
Số DN hết hạn (cần gia hạn) |
| Chấm dứt | COUNT service_status = 'TERMINATED' |
Số DN đã chấm dứt |
Thanh công cụ (Toolbar):
| Phần tử | Loại | Mô tả |
|---|---|---|
| Tìm kiếm | Text Input (Debounced 300ms) | Tìm theo company_tax_code (MST), company_name, contact_phone, company_email |
| Lọc theo trạng thái | Dropdown (CSelect) | DRAFT / TRIAL / ACTIVE / EXPIRED / TERMINATED / Tất cả |
| Lọc theo gói | Dropdown (Search) | Danh sách service_plans (plan_code) |
| Lọc theo khoảng Active | Date Range Picker | service_start_date (hoặc activated_at) từ ngày – đến ngày |
| Nút "Tạo yêu cầu kích hoạt" | Button (Primary) | Mở SCR-F46-02 |
| Nút "Xuất Excel" | Button (Secondary) | Export danh sách theo filter |
Bảng dữ liệu (Data Table):
| Cột | Kiểu | Sortable | Mô tả |
|---|---|---|---|
| STT | Number | Không | Số thứ tự |
| Tên DN | Text (Link) | Có | companies.company_name — click mở SCR-F46-04 |
| MST | Text | Có | companies.company_tax_code |
| Gói | Tag | Có | plan_code (snapshot, color-coded) |
| Mã hợp đồng | Text | Có | contract_code |
| Trạng thái | Badge | Có | SERVICE_STATUS (Section 3.1, color-coded) |
| Ngày bắt đầu | Date | Có | service_start_date (dd/MM/yyyy) |
| Hết trial | Date | Có | trial_end_date (dd/MM/yyyy) — warning nếu ≤ 3 ngày |
| Thanh toán đến | Date | Có | paid_until_date (dd/MM/yyyy) — warning nếu sắp hết |
| Kích hoạt lúc | DateTime | Có | activated_at |
| Thao tác | Actions | Không | Xem / Sửa (nếu DRAFT) / Kích hoạt / Gia hạn / Chấm dứt |
Phân trang: 10 / 20 / 50 / 100 bản ghi/trang; envelope { page, page_size, total_items, total_pages }.
5.2. SCR-F46-02: Tạo yêu cầu kích hoạt ​
Card 1: Chọn Doanh nghiệp ​
| Tên trường | Field name | Kiểu dữ liệu | Bắt buộc | Validation | Mô tả |
|---|---|---|---|---|---|
| Doanh nghiệp | company_id | Dropdown (Search) | Có | FK companies; chỉ DN đủ điều kiện (BR-F46-02) |
Chọn DN chưa-active (chưa có sub / DRAFT / EXPIRED / TERMINATED) |
| Mã số thuế | company_tax_code | Text (auto) | — | Read-only | Auto-fill từ DN đã chọn |
| Người liên hệ | contact_name | Text (auto) | — | Read-only | Auto-fill từ DN |
| SĐT / Email | contact_phone / company_email | Text (auto) | — | Read-only | Auto-fill từ DN |
Card 2: Gói dịch vụ & Điều khoản ​
| Tên trường | Field name | Kiểu dữ liệu | Bắt buộc | Validation | Mô tả |
|---|---|---|---|---|---|
| Gói dịch vụ | plan_id | Dropdown (Search) | Có | FK service_plans; chỉ gói status = ACTIVE (BR-F45-05) |
Chọn 1 trong các gói ACTIVE; hiển thị phí/quota kèm theo |
| Mã gói (snapshot) | plan_code | Text (auto) | Có | Inherit từ plan | Read-only; snapshot lúc chọn |
| Mã hợp đồng | contract_code | Text | Có | Unique (partial); max 100 | Mã hợp đồng dịch vụ (E-F46-002 nếu thiếu) |
| Thời hạn (tháng) | term_months | Number | Có | > 0; gợi ý 12/24/36 | Kỳ hạn dịch vụ (áp chiết khấu 24m/36m) |
| Số ngày dùng thử | trial_days | Number | Có | ≥ 0; mặc định 7 | 0 = kích hoạt thẳng ACTIVE |
| Ngày bắt đầu dịch vụ | service_start_date | Date | Có | mặc định hôm nay | trial_end_date = start + trial_days |
| Bonus QR (tặng thêm) | bonus_qr_quota | Number | Không | ≥ 0; mặc định 0 | Cộng vào effective_qr_quota |
| Bonus sản phẩm | bonus_max_products | Number | Không | ≥ 0; mặc định 0 | Cộng vào effective_max_products |
| Chiết khấu (%) | discount_percent | Decimal(5,2) | Không | 0–100; mặc định 0 | Chiết khấu thủ công (ngoài chiết khấu term) |
| Ghi chú | note | Textarea | Không | max 1000 | Ghi chú nội bộ |
Card 3: Hồ sơ đính kèm (Documents) ​
| Tên trường | Field name | Kiểu dữ liệu | Bắt buộc | Validation | Mô tả |
|---|---|---|---|---|---|
| Loại tài liệu | doc_type | Dropdown | *Có (nếu upload) | Enum §2.47 (CONTRACT/MOU/APPENDIX/OTHER) | Loại hồ sơ |
| File hồ sơ | file | File Upload Multi | Không | PDF/JPG/PNG; ≤ 20MB/file (E-F46-003) | Lưu MinIO (MinioStorageService + FileFieldsInterceptor) |
Hành động Footer ​
- Nút "Lưu nháp" (Primary): INSERT
company_subscriptions{service_status: DRAFT} + history {CREATE}. - Nút "Lưu & Kích hoạt ngay" (Secondary): tạo DRAFT rồi gọi luôn UC-F46-03.
- Nút "Hủy" (Tertiary): đóng form không lưu.
5.3. SCR-F46-03: Sửa yêu cầu kích hoạt ​
Chỉ khả dụng khi service_status = DRAFT (BR-F46 — sau kích hoạt các trường điều khoản đã bị chốt qua plan_snapshot). Form giống SCR-F46-02, load data hiện tại. Khi lưu:
- Optimistic lock qua
version(E-F46-007 nếu lệch). - INSERT
subscription_histories{action: UPDATE, changes_json = diff before/after} bắt buộc. - KHÔNG cho đổi
company_id(bất biến sau tạo).
5.4. SCR-F46-04: Chi tiết yêu cầu kích hoạt ​
Header Info Box (read-only):
| Phần tử | Mô tả |
|---|---|
| Tên DN + MST | company_name + company_tax_code, badge trạng thái màu |
| Gói | plan_code (snapshot) + tên gói |
| Mã hợp đồng | contract_code |
| Ngày bắt đầu / hết trial / thanh toán đến | service_start_date / trial_end_date / paid_until_date |
| Quota hiệu lực | effective_qr_quota (QR/năm) + effective_max_products |
| Người kích hoạt / lúc | activated_by + activated_at |
| Người chấm dứt / lý do | terminated_by + terminated_at + terminate_reason (nếu có) |
Tabs:
- Tab "Thông tin": toàn bộ field subscription read-only +
plan_snapshot(phí/quota/features đã chốt). - Tab "Hồ sơ": gallery
subscription_documents(CONTRACT/MOU/APPENDIX/OTHER), download. - Tab "Lịch sử" ({CompanySubscriptionHistoryTab}): timeline
subscription_histories(components/admin/shared/timeline.tsx) — mỗi action hiển thị: actor, action,from_status → to_status,changed_at,reason, diffchanges_json.
Action buttons (theo service_status):
| Status | Nút khả dụng (PLATFORM_ADMIN) |
|---|---|
| DRAFT | Sửa / Kích hoạt / Xóa (soft) |
| TRIAL | Chuyển Active / Chấm dứt |
| ACTIVE | Gia hạn / Chấm dứt |
| EXPIRED | Gia hạn (→ ACTIVE) / Chấm dứt |
| TERMINATED | Mở lại (Reactivate) |
Dialog xác nhận theo hành động:
| Hành động | Trường bắt buộc trong dialog |
|---|---|
| Kích hoạt | Xác nhận chốt plan_snapshot + effective_qr_quota/effective_max_products |
| Chuyển Active | Xác nhận active_start_date (default NOW) + tính paid_until_date |
| Gia hạn | term_months (kỳ gia hạn) + chiết khấu preview |
| Chấm dứt | terminate_reason (Textarea ≥ 20 ký tự, E-F46-004) |
| Mở lại | Chọn trạng thái đích (TRIAL/ACTIVE) + lý do |
5.5. SCR-F46-05: Tab "Lịch sử sử dụng dịch vụ" ​
Component {CompanySubscriptionHistoryTab} nhúng vào màn chi tiết DN (/quan-ly/doanh-nghiep/[id]/chi-tiet). Dùng components/admin/shared/timeline.tsx (TimelineEvent[]).
| Phần tử | Mô tả |
|---|---|
| Header | Gói hiện hành + trạng thái + quota còn lại |
| Timeline | Mỗi bản ghi subscription_histories: icon theo action, changed_at (dd/MM/yyyy HH:mm), actor, from_status → to_status, reason |
| Bảng hồ sơ | Danh sách subscription_documents (chỉ đọc) |
Multi-tenant: ENTERPRISE_ADMIN/ENTERPRISE_USER chỉ thấy tab này với
company_id = current_company_id(BR-F46-14). PLATFORM_ADMIN thấy mọi DN.
6. Danh sách Use Case ​
| ID | Tên Use Case | Actor chính | Mô tả ngắn |
|---|---|---|---|
| UC-F46-01 | Tạo yêu cầu kích hoạt | PLATFORM_ADMIN | Tạo subscription mới (DRAFT) cho DN đủ điều kiện |
| UC-F46-02 | Sửa yêu cầu kích hoạt | PLATFORM_ADMIN | Sửa subscription ở DRAFT — ghi history UPDATE bắt buộc |
| UC-F46-03 | Kích hoạt dịch vụ | PLATFORM_ADMIN | DRAFT → TRIAL (hoặc → ACTIVE nếu trial_days=0); chốt plan_snapshot |
| UC-F46-04 | Chuyển sang ACTIVE | PLATFORM_ADMIN | TRIAL → ACTIVE; tính paid_until_date |
| UC-F46-05 | Gia hạn dịch vụ | PLATFORM_ADMIN | EXPIRED → ACTIVE / extend paid_until_date |
| UC-F46-06 | Chấm dứt dịch vụ | PLATFORM_ADMIN | → TERMINATED (bắt buộc terminate_reason) |
| UC-F46-07 | Chặn đăng nhập + lazy-expire | SYSTEM | Login gate: chặn EXPIRED/TERMINATED; lazy-expire khi quá hạn |
| UC-F46-08 | Xem lịch sử sử dụng dịch vụ | PLATFORM_ADMIN, ENTERPRISE_ADMIN, ENTERPRISE_USER | Xem timeline lịch sử (admin mọi DN; DN chỉ của mình) |
| UC-F46-09 | Mở lại dịch vụ (Reactivate) | PLATFORM_ADMIN | TERMINATED → TRIAL/ACTIVE |
7. Mô tả chi tiết Use Case ​
UC-F46-01: Tạo yêu cầu kích hoạt ​
Mô tả: PLATFORM_ADMIN tạo một yêu cầu kích hoạt dịch vụ cho DN đủ điều kiện.
Tiền điều kiện:
- User đăng nhập là
PLATFORM_ADMIN(quaAdminGuard). - DN được chọn có
service_status ∈ {chưa có subscription, DRAFT, EXPIRED, TERMINATED}(BR-F46-02). - Có ít nhất 1 gói
service_plansởstatus = ACTIVE.
Hậu điều kiện:
- Một bản ghi
company_subscriptions{service_status: DRAFT} được tạo. - Nếu có upload: một hoặc nhiều
subscription_documents. - Một
subscription_histories{action: CREATE} được tạo (cùng transaction). companies.statusmirror = 0 (DRAFT).
| Bước | Tác nhân | Hành động / Mô tả | Phản hồi / Xử lý hệ thống | Ghi chú |
|---|---|---|---|---|
| 1 | ADMIN | Click "Tạo yêu cầu kích hoạt" trên SCR-F46-01 | Mở SCR-F46-02 | |
| 2 | ADMIN | Chọn DN (Card 1) | Validate DN đủ điều kiện (BR-F46-02); nếu không → E-F46-001; auto-fill MST/liên hệ | |
| 3 | ADMIN | Chọn gói + nhập contract_code, term_months, trial_days, bonus (Card 2) |
Validate contract_code unique; hiển thị preview phí/quota |
|
| 4 | ADMIN | (Tùy chọn) upload hồ sơ (Card 3) | Validate PDF/JPG/PNG ≤ 20MB (E-F46-003); upload MinIO | |
| 5 | ADMIN | Click "Lưu nháp" | 1. Validate toàn bộ form (server-side) 2. Validate DN đủ điều kiện (recheck BR-F46-02) 3. Validate contract_code unique (partial)4. BEGIN transaction 5. INSERT company_subscriptions {service_status: DRAFT, plan_code (snapshot), trial_end_date = start + trial_days, created_by, version: 1}6. INSERT subscription_documents (nếu có)7. INSERT subscription_histories {action: CREATE, from_status: NULL, to_status: DRAFT, changed_by, changed_at, ip_address}8. UPDATE companies.status = 0 (mirror DRAFT)9. COMMIT 10. Return 201 Created + subscription_id |
Status: 201 |
Alternative Flow:
| Bước | Điều kiện | Xử lý |
|---|---|---|
| 2a | DN đang TRIAL/ACTIVE (đã có sub hiện hành) |
E-F46-001 "DN không đủ điều kiện kích hoạt (đang dùng dịch vụ)" |
| 3a | Thiếu contract_code |
E-F46-002 "Bắt buộc nhập mã hợp đồng" |
| 3b | contract_code đã tồn tại |
E-F46-002 (biến thể) "Mã hợp đồng đã tồn tại" |
| 3c | Gói đã chọn ở DEPRECATED |
E-F46-001 (biến thể) "Gói dịch vụ đã ngừng bán, chọn gói khác" (BR-F45-05) |
| 4a | File > 20MB hoặc sai định dạng | E-F46-003 "File tối đa 20MB, chỉ chấp nhận PDF/JPG/PNG" |
| 5a | Concurrent: DN vừa được tạo sub bởi admin khác | HTTP 409 + "DN đã có subscription hiện hành, vui lòng tải lại" (BR-F46-10) |
UC-F46-02: Sửa yêu cầu kích hoạt ​
Tiền điều kiện: PLATFORM_ADMIN; subscription ở service_status = DRAFT.
Hậu điều kiện: subscription cập nhật; subscription_histories {action: UPDATE, changes_json} bắt buộc.
| Bước | Tác nhân | Hành động / Mô tả | Phản hồi / Xử lý hệ thống |
|---|---|---|---|
| 1 | ADMIN | Click "Sửa" trên SCR-F46-04 | Mở SCR-F46-03 với data hiện tại |
| 2 | ADMIN | Chỉnh sửa gói/điều khoản/hồ sơ → "Lưu" | 1. Validate: service_status = DRAFT (E-F46-001 nếu khác)2. Validate optimistic lock version (E-F46-007 nếu lệch)3. Tính diff before/after 4. BEGIN transaction 5. UPDATE company_subscriptions SET ..., version = version + 1, updated_by, updated_at6. INSERT subscription_histories {action: UPDATE, changes_json = diff, changed_by, changed_at}7. COMMIT 8. Return 200 OK |
Alternative Flow:
| Bước | Điều kiện | Xử lý |
|---|---|---|
| 2a | service_status ≠ DRAFT |
E-F46-001 "Chỉ sửa được yêu cầu ở trạng thái Nháp" |
| 2b | version lệch (đã bị sửa) |
E-F46-007 "Bản ghi đã được cập nhật bởi người khác, vui lòng tải lại" |
UC-F46-03: Kích hoạt dịch vụ ​
Mô tả: PLATFORM_ADMIN kích hoạt subscription từ DRAFT; chốt snapshot; login gate của DN chuyển sang cho phép.
Tiền điều kiện: PLATFORM_ADMIN; subscription ở service_status = DRAFT; gói tham chiếu còn tồn tại.
Hậu điều kiện: service_status = TRIAL (hoặc ACTIVE nếu trial_days=0); plan_snapshot chốt; history {ACTIVATE}; companies.status mirror; cache user_perms invalidated.
| Bước | Tác nhân | Hành động / Mô tả | Phản hồi / Xử lý hệ thống | Ghi chú |
|---|---|---|---|---|
| 1 | ADMIN | Mở SCR-F46-04 → click "Kích hoạt" → dialog xác nhận | Hiển thị preview plan_snapshot + quota hiệu lực |
|
| 2 | ADMIN | Xác nhận | ATOMIC TRANSACTION (BEGIN … COMMIT): 1. Validate: service_status = DRAFT, optimistic lock version2. Load gói service_plans (recheck ACTIVE)3. Chốt plan_snapshot = JSONB {phí, quota, features, chiết khấu}4. effective_qr_quota = plan.qr_quota_year + bonus_qr_quota (BR-F46-04)5. effective_max_products = plan.max_products + bonus_max_products6. trial_end_date = service_start_date + trial_days7. Nếu trial_days > 0: service_status = TRIAL; nếu = 0: service_status = ACTIVE, active_start_date = service_start_date, paid_until_date = active_start_date + term_months8. UPDATE company_subscriptions SET service_status, plan_snapshot, effective_*, activated_at = NOW, activated_by, version = version + 19. INSERT subscription_histories {action: ACTIVATE, from_status: DRAFT, to_status: (TRIAL/ACTIVE), changed_by, changed_at, ip_address}10. Mirror: UPDATE companies.status = (TRIAL=1 / ACTIVE=2) (BR-F46-09)11. Invalidate cache: xóa key user_perms:{userId} của MỌI user thuộc company_id (BR-F46-08)12. COMMIT 13. Notification N-F46-01 to DN admin 14. Return 200 OK |
Nếu bất kỳ step lỗi → ROLLBACK toàn bộ |
Alternative Flow:
| Bước | Điều kiện | Xử lý |
|---|---|---|
| 2a | service_status ≠ DRAFT |
E-F46-001 "Yêu cầu không ở trạng thái có thể kích hoạt" |
| 2b | Gói tham chiếu DEPRECATED |
E-F46-001 (biến thể) — chặn, yêu cầu đổi gói (BR-F45-05) |
| 2c | Optimistic lock lệch | E-F46-007 (HTTP 409) |
| 2d | Cache invalidation lỗi (Redis down) | Log cảnh báo; transaction DB vẫn commit; cache tự hết hạn sau 10' (fail-safe, không block) |
UC-F46-04: Chuyển sang ACTIVE ​
Tiền điều kiện: PLATFORM_ADMIN; subscription ở service_status = TRIAL.
Hậu điều kiện: service_status = ACTIVE; active_start_date + paid_until_date set; history {ACTIVATE}; mirror; invalidate cache.
| Bước | Tác nhân | Hành động / Mô tả | Phản hồi / Xử lý hệ thống |
|---|---|---|---|
| 1 | ADMIN | SCR-F46-04 → "Chuyển Active" → dialog | |
| 2 | ADMIN | Xác nhận active_start_date (default NOW) |
ATOMIC: 1. Validate service_status = TRIAL, lock version2. active_start_date = NOW (hoặc nhập), paid_until_date = active_start_date + term_months3. UPDATE company_subscriptions SET service_status = ACTIVE, active_start_date, paid_until_date, version+14. INSERT history {ACTIVATE, from: TRIAL, to: ACTIVE} 5. Mirror companies.status = 26. Invalidate cache user_perms mọi user DN7. COMMIT + Return 200 OK |
Alternative Flow: service_status ≠ TRIAL → E-F46-001.
UC-F46-05: Gia hạn dịch vụ ​
Tiền điều kiện: PLATFORM_ADMIN; subscription ở service_status ∈ {ACTIVE, EXPIRED}.
Hậu điều kiện: paid_until_date extend; nếu từ EXPIRED → ACTIVE; history {RENEW}; mirror; invalidate cache.
| Bước | Tác nhân | Hành động / Mô tả | Phản hồi / Xử lý hệ thống |
|---|---|---|---|
| 1 | ADMIN | SCR-F46-04 → "Gia hạn" → dialog nhập term_months (kỳ gia hạn) |
Hiển thị chiết khấu preview theo term_months (BR-F46-13) |
| 2 | ADMIN | Xác nhận | ATOMIC: 1. Validate service_status ∈ {ACTIVE, EXPIRED}, lock version2. base = GREATEST(paid_until_date, today) (nếu đã EXPIRED tính từ hôm nay)3. paid_until_date = base + term_months; term_months += renew_months4. service_status = ACTIVE (nếu trước đó EXPIRED)5. UPDATE company_subscriptions SET paid_until_date, term_months, service_status, version+16. INSERT history {RENEW, from_status, to_status: ACTIVE, reason: kỳ gia hạn} 7. Mirror companies.status = 28. Invalidate cache user_perms mọi user DN9. COMMIT + Return 200 OK |
Alternative Flow: service_status ∉ {ACTIVE, EXPIRED} → E-F46-001.
UC-F46-06: Chấm dứt dịch vụ ​
Tiền điều kiện: PLATFORM_ADMIN; subscription ở service_status ∈ {TRIAL, ACTIVE, EXPIRED}.
Hậu điều kiện: service_status = TERMINATED (bắt buộc terminate_reason); history {TERMINATE}; mirror companies.status = 4; invalidate cache → user DN bị chặn login.
| Bước | Tác nhân | Hành động / Mô tả | Phản hồi / Xử lý hệ thống |
|---|---|---|---|
| 1 | ADMIN | SCR-F46-04 → "Chấm dứt" → dialog | |
| 2 | ADMIN | Nhập terminate_reason (≥ 20 ký tự) → xác nhận |
ATOMIC: 1. Validate service_status ∈ {TRIAL, ACTIVE, EXPIRED}, lock version2. Validate terminate_reason length ≥ 20 (E-F46-004)3. UPDATE company_subscriptions SET service_status = TERMINATED, terminated_at = NOW, terminated_by, terminate_reason, version+14. INSERT history {TERMINATE, from_status, to_status: TERMINATED, reason: terminate_reason} 5. Mirror companies.status = 46. Invalidate cache user_perms mọi user DN (BR-F46-08)7. COMMIT 8. Notification N-F46-04 to DN admin 9. Return 200 OK |
Alternative Flow:
| Bước | Điều kiện | Xử lý |
|---|---|---|
| 2a | terminate_reason < 20 ký tự |
E-F46-004 "Bắt buộc nhập lý do chấm dứt (≥ 20 ký tự)" |
| 2b | service_status đã TERMINATED |
E-F46-001 "Dịch vụ đã chấm dứt" |
UC-F46-07: Chặn đăng nhập + lazy-expire (Login Gate) ​
Mô tả: SYSTEM chặn đăng nhập của user thuộc DN có dịch vụ EXPIRED/TERMINATED; đồng thời lazy-expire khi stored TRIAL/ACTIVE đã quá hạn theo ngày.
Tiền điều kiện: User thuộc một DN gọi POST /api/v1/auth/login (hoặc mọi request qua JwtStrategy.validate()).
Hậu điều kiện: Login bị chặn (403) nếu EXPIRED/TERMINATED; nếu lazy-expire, trạng thái flip + history + mirror + cache invalidated trong cùng transaction.
| Bước | Tác nhân | Hành động / Mô tả | Phản hồi / Xử lý hệ thống | Ghi chú |
|---|---|---|---|---|
| 1 | User DN | POST /api/v1/auth/login với credential hợp lệ |
1. AuthService.login() xác thực mật khẩu (như hiện tại)2. Đọc companies.status (mirror) của DN — rẻ, không JOIN3. Nếu status = 3 (EXPIRED) → dừng, return 403 (bước 5)4. Nếu status = 4 (TERMINATED) → dừng, return 403 (bước 6) |
Đọc mirror trước để chặn nhanh, tránh JOIN mọi login |
| 2 | SYSTEM | Nếu status ∈ {1 TRIAL, 2 ACTIVE} → kiểm tra hết hạn lazy |
Load company_subscriptions hiện hành; tính effective (Section 3.2):- TRIAL & today > trial_end_date → cần expire- ACTIVE & today > paid_until_date → cần expire |
|
| 3 | SYSTEM | Nếu cần expire → LAZY-EXPIRE | ATOMIC TRANSACTION: 1. UPDATE company_subscriptions SET service_status = EXPIRED, expired_at = NOW, version+12. INSERT subscription_histories {action: EXPIRE, from_status: (TRIAL/ACTIVE), to_status: EXPIRED, changed_by: SYSTEM, changed_at, reason: "Lazy-expire khi đăng nhập"}3. Mirror companies.status = 3 (EXPIRED)4. Invalidate cache user_perms:{userId} mọi user DN5. COMMIT |
BR-F46-07; SYSTEM là actor |
| 4 | SYSTEM | Sau lazy-expire → chặn login | Return 403 với message EXPIRED (bước 5) | |
| 5 | SYSTEM | Chặn EXPIRED | Response 403: { success: false, statusCode: 403, error: { code: "SERVICE_EXPIRED", message: "Cần liên hệ sale của gotrace để kích hoạt dịch vụ" } } (BR-F46-05, E-F46-005) |
|
| 6 | SYSTEM | Chặn TERMINATED | Response 403: { success: false, statusCode: 403, error: { code: "SERVICE_TERMINATED", message: "Cần liên hệ với quản trị viên gotrace" } } (BR-F46-06, E-F46-006) |
|
| 7 | SYSTEM | Nếu còn hạn (TRIAL/ACTIVE) hoặc DRAFT | Phát JWT bình thường; JwtStrategy.validate() cache user_perms:{userId} 10 phút |
DRAFT cho phép onboarding hạn chế |
Alternative Flow:
| Bước | Điều kiện | Xử lý |
|---|---|---|
| 2a | DN chưa có subscription nào | Coi như DRAFT — cho phép login (onboarding) |
| 3a | Redis down khi invalidate | Log cảnh báo; transaction DB commit; cache tự hết hạn ≤ 10' (fail-safe) |
| 7a | Token cache còn hiệu lực nhưng DN vừa bị TERMINATED trong 10' qua | Vì đã invalidate cache lúc TERMINATE (BR-F46-08), lần validate kế tiếp re-load → chặn ngay |
UC-F46-08: Xem lịch sử sử dụng dịch vụ ​
Tiền điều kiện: PLATFORM_ADMIN (mọi DN) HOẶC ENTERPRISE_ADMIN/ENTERPRISE_USER (chỉ DN của mình).
| Bước | Tác nhân | Hành động / Mô tả | Phản hồi / Xử lý hệ thống |
|---|---|---|---|
| 1 | User | Mở tab "Lịch sử sử dụng dịch vụ" (SCR-F46-05) trong chi tiết DN | Navigate; render {CompanySubscriptionHistoryTab} |
| 2 | SYSTEM | Query lịch sử | 1. Xác định company_id: nếu DN → ép company_id = current_company_id (BR-F46-14, multi-tenant); nếu ADMIN → theo tham số2. SELECT * FROM subscription_histories WHERE company_id = X ORDER BY changed_at DESC3. SELECT subscription hiện hành + subscription_documents4. Render timeline |
Alternative Flow: DN cố xem company_id ≠ của mình → 403 "Không có quyền xem lịch sử của DN khác".
UC-F46-09: Mở lại dịch vụ (Reactivate) ​
Tiền điều kiện: PLATFORM_ADMIN; subscription ở service_status = TERMINATED.
Hậu điều kiện: service_status = TRIAL/ACTIVE; history {REACTIVATE}; mirror; invalidate cache.
| Bước | Tác nhân | Hành động / Mô tả | Phản hồi / Xử lý hệ thống |
|---|---|---|---|
| 1 | ADMIN | SCR-F46-04 → "Mở lại" → dialog chọn trạng thái đích + lý do | |
| 2 | ADMIN | Xác nhận | ATOMIC: 1. Validate service_status = TERMINATED, lock version2. UPDATE service_status = (TRIAL/ACTIVE), terminated_at = NULL, terminate_reason = NULL, (set trial_end/paid_until nếu cần), version+1 3. INSERT history {REACTIVATE, from: TERMINATED, to: (TRIAL/ACTIVE), reason} 4. Mirror companies.status5. Invalidate cache user_perms mọi user DN6. COMMIT + Return 200 OK |
8. Thiết kế Database Schema ​
8.1. ER Diagram ​
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ companies (F01) │ │ service_plans (F45) │
│ id (BIGINT, PK) │ │ id (BIGSERIAL, PK) │
│ company_tax_code │ │ plan_code (UNIQUE) │
│ company_name │ │ plan_name │
│ company_email, contact_phone │ │ setup_fee, standardization_fee │
│ status (INT) ◄── mirror │ │ maintenance_fee_year │
│ service_status (0..4) │ │ qr_quota_year, max_products │
│ sale_user_id │ │ features (JSONB) │
└──────────┬──────────────────────┘ │ status (DRAFT/ACTIVE/DEPRECATED)│
│ 1:1 (hiện hành) └──────────┬──────────────────────┘
│ │ N:1 (plan_id + snapshot)
▼ ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ company_subscriptions (F46) │
│ id (BIGSERIAL, PK) │
│ company_id (BIGINT, FK → companies) ── UNIQUE WHERE deleted_at IS NULL │
│ plan_id (BIGINT, FK → service_plans) │
│ plan_code (VARCHAR 30) ── snapshot │
│ contract_code (VARCHAR 100) ── UNIQUE partial │
│ service_status (VARCHAR 20) ── DRAFT/TRIAL/ACTIVE/EXPIRED/TERMINATED │
│ service_start_date, trial_days, trial_end_date │
│ active_start_date, term_months, paid_until_date │
│ bonus_qr_quota, bonus_max_products │
│ effective_qr_quota, effective_max_products │
│ plan_snapshot (JSONB) ── chốt phí/quota/features lúc kích hoạt │
│ discount_percent │
│ activated_at/by, terminated_at/by, terminate_reason, expired_at │
│ version (INT) ── optimistic lock │
│ created_by/at, updated_by/at, deleted_at │
└──────┬────────────────────────────┬──────────────────────────────────────┘
│ 1:N │ 1:N
▼ ▼
┌───────────────────────────┐ ┌──────────────────────────────────────────┐
│ subscription_documents │ │ subscription_histories (APPEND-ONLY) │
│ id (BIGSERIAL, PK) │ │ id (BIGSERIAL, PK) │
│ subscription_id │ │ subscription_id (FK) │
│ (FK ON DELETE CASCADE) │ │ company_id (BIGINT) │
│ doc_type (CONTRACT/MOU/ │ │ action (CREATE/UPDATE/ACTIVATE/RENEW/ │
│ APPENDIX/OTHER) │ │ EXPIRE/TERMINATE/REACTIVATE) │
│ file_name, file_url │ │ from_status, to_status │
│ file_size, mime_type │ │ changed_by, changed_at │
│ uploaded_by/at │ │ changes_json (JSONB — diff before/after) │
│ deleted_at │ │ reason, ip_address │
└───────────────────────────┘ │ ── IMMUTABLE (trigger chống UPDATE/DELETE)│
└──────────────────────────────────────────┘
8.2. Mô tả bảng ​
Bảng company_subscriptions (1 bản ghi hiện hành / DN) ​
| Cột | Kiểu dữ liệu | Nullable | Default | Mô tả | Constraint |
|---|---|---|---|---|---|
id |
BIGSERIAL | NOT NULL | Khóa chính | PRIMARY KEY | |
company_id |
BIGINT | NOT NULL | DN sở hữu | FK → companies(id) | |
plan_id |
BIGINT | NOT NULL | Gói dịch vụ | FK → service_plans(id) | |
plan_code |
VARCHAR(30) | NOT NULL | Snapshot mã gói | ||
contract_code |
VARCHAR(100) | NULL | Mã hợp đồng | UNIQUE partial | |
service_status |
VARCHAR(20) | NOT NULL | 'DRAFT' | Trạng thái dịch vụ | CHECK IN (DRAFT,TRIAL,ACTIVE,EXPIRED,TERMINATED) |
service_start_date |
DATE | NULL | CURRENT_DATE | Ngày bắt đầu dịch vụ | |
trial_days |
INT | NOT NULL | 7 | Số ngày dùng thử | CHECK ≥ 0 |
trial_end_date |
DATE | NULL | service_start_date + trial_days |
||
active_start_date |
DATE | NULL | Ngày bắt đầu chính thức | ||
term_months |
INT | NULL | Kỳ hạn (tháng) | CHECK > 0 | |
paid_until_date |
DATE | NULL | Thanh toán đến ngày | ||
bonus_qr_quota |
INT | NOT NULL | 0 | QR tặng thêm | CHECK ≥ 0 |
bonus_max_products |
INT | NOT NULL | 0 | SP tặng thêm | CHECK ≥ 0 |
effective_qr_quota |
INT | NULL | plan.qr_quota_year + bonus_qr_quota |
||
effective_max_products |
INT | NULL | plan.max_products + bonus_max_products |
||
plan_snapshot |
JSONB | NULL | Chốt phí/quota/features lúc kích hoạt | ||
discount_percent |
NUMERIC(5,2) | NOT NULL | 0 | Chiết khấu thủ công | CHECK 0–100 |
note |
TEXT | NULL | Ghi chú nội bộ | ||
activated_at |
TIMESTAMPTZ | NULL | Thời điểm kích hoạt | ||
activated_by |
BIGINT | NULL | Người kích hoạt | FK → users(id) | |
terminated_at |
TIMESTAMPTZ | NULL | Thời điểm chấm dứt | ||
terminated_by |
BIGINT | NULL | Người chấm dứt | FK → users(id) | |
terminate_reason |
TEXT | NULL | Lý do chấm dứt | *NOT NULL khi TERMINATED | |
expired_at |
TIMESTAMPTZ | NULL | Thời điểm hết hạn (lazy) | ||
version |
INT | NOT NULL | 1 | Optimistic lock | |
created_by |
BIGINT | NULL | Người tạo | FK → users(id) | |
updated_by |
BIGINT | NULL | Người sửa | FK → users(id) | |
created_at |
TIMESTAMPTZ | NOT NULL | NOW() | Ngày tạo | |
updated_at |
TIMESTAMPTZ | NOT NULL | NOW() | Ngày cập nhật | |
deleted_at |
TIMESTAMPTZ | NULL | Soft delete |
Bảng subscription_documents ​
| Cột | Kiểu dữ liệu | Nullable | Default | Mô tả | Constraint |
|---|---|---|---|---|---|
id |
BIGSERIAL | NOT NULL | Khóa chính | PRIMARY KEY | |
subscription_id |
BIGINT | NOT NULL | Subscription cha | FK → company_subscriptions(id) ON DELETE CASCADE | |
doc_type |
VARCHAR(20) | NOT NULL | Loại tài liệu | CHECK IN (CONTRACT,MOU,APPENDIX,OTHER) | |
file_name |
VARCHAR(255) | NOT NULL | Tên file gốc | ||
file_url |
VARCHAR(500) | NOT NULL | MinIO key | ||
file_size |
BIGINT | NULL | Kích thước byte | CHECK ≤ 20971520 (20MB) | |
mime_type |
VARCHAR(100) | NULL | MIME | CHECK IN (application/pdf, image/jpeg, image/png) | |
uploaded_by |
BIGINT | NULL | Người upload | FK → users(id) | |
uploaded_at |
TIMESTAMPTZ | NOT NULL | NOW() | Thời điểm upload | |
deleted_at |
TIMESTAMPTZ | NULL | Soft delete |
Bảng subscription_histories (APPEND-ONLY, immutable) ​
| Cột | Kiểu dữ liệu | Nullable | Default | Mô tả | Constraint |
|---|---|---|---|---|---|
id |
BIGSERIAL | NOT NULL | Khóa chính | PRIMARY KEY | |
subscription_id |
BIGINT | NOT NULL | Subscription liên quan | FK → company_subscriptions(id) | |
company_id |
BIGINT | NOT NULL | DN (denormalize để query tab DN) | ||
action |
VARCHAR(20) | NOT NULL | Loại thao tác | CHECK IN (CREATE,UPDATE,ACTIVATE,RENEW,EXPIRE,TERMINATE,REACTIVATE) | |
from_status |
VARCHAR(20) | NULL | Trạng thái trước | ||
to_status |
VARCHAR(20) | NULL | Trạng thái sau | ||
changed_by |
BIGINT | NULL | Người thực hiện (NULL nếu SYSTEM) | FK → users(id) | |
changed_at |
TIMESTAMPTZ | NOT NULL | NOW() | Thời điểm | |
changes_json |
JSONB | NULL | Diff before/after (BẮT BUỘC cho UPDATE) | ||
reason |
TEXT | NULL | Lý do (bắt buộc cho TERMINATE) | ||
ip_address |
VARCHAR(45) | NULL | IP người thực hiện |
8.3. Indexes ​
company_subscriptions:
UNIQUE (company_id) WHERE deleted_at IS NULL— 1 subscription hiện hành/DN (BR-F46-10)UNIQUE (contract_code) WHERE contract_code IS NOT NULL AND deleted_at IS NULL(BR-F46-11)INDEX (service_status)INDEX (service_start_date)INDEX (activated_at)INDEX (paid_until_date)
subscription_documents:
INDEX (subscription_id)
subscription_histories:
INDEX (subscription_id, changed_at DESC)INDEX (company_id, changed_at DESC)
companies (mirror):
INDEX (status)— phục vụ login gate đọc nhanh
8.4. Migration SQL ​
-- =========================================================================
-- Migration: TXNG-F46 — Kích hoạt & Vòng đời Dịch vụ Doanh nghiệp
-- Depends: TXNG-F45 (service_plans), companies, users
-- =========================================================================
-- 1) company_subscriptions -----------------------------------------------
CREATE TABLE company_subscriptions (
id BIGSERIAL PRIMARY KEY,
company_id BIGINT NOT NULL REFERENCES companies(id),
plan_id BIGINT NOT NULL REFERENCES service_plans(id),
plan_code VARCHAR(30) NOT NULL,
contract_code VARCHAR(100),
service_status VARCHAR(20) NOT NULL DEFAULT 'DRAFT'
CHECK (service_status IN ('DRAFT','TRIAL','ACTIVE','EXPIRED','TERMINATED')),
service_start_date DATE DEFAULT CURRENT_DATE,
trial_days INT NOT NULL DEFAULT 7 CHECK (trial_days >= 0),
trial_end_date DATE,
active_start_date DATE,
term_months INT CHECK (term_months IS NULL OR term_months > 0),
paid_until_date DATE,
bonus_qr_quota INT NOT NULL DEFAULT 0 CHECK (bonus_qr_quota >= 0),
bonus_max_products INT NOT NULL DEFAULT 0 CHECK (bonus_max_products >= 0),
effective_qr_quota INT,
effective_max_products INT,
plan_snapshot JSONB,
discount_percent NUMERIC(5,2) NOT NULL DEFAULT 0 CHECK (discount_percent >= 0 AND discount_percent <= 100),
note TEXT,
activated_at TIMESTAMPTZ,
activated_by BIGINT REFERENCES users(id),
terminated_at TIMESTAMPTZ,
terminated_by BIGINT REFERENCES users(id),
terminate_reason TEXT,
expired_at TIMESTAMPTZ,
version INT NOT NULL DEFAULT 1,
created_by BIGINT REFERENCES users(id),
updated_by BIGINT REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
-- TERMINATED bắt buộc có lý do
CONSTRAINT chk_terminate_reason
CHECK (service_status <> 'TERMINATED' OR terminate_reason IS NOT NULL)
);
CREATE UNIQUE INDEX uq_company_subscriptions_company
ON company_subscriptions (company_id) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX uq_company_subscriptions_contract
ON company_subscriptions (contract_code)
WHERE contract_code IS NOT NULL AND deleted_at IS NULL;
CREATE INDEX idx_company_subscriptions_status ON company_subscriptions (service_status);
CREATE INDEX idx_company_subscriptions_start ON company_subscriptions (service_start_date);
CREATE INDEX idx_company_subscriptions_activated ON company_subscriptions (activated_at);
CREATE INDEX idx_company_subscriptions_paid_until ON company_subscriptions (paid_until_date);
-- 2) subscription_documents ----------------------------------------------
CREATE TABLE subscription_documents (
id BIGSERIAL PRIMARY KEY,
subscription_id BIGINT NOT NULL REFERENCES company_subscriptions(id) ON DELETE CASCADE,
doc_type VARCHAR(20) NOT NULL
CHECK (doc_type IN ('CONTRACT','MOU','APPENDIX','OTHER')),
file_name VARCHAR(255) NOT NULL,
file_url VARCHAR(500) NOT NULL,
file_size BIGINT CHECK (file_size IS NULL OR file_size <= 20971520),
mime_type VARCHAR(100)
CHECK (mime_type IS NULL OR mime_type IN ('application/pdf','image/jpeg','image/png')),
uploaded_by BIGINT REFERENCES users(id),
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_subscription_documents_sub ON subscription_documents (subscription_id);
-- 3) subscription_histories (APPEND-ONLY) --------------------------------
CREATE TABLE subscription_histories (
id BIGSERIAL PRIMARY KEY,
subscription_id BIGINT NOT NULL REFERENCES company_subscriptions(id),
company_id BIGINT NOT NULL,
action VARCHAR(20) NOT NULL
CHECK (action IN ('CREATE','UPDATE','ACTIVATE','RENEW','EXPIRE','TERMINATE','REACTIVATE')),
from_status VARCHAR(20),
to_status VARCHAR(20),
changed_by BIGINT REFERENCES users(id),
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
changes_json JSONB,
reason TEXT,
ip_address VARCHAR(45)
);
CREATE INDEX idx_subscription_histories_sub
ON subscription_histories (subscription_id, changed_at DESC);
CREATE INDEX idx_subscription_histories_company
ON subscription_histories (company_id, changed_at DESC);
-- 4) Trigger chống UPDATE/DELETE trên subscription_histories (immutable) --
CREATE OR REPLACE FUNCTION trg_block_subscription_histories_mutation()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'subscription_histories is append-only (immutable audit log); % không được phép', TG_OP;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER block_subscription_histories_update
BEFORE UPDATE ON subscription_histories
FOR EACH ROW EXECUTE FUNCTION trg_block_subscription_histories_mutation();
CREATE TRIGGER block_subscription_histories_delete
BEFORE DELETE ON subscription_histories
FOR EACH ROW EXECUTE FUNCTION trg_block_subscription_histories_mutation();
-- 5) Mirror companies.status ---------------------------------------------
-- companies.status (INT, đã có sẵn) = mirror service_status:
-- DRAFT=0, TRIAL=1, ACTIVE=2, EXPIRED=3, TERMINATED=4
-- Nguồn sự thật = company_subscriptions.service_status; cập nhật cùng transaction.
COMMENT ON COLUMN companies.status IS
'Mirror company_subscriptions.service_status (DRAFT=0,TRIAL=1,ACTIVE=2,EXPIRED=3,TERMINATED=4) — phục vụ login gate; nguồn sự thật = company_subscriptions';
-- =========================================================================
-- INSERT ví dụ: kích hoạt DN id=1001 gói PKG_STANDARD, trial 7 ngày
-- =========================================================================
-- Giả định service_plans có bản ghi PKG_STANDARD id=2 (qr_quota_year=20000, max_products=50)
INSERT INTO company_subscriptions (
company_id, plan_id, plan_code, contract_code, service_status,
service_start_date, trial_days, trial_end_date, term_months,
bonus_qr_quota, bonus_max_products, effective_qr_quota, effective_max_products,
discount_percent, created_by, created_at, updated_at
) VALUES (
1001, 2, 'PKG_STANDARD', 'HD-2026-0001', 'DRAFT',
'2026-07-01', 7, '2026-07-08', 12,
0, 0, 20000, 50,
0, 1, NOW(), NOW()
);
-- Ghi nhật ký CREATE (cùng transaction thực tế)
INSERT INTO subscription_histories (
subscription_id, company_id, action, from_status, to_status,
changed_by, changed_at, reason, ip_address
) VALUES (
currval('company_subscriptions_id_seq'), 1001, 'CREATE', NULL, 'DRAFT',
1, NOW(), 'Tạo yêu cầu kích hoạt gói Chuẩn hóa', '10.0.0.5'
);
-- Ví dụ ACTIVATE (DRAFT -> TRIAL): mirror companies.status = 1
-- UPDATE company_subscriptions SET service_status='TRIAL', plan_snapshot='{...}'::jsonb,
-- activated_at=NOW(), activated_by=1, version=version+1 WHERE id=...;
-- INSERT subscription_histories (..., action='ACTIVATE', from_status='DRAFT', to_status='TRIAL', ...);
-- UPDATE companies SET status=1 WHERE id=1001;
9. Thiết kế API ​
9.1. Danh sách endpoint ​
Tất cả endpoint quản trị dùng prefix /api/v1/admin, Bearer Token JWT, chặn qua AdminGuard (chỉ PLATFORM_ADMIN). Envelope: { success, statusCode, data }.
| # | Method | Endpoint | Mô tả | Role |
|---|---|---|---|---|
| 1 | GET | /api/v1/admin/company-subscriptions |
Danh sách + filter (status, plan_code, khoảng Active, search MST/tên/SĐT/email) + pagination | PLATFORM_ADMIN |
| 2 | GET | /api/v1/admin/company-subscriptions/analysis |
Summary cards đếm theo trạng thái | PLATFORM_ADMIN |
| 3 | POST | /api/v1/admin/company-subscriptions |
Tạo yêu cầu kích hoạt (DRAFT) |
PLATFORM_ADMIN |
| 4 | GET | /api/v1/admin/company-subscriptions/:id |
Chi tiết subscription + hồ sơ | PLATFORM_ADMIN |
| 5 | PUT | /api/v1/admin/company-subscriptions/:id |
Sửa (chỉ DRAFT) — ghi history UPDATE |
PLATFORM_ADMIN |
| 6 | POST | /api/v1/admin/company-subscriptions/:id/activate |
Kích hoạt (DRAFT→TRIAL/ACTIVE) hoặc chuyển TRIAL→ACTIVE |
PLATFORM_ADMIN |
| 7 | POST | /api/v1/admin/company-subscriptions/:id/renew |
Gia hạn (EXPIRED→ACTIVE / extend) |
PLATFORM_ADMIN |
| 8 | POST | /api/v1/admin/company-subscriptions/:id/terminate |
Chấm dứt (→TERMINATED, bắt buộc reason) |
PLATFORM_ADMIN |
| 9 | POST | /api/v1/admin/company-subscriptions/:id/reactivate |
Mở lại (TERMINATED→TRIAL/ACTIVE) |
PLATFORM_ADMIN |
| 10 | POST | /api/v1/admin/company-subscriptions/:id/documents |
Upload hồ sơ (multipart) | PLATFORM_ADMIN |
| 11 | GET | /api/v1/admin/company-subscriptions/:id/history |
Nhật ký của subscription | PLATFORM_ADMIN |
| 12 | GET | /api/v1/companies/:id/subscription-history |
Lịch sử dịch vụ của DN (admin mọi DN; DN chỉ của mình) | PLATFORM_ADMIN, ENTERPRISE_ADMIN, ENTERPRISE_USER |
| 13 | POST | /api/v1/auth/login |
Sửa: login gate — chặn EXPIRED/TERMINATED + lazy-expire | ALL (public) |
9.2. Chi tiết API ​
9.2.1. POST /api/v1/admin/company-subscriptions — Tạo yêu cầu ​
Request:
{
"company_id": 1001,
"plan_id": 2,
"contract_code": "HD-2026-0001",
"term_months": 12,
"trial_days": 7,
"service_start_date": "2026-07-01",
"bonus_qr_quota": 0,
"bonus_max_products": 0,
"discount_percent": 0,
"note": "Khách giới thiệu bởi sale A"
}
Response 201:
{
"success": true,
"statusCode": 201,
"data": {
"id": 55,
"company_id": 1001,
"plan_code": "PKG_STANDARD",
"service_status": "DRAFT",
"contract_code": "HD-2026-0001",
"trial_end_date": "2026-07-08",
"effective_qr_quota": 20000,
"effective_max_products": 50,
"version": 1
}
}
Lỗi: 400 (E-F46-001/002/003), 409 (BR-F46-10 concurrent).
9.2.2. POST /api/v1/admin/company-subscriptions/:id/activate — Kích hoạt ​
Request: { "version": 1 } (optimistic lock; body có thể kèm active_start_date khi chuyển TRIAL→ACTIVE).
Response 200:
{
"success": true,
"statusCode": 200,
"data": {
"id": 55,
"service_status": "TRIAL",
"trial_end_date": "2026-07-08",
"activated_at": "2026-07-01T03:00:00Z",
"plan_snapshot": {
"setup_fee": 3000000, "standardization_fee": 5000000,
"maintenance_fee_year": 9500000, "qr_quota_year": 20000,
"max_products": 50, "features": { "recall_management": false, "api_integration": "none" }
},
"companies_status_mirror": 1,
"version": 2
}
}
Lỗi: 400 (E-F46-001), 409 (E-F46-007 version lệch).
9.2.3. POST /api/v1/admin/company-subscriptions/:id/renew — Gia hạn ​
Request: { "term_months": 12, "version": 3 }
Response 200: { "success": true, "statusCode": 200, "data": { "service_status": "ACTIVE", "paid_until_date": "2027-07-08", "version": 4 } }
9.2.4. POST /api/v1/admin/company-subscriptions/:id/terminate — Chấm dứt ​
Request: { "terminate_reason": "DN vi phạm điều khoản hợp đồng dịch vụ", "version": 4 }
Response 200: { "success": true, "statusCode": 200, "data": { "service_status": "TERMINATED", "terminated_at": "2026-09-01T02:00:00Z", "companies_status_mirror": 4, "version": 5 } }
Lỗi: 400 (E-F46-004 thiếu/ngắn reason).
9.2.5. POST /api/v1/admin/company-subscriptions/:id/documents — Upload hồ sơ ​
Request: multipart/form-data — doc_type=CONTRACT, file=@hopdong.pdf (FileFieldsInterceptor).
Response 201: { "success": true, "statusCode": 201, "data": { "id": 88, "doc_type": "CONTRACT", "file_name": "hopdong.pdf", "file_url": "subscriptions/55/hopdong.pdf", "file_size": 1048576 } }
Lỗi: 400 (E-F46-003 quá 20MB / sai định dạng).
9.2.6. GET /api/v1/companies/:id/subscription-history — Lịch sử dịch vụ ​
Response 200:
{
"success": true,
"statusCode": 200,
"data": {
"current": { "plan_code": "PKG_STANDARD", "service_status": "ACTIVE", "paid_until_date": "2027-07-08" },
"histories": [
{ "action": "ACTIVATE", "from_status": "DRAFT", "to_status": "TRIAL", "changed_at": "2026-07-01T03:00:00Z", "changed_by": "Admin GoTrace" },
{ "action": "ACTIVATE", "from_status": "TRIAL", "to_status": "ACTIVE", "changed_at": "2026-07-08T01:00:00Z" }
]
}
}
Multi-tenant: nếu caller là ENTERPRISE_ADMIN/USER, server ép
company_id = current_company_id; truy vấn DN khác → 403 (BR-F46-14).
9.2.7. POST /api/v1/auth/login — Login gate (SỬA) ​
Request: { "email": "user@dn.com", "password": "..." }
Xử lý bổ sung (login gate):
- Xác thực credential (như cũ).
- Đọc
companies.status(mirror) của DN user. - Nếu
status = 1 (TRIAL)/2 (ACTIVE): kiểm tra lazy-expire theotrial_end_date/paid_until_date; nếu quá hạn → flipEXPIRED(transaction: UPDATE + history {EXPIRE} + mirror status=3 + invalidateuser_perms:{userId}mọi user DN). - Nếu (sau lazy)
EXPIRED→ 403 SERVICE_EXPIRED; nếuTERMINATED→ 403 SERVICE_TERMINATED. - Ngược lại (DRAFT/TRIAL/ACTIVE còn hạn) → phát JWT;
JwtStrategy.validate()cacheuser_perms:{userId}10'.
Response 403 — EXPIRED (E-F46-005):
{
"success": false,
"statusCode": 403,
"error": { "code": "SERVICE_EXPIRED", "message": "Cần liên hệ sale của gotrace để kích hoạt dịch vụ" }
}
Response 403 — TERMINATED (E-F46-006):
{
"success": false,
"statusCode": 403,
"error": { "code": "SERVICE_TERMINATED", "message": "Cần liên hệ với quản trị viên gotrace" }
}
9.2.8. GET /api/v1/admin/company-subscriptions — Danh sách ​
Query params: page, page_size, service_status, plan_code, active_from, active_to, keyword (MST/tên/SĐT/email).
Response 200:
{
"success": true,
"statusCode": 200,
"data": {
"items": [ { "id": 55, "company_name": "Công ty ABC", "company_tax_code": "0101234567", "plan_code": "PKG_STANDARD", "service_status": "ACTIVE", "paid_until_date": "2027-07-08" } ],
"pagination": { "page": 1, "page_size": 20, "total_items": 137, "total_pages": 7 }
}
}
10. Test Cases cho QA ​
| TC ID | Use Case | Tiền điều kiện | Bước thực hiện | Kết quả mong đợi |
|---|---|---|---|---|
| TC-01-01 | UC-F46-01 | DN chưa có sub | Tạo yêu cầu gói PKG_STANDARD, trial 7 | 201; service_status=DRAFT; history {CREATE}; companies.status=0 |
| TC-01-02 | UC-F46-01 | DN đang ACTIVE |
Tạo yêu cầu mới cho DN đó | 400 E-F46-001 |
| TC-01-03 | UC-F46-01 | — | Bỏ trống contract_code |
400 E-F46-002 |
| TC-01-04 | UC-F46-01 | contract_code "HD-1" đã dùng |
Tạo với "HD-1" | 400 E-F46-002 (trùng) |
| TC-01-05 | UC-F46-01 | — | Chọn gói DEPRECATED |
400 (BR-F45-05) |
| TC-01-06 | UC-F46-01 | — | Upload file 25MB | 400 E-F46-003 |
| TC-02-01 | UC-F46-02 | Sub DRAFT |
Sửa term_months 12→24 |
200; history {UPDATE, changes_json diff}; version+1 |
| TC-02-02 | UC-F46-02 | Sub TRIAL |
Cố sửa | 400 E-F46-001 |
| TC-02-03 | UC-F46-02 | version cũ | Sửa với version lệch | 409 E-F46-007 |
| TC-03-01 | UC-F46-03 | Sub DRAFT, trial=7 |
Kích hoạt | 200; TRIAL; plan_snapshot chốt; effective_qr_quota=plan+bonus; companies.status=1; cache invalidated; N-F46-01 |
| TC-03-02 | UC-F46-03 | Sub DRAFT, trial=0 |
Kích hoạt | 200; nhảy thẳng ACTIVE; paid_until_date set; companies.status=2 |
| TC-03-03 | UC-F46-03 | Sub TRIAL |
Gọi activate | 400 E-F46-001 (dùng /activate cho TRIAL→ACTIVE riêng) |
| TC-04-01 | UC-F46-04 | Sub TRIAL |
Chuyển Active | 200; ACTIVE; paid_until_date=active_start+term; mirror 2 |
| TC-05-01 | UC-F46-05 | Sub EXPIRED |
Gia hạn 12 tháng | 200; ACTIVE; paid_until_date +12m từ hôm nay; history {RENEW}; mirror 2 |
| TC-05-02 | UC-F46-05 | Sub ACTIVE |
Gia hạn 12 tháng | 200; paid_until_date cộng dồn từ ngày cũ |
| TC-06-01 | UC-F46-06 | Sub ACTIVE |
Chấm dứt, reason 30 ký tự | 200; TERMINATED; companies.status=4; cache invalidated; N-F46-04 |
| TC-06-02 | UC-F46-06 | Sub ACTIVE |
Chấm dứt, reason 5 ký tự | 400 E-F46-004 |
| TC-07-01 | UC-F46-07 | DN EXPIRED |
User DN login | 403 SERVICE_EXPIRED, đúng message |
| TC-07-02 | UC-F46-07 | DN TERMINATED |
User DN login | 403 SERVICE_TERMINATED, đúng message |
| TC-07-03 | UC-F46-07 | DN TRIAL, trial_end_date = hôm qua |
User DN login | Lazy-expire: flip EXPIRED + history {EXPIRE} + mirror 3 + invalidate; rồi 403 SERVICE_EXPIRED |
| TC-07-04 | UC-F46-07 | DN ACTIVE, paid_until_date = hôm qua |
User DN login | Lazy-expire tương tự → 403 |
| TC-07-05 | UC-F46-07 | DN ACTIVE còn hạn |
User DN login | 200; JWT phát; cache user_perms 10' |
| TC-07-06 | UC-F46-07 | DN DRAFT |
User DN login | 200 (onboarding hạn chế) |
| TC-08-01 | UC-F46-08 | DN A đăng nhập | Xem lịch sử DN A | 200; timeline đúng |
| TC-08-02 | UC-F46-08 | DN A đăng nhập | Gọi history của DN B | 403 (multi-tenant BR-F46-14) |
| TC-09-01 | UC-F46-09 | Sub TERMINATED |
Mở lại → ACTIVE | 200; ACTIVE; terminate_reason clear; history {REACTIVATE}; mirror 2 |
| TC-10-01 | Immutable | History đã ghi | UPDATE subscription_histories SET reason='x' |
RAISE EXCEPTION (trigger chặn) |
| TC-10-02 | Immutable | History đã ghi | DELETE FROM subscription_histories |
RAISE EXCEPTION (trigger chặn) |
| TC-11-01 | Performance | 137 sub | GET list page 1 | ≤ 500ms; pagination đúng |
| TC-11-02 | Performance | — | Kích hoạt (transaction) | ≤ 1 giây (write) |
11. Acceptance Criteria ​
| AC ID | Tiêu chí chấp nhận |
|---|---|
| AC-F46-01 | Chỉ PLATFORM_ADMIN (qua AdminGuard) truy cập được các endpoint /api/v1/admin/company-subscriptions/*; role khác → 403 |
| AC-F46-02 | Mỗi DN chỉ có tối đa 1 subscription hiện hành (partial unique company_id); tạo trùng → 409 |
| AC-F46-03 | contract_code unique (partial); trùng → 400 |
| AC-F46-04 | Kích hoạt chốt plan_snapshot và effective_qr_quota = plan.qr_quota_year + bonus_qr_quota, effective_max_products = plan.max_products + bonus_max_products |
| AC-F46-05 | Mọi chuyển trạng thái ghi 1 bản ghi subscription_histories trong cùng transaction (append-only); rollback nếu lỗi |
| AC-F46-06 | subscription_histories không thể UPDATE/DELETE (trigger raise exception) |
| AC-F46-07 | Mọi chuyển trạng thái mirror companies.status đúng bảng map (0..4) trong cùng transaction |
| AC-F46-08 | Mọi chuyển trạng thái invalidate cache user_perms:{userId} của tất cả user thuộc DN |
| AC-F46-09 | User DN có dịch vụ EXPIRED bị chặn login với message "Cần liên hệ sale của gotrace để kích hoạt dịch vụ" |
| AC-F46-10 | User DN có dịch vụ TERMINATED bị chặn login với message "Cần liên hệ với quản trị viên gotrace" |
| AC-F46-11 | Login của user DN có stored TRIAL/ACTIVE quá hạn tự động lazy-expire (flip EXPIRED + history + mirror + invalidate) rồi chặn |
| AC-F46-12 | Chấm dứt bắt buộc terminate_reason ≥ 20 ký tự; thiếu → 400 |
| AC-F46-13 | Upload hồ sơ chỉ PDF/JPG/PNG ≤ 20MB; sai → 400 |
| AC-F46-14 | ENTERPRISE_ADMIN/USER chỉ xem lịch sử dịch vụ của chính DN mình; DN khác → 403 |
| AC-F46-15 | Sửa/chuyển trạng thái dùng optimistic lock version; concurrent → 409 |
| AC-F46-16 | Không phát sinh CTE/KDE (module vận hành thương mại) |
| AC-F46-17 | Không dùng cron ở v1; hết hạn đánh giá lazy khi login |
12. Chuyển đổi trạng thái (State Transitions) ​
Bảng đầy đủ transition của SERVICE_STATUS. Mọi transition (trừ không có) thực hiện trong cùng $transaction: ghi subscription_histories + mirror companies.status + invalidate cache user_perms.
| # | From | To | Actor | Action (history) | Precondition | Hành động hệ thống |
|---|---|---|---|---|---|---|
| T1 | (none) | DRAFT | PLATFORM_ADMIN | CREATE | DN đủ điều kiện (BR-F46-02); gói ACTIVE | INSERT sub; history CREATE; mirror 0 |
| T2 | DRAFT | DRAFT | PLATFORM_ADMIN | UPDATE | version khớp | UPDATE; history UPDATE (changes_json); mirror giữ 0 |
| T3 | DRAFT | TRIAL | PLATFORM_ADMIN | ACTIVATE | trial_days > 0 |
Chốt snapshot; effective quota; trial_end_date; history ACTIVATE; mirror 1; invalidate cache |
| T4 | DRAFT | ACTIVE | PLATFORM_ADMIN | ACTIVATE | trial_days = 0 |
Chốt snapshot; active_start_date, paid_until_date; history ACTIVATE; mirror 2; invalidate |
| T5 | TRIAL | ACTIVE | PLATFORM_ADMIN | ACTIVATE | status=TRIAL | active_start_date, paid_until_date; history ACTIVATE; mirror 2; invalidate |
| T6 | TRIAL | EXPIRED | SYSTEM | EXPIRE | today > trial_end_date (login) |
Lazy-expire; history EXPIRE; mirror 3; invalidate |
| T7 | ACTIVE | EXPIRED | SYSTEM | EXPIRE | today > paid_until_date (login) |
Lazy-expire; history EXPIRE; mirror 3; invalidate |
| T8 | EXPIRED | ACTIVE | PLATFORM_ADMIN | RENEW | status=EXPIRED | paid_until_date += term; history RENEW; mirror 2; invalidate |
| T9 | ACTIVE | ACTIVE | PLATFORM_ADMIN | RENEW | status=ACTIVE (extend) | paid_until_date += term; history RENEW; mirror giữ 2; invalidate |
| T10 | TRIAL | TERMINATED | PLATFORM_ADMIN | TERMINATE | reason ≥ 20 | history TERMINATE; mirror 4; invalidate |
| T11 | ACTIVE | TERMINATED | PLATFORM_ADMIN | TERMINATE | reason ≥ 20 | history TERMINATE; mirror 4; invalidate |
| T12 | EXPIRED | TERMINATED | PLATFORM_ADMIN | TERMINATE | reason ≥ 20 | history TERMINATE; mirror 4; invalidate |
| T13 | TERMINATED | TRIAL | PLATFORM_ADMIN | REACTIVATE | status=TERMINATED | clear terminate_*; history REACTIVATE; mirror 1; invalidate |
| T14 | TERMINATED | ACTIVE | PLATFORM_ADMIN | REACTIVATE | status=TERMINATED | clear terminate_*; paid_until_date; history REACTIVATE; mirror 2; invalidate |
Các transition không hợp lệ (vd
EXPIRED → TRIALtrực tiếp,ACTIVE → DRAFT) đều bị chặn với E-F46-001.
13. Notification List ​
| ID | Sự kiện | Kênh | Người nhận | Template message |
|---|---|---|---|---|
| N-F46-01 | Kích hoạt thành công | In-app + Email | ENTERPRISE_ADMIN (DN) | "Dịch vụ GoTrace của [company_name] đã được kích hoạt gói [plan_code]. Thời hạn dùng thử đến [trial_end_date]." |
| N-F46-02 | Sắp hết trial (≤ 3 ngày) | In-app + Email | ENTERPRISE_ADMIN (DN) | "Thời gian dùng thử của [company_name] còn [n] ngày (đến [trial_end_date]). Vui lòng liên hệ để chuyển gói chính thức." |
| N-F46-03 | Hết hạn dịch vụ | In-app + Email | ENTERPRISE_ADMIN (DN) | "Dịch vụ GoTrace của [company_name] đã hết hạn ngày [expired_at]. Vui lòng liên hệ sale để gia hạn." |
| N-F46-04 | Dịch vụ bị chấm dứt | In-app + Email | ENTERPRISE_ADMIN (DN) | "Dịch vụ GoTrace của [company_name] đã bị chấm dứt. Lý do: [terminate_reason]. Vui lòng liên hệ quản trị viên." |
Ghi chú: N-F46-02/N-F46-03 ở v1 phát khi có sự kiện login/thao tác admin phát hiện (không cron). Warning W-F46-001/002 (Section 14) là biến thể hiển thị trên UI admin.
14. Error / Warning Message Handling ​
14.1. Errors (chặn hành động) ​
| Mã | Mô tả | Điều kiện kích hoạt | Xử lý hệ thống |
|---|---|---|---|
| E-F46-001 | DN không đủ điều kiện kích hoạt (đang TRIAL/ACTIVE) hoặc trạng thái không cho phép thao tác | Tạo/kích hoạt/sửa khi DN đang có sub hiện hành hoặc trạng thái sai; gói DEPRECATED | HTTP 400; chặn; hiển thị "DN không đủ điều kiện kích hoạt" |
| E-F46-002 | Thiếu / trùng mã hợp đồng | contract_code rỗng hoặc đã tồn tại (partial unique) |
HTTP 400; chặn; "Bắt buộc nhập mã hợp đồng" / "Mã hợp đồng đã tồn tại" |
| E-F46-003 | File vượt 20MB hoặc sai định dạng | Upload file > 20MB hoặc ≠ PDF/JPG/PNG | HTTP 400; chặn; "File tối đa 20MB, chỉ chấp nhận PDF/JPG/PNG" |
| E-F46-004 | Chấm dứt thiếu lý do | terminate_reason rỗng hoặc < 20 ký tự |
HTTP 400; chặn; "Bắt buộc nhập lý do chấm dứt (≥ 20 ký tự)" |
| E-F46-005 | Login khi EXPIRED | User DN có service_status/mirror = EXPIRED đăng nhập |
HTTP 403 SERVICE_EXPIRED; "Cần liên hệ sale của gotrace để kích hoạt dịch vụ" |
| E-F46-006 | Login khi TERMINATED | User DN có service_status/mirror = TERMINATED đăng nhập |
HTTP 403 SERVICE_TERMINATED; "Cần liên hệ với quản trị viên gotrace" |
| E-F46-007 | Concurrent edit (version lệch) | version gửi lên ≠ version DB |
HTTP 409; "Bản ghi đã được cập nhật bởi người khác, vui lòng tải lại" |
14.2. Warnings (cho phép tiếp tục, cảnh báo) ​
| Mã | Mô tả | Điều kiện kích hoạt | Xử lý hệ thống |
|---|---|---|---|
| W-F46-001 | Trial sắp hết hạn | trial_end_date - today ≤ 3 ngày |
Badge cảnh báo trên list/detail; N-F46-02 |
| W-F46-002 | Dịch vụ sắp hết hạn | paid_until_date - today ≤ 30 ngày |
Badge cảnh báo; gợi ý gia hạn |
15. Ma trận truy xuất (Traceability Matrix) ​
| Use Case | Screen | Business Rule | Acceptance | Test Case | Error/Warning | Notification |
|---|---|---|---|---|---|---|
| UC-F46-01 | SCR-F46-02 | BR-F46-01,02,10,11,12 | AC-F46-02,03,05 | TC-01-01..06 | E-F46-001,002,003 | — |
| UC-F46-02 | SCR-F46-03 | BR-F46-01,15,17 | AC-F46-05,15 | TC-02-01..03 | E-F46-001,007 | — |
| UC-F46-03 | SCR-F46-04 | BR-F46-01,03,04,08,09,16 | AC-F46-04,05,07,08 | TC-03-01..03 | E-F46-001,007 | N-F46-01 |
| UC-F46-04 | SCR-F46-04 | BR-F46-01,08,09,16 | AC-F46-05,07,08 | TC-04-01 | E-F46-001 | — |
| UC-F46-05 | SCR-F46-04 | BR-F46-01,08,09,13,16 | AC-F46-05,07,08 | TC-05-01,02 | E-F46-001 | — |
| UC-F46-06 | SCR-F46-04 | BR-F46-01,06,08,09,16 | AC-F46-05,07,08,12 | TC-06-01,02 | E-F46-004 | N-F46-04 |
| UC-F46-07 | (login gate) | BR-F46-05,06,07,08,09 | AC-F46-09,10,11 | TC-07-01..06 | E-F46-005,006 | N-F46-03 |
| UC-F46-08 | SCR-F46-05 | BR-F46-14 | AC-F46-14 | TC-08-01,02 | — | — |
| UC-F46-09 | SCR-F46-04 | BR-F46-01,08,09,16 | AC-F46-05,07,08 | TC-09-01 | E-F46-001 | — |
| (Immutable log) | — | BR-F46-01 | AC-F46-06 | TC-10-01,02 | — | — |
16. Quy tắc nghiệp vụ (Business Rules) ​
| Mã | Quy tắc | Giải thích / Cơ sở |
|---|---|---|
| BR-F46-01 | Mỗi thay đổi trạng thái/dữ liệu ghi 1 bản ghi subscription_histories trong CÙNG $transaction (append-only, immutable qua trigger) |
Audit trail bất biến; Luật GDĐT 2023; mẫu insertPoHistory |
| BR-F46-02 | Chỉ DN có service_status ∈ {DRAFT, EXPIRED, TERMINATED} hoặc chưa có subscription mới được tạo/kích hoạt mới |
Tránh double-active; nghiệp vụ thương mại |
| BR-F46-03 | trial_days mặc định 7 (≥ 0); service_start_date mặc định hôm nay; trial_end_date = service_start_date + trial_days |
Chính sách dùng thử |
| BR-F46-04 | effective_qr_quota = plan.qr_quota_year + bonus_qr_quota; effective_max_products = plan.max_products + bonus_max_products |
Ràng buộc TXNG-F16/F03 |
| BR-F46-05 | EXPIRED chặn login với message "Cần liên hệ sale của gotrace để kích hoạt dịch vụ" |
Login gate; §2.43 |
| BR-F46-06 | TERMINATED chặn login với message "Cần liên hệ với quản trị viên gotrace"; bắt buộc terminate_reason |
Login gate; §2.43 |
| BR-F46-07 | Lazy-expire: login thấy stored TRIAL/ACTIVE quá hạn → flip EXPIRED + ghi history + mirror + invalidate cache |
Không cron ở v1 |
| BR-F46-08 | Invalidate cache user_perms:{userId} của MỌI user thuộc DN khi đổi status |
JwtStrategy.validate() cache 10' |
| BR-F46-09 | Mirror companies.status ↔ service_status (0..4) trong cùng transaction |
Chặn login rẻ; nguồn sự thật = company_subscriptions |
| BR-F46-10 | 1 subscription hiện hành/DN (partial unique company_id WHERE deleted_at IS NULL) |
Data integrity |
| BR-F46-11 | contract_code unique (partial) khi không NULL |
Data integrity |
| BR-F46-12 | Upload chỉ PDF/JPG/PNG ≤ 20MB; doc_type theo enum §2.47 |
Bảo mật file; NĐ 13/2023 |
| BR-F46-13 | Chiết khấu duy trì theo term_months: 24 tháng −10%, 36 tháng −20% (trước VAT); áp khi kích hoạt/gia hạn |
Sale_Pricing.md; BR-F45-06 |
| BR-F46-14 | Multi-tenant: ENTERPRISE_ADMIN/USER chỉ xem lịch sử dịch vụ của chính DN (company_id = current) |
Multi-tenant isolation |
| BR-F46-15 | Optimistic lock qua version; concurrent edit → 409 |
Concurrent access |
| BR-F46-16 | Kích hoạt/gia hạn/chấm dứt/mở lại chỉ PLATFORM_ADMIN (qua AdminGuard) |
Phân quyền |
| BR-F46-17 | changes_json ghi before/after (ai sửa field gì) cho action UPDATE |
Audit chi tiết; NĐ 13/2023 |
17. Ràng buộc & Giả định (Constraints / Assumptions) ​
Ràng buộc:
- Không cron ở v1 — hết hạn (
EXPIRED) đánh giá theo kiểu lazy khi user DN đăng nhập (UC-F46-07). Cron quétpaid_until_date/trial_end_datenền tảng là hạng mục v1.5 (để notification N-F46-02/03 chủ động hơn). - Cache invalidation — mọi chuyển trạng thái phải xóa key
user_perms:{userId}của mọi user thuộc DN; nếu Redis lỗi, transaction DB vẫn commit, cache tự hết hạn ≤ 10' (fail-safe, không block nghiệp vụ). - Optimistic lock — dùng cột
version; không dùng pessimistic lock để tránh giữ khóa lâu. - Mirror
companies.status— chỉ để chặn login rẻ; nguồn sự thật làcompany_subscriptions.service_status. Nếu lệch (do lỗi), ưu tiêncompany_subscriptions; cần job đối soát định kỳ (v1.5). - KHÔNG CTE/KDE — TXNG-F46 là module vận hành thương mại nội bộ, KHÔNG phát sinh Critical Tracking Event / Key Data Element và KHÔNG tham gia chuỗi truy xuất TCVN 12850:2019.
- Giá VND chưa gồm VAT — mọi phí trong
plan_snapshotlà NUMERIC, VND, chưa bao gồm VAT (BR-F45-10). - Immutable audit —
subscription_historieschặn UPDATE/DELETE bằng trigger; retention ≥ 2 năm (khuyến nghị ≥ 5 năm theo hồ sơ hợp đồng). - SLA — GET list ≤ 500ms; write (activate/renew/terminate transaction) ≤ 1 giây; login gate thêm ≤ 50ms (đọc mirror, không JOIN).
Giả định:
- TXNG-F45 (
service_plans) đã seed 4 gói canonical (PKG_BASIC/STANDARD/LINKED/ENTERPRISE) ởstatus = ACTIVEtrước khi kích hoạt DN. - TXNG-F01 (
companies) đã có DN vớicompany_tax_code, thông tin liên hệ đầy đủ. AuthService.login()+JwtStrategy.validate()đã có cơ chế cacheuser_perms:{userId}10' để login gate hook vào.MinioStorageService+FileFieldsInterceptorsẵn sàng cho upload hồ sơ.- Sale thao tác qua tài khoản
PLATFORM_ADMIN;SALE_STAFFkhông phải actor trực tiếp.
18. Phụ lục (Appendix) ​
18.1. Bảng seed 4 gói tham chiếu (từ docs/sale/Sale_Pricing.md, đối chiếu TXNG-F45) ​
| field | PKG_BASIC | PKG_STANDARD | PKG_LINKED | PKG_ENTERPRISE |
|---|---|---|---|---|
| plan_name | Gói Cơ bản | Gói Chuẩn hóa | Gói Liên kết | Gói Quản trị |
| setup_fee | 2.000.000 | 3.000.000 | 6.000.000 | 9.000.000 |
| standardization_fee | 3.000.000 | 5.000.000 | 8.000.000 | 15.000.000 |
| maintenance_fee_year | 4.500.000 | 9.500.000 | 25.500.000 | 85.000.000 |
| qr_quota_year | 5.000 | 20.000 | 100.000 | 500.000 |
| max_products | 10 | 50 | 150 | 500 |
| max_users | 2 | 5 | 20 | 50 |
| storage_gb | 1 | 3 | 10 | 50 |
| extra_qr_fee | 150 | 120 | 100 | 80 |
| extra_product_fee | 490.000 | 490.000 | 490.000 | 490.000 |
| training_type / onsite | ONLINE / 0 | ONLINE / 0 | ONSITE / 1 | ONSITE / 3 |
| go_live_support_days | 7 | 15 | 30 | 60 |
| sla_support | Trong giờ hành chính | 24/7 | Ưu tiên 24/7 | Ưu tiên 24/7 |
| Tổng năm đầu | 9.500.000 | 17.500.000 | 39.500.000 | 109.000.000 |
| Từ năm 2 (duy trì) | 4.500.000 | 9.500.000 | 25.500.000 | 85.000.000 |
Tổng năm đầu =
setup_fee + standardization_fee + maintenance_fee_year. Từ năm 2 =maintenance_fee_year. Chiết khấu duy trì: 2 năm −10%, 3 năm −20% (trước VAT). Giá VND, chưa gồm VAT.
18.2. Ví dụ plan_snapshot JSONB (chốt lúc kích hoạt) ​
{
"plan_code": "PKG_STANDARD",
"setup_fee": 3000000,
"standardization_fee": 5000000,
"maintenance_fee_year": 9500000,
"qr_quota_year": 20000,
"max_products": 50,
"max_users": 5,
"storage_gb": 3,
"extra_qr_fee": 120,
"extra_product_fee": 490000,
"discount_2y_percent": 10,
"discount_3y_percent": 20,
"features": {
"supply_chain_link": false,
"recall_management": false,
"operation_log": false,
"api_integration": "none",
"blockchain": "optional",
"public_trace_page": "standard",
"qr_scan_stats": true,
"dashboard": true
},
"snapshot_at": "2026-07-01T03:00:00Z"
}
18.3. Ví dụ changes_json cho action UPDATE ​
{
"term_months": { "before": 12, "after": 24 },
"discount_percent": { "before": 0, "after": 10 },
"bonus_qr_quota": { "before": 0, "after": 5000 }
}
18.4. Map trạng thái ↔ mirror companies.status ​
| service_status | companies.status | Login |
|---|---|---|
| DRAFT | 0 | Cho phép (hạn chế) |
| TRIAL | 1 | Cho phép (đầy đủ) |
| ACTIVE | 2 | Cho phép (đầy đủ) |
| EXPIRED | 3 | Chặn (E-F46-005) |
| TERMINATED | 4 | Chặn (E-F46-006) |
18.5. Thuật ngữ (Glossary) ​
| Thuật ngữ | Giải thích |
|---|---|
| Login gate | Cơ chế chặn đăng nhập tại AuthService.login()/JwtStrategy.validate() dựa trên trạng thái dịch vụ DN |
| Lazy-expire | Đánh giá & chuyển EXPIRED tại thời điểm login (không cron), khi phát hiện stored TRIAL/ACTIVE đã quá hạn |
| Mirror | Bản sao companies.status (INT) phản chiếu service_status để chặn login nhanh, không JOIN |
| plan_snapshot | Bản chốt phí/quota/features của gói tại thời điểm kích hoạt; DN đang dùng giữ nguyên kể cả khi gói đổi/DEPRECATED |
| effective quota | Hạn mức thực dùng = quota gói + bonus (QR/năm, sản phẩm) |
Hết tài liệu SRS TXNG-F46 v1.0.