update skill and docs

This commit is contained in:
Canhchimlac
2026-09-22 13:46:36 +07:00
parent 343ad8bbbc
commit 51506aabc7
149 changed files with 43752 additions and 300 deletions

View File

@@ -0,0 +1,118 @@
# CTR — Contract Files Index — <PROJECT>
| | |
|---|---|
| **Version** | 1.0 |
| **Date** | YYYY-MM-DD |
| **Author** | <SA> (skill sa-2-architecture) |
| **Status** | 🟡 Draft |
| **Approved by** | Tech Lead: — · BE Lead: — · FE Lead: — |
| **Source** | ICD_… v1.0 · API_… của BA · SRS_… §4.1 (mã lỗi) · DOM_… §5 (event) · SEC_… §3 |
| **Scope** | |
| **Confidence** | 🟡 |
## Change Log
| Version | Date | Người sửa | Thay đổi | ADR |
|---|---|---|---|---|
| 1.0 | | | Bản đầu | — |
> **Thẩm quyền:** file trong `02-architecture/contracts/` **thắng** mọi bảng mô tả endpoint — ở `ICD`,
> ở `API` của BA, ở đây. Tài liệu này là **mục lục và biên bản lint**, không chép nội dung contract.
>
> **Vì sao có tài liệu này:** `ICD` mô tả interface bằng bảng, `API` của BA cũng bằng bảng. Dev phải
> tự viết lại OpenAPI từ bảng, FE và BE viết hai bản khác nhau, mock server không dựng được, contract
> test không có gì để chạy. File máy đọc đóng đúng lỗ đó.
---
## 1. Quy ước
| | |
|---|---|
| **REST** | OpenAPI **3.1** · một file mỗi module/bounded context · `contracts/openapi/<module>.yaml` |
| **Event** | AsyncAPI **3.0** · một file mỗi domain event group · `contracts/asyncapi/<domain>-events.yaml` |
| **gRPC** *(nếu có)* | `contracts/proto/<service>.proto` · versioning theo package |
| **Đối tác ngoài** | `contracts/partners/<ten>.yaml` — **mirror** tài liệu đối tác, ghi rõ nguồn + ngày lấy; không tự viết thay đối tác |
| **operationId** | `<module>.<động từ><DanhTừ>` — ví dụ `cartOrder.getCart`, `cartOrder.checkout`; **duy nhất toàn dự án** |
| **Mã lỗi** | schema `Error { code, message, details? }`; `code` là `E-<DOMAIN>-nnnn` của SRS §4.1, liệt kê trong `x-error-codes` mỗi operation |
| **Ba quy ước chốt** (`ICD` §1) | số lớn `type: string`; thời gian `format: date-time` UTC; phân trang theo `ADR-nnn` — khai báo một lần trong `components/` và `$ref` |
| **Ví dụ** | mỗi operation ≥ 1 `example` thành công + 1 ví dụ lỗi 4xx |
| **Lint** | `spectral lint` với ruleset `contracts/.spectral.yaml` nếu có; không có ⇒ kiểm cấu trúc bằng script ở §4 |
---
## 2. Mục lục file — `CTR-nn`
| `CTR` | File | Loại | Phủ `IF-nnn` | Owner (`ICD` §2) | Version contract | Lint | Ngày lint |
|---|---|---|---|---|---|---|---|
| `CTR-01` | `contracts/openapi/cart-order.yaml` | OpenAPI 3.1 | `IF-002` | BE Nhóm Giao dịch | `1.0.0` | ☐ | |
| `CTR-02` | `contracts/asyncapi/order-events.yaml` | AsyncAPI 3.0 | `IF-009` | BE Nhóm Giao dịch (publish) | `1.0.0` | ☐ | |
| `CTR-03` | `contracts/partners/vnpay.yaml` | mirror | `IF-007` | VNPay — 🔴 chưa có tài liệu thật (`ARISK-03`) | — | — | |
---
## 3. Ánh xạ `IF` → operation → BA
*Bảng `sa-conformance` đọc. Mỗi `IF-nnn` sync trong `ICD` §2 phải có ≥ 1 dòng; mỗi endpoint trong
`API` của BA phải có `operationId`.*
| `IF-nnn` | `operationId` / channel | Method + path / topic | Endpoint `API` của BA | `AC` liên quan | Mã lỗi (`E-…`) trong schema | `ROLE` (`SEC` §3) |
|---|---|---|---|---|---|---|
| `IF-002` | `cartOrder.getCart` | `GET /v1/cart` | `API_US002-003 §3.1` | `AC-US002-01..03` | `E-CART-0401`, `E-CART-0503` | `ROLE-01`, Guest |
| `IF-002` | `cartOrder.checkout` | `POST /v1/checkout` | `API_US002-003 §3.4` | `AC-US003-01..10` | `E-CHK-0409`, `E-CHK-0422`, `E-CHK-0503` | `ROLE-01`, Guest |
| `IF-009` | `OrderPlaced` | topic `order.placed.v1` | — | — | — | publish `CMP-04` |
🔴 Endpoint có trong `API` của BA mà không có `operationId` ⇒ hoặc BA đề xuất endpoint không tồn tại,
hoặc `CTR` bỏ sót. Xử lý ngay, báo BA qua `baSyncIssues`.
---
## 4. Kết quả lint và kiểm cấu trúc
*Người điều phối (có Bash) chạy, chép nguyên văn vào đây. Runner không có Bash ⇒ ghi "chưa lint".*
```bash
# có spectral
npx @stoplight/spectral-cli lint contracts/openapi/*.yaml contracts/asyncapi/*.yaml
# không có spectral — kiểm cấu trúc tối thiểu
node .claude/skills/sa-2-architecture/scripts/contract-check.mjs --dir sa-output/<P>/02-architecture/contracts \
--icd sa-output/<P>/02-architecture/ICD_<P>_v1.0.md --srs ba-output/<P>/03-specification/SRS_*.md
```
| Kiểm | Kết quả | Chi tiết |
|---|---|---|
| Mọi file parse được, `openapi: 3.1.x` / `asyncapi: 3.0.x` | ☐ | |
| Mọi `operationId` duy nhất | ☐ | |
| Mọi `IF-nnn` sync của `ICD` §2 có operation | ☐ | |
| Mọi mã lỗi `E-…` trong SRS §4.1 xuất hiện trong ≥ 1 `x-error-codes` | ☐ | |
| Số lớn là `string`, thời gian `format: date-time` | ☐ | |
| Mỗi operation có `example` thành công + lỗi | ☐ | |
---
## 5. Sinh code và mock từ contract *(khuyến nghị cho `AGD`)*
| Việc | Công cụ gợi ý | Ai | Ghi ở |
|---|---|---|---|
| Sinh client/server stub | openapi-generator · orval · oapi-codegen | BE/FE | `AGD` §2 reference implementation |
| Mock server cho FE | Prism · MSW từ OpenAPI | FE | `AGD` |
| Contract test | Pact · schemathesis | QA/BE | `FIT-nn` |
---
## 6. Giả định & Ngoài phạm vi
**Giả định:**
| ID | Giả định | Cách xác minh | Nếu sai |
|---|---|---|---|
**Ngoài phạm vi:**
- *(ví dụ: không viết contract cho interface in-process cùng module — thuộc `AGD`)*
## 7. Open Questions
| ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì | Hệ quả nếu trả lời ngược |
|---|---|---|---|---|---|

View File

@@ -0,0 +1,71 @@
# CTR-nn · contracts/asyncapi/<domain>-events.yaml — AsyncAPI 3.0
# Nguồn: ICD_<P> §4 (hợp đồng sự kiện) · DOM_<P> §5 (domain event) · DAT §4.1 (outbox/saga) · SEC §5 (PII)
# Quy ước: at-least-once, người nhận khử trùng bằng eventId; breaking change ⇒ eventVersion mới + channel mới
asyncapi: 3.0.0
info:
title: <Domain> Events
version: 1.0.0
description: |
Sự kiện phát từ CMP-nn qua IF-nnn. File này THẮNG bảng ICD §4 khi lệch.
Không chứa PII ngoài id trừ khi SEC §5 cho phép đích danh.
defaultContentType: application/json
servers:
prod:
host: <broker-host>
protocol: sqs # kafka | sqs | amqp | eventbridge
description: production
channels:
orderPlaced:
address: <domain>.<event>.v1
description: Phát khi aggregate <Root> commit xong (DOM §5); publish qua outbox (ADR-nnn)
messages:
OrderPlaced:
$ref: '#/components/messages/OrderPlaced'
operations:
publishOrderPlaced:
action: send
channel: { $ref: '#/channels/orderPlaced' }
summary: CMP-nn publish — IF-nnn
x-if: IF-nnn
x-producer: CMP-nn
x-consumers: [CMP-06, CMP-09]
x-ordering: theo partition key <aggregateId>
x-delivery: at-least-once · DLQ giữ 14 ngày (FAIL FM-nn)
components:
messages:
OrderPlaced:
name: OrderPlaced
title: Đơn hàng đã đặt
contentType: application/json
headers:
$ref: '#/components/schemas/EventHeaders'
payload:
$ref: '#/components/schemas/OrderPlacedPayload'
examples:
- name: ok
headers: { eventId: "018f…", eventVersion: "1.0", occurredAt: "2026-09-22T03:00:00Z", correlationId: "req-…" }
payload: { orderId: "018f…", customerId: "018e…", sellerIds: ["018d…"], totalAmount: { amount: "150000.00", currency: "VND" } }
schemas:
EventHeaders:
type: object
required: [eventId, eventVersion, occurredAt, correlationId]
properties:
eventId: { type: string, description: 'Khoá khử trùng cho consumer (ICD §4)' }
eventVersion: { type: string, pattern: '^[0-9]+\.[0-9]+$' }
occurredAt: { type: string, format: date-time }
correlationId: { type: string, description: 'Truyền xuyên suốt từ request gốc (ICD §1 điểm 8)' }
Money:
type: object
required: [amount, currency]
properties:
amount: { type: string, pattern: '^-?[0-9]+(\.[0-9]{1,2})?$' }
currency: { type: string, minLength: 3, maxLength: 3 }
OrderPlacedPayload:
type: object
additionalProperties: false
required: [orderId, sellerIds, totalAmount]
properties:
orderId: { type: string }
customerId: { type: [string, 'null'], description: 'null = Guest' }
sellerIds: { type: array, items: { type: string }, minItems: 1 }
totalAmount: { $ref: '#/components/schemas/Money' }

View File

@@ -0,0 +1,155 @@
# CTR-nn · contracts/openapi/<module>.yaml — OpenAPI 3.1
# Nguồn: ICD_<P> §3 (IF-nnn) · API_<US> của BA · SRS §4.1 (mã lỗi) · SEC §3 (quyền)
# Quy ước: ICD §1 — số lớn là string, thời gian date-time UTC, lỗi nghiệp vụ 4xx, phân trang theo ADR-nnn
openapi: 3.1.0
info:
title: <Module> API
version: 1.0.0
description: |
Contract cho IF-nnn. Tài liệu này THẮNG bảng mô tả trong ICD/API khi lệch.
Mã lỗi theo SRS §4.1; quyền theo SEC §3.
servers:
- url: https://api.<domain>/v1
description: production
- url: https://api-stg.<domain>/v1
description: staging
tags:
- name: <resource>
security:
- bearerAuth: []
paths:
/<resource>:
get:
operationId: <module>.list<Resource>
summary: <Mục đích một câu — khớp ICD §3.x>
tags: [<resource>]
x-if: IF-nnn
x-roles: [ROLE-01, ROLE-02]
x-error-codes: [E-<DOMAIN>-0401, E-<DOMAIN>-0403, E-<DOMAIN>-0503]
parameters:
- $ref: '#/components/parameters/PageCursor'
- $ref: '#/components/parameters/PageSize'
responses:
'200':
description: Danh sách
content:
application/json:
schema:
$ref: '#/components/schemas/<Resource>Page'
examples:
ok:
value: { items: [], nextCursor: null, hasNext: false }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'503': { $ref: '#/components/responses/Unavailable' }
post:
operationId: <module>.create<Resource>
summary: <Mục đích>
tags: [<resource>]
x-if: IF-nnn
x-roles: [ROLE-01]
x-idempotency: header Idempotency-Key, giữ 24h (ADR-nnn)
x-error-codes: [E-<DOMAIN>-0400, E-<DOMAIN>-0409, E-<DOMAIN>-0503]
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/<Resource>Create'
examples:
ok:
value: { name: "Cửa hàng 01" }
responses:
'201':
description: Đã tạo
content:
application/json:
schema: { $ref: '#/components/schemas/<Resource>' }
'200':
description: Trùng Idempotency-Key — trả bản ghi cũ, không tạo mới
content:
application/json:
schema: { $ref: '#/components/schemas/<Resource>' }
'400': { $ref: '#/components/responses/BadRequest' }
'409': { $ref: '#/components/responses/Conflict' }
'503': { $ref: '#/components/responses/Unavailable' }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
parameters:
IdempotencyKey:
name: Idempotency-Key
in: header
required: true
schema: { type: string, maxLength: 64 }
PageCursor:
name: cursor
in: query
schema: { type: string }
PageSize:
name: size
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
schemas:
Id:
type: string
description: Số lớn truyền dạng string (ICD §1 điểm 1)
pattern: '^[0-9a-f-]{36}$'
Money:
type: object
required: [amount, currency]
properties:
amount: { type: string, pattern: '^-?[0-9]+(\.[0-9]{1,2})?$', description: 'decimal dạng string' }
currency: { type: string, minLength: 3, maxLength: 3, example: VND }
Error:
type: object
required: [code, message]
properties:
code: { type: string, pattern: '^E-[A-Z]+-[0-9]{4}$', description: 'Mã lỗi SRS §4.1' }
message: { type: string, description: 'Text hiển thị theo SRS §4.2 hoặc mã để client dịch (ICD §1 điểm 6)' }
details:
type: array
items:
type: object
properties:
field: { type: string }
code: { type: string }
<Resource>:
type: object
required: [id, createdAt]
properties:
id: { $ref: '#/components/schemas/Id' }
createdAt: { type: string, format: date-time, description: 'UTC ISO-8601' }
<Resource>Create:
type: object
required: [name]
properties:
name: { type: string, minLength: 1, maxLength: 40, description: '40 code point — khớp FLD-<SCR>-nn' }
<Resource>Page:
type: object
required: [items, hasNext]
properties:
items: { type: array, items: { $ref: '#/components/schemas/<Resource>' } }
nextCursor: { type: [string, 'null'] }
hasNext: { type: boolean }
responses:
BadRequest:
description: Sai dữ liệu vào
content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { code: E-<DOMAIN>-0400, message: "…" } } }
Unauthorized:
description: Hết phiên
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
Forbidden:
description: Không đủ quyền — client không xoá dữ liệu đã nhập (ICD §6)
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
Conflict:
description: Vi phạm ràng buộc nghiệp vụ (BR-nnn)
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
Unavailable:
description: Phụ thuộc hỏng/timeout (FAIL FM-nn) — client cho thử lại, giữ dữ liệu đã nhập
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }

View File

@@ -32,8 +32,10 @@ read model.
## 2. Mô hình dữ liệu mức khái niệm
*Thực thể và quan hệ, chưa phải schema. Schema chi tiết thuộc dev.*
*Thực thể và quan hệ, chưa phải schema. Schema vật lý đủ cột, index, DDL và migration nằm ở
`PDM` (`--focus pdm`), sinh sau tài liệu này — mỗi thực thể ở đây phải có ≥ 1 bảng trong `PDM` §2.*
<!-- archify: mermaid-only -->
```mermaid
erDiagram
```

View File

@@ -0,0 +1,253 @@
{
"schema_version": 1,
"diagram_type": "architecture",
"meta": {
"title": "C4 Container — Hệ thống đối soát (mẫu)",
"quality_profile": "showcase",
"views": [
{
"id": "duong-chinh",
"label": "Đường chính nạp và đối soát",
"focus": [
"NV",
"WEB",
"API",
"DB"
],
"note": "Người dùng tải file POS, API ghi và đối soát trên CSDL chính."
},
{
"id": "duong-bat-dong-bo",
"label": "Nhánh bất đồng bộ",
"focus": [
"API",
"QUEUE",
"WORKER",
"POS"
],
"note": "Đối soát nặng chạy nền, gọi hệ thống POS ngoài có timeout."
}
]
},
"components": [
{
"id": "NV",
"type": "external",
"label": "Nhân viên đối soát",
"sublabel": "ROLE-01 · trình duyệt",
"pos": [
40,
300
],
"size": [
150,
60
]
},
{
"id": "IDP",
"type": "security",
"label": "IdP nội bộ",
"sublabel": "OIDC · IF-001",
"pos": [
560,
100
],
"size": [
150,
60
]
},
{
"id": "WEB",
"type": "frontend",
"label": "CMP-01 Web App",
"sublabel": "SPA · SCR-01..05",
"pos": [
300,
300
],
"size": [
150,
60
]
},
{
"id": "API",
"type": "backend",
"label": "CMP-02 Settlement API",
"sublabel": "REST/JSON · /v1",
"pos": [
560,
300
],
"size": [
150,
60
]
},
{
"id": "DB",
"type": "database",
"label": "CMP-03 CSDL chính",
"sublabel": "PostgreSQL · SoT",
"pos": [
820,
300
],
"size": [
150,
60
]
},
{
"id": "QUEUE",
"type": "messagebus",
"label": "CMP-04 Hàng đợi",
"sublabel": "SQS FIFO · IF-004",
"pos": [
560,
520
],
"size": [
150,
60
]
},
{
"id": "WORKER",
"type": "backend",
"label": "CMP-05 Worker",
"sublabel": "job đối soát nền",
"pos": [
820,
520
],
"size": [
150,
60
]
},
{
"id": "POS",
"type": "external",
"label": "Hệ thống POS",
"sublabel": "đối tác · IF-005",
"pos": [
1080,
520
],
"size": [
150,
60
]
}
],
"boundaries": [
{
"kind": "region",
"label": "Vùng hệ thống của ta · ap-southeast-1",
"wraps": [
"WEB",
"API",
"DB",
"QUEUE",
"WORKER"
]
},
{
"kind": "security-group",
"label": "sg-app · chỉ nhận từ ALB",
"wraps": [
"API",
"WORKER"
]
}
],
"connections": [
{
"id": "nv-web",
"from": "NV",
"to": "WEB",
"label": "HTTPS · sync",
"variant": "emphasis"
},
{
"id": "web-api",
"from": "WEB",
"to": "API",
"label": "REST · IF-002",
"variant": "emphasis"
},
{
"id": "idp-api",
"from": "IDP",
"to": "API",
"label": "JWT · IF-001",
"variant": "security",
"labelDy": 24
},
{
"id": "api-db",
"from": "API",
"to": "DB",
"label": "SQL · sync",
"variant": "emphasis"
},
{
"id": "api-queue",
"from": "API",
"to": "QUEUE",
"label": "publish · async",
"variant": "dashed",
"labelDy": 24
},
{
"id": "queue-worker",
"from": "QUEUE",
"to": "WORKER",
"label": "consume",
"variant": "dashed"
},
{
"id": "worker-db",
"from": "WORKER",
"to": "DB",
"label": "SQL · sync",
"labelDy": -24
},
{
"id": "worker-pos",
"from": "WORKER",
"to": "POS",
"label": "REST · 3s ·×3",
"variant": "dashed"
}
],
"cards": [
{
"dot": "emerald",
"title": "Đường chính",
"items": [
"Người dùng → Web → API → CSDL chính, toàn bộ đồng bộ",
"CSDL chính là single source of truth (DAT §1)"
]
},
{
"dot": "amber",
"title": "Nhánh bất đồng bộ",
"items": [
"Đối soát nặng đẩy qua hàng đợi FIFO, worker xử lý at-least-once",
"Gọi POS ngoài có timeout 3s, retry 3 lần theo FAIL FM-05"
]
},
{
"dot": "rose",
"title": "Bảo mật",
"items": [
"IdP nội bộ phát JWT, API kiểm ở tầng service (SEC §3)",
"API và Worker nằm trong security group chỉ nhận từ ALB"
]
}
]
}

View File

@@ -0,0 +1,45 @@
{
"schema_version": 1,
"diagram_type": "sequence",
"meta": {
"title": "Luồng chính — Lưu cửa hàng có kiểm tra POS (mẫu)",
"quality_profile": "showcase",
"viewBox": [1080, 600],
"column_fit": "spread",
"views": [
{ "id": "thanh-cong", "label": "Nhánh thành công", "focus": ["U", "FE", "BE", "POS"], "note": "POS phản hồi trong 3s, hệ thống lưu và báo đã lưu." },
{ "id": "timeout", "label": "Nhánh POS timeout", "focus": ["BE", "POS", "FE", "U"], "note": "Hết 3s không có phản hồi: trả 503 E-STR-0503, giữ nguyên dữ liệu đã nhập." }
]
},
"participants": [
{ "id": "U", "type": "external", "label": "Nhân viên", "sublabel": "ROLE-01" },
{ "id": "FE", "type": "frontend", "label": "Giao diện", "sublabel": "SCR-02 Chi tiết" },
{ "id": "BE", "type": "backend", "label": "Settlement API", "sublabel": "POST /stores" },
{ "id": "POS", "type": "external", "label": "Hệ thống POS", "sublabel": "IF-005 · timeout 3s" }
],
"segments": [
{ "from": 160, "to": 300, "label": "Gửi và kiểm tra" },
{ "from": 310, "to": 400, "label": "Nhánh thành công" },
{ "from": 410, "to": 520, "label": "Nhánh POS timeout" }
],
"messages": [
{ "id": "m1", "from": "U", "to": "FE", "y": 175, "label": "Bấm Lưu", "variant": "default" },
{ "id": "m2", "from": "FE", "to": "BE", "y": 210, "label": "POST /stores", "variant": "emphasis" },
{ "id": "m3", "from": "BE", "to": "POS", "y": 250, "label": "GET /verify (timeout 3s)", "variant": "emphasis" },
{ "id": "m4", "from": "POS", "to": "BE", "y": 320, "label": "200 OK", "variant": "return" },
{ "id": "m5", "from": "BE", "to": "FE", "y": 350, "label": "200 + id", "variant": "return" },
{ "id": "m6", "from": "FE", "to": "U", "y": 380, "label": "Toast \"Đã lưu\"", "variant": "return" },
{ "id": "m7", "from": "POS", "to": "BE", "y": 430, "label": "timeout sau 3s", "variant": "dashed", "note": "FAIL FM-05" },
{ "id": "m8", "from": "BE", "to": "FE", "y": 465, "label": "503 · E-STR-0503", "variant": "return" },
{ "id": "m9", "from": "FE", "to": "U", "y": 500, "label": "Báo lỗi, GIỮ dữ liệu đã nhập", "variant": "return" }
],
"activations": [
{ "participant": "FE", "from": 170, "to": 505, "type": "frontend" },
{ "participant": "BE", "from": 205, "to": 470, "type": "backend" },
{ "participant": "POS", "from": 245, "to": 435, "type": "external" }
],
"cards": [
{ "dot": "emerald", "title": "Thành công", "items": ["POS trả 200 trong 3s, API lưu và trả id", "Giao diện hiện toast theo UICONV §7"] },
{ "dot": "rose", "title": "Lỗi", "items": ["Timeout 3s là ngân sách theo FAIL §2, không tự đặt", "503 E-STR-0503 cho thử lại, dữ liệu đã nhập không mất (W4)"] }
]
}

View File

@@ -0,0 +1,157 @@
# DOM — Domain Model — <PROJECT>
| | |
|---|---|
| **Version** | 1.0 |
| **Date** | YYYY-MM-DD |
| **Author** | <SA> (skill sa-2-architecture) |
| **Status** | 🟡 Draft |
| **Approved by** | Tech Lead: — · BE Lead: — |
| **Source** | SAD_… v1.0 · DAT_… v1.0 · BR_… của BA · BACKLOG_… của BA |
| **Scope** | |
| **Confidence** | 🟡 |
## Change Log
| Version | Date | Người sửa | Thay đổi | ADR |
|---|---|---|---|---|
| 1.0 | | | Bản đầu | — |
> **Thẩm quyền khi mâu thuẫn:** sơ đồ thắng về **quan hệ và luồng**; bảng thắng về **ràng buộc và
> con số**. `BR` của BA là **nguồn** của mọi invariant ở đây; `DOM` chỉ nói invariant đó sống ở
> class nào. Lệch với `BR` ⇒ `OQ`, không tự sửa rule.
> **Vì sao có tài liệu này:** `DAT` nói *ai sở hữu dữ liệu gì*, `PDM` nói *bảng vật lý trông thế
> nào*. Không có `DOM`, dev tự suy ra aggregate từ bảng — và mỗi người suy một kiểu, transaction
> boundary lệch nhau, invariant bị kiểm ở ba chỗ hoặc không chỗ nào.
---
## 1. Bounded context và aggregate
*Một dòng mỗi aggregate. Aggregate là **ranh giới transaction**: mọi invariant trong aggregate được
kiểm trong một lần ghi; giữa các aggregate chỉ có eventual consistency (`DAT` §4).*
| Bounded context | `CMP-nn` sở hữu | Aggregate root | Entity con | Value object | Invariant chính (`BR`) | Sự kiện phát ra |
|---|---|---|---|---|---|---|
| Cart & Order | `CMP-04` | `Order` | `OrderSeller`, `OrderItem` | `Money`, `Address` | `BR-014` tổng tiền = Σ dòng | `OrderPlaced` |
| | | | | | | |
🔴 **Một entity thuộc đúng một aggregate.** Entity xuất hiện ở hai aggregate ⇒ một trong hai chỉ
được giữ **id tham chiếu** (không giữ object), ghi rõ ở §3.
---
## 2. Class diagram theo bounded context
*Một sơ đồ mỗi bounded context, ≤ 12 class. Chỉ vẽ thuộc tính nghiệp vụ và phương thức đổi trạng
thái; getter/setter, DTO, repository không vẽ ở đây (thuộc `AGD`).*
### 2.1 <Bounded context>
<!-- archify: mermaid-only -->
```mermaid
classDiagram
class Order {
+OrderId id
+CustomerId customerId
+Money totalAmount
+OrderStatus status
+place(cart) Order
+cancel(reason)
}
class OrderSeller {
+SellerId sellerId
+Money subtotal
+confirm()
}
class OrderItem {
+ProductVariantId variantId
+int quantity
+Money unitPriceSnapshot
}
class Money {
<<value object>>
+decimal amount
+Currency currency
}
Order "1" *-- "1..*" OrderSeller : splits into
OrderSeller "1" *-- "1..*" OrderItem : contains
Order ..> Money
OrderItem ..> Money
```
**Bảng đi kèm** *(W13 — sơ đồ không nói được invariant kiểm ở đâu và ai được gọi)*:
| Class | Loại | Thuộc tính nghiệp vụ | Invariant (`BR`) | Kiểm ở phương thức | Ai được gọi (`ROLE`/`CMP`) | Bảng `PDM` |
|---|---|---|---|---|---|---|
| `Order` | aggregate root | `totalAmount`, `status` | `BR-014`, `BR-021` | `place()`, `cancel()` | `ROLE-01` qua `IF-002` | `TBL-012 order` |
| `Money` | value object | `amount`, `currency` | `BR-003` không âm | constructor | — | cột `*_amount` |
**Quan hệ:** `*--` composition (cùng transaction) · `o--` aggregation (khác vòng đời) · `..>` phụ
thuộc · `-->` tham chiếu bằng id sang aggregate khác.
### 2.2 <Bounded context tiếp theo>
*(cùng cấu trúc)*
---
## 3. Tham chiếu xuyên aggregate
*Chỗ hay sai nhất: giữ object của aggregate khác thay vì id, rồi load cả cây.*
| Từ aggregate | Tham chiếu tới | Bằng gì | Đọc dữ liệu kia bằng | Trễ chấp nhận (`DAT` §4) |
|---|---|---|---|---|
| `Order` | `Customer` | `CustomerId` | `IF-0nn` / read model | eventual ≤ … |
| `OrderItem` | `ProductVariant` | `ProductVariantId` + snapshot giá/tên | snapshot lúc đặt | không cần đồng bộ |
🔴 **Snapshot hay tham chiếu sống?** Giá, tên, địa chỉ lúc giao dịch phải là **snapshot** (thay đổi
sau đó không được đổi lịch sử). Ghi rõ từng trường, đây là nguồn của nhiều bug đối soát.
---
## 4. Vòng đời trạng thái → class
*Bảng chuyển trạng thái nằm ở `BR` §3; đây chỉ map xuống class và phương thức.*
| Entity | Enum trạng thái | Thuộc `BR` | Phương thức chuyển | Guard kiểm ở đâu | Ghi vết (`SEC` §audit) |
|---|---|---|---|---|---|
| `Order` | `OrderStatus` | `BR-021` | `confirm()`, `cancel()` | trong aggregate | `order_status_history` |
---
## 5. Domain event
| Sự kiện | Phát từ aggregate | Khi nào | Payload tối thiểu | Người nhận (`CMP`) | Contract (`CTR`) |
|---|---|---|---|---|---|
| `OrderPlaced` | `Order` | `place()` commit xong | `orderId`, `customerId`, `sellerIds[]`, `totalAmount` | `CMP-06`, `CMP-09` | `contracts/asyncapi/order-events.yaml#OrderPlaced` |
🔴 Payload event **không chứa PII** ngoài id trừ khi `SEC` §5 cho phép đích danh.
---
## 6. Đối chiếu
| Kiểm | Kết quả | Hành động |
|---|---|---|
| Mọi thực thể trong `DAT` §1 có class ở §2 | n/n | |
| Mọi `BR` loại "ràng buộc dữ liệu"/"tính toán" có class kiểm | n/n | |
| Mọi entity có trạng thái ở `BR` §3 có enum + phương thức ở §4 | n/n | |
| Mọi class root có bảng trong `PDM` §2 | n/n | *(điền khi `PDM` xong)* |
## 7. Giả định & Ngoài phạm vi
**Giả định:**
| ID | Giả định | Cách xác minh | Nếu sai |
|---|---|---|---|
**Ngoài phạm vi:**
- *(ví dụ: không mô hình hoá module báo cáo — chỉ đọc, dùng read model của `DAT` §4)*
## 8. Open Questions
| ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì | Hệ quả nếu trả lời ngược |
|---|---|---|---|---|---|

View File

@@ -48,6 +48,7 @@ quả đo `QAS` không?** Đo hiệu năng trên máy nhỏ hơn 4 lần rồi k
## 3. Sơ đồ triển khai
<!-- archify: architecture · diagrams/INF_deployment.architecture.json -->
```mermaid
flowchart TB
```

View File

@@ -60,6 +60,11 @@ diện không hiện lỗi. Chốt 4xx ngay ở đây.
🔴 **Interface không biết ai sở hữu là interface sẽ bị đổi mà không ai báo.** Cột này không
được để trống, kể cả với hệ thống nội bộ.
🔴 **Cột "Contract ở đâu" phải trỏ tới file thật** trong `02-architecture/contracts/` do `CTR`
(`--focus ctr`) sinh: `contracts/openapi/<module>.yaml#<operationId>` hoặc
`contracts/asyncapi/<domain>.yaml#<channel>`. Ghi "dự kiến" ⇒ `OQ` + `Confidence` 🔴 và AG2 chặn.
Interface đối tác chưa có tài liệu ⇒ ghi `ARISK`, không tự viết contract thay đối tác.
### 2.1 Interface với hệ thống ngoài
| ID | Hệ thống | Ai liên hệ được | **SLA của họ** | Giới hạn tốc độ | Cơ chế xác thực | Môi trường thử | Đã gọi thử chưa |
@@ -87,8 +92,8 @@ diện không hiện lỗi. Chốt 4xx ngay ở đây.
**Contract**
Nguồn sự thật: `<đường dẫn file OpenAPI/AsyncAPI/proto>` — **không chép nội dung contract vào
đây**, chỉ ghi những điểm cần chú ý:
Nguồn sự thật: `contracts/openapi/<module>.yaml` · `operationId: <…>` (xem `CTR` §2) — **không
chép nội dung contract vào đây**, chỉ ghi những điểm cần chú ý:
| Điểm cần chú ý | Quyết định |
|---|---|

View File

@@ -0,0 +1,180 @@
# PDM — Physical Data Model — <PROJECT>
| | |
|---|---|
| **Version** | 1.0 |
| **Date** | YYYY-MM-DD |
| **Author** | <SA> (skill sa-2-architecture) |
| **Status** | 🟡 Draft |
| **Approved by** | Tech Lead: — · DBA: — · BE Lead: — |
| **Source** | DAT_… v1.0 · DOM_… v1.0 · SRS_… §2.3.3 (bảng field) · BR_… của BA · SEC_… §5 |
| **Scope** | *(kho nào — một PDM mỗi kho vật lý, hoặc một file nhiều §2.x)* |
| **Confidence** | 🟡 |
## Change Log
| Version | Date | Người sửa | Thay đổi | ADR / MIG |
|---|---|---|---|---|
| 1.0 | | | Bản đầu | `MIG-001` |
> **Thẩm quyền:** file DDL trong `02-architecture/schema/` **thắng** bảng ở đây khi lệch — bảng là
> bản đọc cho người, DDL là thứ chạy. Lệch ⇒ lỗi tài liệu, sửa bảng.
>
> **Vì sao có tài liệu này:** `DAT` dừng ở "thực thể, quan hệ, ai sở hữu". Không có `PDM`, mỗi dev
> tự đặt kiểu cột, độ dài, nullable, index — và bảng field của BA (`FLD-…`) chặn ở tầng UI nhưng
> không chặn ở tầng DB, hoặc ngược lại.
---
## 1. Kho và quy ước
| | |
|---|---|
| **Kho** | *(tên · loại · phiên bản, ví dụ PostgreSQL 16 · RDS)* — quyết định ở `DAT` §3 / `ADR-nnn` |
| **Schema / namespace** | *(schema-per-module theo `ADR-nnn`: `identity`, `catalog`, `cart_order`…)* |
| **Công cụ migration** | *(Flyway / Liquibase / Prisma Migrate / Alembic / golang-migrate…)* — `ADR-nnn` |
| **Đặt tên** | bảng `snake_case` số ít · PK `id` · FK `<bảng>_id` · index `ix_<bảng>_<cột>` · unique `ux_…` · FK constraint `fk_<bảng>_<cột>` |
| **Kiểu id** | `uuid v7` / `bigint identity` — `ADR-nnn`; **truyền qua API dạng string** (`ICD` §1) |
| **Thời gian** | `timestamptz`, lưu UTC; cột `created_at`, `updated_at` bắt buộc mọi bảng |
| **Tiền** | `numeric(18,2)` + cột `currency char(3)` hoặc value object cố định VND — theo `DOM` §2 `Money` |
| **Xoá** | soft delete (`deleted_at`) cho bảng có yêu cầu retention ở `DAT` §5; hard delete cho bảng còn lại — ghi từng bảng ở §2 |
| **Ký tự** | `UTF-8`; độ dài `varchar(n)` tính theo **code point**, khớp bảng field của SRS (W3) |
---
## 2. Bảng — `TBL-nnn`
*Một mục mỗi bảng. Mỗi bảng phải truy về một thực thể `DAT` §1 và một class `DOM` §2; bảng không
truy được là bảng thừa hoặc thiếu mô hình.*
### 2.1 `TBL-001` · `cart_order.order` ← `Order` (`DAT` §1.3 · `DOM` §2.1)
| | |
|---|---|
| **Chủ sở hữu** | `CMP-04` — chỉ component này được ghi (`DAT` §1) |
| **Ước lượng** | … bản ghi/năm 1 · … năm 3 (`DAT` §2.2) · tăng …/tháng |
| **Xoá** | soft delete · retention … năm (`DAT` §5) |
| **Phân mảnh** | không / theo `placed_at` tháng (`DAT` §7.4 · `ADR-nnn`) |
**Cột**
| Cột | Kiểu | Null | Default | Ràng buộc | PII (`SEC` §5) | Nguồn (`FLD`/`BR`) | Ghi chú |
|---|---|---|---|---|---|---|---|
| `id` | `uuid` | ✗ | `gen_uuid_v7()` | PK | — | — | |
| `order_number` | `varchar(20)` | ✗ | — | `ux_order_order_number` | — | `BR-010` định dạng | khoá nghiệp vụ |
| `customer_id` | `uuid` | ✓ | `NULL` | *(không FK vật lý — khác schema, `DAT` §1)* | 🔶 gián tiếp | `FLD-SCR04-01` | null = Guest |
| `total_amount` | `numeric(18,2)` | ✗ | — | `CHECK (total_amount >= 0)` | — | `BR-014` | = Σ `order_seller.subtotal_amount` |
| `status` | `varchar(32)` | ✗ | `'pending'` | `CHECK (status IN (…))` theo `BR` §3 | — | `BR-021` | enum ở tầng app, CHECK ở DB |
| `idempotency_key` | `varchar(64)` | ✗ | — | `ux_order_idempotency_key` | — | `ADR-004` | khoá của `POST /v1/checkout` |
| `placed_at` | `timestamptz` | ✗ | `now()` | | — | | |
| `created_at` / `updated_at` | `timestamptz` | ✗ | `now()` | | — | quy ước §1 | |
| `deleted_at` | `timestamptz` | ✓ | `NULL` | | — | quy ước §1 | soft delete |
🔴 **Bốn ô không được trống ở bất kỳ cột nào**: kiểu có độ dài/độ chính xác · Null · Default ·
Nguồn. Trùng nguyên tắc "bảng ràng buộc cụ thể tới mức không cần hỏi lại" của SRS PART 2.
**Index và khoá ngoại**
| Tên | Loại | Cột | Vì sao (`QAS`/truy vấn `DAT` §7.1) | Kích cỡ ước lượng |
|---|---|---|---|---|
| `ix_order_customer_placed` | btree | `(customer_id, placed_at DESC)` | lịch sử đơn · `QAS-006` | |
| `fk_order_seller_order` | FK | `order_seller.order_id → order.id` | `ON DELETE RESTRICT` | |
🔴 Index không truy về truy vấn nào ⇒ bỏ. Truy vấn trong `DAT` §7.1 không có index ⇒ thiếu.
### 2.2 `TBL-002` · `<schema>.<bảng>` ← `<Entity>`
*(cùng cấu trúc)*
---
## 3. Ánh xạ thực thể → bảng
*Bảng đối chiếu bắt buộc, `sa-conformance` đọc bảng này.*
| Thực thể (`DAT` §1) | Class (`DOM` §2) | Bảng (`TBL`) | Quan hệ 1 thực thể → n bảng? | Ghi chú |
|---|---|---|---|---|
| `Order` | `Order` | `TBL-001` | 1→1 | |
| `Order` × `Seller` | `OrderSeller` | `TBL-002` | tách theo `ADR-003` | |
| `Cart` (Redis) | `Cart` | *(không phải bảng — key schema §5)* | | |
Thực thể không có dòng ⇒ 🔴 chặn AG2. Bảng không có thực thể ⇒ bảng thừa hoặc thiếu mô hình.
---
## 4. DDL và migration — `MIG-nnn`
**Vị trí:** `sa-output/<PROJECT>/02-architecture/schema/<kho>/`
```
schema/<kho>/
├── V001__init_<schema>.sql ← MIG-001: toàn bộ bảng §2, chạy được từ CSDL rỗng
├── V001__init_<schema>.down.sql ← rollback tương ứng (bắt buộc)
├── V002__<thay-doi>.sql ← mỗi thay đổi sau baseline một cặp up/down
├── V002__<thay-doi>.down.sql
└── seed/
└── R__reference_data.sql ← dữ liệu tham chiếu (enum bảng, cấu hình) — idempotent
```
| `MIG` | File | Nội dung | Rollback | Dữ liệu phát sinh trong lúc chạy | Thời lượng ước lượng | Chạy trên |
|---|---|---|---|---|---|---|
| `MIG-001` | `V001__init_cart_order.sql` | tạo `TBL-001..0nn` | `V001__….down.sql` drop ngược thứ tự FK | không (CSDL rỗng) | < 1 phút | dev/stg/prod |
| `MIG-002` | | | | | | |
🔴 **Migration không có down là migration một chiều** (`DAT` §8). Với migration đổi dữ liệu
(không chỉ DDL) phải ghi thêm: cách đối chiếu sau khi chạy, ngưỡng chênh lệch chấp nhận, ai duyệt.
**Quy ước file SQL:** một câu lệnh một dòng logic · comment đầu file ghi `MIG-nnn`, `TBL` liên quan,
`ADR` · không dùng lệnh phụ thuộc quyền superuser · `CREATE … IF NOT EXISTS` chỉ trong seed.
**Kiểm bằng máy** *(người điều phối chạy, ghi kết quả vào đây)*:
| Kiểm | Lệnh | Kết quả |
|---|---|---|
| Up từ rỗng | `<tool> migrate` trên container CSDL rỗng | ☐ |
| Down toàn bộ rồi up lại | `<tool> migrate down --all && <tool> migrate` | ☐ |
| Mọi bảng §2 có trong DDL | `grep -c "CREATE TABLE" V001*.sql` = số `TBL` | ☐ |
| Mọi cột PII có cờ ở `SEC` §5 | đối chiếu cột 🔶/✅ PII với `SEC` §5 | ☐ |
---
## 5. Kho không quan hệ *(nếu có — cache, document, search)*
| Kho | Key / collection | Cấu trúc value | TTL | Invalidation (`DAT` §7.4) | Kích cỡ ước lượng |
|---|---|---|---|---|---|
| Redis `CMP-15` | `cart:{sessionId}` | JSON `CartSnapshot` (`DOM` §2) | 7 ngày | write-through khi `PATCH`/`DELETE` | ≤ 50 dòng × … |
---
## 6. Dữ liệu tham chiếu và seed
| Bảng | Nguồn dữ liệu | Ai duy trì | Cách nạp | Idempotent |
|---|---|---|---|---|
---
## 7. Đối chiếu
| Kiểm | Kết quả | Hành động |
|---|---|---|
| `DAT` thực thể → `TBL` (§3) | n/n | |
| `FLD-*` trong SRS PART 2 → cột (kiểu/độ dài/null khớp) | n/n | lệch ⇒ `OQ`, BA cập nhật SRS hoặc PDM sửa |
| Cột PII → `SEC` §5 có dòng | n/n | |
| Truy vấn `DAT` §7.1 → index | n/n | |
| Mọi `MIG` có down | n/n | |
## 8. Giả định & Ngoài phạm vi
**Giả định:**
| ID | Giả định | Cách xác minh | Nếu sai |
|---|---|---|---|
**Ngoài phạm vi:**
- *(ví dụ: không thiết kế kho phân tích/BI — ngoài `CON-…`)*
## 9. Open Questions
| ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì | Hệ quả nếu trả lời ngược |
|---|---|---|---|---|---|

View File

@@ -53,6 +53,7 @@ Tách service là quyết định phải *thắng* một lập luận, không ph
*Ai dùng hệ thống, hệ thống nói chuyện với cái gì bên ngoài. Không có chi tiết bên trong.*
<!-- archify: architecture · diagrams/SAD_c4-context.architecture.json -->
```mermaid
flowchart LR
```
@@ -70,6 +71,7 @@ flowchart LR
*Các khối chạy được và triển khai được (ứng dụng, CSDL, hàng đợi, job). Đây là sơ đồ dev dùng
nhiều nhất.*
<!-- archify: architecture · diagrams/SAD_c4-container.architecture.json -->
```mermaid
flowchart TB
```
@@ -107,6 +109,7 @@ Một loại thay đổi thường xuyên mà chạm ≥ 3 container ⇒ phân r
*Không vẽ mức này cho mọi container. Vẽ cho cái khó nhất, để dev có mẫu.*
<!-- archify: architecture · diagrams/SAD_c4-component.architecture.json -->
```mermaid
flowchart TB
```
@@ -120,6 +123,7 @@ timeout ở đâu, giao dịch bắt đầu và kết thúc ở đâu.*
### 6.1 <tên luồng>
<!-- archify: sequence · diagrams/SAD_luong-chinh.sequence.json -->
```mermaid
sequenceDiagram
```
@@ -133,6 +137,7 @@ sequenceDiagram
*Cái gì chạy ở đâu, mấy bản, ranh giới mạng, ranh giới tin cậy.*
<!-- archify: architecture · diagrams/SAD_deployment.architecture.json -->
```mermaid
flowchart TB
```

View File

@@ -26,6 +26,7 @@
*Nơi dữ liệu đi từ vùng ít tin cậy sang vùng tin cậy hơn. Mỗi ranh giới phải có kiểm tra đầu vào.*
<!-- archify: architecture · diagrams/SEC_ranh-gioi-tin-cay.architecture.json -->
```mermaid
flowchart LR
```