Skip to content

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 the ledger Postgres schema.

1. Full ERD

2. Entities

Ledger

PropertyValue
Tableledger.Ledger
Sourcecore/src/models/schemas/ledger/ledger/schema.ts
Soft-deleteyes
Owner ID columnmerchantId

Fields:

FieldTypeRequiredDefaultDescription
idtextSnowflakePK
typetextTLedgerIdentifier (S1a-HKD..S2e-HKD)
statustextDRAFTSee enum below
periodtextYYYY-MN / YYYY-QN / YYYY-Y (e.g. 2026-M3, 2026-Q1, 2026-Y)
periodStart / periodEndtimestamptzPeriod bounds
merchantIdtextOwner merchant
isCurrentbooleantrueCurrent version flag
versionnumeric(_,1)1.0Revision version
previousVersionIdtextnullPrior version (set on revise)
summaryjsonbnullTLedgerSummary (per-form totals)
notejsonbnulli18n { en, vi } revision note

Status enum (LedgerStatuses):

ValueDescription
DRAFTEditable; generation + regeneration allowed
200_FINALIZEDVersion locked; must revise to change
ARCHIVEDSuperseded by a finalized revision; read-only
400_SUBMITTEDReserved — submitted to tax authority (not implemented)

Indexes & constraints:

NameColumnsType
PK_LedgeridPrimary key
UPQ_Ledger_*merchantId, type, period, versionUnique partial (deleted_at IS NULL)
IDX_Ledger_*isCurrent · merchantId,period · merchantId,periodStart,periodEnd · merchantId,status · previousVersionId · statusBtree

LedgerJob

PropertyValue
Tableledger.LedgerJob
Sourcecore/src/models/schemas/ledger/ledger-job/schema.ts
Soft-deleteyes
Owner ID column (via ledgerId → Ledger)

Fields:

FieldTypeRequiredDefaultDescription
idtextSnowflakePK
ledgerIdtextOwning ledger (soft ref)
statustextPENDINGSee enum below
attemptCountinteger0Lifetime attempts; not reset on retry
processStartAttimestamptzStall-detection anchor
processCompletedAttimestamptz
failureReasonjsonb{ default, en?, vi?, errorCode }
enqueuedAttimestamptzFirst enqueue
lastEnqueuedAttimestamptzLast 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

PropertyValue
Tableledger.LedgerSnapshot
Sourcecore/src/models/schemas/ledger/ledger-snapshot/schema.ts
Soft-deleteyes (+ user-audit columns)
Owner ID column (via ledgerId)

Fields:

FieldTypeRequiredDefaultDescription
idtextSnowflakePK
ledgerIdtextOwning ledger; unique
headerDatajsonbTSnapshotHeaderData (businessName, taxCode, address…)
snapshotMetajsonbPer-type staleness aggregate (count, maxUpdatedAt)
pulledAttimestamptzPull time
hasUnrecordedChangebooleanfalseStaleness flag (blocks finalize)
lastChangeDetectedAttimestamptz

Indexes: UQ_LedgerSnapshot_ledgerId (one snapshot per ledger).

LedgerSnapshotEntry

PropertyValue
Tableledger.LedgerSnapshotEntry
Sourcecore/src/models/schemas/ledger/ledger-snapshot-entry/schema.ts
Soft-deleteyes (+ user-audit columns)

Fields:

FieldTypeRequiredDefaultDescription
idtextSnowflakePK
snapshotIdtextOwning snapshot
rowIndexintegerRow order
originalDatajsonbnullSource row; null for user-added entries
currentDatajsonbEdited/effective row

Indexes: IDX_LedgerSnapshotEntry_snapshotId.

MerchantLedgerConfig

PropertyValue
Tableledger.MerchantLedgerConfig
Sourcecore/src/models/schemas/ledger/merchant-ledger-config/schema.ts
Soft-deleteyes (+ user-audit columns)
Owner ID columnmerchantId

Fields:

FieldTypeRequiredDefaultDescription
idtextSnowflakePK
yearintegercurrent yearConfig year
merchantIdtextOwner merchant
taxDeclarationLevelIdtextFK-soft to TaxDeclarationLevel
taxMethodtext
isMultiSectorbooleanfalseSelects multiSectorLedgerTypes rules
filingSchedulesjsonb[]{ purpose, periodType }[]
requiredLedgerTypesjsonb[]Computed TLedgerIdentifier[]
confirmedAttimestamptzSet on confirm
metadata.originjsonb`'migration'

Indexes: UPQ_MerchantLedgerConfig_merchantId_year (one config per merchant-year, partial).

TaxDeclarationLevel

PropertyValue
Tableledger.TaxDeclarationLevel
Sourcecore/src/models/schemas/ledger/tax-declaration-level/schema.ts
Soft-deleteyes (+ user-audit columns)

Fields:

FieldTypeRequiredDefaultDescription
idtextSnowflakePK
codetextTIRE_0..TIRE_3
namejsonbi18n { en, vi }
descriptionjsonbi18n
revenueThresholdMin / MaxtextRevenue band bounds
filingScheduleRulesjsonb[]{ purpose, periodType, required, ledgerTypes[], multiSectorLedgerTypes? }[]

Indexes: UPQ_TaxDeclarationLevel_code (unique partial).

3. Cross-entity Invariants

InvariantEnforcement
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 ledgerUQ_LedgerSnapshot_ledgerId
finalize blocked while snapshot hasUnrecordedChange = trueLedgerSnapshotService.finalize guard
requiredLedgerTypes derived from tax level's filingScheduleRules × filingSchedulesisMultiSector); empty → [S1a-HKD]MerchantLedgerConfigService._computeRequiredLedgerTypes
Worker mutates LedgerJob.status only — never Ledger.statusLedgerWorkerService (Ledger-status write commented out)

4. Soft-delete Behavior

BehaviorDetail
Read defaultdeletedAt IS NULL (all repos via SoftDeletableRepository)
Hard-deleteSnapshot re-pull soft-deletes prior entries + snapshot before recreating
Unique indexesPartial (WHERE deleted_at IS NULL) so soft-deleted rows don't block re-create

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