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
typetext-TLedgerIdentifierCode (S1a-HKD..S2e-HKD)
statustextPENDINGSee enum below
periodtext-YYYY-MN / YYYY-QN / YYYY-Y (e.g. 2026-M3, 2026-Q1, 2026-Y)
periodStart / periodEndtimestamptz-Period bounds
merchantIdtext-Owner merchant
isCurrentbooleantrueCurrent version flag
versionnumeric(_,1)1.0Revision version
previousVersionIdtextnullPrior version (set on revise)
ledgerIdentifierIdtext-Soft ref to LedgerIdentifier
summaryjsonbnullTLedgerSummary (per-form totals)
notejsonbnulli18n { en, vi } revision note

Status enum (LedgerStatuses - reuses IGNIS Statuses):

ValueDescription
PENDINGDefault; editable draft (record created without/awaiting a job)
NEWRecord created alongside a job to be processed
PROCESSINGGeneration in progress
COMPLETEDGeneration finished
FAILGeneration failed
SETTLEDFinalized; version locked - must revise to change
ARCHIVEDSuperseded by a re-generated/revised version; read-only

Active user-driven flow uses PENDING (draft) → SETTLED (finalize) → revise → new PENDING. The worker mutates LedgerJob.status only - the Ledger.status write is commented out in LedgerWorkerService.

Indexes & constraints:

NameColumnsType
PK_LedgeridPrimary key
UPQ_Ledger_*merchantId, type, period, versionUnique partial (deleted_at IS NULL)
UQ_Ledger_*ledgerIdentifierId, merchantId, period, versionUnique
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
ledgerIdtext-Owning ledger (soft ref)
statustextPENDINGSee enum below
attemptCountinteger0Lifetime attempts; not reset on retry
processStartAttimestamptz-Stall-detection anchor
processCompletedAttimestamptz--
failureReasonjsonb-{ default, en?, vi?, errorCode }
enqueuedAttimestamptz-First enqueue
lastEnqueuedAttimestamptz-Last re-enqueue

Status enum (LedgerJobStatuses): DRAFT, PENDING, PROCESSING, COMPLETED, REJECTED (DRAFT = created but not yet queued; active flow runs PENDING → PROCESSING → COMPLETED|REJECTED).

Indexes: IDX_LedgerJob_ledgerId, IDX_LedgerJob_status, IDX_LedgerJob_status_processStartAt (stalled-job sweep), UPQ_LedgerJob_ledgerId (partial - one in-flight job per ledger where status IN (PENDING, PROCESSING, DRAFT)).

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
ledgerIdtext-Owning ledger; unique
headerDatajsonb-TSnapshotHeaderData (businessName, taxCode, address…)
snapshotMetajsonb-Per-type staleness aggregate (count, maxUpdatedAt)
pulledAttimestamptz-Pull 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
snapshotIdtext-Owning snapshot
rowIndexinteger-Row order
originalDatajsonbnullSource row; null for user-added entries
currentDatajsonb-Edited/effective row

Indexes: IDX_LedgerSnapshotEntry_snapshotId.

LedgerIdentifier

PropertyValue
Tableledger.LedgerIdentifier
Sourcecore/src/models/schemas/ledger/ledger-identifier/schema.ts
Soft-deleteyes

Catalogue of the 6 HKD form codes. Seeded once; referenced by Ledger.type and Ledger.ledgerIdentifierId.

Fields:

FieldTypeRequiredDefaultDescription
idtextSnowflakePK
identifiertext-TLedgerIdentifierCode (S1a-HKD..S2e-HKD); unique
namejsonb-i18n { en, vi }
descriptionjsonb-i18n

Indexes: unique on identifier.

TaxTier

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

Revenue-band group a merchant falls into (TIRE_1..TIRE_4). Seeded once.

Fields:

FieldTypeRequiredDefaultDescription
idtextSnowflakePK
codetext-TTaxTierCode (100_TIRE_1..400_TIRE_4)
namejsonb-i18n { en, vi }
descriptionjsonb-i18n
statustextACTIVATEDACTIVATED / DEACTIVATED
businessScaletext000_HOUSEHOLDTBusinessScaleType (000_HOUSEHOLD / 100_ENTERPRISE)
revenueMindecimal(15,4)0Band lower bound (VND)
revenueMaxdecimal(15,4)nullBand upper bound (VND); null = open-ended

Indexes: IDX_TaxTier_status, UQ_TaxTier_code (unique partial, deleted_at IS NULL).

LedgerTaxConfig

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

A tax approach available within a tier, with the set of ledger forms it requires. Seeded once.

Fields:

FieldTypeRequiredDefaultDescription
idtextSnowflakePK
codetext-TTaxApproachCode (100_EXEMPT, 201_PERCENTAGE_BOTH, 202_PERCENTAGE_VAT_INCOME_PIT)
taxTierIdtext-Soft ref to TaxTier
namejsonb-i18n { en, vi }
descriptionjsonb-i18n
requiredLedgersjsonb[]TLedgerIdentifierCode[] required by this approach
statustextACTIVATEDACTIVATED / DEACTIVATED
metadatajsonb-{ isDefault, isSelectable }

Indexes: IDX_LedgerTaxConfig_tax_tier_id, IDX_LedgerTaxConfig_status, UPQ_LedgerTaxConfig_tax_tier_id_code (unique partial, deleted_at IS NULL).

MerchantTaxConfig

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

Per-merchant per-year tax onboarding selection. Schema and model are defined; no repository/service/controller is wired in this package yet.

Fields:

FieldTypeRequiredDefaultDescription
idtextSnowflakePK
merchantIdtext-Owner merchant
yearintegercurrent yearConfig year
taxTierIdtext-Soft ref to TaxTier
ledgerTaxConfigIdtext-Soft ref to LedgerTaxConfig (selected approach)
filingPeriodtext-TPeriodType (MONTHLY / QUARTERLY / YEARLY)
declaredAnnualRevenuedecimal(15,4)-Self-declared annual revenue (VND)
onboardingStatustextPENDINGPENDING / PARTIAL / COMPLETED
disclaimerAcceptedAttimestamptz-Disclaimer acceptance time
disclaimerAcceptedBytext-User who accepted (DB column disclaimer_accepted_by)
disclaimerVersiontext-Accepted disclaimer version
metadatajsonb-{ disclaimerHistory: { acceptedAt, version }[] }

Indexes: IDX_MerchantTaxConfig_year, IDX_MerchantTaxConfig_merchant_id_onboarding_status_year, UPQ_MerchantTaxConfig_merchant_id_year (unique partial, deleted_at IS NULL).

3. Cross-entity Invariants

InvariantEnforcement
At most one current ledger per (merchantId, type, period, version)Unique partial index + isCurrent flag
revise always produces a new PENDING row (version+1, isCurrent=false, previousVersionId set, requires source SETTLED)LedgerSnapshotService.revise
Exactly one snapshot per ledgerUQ_LedgerSnapshot_ledgerId
At most one in-flight job per ledgerUPQ_LedgerJob_ledgerId (partial on PENDING/PROCESSING/DRAFT)
finalize blocked while snapshot hasUnrecordedChange = true; sets Ledger.status = SETTLEDLedgerSnapshotService.finalize guard
One LedgerTaxConfig approach is unique per (taxTierId, code); its requiredLedgers drives the forms a merchant must keepUPQ_LedgerTaxConfig_tax_tier_id_code + APPROACH_REQUIRED_LEDGERS seed
One MerchantTaxConfig per (merchantId, year)UPQ_MerchantTaxConfig_merchant_id_year
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.