Architecture
1. Bối cảnh hệ thống (C4 L1)
2. Góc nhìn Container (C4 L2)
3. Góc nhìn Component (C4 L3) — Phân lớp nội bộ
| Lớp | Trách nhiệm |
|---|---|
| Routes | Bề mặt HTTP, khai báo trong RestPaths (src/common/constants.ts) |
| Controllers | Auth + cổng permission + ánh xạ DTO |
| Worker | Kafka → quyết định hành động phát hành theo issuanceMode / taxMethod; resync TaxInfo |
| Queue service | Enqueue (hash phân vùng) + xử lý job; chính sách retry/DLQ |
| Action services | Dựng payload adapter, gọi nhà cung cấp, ghi audit + WS |
| Connection components | Đăng ký client nhà cung cấp với iiapi / t-van lúc boot |
| Repositories | Truy vấn Drizzle (thuộc @nx/core), soft-delete |
4. Mục lục máy trạng thái
| Entity | Trạng thái | Sơ đồ |
|---|---|---|
Invoice.issuanceStatus | PENDING, PROCESSING, SUCCESS, FAILED, CANCELLED | → nhảy tới |
InvoiceRequest.claimState | PENDING, CLAIMED, EXPIRED | → nhảy tới |
Phát hành hoá đơn
| Từ | Sự kiện | Đến | Guard |
|---|---|---|---|
| (không) | createPendingInvoice | PENDING | một hoá đơn ORIGIN chưa xoá mỗi nguồn |
PENDING | worker dequeue | PROCESSING | đã resolve config nhà cung cấp active |
PROCESSING | nhà cung cấp 2xx | SUCCESS | response mang số hoá đơn |
PROCESSING | tạm thời (5xx / 429 / mạng) | PENDING | retryCount ≤ maxRetryCount → re-enqueue với backoff |
PROCESSING | vĩnh viễn 4xx (≠429) hoặc hết retry | FAILED | metadata.permanent = true khi 4xx |
SUCCESS | huỷ | CANCELLED | InvoiceCancellationService |
Nguồn:
Invoice.constants.ts(IssuanceStatuses),InvoiceIssuanceQueueService._handleIssuanceFailure. Retry mặc định:maxRetryCount = 3,retryDelayMinutes = [5, 15, 60](từInvoiceProviderConfig.retryMetadata).
Khai báo người mua
| Từ | Sự kiện | Đến | Guard |
|---|---|---|---|
| (không) | payment success, mode BUYER_SELF_SERVICE | PENDING | tạo token claim + deadline; job expiry delay đến deadline |
PENDING | người mua gửi | CLAIMED | enqueue issuance |
PENDING | job expiry | EXPIRED | áp dụng defaultBuyerInfo, enqueue issuance |
CLAIMED | job expiry | EXPIRED | issuance đã enqueue; no-op |
5. Kịch bản runtime
5.1 Payment success → phát hành bất đồng bộ
| Bước | Chi tiết |
|---|---|
| kiểm tra | order.status === COMPLETED, có saleChannelId, chưa có hoá đơn active (chưa huỷ) |
| resolve | mapping theo principal SALE_CHANNEL; guard khớp merchant-id |
| phân vùng | getPartitionByKey(orderId) (Java hashCode mod 3) — cùng đơn luôn cùng phân vùng |
| chế độ | AUTO/SCHEDULED để PENDING; REAL_TIME enqueue ngay; BUYER_SELF_SERVICE mở cửa sổ claim |
5.2 Merchant CDC → TaxInfo có thẩm quyền
| Bước | Chi tiết |
|---|---|
| phát hiện thay đổi | diff so với TaxInfo đã lưu, KHÔNG phải before-image của Debezium (đúng bất kể REPLICA IDENTITY) |
| ghi có thẩm quyền | TaxInfo (principalType=Merchant) là nguồn sự thật; FE đọc quan hệ merchant.taxInfo, không phải metadata.tax |
| onboarding | đánh dấu bước onboarding TAX_INFO hoàn tất (idempotent) |
5.3 Retry phát hành / DLQ
6. Mối quan tâm xuyên suốt
| Mối quan tâm | Service xử lý thế nào |
|---|---|
| AuthN | JWT (ES256, JWKS từ identity); VerifierApplication |
| AuthZ | Controller có cổng permission + AuthorizationService ở mức merchant |
| i18n | jsonb / label map { en, vi } (vd INVOICE_TYPE_DISPLAY_NAMES) |
| Logging | Key-value có cấu trúc (key: %s) |
| Idempotency | Issuance jobId = orderId; index unique-một-phần (sourceType, sourceId) cho ORIGIN; một InvoiceIssuance mỗi hoá đơn (lần thử mới nhất thắng) |
| Mã hoá credential | AES-256-GCM; khoá 32 byte (APP_ENV_INVOICE_CREDENTIALS_KEY) |
| Tin cậy webhook | Hai secret HMAC (merchant→platform, iiapi→platform) |
| Soft-delete | SoftDeletableRepository (deletedAt); index unique-một-phần loại trừ hàng đã xoá |
| IDs | Snowflake qua IdGenerator (worker id ⚠️ chưa đặt) |