Domain Model
Schema source:
packages/core/src/models/schemas/ledger/— this package owns no Drizzle schemas; it re-exports repositories from@nx/core. All tables live in theledgerPostgres schema.
1. Full ERD
2. Entities
Ledger
| Property | Value |
|---|---|
| Table | ledger.Ledger |
| Source | core/src/models/schemas/ledger/ledger/schema.ts |
| Soft-delete | yes |
| Owner ID column | merchantId |
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | text | ✓ | Snowflake | PK |
type | text | ✓ | — | TLedgerIdentifier (S1a-HKD..S2e-HKD) |
status | text | ✓ | DRAFT | See enum below |
period | text | ✓ | — | YYYY-MN / YYYY-QN / YYYY-Y (e.g. 2026-M3, 2026-Q1, 2026-Y) |
periodStart / periodEnd | timestamptz | ✓ | — | Period bounds |
merchantId | text | ✓ | — | Owner merchant |
isCurrent | boolean | ✓ | true | Current version flag |
version | numeric(_,1) | ✓ | 1.0 | Revision version |
previousVersionId | text | null | Prior version (set on revise) | |
summary | jsonb | null | TLedgerSummary (per-form totals) | |
note | jsonb | null | i18n { en, vi } revision note |
Status enum (LedgerStatuses):
| Value | Description |
|---|---|
DRAFT | Editable; generation + regeneration allowed |
200_FINALIZED | Version locked; must revise to change |
ARCHIVED | Superseded by a finalized revision; read-only |
400_SUBMITTED | Reserved — submitted to tax authority (not implemented) |
Indexes & constraints:
| Name | Columns | Type |
|---|---|---|
PK_Ledger | id | Primary key |
UPQ_Ledger_* | merchantId, type, period, version | Unique partial (deleted_at IS NULL) |
IDX_Ledger_* | isCurrent · merchantId,period · merchantId,periodStart,periodEnd · merchantId,status · previousVersionId · status | Btree |
LedgerJob
| Property | Value |
|---|---|
| Table | ledger.LedgerJob |
| Source | core/src/models/schemas/ledger/ledger-job/schema.ts |
| Soft-delete | yes |
| Owner ID column | — (via ledgerId → Ledger) |
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | text | ✓ | Snowflake | PK |
ledgerId | text | ✓ | — | Owning ledger (soft ref) |
status | text | ✓ | PENDING | See enum below |
attemptCount | integer | ✓ | 0 | Lifetime attempts; not reset on retry |
processStartAt | timestamptz | — | Stall-detection anchor | |
processCompletedAt | timestamptz | — | — | |
failureReason | jsonb | — | { default, en?, vi?, errorCode } | |
enqueuedAt | timestamptz | ✓ | — | First enqueue |
lastEnqueuedAt | timestamptz | — | Last re-enqueue |
Status enum (LedgerJobStatuses): DRAFT, PENDING, PROCESSING, COMPLETED, PARTIAL, REJECTED (active flow uses PENDING → PROCESSING → COMPLETED|REJECTED).
Indexes: IDX_LedgerJob_ledgerId, IDX_LedgerJob_status, IDX_LedgerJob_status_processStartAt (stalled-job sweep).
LedgerSnapshot
| Property | Value |
|---|---|
| Table | ledger.LedgerSnapshot |
| Source | core/src/models/schemas/ledger/ledger-snapshot/schema.ts |
| Soft-delete | yes (+ user-audit columns) |
| Owner ID column | — (via ledgerId) |
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | text | ✓ | Snowflake | PK |
ledgerId | text | ✓ | — | Owning ledger; unique |
headerData | jsonb | — | TSnapshotHeaderData (businessName, taxCode, address…) | |
snapshotMeta | jsonb | — | Per-type staleness aggregate (count, maxUpdatedAt) | |
pulledAt | timestamptz | ✓ | — | Pull time |
hasUnrecordedChange | boolean | ✓ | false | Staleness flag (blocks finalize) |
lastChangeDetectedAt | timestamptz | — | — |
Indexes: UQ_LedgerSnapshot_ledgerId (one snapshot per ledger).
LedgerSnapshotEntry
| Property | Value |
|---|---|
| Table | ledger.LedgerSnapshotEntry |
| Source | core/src/models/schemas/ledger/ledger-snapshot-entry/schema.ts |
| Soft-delete | yes (+ user-audit columns) |
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | text | ✓ | Snowflake | PK |
snapshotId | text | ✓ | — | Owning snapshot |
rowIndex | integer | ✓ | — | Row order |
originalData | jsonb | null | Source row; null for user-added entries | |
currentData | jsonb | ✓ | — | Edited/effective row |
Indexes: IDX_LedgerSnapshotEntry_snapshotId.
MerchantLedgerConfig
| Property | Value |
|---|---|
| Table | ledger.MerchantLedgerConfig |
| Source | core/src/models/schemas/ledger/merchant-ledger-config/schema.ts |
| Soft-delete | yes (+ user-audit columns) |
| Owner ID column | merchantId |
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | text | ✓ | Snowflake | PK |
year | integer | ✓ | current year | Config year |
merchantId | text | ✓ | — | Owner merchant |
taxDeclarationLevelId | text | — | FK-soft to TaxDeclarationLevel | |
taxMethod | text | — | — | |
isMultiSector | boolean | ✓ | false | Selects multiSectorLedgerTypes rules |
filingSchedules | jsonb | ✓ | [] | { purpose, periodType }[] |
requiredLedgerTypes | jsonb | ✓ | [] | Computed TLedgerIdentifier[] |
confirmedAt | timestamptz | — | Set on confirm | |
metadata.origin | jsonb | — | `'migration' |
Indexes: UPQ_MerchantLedgerConfig_merchantId_year (one config per merchant-year, partial).
TaxDeclarationLevel
| Property | Value |
|---|---|
| Table | ledger.TaxDeclarationLevel |
| Source | core/src/models/schemas/ledger/tax-declaration-level/schema.ts |
| Soft-delete | yes (+ user-audit columns) |
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | text | ✓ | Snowflake | PK |
code | text | ✓ | — | TIRE_0..TIRE_3 |
name | jsonb | ✓ | — | i18n { en, vi } |
description | jsonb | — | i18n | |
revenueThresholdMin / Max | text | — | Revenue band bounds | |
filingScheduleRules | jsonb | ✓ | [] | { purpose, periodType, required, ledgerTypes[], multiSectorLedgerTypes? }[] |
Indexes: UPQ_TaxDeclarationLevel_code (unique partial).
3. Cross-entity Invariants
| Invariant | Enforcement |
|---|---|
At most one current finalized ledger per (merchantId, type, period, version) | Unique partial index + isCurrent flag |
revise always produces a new DRAFT row (version+1, isCurrent=false, previousVersionId set) | LedgerSnapshotService.revise |
| Exactly one snapshot per ledger | UQ_LedgerSnapshot_ledgerId |
finalize blocked while snapshot hasUnrecordedChange = true | LedgerSnapshotService.finalize guard |
requiredLedgerTypes derived from tax level's filingScheduleRules × filingSchedules (×isMultiSector); empty → [S1a-HKD] | MerchantLedgerConfigService._computeRequiredLedgerTypes |
Worker mutates LedgerJob.status only — never Ledger.status | LedgerWorkerService (Ledger-status write commented out) |
4. Soft-delete Behavior
| Behavior | Detail |
|---|---|
| Read default | deletedAt IS NULL (all repos via SoftDeletableRepository) |
| Hard-delete | Snapshot re-pull soft-deletes prior entries + snapshot before recreating |
| Unique indexes | Partial (WHERE deleted_at IS NULL) so soft-deleted rows don't block re-create |