Ledger Service
@nx/ledger generates official Vietnamese HKD (Hộ Kinh Doanh — household-business) accounting ledgers. A Kafka-driven worker fetches business data, renders PDF (Typst) + XLSX (ExcelJS), encrypts with AES-256-GCM, and uploads to S3. It also owns the ledger lifecycle (DRAFT → FINALIZED → revise), snapshot-based draft editing, and per-merchant annual configuration. Upstream data comes from @nx/core repositories (finance/sale/merchant); files are stored via @nx/asset MetaLinks.
1. Quick Reference
| Property | Value |
|---|---|
| Package | @nx/ledger |
| Code | SVC-00060-LEDGER |
| Type | Microservice |
| Runtime | Bun |
| Base Class | VerifierApplication |
| Location | packages/ledger |
| Base Path | /v1/api/ledger |
| Dev Port | 31060 |
| Container Port | 3000 (external 31060) |
| Snowflake ID | 6 |
| DB Schema | ledger (6 tables) |
| Binding Namespace | @nx/ledger |
| Application Roles | api and/or worker (APP_ENV_APPLICATION_ROLES, default both) |
2. Purpose & Scope
| Included | Excluded |
|---|---|
| 6 HKD ledger forms (S1a, S2a–S2e) | Tax calculation / declaration engine |
| Async generation pipeline (fetch → render → encrypt → upload) | Payment / invoicing (owned by @nx/payment, @nx/invoice) |
| PDF (Typst) + XLSX (ExcelJS) rendering | Ledger Zod schemas + Drizzle models (live in @nx/core) |
Ledger lifecycle DRAFT → FINALIZED → revise (new version) | File storage transport (delegated to @nx/asset MetaLink/S3) |
| Snapshot-based draft editing + staleness tracking | Submission to tax authority (SUBMITTED status reserved, not built) |
| Per-merchant annual config + tax-declaration levels | — |
| Stalled-job recovery sweep + manual retry/regenerate | — |
| Real-time WebSocket job-status notifications (worker role) | — |
3. Tech Stack
External:
| Library | Purpose |
|---|---|
@venizia/ignis | IoC container, DI, BaseService, BaseRestController, ControllerFactory |
@venizia/ignis-helpers | Logger, Kafka helpers, Redis, WebSocketEmitter, AES, BunS3Helper |
@platformatic/kafka | Kafka producer + consumer client |
@myriaddreamin/typst-ts-node-compiler | Typst NodeCompiler — PDF rendering |
exceljs | XLSX workbook builder |
hono + @hono/zod-openapi + @scalar/hono-api-reference | HTTP server, OpenAPI, /doc explorer |
drizzle-orm + pg | DB access via PostgresCoreDataSource |
lodash | Utilities |
Internal:
| Package | Purpose |
|---|---|
@nx/core | VerifierApplication, ledger schemas/models, repositories, KafkaTopics, LedgerStatuses/LedgerIdentifiers/PeriodTypes, LedgerErrors, WS channels/rooms |
@nx/asset | MetaLinkService / MetaLink — S3 upload + download of generated files |
4. Project Structure
packages/ledger/
├── src/
│ ├── application.ts # VerifierApplication subclass; role-gated wiring
│ ├── index.ts # bootstrapApplication()
│ ├── migrate.ts # bootstrapMigration()
│ ├── common/ # constants, keys, rest-paths, roles, environments, types
│ ├── components/ # kafka (producer+consumer), recovery, websocket, stress-test*
│ ├── controllers/ # actions, crud, batch, merchant-ledger-config
│ ├── datasources/ # PostgresCoreDataSource
│ ├── helpers/ # S3Helper (lazy BunS3Helper singleton)
│ ├── migrations/processes/ # 4 seed migrations
│ ├── models/ # zod request/response schemas
│ ├── repositories/ # re-exports from @nx/core
│ ├── services/ # business logic + fetchers/ + generators/
│ └── utilities/ # buildPeriodString, filename helpers
├── resources/templates/ # 6 Typst .typ + common.typ + previews/
├── package.json
└── tsconfig.json* stress-test/ exists but is not registered in application.ts.
5. Architecture
Detail: see Architecture.
6. Domain Snapshot
Full ERD + per-entity tables: see Domain Model.
7. Surface Summary
REST controllers — full reference rendered live from /v1/api/ledger/doc/openapi.json (Scalar viewer at /doc, gateway portal). Registered only in api role; order matters (LedgerActionsController first to avoid /:id shadowing).
| Controller | Base path | Surface |
|---|---|---|
LedgerActionsController | /ledgers | generate, status, retry, download, check-exists, search, snapshot pull/get, acknowledge-change, finalize, regenerate, revise |
LedgerController | /ledgers | CRUD (merchant-scoped find/findById/findOne/count) |
LedgerBatchController | /ledgers | status/batch, generate/batch, retry/batch |
MerchantLedgerConfigController | /merchant-ledger-configs | create, list (by merchant+year), update, confirm |
Async surface — full reference in API Events:
| Direction | Channel | Count |
|---|---|---|
| Inbound | Kafka ledger.generate | 1 topic (worker role) |
| Outbound | Kafka ledger.generate | 1 topic (enqueue + recovery re-enqueue) |
| Outbound | WebSocket | 1 topic (observation/ledger/job/status, worker role) |
| BullMQ | — | none |
8. Components
Registered in
configureComponents()(skipped entirely whenRUN_MODE=migrate). WebSocket is registered only inworkerrole; the Kafka consumer + recovery sweep are registered unconditionally but self-skip unless the worker role is active.
| Component | File | Purpose |
|---|---|---|
KafkaProducerComponent | src/components/kafka.component.ts | Initializes the producer, binds APPLICATION_KAFKA_PRODUCER |
KafkaConsumerComponent | src/components/kafka.component.ts | Worker-only; N consumers on ledger.generate → LedgerWorkerService.handleGeneration() |
RecoveryComponent | src/components/recovery.component.ts | Worker-only; periodic stalled-job sweep → reset PENDING + re-enqueue |
LedgerWebSocketComponent | src/components/websocket.component.ts | Worker-only; Redis-backed WebSocketEmitter, registers LedgerNotificationService |
9. Services
Always registered (
apiorworker) unless marked worker-only.
| Service | File | One-liner |
|---|---|---|
LedgerService | services/ledger.service.ts | Ledger record CRUD/queries (findExisting, findOrCreateDraft, saveSummary) |
LedgerJobService | services/ledger-job.service.ts | LedgerJob state machine: setProcessing/Completed/Rejected, stalled-job sweep |
LedgerSnapshotService | services/ledger-snapshot.service.ts | Draft editing: pull, getSnapshot, acknowledgeChange, finalize, revise |
MerchantLedgerConfigService | services/merchant-ledger-config.service.ts | Per-merchant per-year config; resolves required types, validates batch |
MetaLinkService | services/meta-link.service.ts | S3/MetaLink upload + decrypted download (via @nx/asset) |
LedgerQueueService | services/ledger-queue.service.ts | Enqueue orchestration: enqueue, batch, retry, regenerate |
LedgerDataFetcherService | services/ledger-data-fetcher.service.ts | Per-type fetcher facade (validate, fetch) |
S1aHkdDataFetcherService | services/fetchers/ | S1a-HKD — real data (finance/sale/merchant/tax-info) |
S2xHkdDataFetcherService | services/fetchers/ | S2a–S2e — fixture data (placeholder) |
LedgerDataFixtureService | services/ledger-data-fixture.service.ts | Reads pre-generated fixture JSON |
StorageEncryptionService | services/storage-encryption.service.ts | AES-256-GCM encrypt/decrypt |
LedgerWorkerService (worker) | services/ledger-worker.service.ts | Pipeline orchestrator; touches LedgerJob.status only |
PdfGeneratorService (worker) | services/generators/pdf-generator.service.ts | Typst NodeCompiler PDF renderer (lazy singleton) |
XlsxGeneratorService (worker) | services/generators/xlsx-generator.service.ts | ExcelJS workbook builder |
LedgerNotificationService (worker) | services/ledger-notification.service.ts | WS job-status emitter (registered by WS component) |
10. Repositories
All re-exported from
@nx/core; bound inconfigureRepositories(). The package owns no schemas.
| Repository | Table / Source | Notes |
|---|---|---|
LedgerRepository | ledger.Ledger | Owned |
LedgerJobRepository | ledger.LedgerJob | + setProcessing, setCompletedIfProcessing, stalled-job query |
LedgerSnapshotRepository | ledger.LedgerSnapshot | Owned |
LedgerSnapshotEntryRepository | ledger.LedgerSnapshotEntry | Owned |
MerchantLedgerConfigRepository | ledger.MerchantLedgerConfig | Owned |
TaxDeclarationLevelRepository | ledger.TaxDeclarationLevel | Owned |
MetaLinkRepository | @nx/asset | Generated-file links |
MerchantRepository, TaxInfoRepository, VnProvinceRepository, VnWardRepository | @nx/core | Cross-package read (S1a header) |
FinanceTransactionRepository, SaleOrderRepository | @nx/core | Cross-package read (S1a entries) |
PermissionRepository, RoleRepository, PolicyDefinitionRepository | @nx/core | Authorization seeds |
11. Entry Points
| File | Purpose |
|---|---|
src/index.ts | Service entry → bootstrapApplication() |
src/migrate.ts | Migration entry → bootstrapMigration() |
src/application.ts | Application extends VerifierApplication |
12. Configuration
Env vars + roles + seeded data: see Configuration.
13. Operations
Deployment + observability + security + runbook: see Operations.
14. Related Pages
Concepts — why/how:
Reference — lookup:
- API Events
- Configuration
- Operations
- REST endpoints — live OpenAPI at
/v1/api/ledger/doc/openapi.json(Scalar viewer at/doc, gateway portal)
Features — deep dives:
Decisions: