# Kiến trúc hệ thống và cấu trúc codebase
Ngày lập: 04/09/2026
Tài liệu bổ sung cho [`02-giai-phap-va-estimate.md`](./02-giai-phap-va-estimate.md).
> **Tài liệu này trả lời câu hỏi:** "Đồng ý là 15/22 tính năng chỉ cấu hình, 4 tính năng phải tự viết — nhưng cụ thể *hệ thống trông như thế nào*, *code nằm ở đâu*, *file nào làm gì*, và *mỗi tính năng được giải quyết bằng cách nào*?"
>
> Phạm vi: **MVP Phương án A**. Phần Standard/Full và Phương án B nằm ở §14–§15 để thấy codebase sẽ phình ra theo hướng nào.
>
> Tài liệu này là **thiết kế đề xuất**, không phải bản đã kiểm chứng chạy thật. Những chỗ phụ thuộc vào khả năng của AWS mà chưa xác minh trực tiếp trên account đều được đánh dấu **⚠️ cần xác minh ở M02/M03**.
---
## Mục lục
| § | Nội dung |
|---|---|
| 1 | Nguyên tắc kiến trúc — vì sao codebase phải nhỏ |
| 2 | Bức tranh tổng thể: 5 tầng và ranh giới trách nhiệm |
| 3 | Bốn điểm nối giữa Amazon Connect và code tự viết |
| 4 | Cấu trúc thư mục repository |
| 5 | Lựa chọn công nghệ và lý do |
| 6 | Mô hình dữ liệu DynamoDB |
| 7 | Hợp đồng API nội bộ |
| 8 | Thiết kế chi tiết từng tính năng MVP |
| 9 | Xử lý sự kiện, idempotency và lỗi |
| 10 | Bảo mật và phân quyền trong code |
| 11 | Môi trường, IaC và CI/CD |
| 12 | Chiến lược kiểm thử theo tầng |
| 13 | Giám sát, cảnh báo và runbook |
| 14 | Codebase sẽ thay đổi thế nào ở Standard/Full |
| 15 | Codebase của Phương án B khác gì |
| 16 | Ánh xạ WBS M01–M16 sang thư mục/artifact |
| 17 | Quy ước code |
| 18 | Checklist khởi tạo repo ngày đầu tiên |
---
## 1. Nguyên tắc kiến trúc — vì sao codebase phải nhỏ
Năm nguyên tắc dưới đây quyết định mọi lựa chọn kỹ thuật còn lại. Khi có tranh cãi trong lúc code, đây là thứ dùng để phân xử.
| # | Nguyên tắc | Nghĩa cụ thể khi viết code | Hệ quả nếu vi phạm |
|---:|---|---|---|
| 1 | **Native-first** | Trước khi viết một hàm, phải trả lời được: "Connect đã có sẵn cái này chưa?" Nếu có, dùng, kể cả khi giao diện xấu hơn | Mỗi dòng code tự viết là chi phí bảo trì vĩnh viễn, còn tính năng Connect được AWS nâng cấp miễn phí |
| 2 | **Code chỉ nằm ở rìa** | Code tự viết **không** đứng giữa luồng thoại. Nó chỉ *nghe* sự kiện sau khi Connect xong việc, hoặc *được gọi* từ một điểm mở rộng của Connect | Nếu Lambda nằm trên đường đi của cuộc gọi và nó chết, cuộc gọi chết theo |
| 3 | **Connect là nguồn sự thật của cuộc gọi, DynamoDB là nguồn sự thật của nghiệp vụ** | Không bao giờ chép lại số liệu cuộc gọi vào DynamoDB để "cho tiện báo cáo". Chỉ lưu `contactId` để nối | Hai nguồn số liệu lệch nhau là lỗi không thể debug ở call center |
| 4 | **Mọi thứ tự viết phải chịu được chạy lại** | Mọi handler đều idempotent theo một khóa tự nhiên. Sự kiện của AWS có thể đến hai lần, đến muộn, hoặc sai thứ tự | Kết quả cuộc gọi bị ghi đè, khách bị gọi lại sau khi đã nói "đừng gọi nữa" |
| 5 | **Cấu hình là code** | Contact flow, guide, queue, hours of operation đều nằm trong Git dưới dạng file, không phải "đã bấm trên console" | Không có Git thì không rollback được, và không ai biết prod đang chạy cấu hình nào |
**Hệ quả cho quy mô codebase MVP:** khoảng **8–12 PD code** (§7.1 của `02`) tương đương chừng **7 Lambda function nhỏ, 1 bảng DynamoDB, 1 CLI công cụ và bộ file cấu hình Connect**. Nếu trong lúc làm thấy codebase phình to hơn nhiều so với con số này, đó là tín hiệu đã vi phạm nguyên tắc 1 hoặc 2 — phải dừng lại rà soát, không phải tăng estimate.
---
## 2. Bức tranh tổng thể: 5 tầng và ranh giới trách nhiệm
### 2.1 Sơ đồ phân tầng
```mermaid
flowchart TB
subgraph L1["Tầng 1 — Người dùng"]
AG[Agent
trình duyệt Chrome/Edge]
SV[Supervisor]
AD[Admin]
end
subgraph L2["Tầng 2 — Amazon Connect ⚙️ KHÔNG viết code"]
AWS1[Agent Workspace + CCP
softphone WebRTC]
AWS2[Contact Flows
luồng gọi ra và gọi vào]
AWS3[Outbound Campaigns
quay số, AMD, retry, lịch]
AWS4[Customer Profiles + Segments]
AWS5[Conversational Analytics
transcript, sentiment ja_JP]
AWS6[Dashboard + Contact Search]
end
subgraph L3["Tầng 3 — Điểm nối 4 cửa"]
G1[Step-by-step Guide
form nhập kết quả]
G2[Invoke Lambda block
trong contact flow]
G3[EventBridge
contact + campaign events]
G4[S3 Event
file CSV, file recording]
end
subgraph L4["Tầng 4 — Code tự viết 💻 ~8-12 PD"]
F1[disposition-api]
F2[dnc-service]
F3[callback-scheduler]
F4[list-ingest]
F5[contact-event-sink]
F6[recording-audit]
F7[reconciliation]
end
subgraph L5["Tầng 5 — Lưu trữ và báo cáo"]
DDB[(DynamoDB
cc-core single table)]
S3R[(S3 recordings
+ analytics, KMS)]
S3D[(S3 data
CSV in/out, report)]
ATH[Athena + Connect data lake]
end
AG --> AWS1
SV --> AWS6
AD --> AWS2
AWS1 --> G1
AWS2 --> G2
AWS3 --> AWS2
AWS4 --> AWS3
AWS2 --> AWS5
G1 --> F1
G2 --> F2
G3 --> F5
G3 --> F3
G4 --> F4
F1 --> DDB
F2 --> DDB
F2 --> AWS4
F3 --> DDB
F4 --> AWS4
F4 --> S3D
F5 --> DDB
F6 --> S3R
F7 --> DDB
F7 --> S3D
AWS5 --> S3R
DDB --> ATH
S3R --> ATH
```
### 2.2 Đọc sơ đồ theo một câu
> **Tầng 2 là sản phẩm mua sẵn, tầng 4 là phần mềm của dự án, tầng 3 là bốn cái cửa duy nhất nối hai bên.** Mọi thiết kế sau này chỉ được đi qua bốn cửa đó. Nếu phát sinh nhu cầu nối theo cách thứ năm, đó là dấu hiệu thiết kế sai.
### 2.3 Bảng ranh giới trách nhiệm
| Câu hỏi | Ai trả lời | Ghi chú |
|---|---|---|
| Gọi số nào tiếp theo? | Outbound Campaigns ⚙️ | Không tự viết hàng đợi quay số |
| Có phải người thật bắt máy không? | Block `Check call progress` ⚙️ | Chỉ chạy được với cuộc gọi của campaign — xem `01` §3.2 |
| Cuộc gọi này ghi âm chưa? | Contact flow ⚙️ + S3 | Code chỉ *kiểm tra* file có tồn tại không, không tự ghi âm |
| Khách nói gì? | Conversational Analytics ⚙️ | Code không chạm vào audio |
| Kết quả bán hàng là gì? | **disposition-api 💻** | Connect không có khái niệm này |
| Số này có bị cấm gọi không? | **dnc-service 💻** + lọc segment | Campaign không có blocklist dựng sẵn |
| Đến giờ gọi lại chưa? | **callback-scheduler 💻** | Callback của Connect phục vụ khách gọi vào, không phải lịch hẹn của sales |
| Hôm qua gọi bao nhiêu, thiếu bao nhiêu file? | **reconciliation 💻** | Ghép 4 nguồn dữ liệu |
| Ai được nghe ghi âm? | Security profile của Connect ⚙️ | Code **không** tự làm phân quyền nghe |
### 2.4 Điều codebase cố tình **không** làm
Đây là danh sách phải nói rõ trong review nội bộ, vì đây là những chỗ dễ bị "tiện tay viết thêm":
- **Không** có backend API cho agent nói chuyện trực tiếp. Agent chỉ dùng Agent Workspace.
- **Không** có bản sao dữ liệu cuộc gọi (CDR) trong DynamoDB. Chỉ lưu con trỏ `contactId` + vài trường tối thiểu phục vụ đối soát.
- **Không** có bộ quay số, bộ điều tiết nhịp gọi, bộ retry. Đó là việc của Campaigns.
- **Không** có màn hình web nào ở MVP. Màn hình quản lý riêng là **S01, phase Standard**.
- **Không** đụng vào file audio hay transcript. Che PII là **X02**, workstream riêng.
---
## 3. Bốn điểm nối giữa Amazon Connect và code tự viết
Toàn bộ tích hợp gói gọn trong 4 cơ chế. Hiểu 4 cái này là hiểu toàn bộ kiến trúc.
### Cửa 1 — Step-by-step Guide gọi Lambda (đồng bộ, có mặt agent)
Dùng cho: **ghi kết quả cuộc gọi (F9)**, **bấm nút không gọi nữa (F10)**, **đặt hẹn gọi lại (F11)**.
```mermaid
sequenceDiagram
participant A as Agent
participant W as Agent Workspace
participant G as Step-by-step Guide
participant L as disposition-api Lambda
participant D as DynamoDB
A->>W: Kết thúc cuộc gọi, vào trạng thái ACW
W->>G: Hiển thị form kết quả
A->>G: Chọn kết quả + ghi chú + giờ hẹn
G->>L: Invoke với contactId và các trường
L->>D: Ghi disposition, DNC hoặc callback
L-->>G: OK hoặc thông báo lỗi tiếng Nhật
G-->>A: Xác nhận đã lưu
```
**Ràng buộc kỹ thuật phải tôn trọng khi viết code:**
| Ràng buộc | Con số | Hệ quả thiết kế |
|---|---|---|
| Timeout của block Invoke Lambda | 8 giây | Handler phải xong trong ~1s; mọi việc nặng đẩy sang async qua EventBridge |
| Kích thước payload trả về | ~32 KB | Không trả danh sách dài về guide |
| Kiểu dữ liệu trả về | Chuỗi phẳng, không lồng nhau | Lambda phải "làm phẳng" object trước khi return — có helper dùng chung trong `services/shared` |
| Lambda phải được associate với instance | Bắt buộc | Khai báo trong `infra`, không bấm tay |
> **⚠️ Cần xác minh ở M02:** khả năng và giới hạn cụ thể của step-by-step guide khi gọi Lambda trên account của khách (phiên bản Agent Workspace, quyền, cách bind guide vào ACW). Nếu guide không đáp ứng, phương án dự phòng là **Connect Task + form đơn giản**, chi phí tương đương, đã nằm trong range M08.
### Cửa 2 — Contact flow gọi Lambda (đồng bộ, trong lúc gọi)
Dùng cho: **kiểm tra DNC ngay trước khi nối agent** (lớp chặn thứ hai), và **lấy dữ liệu screen pop** nếu cần.
Nguyên tắc bắt buộc: **nhánh `Error` của block Invoke Lambda phải luôn được nối vào đường đi tiếp an toàn**, không được để cuộc gọi rơi. Lambda chết thì cuộc gọi vẫn phải nối được cho agent — trừ đúng một trường hợp: kiểm tra DNC, nơi lỗi được xử lý theo hướng "an toàn là ngắt máy", vì gọi nhầm một số đã cấm là rủi ro pháp lý, còn bỏ lỡ một cuộc gọi thì không.
### Cửa 3 — EventBridge (bất đồng bộ, sau khi việc đã xong)
Dùng cho: **đối soát dữ liệu (F21)**, **cảnh báo (F20)**, **kiểm tra thiếu file ghi âm**, **đánh thức lịch gọi lại**.
| Nguồn sự kiện | Dùng để làm gì | Service tiêu thụ |
|---|---|---|
| Amazon Connect Contact Events | Ghi mốc thời gian cuộc gọi, trạng thái kết thúc | `contact-event-sink` |
| Campaign events / dữ liệu campaign | Đếm số lượt quay số, disposition kỹ thuật | `contact-event-sink`, `reconciliation` |
| Contact Lens rules | Gắn cờ cuộc gọi cần QA, cảnh báo từ khóa | `contact-event-sink` |
| EventBridge Scheduler theo giờ | Chạy đối soát ngày, quét lịch gọi lại đến hạn | `reconciliation`, `callback-scheduler` |
> **⚠️ Cần xác minh ở M02/M03:** danh sách trường chính xác của từng loại event, và loại nào bắn qua EventBridge, loại nào chỉ có trong data lake. Đây là việc bắt buộc phải làm sớm vì nó quyết định `reconciliation` lấy số liệu từ đâu.
### Cửa 4 — S3 event (bất đồng bộ, theo file)
Dùng cho: **nạp danh sách khách hàng (F2)**, **kiểm tra file ghi âm và file phân tích (F20)**.
```
s3://cc-data-/inbound/lists//.csv → kích hoạt list-ingest
s3://cc-data-/outbound/reports//... → báo cáo dòng lỗi trả cho quản lý
s3://cc-recordings-/connect//CallRecordings/... → do Connect ghi, code chỉ đọc metadata
```
---
## 4. Cấu trúc thư mục repository
Một repo duy nhất (monorepo). Lý do: khối lượng code nhỏ, cấu hình Connect và code phải deploy đồng bộ với nhau, và đội chỉ 2–3 người.
```text
callcenter-connect/
├─ README.md # cách chạy, cách deploy, sơ đồ 1 trang
├─ package.json # npm workspaces
├─ tsconfig.base.json
├─ .env.example
│
├─ infra/ # ⚙️ hạ tầng dạng code — AWS CDK TypeScript
│ ├─ bin/app.ts # điểm vào, chọn env
│ ├─ lib/
│ │ ├─ stacks/
│ │ │ ├─ foundation-stack.ts # KMS key, S3 bucket, log group, budget, tag
│ │ │ ├─ connect-core-stack.ts # hours of operation, queue, routing profile, security profile, user
│ │ │ ├─ connect-flows-stack.ts # nạp file trong connect/flows và connect/guides
│ │ │ ├─ data-stack.ts # bảng DynamoDB, Customer Profiles domain
│ │ │ ├─ app-stack.ts # 7 Lambda + EventBridge rule + Scheduler
│ │ │ ├─ analytics-stack.ts # Glue/Athena, job export dài hạn
│ │ │ └─ observability-stack.ts # CloudWatch alarm, dashboard, SNS topic
│ │ ├─ constructs/
│ │ │ └─ nodejs-function.ts # wrapper chuẩn: runtime, log retention, powertools, tracing
│ │ └─ config/
│ │ ├─ common.ts # hằng số dùng chung: mã kết quả, khung giờ gọi
│ │ ├─ dev.ts
│ │ └─ prod.ts
│ └─ test/ # snapshot test của CloudFormation template
│
├─ connect/ # ⚙️ cấu hình Amazon Connect — NGUỒN SỰ THẬT nằm ở đây, không phải trên console
│ ├─ flows/
│ │ ├─ outbound-campaign.flow.json # luồng cuộc gọi ra: AMD → ghi âm → analytics → queue
│ │ ├─ inbound-main.flow.json # luồng gọi vào trong giờ
│ │ ├─ inbound-afterhours.flow.json # ngoài giờ: thông báo + lời nhắn
│ │ ├─ agent-whisper.flow.json # câu nhắc agent trước khi nối
│ │ └─ error-handler.flow.json # nhánh lỗi dùng chung
│ ├─ guides/
│ │ └─ disposition-guide.flow.json # form nhập kết quả trên Agent Workspace
│ ├─ rules/
│ │ ├─ negative-sentiment.rule.json # gắn cờ cuộc cần QA
│ │ └─ forbidden-keyword.rule.json
│ ├─ vocabulary/
│ │ └─ ja-JP.txt # tên sản phẩm, tên thương hiệu — KHÔNG chứa dữ liệu cá nhân
│ ├─ prompts/ # file âm thanh tiếng Nhật
│ ├─ hours-of-operation.json
│ └─ MANUAL-CHECKLIST.md # ⚠️ những gì KHÔNG tự động hoá được, phải bấm tay
│
├─ services/ # 💻 toàn bộ code tự viết của MVP
│ ├─ shared/ # thư viện dùng chung
│ │ ├─ src/
│ │ │ ├─ ddb.ts # client + helper single-table
│ │ │ ├─ logger.ts # log có cấu trúc, TỰ ĐỘNG che số điện thoại
│ │ │ ├─ phone.ts # chuẩn hoá E.164 cho số Nhật
│ │ │ ├─ jst.ts # mọi thứ liên quan giờ Nhật, ngày nghỉ
│ │ │ ├─ dispositions.ts # bộ mã kết quả, nguồn duy nhất
│ │ │ ├─ connect-response.ts # làm phẳng object để trả về contact flow
│ │ │ ├─ idempotency.ts
│ │ │ └─ errors.ts
│ │ └─ test/
│ ├─ disposition-api/ # F9 — ghi kết quả cuộc gọi
│ ├─ dnc-service/ # F10 — danh sách không gọi
│ ├─ callback-scheduler/ # F11 — hẹn gọi lại
│ ├─ list-ingest/ # F2 — nạp và kiểm tra danh sách CSV
│ ├─ contact-event-sink/ # nền cho F20/F21 — thu sự kiện cuộc gọi
│ ├─ recording-audit/ # F20 — dò cuộc gọi thiếu ghi âm hoặc thiếu text
│ └─ reconciliation/ # F21 — báo cáo đối soát hằng ngày
│
├─ tools/
│ ├─ csv-preflight/ # CLI cho quản lý: kiểm tra file TRƯỚC khi tải lên
│ └─ flow-sync/ # kéo flow từ console về file, phục vụ khi buộc phải sửa tay
│
├─ tests/
│ ├─ integration/ # chạy thật trên môi trường dev
│ └─ fixtures/
│ ├─ lists/ # CSV mẫu: sạch, bẩn, trùng, sai định dạng
│ └─ events/ # payload EventBridge mẫu
│
└─ docs/
├─ adr/ # quyết định kiến trúc, mỗi quyết định 1 file
├─ runbooks/ # mỗi cảnh báo 1 file hướng dẫn xử lý
└─ ja/ # tài liệu bàn giao tiếng Nhật — M13
```
### Cấu trúc bên trong một service (áp dụng cho cả 7)
```text
services/disposition-api/
├─ package.json
├─ src/
│ ├─ handler.ts # điểm vào Lambda — chỉ parse, gọi, format. KHÔNG chứa nghiệp vụ
│ ├─ schema.ts # zod schema cho input/output
│ ├─ service.ts # toàn bộ nghiệp vụ nằm ở đây, không phụ thuộc AWS
│ └─ repository.ts # truy cập DynamoDB
└─ test/
├─ service.test.ts # test nghiệp vụ, không cần AWS
└─ handler.test.ts
```
Quy tắc: **`service.ts` không được import SDK của AWS.** Nhờ vậy phần nghiệp vụ khó nhất — thứ tự ghi DNC, chống trùng lịch hẹn, quy tắc giờ Nhật — test được bằng unit test chạy trong vài giây, không cần môi trường AWS. Đây là lý do chính khiến M08 (5–7 PD) khả thi.
---
## 5. Lựa chọn công nghệ và lý do
| Hạng mục | Chọn | Vì sao | Phương án loại bỏ |
|---|---|---|---|
| Ngôn ngữ | **TypeScript** | Payload của Connect là JSON, kiểu dữ liệu khai báo được; đội cloud/backend phổ biến kỹ năng này | Python cũng được, nhưng chia sẻ kiểu dữ liệu với S01 phase sau sẽ mất |
| Runtime | **Node.js 22 trên Lambda** | Khởi động nhanh, đủ cho ràng buộc 8 giây | Container: thừa cho khối lượng này |
| IaC | **AWS CDK** | Sinh được cả tài nguyên Connect lẫn Lambda trong một cây phụ thuộc | Terraform hợp nếu khách đã chuẩn hoá Terraform — hỏi ở câu 20 §13 của `02` |
| Cơ sở dữ liệu | **DynamoDB một bảng** | Truy vấn đều theo khóa đã biết trước; không cần join; tính tiền theo lượt dùng | RDS: thừa, thêm VPC, thêm chi phí cố định |
| Xác thực dữ liệu vào | **zod** | Một schema dùng cho cả validate và sinh kiểu | Viết tay: dễ lệch giữa validate và type |
| Log/metric | **AWS Lambda Powertools** | Có sẵn log có cấu trúc, metric, tracing, idempotency | Tự viết: tốn PD không cần thiết |
| Kiểm thử | **Vitest** + snapshot CDK | Nhanh, đủ dùng | |
| CI/CD | **GitHub Actions** ⚠️ | Cần chốt theo hạ tầng của khách | |
**Ước lượng khối lượng code MVP:** khoảng **2.500–4.000 dòng TypeScript** kể cả test, cộng khoảng 8 file JSON cấu hình Connect. Con số này để đội tự kiểm chứng estimate M08 khi làm — nếu đến giữa dự án đã vượt 8.000 dòng thì phạm vi đã trôi.
---
## 6. Mô hình dữ liệu DynamoDB
### 6.1 Vì sao một bảng
Toàn bộ truy vấn của MVP đều là "tra theo khóa đã biết": theo `contactId`, theo số điện thoại, theo ngày đến hạn. Không có truy vấn tự do. Một bảng `cc-core-` với 2 chỉ mục phụ là đủ, và tránh được việc phải quản lý 6 bảng rời rạc.
### 6.2 Bảng `cc-core-`
Khóa chính: `PK` (partition) + `SK` (sort). Hai chỉ mục toàn cục: `GSI1`, `GSI2`.
| Thực thể | PK | SK | GSI1PK / GSI1SK | Trường chính | TTL |
|---|---|---|---|---|:--:|
| **Hồ sơ khách** | `CUST#` | `PROFILE` | `PHONE#` / `CUST#` | tên, timezone, nguồn đồng ý, thời điểm đồng ý, cờ DNC, nguồn danh sách | — |
| **Mục DNC** | `DNC#` | `ENTRY` | `DNCDATE#` / `` | lý do, `contactId` gốc, người tạo, thời điểm, đã đồng bộ sang Profiles chưa | — |
| **Kết quả cuộc gọi** | `CONTACT#` | `DISPOSITION` | `AGENT#` / `` | mã kết quả, ghi chú, `campaignId`, `externalCustomerId`, người nhập, phiên bản | — |
| **Bản ghi cuộc gọi tối giản** | `CONTACT#` | `META` | `DAY#` / `` | các mốc thời gian, disposition kỹ thuật, `campaignId`, agent, có ghi âm, có transcript | 400 ngày |
| **Lịch gọi lại** | `CB#` | `META` | `DUE#` / `#` | số điện thoại, `externalCustomerId`, giờ hẹn theo JST, trạng thái, agent yêu cầu, `contactId` gốc | — |
| **Khoá chống trùng lịch hẹn** | `CBIDX#` | `SLOT#` | — | `callbackId` đang giữ chỗ | 90 ngày |
| **Đối soát ngày** | `RECON#` | `SUMMARY` | — | đã gọi, kết nối, có ghi âm, có text, đã nhập kết quả, danh sách lệch | — |
| **Nhật ký thao tác** | `AUDIT#` | `#` | `ACTOR#` / `` | hành động, đối tượng, giá trị cũ/mới | 2.555 ngày |
| **Lô nạp danh sách** | `IMPORT#` | `META` | `IMPDATE#` / `` | tên file, tổng dòng, hợp lệ, lỗi, bị loại do DNC, đường dẫn báo cáo | — |
### 6.3 Các mẫu truy vấn được hỗ trợ
| Câu hỏi nghiệp vụ | Truy vấn |
|---|---|
| Cuộc gọi này agent nhập kết quả gì? | `get(PK=CONTACT#, SK=DISPOSITION)` |
| Số này có bị cấm gọi không? | `get(PK=DNC#, SK=ENTRY)` |
| Hôm nay có lịch hẹn nào đến hạn? | `query(GSI1, GSI1PK=DUE#)` |
| Agent A hôm nay nhập bao nhiêu kết quả? | `query(GSI1, GSI1PK=AGENT#A, GSI1SK between ...)` |
| Ngày hôm qua có cuộc nào thiếu ghi âm? | `query(GSI1, GSI1PK=DAY#)` rồi lọc |
| Số điện thoại này là khách nào? | `query(GSI1, GSI1PK=PHONE#)` |
| Ai đã bấm "không gọi nữa" cho số này? | `get(PK=DNC#)` + nhật ký |
### 6.4 Ba quyết định dữ liệu cần giải thích cho khách
| Quyết định | Vì sao |
|---|---|
| **Không lưu tên và số điện thoại trong bản ghi cuộc gọi tối giản** | Bản ghi này chỉ phục vụ đối soát. Không có dữ liệu cá nhân trong đó thì rủi ro rò rỉ giảm hẳn, và TTL 400 ngày xoá sạch tự động |
| **DNC dùng số điện thoại làm khóa, không dùng mã khách** | Một người có thể xuất hiện nhiều lần trong nhiều danh sách với mã khác nhau. Chặn theo số mới là chặn thật |
| **Kết quả cuộc gọi khóa theo `contactId`, không theo khách** | Một khách gọi 3 lần thì có 3 kết quả, đọc được lịch sử. Nếu khóa theo khách thì lần sau ghi đè lần trước |
---
## 7. Hợp đồng API nội bộ
Không có API Gateway ở MVP. Lambda được gọi trực tiếp từ Connect. Nhưng vẫn cần hợp đồng viết ra giấy, vì đây là ranh giới giữa hai đội (người cấu hình Connect và người viết code).
### 7.1 `disposition-api` — được gọi từ Step-by-step Guide
**Vào:**
```json
{
"action": "SAVE_DISPOSITION",
"contactId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"agentId": "agent-07",
"externalCustomerId": "CUST-000123",
"phoneNumber": "+81312345678",
"campaignId": "camp-2026-10-a",
"outcome": "CALLBACK",
"note": "資料送付後に再連絡希望",
"callbackAtJst": "2026-10-09T14:00",
"dncReason": null
}
```
**Ra — phẳng, mọi giá trị là chuỗi, theo ràng buộc của Connect:**
```json
{
"status": "OK",
"dispositionSaved": "true",
"callbackId": "cb-01JB...",
"dncApplied": "false",
"messageJa": "保存しました"
}
```
**Bộ mã kết quả** — khai báo một chỗ duy nhất tại `services/shared/src/dispositions.ts`, và mọi nơi khác import từ đó (guide, báo cáo, đối soát):
| Mã | Tiếng Nhật | Hành động phụ kèm theo |
|---|---|---|
| `SOLD` | 成約 | — |
| `INTERESTED` | 見込みあり | — |
| `CALLBACK` | 再コール希望 | **Bắt buộc** có `callbackAtJst`, tạo lịch hẹn |
| `NOT_INTERESTED` | 興味なし | — |
| `WRONG_NUMBER` | 番号違い | Gắn cờ chất lượng dữ liệu cho lô nạp |
| `DO_NOT_CALL` | 今後連絡不要 | **Bắt buộc** ghi DNC trước khi trả về |
> Bộ mã này là **kết quả chốt ở M01**, không phải do đội kỹ thuật tự quyết. Trước khi chốt, mọi code dùng danh sách tạm và đánh dấu `TODO(M01)`.
### 7.2 `dnc-service` — được gọi từ contact flow
Vào: `{ "phoneNumber": "+81..." }` → Ra: `{ "blocked": "true" | "false" }`.
Quy tắc bắt buộc: nếu Lambda lỗi hoặc timeout, contact flow đi vào nhánh **kết thúc cuộc gọi**, không nối agent. Đây là ngoại lệ duy nhất của nguyên tắc "lỗi thì vẫn cho cuộc gọi đi tiếp", và lý do đã nêu ở §3 cửa 2.
### 7.3 Quy ước chung cho mọi handler
| Quy ước | Nội dung |
|---|---|
| Mọi phản hồi đều có `status` | `OK` / `VALIDATION_ERROR` / `INTERNAL_ERROR` |
| Thông báo cho agent luôn bằng tiếng Nhật | Trường `messageJa`, tra từ bảng thông điệp trong `shared` |
| Không bao giờ ném lỗi ra ngoài handler | Bắt hết, ghi log, trả về mã lỗi có ý nghĩa |
| Mọi ghi dữ liệu đều idempotent | Khóa nêu ở §9.2 |
| Không log số điện thoại đầy đủ | Logger tự che thành `+8131****678` |
---
## 8. Thiết kế chi tiết từng tính năng MVP
Phần này đi theo đúng thứ tự bảng tính năng ở `02` §2.1. Với mỗi tính năng: **làm bằng cách nào**, **chạm vào file nào**, và **cái gì dễ sai**.
---
### F1 — Tài khoản và phân quyền ⚙️
| | |
|---|---|
| **Thực hiện** | Security profile của Connect. Ba nhóm: `Agent`, `Supervisor`, `Admin` |
| **Không viết code** | Đúng vậy — kể cả quyền nghe ghi âm |
| **File** | `infra/lib/stacks/connect-core-stack.ts`, `infra/lib/config/common.ts` |
| **Điểm khó** | Tách bạch ba quyền: xem chi tiết cuộc gọi / nghe ghi âm / tải file về máy. Mặc định gộp chung là sai với yêu cầu "chỉ quản lý mới nghe được" |
| **Nghiệm thu** | Đăng nhập bằng tài khoản agent thật, xác nhận **không** thấy nút nghe ghi âm |
---
### F2 — Nạp danh sách khách hàng ⚙️+💻
Đây là tính năng có code, và là chỗ dữ liệu bẩn của khách đi vào hệ thống — nên xử lý kỹ.
```mermaid
sequenceDiagram
participant M as Quản lý
participant CLI as tools/csv-preflight
participant S3 as S3 inbound
participant LI as list-ingest Lambda
participant DD as DynamoDB
participant CP as Customer Profiles
M->>CLI: Chạy kiểm tra trước trên máy mình
CLI-->>M: Báo lỗi ngay, sửa file trước khi tải lên
M->>S3: Tải file CSV đã sửa
S3->>LI: Sự kiện tạo object
LI->>LI: 1 Kiểm tra header và mã hoá UTF-8 không BOM
LI->>LI: 2 Chuẩn hoá số sang E.164
LI->>LI: 3 Bỏ trùng trong nội bộ file
LI->>DD: 4 Đối chiếu danh sách DNC
LI->>LI: 5 Kiểm tra trường đồng ý liên hệ
LI->>CP: 6 Ghi file đã làm sạch cho Customer Profiles
LI->>DD: 7 Ghi tổng kết lô nạp
LI->>S3: 8 Ghi báo cáo dòng lỗi kèm lý do
LI-->>M: Thông báo hoàn tất
```
**Mẫu CSV bắt buộc** (khớp `01` §3.1):
```text
external_customer_id,phone_number,full_name,timezone,consent_source,consent_at,do_not_call,campaign_group
```
**Bảy luật kiểm tra, mỗi luật một hàm, mỗi hàm một bộ test:**
| # | Luật | Xử lý khi vi phạm |
|---:|---|---|
| 1 | File là UTF-8 không BOM, có header đúng | Từ chối cả file, báo lỗi rõ |
| 2 | `phone_number` chuyển được sang E.164 Nhật | Loại dòng, ghi lý do `INVALID_PHONE` |
| 3 | `external_customer_id` không rỗng và không trùng trong file | Giữ dòng đầu, loại dòng sau, lý do `DUPLICATE_IN_FILE` |
| 4 | Số không nằm trong DNC | Loại dòng, lý do `IN_DNC_LIST` |
| 5 | `consent_source` và `consent_at` có giá trị | Loại dòng, lý do `NO_CONSENT` — **luật tuân thủ, không được nới** |
| 6 | `consent_at` không ở tương lai, đúng định dạng ngày | Loại dòng, lý do `INVALID_CONSENT_DATE` |
| 7 | `do_not_call` = true thì ghi thẳng vào DNC | Ghi DNC rồi loại dòng, lý do `MARKED_DNC_IN_FILE` |
**Báo cáo trả về quản lý** — file CSV cùng cấu trúc file gốc, thêm 2 cột `error_code` và `error_message_ja`, để quản lý sửa ngay trên file đó rồi nạp lại.
**Điểm khó thật sự:**
- Chuẩn hoá số Nhật: `03-1234-5678`, `0312345678`, `+81 3 1234 5678`, số có ký tự toàn chiều rộng, số di động `090/080/070`. Hàm `phone.ts` cần bộ test ít nhất **30 ca** — đây là chỗ rẻ nhất để phòng lỗi đắt nhất.
- File lớn: giới hạn 1 GB, phải đọc theo dòng, không nạp cả file vào bộ nhớ. Nếu vượt thời gian chạy Lambda thì chuyển sang xử lý theo lô — đã tính trong range M06.
- `tools/csv-preflight` là **thứ tiết kiệm nhiều thời gian nhất cho vận hành**: quản lý phát hiện lỗi trong 5 giây thay vì chờ nạp xong mới biết.
**File:** `services/list-ingest/`, `services/shared/src/phone.ts`, `tools/csv-preflight/`
---
### F3, F4, F5 — Tạo đợt gọi, tự động quay số, nhận biết máy trả lời ⚙️
| | |
|---|---|
| **Thực hiện** | Outbound Campaigns + Guided Campaign Builder. Chế độ mặc định **progressive**, preview cho nhóm lead cần xem trước |
| **Code** | **Không có dòng nào.** Đây là điểm quan trọng nhất của cả kiến trúc |
| **Cấu hình gồm** | Segment nguồn, số gọi ra (DID dùng chung), khung giờ được phép gọi, quy tắc gọi lại theo disposition kỹ thuật, giới hạn số lần liên hệ, ngày nghỉ |
| **AMD** | Bật block `Check call progress` trong `outbound-campaign.flow.json` |
| **File** | `connect/flows/outbound-campaign.flow.json`, cấu hình campaign trong `infra` nếu IaC hỗ trợ, còn lại vào `MANUAL-CHECKLIST.md` |
**Điểm khó — đã nêu ở `02` §5.4:** AMD được huấn luyện chủ yếu trên thị trường nói tiếng Anh. Với 留守番電話サービス và các thông báo nhà mạng Nhật, tỷ lệ nhận đúng phải **đo bằng bộ mẫu thật** ở M11, và ngưỡng chấp nhận phải đưa vào tiêu chí nghiệm thu **trước** UAT. Đây là việc kiểm thử, không phải việc code.
> **⚠️ Cần xác minh ở M03:** mức độ CloudFormation/CDK phủ được cấu hình campaign. Phần nào không phủ được thì ghi vào `MANUAL-CHECKLIST.md` kèm ảnh chụp màn hình, để dựng lại được khi có sự cố.
---
### F6, F7 — Cùng một số gọi ra, 15 cuộc đồng thời ⚙️+📄
Đây là hai hạng mục **thủ tục là chính, kỹ thuật là phụ**, nhưng lại là rủi ro lớn nhất dự án.
| | |
|---|---|
| **F6 thực hiện** | Đặt outbound caller ID ở **một** queue dùng chung. Không cho phép mỗi routing profile một số khác nhau |
| **F7 thực hiện** | Xin tăng quota: cuộc gọi đồng thời của instance và của campaign, mục tiêu **25/25** |
| **Code** | Không |
| **File** | `infra/lib/stacks/connect-core-stack.ts` (queue), `docs/runbooks/quota.md` |
| **Việc phải làm sớm nhất** | Tuần 1: mở AWS Support case. Quota mặc định là 10 và **0** — con số 0 nghĩa là campaign hoàn toàn không chạy được cho đến khi được duyệt |
| **Giám sát** | Alarm khi số cuộc đồng thời chạm 80% hạn mức, để biết trước khi nghẽn |
Một việc kỹ thuật nhỏ nhưng đáng làm: thêm vào dashboard một chỉ số **tỷ lệ bắt máy theo ngày**. Đây là hệ thống cảnh báo sớm duy nhất cho rủi ro số gọi ra bị gắn nhãn 迷惑電話 (`02` §12) — hiện tượng này không sinh ra lỗi kỹ thuật nào, chỉ thấy qua việc tỷ lệ bắt máy tụt dần.
---
### F8 — Màn hình làm việc của agent ⚙️
| | |
|---|---|
| **Thực hiện** | Agent Workspace có sẵn: điều khiển cuộc gọi, hồ sơ khách, guide, thời gian nhập kết quả |
| **Code** | Không. **Không xây CCP tuỳ biến ở MVP** |
| **Cấu hình đáng chú ý** | Bật auto-accept và persistent connection để giảm khoảng lặng khi khách bắt máy; ACW đặt vừa đủ để nhập kết quả — **đo thực tế**, không lấy con số 30 giây của AWS làm mặc định |
| **Screen pop** | Dữ liệu khách lấy từ Customer Profiles, đã có sẵn từ khâu nạp danh sách |
| **Kịch bản nói** | Đặt trong guide, sửa được mà không phải deploy code |
---
### F9 — Ghi kết quả cuộc gọi 💻 *(lõi của M08)*
**Luồng ghi, thứ tự có ý nghĩa:**
```mermaid
flowchart TD
A[Agent bấm Lưu trên guide] --> B[Lambda nhận, validate bằng zod]
B --> C{"Hợp lệ?"}
C -- Không --> Z[Trả lỗi tiếng Nhật, agent sửa lại]
C -- Có --> D{"outcome = DO_NOT_CALL?"}
D -- Có --> E[Ghi DNC TRƯỚC]
D -- Không --> F
E --> F{"outcome = CALLBACK?"}
F -- Có --> G[Kiểm tra trùng, tạo lịch hẹn]
F -- Không --> H
G --> H[Ghi kết quả cuộc gọi]
H --> I[Ghi nhật ký thao tác]
I --> J[Trả OK kèm thông báo tiếng Nhật]
```
**Ba quy tắc khiến thiết kế này khác một CRUD thường:**
1. **DNC ghi trước kết quả.** Nếu ghi kết quả trước rồi Lambda chết, ta có một cuộc gọi ghi "không gọi nữa" nhưng số vẫn được gọi lại — hỏng đúng cam kết pháp lý. Ngược lại thì tệ nhất là số bị chặn mà thiếu bản ghi kết quả, phát hiện được qua đối soát và sửa được.
2. **Idempotent theo `contactId`.** Agent bấm hai lần, mạng chập chờn, guide gửi lại — đều phải ra cùng một kết quả. Ghi có điều kiện, lần ghi sau tăng `version` và giữ lại giá trị cũ trong nhật ký.
3. **Không tin `agentId` do client gửi.** Lấy từ ngữ cảnh của Connect. ⚠️ Cần xác minh ở M02 trường nào của guide mang thông tin này đáng tin.
**Ca biên phải có test:**
| Ca | Kỳ vọng |
|---|---|
| Agent không nhập gì rồi hết thời gian ACW | Có bản ghi `NO_DISPOSITION`, hiện trong đối soát |
| Nhập `CALLBACK` nhưng không có giờ hẹn | Từ chối, báo tiếng Nhật, không lưu gì |
| Giờ hẹn nằm ngoài khung giờ được phép gọi | Từ chối kèm gợi ý khung giờ hợp lệ gần nhất |
| Gửi hai lần cùng `contactId` | Chỉ một bản ghi, `version` = 2 |
| Ghi chú dài 10.000 ký tự | Cắt còn giới hạn đã định, không làm hỏng bản ghi |
**File:** `services/disposition-api/`
---
### F10 — Danh sách không gọi 💻 *(lõi của M08)*
Đây là tính năng **được kiểm tra ở ba lớp**, vì đây là chỗ mà sai một lần là mất uy tín với khách hàng cuối và có rủi ro pháp lý.
```mermaid
flowchart LR
A[Agent bấm không gọi nữa] --> B[Ghi DNC vào DynamoDB
lớp 1: nguồn sự thật]
B --> C[Gắn cờ trên hồ sơ Customer Profiles
lớp 2: lọc khi tạo segment]
D[Nạp danh sách mới] --> E[Đối chiếu DNC
lớp 3: loại trước khi vào campaign]
F[Trước khi nối agent] --> G[Contact flow gọi kiểm tra
lớp 4: chốt chặn cuối]
```
| Lớp | Chặn được gì | Vì sao vẫn cần lớp sau |
|---|---|---|
| 1. DynamoDB | Nguồn sự thật, ghi ngay lập tức | Campaign không đọc DynamoDB |
| 2. Cờ trên hồ sơ khách | Segment lọc được khi tạo đợt gọi mới | Đồng bộ có thể trễ hoặc lỗi |
| 3. Lọc lúc nạp danh sách | Chặn số đã cấm quay lại qua file CSV mới | Danh sách đã nạp trước đó vẫn còn trong campaign đang chạy |
| 4. Kiểm tra trong contact flow | Chốt chặn cuối cùng, chặn cả số vừa mới bị cấm | — |
**Vì sao phải bốn lớp mà không phải một:** như `02` §2.4 đã nêu, Outbound Campaigns **không có blocklist dựng sẵn**. Cách duy nhất là gắn cờ trên hồ sơ rồi lọc segment — mà segment được tính tại thời điểm tạo đợt gọi, nên một số bị cấm lúc 10h sáng vẫn nằm trong đợt gọi đã tạo từ 8h. Lớp 4 là thứ đóng khoảng trống đó.
**Chi phí của lớp 4:** thêm một lần gọi Lambda trên mỗi cuộc gọi ra. Cần đo độ trễ ở M11; nếu vượt ngưỡng, phương án thay thế là cache trong bộ nhớ Lambda với thời hạn ngắn — nhưng cache thì mất tính tức thời, phải cân nhắc và ghi ADR.
**Nghiệm thu (`02` §11):** *"100% số được đánh dấu không gọi nữa bị loại trước đợt gọi kế tiếp."* Test tương ứng: đánh dấu 10 số trong lúc campaign đang chạy, xác nhận không số nào được quay.
**File:** `services/dnc-service/`, dùng chung trong `services/list-ingest/`
---
### F11 — Hẹn gọi lại 💻 *(lõi của M08)*
| | |
|---|---|
| **Thực hiện** | Bản ghi lịch hẹn trong DynamoDB + EventBridge Scheduler quét theo giờ |
| **Vì sao không dùng callback của Connect** | Cơ chế đó phục vụ khách gọi vào đang chờ máy. Ở đây là lịch hẹn của nhân viên bán hàng, theo giờ Nhật, có chống trùng, có trạng thái, và phải xuất hiện lại trong danh sách gọi |
| **Đến giờ thì làm gì** | Bộ quét gom các lịch đến hạn, sinh danh sách và đưa vào một segment dành riêng cho đợt gọi lại. **Không tự đặt cuộc gọi** — vẫn để campaign quay số, giữ được AMD và mọi quy tắc |
**Bốn quy tắc nghiệp vụ:**
| Quy tắc | Cách làm |
|---|---|
| Theo giờ Nhật | Mọi thứ tính bằng `Asia/Tokyo`, lưu cả ISO có múi giờ lẫn khóa ngày JST. `services/shared/src/jst.ts` là nơi duy nhất được phép tính giờ |
| Không trùng | Khóa chống trùng `CBIDX##SLOT#` ghi có điều kiện. Trùng thì trả về lịch đã có, không tạo mới |
| Không rơi ra ngoài khung giờ hợp lệ | Kiểm tra khung giờ gọi và ngày nghỉ ngay lúc tạo, không đợi đến lúc quét |
| Có vòng đời rõ ràng | `SCHEDULED` → `DUE` → `COMPLETED` / `CANCELLED` / `EXPIRED`. Lịch quá hạn không tự biến mất, phải hiện ra trong đối soát |
**Ca biên:** hẹn vào ngày nghỉ, hẹn vào 23h, hẹn cho số vừa bị đưa vào DNC (phải huỷ lịch), hẹn hai lần cùng khung giờ, lịch đến hạn nhưng campaign đã kết thúc.
**File:** `services/callback-scheduler/`, `services/shared/src/jst.ts`
---
### F12, F13, F14, F16 — Ghi âm, transcript, sắc thái, tìm kiếm ⚙️
| | |
|---|---|
| **Thực hiện** | Block `Set recording and analytics behavior` trong contact flow. Bật ghi âm hai chiều, bật phân tích sau cuộc gọi với `ja_JP` |
| **Code** | Không. Code **không bao giờ** chạm vào audio hay transcript |
| **Lưu trữ** | S3 chặn truy cập công khai, mã hoá SSE-KMS bằng khoá do khách quản lý, vòng đời chuyển sang lớp lưu trữ rẻ rồi xoá theo chính sách |
| **Từ điển riêng** | `connect/vocabulary/ja-JP.txt` — tên sản phẩm, tên thương hiệu. **Tuyệt đối không đưa dữ liệu cá nhân vào đây.** Từ điển mới không sửa được transcript cũ, nên phải nạp trước khi bắt đầu chạy thật |
| **Quy tắc gắn cờ** | `connect/rules/*.json` — kết hợp sắc thái tiêu cực, độ to giọng, số lần nói chen để chọn cuộc cần QA |
**Ba điều phải nói thẳng với khách, đã có trong `02` §5.3 và §5.6:**
1. Chỉ cuộc gọi **có người bắt máy và có hội thoại** mới sinh ghi âm và transcript. Máy bận, số sai, không ai nghe thì không có gì để ghi — nhưng vẫn có bản ghi sự kiện.
2. "Có text" khác "text chính xác". Cần bộ khoảng 20 cuộc gọi được người Nhật đối chiếu, thống nhất cách đo và ngưỡng chấp nhận **trước UAT**.
3. Sắc thái là phân tích **nội dung lời nói**, không phải máy đọc cảm xúc từ chất giọng. Dùng để chọn cuộc cho QA, **không** dùng để tự động đánh giá agent.
---
### F15, F17, F18 — Nghe lén/nhắc riêng/chen ngang, dashboard, báo cáo ⚙️
Toàn bộ có sẵn. Việc của dự án là **cấu hình quyền đúng người** và **lưu sẵn các báo cáo mà quản lý cần**, rồi đào tạo cách dùng (M13). Không có code.
Một lưu ý vận hành đáng ghi vào runbook: quản lý sẽ phải thao tác trên màn hình quản trị của AWS bằng tiếng Nhật. `02` §12 xếp đây là rủi ro "MVP bị đánh giá không dùng được trên thực tế". Cách giảm rủi ro rẻ nhất: **demo màn hình thật ngay trong discovery**, trước khi khách ký.
---
### F19 — Nhận cuộc gọi vào ⚙️
| | |
|---|---|
| **Thực hiện** | Contact flow riêng cho luồng gọi vào + hours of operation + queue |
| **Trong giờ** | Lời chào tiếng Nhật → nối agent rảnh → nếu không có ai thì thông báo chờ hoặc để lại lời nhắn |
| **Ngoài giờ** | Thông báo giờ làm việc → cho để lại lời nhắn |
| **Ghi âm và phân tích** | Bật giống hệt luồng gọi ra, để đối soát và báo cáo không phải chia hai nhánh |
| **File** | `connect/flows/inbound-main.flow.json`, `connect/flows/inbound-afterhours.flow.json`, `connect/hours-of-operation.json` |
Tính năng này ở bản 1 là tuỳ chọn, bản 2 đưa vào bắt buộc — vì hiển thị một số chung khi gọi ra thì **chắc chắn** có khách gọi lại. Không làm phần này nghĩa là để khách gọi vào số công ty và nghe tín hiệu bận.
---
### F20 — Cảnh báo và xử lý sự cố ⚙️+💻
| Cảnh báo | Phát hiện bằng cách nào | Code |
|---|---|:--:|
| Cuộc gọi có kết nối nhưng sau 15 phút chưa có file ghi âm | `recording-audit` quét bản ghi sự kiện, đối chiếu S3 | 💻 |
| Phân tích hội thoại lỗi | Sự kiện lỗi từ EventBridge | ⚙️ |
| Chạm 80% hạn mức cuộc gọi đồng thời | CloudWatch alarm | ⚙️ |
| Tỷ lệ bắt máy tụt bất thường so với 7 ngày trước | Chỉ số tự tính trong `reconciliation` | 💻 |
| Lambda lỗi hoặc có message vào hàng đợi lỗi | CloudWatch alarm | ⚙️ |
| Campaign dừng ngoài kế hoạch | Sự kiện campaign | ⚙️ |
**Vì sao ngưỡng 15 phút:** thời gian phân tích một cuộc gọi thường bằng khoảng 40% độ dài cuộc gọi (`02` §5.3), nên 15 phút là dư an toàn cho cuộc gọi thông thường và không sinh cảnh báo giả.
**Nguyên tắc bắt buộc:** mỗi cảnh báo phải có **một file runbook tương ứng** trong `docs/runbooks/` và **một người chịu trách nhiệm**. Cảnh báo không có runbook thì không được đưa lên production — vì nó sẽ bị bỏ qua và làm mọi người mất niềm tin vào toàn bộ hệ thống cảnh báo.
**File:** `services/recording-audit/`, `infra/lib/stacks/observability-stack.ts`, `docs/runbooks/`
---
### F21 — Đối soát dữ liệu 💻
Tính năng nhỏ nhưng là **thứ chứng minh hệ thống chạy đúng** — và là công cụ đầu tiên được dùng khi có tranh cãi "sao hôm qua thiếu cuộc gọi".
Bảng đối soát hằng ngày, đúng như `02` §2.1 mô tả:
```text
Ngày 2026-10-20
────────────────────────────────────────────────
Số lượt quay số 1.240
Kết nối được người thật 312 (25,2%)
Có file ghi âm 310 (99,4%) ⚠️ thiếu 2
Có bản text 308 (98,7%) ⚠️ thiếu 2
Agent đã nhập kết quả 305 (97,8%) ⚠️ thiếu 7
────────────────────────────────────────────────
Cần xử lý tay: 2 cuộc thiếu ghi âm, 7 cuộc chưa nhập kết quả
```
**Bốn nguồn dữ liệu phải ghép lại:** sự kiện cuộc gọi, dữ liệu campaign, danh sách file trong S3, và kết quả nghiệp vụ trong DynamoDB. `contactId` là khóa nối. Đây chính là lý do `02` §2.4 xếp đối soát vào nhóm phải tự viết — không có sẵn chỗ nào ghép bốn thứ này.
**Chạy khi nào:** một lần mỗi sáng cho dữ liệu ngày hôm trước, qua EventBridge Scheduler. Kết quả ghi vào DynamoDB và xuất CSV vào S3; gửi tóm tắt qua email/Slack ⚠️ (kênh cần chốt với khách).
**File:** `services/reconciliation/`, `services/contact-event-sink/`
---
### F22 — Hồ sơ bảo mật và quyền riêng tư 📄
Không có code. Bộ tài liệu tiếng Nhật: bảng kiểm bảo mật, tài liệu quản lý bên nhận uỷ thác, chính sách thời hạn lưu, quy trình khi cá nhân yêu cầu xem/xoá dữ liệu. Nằm ở `docs/ja/`.
Việc kỹ thuật duy nhất liên quan: **quy trình xoá dữ liệu của một cá nhân** phải chỉ ra được dữ liệu người đó nằm ở những đâu — hồ sơ khách, kết quả cuộc gọi, lịch hẹn, DNC, ghi âm, transcript. Bảng ở §6.2 chính là câu trả lời cho câu hỏi đó, nên `docs/ja/` phải trỏ về đây.
> Lưu ý: mục DNC **không được xoá** khi khách yêu cầu xoá dữ liệu — vì xoá nó đi thì khách sẽ bị gọi lại. Đây là điểm cần legal Nhật xác nhận cách xử lý, ghi rõ trong hồ sơ.
---
## 9. Xử lý sự kiện, idempotency và lỗi
### 9.1 Vì sao phần này quan trọng hơn vẻ ngoài của nó
Codebase chỉ có ~3.000 dòng, nhưng phần lớn lỗi khó chịu ở một hệ thống như thế này không đến từ logic sai — mà đến từ **sự kiện đến hai lần, đến muộn, hoặc sai thứ tự**. Ba mẫu dưới đây áp dụng cho mọi handler, không có ngoại lệ.
### 9.2 Khoá idempotency của từng service
| Service | Khoá | Hành vi khi trùng |
|---|---|---|
| `disposition-api` | `contactId` | Cập nhật, tăng `version`, giữ giá trị cũ trong nhật ký |
| `dnc-service` | Số điện thoại E.164 | Ghi lần đầu thắng; lần sau chỉ bổ sung lý do |
| `callback-scheduler` | `số + khung giờ` | Trả về lịch đã có, không tạo mới |
| `list-ingest` | Mã lô nạp từ đường dẫn file | Chạy lại cùng file không tạo hồ sơ trùng |
| `contact-event-sink` | `contactId + loại sự kiện + thời điểm` | Ghi đè, vô hại |
| `reconciliation` | Ngày | Chạy lại ghi đè kết quả ngày đó |
### 9.3 Ba chế độ lỗi và cách xử lý
| Chế độ | Ví dụ | Xử lý |
|---|---|---|
| **Lỗi dữ liệu vào** | Agent gửi giờ hẹn sai định dạng | Trả về ngay kèm thông báo tiếng Nhật. **Không** thử lại, **không** cảnh báo |
| **Lỗi tạm thời** | DynamoDB bị chậm, Lambda bị điều tiết | Thử lại có giãn cách; quá số lần thì đưa vào hàng đợi lỗi + cảnh báo |
| **Lỗi lập trình** | Không đọc được trường của một object | Vào hàng đợi lỗi, cảnh báo ngay, có runbook để phát lại sau khi sửa |
**Mọi Lambda bất đồng bộ đều có hàng đợi lỗi.** Message trong đó phải phát lại được sau khi sửa code — nghĩa là payload gốc phải được giữ nguyên, không bị biến đổi trước khi lỗi.
### 9.4 Bảng sự kiện tiêu thụ
| Sự kiện | Service | Việc làm |
|---|---|---|
| Contact event: cuộc gọi kết nối / kết thúc | `contact-event-sink` | Ghi bản ghi cuộc gọi tối giản |
| Contact Lens: phân tích xong / lỗi | `contact-event-sink` | Cập nhật cờ "có text" |
| Contact Lens rule khớp | `contact-event-sink` | Gắn cờ cuộc cần QA |
| Campaign: bắt đầu / dừng / xong | `contact-event-sink` | Ghi nhận, cảnh báo nếu dừng ngoài kế hoạch |
| S3: có file CSV mới | `list-ingest` | Nạp danh sách |
| Scheduler: mỗi 15 phút | `callback-scheduler` | Quét lịch đến hạn |
| Scheduler: mỗi giờ | `recording-audit` | Dò cuộc gọi thiếu file |
| Scheduler: 06:00 JST | `reconciliation` | Đối soát ngày hôm trước |
---
## 10. Bảo mật và phân quyền trong code
### 10.1 Nguyên tắc
| Nguyên tắc | Áp dụng cụ thể |
|---|---|
| Quyền tối thiểu cho từng Lambda | Mỗi service một IAM role riêng. `dnc-service` chỉ đọc và ghi các mục `DNC#`; không role nào được `dynamodb:*` |
| Không có bí mật trong code | Không có khoá cứng trong repo. Tham số cấu hình qua biến môi trường và Parameter Store |
| Mã hoá mọi nơi | S3 và DynamoDB đều dùng khoá KMS do khách quản lý ở production |
| Không có dữ liệu cá nhân trong log | Logger tự che số điện thoại. Cấm log toàn bộ payload. Có test kiểm tra điều này |
| Ghi vết mọi thao tác nhạy cảm | Bản ghi nhật ký cho mỗi lần ghi DNC, sửa kết quả, huỷ lịch hẹn; CloudTrail cho mọi thao tác API |
### 10.2 Quyền nghe ghi âm — nói rõ vì hay bị hiểu nhầm
Phân quyền nghe ghi âm **hoàn toàn nằm ở security profile của Connect**, không có dòng code nào tham gia. Code không tạo link tải file, không proxy audio, không kiểm tra quyền. Điều này là cố ý: mọi cơ chế tự viết quanh việc truy cập ghi âm đều là bề mặt tấn công mới và là gánh nặng audit.
### 10.3 Kiểm tra tự động trong CI
- Quét bí mật lộ trong commit.
- Kiểm tra IAM policy không có `Action: "*"` hoặc `Resource: "*"` ngoài danh sách ngoại lệ có ghi lý do.
- Kiểm tra bucket S3 đều bật chặn truy cập công khai.
- Test riêng khẳng định logger che số điện thoại.
---
## 11. Môi trường, IaC và CI/CD
### 11.1 Hai môi trường
| Môi trường | Instance Connect | Số điện thoại | Dữ liệu |
|---|---|---|---|
| `dev` | Instance riêng | Số test | **Không dùng dữ liệu cá nhân thật** |
| `prod` | Instance production | Số DID chính thức + 0120 | Dữ liệu thật |
> Hạn mức mặc định là **2 instance mỗi Region** (`02` §5.2) — vừa đủ hai môi trường này, **không còn chỗ cho sandbox**. Nghĩa là mọi thử nghiệm phá phách phải làm trên `dev`, và `dev` phải khôi phục lại được. Đây là lý do cấu hình phải nằm trong Git.
### 11.2 Cái gì IaC hoá được, cái gì không
Đây là phần quan trọng nhất của mục này, vì Amazon Connect **không phủ 100%** bằng hạ tầng dạng code.
| Nhóm | Trạng thái | Cách xử lý |
|---|---|---|
| S3, KMS, DynamoDB, Lambda, EventBridge, CloudWatch | Phủ đủ | CDK |
| Hours of operation, queue, routing profile, security profile, user, contact flow | Phủ được ⚠️ cần xác minh phiên bản | CDK, nội dung flow nạp từ file JSON trong `connect/` |
| Cấu hình campaign, Customer Profiles domain và mapping, quy tắc Contact Lens, từ điển riêng, guide | **Có thể phải làm tay** ⚠️ | Ghi từng bước vào `connect/MANUAL-CHECKLIST.md`, kèm ảnh chụp; định kỳ dùng `tools/flow-sync` kéo về đối chiếu |
| Xin số, xin tăng hạn mức | Không thể tự động | Thủ tục, M04 |
**Việc bắt buộc ở M03:** xác định chính xác ranh giới này trên account thật, rồi cập nhật lại mục này. Đừng giả định — cứ mỗi hạng mục phải làm tay là một chỗ hệ thống không dựng lại được sau sự cố, và khách cần biết trước.
### 11.3 Luồng phát hành
```mermaid
flowchart LR
A[Pull request] --> B[CI: lint, unit test, snapshot IaC, quét bảo mật]
B --> C[Merge vào main]
C --> D[Tự động triển khai lên dev]
D --> E[Test tích hợp trên dev]
E --> F[Phê duyệt tay]
F --> G[Triển khai lên prod]
G --> H[Kiểm tra sau phát hành: gọi thử 1 cuộc]
```
**Quy tắc phát hành riêng cho call center:** không phát hành lên production trong giờ làm việc của call center. Cửa sổ phát hành nằm ngoài khung giờ gọi — ghi rõ trong runbook, vì việc cập nhật contact flow có thể ảnh hưởng cuộc gọi đang diễn ra.
---
## 12. Chiến lược kiểm thử theo tầng
Kiểm thử chiếm ~18% khối lượng MVP (`02` §7.1) — gần bằng phần cấu hình. Bảng này cho biết 18% đó đi vào đâu.
| Tầng | Kiểm cái gì | Công cụ | Chạy khi nào |
|---|---|---|---|
| **Unit** | Chuẩn hoá số điện thoại, luật kiểm tra CSV, tính giờ Nhật, chống trùng lịch hẹn, thứ tự ghi DNC | Vitest, không cần AWS | Mỗi commit |
| **Snapshot hạ tầng** | Template CloudFormation không thay đổi ngoài ý muốn | CDK assertions | Mỗi commit |
| **Tích hợp** | Lambda thật ghi vào DynamoDB thật trên `dev`; sự kiện EventBridge đi đúng đường | Vitest chạy trên `dev` | Mỗi lần merge |
| **Luồng cuộc gọi** | Gọi thử số test, đi qua AMD, ghi âm, đến đúng queue, guide hiện ra | Thủ công + kịch bản có sẵn | Mỗi lần đổi flow |
| **Đầu-cuối nghiệp vụ** | Nạp CSV → chạy campaign → agent nhận cuộc → nhập kết quả → thấy trong đối soát | Thủ công theo kịch bản | Trước UAT |
| **Tải và đồng thời** | 15 cuộc kết nối đồng thời liên tục 30 phút | Số test + đội thật | M11 |
| **AMD tiếng Nhật** | Bộ mẫu: cố định, di động, số sai, máy trả lời, thông báo nhà mạng | Bộ mẫu thật | M11 |
| **Ma trận nhà mạng** | Hiển thị số đúng trên NTT cố định và các mạng di động | Thủ công | M11 |
| **Độ chính xác text** | ~20 cuộc gọi được người Nhật đối chiếu | Người Nhật đánh giá | Trước UAT |
**Điểm đáng chú ý:** năm dòng cuối **không phải test phần mềm** — chúng là test hệ thống viễn thông và test chất lượng ngôn ngữ. Đây chính là lý do `02` §7.1 nói "cắt phần kiểm thử là cắt vào chỗ nguy hiểm nhất": rủi ro của dự án này không nằm ở chỗ code có chạy hay không.
**Dữ liệu test:** `tests/fixtures/lists/` chứa CSV sạch, CSV có số sai định dạng, có dòng trùng, có dòng thiếu thông tin đồng ý, có ký tự toàn chiều rộng, có BOM. Toàn bộ là dữ liệu bịa, **không có số điện thoại thật**.
---
## 13. Giám sát, cảnh báo và runbook
### 13.1 Dashboard vận hành
Ba dashboard, ba đối tượng khác nhau:
| Dashboard | Cho ai | Nội dung |
|---|---|---|
| Thời gian thực của Connect ⚙️ | Trưởng nhóm | Ai đang gọi, ai rảnh, tiến độ đợt gọi |
| Sức khoẻ hệ thống (CloudWatch) | Admin | Số cuộc đồng thời so với hạn mức, lỗi Lambda, độ sâu hàng đợi lỗi, độ trễ |
| Đối soát và chất lượng dữ liệu 💻 | Quản lý + Admin | Bảng đối soát ngày, tỷ lệ bắt máy theo ngày, số cuộc thiếu ghi âm/text |
### 13.2 Runbook — mỗi cảnh báo một file
`docs/runbooks/` là điều kiện mở dịch vụ, không phải tài liệu làm cho đẹp. Mỗi file trả lời bốn câu: **triệu chứng gì**, **kiểm tra ở đâu**, **xử lý thế nào**, **khi nào leo thang cho ai**.
Danh sách tối thiểu: thiếu file ghi âm; phân tích lỗi; chạm trần cuộc gọi đồng thời; Lambda lỗi và hàng đợi lỗi; campaign dừng ngoài kế hoạch; tỷ lệ bắt máy tụt bất thường; nạp danh sách thất bại; khôi phục cấu hình Connect sau khi sửa nhầm.
`02` §11 yêu cầu **đã diễn tập khôi phục cấu hình** trước khi mở dịch vụ — nghĩa là runbook cuối cùng trong danh sách trên phải được chạy thử thật ít nhất một lần trên `dev`.
---
## 14. Codebase sẽ thay đổi thế nào ở Standard/Full
Phần này để trả lời câu hỏi "làm MVP thế này thì sau có phải đập đi làm lại không?".
### 14.1 Standard — thêm một tầng giao diện, không đụng vào tầng dưới
```text
callcenter-connect/
├─ apps/ # ← THƯ MỤC MỚI
│ └─ admin-portal/ # S01 — Next.js, tiếng Nhật
│ ├─ app/
│ ├─ components/
│ └─ lib/
├─ services/
│ ├─ ...7 service của MVP giữ nguyên...
│ ├─ admin-api/ # S01/S02 — API cho portal
│ ├─ crm-connector/ # S03 — đồng bộ hai chiều
│ └─ bi-export/ # S05 — xuất dữ liệu cho dashboard
```
| Hạng mục | Ảnh hưởng tới code MVP |
|---|---|
| S01 màn hình quản lý danh sách | **Không đụng.** Portal gọi lại đúng `list-ingest` đã có |
| S02 kết quả và hẹn lại nâng cao | Mở rộng `disposition-api` và `callback-scheduler`, giữ nguyên mô hình dữ liệu |
| S03 tích hợp CRM | Service mới, nghe sự kiện có sẵn |
| S05 dashboard kinh doanh | Đọc thêm, không sửa gì |
| S06 SSO/IaC/CI-CD | Củng cố phần đã có |
**Đây là lợi ích cụ thể của việc thiết kế theo §1:** vì MVP không có giao diện web nào và mọi nghiệp vụ đã nằm trong Lambda có hợp đồng rõ ràng, S01 chỉ là thêm một tầng gọi vào các API sẵn có. Không có gì phải viết lại.
**Một cảnh báo phải nói trước với khách** (`02` §12): nếu trong discovery thấy quản lý **không thể** thao tác trên màn hình quản trị AWS bằng tiếng Nhật, thì S01 phải kéo lên MVP, và MVP tăng thêm **10–14 PD**. Quyết định này nên chốt ở M01, không để đến lúc UAT mới phát hiện.
### 14.2 Full core — phần lớn không phải code
Như `02` §2.4 đã nêu, tỷ lệ đảo chiều ở Full core: quay số dự đoán, tóm tắt AI, chấm điểm tự động, xếp ca **đều là bật thêm tính năng có sẵn**. Phần code thật sự chỉ có F02 (cảnh báo tức thời), F04 (trợ lý agent, cần kho tri thức), F07 (BI nâng cao) và F08 (SMS/email theo sau).
### 14.3 X01/X02 — hai thứ **không** nên nằm trong repo này
`X01` nhận diện cảm xúc theo giọng và `X02` che thông tin cá nhân tiếng Nhật là **pipeline học máy**, có vòng đời hoàn toàn khác: dữ liệu gán nhãn, huấn luyện, đánh giá, giám sát trôi mô hình. Chúng nên nằm ở repo riêng, kết nối qua sự kiện, để không kéo độ phức tạp của MLOps vào một codebase đang cố tình giữ nhỏ.
---
## 15. Codebase của Phương án B khác gì
Nếu khách bắt buộc gọi ra bằng số 0120, kiến trúc ở §2 thay đổi ở đúng một chỗ — nhưng là chỗ nặng nhất.
```mermaid
flowchart LR
subgraph A["Phương án A — hiện tại"]
OC[Outbound Campaigns ⚙️
AWS lo hết]
end
subgraph B["Phương án B"]
DS[dialer-service 💻
tự viết và tự bảo trì]
DS1[hàng đợi danh sách]
DS2[điều tiết nhịp gọi]
DS3[quy tắc gọi lại]
DS4[khung giờ và ngày nghỉ]
DS5[giới hạn số lần liên hệ]
DS6[lọc DNC trước khi gọi]
DS --> DS1 & DS2 & DS3 & DS4 & DS5 & DS6
end
```
| Khía cạnh | Phương án A | Phương án B |
|---|---|---|
| Service tự viết | 7 | **8+**, service mới lớn hơn cả 7 cái kia cộng lại |
| Dòng code ước tính | 2.500–4.000 | **8.000–12.000** |
| PD phần code | 8–12 | **26–42** |
| Nhận biết máy trả lời | Có sẵn ⚙️ | **Mất** — block AMD chỉ chạy cho cuộc gọi của campaign và callback kiểu khách được nối trước |
| Ai bảo trì logic quay số | AWS | **Dự án, vĩnh viễn** |
| Bổ sung phải xây | — | Bộ quản lý trạng thái quay số, chống gọi trùng, xử lý agent rảnh/bận, kiểm soát nhịp |
**Điều đáng nói nhất về mặt kỹ thuật:** Phương án B không chỉ là "thêm 18–30 PD". Nó chuyển dự án từ **cấu hình một sản phẩm có sẵn** sang **xây một sản phẩm**, với toàn bộ hệ quả: nhiều bug hơn, cần nhiều test hơn, cần người hiểu hệ thống trực khi có sự cố, và không được AWS nâng cấp theo thời gian. Vì vậy `02` §9 đề nghị tách B01 thành một hợp đồng PoC nhỏ trước — đề nghị đó là đúng và nên giữ.
---
## 16. Ánh xạ WBS M01–M16 sang thư mục/artifact
Bảng này để trả lời "hạng mục này rồi sẽ nằm ở đâu trong repo" khi lập kế hoạch và khi nghiệm thu.
| ID | Hạng mục | PD | Sản phẩm nằm ở đâu |
|---|---|---:|---|
| M01 | Khảo sát nghiệp vụ và tuân thủ | 3–4 | `docs/ja/`, `services/shared/src/dispositions.ts` (bộ mã kết quả), `infra/lib/config/common.ts` (khung giờ gọi) |
| M02 | Thiết kế giải pháp và bảo mật | 2–3 | `docs/adr/`, chính tài liệu này, xác minh các mục ⚠️ |
| M03 | Dựng nền tảng AWS | 4–5 | `infra/lib/stacks/foundation-stack.ts`, `data-stack.ts`, hai môi trường chạy được |
| M04 | Số điện thoại và hạn mức | 4–6 | `docs/runbooks/quota.md`, hồ sơ ngoài repo, ticket AWS |
| M05 | Người dùng và định tuyến | 2–3 | `infra/lib/stacks/connect-core-stack.ts` |
| M06 | Danh sách và đợt gọi | 5–7 | `services/list-ingest/`, `tools/csv-preflight/`, cấu hình campaign + `MANUAL-CHECKLIST.md` |
| M07 | Luồng gọi, ghi âm, phân tích | 4–5 | `connect/flows/`, `connect/vocabulary/`, `connect/rules/` |
| M08 | Kết quả, DNC, hẹn lại | 5–7 | `services/disposition-api/`, `dnc-service/`, `callback-scheduler/`, `connect/guides/` |
| M09 | Công cụ cho quản lý | 3–4 | Cấu hình security profile + báo cáo lưu sẵn, `docs/ja/` |
| M10 | Giám sát và vận hành | 2–3 | `services/recording-audit/`, `observability-stack.ts`, `docs/runbooks/` |
| M11 | Kiểm thử kỹ thuật và tải | 6–9 | `tests/`, báo cáo test AMD và ma trận nhà mạng |
| M12 | UAT và ổn định | 4–5 | Sửa lỗi rải khắp repo, nhật ký hypercare |
| M13 | Tài liệu và đào tạo | 4–6 | `docs/ja/` — sổ tay admin/agent/quản lý, từ điển dữ liệu |
| M14 | Quản lý dự án | 3–4 | Ngoài repo |
| M15 | Nhận cuộc gọi | 5–7 | `connect/flows/inbound-*.json`, `connect/hours-of-operation.json`, `connect/prompts/` |
| M16 | Hồ sơ bảo mật | 1–2 | `docs/ja/security/` |
**Kiểm tra chéo:** chỉ có **M06, M08, M10** sinh ra code nghiệp vụ đáng kể — khớp đúng với con số 8–12 PD (~15%) ở `02` §7.1. Nếu trong quá trình làm thấy code xuất hiện ở các hạng mục khác, đó là dấu hiệu phạm vi đang trôi và cần đưa ra bàn.
---
## 17. Quy ước code
| Chủ đề | Quy ước |
|---|---|
| Ngôn ngữ trong code | Tên biến, hàm, comment: **tiếng Anh**. Thông điệp hiện cho người dùng: **tiếng Nhật**, tập trung trong một file thông điệp |
| Múi giờ | Trong code chỉ dùng UTC. Chuyển sang giờ Nhật đúng một chỗ: `shared/src/jst.ts`. **Cấm** tự viết phép cộng 9 tiếng ở nơi khác |
| Số điện thoại | Chỉ tồn tại một dạng trong hệ thống: E.164. Chuẩn hoá ngay tại biên, không chuẩn hoá ở giữa |
| Tiền và tỷ lệ | Không có ở MVP |
| Xử lý lỗi | Không nuốt lỗi im lặng. Mọi `catch` phải ghi log hoặc ném lại |
| Comment | Chỉ giải thích **vì sao**, không mô tả lại code. Ví dụ đúng: `// Ghi DNC trước disposition: nếu lỗi ở giữa, thà thừa chặn còn hơn gọi nhầm` |
| Kiểm tra dữ liệu vào | Mọi biên đều validate bằng zod, kể cả sự kiện từ AWS |
| Commit | Conventional commits, tiếng Anh, có tham chiếu mã hạng mục: `feat(M08): add DNC write-before-disposition ordering` |
| ADR | Mọi quyết định khó đảo ngược ghi một file trong `docs/adr/`. Tối thiểu phải có: chọn CDK, chọn một bảng DynamoDB, thứ tự ghi DNC, bốn lớp chặn DNC |
---
## 18. Checklist khởi tạo repo ngày đầu tiên
Thứ tự này cố ý — mỗi bước mở đường cho bước sau, và bước 1–2 là hai việc có thời gian chờ dài nhất của cả dự án.
| # | Việc | Ai | Vì sao trước |
|---:|---|---|---|
| 1 | **Tạo Connect instance trên `dev` và mở AWS Support case xin tăng hạn mức** | Kiến trúc sư | Phải có instance mới xin được quota; duyệt có thể mất tới **3 tuần** |
| 2 | **Gửi khách danh sách 3 loại hồ sơ pháp nhân để xin/chuyển số** | PM/BA | Cửa sổ chuyển số là ngày 1 và 15 tháng kế tiếp; trượt một nhịp là chậm nửa tháng |
| 3 | Khởi tạo repo, workspaces, TypeScript, lint, CI rỗng | Kỹ sư cloud | Nền cho mọi thứ |
| 4 | `services/shared` với `phone.ts` và bộ test 30 ca | Kỹ sư cloud | Được dùng lại ở khắp nơi, và là chỗ dễ sai nhất |
| 5 | `foundation-stack` và `data-stack` lên `dev` | Kỹ sư cloud | Cần có bảng và bucket mới làm được service |
| 6 | Xác minh 5 mục ⚠️ trong tài liệu này, cập nhật lại | Kiến trúc sư | Quyết định cách làm M06 và M08 |
| 7 | `disposition-api` chạy được đầu-cuối với một guide tối giản | Kỹ sư cloud + Connect | Chứng minh **cửa 1** hoạt động — rủi ro kỹ thuật lớn nhất của M08 |
| 8 | Chốt bộ mã kết quả với khách, thay `TODO(M01)` | PM/BA | Chặn `disposition-api`, guide, báo cáo và đối soát |
| 9 | Xin bộ số điện thoại test từ khách | PM/BA | Không có số test thì không test được gì |
**Ba việc trong tuần 1 mà nếu quên sẽ trả giá bằng lịch, không phải bằng PD:** bước 1, bước 2 và bước 9.
---
## Phụ lục — Đối chiếu nhanh 22 tính năng MVP với nơi thực hiện
| # | Tính năng | Cách làm | Thực hiện ở đâu |
|---:|---|:--:|---|
| 1 | Tài khoản và phân quyền | ⚙️ | `connect-core-stack.ts` |
| 2 | Nạp danh sách khách hàng | ⚙️+💻 | `services/list-ingest/`, `tools/csv-preflight/` |
| 3 | Tạo đợt gọi | ⚙️ | Campaign config + `MANUAL-CHECKLIST.md` |
| 4 | Tự động quay số | ⚙️ | Campaign config |
| 5 | Nhận biết máy trả lời | ⚙️ | `connect/flows/outbound-campaign.flow.json` |
| 6 | Mọi agent hiện cùng một số | ⚙️+📄 | `connect-core-stack.ts` + thủ tục M04 |
| 7 | 15 cuộc đồng thời | ⚙️+📄 | Thủ tục M04 + alarm |
| 8 | Màn hình làm việc của agent | ⚙️ | Agent Workspace, không code |
| 9 | Ghi kết quả cuộc gọi | 💻 | `services/disposition-api/` + `connect/guides/` |
| 10 | Danh sách không gọi | 💻 | `services/dnc-service/` — 4 lớp chặn |
| 11 | Hẹn gọi lại | 💻 | `services/callback-scheduler/` |
| 12 | Ghi âm | ⚙️ | Contact flow + S3/KMS |
| 13 | Chuyển thành văn bản | ⚙️ | Conversational Analytics `ja_JP` |
| 14 | Phân tích sắc thái | ⚙️ | `connect/rules/` |
| 15 | Nghe lén/nhắc riêng/chen ngang | ⚙️ | Security profile |
| 16 | Tìm lại cuộc gọi | ⚙️ | Contact search |
| 17 | Bảng theo dõi thời gian thực | ⚙️ | Dashboard có sẵn |
| 18 | Báo cáo | ⚙️ | Báo cáo lưu sẵn |
| 19 | Nhận cuộc gọi vào | ⚙️ | `connect/flows/inbound-*.json` |
| 20 | Cảnh báo và xử lý sự cố | ⚙️+💻 | `services/recording-audit/`, `observability-stack.ts`, `docs/runbooks/` |
| 21 | Đối soát dữ liệu | 💻 | `services/reconciliation/`, `services/contact-event-sink/` |
| 22 | Hồ sơ bảo mật | 📄 | `docs/ja/security/` |
---
## Danh sách các mục ⚠️ phải xác minh trước khi chốt thiết kế
Năm mục dưới đây là **giả định chưa kiểm chứng**. Cả năm đều phải được xác minh trên account thật ở M02/M03, trước khi bắt đầu viết code của M08.
| # | Giả định | Nếu sai thì sao | Phương án dự phòng |
|---:|---|---|---|
| 1 | Step-by-step guide gọi được Lambda và nhận được phản hồi | Không có form nhập kết quả trên màn hình agent | Dùng Connect Task + form; chi phí tương đương, đã nằm trong range M08 |
| 2 | Contact/campaign event có đủ trường cho đối soát và bắn qua EventBridge | `reconciliation` thiếu dữ liệu | Lấy từ Connect data lake qua Athena, chạy theo lô hằng ngày |
| 3 | CDK/CloudFormation phủ được contact flow, queue, security profile | Nhiều thứ phải làm tay | Mở rộng `MANUAL-CHECKLIST.md`, dùng `tools/flow-sync` đối chiếu định kỳ |
| 4 | Cấu hình campaign và Customer Profiles IaC hoá được | Không dựng lại tự động được sau sự cố | Ghi thủ tục chi tiết + diễn tập khôi phục bằng tay ở M11 |
| 5 | Trường ngữ cảnh của guide đủ tin cậy để xác định agent | Không quy trách nhiệm được ai nhập kết quả | Lấy `agentId` từ contact event bất đồng bộ, đối chiếu sau |
---
*Tài liệu này bổ sung cho `02-giai-phap-va-estimate.md` và không thay đổi bất kỳ con số estimate nào trong đó. Nếu việc xác minh 5 mục ⚠️ ở trên làm thay đổi thiết kế, phải cập nhật cả hai tài liệu.*