Files
sys-analysis-design/sa-output/e-commerce/02-architecture/DAT_e-commerce_v1.0.md
Canhchimlac 343ad8bbbc save
2026-09-15 16:02:30 +07:00

686 lines
75 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# DAT — Data Architecture — e-commerce
| | |
|---|---|
| **Version** | 1.1 |
| **Date** | 2026-09-15 |
| **Author** | SA (skill sa-2-architecture, chế độ `go`, hoạt động `dat`) |
| **Status** | 🟡 Draft *(duyệt từng phần — xem `Approved by`; AG2 cần cả ba vai trò Tech Lead/Security/Ops-SRE ký, mới có một chữ ký thay có điều kiện — **chưa đủ điều kiện chuyển `🔵 Approved`**)* |
| **Approved by** | Tech Lead: **Điều phối dự án** (uỷ quyền, chế độ chạy thử — ký thay theo ngoại lệ `DEC-01`, **không phải chữ ký Tech Lead thật**) — duyệt **từng phần** bản v1.0 · 2026-09-15: chấp nhận bảng ownership 15 `CMP` (§1), mô hình `Order`/`OrderSeller`/`OrderItem` + `payment_init_status` (`ADR-013`, §2), snapshot giá/`sellerName`, idempotency store (§4.2), PITR RPO ≤15 phút (§9). Chấp nhận **TẠM** (🔴): `ASM-35` (1 `Order` = 1 `Payment`) — `OQ-043` **giữ nguyên mở**; chọn **Transactional Outbox** cho `OQ-044` — yêu cầu SA viết `ADR-014` (`Proposed`) ở lượt hoạt động `adr` kế tiếp, `OQ-044` **giữ mở** tới khi có ADR; chọn phương án (a) — mỗi module tự ghi `audit_log` riêng cho `OQ-047`, **không thêm `CMP-16`**; ngưỡng job quét `Order` kẹt **5 phút** cho `OQ-042`; cơ chế FE **polling** cho `OQ-041` — cả bốn `OQ-041/042/044/047` **giữ nguyên mở**, chờ Tech Lead thật xác nhận. `OQ-045`/`OQ-046` **giữ nguyên mở**, không có hướng TẠM. **§5 (phân loại PII/retention/whitelist chia sẻ seller) CHƯA CÓ HIỆU LỰC** — chờ đại diện Pháp chế/Bảo mật phủ quyết (`OQ-007`), không nằm trong phạm vi duyệt lần này. Ghi thêm `DEC-25`. Security/Legal: — *(chưa ký; §5 chặn hoàn toàn tới khi có người, `OQ-007`)* · Ops/SRE: — *(chưa ký)*. `OQ-004` (tính hợp lệ ký thay) vẫn mở. `Confidence` **giữ nguyên** 🔴 — duyệt từng phần không phải bằng chứng nguồn mới. Status **giữ** 🟡 Draft — AG2 chưa đủ ba chữ ký thật/hợp lệ. |
| **Source** | `02-architecture/SAD_e-commerce_v1.0.md` v1.3 §4.1 (bảng `CMP-nn`, cột "Dữ liệu sở hữu"), §6.1/§6.3, §7 · `02-architecture/ICD_e-commerce_v1.0.md` v1.0 §3.1.1, §3.7, §5 (denormalize `sellerName`, `unitPriceSnapshotVnd`) · `02-architecture/FAIL_e-commerce_v1.0.md` v1.1 §1.2, §4, §7, §9 (khoảng trống outbox, `OQ-040/042`) · `02-architecture/QAS_e-commerce_v1.0.md` `QAS-003/004/006/008/011/014` · `02-architecture/ASR_e-commerce_v1.0.md` `ASR-002/003/006/008` · `02-architecture/adr/ADR-002_*.md`, `ADR-003_*.md`, `ADR-004_*.md`, `ADR-005_*.md`, `ADR-006_*.md`, `ADR-007_*.md`, `ADR-008_*.md`, `ADR-009_*.md`, `ADR-011_*.md`, `ADR-013_*.md`, `ADR-014_*.md` · `00-index/OQ_e-commerce.md` v1.20 · `00-index/DEC_e-commerce.md` v1.21 · `01-context/TCO_e-commerce_v1.0.md` §3.2 · `ba-output/e-commerce/02-analysis/BR_CartCheckout_v1.0.md` §4 (ERD khái niệm), §3 (vòng đời) · `ba-output/e-commerce/02-analysis/RBAC_CartCheckout_v1.0.md` §4 (dữ liệu nhạy cảm) · `ba-output/e-commerce/03-specification/SRS_US002-003_v1.0.md` · `e-commerce/docs/sections/05-thiet-ke-du-lieu.md` (tham khảo P1 — database-per-service, Kafka/MSK; **không** kế thừa kiểu kiến trúc đó, chỉ tham khảo tên bảng/cột và số retention đề xuất) |
| **Scope** | Toàn sàn e-commerce theo P2 (SAD v1.3) — kiến trúc dữ liệu + ownership cho toàn bộ 15 `CMP-01…15`. Chi tiết nhất: **Cart & Order** (`CMP-04`), **Payment** (`CMP-11`/`CMP-14`), và mọi thực thể chứa PII (`CMP-02` Identity, `CMP-04` địa chỉ giao hàng, `CMP-05` KYC seller). Các `CMP` còn lại (Catalog, Seller quản trị, Commission, Promotion, Review, Notification, Shipping) ở mức ownership + entity list, không đào sâu schema vật lý (thuộc dev). Đây là hoạt động 5/9 của `sa-2-architecture`. |
| **Confidence** | 🔴 Giả định chưa xác minh — theo yêu cầu điều phối, tiếp nối `Confidence` 🔴 của `SAD`/`ICD`/`FAIL`/`ASR`/`QAS`. AG1 chưa ký thật; AG2 chưa tới hạn; `ADR-005`/`ADR-006`/`ADR-009` (radar ≥8) chưa `Accepted` (chờ `POC-01`/`POC-02`/diễn tập DR); `ADR-008` (PII) chặn hoàn toàn vì chưa có đại diện Pháp chế/Bảo mật (`OQ-007`). Mọi con số retention/index/pool trong tài liệu này là **đề xuất SA**, chưa đo thật. |
## Change Log
| Version | Date | Người sửa | Thay đổi | ADR/DEC |
|---|---|---|---|---|
| 1.0 | 2026-09-15 | SA (qua skill sa-2-architecture, chế độ `go`, hoạt động `dat`) | Bản đầu — hoạt động 5/9 của GĐ2. Bảng ownership D7 cho toàn bộ thực thể của 15 `CMP` (schema-per-module theo `ADR-006`, RDS Payment riêng theo `ADR-002`); mô hình `Order`/`OrderSeller`/`OrderItem` chi tiết theo `ADR-003`, bổ sung `order.payment_init_status` (`PENDING_PAYMENT_INIT`/`PAYMENT_INIT_READY`/`PAYMENT_INIT_FAILED`) theo `ADR-013`, `cart_item.seller_name_snapshot`/`unit_price_snapshot` theo `ICD §3.1.1/§3.7` (`OQ-033`); phân loại dữ liệu PII/Payment/nghiệp vụ kèm đề xuất retention (chờ Legal, `OQ-007`) và whitelist chia sẻ seller theo `ADR-008`; chiến lược đọc/ghi (index cho `QAS-003/004/006`, read replica chỉ dùng cho Catalog, đánh giá cache nội dung Cart cho `OQ-040`); ranh giới transaction + đề xuất outbox pattern cho khoảng trống `FAIL §7` (ADR ứng viên, chưa viết); idempotency store cho `POST /v1/checkout`; backup/PITR đối chiếu `QAS-008`/`ADR-009`; migration/versioning schema (đề xuất Flyway); dữ liệu đối soát (`ReconciliationLog`, `ADR-004`) và job dọn `Order` kẹt (đề xuất ngưỡng cho `OQ-042`). Đối chiếu `docs/sections/05-thiet-ke-du-lieu.md` (P1) — 5 khác biệt chính được ghi nhận. Thêm `OQ-043…047`, cập nhật `OQ-007/040/042`, `ASM-35…38`. Ghi `DEC-24` (tiếp nối `DEC-01…23`, ngoại lệ gate tiếp tục GĐ2 hoạt động `dat`). Không sửa `SAD`/`ICD`/`FAIL`/`ADR`. `Confidence` 🔴 toàn bộ. | `DEC-24` |
| 1.0 | 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, ngoại lệ `DEC-01`) — tác vụ **sign/approve** | Duyệt **từng phần**: chấp nhận bảng ownership 15 `CMP`, mô hình `Order`/`OrderSeller`/`OrderItem` + `payment_init_status` (`ADR-013`), snapshot giá/`sellerName`, idempotency store, PITR RPO ≤15 phút. Chấp nhận **TẠM** (🔴): `ASM-35` (`OQ-043` giữ mở); chọn Transactional Outbox cho `OQ-044` — yêu cầu `ADR-014` (`Proposed`) ở lượt `adr` kế tiếp; chọn phương án (a) mỗi module tự ghi `audit_log` cho `OQ-047` — không thêm `CMP-16`; ngưỡng job quét 5 phút cho `OQ-042`; polling cho `OQ-041` — cả bốn giữ nguyên mở. `OQ-045`/`OQ-046` giữ mở. **§5 (PII/retention/whitelist) CHƯA CÓ HIỆU LỰC** — chờ Legal/Security phủ quyết (`OQ-007`). Không đổi nội dung chuyên môn. Security/Ops chưa ký; `OQ-004` mở; `Confidence` giữ 🔴; Status giữ 🟡 Draft; AG2 **chưa** ký. Ghi thêm `DEC-25`. | `DEC-25` |
| 1.1 | 2026-09-15 | SA (qua skill sa-2-architecture, chế độ `go`, hoạt động `adr`) | **Chỉ link ADR, không đổi nội dung.** `ADR-014` (Transactional Outbox, `Proposed`) vừa được viết theo hướng TẠM đã chọn ở `OQ-044`/`DEC-25` — thay các dòng "chưa viết"/"Ứng viên — chưa viết" ở §4.1 bằng đường dẫn `adr/ADR-014_transactional-outbox-publish-su-kien.md`. `OQ-044` giữ nguyên mở (chờ Tech Lead thật xác nhận thiết kế relay, xem `ADR-014 §3`). Không sửa nội dung chuyên môn nào khác của `DAT`. Ghi `DEC-31` (tiếp nối `DEC-01…30`). | `ADR-014`, `DEC-31` |
> Sơ đồ thắng về **quan hệ và luồng** (ai sở hữu gì, ai đọc/ghi ai).
> Bảng/văn bản thắng về **ràng buộc và con số** (retention, index, TTL, RPO/RTO).
> Mâu thuẫn ngoài hai loại trên là lỗi tài liệu, phải sửa chứ không chọn bên (`D12`).
---
## 0. Ghi chú Preflight & ngoại lệ gate — bắt buộc đọc trước
**AG1 vẫn chưa ký thật; AG2 (gate của chính GĐ2) chưa tới hạn.** Theo `SKILL.md`, hoạt động 1
(`QAS`) → 2 (`ASR`) → 3 (`SAD`) đã hoàn tất và được duyệt từng phần (`DEC-12/14/16`); hoạt động 4
(`ICD`) và 8 (`FAIL`) đã chạy (`DEC-18…23`), `ADR-001…013` đã viết (tất cả `Proposed`). Điều kiện
đầu vào cho hoạt động 5 (`DAT`) đã đủ. Người dùng (vai điều phối dự án chạy thử) đã được cảnh báo
lại và **khẳng định muốn tiếp tục**.
> **DEC-24 (SA)** — Tiếp tục `sa-2-architecture` (hoạt động 5 — `DAT`) cho e-commerce trong khi
> AG1 chưa ký thật và AG2 chưa tới hạn — tiếp nối `DEC-01…23`.
> **Quyết định:** Thiết kế kiến trúc dữ liệu + ownership cho 15 `CMP` dựa trên `SAD v1.3`/`ICD
> v1.0`/`FAIL v1.1`/`ADR-001…013` đã có, giữ `Confidence 🔴` cho tới khi: (a) AG1/AG2 ký thật,
> (b) `POC-02` (tải RDS gộp) chạy xong để xác nhận connection pool/index đề xuất, (c) đại diện
> Pháp chế/Bảo mật xác nhận phân loại PII và retention (`OQ-007`), (d) Tech Lead xác nhận cơ chế
> outbox/idempotency store đề xuất (`OQ-044/045`).
> **Người quyết:** Điều phối dự án (đại diện PO, dự án chạy thử).
> **Radar (ước lượng cho việc tiếp tục dưới ngoại lệ):** ~4 — chi phí đảo ngược thấp (đây là ghi
> tài liệu thiết kế, chưa có dòng code phụ thuộc) · bán kính ảnh hưởng: hoạt động `sec`/`inf`
> kế tiếp và toàn bộ dev BE dùng `DAT` làm nguồn sự thật ownership, nhưng bản thân *việc ghi tài
> liệu dưới ngoại lệ* thì nhỏ · có chạm `QAS-003/004/006/008/011/014` Must trực tiếp (đang hiện
> thực hoá thành index/consistency/retention) nhưng chưa phải cam kết thi công · không ràng buộc
> dài hạn tự thân (văn bản `DAT` sửa được qua version mới) · không tranh cãi mới, tiếp nối tiền lệ
> `DEC-11…23` → **ghi `DEC-nn`, không cần `ADR` cho việc chạy dưới ngoại lệ**.
> **Hệ quả nếu không chấp nhận ngoại lệ:** hoạt động `sec`/`inf` (hoạt động 6–7) không có ownership
> dữ liệu/chiến lược consistency để thiết kế threat model dữ liệu và backup/DR theo schema — chậm
> tiến độ chạy thử tương ứng thời gian xử lý các `OQ` đang mở, đặc biệt `OQ-007` (chặn `ADR-008`).
🔴 **Nhắc quan trọng:** đây là hoạt động 5/9. `SEC`, `INF` (hoạt động 6–7) **chưa được tạo**.
Tài liệu này là input trực tiếp cho cả hai — đặc biệt phân loại PII (§5) cho `sec`, và
backup/RPO-RTO (§9) cho `inf`.
---
## 1. Nguyên tắc: mỗi thực thể có đúng một chủ
*Quy tắc `D7`. Cột "Schema" theo `ADR-006` (1 cụm RDS chính, schema-per-module) trừ `Payment`/
`PaymentTransaction`/`PaymentReconciliationLog` nằm ở **RDS Payment hoàn toàn riêng biệt** theo
`ADR-002` — **không module nào khác được join/truy vấn SQL trực tiếp vào RDS Payment**; mọi trao
đổi chỉ qua `IF-006` (sync, khởi tạo), sự kiện `PaymentConfirmed`/`PaymentInitReady`/
`PaymentInitFailed` (async), hoặc job đối soát nội bộ Payment.*
### 1.1 Nhóm Giao dịch — `CMP-02` Identity & Access *(schema `identity`)*
| Thực thể | **SoT** | Bản sao ở đâu | Cập nhật bằng cơ chế gì | Trễ tối đa | Lệch quá thì sao |
|---|---|---|---|---|---|
| `user_account` | `CMP-02` | Cache JWT claims đã xác thực trong Redis (`CMP-15`) | Ghi tại lúc đăng nhập/refresh token | TTL access token (đề xuất 15-60 phút, `ICD §1` mục 9) | Token hết hạn tự nhiên — không phải "lệch", là thiết kế |
| `customer_profile`, `customer_address` | `CMP-02` | Không có bản sao RDS; **`cart_item.seller_name_snapshot`/checkout copy tên người nhận vào `order_seller`** là snapshot một chiều, không phải bản sao đồng bộ | Snapshot tại thời điểm checkout (không cập nhật ngược khi khách đổi địa chỉ sau đó) | N/A — snapshot cố ý, không phải cache | Không áp dụng — đơn đã tạo giữ nguyên địa chỉ tại thời điểm đặt hàng (đúng thông lệ thương mại điện tử) |
| `oauth_identity`, `mfa_device` | `CMP-02` | Không có | — | — | — |
| Session (Guest `X-Guest-Session-Id`, ownership cache) | `CMP-15` (Redis) — **đây là SoT của session, không phải read model** vì session không có bản ghi RDS song song | Không | — | TTL 30 ngày không hoạt động (`ICD §1` mục 10) | Hết TTL → Guest mất phiên, phải tạo giỏ mới (đúng thiết kế `BR-CART §3`) |
### 1.2 Nhóm Giao dịch — `CMP-03` Catalog & Inventory *(schema `catalog`)*
| Thực thể | **SoT** | Bản sao ở đâu | Cập nhật bằng cơ chế gì | Trễ tối đa | Lệch quá thì sao |
|---|---|---|---|---|---|
| `category`, `category_i18n`, `product`, `product_i18n` | `CMP-03` | Chỉ mục tìm kiếm — **hoãn dùng OpenSearch ở P2 MVP** (`SAD §10`), dùng Postgres full-text search trực tiếp trên schema `catalog` | N/A (không có read model phái sinh ở MVP) | N/A | N/A |
| `product_variant` (giá `price_amount`) | `CMP-03` | `cart_item.unit_price_snapshot`, `order_item.unit_price_snapshot` (snapshot một chiều, không đồng bộ ngược) | Snapshot lúc thêm giỏ/lúc đặt hàng | N/A — cố ý đóng băng | Khách thấy `priceChanged=true` ở `GET /v1/cart` (`ICD §3.1.1`) nếu giá hiện tại khác snapshot — không tự cập nhật `unit_price_snapshot`, chỉ hiển thị cảnh báo |
| `inventory_stock` (`quantity_available`, `quantity_reserved`) | `CMP-03` | Không có bản sao — mọi kiểm tra/giữ tồn kho gọi trực tiếp `IF-005` (in-process, `ASR-006`) | N/A | 0 (đọc trực tiếp SoT) | N/A — đúng lý do không cache: tồn kho là dữ liệu cần chính xác tức thời (chống oversell, `BR-CART-02`) |
| `wishlist_item`, `language`, `currency`, `exchange_rate` | `CMP-03` | Cache `exchange_rate` ở Redis, TTL 1 giờ (tham khảo `docs/05 §5.3.1`, chỉ hiển thị, không dùng để thanh toán) | Batch job refresh | 1 giờ | Hiển thị quy đổi lệch tạm thời — chấp nhận vì không dùng để tính tiền thật |
### 1.3 Nhóm Giao dịch — `CMP-04` Cart & Order (+ Dispute) *(schema `cart_order`) — chi tiết nhất*
| Thực thể | **SoT** | Bản sao ở đâu | Cập nhật bằng cơ chế gì | Trễ tối đa | Lệch quá thì sao |
|---|---|---|---|---|---|
| `cart`, `cart_item` | `CMP-04` | Không có bản sao RDS. Nội dung giỏ **hiện không cache ở Redis** (chỉ session/ownership cache) — xem đánh giá `OQ-040` ở §7.3 | — | — | `QAS-003` không đạt khi RDS chậm và không có fallback (xem `FAIL FM-06`) |
| `order` (đơn cha) | `CMP-04` | Không có | — | — | — |
| `order_seller`, `order_item`, `order_status_history` | `CMP-04` | `commission_transaction.order_seller_id`, `shipment.order_seller_id`, `dispute.order_seller_id` (FK logic, tiêu thụ qua sự kiện `OrderPlaced`/`PaymentConfirmed`, không SQL trực tiếp) | Consume event `OrderPlaced` (per-seller, `ADR-005`) | Fan-out lag ≤5 giây (`QAS-005`) | Vượt 5 giây → cảnh báo lag hàng đợi (`OQ-036`, ứng viên `inf`) |
| `return_request` | `CMP-04` | Không có | — | — | — |
| `dispute` | `CMP-04` | `payout_hold.release_status` (Commission & Payout) tiêu thụ trạng thái `dispute` qua sự kiện (chưa thiết kế tên sự kiện cụ thể — ứng viên module Quản lý đơn hàng, ngoài phạm vi US-002/003) | Sự kiện (chưa đặt tên) | Chưa chốt | Chưa chốt — **ngoài phạm vi module Giỏ hàng & Checkout**, ghi nhận điểm nối cho module kế tiếp |
| `outbox_event` *(mới, đề xuất — xem §4.1)* | `CMP-04` | Không — bảng nội bộ, không phải dữ liệu nghiệp vụ | — | — | — |
| `idempotency_key` *(mới, đề xuất — xem §4.2)* | `CMP-04` | Không | — | — | — |
### 1.4 Payment — `CMP-11`/`CMP-14` *(RDS Payment riêng, cô lập hoàn toàn theo `ADR-002`)*
| Thực thể | **SoT** | Bản sao ở đâu | Cập nhật bằng cơ chế gì | Trễ tối đa | Lệch quá thì sao |
|---|---|---|---|---|---|
| `payment` | `CMP-11` | `order.payment_init_status` (Cart & Order, schema `cart_order`) là **read model tối thiểu** — chỉ phản ánh 1 trong 4 trạng thái sơ khởi (`not_applicable`/`pending_payment_init`/`payment_init_ready`/`payment_init_failed`), **không** phải bản sao đầy đủ của `payment` | Consume `PaymentInitReady`/`PaymentInitFailed` (`ADR-013`) | Theo `ADR-013 §3.1`: ngưỡng UX "kẹt" 15 giây | Quá 15 giây chưa cập nhật → coi là bất thường, xem job dọn ở §4.3 |
| `payment_transaction` *(mới, đề xuất — xem §1.4.1)* | `CMP-11` | Không | — | — | — |
| `payment_reconciliation_log` | `CMP-11` | Không | — | — | — |
| `outbox_event` (bản riêng của Payment, khác bảng của `CMP-04` — 2 DB khác nhau) | `CMP-11` | Không | — | — | — |
#### 1.4.1 Vì sao cần `payment_transaction` tách khỏi `payment`
`payment` là bản ghi **theo Order** (xem §2.2 — 1 `Order` có 0..1 `payment`). Nhưng `ADR-013`
cho phép khách **thử lại** khởi tạo thanh toán sau `PAYMENT_INIT_FAILED` (§3.3 của `ADR-013`),
và mỗi lần thử là một lời gọi riêng tới VNPay/Momo với `gateway_transaction_ref` riêng (có thể).
Nếu chỉ có 1 dòng `payment`, mỗi lần thử lại phải ghi đè `gateway_transaction_ref` cũ — mất dấu
vết đối soát các lần thử thất bại trước đó (vi phạm `BR-CART-06`/`ADR-004` — "bản ghi thanh toán
không bao giờ bị xoá"). `payment_transaction` ghi **mỗi lần gọi gateway** (init hoặc webhook xác
nhận) như một dòng riêng, `payment.status` là trạng thái **tổng hợp mới nhất** suy ra từ dòng
`payment_transaction` gần nhất thành công.
### 1.5 Nhóm Hỗ trợ — 6 `CMP` còn lại *(schema-per-module trong cùng RDS chính, `ADR-006`)*
| `CMP` | Schema | Thực thể chính | SoT | Đối chiếu `docs/05` (P1) |
|---|---|---|---|---|
| `CMP-05` Seller Management | `seller` | `seller`, `kyc_document`, `seller_bank_account` | `CMP-05` | Giữ nguyên tên bảng/cột `docs/05 §5.2.5` — chỉ đổi database-per-service → schema-per-module |
| `CMP-06` Commission & Payout | `commission` | `commission_rule` (gồm `hold_days`), `commission_transaction`, `payout`, `payout_hold` | `CMP-06` | Giữ nguyên `docs/05 §5.2.6`, kể cả trạng thái `payout_hold.release_status` (`holding`/`released`/`disputed_frozen`/`reversed`) |
| `CMP-07` Promotion & Loyalty | `promotion` | `promotion`, `promotion_usage`, `loyalty_account`, `loyalty_transaction`, `membership_tier` | `CMP-07` | Giữ nguyên `docs/05 §5.2.7` |
| `CMP-08` Review | `review` | `review` | `CMP-08` | Giữ nguyên `docs/05 §5.2.8` |
| `CMP-09` Notification | `notification` | `notification_log`, `template` | `CMP-09` | Giữ nguyên `docs/05 §5.2.9` (thêm bảng `template` — không có trong `docs/05`, do `SAD §4.1` liệt kê `CMP-09` sở hữu "Template") |
| `CMP-10` Shipping & Fulfillment | `shipping` | `shipment`, `shipment_event` | `CMP-10` | Giữ nguyên `docs/05 §5.2.10` |
*Các module này không thuộc phạm vi "chi tiết nhất" của ghi chú người duyệt — chỉ xác nhận
ownership + tên bảng kế thừa từ `docs/05` (điều chỉnh sang schema-per-module). Schema chi tiết
đầy đủ (kiểu dữ liệu, index) thuộc dev khi module đó tới lượt thiết kế chi tiết ở GĐ2 kế tiếp.*
### 1.6 Hạ tầng dữ liệu dùng chung
| Hạ tầng | Vai trò | Ai sở hữu | Dữ liệu tồn tại bao lâu |
|---|---|---|---|
| `CMP-13` RDS chính | Physical host cho 8 schema (`identity`/`catalog`/`cart_order`/`seller`/`commission`/`promotion`/`review`/`notification`/`shipping`) | Ops/DBA | Theo backup/PITR §9 |
| `CMP-14` RDS Payment | Physical host riêng cho schema `payment` | Ops/DBA (quyền tách biệt hoàn toàn, `ADR-002`) | Theo backup/PITR §9 |
| `CMP-15` Redis | Cache session/ownership (hiện có); nội dung `Cart` (đề xuất, chưa chốt — `OQ-040`) | Ops/SRE (hạ tầng) + module tương ứng (key schema) | TTL theo loại key (§1.1, §7.3) |
| `CMP-12` Message Backbone (SQS FIFO + EventBridge) | Transit, không phải kho dữ liệu bền vững | Platform/SRE | DLQ giữ 14 ngày (đề xuất TẠM, `OQ-036`) |
| S3 + CloudFront (`SAD §4`) | Ảnh sản phẩm (public), KYC documents (private) | Ops/SRE + `CMP-05`/`CMP-03` | Xem §6 |
🔴 **15/15 `CMP` đã có bảng ownership ở trên — không `CMP` mồ côi dữ liệu, không thực thể nào có
hai chủ.** `CMP-01` (ALB) và `CMP-12` (Message Backbone) không sở hữu dữ liệu bền vững (đúng theo
`SAD §4.1` "không" ở cột dữ liệu sở hữu).
---
## 2. Mô hình dữ liệu mức khái niệm
*Thực thể và quan hệ, chưa phải schema vật lý. Chi tiết nhất: Cart & Order + Payment.*
### 2.1 ERD — Cart & Order + Payment *(chi tiết nhất, theo yêu cầu)*
```mermaid
erDiagram
CART ||--o{ CART_ITEM : contains
ORDER ||--o{ ORDER_SELLER : "splits_into (ADR-003)"
ORDER ||--o| PAYMENT : "paid_by (1 payment / order — xem OQ-043)"
ORDER_SELLER ||--o{ ORDER_ITEM : contains
ORDER_SELLER ||--o{ ORDER_STATUS_HISTORY : tracks
ORDER_SELLER ||--o{ RETURN_REQUEST : may_have
ORDER_SELLER ||--o{ DISPUTE : may_have
PAYMENT ||--o{ PAYMENT_TRANSACTION : "mỗi lần gọi gateway"
PAYMENT ||--o{ PAYMENT_RECONCILIATION_LOG : reconciled_by
CART {
uuid id PK
uuid customer_id FK "logic, nullable — Guest"
string session_id "dùng khi Guest"
enum status "active/converted/abandoned — BR-CART §3"
}
CART_ITEM {
uuid id PK
uuid cart_id FK
uuid product_variant_id FK "logic → catalog"
uuid seller_id FK "logic → seller"
string seller_name_snapshot "MỚI — ICD §3.1.1/§3.7, OQ-033 chốt TẠM"
int quantity
numeric unit_price_snapshot "VND, snapshot lúc thêm giỏ"
}
ORDER {
uuid id PK
uuid customer_id FK "logic, nullable — Guest checkout"
string order_number "khoá nghiệp vụ"
numeric total_amount "= Σ order_seller.subtotal_amount"
enum payment_init_status "MỚI — ADR-013: not_applicable/pending_payment_init/payment_init_ready/payment_init_failed"
string idempotency_key "khoá gốc của POST /v1/checkout, ADR-004/013"
timestamp placed_at
}
ORDER_SELLER {
uuid id PK
uuid order_id FK
uuid seller_id FK "logic → seller"
string sub_order_number "khoá nghiệp vụ"
numeric subtotal_amount
enum status "pending/confirmed/cancelled — BR-CART §3 (đoạn khởi tạo)"
}
ORDER_ITEM {
uuid id PK
uuid order_seller_id FK
uuid product_variant_id FK "logic → catalog"
string product_name_snapshot
int quantity
numeric unit_price_snapshot "VND, snapshot lúc đặt hàng — có thể khác cart_item nếu giá đổi giữa 2 mốc"
}
ORDER_STATUS_HISTORY {
bigint id PK
uuid order_seller_id FK
string status
timestamp changed_at
}
RETURN_REQUEST {
uuid id PK
uuid order_seller_id FK
string status
}
DISPUTE {
uuid id PK
uuid order_seller_id FK
uuid assigned_csr_id FK "logic → identity"
string status
}
PAYMENT {
uuid id PK
uuid order_id "logic — KHÔNG FK vật lý, khác RDS (ADR-002)"
enum method "vnpay/momo/cod"
numeric amount
enum status "pending_init/success/failed/refunded"
timestamp paid_at
}
PAYMENT_TRANSACTION {
uuid id PK
uuid payment_id FK
enum direction "init/webhook"
string gateway_transaction_ref
jsonb gateway_response_snapshot "không chứa số thẻ/CVV — BR-CART-05"
string status
timestamp created_at
}
PAYMENT_RECONCILIATION_LOG {
uuid id PK
uuid payment_id FK
string gateway_status
string discrepancy_note
timestamp reconciled_at
}
```
**Legend:** `||--o{` một-nhiều · `||--o|` một-không hoặc một · đường nối `PAYMENT`↔`ORDER` là
**FK logic** (khác RDS, không ràng buộc vật lý được — kiểm chứng bằng `IF-006`/sự kiện, không
bằng JOIN SQL).
🔴 **Khác biệt so với `BR_CartCheckout §4` (BA):** BA chưa biết `ADR-013` khi viết ERD — DAT bổ
sung `order.payment_init_status` và `order.idempotency_key`, `cart_item.seller_name_snapshot`.
Đây là **bổ sung kỹ thuật**, không đổi mô hình nghiệp vụ BA đã chốt (`Order`/`OrderSeller`/
`OrderItem` giữ nguyên cấu trúc cha-con). Không sửa `BR_CartCheckout` — ghi nhận ở §12 (đối chiếu
BA).
### 2.2 Bảng thực thể chi tiết (Cart & Order + Payment)
| Thực thể | Ý nghĩa nghiệp vụ | Khoá nghiệp vụ | Ước lượng số bản ghi (năm 1 / năm 3) | Tăng trưởng | Nguồn |
|---|---|---|---|---|---|
| `cart` | Giỏ hàng | *(id kỹ thuật)* | 🔴 Chưa có số — `TCO §2` chỉ có concurrent user (2.000/10.000), không có số giỏ hàng tuyệt đối | Theo `QAS-009` (×3/2 năm) | `BR_CartCheckout §4.1` |
| `cart_item` | Dòng sản phẩm trong giỏ | `(cart_id, product_variant_id)` | 🔴 Chưa có số | Theo `cart` | `BR_CartCheckout §4.1` |
| `order`/`order_seller`/`order_item` | Đơn hàng cha/con/dòng | `order_number`/`sub_order_number` | Kịch bản A: ~400 đơn/giờ đỉnh · B: ~15.000 đơn/giờ đỉnh (`QAS-002` nguồn) — **chưa có số/ngày hay /năm tuyệt đối**, `Confidence` 🔴 | Theo `QAS-009` | `QAS-002`, `TCO §2` |
| `payment` | Giao dịch thanh toán (1/`order`) | — | ≈ bằng số `order` (trừ đơn hủy trước khi khởi tạo thanh toán) | Theo `order` | Suy từ `order` |
| `payment_transaction` | Mỗi lần gọi gateway (init/webhook) | — | ≥1× số `payment` (có thể nhiều nếu khách thử lại — `ADR-013 §3.3`) | Theo `payment` × tỷ lệ thử lại (chưa đo — `ADR-013 §7`) | §1.4.1 |
| `payment_reconciliation_log` | Log đối soát định kỳ | — | Số `payment` × số chu kỳ đối soát chạy trong đời `payment` (đề xuất ≤15 phút/lần, `QAS-014`) — **có thể lớn hơn nhiều lần số `payment`**, cần retention ngắn (§6) | Cao — cần partition theo tháng (tham khảo `docs/05 §5.3.3`) | `ADR-004` |
🔴 **`TCO §2`/`QAS-009` chỉ có số concurrent user và đơn/giờ đỉnh, không có ước lượng số bản ghi
tuyệt đối theo năm** — mục "Ước lượng số bản ghi" của template không điền được đầy đủ. Không bịa
con số — đây là khoảng trống thật, ghi `OQ-047` (mới, §11).
### 2.3 Entity list rút gọn — các module còn lại
*Không lặp lại `docs/05 §5.2` — chỉ liệt kê để đối chiếu ownership (đã có ở §1.5), tên bảng/cột
đầy đủ xem tài liệu tham khảo đó (điều chỉnh database-per-service → schema-per-module).*
---
## 3. Chọn công nghệ lưu trữ
| Kho | Loại | Chứa gì | Vì sao loại này | `ADR` |
|---|---|---|---|---|
| RDS PostgreSQL Multi-AZ chính | Quan hệ | 8 schema (Identity, Catalog, Cart&Order, Seller, Commission, Promotion, Review, Notification, Shipping) | Gắn `QAS-006` (p95 write ≤300ms, read ≤200ms, tải gộp); Conway (`CON-05`) — 1 instance thay vì ~9-10 | `ADR-006` |
| RDS PostgreSQL Multi-AZ riêng | Quan hệ | Schema `payment` (`payment`, `payment_transaction`, `payment_reconciliation_log`) | Gắn `ASR-002`/`QAS-011` — cô lập PCI-DSS SAQ A | `ADR-002` |
| ElastiCache Redis | Khoá-giá trị | Session, ownership cache (hiện có); nội dung `Cart` (đề xuất — §7.3) | Gắn `QAS-003`/`QAS-004`/`QAS-010` | `ADR-007` |
| S3 + CloudFront | Object storage | Ảnh sản phẩm (public), KYC documents (private) | Kế thừa `SAD §4`, không đổi | — |
| Postgres full-text search (trong schema `catalog`) | Tìm kiếm (tạm) | Chỉ mục tìm kiếm sản phẩm | `SAD §10`: hoãn OpenSearch ở P2 MVP, dùng FTS Postgres tới khi có bằng chứng tải cần OpenSearch (ngưỡng: traffic thật ≥ kịch bản B ×2, `QAS-009`) | — |
| SQS FIFO + EventBridge | Message queue | Transit sự kiện `OrderPlaced`/`PaymentConfirmed`/`PaymentInitRequested`/`PaymentInitReady`/`PaymentInitFailed` | Gắn `QAS-005` | `ADR-005` |
**Phương án bị loại** *(kế thừa từ `OPT`/`ADR-001`, không lặp lại chi tiết — quy tắc `D3`)*:
| Loại | Loại vì | Xét lại khi |
|---|---|---|
| Database-per-service (mỗi `CMP` một RDS riêng, như `docs/05` P1) | Vi phạm Conway với năng lực team hiện tại (`CON-05`), tăng chi phí vận hành ~9-10 instance thay vì 2 | `POC-02` FAIL nghiêm trọng **và** team đủ lớn để vận hành nhiều DB (`ADR-006 §4`) |
| OpenSearch ngay từ đầu | `TCO §3.2`/`OPT §3` P2 hoãn chi phí này ở MVP | Traffic thật ≥ kịch bản B ×2 chưa kiểm chứng (`SAD §10`) |
| Kafka/MSK thay SQS/EventBridge | Đội chưa xác nhận kinh nghiệm production (`CON-05`, `OQ-010`) | `POC-01` FAIL nghiêm trọng (`ADR-005 §4`) |
---
## 4. Consistency
| Nhánh dữ liệu | Mức nhất quán | Trễ tối đa | Vì sao chấp nhận được | Ai chấp nhận | `ADR`/`QAS` |
|---|---|---|---|---|---|
| Tạo `Order`/`OrderSeller`/`OrderItem` trong 1 lần checkout | **Mạnh** (1 transaction ACID, schema `cart_order`) | 0 | `ASR-003` — cấu trúc cha-con phải tạo trọn vẹn hoặc không tạo gì | SA + Tech Lead | `ADR-003`, `QAS-002` |
| `order.payment_init_status` phản ánh kết quả khởi tạo thanh toán online | **Eventual** | Ngưỡng UX 15 giây (`ADR-013 §3.1`); ngưỡng "kẹt" cần job dọn — đề xuất §4.3 | Khách chấp nhận chờ ngắn để đổi lấy `QAS-002` luôn đạt (`ADR-013`, đã chọn Lựa chọn B) | Tech Lead + PO (đã chốt hướng, chờ ký thật) | `ADR-013`, `QAS-002` |
| `payment.status` (Payment Service) ↔ trạng thái thật ở VNPay/Momo | **Eventual**, có giới hạn thời gian bù bằng job đối soát | ≤15 phút (`QAS-014`, đề xuất — `OQ-021` chưa xác nhận) | Không thể đảm bảo mạnh vì phụ thuộc bên thứ ba (`ASR-004`) — job đối soát bù là biện pháp bắt buộc, không phải "chấp nhận rủi ro" | Tech Lead (kỹ thuật) — **không phải quyết định nghiệp vụ**, vì đây là ràng buộc vật lý (không kiểm soát được uptime đối tác) | `ADR-004`, `QAS-014` |
| `OrderSeller.status` (Cart&Order) ← `PaymentConfirmed` (Payment) | **Eventual** | Fan-out lag ≤5 giây (`QAS-005`) | Người dùng không cần thấy cập nhật tức thời sau khi đã thấy trang xác nhận đặt hàng | PO (đã ngụ ý chấp nhận qua `QAS-005`) | `ADR-005` |
| `CommissionTransaction`/`NotificationLog`/`Review` (eligibility)/`Shipment` ← `OrderPlaced` | **Eventual** | Fan-out lag ≤5 giây (`QAS-005`) + xử lý riêng từng consumer (chưa đo) | Không nằm trên đường găng checkout; nghiệp vụ chấp nhận trễ vài giây tới vài phút | PO | `ADR-005` |
| `inventory_stock.quantity_reserved` khi checkout thất bại/hủy | **Mạnh trong phạm vi 1 transaction** cho bước reserve; **compensation bất đồng bộ** khi `PAYMENT_INIT_FAILED` không được khách xử lý (job dọn, §4.3) giải phóng reserve | Theo ngưỡng job dọn — đề xuất 5 phút (§4.3, `OQ-042`) | `BR-CART-02` yêu cầu không oversell — reserve phải mạnh; nhưng release khi hủy có thể trễ vài phút không gây oversell (chỉ tạm "khoá nhầm" tồn kho) | Tech Lead | `ASR-006`, `BR-CART-02` |
🔴 **"Eventual" không có con số trễ là câu nói suông** (`D2`/template §4) — mọi dòng trên đều có
con số hoặc tham chiếu tới `OQ` đang chờ con số (`OQ-021` cho tần suất đối soát thật).
### 4.1 Giao dịch xuyên service — khoảng trống outbox (`FAIL §7`)
`FAIL_e-commerce_v1.0.md §7` đã ghi nhận khoảng trống: nếu service chết **sau** `COMMIT` transaction
tạo `Order` nhưng **trước** khi publish `OrderPlaced`/`PaymentInitRequested` thành công, sự kiện
bị mất vĩnh viễn — không consumer nào (Commission, Promotion, Notification, Shipping, Payment)
biết `Order` đó tồn tại. Đây là bài toán **dual write** kinh điển (ghi DB + gọi message queue không
nguyên tử).
**Hai phương án đã cân nhắc** *(quy tắc `D3` — chưa viết `ADR` chính thức, xem cảnh báo dưới)*:
| Phương án | Mô tả | Ưu | Nhược |
|---|---|---|---|
| **PA-1 — Job quét đối chiếu** | Job định kỳ (đề xuất mỗi 5 phút) quét `order` không có event tương ứng đã publish thành công (dựa cột đánh dấu `event_published_at`), publish lại | Không cần bảng/hạ tầng mới | Cần một cột đánh dấu đủ tin cậy; có race condition giữa "đang xử lý" và "đã mất" nếu không thiết kế cẩn thận; không tổng quát cho các module khác publish event sau này |
| **PA-2 — Transactional Outbox** *(đề xuất SA)* | Ghi một dòng `outbox_event` **trong cùng transaction DB** với việc tạo `Order`/`OrderSeller`/`OrderItem` (cùng schema `cart_order`, cùng ACID). Một relay (poller nội bộ hoặc job định kỳ ngắn, đề xuất mỗi 1-2 giây) đọc `outbox_event` trạng thái `pending`, publish lên SQS/EventBridge, đánh dấu `published` khi thành công | Đảm bảo tính nguyên tử thật sự giữa ghi DB và phát sự kiện (industry-standard, dùng được cho **mọi** module publish event sau này — Payment cũng dùng bảng `outbox_event` riêng trong RDS Payment của nó) | Thêm bảng + một relay process cần giám sát (lag alerting); độ trễ publish tăng nhẹ (1-2 giây thay vì tức thời) |
**Đề xuất SA: PA-2 (Outbox pattern).** Vì đây là **quyết định thiết lập tiền lệ** cho toàn hệ
thống (mọi module publish event sau này sẽ dùng cùng cơ chế, không chỉ Cart & Order) —
theo `decision-radar.md §4` ("ngoại lệ tiền lệ"), ước lượng radar **~6/10** (đảo ngược trung
bình=1 · bán kính: Cart&Order + Payment (2 CMP publish) + tiền lệ cho các module tương lai=1 ·
chạm `QAS-005`/gián tiếp `DRV-08` (không mất giao dịch)=1 · ràng buộc trở thành chuẩn chung ≥1
năm=2 · chưa tranh cãi=0, +1 vì thiết lập tiền lệ áp dụng toàn hệ thống) → **`ADR` bắt buộc** —
xem [`adr/ADR-014_transactional-outbox-publish-su-kien.md`](adr/ADR-014_transactional-outbox-publish-su-kien.md)
(`Proposed`, viết ở lượt hoạt động `adr` kế tiếp). `OQ-044` **giữ nguyên mở**, chờ Tech Lead thật
xác nhận thiết kế relay (số instance, HA) trước khi `ADR-014` chuyển `Accepted` — xem `ADR-014 §3`.
**Bù trừ khi lỗi giữa chừng (nếu PA-2 được chọn):**
| Bước lỗi | Ai phát hiện | Xử lý |
|---|---|---|
| Relay chết trước khi publish | Alerting theo `outbox_event` tuổi > ngưỡng (đề xuất 1 phút, chưa xác nhận) chưa `published` | Restart relay (nhiều instance, không đơn điểm lỗi) — `outbox_event` vẫn còn trong DB, không mất |
| Publish thành công nhưng đánh dấu `published` thất bại (crash giữa hai bước) | Relay quét lại thấy `pending` dù đã publish | Chấp nhận **at-least-once** — consumer đã có dedupe theo `eventId` (`ICD §4`), publish trùng không gây hiệu ứng phụ |
| `outbox_event` tồn đọng số lượng lớn (relay chậm hơn tốc độ ghi) | CloudWatch metric số dòng `pending` | Backpressure — chưa thiết kế cụ thể, ứng viên `inf` |
**Bảng theo template (`saga`/`outbox`/`2PC`/không có):**
| Luồng nghiệp vụ | Cơ chế | Bù trừ khi lỗi giữa chừng | Ai phát hiện lệch | `ADR` |
|---|---|---|---|---|
| Checkout → publish `OrderPlaced`/`PaymentInitRequested` | **Outbox** | Xem bảng trên | Alerting lag `outbox_event` (`inf`, chưa thiết kế) | [`ADR-014`](adr/ADR-014_transactional-outbox-publish-su-kien.md) *(`Proposed`, `OQ-044` giữ mở)* |
| Payment Service → publish `PaymentConfirmed`/`PaymentInitReady`/`PaymentInitFailed` | **Outbox** (bảng riêng trong RDS Payment) *(cùng cơ chế)* | Tương tự | Tương tự | [`ADR-014`](adr/ADR-014_transactional-outbox-publish-su-kien.md) |
| Webhook VNPay/Momo → cập nhật `payment.status` | Không saga — xử lý đồng bộ trong 1 transaction schema `payment`, bù bằng job đối soát nếu webhook không tới (`ADR-004`) | Job đối soát ≤15 phút phát hiện lệch, ghi `payment_reconciliation_log`, cảnh báo — **không tự động sửa** | Job đối soát | `ADR-004` |
| Checkout thất bại giữa chừng (VD `IF-005` timeout trước bước ghi `Order`) | Không có gì để bù — transaction chưa `BEGIN`/đã `ROLLBACK`, không có trạng thái nửa vời | N/A | N/A | — |
### 4.2 Idempotency store cho `POST /v1/checkout`
*Theo `ADR-004`/`ICD §1` mục 7 — `Idempotency-Key` bắt buộc cho `POST /v1/checkout`.*
| Thực thể | Cột | Ý nghĩa |
|---|---|---|
| `idempotency_key` *(mới, schema `cart_order`)* | `idempotency_key` (UNIQUE) | Giá trị header `Idempotency-Key` do client gửi |
| | `endpoint` | VD `POST /v1/checkout` — cho phép tái sử dụng bảng này cho các endpoint ghi khác sau này |
| | `request_hash` | Hash body request — phát hiện client gửi lại **cùng key nhưng khác nội dung** (lỗi client, phải từ chối `409`, không âm thầm dùng response cũ) |
| | `response_snapshot` (jsonb) | Response đã trả lần đầu — trả lại nguyên vẹn nếu gọi lại trong TTL |
| | `status` | `processing` (đang xử lý, tránh race hai request đồng thời cùng key) / `completed` |
| | `expires_at` | **Đề xuất TTL 48 giờ** (đủ dài cho khách quay lại thử thanh toán trong ngày, đủ ngắn để không phình bảng vô hạn) — 🔴 chưa xác nhận Tech Lead |
**Webhook thanh toán KHÔNG dùng bảng này** — dùng khoá nghiệp vụ tự nhiên
`payment_transaction.gateway_transaction_ref` (UNIQUE constraint), đúng theo `ICD §1` mục 7 (đối
tác ngoài không tự đặt `Idempotency-Key`).
### 4.3 Job dọn `Order` kẹt ở `PENDING_PAYMENT_INIT` — đề xuất cho `OQ-042`
*`OQ-042` (`00-index/OQ_e-commerce.md`) hỏi ngưỡng thời gian giữ `Order` ở `PENDING_PAYMENT_INIT`
trước khi coi là "kẹt". Đây là đề xuất của `DAT`, **không tự quyết** — Tech Lead xác nhận.*
| | Đề xuất SA |
|---|---|
| **Ngưỡng quét** | `order.payment_init_status = pending_payment_init` **và** `order.placed_at` cũ hơn **5 phút** (gấp ~2× ngưỡng UX "kẹt" 15 giây × margin an toàn cho retry/hàng đợi chậm — không dùng chung ngưỡng 15 giây của UX vì đó là ngưỡng *hiển thị cho khách*, không phải ngưỡng *hệ thống coi là thất bại vĩnh viễn*) |
| **Tần suất job** | Mỗi 5 phút (tham khảo mẫu tương tự `docs/05 §5.2.6` job quét `payout_hold`) |
| **Hành động khi phát hiện** | (1) Cập nhật `order.payment_init_status = payment_init_failed` (nếu chưa có sự kiện `PaymentInitFailed` nào tới — coi như mất sự kiện); (2) Giải phóng `inventory_stock.quantity_reserved` tương ứng (gọi `IF-005` release, đối xứng với bước reserve ở checkout); (3) Ghi `order_status_history`; (4) Cảnh báo Ops nếu số lượng "kẹt" phát hiện trong 1 lần quét vượt ngưỡng bất thường (đề xuất >5 đơn/lần quét — chưa xác nhận) |
| **Rủi ro ngưỡng quá ngắn** | Hủy nhầm `Order` mà VNPay/Momo chỉ đang xử lý chậm bất thường (chưa timeout thật ở phía Payment Service) |
| **Rủi ro ngưỡng quá dài** | `Order` "ma" tồn đọng lâu, tồn kho reserve bị khoá không cần thiết, nhiễu báo cáo doanh số |
🔴 Cập nhật `OQ-042` trong sổ `00-index/OQ_e-commerce.md` với đề xuất này (giữ nguyên **mở**, chờ
Tech Lead xác nhận) — xem §11.
---
## 5. Phân loại dữ liệu & tuân thủ
*🔴 **`ADR-008` (Security/Legal) chưa có người ký** (`OQ-007`) — bảng dưới là đề xuất phân loại +
retention của SA, **không phải quyết định cuối**. Phủ quyết thuộc Pháp chế/Bảo mật.*
| Nhóm dữ liệu | Mức nhạy cảm | Ví dụ trường | Lưu ở đâu | Mã hoá at-rest | Che khi hiển thị | Retention *(đề xuất, chờ Legal)* | Cách xoá theo yêu cầu |
|---|---|---|---|---|---|---|---|
| Danh tính & liên hệ khách hàng | **PII** | `user_account.email/phone`, `customer_profile.full_name/date_of_birth` | schema `identity` | RDS KMS (mặc định) + mã hoá tầng ứng dụng cho `mfa_device.secret_encrypted` (khuyến nghị, `sec` xác nhận) | Không che với chính chủ; không hiển thị cho seller | Ẩn danh hoá trong **30 ngày** kể từ yêu cầu xoá hợp lệ (NĐ13/2023), giữ `order`/`payment` liên quan ở dạng tách định danh (tham khảo `docs/05 §5.3.6`, **assumption** `ASM-37`) | Ẩn danh hoá (anonymize), không xoá cứng bản ghi tài chính liên quan |
| Địa chỉ giao hàng | **PII** | `customer_address.recipient_name/phone/address_line`, snapshot trong `order_seller` lúc checkout | schema `identity` (SoT) + snapshot trong `cart_order` (một chiều) | RDS KMS | **Whitelist cho seller theo `ADR-008` PA-2** (đề xuất, chờ Security/Legal): chỉ `recipient_name`/`phone`/`address_line` của `OrderSeller` tương ứng — **không** chia sẻ email, lịch sử mua hàng, hay địa chỉ của seller khác trong cùng `Order` | Cùng nhóm với danh tính khách hàng ở trên | Cùng cơ chế trên |
| Tên gian hàng (denormalize) | Nội bộ (không PII) | `cart_item.seller_name_snapshot` | schema `cart_order` | Không cần mã hoá riêng (dữ liệu công khai — tên gian hàng hiển thị công khai trên sàn) | Không cần che | Theo vòng đời `cart` (§6) | Xoá cùng `cart` |
| KYC seller | **PII nhạy cảm** | `kyc_document.file_url_s3` (ảnh CMND/giấy phép kinh doanh), `seller.tax_code/business_license_number` | schema `seller` + S3 bucket riêng (private) | S3 server-side encryption + RDS KMS; đề xuất mã hoá tầng ứng dụng cho object S3 (khuyến nghị `sec` xác nhận) | Chỉ Admin/CSR được duyệt xem (`RBAC` của module Seller, ngoài phạm vi US-002/003) | Tối thiểu **5 năm** sau khi seller ngừng hoạt động (tham khảo `docs/05 §5.3.6`, **assumption** `ASM-37`, chờ Legal) | Không xoá trong thời hạn lưu trữ pháp lý — chỉ ẩn khỏi truy cập vận hành thường ngày sau khi seller đóng |
| Thông tin tài khoản ngân hàng seller | **Payment** | `seller_bank_account.account_number/account_holder_name` | schema `seller` | **Bắt buộc mã hoá tầng ứng dụng** (không chỉ dựa KMS) — đề xuất, `sec` xác nhận | Che một phần khi hiển thị (VD `****1234`) — chi tiết thuộc `sec` | Theo thời hạn hợp đồng seller + tối thiểu 10 năm sau giao dịch cuối (kế toán, tham khảo `docs/05`) | Không xoá trong thời hạn kế toán |
| Dữ liệu thanh toán khách hàng | **Payment — KHÔNG lưu số thẻ/CVV** (`BR-CART-05`, SAQ A) | `payment.amount`, `payment_transaction.gateway_transaction_ref`, `gateway_response_snapshot` (đã lọc, không chứa thẻ) | schema `payment` (RDS riêng, `ADR-002`) | RDS KMS + mã hoá tầng ứng dụng cho `gateway_transaction_ref` (khuyến nghị) | Chỉ hiển thị tham chiếu rút gọn cho khách, không hiển thị `gateway_response_snapshot` thô | Tối thiểu **10 năm** (chứng từ kế toán, tham khảo `docs/05 §5.3.6`, `ASM-37`) | Không xoá — dữ liệu tài chính bắt buộc lưu theo luật kế toán |
| Dữ liệu nghiệp vụ công khai/nội bộ | Công khai/nội bộ | `product`, `category`, `review`, `commission_rule` | Các schema tương ứng | RDS KMS mặc định | Không cần che | Theo vòng đời nghiệp vụ (§6) | Xoá thường theo yêu cầu quản trị (không liên quan NĐ13/2023) |
### 5.1 Ràng buộc pháp lý
| Ràng buộc pháp lý | Nguồn | Hệ quả kiến trúc |
|---|---|---|
| Dữ liệu cá nhân phải xác nhận nơi lưu trữ đáp ứng NĐ13/2023 | `CON-06`, `ASM-04` (chưa xác nhận), `ADR-008` | RDS/S3 đặt tại `ap-southeast-1` — **chưa được Legal xác nhận đủ** (`OQ-007`); áp dụng cho cả backup/snapshot, không chỉ dữ liệu online (xem cảnh báo dưới) |
| PCI-DSS SAQ A — không lưu thẻ/CVV | `CON-06`, `BR-CART-05`, `ADR-002` | Không trường nào trong bất kỳ schema nào ngoài `payment` được chứa dữ liệu thẻ; `payment_transaction.gateway_response_snapshot` phải được lọc trước khi ghi (loại bỏ trường thẻ nếu gateway trả về, dù không mong đợi) |
| Quyền được xoá (NĐ13/2023) | `CON-06` | Ẩn danh hoá, không xoá cứng, cho dữ liệu có ràng buộc tài chính/kế toán song song (xem bảng §5) |
🔴 **Backup và log cũng chứa PII** (`design-rules.md D9`/template §5) — snapshot PITR của schema
`identity`/`cart_order` chứa toàn bộ PII chưa ẩn danh tại thời điểm backup; `payment_transaction.
gateway_response_snapshot` trong backup cũng phải tuân cùng ràng buộc residency. Retention backup
(§9) phải đối chiếu lại với retention dữ liệu ở bảng trên khi Legal xác nhận — hiện **chưa đối
chiếu được vì Legal chưa có** (`OQ-007`).
### 5.2 Whitelist chia sẻ PII cho seller — hiện thực hoá `ADR-008` PA-2
*`ADR-008` (Security/Legal, chưa ký) đề xuất whitelist `recipient_name`/`phone`/`address_line`.
DAT hiện thực hoá thành cơ chế: `order_seller` (schema `cart_order`) chỉ lưu **snapshot đúng 3
trường này** của địa chỉ giao hàng liên quan tới `OrderSeller` đó — **không** lưu tham chiếu tới
toàn bộ `customer_address`/`user_account` của khách. Bất kỳ API nào trả dữ liệu `OrderSeller` cho
Seller Management (`IF` tương lai, ngoài phạm vi US-002/003) chỉ đọc 3 trường này, không JOIN
ngược sang schema `identity`.*
🔴 Đây là thiết kế **hiện thực hoá đề xuất PA-2 của `ADR-008`** — không phải quyết định độc lập
của `DAT`. Nếu Security/Legal từ chối PA-2 khi có người ký, thiết kế này phải sửa theo phương án
được chọn (`ADR-008` vẫn `Proposed`, chưa `Accepted`).
---
## 6. Vòng đời & lưu trữ dài hạn
| Thực thể | Dữ liệu nóng | Chuyển sang lạnh sau | Xoá sau | Ai duyệt |
|---|---|---|---|---|
| `cart` (Guest) | Redis/RDS, hoạt động | — | 7 ngày không hoạt động → `abandoned` (`BR-CART §3`, tham khảo `docs/05 §5.3.1`) | Tech Lead (đã có tiền lệ tham khảo) |
| `cart` (Customer) | RDS, hoạt động | — | 30 ngày không hoạt động → `abandoned` (tham khảo `docs/05`, `BR-CART §3` chưa chốt số chính thức — `OQ` của BA) | PO/Tech Lead |
| `order`/`order_seller`/`order_item` | RDS, 12 tháng gần nhất | Partition theo tháng (tham khảo `docs/05 §5.3.3`), archive sau 12 tháng sang storage rẻ hơn (đề xuất, chưa xác nhận) | Không xoá — dữ liệu tài chính/pháp lý (10 năm, xem §5) | Tech Lead + Kế toán |
| `payment`/`payment_transaction`/`payment_reconciliation_log` | RDS Payment, 12 tháng gần nhất | Partition theo tháng | Không xoá trong 10 năm (§5) | Tech Lead + Kế toán |
| `outbox_event` (cả hai schema) | RDS, vài giờ gần nhất | — | **Xoá sau 7 ngày kể từ `published`** (đề xuất — không cần giữ lâu, chỉ phục vụ relay + debug ngắn hạn) | Tech Lead |
| `idempotency_key` | RDS | — | Xoá khi `expires_at` qua (đề xuất 48 giờ, §4.2) — job dọn định kỳ | Tech Lead |
| `notification_log`, `shipment_event` | RDS | 90 ngày (tham khảo `docs/05 §5.3.6`) | Archive lạnh hoặc xoá sau 90 ngày | Ops |
| `kyc_document` (S3 + metadata) | S3 versioning + cross-region replication | — | Tối thiểu 5 năm sau khi seller ngừng hoạt động (§5, `ASM-37`) | Legal + PO |
---
## 7. Hiệu năng dữ liệu
### 7.1 Chỉ mục cho `QAS-003`/`QAS-004`/`QAS-006`
| Truy vấn quan trọng | Tần suất | Khối lượng quét | Chỉ mục cần | `QAS` |
|---|---|---|---|---|
| `GET /v1/cart` — lấy `cart` theo `customer_id`/`session_id` + `cart_item` theo `cart_id` | Đọc nhiều (mỗi lần mở SCR-04) | ≤50 dòng/giỏ (`ASM-16`, `QAS §A3`) | `index(cart.customer_id)`, `index(cart.session_id)`, `index(cart_item.cart_id)` | `QAS-003` |
| `PATCH`/`DELETE /v1/cart/items/{id}` | Ghi đơn lẻ | 1 dòng | PK `cart_item.id` (đủ, không cần thêm) | `QAS-004` |
| `POST /v1/checkout` — ghi `order`+N `order_seller`+N `order_item` | Ghi theo tải đỉnh (`QAS-002`) | N ≤ số seller trong giỏ (thường nhỏ) | PK tự nhiên đủ; cần `index(order.customer_id, placed_at DESC)` cho truy vấn lịch sử đơn sau này | `QAS-002`, `QAS-006` |
| Kiểm tra/giữ tồn kho (`IF-005`) | Đọc+ghi trong mọi checkout | 1 dòng/`product_variant` | PK `inventory_stock.variant_id` (1-1, đã đủ) | `QAS-006` |
| Job đối soát (`payment_reconciliation_log`) | Theo chu kỳ (≤15 phút, `QAS-014`) | Số `payment` đang `pending_init`/`success` gần đây | `index(payment.status, paid_at)` | `QAS-014` |
### 7.2 Read replica
| | Quyết định |
|---|---|
| **Có read replica không** | Có — nhưng **chỉ tồn tại ở kịch bản B** (`SAD §7`: "B: r6g.2xlarge + 1 read replica"; kịch bản A không có) |
| **Route cái gì sang replica** | Đề xuất SA: **Catalog & Inventory** (đọc nhiều, phục vụ tìm kiếm/duyệt sản phẩm — không nằm trên đường găng ghi) route sang replica khi có (kịch bản B). **Cart & Order KHÔNG route sang replica** — `GET /v1/cart` cần đọc-ngay-sau-ghi (khách vừa `PATCH` xong phải thấy kết quả ngay), replica lag (`ASM-36`, chưa đo) có thể vi phạm tính đúng đắn cảm nhận được, rủi ro cao hơn lợi ích latency |
| **Vì sao** | Gắn `QAS-003`/`QAS-006` — Catalog chịu tải đọc gấp ~10× tải ghi (`QAS-006` nguồn: "~150.000 truy vấn catalog/giờ so với ~15.000 đơn/giờ"), hưởng lợi rõ từ replica; Cart&Order tải đọc/ghi cân bằng hơn và nhạy cảm với độ tươi dữ liệu |
| **`ADR`** | Không đạt ngưỡng `ADR` (radar ước lượng ~3: đảo ngược thấp, bán kính 1 module, không chạm `QAS` Must trực tiếp theo hướng tiêu cực, có thể đổi routing qua config) — ghi `OQ-046` (§11), không cần `ADR` |
### 7.3 Cache nội dung `Cart` — đánh giá cho `OQ-040`
*`OQ-040` (`FAIL §4`) hỏi: có nên thêm cache nội dung `Cart`/`CartItem` vào Redis để `GET
/v1/cart` có fallback thật khi RDS chậm không? Đây là đánh giá của `DAT`, **không tự quyết**.*
| | Phương án A — Không cache (giữ nguyên) | Phương án B — Thêm cache nội dung `Cart` |
|---|---|---|
| **Mô tả** | `GET /v1/cart` luôn đọc RDS; RDS chậm/lỗi → `503` (timeout 200ms theo `FAIL FM-06`) | Cache-aside: ghi cache khi `PATCH`/`DELETE`/thêm giỏ (write-through), đọc cache trước, fallback RDS nếu miss; TTL đề xuất ngắn (5-10 phút) vì giá/tồn kho có thể đổi |
| **Đạt `QAS-003` khi RDS chậm** | ❌ Không — trả lỗi nhanh hơn, không phải trả đúng | ✅ Có — miễn cache còn dữ liệu |
| **Rủi ro dữ liệu cũ** | Không có (luôn đọc SoT) | Giá/tồn kho hiển thị có thể lệch trong TTL — đã có cơ chế `priceChanged` (`ICD §3.1.1`) giảm nhẹ rủi ro giá, nhưng **không** giảm rủi ro hiển thị sai `quantity` nếu khách sửa giỏ ở tab khác trong TTL |
| **Độ phức tạp thêm** | Không | Cần thiết kế invalidation (write-through khi `PATCH`/`DELETE`), thêm 1 loại key Redis mới, thêm giám sát hit/miss ratio |
| **Chi phí hạ tầng** | Không đổi | Redis hiện tại (`CMP-15`) đã có sẵn — chi phí thêm chủ yếu là dung lượng bộ nhớ (nhỏ, giỏ hàng ≤50 dòng) — không cần instance Redis mới |
| **Khuyến nghị SA** | — | **Nghiêng về B** nếu Tech Lead chấp nhận độ phức tạp invalidation, vì chi phí hạ tầng thấp và cải thiện trực tiếp khả dụng đúng lúc tải cao (flash sale — thời điểm RDS dễ chậm nhất, theo `QAS-006`/`ARISK-02`) |
🔴 Cập nhật `OQ-040` trong sổ với bảng đánh giá này (giữ nguyên **mở**, chờ Tech Lead quyết định
cuối) — xem §11.
### 7.4 Bảng quyết định chung
| Vấn đề | Quyết định | `ADR` |
|---|---|---|
| Phân mảnh (sharding/partitioning) | **Không sharding ở MVP** (kế thừa lý do `docs/05 §5.3.4` — schema-per-module đã là lớp scale đầu tiên); **partitioning theo tháng** cho `order`/`order_seller`/`order_item`, `payment`/`payment_transaction`/`payment_reconciliation_log` (tăng trưởng nhanh nhất, tham khảo `docs/05 §5.3.3`) | Không cần `ADR` riêng (kỹ thuật thi công, radar thấp) |
| Đọc/ghi tách nhau | Có, chỉ Catalog dùng read replica (kịch bản B) — xem §7.2 | Không cần `ADR` (`OQ-046`) |
| Cache: cái gì, invalidate thế nào | Session/ownership (đã có, `ADR-007`); nội dung `Cart` (đề xuất, chưa chốt — `OQ-040`) | `ADR-007` (đã có); cache Cart chưa cần `ADR` nếu chỉ áp dụng 1 module (radar thấp) |
🔴 **Cache invalidation phải thiết kế cùng lúc với cache** (`D` template) — nếu Phương án B ở
§7.3 được chọn, cơ chế invalidate (write-through khi `PATCH`/`DELETE`) phải triển khai đồng thời,
không được để cache "TTL tự nhiên" là biện pháp duy nhất.
---
## 8. Migration dữ liệu & Versioning schema
### 8.1 Migration dữ liệu legacy
**Không áp dụng — dự án greenfield**, theo `CTX`/`docs/05 §5.3.5`: không có hệ thống cũ cần
migrate cho module Giỏ hàng & Checkout. Dữ liệu khởi tạo chỉ gồm cấu hình tĩnh (`language`,
`currency`, `commission_rule` mặc định — thuộc các module khác, ngoài phạm vi chi tiết của
`DAT` lượt này).
### 8.2 Versioning schema (đề xuất — chưa chốt)
| | Đề xuất SA |
|---|---|
| **Công cụ** | Flyway (SQL migration thuần, phổ biến với PostgreSQL, không yêu cầu ORM cụ thể) — **đề xuất, chưa xác nhận Tech Lead** (`ICD §2` đã ghi "Flyway/Liquibase chưa chọn"); ghi `OQ-045` |
| **Chính sách thay đổi schema** | Additive trước, breaking sau — khớp `ICD §2 IF-016/017` ("Migration versioned tăng dần, không breaking ngược") |
| **Ranh giới migration theo schema** | Mỗi schema (`identity`/`catalog`/`cart_order`/.../`payment`) có thư mục migration riêng, chạy độc lập — khớp nguyên tắc schema-per-module (`ADR-006`), migration của `payment` chạy trên RDS Payment hoàn toàn tách biệt (không chung pipeline CI/CD deploy với schema khác, để giữ ranh giới cô lập `ADR-002`) |
| **Rollback schema** | Mỗi migration có script `down` tương ứng (Flyway Community không hỗ trợ auto-rollback — cần viết migration bù thủ công nếu cần lùi) — 🔴 đây là giới hạn công cụ cần Tech Lead biết trước khi chọn |
---
## 9. Sao lưu & khôi phục
*Đối chiếu `QAS-008`/`ADR-009` (RPO ≤15 phút, RTO ≤1 giờ cho Payment, Cart&Order, Identity —
"nhóm giao dịch cốt lõi") và `TCO §3.2` (dòng "Backup & DR" ≈$30/$60/tháng).*
| | RDS chính (8 schema) | RDS Payment |
|---|---|---|
| Tần suất backup | Automated backup + PITR liên tục (WAL archiving) | Automated backup + PITR liên tục |
| Retention PITR | 🔴 **Đề xuất 35 ngày** (tham khảo `docs/05 §5.3.2` cho nhóm "giao dịch cốt lõi") — chưa xác nhận Ops/SRE | 🔴 Đề xuất 35 ngày (cùng lý do — dữ liệu tài chính) |
| **Lần khôi phục thử gần nhất** | **Chưa từng thử** — `Confidence` 🔴, `ARISK` (đã ghi ở `ADR-009`) | **Chưa từng thử** — cùng `ARISK` |
| RPO | ≤15 phút (mục tiêu `QAS-008`, chưa diễn tập) | ≤15 phút (cùng mục tiêu) |
| RTO | ≤1 giờ (mục tiêu `QAS-008`, chưa diễn tập) | ≤1 giờ (cùng mục tiêu) |
🔴 **Phát hiện quan trọng — RPO/RTO đồng nhất ngoài ý muốn giữa module lõi và không lõi:** vì
`ADR-006` gộp **8 schema vào 1 RDS instance**, backup/PITR/Multi-AZ failover áp dụng ở **mức
instance**, không phân biệt được schema `cart_order`/`identity` (lõi, cần RPO≤15 phút) với schema
`seller`/`commission`/`promotion`/`review`/`notification`/`shipping` (không lõi, `docs/05 §5.3.2`
P1 đề xuất RPO/RTO lỏng hơn — ví dụ ≤1 giờ/≤4 giờ hoặc ≤24 giờ/≤24 giờ tuỳ nhóm). **Hệ quả:** mọi
schema trong RDS chính **đều được hưởng RPO/RTO chặt của nhóm lõi "miễn phí"** (tốt hơn yêu cầu
tối thiểu cho module không lõi) — không phải lỗi, nhưng khác với giả định phân tầng chi phí của
`docs/05` P1 (vốn tách theo instance để có backup rẻ hơn cho module ít quan trọng). `TCO §3.2`
hiện chỉ có 1 dòng "Backup & DR" chung, khớp với thực tế 1 instance — **không có mâu thuẫn số
liệu**, chỉ ghi nhận đây là đặc điểm của kiến trúc gộp, không phải khác biệt cần sửa.
| S3 (KYC, ảnh sản phẩm) | Đề xuất |
|---|---|
| Versioning | Bật cho bucket KYC (PII pháp lý) |
| Cross-region replication | Bật cho bucket KYC — tham khảo `docs/05 §5.3.2` |
| Lifecycle | Ảnh sản phẩm ít truy cập → storage rẻ hơn sau 90 ngày (tham khảo `docs/05`) |
---
## 10. Đối chiếu với `docs/sections/05-thiet-ke-du-lieu.md` (P1) — chỉ tham khảo
*Theo ghi chú người duyệt: tài liệu P1 chỉ dùng để tham khảo tên bảng/cột/số retention đề xuất,
**không** kế thừa kiểu kiến trúc. Danh sách khác biệt chính:*
| # | P1 (`docs/05`) | P2 (`DAT` này) | Ghi chú |
|---|---|---|---|
| 1 | Database-per-service (mỗi service 1 RDS) | 1 RDS chính schema-per-module + 1 RDS Payment riêng (`ADR-006`/`ADR-002`) | Đã ghi ở §3 |
| 2 | Kafka/MSK cho event | SQS FIFO + EventBridge (`ADR-005`) | Ảnh hưởng cơ chế dedupe/ordering — đã thiết kế lại ở `ICD §4` |
| 3 | Không có trạng thái `PENDING_PAYMENT_INIT`/`PAYMENT_INIT_READY`/`PAYMENT_INIT_FAILED` ở `order` | Có, theo `ADR-013` (bất đồng bộ hoá khởi tạo thanh toán) | P1 không có yêu cầu này vì chưa phát hiện xung đột ngân sách `QAS-002` — đặc thù của P2 |
| 4 | Có service **Audit & Compliance** riêng (`audit_log` tập trung, v3) | **P2 `SAD §4` (15 `CMP`) KHÔNG có `CMP` Audit & Compliance riêng** | 🔴 **Khoảng trống thật** — chưa kiến trúc audit trail xuyên module cho P2 dù `RBAC_CartCheckout §5` (BA) và `ADR-008 §6` (audit định kỳ PII) đều cần. Ghi `OQ-047` (§11), ngoài phạm vi giải quyết đầy đủ ở `DAT` lượt này |
| 5 | `cart_item` không có `seller_name_snapshot`; `order_item.unit_price` không phân biệt snapshot vs giá hiện tại | `cart_item.seller_name_snapshot`, `unit_price_snapshot`; `order_item.unit_price_snapshot` | Bổ sung theo `ICD §3.1.1/§3.7` (`OQ-033` chốt TẠM) |
| 6 | Retention cụ thể theo bảng (`docs/05 §5.3.6`) | Kế thừa làm **đề xuất khởi điểm**, chưa xác nhận Legal (`OQ-007`) | Không copy nguyên trạng làm quyết định — chỉ dùng làm baseline đề xuất |
---
## 11. Giả định & Ngoài phạm vi
**Giả định** *(tiếp số toàn dự án — `ASM-01…34` đã dùng, bắt đầu `ASM-35`)*:
| ID | Giả định | Cách xác minh | Nếu sai |
|---|---|---|---|
| `ASM-35` | 1 `Order` (cha) có đúng 1 `Payment` bất kể checkout gồm bao nhiêu seller/phương thức (theo `BR_CartCheckout §4` ERD gốc: `ORDER ||--o| PAYMENT`) | Tech Lead xác nhận qua `OQ-043` | Nếu sai (VD Payment tách theo `OrderSeller`): mô hình `payment.order_id` phải đổi thành `payment.order_seller_id`, ảnh hưởng `IF-006` request/response (`ICD §3.3`, `OQ-030`) và `order.payment_init_status` phải tính lại từ N `Payment` thay vì 1 |
| `ASM-36` | Read replica (kịch bản B) có độ trễ nhân bản đủ thấp (<1 giây) để không ảnh hưởng cảm nhận "mới cập nhật" khi dùng cho Catalog | Đo thật sau khi có replica (`inf`/`POC-02`) | Nếu lag cao hơn: cần điều chỉnh chiến lược cache Catalog (TTL ngắn hơn) hoặc không dùng replica cho các API vừa ghi vừa đọc gần nhau (VD Seller vừa cập nhật giá) |
| `ASM-37` | Số năm retention tham khảo từ `docs/05 §5.3.6` (30 ngày ẩn danh hoá, 5 năm KYC, 10 năm tài chính) là điểm khởi đầu hợp lý cho P2, dù kiến trúc lưu trữ đã đổi (schema-per-module) | Đại diện Pháp chế/Bảo mật xác nhận khi có người (`OQ-007`) | Nếu Legal yêu cầu số khác: sửa bảng §5/§6, có thể ảnh hưởng thiết kế partition (`docs/05 §5.3.3` tham khảo) nếu retention ngắn hơn nhiều |
| `ASM-38` | Outbox relay có thể chạy trong cùng process/task với module publish (Cart & Order, Payment) bằng polling loop 1-2 giây, không cần dịch vụ riêng, ở quy mô kịch bản A/B hiện tại | Đo thật khi có `POC` throughput hoặc sau go-live | Nếu tải cao hơn dự kiến khiến polling loop không theo kịp: cần tách relay thành service riêng hoặc dùng CDC (Debezium) — thay đổi hạ tầng đáng kể |
**Ngoài phạm vi:**
- Schema chi tiết đầy đủ (kiểu dữ liệu cột, index đầy đủ, constraint) cho 6 module Nhóm Hỗ trợ
(Seller, Commission, Promotion, Review, Notification, Shipping) — chỉ xác nhận ownership +
entity list (§1.5), kế thừa tên bảng từ `docs/05`; chi tiết hoá đầy đủ thuộc dev khi module đó
tới lượt.
- Kiến trúc audit trail xuyên module (Audit & Compliance Service kiểu P1, hoặc phương án khác) —
khoảng trống ghi nhận ở §10 dòng 4, `OQ-047`, thuộc hoạt động `sec`/`adr` kế tiếp.
- Thiết kế chi tiết OpenSearch (schema chỉ mục, đồng bộ) — hoãn tới khi có bằng chứng tải cần,
theo `SAD §10`.
- `ADR` chính thức cho outbox pattern (§4.1) — chỉ trình bày phân tích + đề xuất, chưa viết
`ADR`, thuộc hoạt động `adr` kế tiếp (`OQ-044`).
- Cấu hình connection pool cụ thể theo module (`OQ-037`, đã có ở `FAIL`) — `DAT` chỉ xác nhận
ranh giới schema cần pool riêng (§1), số cụ thể chờ `POC-02`.
- Chi tiết mã hoá tầng ứng dụng (thuật toán, quản lý khoá) cho các trường đánh dấu "khuyến nghị
mã hoá" ở §5 — thuộc hoạt động `sec` (hoạt động 6, chưa chạy).
## 12. Open Questions
*Tiếp số sổ SA (`00-index/OQ_e-commerce.md` đang ở `OQ-042`) — bắt đầu `OQ-043`.*
| ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì | Hệ quả nếu trả lời ngược |
|---|---|---|---|---|---|
| `OQ-043` | 1 `Order` (cha) có đúng 1 `Payment` (đa seller, 1 phương thức chung) hay có ngoại lệ tách `Payment` theo `OrderSeller`/phương thức? (nối `OQ-030` của `ICD`) | Tech Lead + PO | 2026-09-15 | Mô hình `payment.order_id`; `IF-006` request/response (`ICD §3.3`); `order.payment_init_status` | Nếu tách theo `OrderSeller`: cần N lời gọi gateway thay vì 1, ảnh hưởng cả `ADR-013` (mỗi `OrderSeller` có trạng thái init riêng, phức tạp hơn UX hiện tại) và chi phí giao dịch (một số gateway tính phí theo giao dịch) |
| `OQ-044` | Chấp nhận **Transactional Outbox pattern** (`outbox_event` + relay poller) làm cơ chế chính thức xử lý khoảng trống "commit `Order` nhưng publish event thất bại" (`FAIL §7`) không, hay dùng job quét đối chiếu đơn giản hơn (PA-1, §4.1)? Radar ước lượng ~6/10 — `ADR` bắt buộc, chưa viết. | Tech Lead | 2026-09-15 | `ADR` mới ở hoạt động `adr` kế tiếp; thiết kế relay ở `inf` | Chọn PA-1 (job quét): tiết kiệm 1 bảng/relay nhưng cần thiết kế cột đánh dấu đủ tin cậy và có race condition tiềm ẩn; không tổng quát cho module publish event sau này — mỗi module tự nghĩ cách riêng |
| `OQ-045` | Công cụ quản lý migration schema — Flyway hay Liquibase (hoặc khác)? | Tech Lead | 2026-09-15 | Quy trình CI/CD; khả năng rollback schema (§8.2) | Flyway: đơn giản, SQL thuần, nhưng Community không auto-rollback (cần viết migration bù thủ công). Liquibase: hỗ trợ rollback tốt hơn nhưng cú pháp XML/YAML phức tạp hơn, đường cong học tập cao hơn cho team chưa dùng |
| `OQ-046` | Đọc `Cart`/`Order` có nên route sang RDS read replica (chỉ có ở kịch bản B) hay luôn đọc primary? SA đề xuất: chỉ Catalog dùng replica, Cart&Order luôn đọc primary (§7.2). | Tech Lead | 2026-09-15 | Cấu hình routing đọc ở `inf`; độ tươi dữ liệu `GET /v1/cart` | Nếu Cart&Order cũng dùng replica để giảm tải primary: có thể vi phạm read-your-write ngay sau `PATCH`, gây bug khó tái hiện ("tôi vừa sửa giỏ mà sao không thấy") |
| `OQ-047` | P2 (`SAD` 15 `CMP`) không có `CMP` Audit & Compliance riêng như P1 (`docs/05` v3) — audit trail xuyên module (`RBAC_CartCheckout §5`, `ADR-008 §6`) nên kiến trúc theo cách nào: (a) mỗi module tự ghi `audit_log` trong schema riêng, hay (b) thêm 1 `CMP` Audit tập trung? | Tech Lead + Security | 2026-09-15 | Danh sách `CMP` (có thể thêm `CMP-16`); `ADR-001` (nếu thêm component mới ảnh hưởng cấu trúc tổng thể) | Chọn (a): đơn giản hơn, không thêm component, nhưng khó tổng hợp audit xuyên module khi CSR/Admin cần tra cứu. Chọn (b): tập trung hoá tra cứu nhưng thêm 1 `CMP`/1 điểm publish event bắt buộc từ mọi module — cần rà lại `SAD §4` |
**Cập nhật các `OQ` đã mở, không tạo ID mới** *(ghi trong sổ `00-index/OQ_e-commerce.md`, xem §13
đối chiếu)*:
- **`OQ-007`** — bổ sung: `DAT §5` đã phân loại PII/Payment/nghiệp vụ + đề xuất retention (30
ngày ẩn danh hoá PII khách hàng, 5 năm KYC, 10 năm tài chính — tham khảo `docs/05`, `ASM-37`)
và whitelist chia sẻ seller (`recipient_name`/`phone`/`address_line`, hiện thực hoá `ADR-008`
PA-2 ở §5.2) — **vẫn chờ đại diện Pháp chế/Bảo mật phủ quyết**, giữ nguyên **mở**.
- **`OQ-040`** — bổ sung: `DAT §7.3` đánh giá 2 phương án (không cache / cache-aside nội dung
`Cart`) kèm hệ quả — SA nghiêng về Phương án B nếu Tech Lead chấp nhận độ phức tạp invalidation,
giữ nguyên **mở**.
- **`OQ-042`** — bổ sung: `DAT §4.3` đề xuất ngưỡng quét 5 phút + tần suất job 5 phút + hành động
(cập nhật trạng thái, giải phóng tồn kho reserve, cảnh báo Ops) — giữ nguyên **mở**, chờ Tech
Lead xác nhận.
---
## 13. Tự chấm
### ① Bảng tự chấm Gate AG2 *(`workflow.md §2` — chỉ dòng liên quan tới `DAT`, tự chấm sớm)*
| # | Tiêu chí | ☐/✅ | Ghi chú |
|---|---|---|---|
| — | `ASR`/`QAS`/`SAD`/`ICD`/`FAIL` | ✅ *(đã đạt ở các hoạt động trước)* | Không lặp lại |
| — | `ADR-nnn` tồn tại cho mọi quyết định đạt ngưỡng radar | 🟡 Một phần | 13 `ADR` đã có (`ADR-001…013`); `DAT` phát hiện 1 quyết định mới đạt ngưỡng (outbox pattern, radar ~6) **chưa viết `ADR`** — đúng phạm vi (`dat` không bao gồm viết `ADR`), ghi `OQ-044`, chờ hoạt động `adr` |
| 1 | `DAT` — mỗi thực thể dữ liệu có **đúng một** chủ sở hữu; có phân loại PII và retention | ✅ | §1 (ownership đủ 15 `CMP`, không thực thể mồ côi/hai chủ), §5 (phân loại PII/Payment/nghiệp vụ + retention đề xuất) |
| — | `SEC`/`INF` | ☐ | Chưa tới — hoạt động 6-7 |
| — | `sa-conformance` báo coverage `ASR → ADR/CMP` ≥ 100% | *(không đổi bởi `DAT`)* | `DAT` không tạo `CMP`/`ASR` mới |
**Kết luận tự chấm (riêng phần `DAT`):** Hoạt động `dat` hoàn tất đúng phạm vi được giao —
ownership đủ 15/15 `CMP` không mồ côi/không hai chủ (`D7`), mô hình `Order`/`OrderSeller`/
`OrderItem` chi tiết theo `ADR-003`/`ADR-013`, phân loại PII/retention đề xuất (chờ Legal), chiến
lược đọc/ghi/cache/consistency đầy đủ theo `QAS-003/004/006/008/014`, phát hiện 1 khoảng trống cần
`ADR` mới (outbox, chưa viết — đúng phạm vi). AG2 còn thiếu `SEC`/`INF` (hoạt động 6-7) và chữ ký
thật của Tech Lead + Security + Ops/SRE.
### ② Checklist D1–D12 *(`design-rules.md`)*
| # | Mục | ☐/✅ | Ghi chú |
|---|---|---|---|
| D1 | Một ADR một quyết định | N/A | `DAT` không viết `ADR` mới (outbox là ứng viên, chưa viết — §4.1) |
| D2 | Không NFR định tính | ✅ | Mọi ngưỡng consistency/retention/TTL đều có con số hoặc tham chiếu `OQ` đang chờ số |
| D3 | Nêu phương án bị loại + lý do | ✅ | §3 (database-per-service/OpenSearch/Kafka bị loại, kế thừa `OPT`); §4.1 (PA-1 job quét bị loại thay bằng đề xuất PA-2 outbox, có bảng so sánh) |
| D4 | Sơ đồ khai báo mức + legend | ✅ | §2.1 khai báo "mức khái niệm" + legend; không trộn với mức schema vật lý |
| D5 | Interface có chủ/contract | N/A | Đã xử lý ở `ICD` (hoạt động 4); `DAT` chỉ tham chiếu |
| D6 | Phụ thuộc ngoài process có timeout/retry | N/A | Đã xử lý ở `FAIL` (hoạt động 8); `DAT` chỉ bổ sung khía cạnh dữ liệu (outbox, idempotency store) |
| D7 | Một chủ sở hữu dữ liệu | ✅ | §1 — bảng ownership đầy đủ 15 `CMP`, mỗi thực thể đúng 1 SoT, mọi bản sao ghi rõ cơ chế/độ trễ |
| D8 | Ràng buộc có FIT hoặc nhãn khuyến nghị | 🟡 Một phần | Đề xuất `FIT-26` (schema owner check mở rộng `FIT-09` đã có ở `ADR-006`), `FIT-27` (outbox lag monitor), `FIT-28` (idempotency TTL test) — ứng viên GĐ3, chưa viết thật |
| D9 | Con số hạ tầng quy ra tiền + nguồn | 🟡 Một phần | §9 dùng đúng số `TCO §3.2` (có nguồn); retention/backup cụ thể (35 ngày) là đề xuất tham khảo `docs/05`, chưa có báo giá riêng cho từng mức retention — không ảnh hưởng `TCO` hiện tại (1 dòng chung) |
| D10 | Không quyết định thay người có thẩm quyền | ✅ | §5 (PII/retention — chờ Legal), §7.3 (cache Cart — chờ Tech Lead), §4.1 (outbox — chờ Tech Lead + `adr`) đều trình phương án kèm hệ quả, không tự quyết |
| D11 | Có mục "Ngoài phạm vi" + `ASM-nn` | ✅ | §11 đầy đủ — `ASM-35…38` mới, có cách xác minh/hệ quả |
| D12 | Câu quy định thẩm quyền sơ đồ vs văn bản | ✅ | Có ngay sau Change Log |
### ③ Bảng đối chiếu với bộ BA
| `BA` | Nội dung | `DAT` tương ứng | Khớp? | Hành động |
|---|---|---|---|---|
| `BR_CartCheckout §4` (ERD khái niệm) | `Order`/`OrderSeller`/`OrderItem`/`Payment`, không có `payment_init_status` | §2.1 (bổ sung `payment_init_status`, `idempotency_key`, `seller_name_snapshot`) | 🟡 Một phần | Bổ sung kỹ thuật theo `ADR-013`/`ICD`, không đổi mô hình nghiệp vụ BA đã chốt — **BA không cần cập nhật `BR_CartCheckout`** vì đây là chi tiết kỹ thuật (state machine nội bộ), không phải quy tắc nghiệp vụ mới; nếu US-004 (Checkout) được BA viết `SRS` riêng sau này, cần phản ánh 4 trạng thái này ở đó (đã ghi `OQ-038` của `FAIL`) |
| `RBAC_CartCheckout §4` (mức che PII khi hiển thị cho seller — chưa chốt) | Whitelist `recipient_name`/`phone`/`address_line` (§5.2, hiện thực hoá `ADR-008` PA-2) | §5, §5.2 | 🟡 Một phần | `DAT` đề xuất cơ chế kỹ thuật (snapshot 3 trường, không JOIN ngược) — **vẫn chờ Security/Legal chốt chính thức** (`OQ-007`/`RISK-03` của BA), `DAT` không tự đóng câu hỏi này |
| `RBAC_CartCheckout §5` (lưu vết/audit — "chưa chốt thời hạn lưu") | §10 dòng 4 (khoảng trống Audit & Compliance `CMP`) | 🟡 Một phần | `DAT` phát hiện P2 chưa có kiến trúc audit trail xuyên module — **BA cần biết đây là khoảng trống kỹ thuật đang mở (`OQ-047`)**, chưa có nơi lưu vết chi tiết như `RBAC §5` kỳ vọng |
| `BR_CartCheckout §4.3` (thực thể ngoài phạm vi — `CUSTOMER`/`SELLER`/`PRODUCT_VARIANT` tham chiếu qua FK logic, snapshot giá) | §1.1/§1.2 (ownership `CMP-02`/`CMP-05`/`CMP-03`, snapshot `unit_price_snapshot`) | ✅ | Khớp hoàn toàn — `DAT` xác nhận đúng nguyên tắc BA đã nêu (FK logic, snapshot giá) |
### ④ Danh sách `OQ` mở kèm người phải trả lời, chặn gì
Xem §12 (`OQ-043…047` mới) cộng cập nhật `OQ-007/040/042`. Ba câu hỏi quan trọng nhất cho `sec`/
`inf` kế tiếp: **`OQ-007`** (PII/residency, chặn `ADR-008` và toàn bộ §5), **`OQ-044`** (outbox
pattern, chặn `ADR` mới + thiết kế relay ở `inf`), **`OQ-043`** (cardinality `Payment`/`Order`,
chặn thiết kế cuối cùng của `IF-006`). Sổ đầy đủ: `00-index/OQ_e-commerce.md`.
**Nhắc:** AG2 cần **Tech Lead + Security + Ops/SRE ký** — còn thiếu `SEC`/`INF` (hoạt động 6-7) và
1 `ADR` mới (outbox, `OQ-044`). Hoạt động kế tiếp có thể chạy song song: `sec` (dùng §5 phân loại
PII làm input threat model), `inf` (dùng §9 backup/RPO-RTO, §7.2 read replica, §4.1 relay outbox
nếu được chấp nhận).