686 lines
75 KiB
Markdown
686 lines
75 KiB
Markdown
# 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).
|