Skip to content

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

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

  1. Login/Auth KHÔNG yêu cầu tenant sớm. JWT chứa user_id, global_roles — KHÔNG bắt buộc tenant_id.
  2. Tenant context chỉ set sau select/switch tenant (endpoint POST /v2/iam/auth/switch-tenant).
  3. Tenant-scoped APIs bắt buộc tenant context — enforce bởi TenantGuard + repository data filter.
  4. System admin APIs có cơ chế bypass — decorator @SkipTenantCheck() cho endpoints admin.
  5. 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ới sub (userId), KHÔNG có tenantId
  • POST /v2/iam/auth/switch-tenant → trả JWT MỚI có tenantId + tenantRole
  • GET /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 ✅)
  • TenantGuard enforces 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

  1. 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.
  2. MiniApp Zalo Login (POST /v2/iam/auth/zalo-tenant-login): bắt buộc phải xác định được tenantId để bind.
  3. 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ẶC appInstanceId (từ context Zalo gửi lên) trong payload.
  • Validate/Reject Rule: Nếu thiếu thông tin bind hoặc lookup appInstance không ra tenant hợp lệ → reject 400/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 trong auth.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

  1. SystemRole hierarchy: SYSTEM_ADMIN > TENANT_OWNER > TENANT_ADMIN > TENANT_OPERATOR > TENANT_MEMBER
  2. Permission format: {DOMAIN}_{ACTION} (e.g., CAT_VIEW, CAT_MANAGE, ORDER_VIEW, ORDER_MANAGE)
  3. Guard stack: JwtAuthGuard (Global) → TenantGuard (Global) → PermissionsGuard (Explicit via @UseGuards).
  4. Guard Evaluation Order & Decorator Semantics (Code Truth):
    • @Public(): CHỈ bypass sự xác thực của JwtAuthGuard (cho phép anonymous access). Nó KHÔNG tự động bypass TenantGuard hay PermissionsGuard.
    • @SkipTenantCheck(): CHỈ bypass sự kiểm tra tenantId của TenantGuard. 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)@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

  1. Database: Mọi trường thời gian lưu 100% chuẩn UTC (Z).
  2. 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).
  3. 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_Minh bằ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

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

  1. 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.
  2. 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.
  3. 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

  1. Dùng phương pháp Auto-ensure minimal seed (Cách A) cho Phase 1.
  2. 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 STANDARD cho BusinessType).
  3. 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.
  4. Đ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

  1. Luồng trạng thái chuẩn (Linear Progression): PENDING -> CONFIRMED -> PROCESSING -> SHIPPING -> COMPLETED.
  2. Khả năng nhảy trạng thái (Shortcuts): Cho phép PENDING -> PROCESSING (bỏ qua CONFIRMED) nếu tenant setting cho phép auto-confirm.
  3. 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).

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 document Tenant.
  • Khi settings phình to (modules, integrations, master data readiness), việc query Tenant trả 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

  1. Tách thực thể: Chuyển toàn bộ settings ra collection riêng: tenant_settings.
  2. 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ừ field settings trong document Tenant.
    • Write: updateSettings() LUÔN ghi vào collection mới (tenant_settings).
  3. Auditing: Mọi thay đổi settings qua collection mới sẽ được bắt bởi AuditLogInterceptor dễ dàng hơn (target entity rõ ràng).

Consequences

  • ✅ Tối ưu hiệu năng: Document Tenant mỏ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 settings cũ trong collection tenants sau 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

FitZalo Platform Documentation