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 |
| Service Queue/Snapshot/Config | 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 nguyên tử, 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 | DRAFT, FINALIZED, ARCHIVED, SUBMITTED* | → nhảy tới |
LedgerJob | PENDING, PROCESSING, COMPLETED, REJECTED (+DRAFT/PARTIAL dành chỗ) | → nhảy tới |
* SUBMITTED đã dành chỗ — logic chuyển trạng thái chưa xây.
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 |
|---|---|---|---|
DRAFT | finalize | FINALIZED | snapshot không có hasUnrecordedChange |
FINALIZED | revise | hàng DRAFT 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 nguyên tử; trả null nếu đã bị claim |
PROCESSING | setCompletedIfProcessing | COMPLETED | WHERE status=PROCESSING nguyên 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ổ FINALIZED 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, tax-level 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 nguyên 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 |