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 FinanceTransactionRepository.findTransactionWithSaleOrder().
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 tax group key
}
interface ITaxGroup {
key: string;
groupName: string;
label: string;
totalRevenue: number;
totalVat: number;
totalPit?: number;
}
// Data shape
{
...commonFields,
taxGroups: ITaxGroup[]; // define column headers
entries: IS2aEntry[];
}Data source: fixture (pending real integration).
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