Toàn bộ tài liệu dự án trong một trang: tổng quan nghiệp vụ, kiến trúc kỹ thuật, và sơ đồ hệ thống.
Cập nhật 14/08/2026 · Bản nội bộ — có phần chia việc & báo giá
Dành cho Ban lãnh đạo
Kazuki Ebike · Phòng Kinh doanh
Trợ lý trên Telegram trả lời chính xác câu hỏi “còn bao nhiêu xe thật sự bán được” — ngay tại thời điểm hỏi, không phải số của sáng nay.
Trong ngày đã có các lệnh xuất giữ mất 35 xe, nhưng chứng từ chưa kịp lên AMIS. Phòng Kinh doanh nhìn số 50 để chốt đơn, tới lúc xuất kho mới phát hiện không đủ xe — phải đổi cơ cấu đơn, kho và kinh doanh cùng mất thời gian.
Độ trễ này khoảng 3 đến 4 tiếng: dắt xe từ nhiều kho mất 2–3 tiếng, kế toán ghi phiếu lên AMIS thêm 30 phút đến 1 tiếng.
Kế toán gửi file như hiện nay, không đổi thói quen. Hệ thống đọc bảng bằng chương trình chứ không đọc bằng mắt AI, nên không có chuyện nhìn nhầm số.
Những lệnh đã nhắn nhưng chưa làm phiếu xuất vẫn được giữ chỗ tiếp — không bị xóa mất như khi ghi tay.
Trừ kho thành phẩm trước; thiếu thì mới giữ chỗ ở kho sản xuất và báo rõ là xe chưa xuất được ngay.
Kèm luôn nguyên nhân nếu thiếu: đơn nào đã lấy mất màu đó, mỗi đơn bao nhiêu xe.
| Model | Màu | SL | Kho TP | Kho SX | Kết quả | Ghi chú |
|---|---|---|---|---|---|---|
| Libra New | Xám Ánh Hồng | 2 | 12→10 | 0 | Đủ | |
| Libra New | Ghi Sần | 4 | 0 | 19→15 | Kẹt Kho SX | Chờ nhập về TP |
| Slay New | Ghi Sần | 10 | 0 | 3 | Thiếu 7 xe | Minh Vương lấy 2, Dương Huệ lấy 2 |
| Slay New | Xám Ánh Hồng | 2 | 0 | 0 | Hiện tồn 0 | Baseline sáng không có màu này |
Kho thành phẩm và kho sản xuất luôn tách riêng hai số, kể cả khi một bên bằng 0 — vì hai tình huống này xử lý khác nhau.
Gõ sai hoặc viết tắt lạ thì hệ thống hỏi lại, không tự suy ra. Thà hỏi thừa còn hơn trừ nhầm kho.
Phần AI chỉ đọc chữ và viết câu trả lời. Toàn bộ phép cộng trừ chạy bằng cơ sở dữ liệu.
Ai lên lệnh trước giữ trước. Xe kẹt lâu thì hệ thống nhắc đúng bộ phận liên quan, còn quyết định vẫn là của anh.
Chúng tôi không đề nghị anh tin vào lời hứa “chính xác 100%”. Mỗi sáng khi có báo cáo AMIS mới, hệ thống tự so số nó dự đoán cuối ngày hôm trước với số AMIS thật và báo chênh lệch theo từng mã xe.
Chạy song song một đến hai tuần: hệ thống tính và báo, nhưng anh vẫn quyết như hiện nay. Khi chênh lệch bằng 0 liên tục thì mới chuyển sang dùng chính thức.
Toàn bộ nội dung trong trang này. Nghiệp vụ đã chốt xong với anh, không còn điểm nào phải chờ. Phạm vi rõ nên báo giá cố định được.
Đối soát số khung và số máy từ bốn nguồn trước khi xuất hóa đơn, để kế toán không phải gõ tay từng chiếc. Cần buổi làm việc với chị Trang trước khi báo giá — chưa hỏi người phụ trách thì con số đưa ra chỉ là ước đoán.
ARCHITECTURE-SPINE — hợp đồng ràng buộc cho người xây
Tài liệu này là nguồn schema và ranh giới duy nhất. Nó thay thế hoàn toàn bản spine 13/08. architecture-consensus.md hạ cấp thành nhật ký quyết định — khi hai bên chỏi nhau, spine thắng.
Hexagonal (Ports & Adapters), lõi tất định thuần, AI nằm ngoài lõi.
Ba vòng, phụ thuộc chỉ đi vào trong:
core/)** — hàm thuần, không I/O, không mạng, không LLM, không đồng hồ. Nhận dữ liệu vào, trả dữ liệu ra. Toàn bộ phép tính tồn kho và đối soát nằm ở đây. Test được không cần mock.ports/)** — giao diện trừu tượng lõi dùng để nói chuyện với bên ngoài: đọc chatlog, đọc CSDL, phát tin nhắn, sinh file.adapters/)** — hiện thực cụ thể của cổng: SQLite, Telegram relay, pdfplumber, openpyxl, LLM normalizer.AI LLM là một adapter, không bao giờ là lõi. Nó chỉ làm hai việc: bóc chuỗi thô từ văn bản gõ tay thành JSON, và diễn đạt danh sách mã lỗi đã tính sẵn thành câu tiếng Việt. Mọi con số đi qua lõi tất định.
Ánh xạ paradigm sang namespace CSDL: core_ (dữ liệu dùng chung), inv_ (Module 01), fin_ (Module 02).
Hướng phụ thuộc — mũi tên là "được phép phụ thuộc vào":
kazuki.db. Node nhận webhook Telegram, đẩy payload sang Python qua socket cục bộ, gọi sendMessage rồi trả message_id về cho Python. Python ghi inv_bot_messages. Mọi lệnh ghi CSDL xuất phát từ một tiến trình Python duy nhất.SQLITE_BUSY do transaction hoãn nâng cấp; khóa ngoại không hiệu lực; kiểu dữ liệu trôikazuki.db, journal_mode=WAL, busy_timeout=5000, foreign_keys=ON đặt lại trên từng connection, mọi bảng khai báo STRICT. Mọi transaction ghi mở bằng BEGIN IMMEDIATE — cấm transaction hoãn rồi nâng cấp lên ghi. Cấm giữ transaction xuyên qua lời gọi LLM, parse PDF/Excel, hay ghi file.core_vehicles mâu thuẫn như hiện trạngvin lẫn engine_no** khớp cùng một bản ghi core_vehicles. Khớp vin đơn lẻ, hoặc khớp bộ ba vin + model + màu, đều không đủ.Căn cứ: dữ liệu đăng kiểm thật cho thấy xe cùng lô mang số khung liền nhau (RNKY6EKUMSDA02621, ...02622), cùng model, cùng màu. Sai một chữ số là rơi trúng xe kế bên và mọi kiểm tra thuộc tính đều lọt. Dãy số máy chạy độc lập với dãy số khung nên cặp này mới là kiểm tra chéo thật. Tem trên xe có sẵn cả hai số.
core_customers (đại lý nhận xe) khỏi core_customer_entities (pháp nhân + MST xuất hóa đơn), quan hệ 1–N. Cấm nhét tax_code vào core_customers. Khi LXN không chỉ rõ MST mà đại lý có từ 2 pháp nhân, hệ thống hỏi lại người, tuyệt đối không suy đoán theo lịch sử, tần suất hay tên gần giống.available luôn được tính lại từ (baseline hiện hành, tập LXN còn hiệu lực) bằng một truy vấn SQL. Cấm lưu tồn dạng cột cộng/trừ tại chỗ. Nhờ vậy mọi thao tác tính lại là idempotent. available_tp = baseline.stock_tp − Σ qty_tp (đơn ACTIVE, còn hiệu lực) available_sx = baseline.stock_sx − Σ qty_sx (đơn ACTIVE, còn hiệu lực)
Không kẹp về 0. Số âm là tín hiệu phải báo, không phải lỗi để giấu.
SETTLED. Dòng còn HOLD_SX_PENDING hoặc chưa có xác nhận xuất phiếu đi xuyên baseline, giữ nguyên created_at gốc để xếp thứ tự FIFO. Khi có file AMIS sửa trong cùng ngày, effective_at kế thừa từ file mở ngày, không nhảy theo file mới.HOLD_SX_PENDING không tự hủy. Batch GENERATED không tự nhả. Mọi chuyển trạng thái giải phóng đều do người thực hiện và có ghi log.IMMEDIATE; bảo đảm chống trùng hóa đơn nằm ở tầng CSDL, không ở tầng ứng dụng.CREATE UNIQUE INDEX ux_vin_active ON fin_batch_items(vin) WHERE state IN ('GENERATED','INVOICED'). Trạng thái VOIDED làm bản ghi rơi khỏi index nên số khung dùng lại được. Chuyển sang VOIDED chỉ do người thực hiện, bắt buộc ghi voided_by và void_reason, không có cơ chế tự động.fin_batch_items mang line_type (VEHICLE | ACCESSORY), vin nullable, kèm CHECK (line_type <> 'VEHICLE' OR vin IS NOT NULL). Khóa chính là khóa thay thế. SQLite coi mỗi NULL trong unique index là giá trị riêng nên dòng phụ kiện không đụng ux_vin_active.raw_model_text, raw_color_text, qty) và văn bản cảnh báo dựng từ mã lỗi đã tính sẵn. Cấm LLM sinh sku_id, vin, tax_code, hay bất kỳ ô nào của file Excel upload. Ánh xạ chuỗi sang định danh là tra cứu tất định trong Python/SQL.rapidfuzz chỉ được dùng để gợi ý hiển thị cho người, không bao giờ tự áp.-TP), sau đó làm giàu bằng dữ liệu nội bộ. Engine không tự sinh dòng hóa đơn từ CSDL riêng. Mỗi ứng viên đi qua chuỗi kiểm tra tất định, dừng ở lỗi đầu tiên, mỗi lỗi có mã riêng — bao gồm ENGINE_MISMATCH theo AD-4.chatlogs/(chat_id, message_id), mốc so sánh là edit_date nếu có, không thì timestamp. Mốc nghiệp vụ luôn là timestamp (giờ tin vào nhóm hiện tại) — tuyệt đối không dùng forward.date. Parser phải chịu được bản ghi trước 14/08/2026 thiếu chat_title, edit_date, media.saved_path; với nhóm bản ghi đó, thứ tự dòng trong file là thứ tự đến.test-history.md, kèm các đáp án sai đã ghi nhận làm negative assertion: VCGT không được thành VC GT 1; ghi sần không được bằng ghi đá; Slay 240W đen sần phải đọc ra 5 chứ không phải 15.core_skusapp/ingest_baseline.py) được tạo mới core_skus. Mọi đường khác — bóc tách LXN, nạp file đăng kiểm, đối soát Module 02 — chỉ được tra cứu; không tìm thấy thì chặn và hỏi lại người theo AD-13, tuyệt đối không tự tạo. Alias mới chỉ ghi khi có người xác nhận, kèm confirmed_by và confirmed_at.Vì sao cần: nếu không có luật này, hai thành phần đều tuân thủ đủ mọi AD khác vẫn có thể tạo ra hai sku_id cho cùng một chiếc xe — một từ chữ trong file AMIS, một từ chữ trong file đăng kiểm — và tồn kho bị chia đôi mà không ai thấy.
core_vehicles trỏ về core_skus, không mang tên model/màu tự docore_vehicles mang sku_id (khóa ngoại) làm định danh chủng loại. Tên model và màu đọc từ file đăng kiểm được giữ lại dưới dạng cột thô chỉ để truy vết (raw_model_text, raw_color_text), không dùng để so khớp. Việc quy tên thô về sku_id đi qua đúng pipeline ở AD-13.Vì sao cần: file đăng kiểm ghi màu thương mại ở cột "Màu xe (Thực tế)" còn báo cáo AMIS dùng cách viết riêng. Nếu mỗi bảng giữ một chuỗi tự do thì phép đối chiếu model/màu giữa core_vehicles và dòng LXN hoặc luôn trượt, hoặc trùng nhầm — cả hai đều hỏng mà vẫn "đúng luật".
core_vehicles là thẩm quyềninv_order_vehicles.engine_no là con số Thủ kho khai, giữ nguyên để truy vết kể cả khi sai. core_vehicles.engine_no là thẩm quyền. Đối soát là so hai giá trị này; lệch thì báo ENGINE_MISMATCH và chặn, không bao giờ ghi đè bên nào.| Concern | Convention |
|---|---|
| Đặt tên bảng | Tiền tố namespace bắt buộc: core_ dùng chung, inv_ Module 01, fin_ Module 02. Số nhiều, snake_case, tiếng Anh. |
| Đặt tên cột | snake_case tiếng Anh. Khóa ngoại là <bảng_số_ít>_id. Cột thời gian kết thúc bằng _at, cột ngày kết thúc bằng _date. |
| Thời gian | ISO8601 UTC lưu dạng TEXT. Không dùng CURRENT_TIMESTAMP của SQLite (không có T, không có Z). Sinh ở tầng ứng dụng. |
| Tiền | INTEGER, đơn vị đồng. Cấm REAL. |
| Số lượng | INTEGER. Cấm số thực trong mọi phép tính tồn kho. |
| Số khung / số máy / mã hàng | Ép kiểu chuỗi ngay khi đọc Excel. pandas và openpyxl tự suy kiểu, một số khung toàn chữ số sẽ thành 1.23e+16 và mất dữ liệu trước khi bất kỳ kiểm tra nào chạy. |
| Nạp dữ liệu | Append-only kèm ingest_id. Không sửa đè bản ghi cũ. Nạp lại cùng một file phải idempotent (khóa tự nhiên từ chính file). |
| Transaction | BEGIN IMMEDIATE cho mọi đường ghi. Parse, gọi LLM, sinh file đều nằm ngoài transaction. |
| Mã lỗi | SCREAMING_SNAKE_CASE, một mã cho một nguyên nhân. Tầng lõi trả mã; tầng LLM diễn đạt thành câu. |
| Ngôn ngữ | Định danh trong code 100% tiếng Anh. Văn bản gửi người dùng 100% tiếng Việt. |
| Mũi tên trong tài liệu | Ký tự Unicode →. Cấm LaTeX. |
Đã kiểm chứng trên web ngày 14/08/2026. Đây là seed — khi có code thì code làm chủ.
| Name | Version |
|---|---|
| Python | 3.12.3 (bản hệ thống sẵn có) |
| SQLite | 3.45.1 (đi kèm Python 3.12.3) — đã chạy thử STRICT trên máy, đạt; STRICT cần ≥ 3.37 |
| pdfplumber | 0.11.10 |
| openpyxl | 3.1.5 |
| rapidfuzz | 3.14.5 — chỉ dùng sinh gợi ý cho người, xem AD-13 |
| uv | 0.12.3 (quản lý môi trường & phụ thuộc) |
| Node.js + AI Gateway | hiện trạng đã có, relay thuần — không đổi [ADOPTED] |
Namespace core_ và inv_ khai báo đầy đủ vì Module 01 phải code được ngay. Namespace fin_ khai báo tới mức invariant — chi tiết cột chờ file mẫu MISA.
-- ══ CORE: dùng chung 2 module ══
CREATE TABLE core_skus (
sku_id INTEGER PRIMARY KEY,
model_name TEXT NOT NULL,
color_name TEXT NOT NULL,
merged_group_id TEXT NOT NULL, -- VC GT + VC GT 1 dùng chung 1 group
created_at TEXT NOT NULL,
UNIQUE(model_name, color_name)
) STRICT;
CREATE TABLE core_sku_aliases (
alias_id INTEGER PRIMARY KEY,
sku_id INTEGER NOT NULL REFERENCES core_skus(sku_id),
normalized_alias TEXT NOT NULL UNIQUE, -- đã hạ chữ thường, gom khoảng trắng
raw_alias TEXT NOT NULL,
confirmed_by TEXT, -- alias mới chỉ thêm khi người xác nhận
confirmed_at TEXT
) STRICT;
CREATE TABLE core_customers (
customer_id INTEGER PRIMARY KEY,
customer_code TEXT NOT NULL UNIQUE,
shipping_customer_name TEXT NOT NULL -- đại lý NHẬN xe
) STRICT;
CREATE TABLE core_customer_entities ( -- AD-5: 1 đại lý N pháp nhân
entity_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES core_customers(customer_id),
legal_name TEXT NOT NULL,
tax_code TEXT NOT NULL,
invoice_address TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
UNIQUE(customer_id, tax_code)
) STRICT;
CREATE TABLE core_vehicles ( -- nguồn Đăng kiểm
vin TEXT PRIMARY KEY, -- 17 ký tự, 9 đầu là mã kiểu loại theo model
engine_no TEXT NOT NULL, -- AD-4: bắt buộc khớp cùng vin; đây là THẨM QUYỀN (AD-23)
sku_id INTEGER NOT NULL REFERENCES core_skus(sku_id), -- AD-22
raw_model_text TEXT NOT NULL, -- lấy từ HEADER file, không phải cột; chỉ truy vết
raw_color_text TEXT NOT NULL, -- lấy cột "Màu xe (Thực tế)", KHÔNG lấy "Màu sơn"
cert_no TEXT,
issued_date TEXT,
ingest_id INTEGER NOT NULL
) STRICT;
-- ══ INV: Module 01 ══
CREATE TABLE inv_baselines (
baseline_id INTEGER PRIMARY KEY,
business_date TEXT NOT NULL,
received_at TEXT NOT NULL,
effective_at TEXT NOT NULL, -- AD-7: file sửa trong ngày KẾ THỪA, không nhảy
is_day_open INTEGER NOT NULL,
supersedes_id INTEGER REFERENCES inv_baselines(baseline_id),
source_file_path TEXT NOT NULL
) STRICT;
CREATE TABLE inv_baseline_stocks (
stock_id INTEGER PRIMARY KEY,
baseline_id INTEGER NOT NULL REFERENCES inv_baselines(baseline_id),
sku_id INTEGER NOT NULL REFERENCES core_skus(sku_id),
stock_tp INTEGER NOT NULL DEFAULT 0,
stock_sx INTEGER NOT NULL DEFAULT 0,
UNIQUE(baseline_id, sku_id)
) STRICT;
CREATE TABLE inv_orders (
order_id INTEGER PRIMARY KEY,
tg_chat_id TEXT NOT NULL,
tg_msg_id TEXT NOT NULL,
issuer_id TEXT NOT NULL,
customer_id INTEGER REFERENCES core_customers(customer_id),
tax_entity_id INTEGER REFERENCES core_customer_entities(entity_id),
scheduled_invoice_date TEXT,
order_type TEXT NOT NULL DEFAULT 'STANDARD'
CHECK(order_type IN ('STANDARD','DIRECT_TRANSFER')),
status TEXT NOT NULL DEFAULT 'ACTIVE'
CHECK(status IN ('ACTIVE','CANCELLED')),
created_at TEXT NOT NULL,
UNIQUE(tg_chat_id, tg_msg_id) -- AD-16: khóa idempotency
) STRICT;
CREATE TABLE inv_order_items (
item_id INTEGER PRIMARY KEY,
order_id INTEGER NOT NULL REFERENCES inv_orders(order_id),
sku_id INTEGER NOT NULL REFERENCES core_skus(sku_id),
qty_tp INTEGER NOT NULL DEFAULT 0,
qty_sx INTEGER NOT NULL DEFAULT 0,
fulfil_status TEXT NOT NULL DEFAULT 'RESERVED'
CHECK(fulfil_status IN ('RESERVED','HOLD_SX_PENDING','SHIPPED','SETTLED','RELEASED'))
) STRICT;
CREATE TABLE inv_order_vehicles ( -- Nguồn 3: Thủ kho xác nhận, dùng chung 2 module
item_id INTEGER NOT NULL REFERENCES inv_order_items(item_id),
vin TEXT NOT NULL REFERENCES core_vehicles(vin),
engine_no TEXT NOT NULL, -- AD-4: lưu để đối chiếu cặp
confirmed_by TEXT NOT NULL,
confirmed_at TEXT NOT NULL,
input_kind TEXT NOT NULL CHECK(input_kind IN ('TEXT','OCR','SELECT')),
PRIMARY KEY (item_id, vin)
) STRICT;
CREATE TABLE inv_bot_messages (
log_id INTEGER PRIMARY KEY,
correlation_id TEXT NOT NULL UNIQUE, -- sinh ở client, chống ghi trùng khi retry
telegram_message_id TEXT,
chat_id TEXT NOT NULL,
message_type TEXT NOT NULL,
payload_json TEXT NOT NULL,
created_at TEXT NOT NULL
) STRICT;
-- ══ FIN: Module 02 — mức invariant ══
-- fin_misa_stockout : FILE A đã parse (AD-15), append-only kèm ingest_id
-- fin_misa_invoiced : Báo cáo 4a đã parse, dùng xác nhận hóa đơn đã phát hành
-- fin_upload_batches : mỗi lần sinh file = 1 batch, có file_sha256 và người tạo
CREATE TABLE fin_batch_items (
batch_item_id INTEGER PRIMARY KEY, -- AD-11: khóa thay thế
batch_id INTEGER NOT NULL REFERENCES fin_upload_batches(batch_id),
line_type TEXT NOT NULL CHECK(line_type IN ('VEHICLE','ACCESSORY')),
vin TEXT REFERENCES core_vehicles(vin), -- NULL với phụ kiện
engine_no TEXT, -- AD-4
item_id INTEGER REFERENCES inv_order_items(item_id),
tax_code TEXT NOT NULL, -- bản chụp lúc sinh file
state TEXT NOT NULL
CHECK(state IN ('GENERATED','INVOICED','RELEASED','VOIDED')),
invoice_no TEXT,
supersedes_invoice_no TEXT, -- neo sẵn, trống ở Phase 1
voided_by TEXT,
void_reason TEXT,
CHECK (line_type <> 'VEHICLE' OR (vin IS NOT NULL AND engine_no IS NOT NULL)),
CHECK (state <> 'VOIDED' OR (voided_by IS NOT NULL AND void_reason IS NOT NULL))
) STRICT;
CREATE UNIQUE INDEX ux_vin_active ON fin_batch_items(vin)
WHERE state IN ('GENERATED','INVOICED'); -- AD-10
src/
core/ # hàm thuần — không I/O, không LLM, không đồng hồ
availability.py # AD-6 derived state, AD-7 carry-forward
sku_matching.py # AD-13 alias dài trước, ba kết cục
reconcile.py # AD-15 chuỗi kiểm tra 4 nguồn, AD-4 cặp vin+engine
delta_report.py # AD-18
ports/ # giao diện trừu tượng
repository.py
messenger.py
file_source.py
adapters/
sqlite_repo.py # AD-2 pragma, BEGIN IMMEDIATE
telegram_relay.py # AD-1 nói chuyện với Gateway qua socket
pdf_parser.py # pdfplumber
excel_parser.py # openpyxl, ép chuỗi cho vin/engine/mã hàng
llm_normalizer.py # AD-12 chỉ trả chuỗi thô
chatlog_reader.py # AD-16
app/ # điều phối use-case, transaction nằm ở đây
ingest_baseline.py
handle_lxn.py
handle_query.py
handle_cancel_edit.py
migrations/ # .sql đánh số, ANSI, không dùng cú pháp riêng SQLite
tests/
golden/ # AD-20 — 17 đơn thật + negative assertions
| Capability | Lives in | Governed by |
|---|---|---|
| M01-CAP-1 Baseline AMIS đầu ngày | app/ingest_baseline.py, adapters/pdf_parser.py | AD-7, AD-18, AD-2 |
| M01-CAP-2 Chuẩn hóa & kiểm tra SKU | core/sku_matching.py, adapters/llm_normalizer.py | AD-12, AD-13, AD-14 |
| M01-CAP-3 Tính & giữ tồn realtime | core/availability.py | AD-6, AD-7, AD-8 |
| M01-CAP-4 Giao thức Bot & xác nhận | app/handle_query.py, adapters/telegram_relay.py | AD-1, AD-17, AD-19 |
| M01-CAP-5 Nhập-xuất thẳng (net = 0) | core/availability.py | AD-6 |
| M02-CAP-1 Đối soát 4 nguồn | core/reconcile.py | AD-4, AD-15, AD-9 |
| M02-CAP-2 Sinh file upload MISA | adapters/excel_parser.py | AD-10, AD-11, AD-12 |
| M02-CAP-3 Cảnh báo & khóa luồng | app/ + adapters/llm_normalizer.py | AD-12, AD-19 |
| M02-CAP-4 Lưu vết tin nhắn Bot | inv_bot_messages | AD-1, AD-16 |
| M02-CAP-5 Danh mục pháp nhân | core_customer_entities | AD-5 |
| M02-CAP-6 Vòng đời cờ hóa đơn | fin_batch_items | AD-8, AD-10 |
6.UP MQC** — vị trí cột nạp từ file cấu hình khai báo MISA_MAP. Chờ file mẫu thật từ Chị Trang. Không chặn schema hay Thuật toán 1.supersedes_invoice_no.docs/references.md kèm ba điều kiện nên mở lại.[GIẢ ĐỊNH — cần soát] Toàn bộ namespace fin_ và các AD liên quan Module 02 chưa qua xác nhận của Chị Trang. Chúng được viết ở mức invariant để đáp án nào cũng khớp (xem AD-10, AD-11), nhưng vẫn là giả định cho tới khi có buổi phỏng vấn. Module 01 thì không có giả định nào chưa xác nhận.
Bản chiếu của kiến trúc theo bốn mức
Ai dùng, hệ thống nói chuyện với cái gì bên ngoài.
Điểm cần thấy: không có đường tự động nào giữa hệ thống và MISA. Dữ liệu đi qua file do Kế toán gửi tay. Đây là quyết định Phase 1, không phải hạn chế kỹ thuật.
Hệ thống gồm những tiến trình nào, ai nói chuyện với ai.
Hai bất biến nhìn thấy được trên sơ đồ này: chỉ có một mũi tên vào CSDL, và LLM nằm ngoài lõi.
Quy tắc đọc sơ đồ: core/ không có mũi tên nào đi ra. Nếu lúc code thấy cần một mũi tên như vậy thì thiết kế đã sai chỗ nào đó, không phải quy tắc sai.
Không phải sơ đồ C4 chuẩn, nhưng đây là thứ khó nắm nhất khi đọc hệ thống.
Hai chỗ dễ hiểu sai: **GiuChoSX không tự hết hạn (AD-8), và Voided chỉ do người bấm**, không có đường tự động nào dẫn tới nó (AD-10).
Ước lượng công sức & cơ sở báo giá
Chia theo ranh giới kiến trúc, không theo màn hình hay tính năng, để mỗi epic có thể giao cho một người và hoàn thành độc lập. Đơn vị ước lượng là ngày công của một dev quen việc, chưa tính thời gian chờ khách phản hồi.
Mức tin cậy: 🟢 chắc (đủ dữ kiện, đã có mẫu thật) · 🟡 vừa (rõ nghiệp vụ, chưa có mẫu) · 🔴 thấp (chưa có xác nhận khách)
Dựng kazuki.db theo schema trong spine, migration đánh số, lớp sqlite_repo với đủ pragma của AD-2, khung thư mục core/ports/adapters/app. Xong khi: chạy migration sạch trên máy trắng; test chứng minh foreign_keys và STRICT có hiệu lực; BEGIN IMMEDIATE được ép ở mọi đường ghi. Ràng buộc: AD-1, AD-2, AD-3
pdfplumber bóc bảng tồn kho, ép chuỗi cho mọi cột mã, nạp inv_baselines + inv_baseline_stocks, idempotent khi nạp lại cùng file. Xong khi: parse đúng 100% cả 5 file PDF thật đang có; nạp lại lần hai không sinh thêm dòng. Rủi ro đã biết: đây chính là chỗ agent cũ đọc nhầm 5 thành 15 — bắt buộc có test khoá con số đó. Ràng buộc: AD-2, AD-21
core_skus, core_sku_aliases, chuẩn hóa bằng luật cố định, khớp alias dài trước, ba kết cục, hàng đợi alias chưa giải được. Xong khi: 17 đơn thật khớp đúng; các negative assertion đều chặn (VCGT ≠ VC GT 1, ghi sần ≠ ghi đá). Ràng buộc: AD-13, AD-14, AD-21
Truy vấn tính tồn, phân bổ TP trước rồi SX, cờ HOLD_SX_PENDING, không kẹp âm. Xong khi: chạy lại 17 đơn thật ra đúng số anh Đức Anh đã tính tay. Ràng buộc: AD-6, AD-7, AD-8
Reset mốc, kế thừa effective_at, carry-forward theo tình trạng xuất phiếu, danh sách chờ GĐKD tick xác nhận mỗi sáng. Ràng buộc: AD-7
Trả JSON chuỗi thô, không sinh định danh, hợp đồng đầu ra chặt, null khi không chắc. Ràng buộc: AD-12
Bảng 6 cột theo đúng mẫu đã chốt, nguồn gốc số liệu inline, truy vết đơn nào đã lấy màu đó, hủy/sửa bằng reply tin gốc, lưu inv_bot_messages. Ràng buộc: AD-16, AD-17, AD-19
So tồn dự đoán hôm trước với AMIS mới, báo chênh lệch theo mã, kích hoạt theo sự kiện nhận file. Vì sao không cắt được: đây là thứ duy nhất biến cam kết "chính xác 100%" thành số đo được. Cũng là công cụ gỡ lỗi khi khách nói sai. Ràng buộc: AD-18
17 đơn thật thành fixture, các đáp án sai thành negative assertion, harness replay từ chatlog. Ràng buộc: AD-20
Cộng Giai đoạn A + B: 20 ngày công. Đây là phạm vi demo cho anh Đức Anh và là Gói 1 báo giá.
Đã có file mẫu thật và đã bóc được cấu trúc: model ở header, lấy cột "Màu xe (Thực tế)", lọc Kết quả = Đạt, khóa idempotency từ số báo cáo + lần. Ràng buộc: AD-21, AD-22
Reply vào tin LXN, khớp cặp (vin, engine_no), ưu tiên cho chọn từ danh sách thay vì gõ tay. Lưu ý triển khai: đổi thói quen hàng ngày của Thủ kho — không đưa vào trước khi ký hợp đồng. Giá trị kép: đây cũng là tín hiệu xuất phiếu mà Module 01 đang thiếu, bỏ được việc GĐKD ghi tay mỗi sáng. Ràng buộc: AD-4, AD-23
Chuỗi kiểm tra tất định, dừng ở lỗi đầu, gồm ENGINE_MISMATCH. Ràng buộc: AD-4, AD-15
6.UP MQC)*Cộng Giai đoạn C + D: 17,5 ngày công — nhưng D1 và D3 đang bị chặn, và toàn bộ D chưa có xác nhận nào từ chị Trang. Đây là Gói 2, chỉ nên báo giá sau khi phỏng vấn.
Đường găng của demo: A1 → A3 → B1 → B4. Rút ngắn được thì rút ở đây, không phải chỗ khác.
Gói 1 (A + B, 20 ngày) — phạm vi rõ, dữ kiện đầy đủ, đã có dữ liệu thật để kiểm chứng. Báo giá cố định được, rủi ro vượt dự toán thấp.
Gói 2 (C + D, ~17,5 ngày) — con số này là ước lượng thô, chưa nên đưa vào hợp đồng. Hai hạng mục đang bị chặn vì thiếu file mẫu, và toàn bộ nghiệp vụ chưa qua xác nhận của người phụ trách. Nên chào một buổi khảo sát với chị Trang trước, rồi mới báo giá phần này.
Nên tách hai gói thay vì gộp một con số. Lý do nói thẳng được với khách: chưa phỏng vấn người phụ trách thì báo giá chỉ là đoán — đoán thiếu thì mình chịu, đoán thừa thì khách thấy đắt. Và demo Gói 1 thành công sẽ tự bán Gói 2, lúc đó đã có dữ liệu thật để báo đúng.
Chưa tính trong hai gói trên: thời gian chạy pilot song song 1–2 tuần (chỉ giám sát báo cáo Delta, không tốn ngày công phát triển), đào tạo người dùng, và vận hành sau bàn giao.