Skip to content

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:

FormatEngineMIME TypePurpose
PDFTypst NodeCompilerapplication/pdfOfficial submission, printing
XLSXExcelJSapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetData editing, analysis

Both outputs are generated in parallel from the same validated data object.

2. Template Registry

Form CodeOrientationHas Business LocationDescription
S1a-HKDPortraitYesPrimary revenue/sales ledger — date, code, description, revenue amount
S2a-HKDLandscapeYesDetailed income/expense with tax breakdown by group
S2b-HKDLandscapeYesPurchase and expense records
S2c-HKDPortraitYesInventory tracking ledger — goods in/out
S2d-HKDLandscapeNoIncome/expense record (no fixed business location)
S2e-HKDLandscapeNoPurchase 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 FileLedger Type
common.typShared macros — header, footer, layout, number/date formatting
s1a-hkd.typS1a-HKD
s2a-hkd.typS2a-HKD
s2b-hkd.typS2b-HKD
s2c-hkd.typS2c-HKD
s2d-hkd.typS2d-HKD
s2e-hkd.typS2e-HKD

3.2. Data Injection

The PdfGeneratorService injects data via Typst's shadow file mechanism:

typescript
// 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:

typst
#let data = json("data.json")

3.3. Shared Components (common.typ)

ComponentPurpose
HeaderTwo-column — government form code + merchant info (name, tax code, address, representative)
FooterHKD signature block (location, date, representative, accountant, director)
LayoutPage margins, table borders, font configuration (Times New Roman)
Number formatVietnamese convention: dot as thousand separator, comma as decimal
Date formatdd/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/. The NodeCompiler is initialized with fontArgs: [{ fontPaths: [FONT_DIR] }].

4. XLSX Templates (ExcelJS)

4.1. Shared Style Constants

Defined as module-level constants in xlsx-generator.service.ts:

ConstantValueUsed 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_BORDERAll 4 sides thinAll data cells
CURRENCY_FMT'#,##0.###'Numeric amount cells
COLOR_GREEN_DARKFF5CB85CPrimary header background
COLOR_GREEN_LIGHTFF8FD18FSecondary header rows
COLOR_GREEN_PRODUCTFFC8E6C9Product/category rows
COLOR_YELLOW_IMPORTFFFFFDE7Import/purchase rows
REGULATION_TEXTThông tư 152/2025/TT-BTCFooter regulation reference

4.2. Per-Form Sheet Layout

FormColumnsKey Features
S1a-HKDSTT, Date, Code, Description, RevenueSimple revenue ledger; one row per transaction
S2a-HKDSTT, Date, Code, Description, Tax columns…Multi-column tax breakdown; tax groups as column headers
S2b-HKDSTT, Date, Code, Description, Amount columnsPurchase and expense with category grouping
S2c-HKDSTT, Date, Code, Description, In/Out, BalanceInventory tracking with running balance
S2d-HKDSame as S2a-HKD (no location header)No business address fields
S2e-HKDSame as S2b-HKD (no location header)No business address fields

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)

FieldTypeDescription
businessNamestringMerchant name
businessAddressstringBusiness address (location forms only)
businessTaxCodestringTax identification code
periodDescriptionstringVietnamese period label (e.g. Quý 1 năm 2026)
titlestringForm title (e.g. SỔ DOANH THU BÁN HÀNG, DỊCH VỤ)
currentDay / currentMonth / currentYearnumberSignature date

5.2. S1a-HKD Specific

typescript
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

typescript
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:

typescript
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:

typescript
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:

StateTriggerAction
null (initial)First generate() callNodeCompiler.create({ fontArgs, workspace })
ActiveConcurrent generatesReused — shadow file ensures data isolation
Compilation errorException from compiler.pdf()replaceCompiler() — creates fresh instance, then throws
Testing / forced resetdispose()Set to null — next call re-initializes

Concurrency note: The NodeCompiler uses a shadow file at a fixed path (data.json). Concurrent calls to generate() 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 own LedgerWorkerService — so there is no concurrent access to the same compiler.

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.