Files
callcenter/05-kien-truc-va-codebase.md

71 KiB
Raw Permalink Blame History

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.

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

flowchart TB
    subgraph L1["Tầng 1 — Người dùng"]
        AG[Agent<br/>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<br/>softphone WebRTC]
        AWS2[Contact Flows<br/>luồng gọi ra và gọi vào]
        AWS3[Outbound Campaigns<br/>quay số, AMD, retry, lịch]
        AWS4[Customer Profiles + Segments]
        AWS5[Conversational Analytics<br/>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<br/>form nhập kết quả]
        G2[Invoke Lambda block<br/>trong contact flow]
        G3[EventBridge<br/>contact + campaign events]
        G4[S3 Event<br/>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<br/>cc-core single table)]
        S3R[(S3 recordings<br/>+ analytics, KMS)]
        S3D[(S3 data<br/>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).

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-<env>/inbound/lists/<yyyy-mm-dd>/<tên-file>.csv   → kích hoạt list-ingest
s3://cc-data-<env>/outbound/reports/<yyyy-mm-dd>/...           → báo cáo dòng lỗi trả cho quản lý
s3://cc-recordings-<env>/connect/<instance>/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.

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)

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-<env> 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-<env>

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#<externalId> PROFILE PHONE#<e164> / CUST#<externalId> tên, timezone, nguồn đồng ý, thời điểm đồng ý, cờ DNC, nguồn danh sách —
Mục DNC DNC#<e164> ENTRY DNCDATE#<yyyy-mm-dd> / <e164> 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#<contactId> DISPOSITION AGENT#<agentId> / <ISO ts> 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#<contactId> META DAY#<yyyy-mm-dd> / <ISO ts> 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#<callbackId> META DUE#<yyyy-mm-dd> / <HH:mm>#<callbackId> 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#<e164> SLOT#<yyyy-mm-dd HH> — callbackId đang giữ chỗ 90 ngày
Đối soát ngày RECON#<yyyy-mm-dd> 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#<yyyy-mm-dd> <ISO ts>#<uuid> ACTOR#<userId> / <ISO ts> hành động, đối tượng, giá trị cũ/mới 2.555 ngày
Lô nạp danh sách IMPORT#<importId> META IMPDATE#<yyyy-mm-dd> / <importId> 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#<id>, SK=DISPOSITION)
Số này có bị cấm gọi không? get(PK=DNC#<e164>, SK=ENTRY)
Hôm nay có lịch hẹn nào đến hạn? query(GSI1, GSI1PK=DUE#<hôm nay>)
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#<hôm qua>) rồi lọc
Số điện thoại này là khách nào? query(GSI1, GSI1PK=PHONE#<e164>)
Ai đã bấm "không gọi nữa" cho số này? get(PK=DNC#<e164>) + 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:

{
  "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:

{
  "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ỹ.

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):

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:

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ý.

flowchart LR
    A[Agent bấm không gọi nữa] --> B[Ghi DNC vào DynamoDB<br/>lớp 1: nguồn sự thật]
    B --> C[Gắn cờ trên hồ sơ Customer Profiles<br/>lớp 2: lọc khi tạo segment]
    D[Nạp danh sách mới] --> E[Đối chiếu DNC<br/>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<br/>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#<số>#SLOT#<ngày giờ> 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ả:

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

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

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.

flowchart LR
    subgraph A["Phương án A — hiện tại"]
        OC[Outbound Campaigns ⚙️<br/>AWS lo hết]
    end
    subgraph B["Phương án B"]
        DS[dialer-service 💻<br/>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.