Architecture
1. Bối cảnh hệ thống (C4 L1)
2. Góc nhìn Container (C4 L2)
Một deployable duy nhất chạy dưới role api, worker, hoặc cả hai, chọn bằng
APP_ENV_APPLICATION_ROLES.
3. Góc nhìn Component (C4 L3) - Phân lớp nội bộ
| Lớp | Trách nhiệm |
|---|---|
| Controllers | Auth + assertMerchantAccess + ánh xạ DTO (action/CRUD; tax-tier, tax-config, identifier là CRUD đọc-chủ-yếu trên dữ liệu seed) |
| Service Queue/Snapshot | Logic vòng đời, idempotency, enqueue |
| Worker service | Điều phối pipeline (fetch → render → encrypt → upload) |
| Fetchers / generators / crypto / metalink | Các pha pipeline đơn trách nhiệm |
| Repositories | Truy vấn Drizzle, bộ chỉnh job-status liền mạch, soft-delete |
| Components | Kafka producer/consumer, recovery sweep, WS emitter |
4. Chỉ mục máy trạng thái
| Entity | Trạng thái | Sơ đồ |
|---|---|---|
Ledger | PENDING, NEW, PROCESSING, COMPLETED, FAIL, SETTLED, ARCHIVED | → nhảy tới |
LedgerJob | PENDING, PROCESSING, COMPLETED, REJECTED (+DRAFT = đã tạo, chưa đẩy queue) | → nhảy tới |
* Luồng active do người dùng điều khiển dùng PENDING (draft) ↔ SETTLED (đã finalize); ARCHIVED đánh dấu version bị thay thế. Các trạng thái tạo (NEW/PROCESSING/COMPLETED/FAIL) có định nghĩa trên enum nhưng phần ghi Ledger.status của worker hiện đã được comment.
Ledger (vòng đời do người dùng điều khiển)
Sở hữu độc quyền bởi
LedgerSnapshotService. Worker không bao giờ thay đổiLedger.status.
| Từ | Sự kiện | Đến | Điều kiện |
|---|---|---|---|
PENDING | finalize | SETTLED | snapshot không có hasUnrecordedChange |
SETTLED | revise | hàng PENDING mới | cần note.default; đặt version+1, previousVersionId |
LedgerJob (trạng thái tạo)
Sở hữu bởi
LedgerJobService+LedgerWorkerService. Độc lập vớiLedger.status.
| Từ | Sự kiện | Đến | Điều kiện |
|---|---|---|---|
PENDING | setProcessing | PROCESSING | UPDATE … WHERE status=PENDING (một thao tác duy nhất); trả null nếu đã bị claim |
PROCESSING | setCompletedIfProcessing | COMPLETED | WHERE status=PROCESSING (một thao tác duy nhất); false nếu bị giành trước |
PROCESSING | lỗi/timeout | REJECTED | ghi failureReason (errorCode + i18n) |
REJECTED | handleRetry | PENDING | re-enqueue; không reset attemptCount |
PROCESSING | kẹt (processStartAt < cutoff) | PENDING | quét RecoveryComponent, rồi re-enqueue |
5. Kịch bản runtime
5.1 Enqueue → generate → complete
| Bước | Chi tiết |
|---|---|
| 3 | Idempotent trên (merchantId, type, period); sổ SETTLED từ chối re-generate |
| 8 | Không có job PENDING → return (đã claim/done) trừ khi APP_ENV_FORCE_GENERATE |
| 11 | Parse thất bại → REJECTED (FETCH_DATA_ERROR), commit, không replay |
| 14 | setCompletedIfProcessing=false → bị worker đồng thời giành trước; bỏ qua finalize |
| 16 | Commit chỉ xảy ra sau upload + finalize; lỗi rethrow và message vẫn được commit (không auto-replay) |
5.2 Phục hồi job kẹt
| Bước | Chi tiết |
|---|---|
| 2 | Chỉ job PROCESSING có processStartAt cũ hơn APP_ENV_STALL_THRESHOLD_MS (mặc định 180s) |
| 4 | RecoveryComponent đăng ký trước KafkaConsumerComponent nên message re-enqueue tồn tại trước khi consumer poll |
5.3 Finalize rồi revise
6. Mối quan tâm xuyên suốt
| Mối quan tâm | Cách service này xử lý |
|---|---|
| AuthN | JWT (issuer = identity), JWKS xác minh mỗi request (VerifierApplication) |
| AuthZ | Casbin qua PolicyDefinition; permission cache trong Redis; mọi endpoint gọi assertMerchantAccess(merchantId) |
| i18n | jsonb { en, vi } (ledger note, identifier/tax-tier/tax-config name/description, failure-reason) |
| Logging | key-value có cấu trúc (key: %s); ghi log các pha pipeline (FETCH/GENERATE/UPLOAD/COMPLETED) |
| Idempotency | Enqueue khoá trên (merchantId, type, period); job claim qua UPDATE có điều kiện trong một thao tác duy nhất |
| Encryption | AES-256-GCM trên mọi file upload (APP_ENV_LEDGER_ENCRYPTION_KEY) |
| Soft-delete | SoftDeletableRepository (deletedAt); unique index là partial (WHERE deleted_at IS NULL) |
| IDs | Snowflake qua IdGenerator, worker 6 |