Decision Log (DocHub V3)
Quy ước:
- Mỗi quyết định là 1 entry, có ID duy nhất (DEC-xxx).
- Không copy spec vào đây; đây là "luật nền + hệ quả".
- Nếu thay đổi quyết định: thêm entry mới "Supersedes: DEC-xxx" (không sửa lịch sử).
- Spec backend/portal/miniapp link tới đây, không copy lặp.
DEC-001 — Auth vs Tenant Context Rules
- Date: 2026-02-25
- Status: Accepted
- Owner: Architecture Team
- Scope Impacted: Backend | Portal | MiniApp | All
- Related: backend_api.md, portal.md, miniapp.md
Context
- Legacy spec (
DOC_HUB/BASE_PLATFORM) ghi "tenant_id mandatory mọi request" — mâu thuẫn với flow thực tế. - User cần login trước (định danh), rồi mới select/switch tenant.
- Nếu không chốt, team sẽ sửa vòng tròn spec↔code.
Decision
- Login/Auth KHÔNG yêu cầu tenant sớm. JWT chứa
user_id,global_roles— KHÔNG bắt buộctenant_id. - Tenant context chỉ set sau
select/switch tenant(endpointPOST /v2/iam/auth/switch-tenant). - Tenant-scoped APIs bắt buộc tenant context — enforce bởi
TenantGuard+ repository data filter. - System admin APIs có cơ chế bypass — decorator
@SkipTenantCheck()cho endpoints admin. - Auth controller toàn bộ
@SkipTenantCheck()— login/register/refresh/me không cần tenant.
Invariants (bất biến)
POST /v2/iam/auth/login→ trả JWT vớisub(userId), KHÔNG cótenantIdPOST /v2/iam/auth/switch-tenant→ trả JWT MỚI cótenantId+tenantRoleGET /v2/iam/auth/me→ trả thông tin user hiện tại (có thể chưa có tenant)- Mọi API nghiệp vụ (
/v2/catalog/*,/v2/ecommerce/*, ...) → reject nếu thiếu tenant context
Alternatives Considered
- Option A: Require tenant ngay khi login → Loại vì user có thể thuộc nhiều tenant, cần chọn
- Option B: Tenant trong header mọi request → Loại vì phức tạp, dễ quên, login flow không cần
Consequences
- ✅ Flow rõ ràng: Auth → Select Tenant → Use APIs
- ✅ Multi-tenant user chỉ cần 1 account
- ⚠️ Frontend phải handle "chưa có tenant" state (workspace selection screen)
- ⚠️ Token refresh cần preserve tenant context
Implementation Notes
AuthControllerđã implement:@SkipTenantCheck()trên class level (code truth ✅)TenantGuardenforces tenant context trên tất cả routes không có@SkipTenantCheck()- Portal route
/workspace→ chính là màn hình select/switch tenant SwitchTenantDto→{ tenantId: string }
DEC-002 — Zalo Login Dual Flow
- Date: 2026-02-25
- Status: Accepted
- Owner: Architecture Team
- Scope Impacted: Backend | Portal | MiniApp
- Related: DEC-001, miniapp.md
Context
- Zalo login có 2 use case hoàn toàn khác: portal quản trị vs mini app end-user.
- Cần 2 DTO riêng biệt, 2 flow riêng biệt, nhưng cùng endpoint prefix.
Decision
- Portal Zalo Login (
POST /v2/iam/auth/zalo-login): giống login bình thường — user Zalo → JWT định danh user, KHÔNG tenant sớm. DTO:ZaloPortalLoginDto. - MiniApp Zalo Login (
POST /v2/iam/auth/zalo-tenant-login): bắt buộc phải xác định đượctenantIdđể bind. - Cả 2 endpoint đều
@Public()(không cần JWT trước).
Invariants (MiniApp Flow Hard Rules)
- Nguồn tenant binding: Bắt buộc truyền
tenantId(nếu app dedicated) HOẶCappInstanceId(từ context Zalo gửi lên) trong payload. - Validate/Reject Rule: Nếu thiếu thông tin bind hoặc lookup
appInstancekhông ra tenant hợp lệ → reject400/404. Giao dịch KHÔNG ĐƯỢC phép tiếp tục thiếu tenant. - Result: JWT trả về cho MiniApp flow LUÔN CÓ claim
tenantId.
Consequences
- ✅ Portal flow tách biệt — giữ nguyên DEC-001 (no tenant early)
- ✅ MiniApp flow bind tenant ngay — UX tốt cho end-user
- ⚠️ Backend cần validate
tenantId/appInstanceIdở MiniApp flow - ⚠️ MiniApp cần mechanism để truyền tenant context khi init
Implementation Notes
AuthControllerđã có cả 2 endpoints:zaloLoginPortal()+zaloLoginTenant()(code truth ✅)ZaloPortalLoginDto,ZaloTenantLoginDto— 2 DTO riêng trongauth.dto.ts
DEC-003 — RBAC Permission Model
- Date: 2026-02-25
- Status: Accepted
- Owner: Architecture Team
- Scope Impacted: Backend | Portal
- Related: permission_manifest.md
Context
- Cần mô hình phân quyền thống nhất cho toàn bộ 22 modules.
- SystemRole (5 levels) + PermissionString (40+ fine-grained permissions).
Decision
- SystemRole hierarchy:
SYSTEM_ADMIN>TENANT_OWNER>TENANT_ADMIN>TENANT_OPERATOR>TENANT_MEMBER - Permission format:
{DOMAIN}_{ACTION}(e.g.,CAT_VIEW,CAT_MANAGE,ORDER_VIEW,ORDER_MANAGE) - Guard stack:
JwtAuthGuard(Global) →TenantGuard(Global) →PermissionsGuard(Explicit via@UseGuards). - Guard Evaluation Order & Decorator Semantics (Code Truth):
@Public(): CHỈ bypass sự xác thực củaJwtAuthGuard(cho phép anonymous access). Nó KHÔNG tự động bypassTenantGuardhayPermissionsGuard.@SkipTenantCheck(): CHỈ bypass sự kiểm tratenantIdcủaTenantGuard. Các endpoint này vẫn yêu cầu có JWT hợp lệ (trừ khi dùng kèm@Public()).PermissionsGuard: Chỉ được kích hoạt khi khai báo@UseGuards(PermissionsGuard)trên Controller/Handler.- SECURITY RULE (P0): Mọi controller/router yêu cầu tenant-scope (không gắn
@SkipTenantCheck()) BẮT BUỘC phải gắn@UseGuards(PermissionsGuard)và@Permissions(). Nếu thiếu, endpoint đó sẽ bị mở toang cho bất kỳ ai có tenant token hợp lệ.
Permission Domains (from code truth)
IAM, NOTIFY, COMMS, CAT, ORDER, VOUCHER, INV, PROC, PROD, SHIP, CMS, CRM, INBOX, AI, ZALO, PLUGIN, BILLING, ADMIN, MDM
Consequences
- ✅ Consistent 2-level permission:
{DOMAIN}_VIEW(read) +{DOMAIN}_MANAGE(write) - ✅ Some domains have extra:
CMS_PUBLISH,INBOX_REPLY - ⚠️ Role → Permission mapping cần cấu hình per tenant (linh hoạt)
DEC-004 — Datetime/Timezone Standard
- Date: 2026-02-25
- Status: Accepted
- Owner: Architecture Team
- Scope Impacted: Backend | Portal | MiniApp
Context
- Hệ thống có nhiều tính năng theo dõi thời gian (Analytics, Orders) trên nhiều thiết bị ở nhiều nơi. Cần chuẩn hóa format để tránh lỗi "1 đơn hàng nhảy ngày".
Decision
- Database: Mọi trường thời gian lưu 100% chuẩn UTC (
Z). - API Data Transfer: Request/Response đều gửi/nhận dưới dạng ISO 8601 UTC string (Ví dụ:
2026-02-25T10:00:00.000Z). - UI / Frontend: Khi hiển thị cho user mới handle parser và format ra Local Timezone của người dùng cuối (e.g.
Asia/Ho_Chi_Minhbằng thư viện Dayjs hoặc Intl formatter). Không gửi múi giờ cục bộ thẳng xuống BE trừ khi tính năng báo cáo (report) yêu cầu explicitly timezone query parameters.
DEC-005 — Phase 1 Tenant Onboarding Policy
- Date: 2026-02-26
- Status: Accepted
- Owner: Architecture Team
- Scope Impacted: Backend | Portal
- Related: 01_db_schema.md, 02_api_contracts.md, 03_state_machine.md, 04_portal_screens.md
Context
- Hệ thống hỗ trợ khởi tạo tenant (Product-Led Growth). Cần xác định luồng duyệt tenant, unique identifier, và cách chạy provision.
Decision
- Slug uniqueness scope: Global unique cho Phase 1. Tất cả các tenant có chung một không gian tên (namespace) cho slug.
- Provisioning sync vs async: Quá trình Provisioning sẽ chạy Async nhưng nhanh nhất có thể; UI Portal sẽ hiển thị polling progress để user quan sát quá trình diễn ra.
- Creation permission: Mọi user login có thể tạo tenant (kèm theo rate-limit và policy flag khóa lại trên Prod khi cần).
DEC-006 — Default Master Data Seeding Strategy
- Date: 2026-02-26
- Status: Accepted
- Owner: Architecture Team
- Scope Impacted: Backend | Portal
- Related: 01_db_schema.md
Context
- Màn hình Portal Wizard cho phép user chọn CatalogTemplate. Nhưng làm sao đảm bảo luôn có template để chọn mà không bắt buộc SystemAdmin phải vào thao tác tay trước?
Decision
- Dùng phương pháp Auto-ensure minimal seed (Cách A) cho Phase 1.
- Khi ứng dụng backend khởi động (bootstrap), hệ thống sẽ tự động kiểm tra và seed dữ liệu tổi thiểu nếu DB đang trống (VD: 1-3 catalog templates cơ bản + template
STANDARDcho BusinessType). - Màn hình "Master Data Initialization" của Admin sẽ phục vụ cho việc "Full seed" (nhiều dữ liệu taxonomy/templates đa dạng hơn) hoặc force re-sync dữ liệu.
- Điều này đảm bảo luồng Onboarding tự động (Product-Led Growth) không bao giờ bị nghẽn (blocked) vì thiếu data cấu hình ban đầu.
DEC-007 — Order State Machine Enforcement
- Date: 2026-02-26
- Status: Accepted
- Owner: Architecture Team
- Scope Impacted: Backend | Ecommerce | Procurement
- Related: backend_api.md
Context
- Quy trình xử lý đơn hàng (Ecommerce & Procurement) cần được khóa chặt để tránh các trạng thái vô lý (VD: đơn đã giao vẫn có thể hủy, hoặc đơn chưa thanh toán đã hoàn thành).
Decision
- Luồng trạng thái chuẩn (Linear Progression):
PENDING->CONFIRMED->PROCESSING->SHIPPING->COMPLETED. - Khả năng nhảy trạng thái (Shortcuts): Cho phép
PENDING->PROCESSING(bỏ quaCONFIRMED) nếu tenant setting cho phép auto-confirm. - Luật bảo vệ (Hard Rules):
- Hủy đơn (CANCELLED): Chỉ được phép khi đơn hàng chưa chuyển sang trạng thái
SHIPPING. Một khi đã "Đang giao", không được phép hủy trực tiếp (phải qua luồng hoàn tiền/trả hàng). - Hoàn thành (COMPLETED): Chỉ được phép khi
paymentStatus === 'PAID'. Tuyệt đối không cho phép đóng đơn nếu chưa ghi nhận thanh toán thành công. - Hoàn tiền (REFUNDED): Chỉ được phép thực hiện từ trạng thái
COMPLETED(sau khi đã thanh toán và nhận hàng).
- Hủy đơn (CANCELLED): Chỉ được phép khi đơn hàng chưa chuyển sang trạng thái
Consequences
- ✅ Quy trình nghiệp vụ nhất quán, tránh thất thoát tài chính.
- ✅ Audit log ghi nhận chính xác Timeline của đơn hàng.
- ⚠️ Một số legacy orders có thể vi phạm rules này nếu schema cũ chưa migrate.
DEC-008 — Tenant Settings Extraction
- Date: 2026-02-26
- Status: Accepted
- Owner: Architecture Team
- Scope Impacted: Backend | IAM
- Related: DEC-005, backend_api.md
Context
- Ban đầu,
settingsđược nhúng trực tiếp (embedded) trong documentTenant. - Khi settings phình to (modules, integrations, master data readiness), việc query
Tenanttrả về quá nhiều dữ liệu không cần thiết, làm chậm perf và khó theo dõi lịch sử thay đổi (Audit).
Decision
- Tách thực thể: Chuyển toàn bộ
settingsra collection riêng:tenant_settings. - Cơ chế Fallback (Migration Readiness):
- Read:
getSettings()ưu tiên tìm trong collection mới. Nếu không có (tenant cũ chưa migrate), fallback đọc từ fieldsettingstrong documentTenant. - Write:
updateSettings()LUÔN ghi vào collection mới (tenant_settings).
- Read:
- Auditing: Mọi thay đổi settings qua collection mới sẽ được bắt bởi
AuditLogInterceptordễ dàng hơn (target entity rõ ràng).
Consequences
- ✅ Tối ưu hiệu năng: Document
Tenantmỏng nhẹ, load cực nhanh. - ✅ Khả năng mở rộng: Settings có thể phình to mà không ảnh hưởng tới core IAM.
- ⚠️ Code migration: Cần refactor lại toàn bộ logic truy cập settings trong hệ thống để dùng
TenantService.getSettings(). - ⚠️ Cần script cleanup field
settingscũ trong collectiontenantssau khi verify xong 100% migration.
Template cho Decision mới
md
## DEC-xxx — <Tiêu đề>
- **Date:** YYYY-MM-DD
- **Status:** Proposed | Accepted | Deprecated
- **Owner:** <name/team>
- **Scope Impacted:** Backend | Portal | MiniApp | All
- **Supersedes:** (optional) DEC-xxx
- **Related:** (links)
### Context
### Decision
### Alternatives Considered
### Consequences
### Migration / Rollout Plan
### Implementation Notes
### Open Questions