HKD Ledger Templates
1. Overview
The Ledger Service generates 6 Vietnamese government accounting forms from the HKD (Hộ Kinh Doanh - Household Business) series. Each form type has two output formats:
| Format | Engine | MIME Type | Purpose |
|---|---|---|---|
Typst NodeCompiler | application/pdf | Official submission, printing | |
| XLSX | ExcelJS | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | Data editing, analysis |
Both outputs are generated in parallel from the same validated data object.
2. Template Registry
| Form Code | Orientation | Has Business Location | Description |
|---|---|---|---|
S1a-HKD | Portrait | Yes | Primary revenue/sales ledger - date, code, description, revenue amount |
S2a-HKD | Landscape | Yes | Detailed income/expense with tax breakdown by group |
S2b-HKD | Landscape | Yes | Purchase and expense records |
S2c-HKD | Portrait | Yes | Inventory tracking ledger - goods in/out |
S2d-HKD | Landscape | No | Income/expense record (no fixed business location) |
S2e-HKD | Landscape | No | Purchase record (no fixed business location) |
"Has Business Location" controls whether the header includes businessAddress, district, and province fields.
3. Typst PDF Templates
3.1. File Locations
Templates are compiled source files loaded at runtime from resources/templates/ (relative to process.cwd()).
| Typst File | Ledger Type |
|---|---|
common.typ | Shared macros - header, footer, layout, number/date formatting |
s1a-hkd.typ | S1a-HKD |
s2a-hkd.typ | S2a-HKD |
s2b-hkd.typ | S2b-HKD |
s2c-hkd.typ | S2c-HKD |
s2d-hkd.typ | S2d-HKD |
s2e-hkd.typ | S2e-HKD |
3.2. Data Injection
The PdfGeneratorService injects data via Typst's shadow file mechanism:
// Serialize data to JSON at a virtual path inside the workspace
compiler.mapShadow(shadowPath, Buffer.from(JSON.stringify(data)));
// Render
const pdfBuffer = compiler.pdf({ mainFilePath });
// Always clean up
compiler.unmapShadow(shadowPath);The shadow file path is resources/templates/data.json. Templates read it via:
#let data = json("data.json")3.3. Shared Components (common.typ)
| Component | Purpose |
|---|---|
| Header | Two-column - government form code + merchant info (name, tax code, address, representative) |
| Footer | HKD signature block (location, date, representative, accountant, director) |
| Layout | Page margins, table borders, font configuration (Times New Roman) |
| Number format | Vietnamese convention: dot as thousand separator, comma as decimal |
| Date format | dd/mm/yyyy |
| Regulation text | (Kèm theo Thông tư số 152/2025/TT-BTC ngày 31 tháng 12 năm 2025...) |
Font files must be present in
resources/fonts/. TheNodeCompileris initialized withfontArgs: [{ fontPaths: [FONT_DIR] }].
4. XLSX Templates (ExcelJS)
4.1. Shared Style Constants
Defined as module-level constants in xlsx-generator.service.ts:
| Constant | Value | Used For |
|---|---|---|
HEADER_FONT | { bold: true, color: white, size: 10 } | Column header rows |
BODY_FONT | { size: 10 } | Data rows |
BOLD_FONT | { bold: true, size: 10 } | Summary/total rows |
THIN_BORDER | All 4 sides thin | All data cells |
CURRENCY_FMT | '#,##0.###' | Numeric amount cells |
COLOR_GREEN_DARK | FF5CB85C | Primary header background |
COLOR_GREEN_LIGHT | FF8FD18F | Secondary header rows |
COLOR_GREEN_PRODUCT | FFC8E6C9 | Product/category rows |
COLOR_YELLOW_IMPORT | FFFFFDE7 | Import/purchase rows |
REGULATION_TEXT | Thông tư 152/2025/TT-BTC | Footer regulation reference |
4.2. Per-Form Sheet Layout
| Form | Columns | Key Features |
|---|---|---|
| S1a-HKD | STT, Date, Code, Description, Revenue | Simple revenue ledger; one row per transaction |
| S2a-HKD | STT, Date, Code, Description, Tax columns… | Multi-column tax breakdown; tax groups as column headers |
| S2b-HKD | STT, Date, Code, Description, Amount columns | Purchase and expense with category grouping |
| S2c-HKD | STT, Date, Code, Description, In/Out, Balance | Inventory tracking with running balance |
| S2d-HKD | Same as S2a-HKD (no location header) | No business address fields |
| S2e-HKD | Same as S2b-HKD (no location header) | No business address fields |
4.3. Footer
All XLSX sheets include a 2-column footer row with the regulation text and signature blocks (same structure as Typst common.typ).
FOOTER_COLUMN_LENGTH = 2 - merged across 2 columns.
5. Data Requirements
5.1. Common Fields (all forms)
| Field | Type | Description |
|---|---|---|
businessName | string | Merchant name |
businessAddress | string | Business address (location forms only) |
businessTaxCode | string | Tax identification code |
periodDescription | string | Vietnamese period label (e.g. Quý 1 năm 2026) |
title | string | Form title (e.g. SỔ DOANH THU BÁN HÀNG, DỊCH VỤ) |
currentDay / currentMonth / currentYear | number | Signature date |
5.2. S1a-HKD Specific
interface IS1aEntry {
code: string; // order number / reference code
transDate: string; // transaction date (ISO)
description: string;
amount: number;
}
// Data shape
{
...commonFields,
entries: IS1aEntry[];
totalRevenue: number;
}Data source: real - fetched from SaleOrderRepository.findCompletedInPeriod(). See Data Sources - S1a-HKD for full field-level detail.
5.3. S2a-HKD Specific
interface ITaxValue {
revenue: number;
vat: number;
pit: number;
}
interface IS2aEntry {
code: string;
transDate: string;
description: string;
taxValues: Record<string, ITaxValue>; // keyed by TaxGroup UUID or 'other'
}
interface ITaxGroup {
key: string; // TaxGroup UUID or 'other'
groupName: string;
label: string; // rate annotation - empty when rate is uniform; shows range when mixed
totalRevenue: number;
totalVat: number;
totalPit: number;
}
// Data shape
{
...commonFields,
taxGroupChunks: ITaxGroup[][]; // 2-D: outer = page chunks (≤3 groups each), inner = groups
entries: IS2aEntry[];
totalGtgt: number;
totalTncn: number;
}Data source: real - fetched from SaleOrderRepository.findCompletedItemsInPeriod(). Sectors are discovered dynamically from priceMetadata.pricing.taxSetId on each SaleOrderItem. See Data Sources - S2a-HKD for the full sector resolution chain, rate derivation logic, and gotchas.
5.4. Other Forms (S2b, S2c, S2d, S2e)
Data structures are defined in Zod schemas in @nx/core (SCHEMA_MAP keyed by TLedgerIdentifiers). Currently served from fixture files in resources/fixtures/.
6. Schema Validation
Before rendering, LedgerWorkerService validates raw data using:
const schema = SCHEMA_MAP[type]; // from @nx/core
const parsed = schema.safeParse(rawData);If validation fails, the job is marked REJECTED with the Zod error message - no file is generated.
After generation, the summary is extracted via:
const summaryData = SUMMARY_SCHEMA_MAP[type].parse(parsedData);
// Saved to ledger.summary (jsonb)Both maps are exported from @nx/core and keyed by TLedgerIdentifiers values.
7. Compiler Lifecycle
The Typst NodeCompiler is a singleton managed by PdfGeneratorService:
| State | Trigger | Action |
|---|---|---|
null (initial) | First generate() call | NodeCompiler.create({ fontArgs, workspace }) |
| Active | Concurrent generates | Reused - shadow file ensures data isolation |
| Compilation error | Exception from compiler.pdf() | replaceCompiler() - creates fresh instance, then throws |
| Testing / forced reset | dispose() | Set to null - next call re-initializes |
Concurrency note: The NodeCompiler uses a shadow file at a fixed path (
data.json). Concurrent calls togenerate()from the same service instance would corrupt each other's data. In production, each Kafka consumer runs in its own execution context, and each consumer instance has its ownLedgerWorkerService- so there is no concurrent access to the same compiler.
8. Related Pages
- Generation Pipeline - where rendering fits in the worker flow
- Domain Model -
Ledger.type/summary - ADR-0003 Typst for PDF rendering