Skip to content

Domain Model

Schema source: packages/core/src/models/schemas/finance/. PostgreSQL schema finance, 6 tables.

1. Full ERD

2. Entities

FinanceAccount

PropertyValue
Tablefinance.FinanceAccount
Sourcepackages/core/src/models/schemas/finance/finance-account/
Soft-deleteyes
Owner ID columnmerchantId

Fields:

FieldTypeRequiredDefaultDescription
idtextSnowflakePrimary key
merchantIdtext-Owner merchant
statustextACTIVATEDSee status enum
namei18n-{ default, en, vi }
typetext-See account-type enum
providertextBANABANA / VNPAY / e-wallet / bank
productCodetext-e.g. QR_MMS, PHONE_POS, INTERNAL_ACCOUNT
accountNumber / accountHoldertext-Bank/e-wallet identity
detailsjsonb-Type-specific (branch, swift, last4, drawer…)
unittextVNDAccount currency
currentBalancenumeric(15,4)0Running balance maintained per posting
postingSequenceLastValuebigint0Monotonic per-account ledger cursor
environmenttextproductionEnv partition
metadatajsonb-isDefault, isExcluded, isPayable, isInternal

Account-type enum (FinanceAccountTypes):

ValueMeaning
100_CASHPhysical cash drawer
200_BANKBank account
300_QR_CODEQR acceptance (e.g. VNPAY QR_MMS)
400_MOBILE_POSMobile/phone POS terminal (e.g. VNPAY PHONE_POS)
998_COGSInternal control - cost of goods sold (isInternal=true)
999_INVENTORYInternal control - inventory asset (isInternal=true)

Status enum: ACTIVATED · DEACTIVATED · ARCHIVED.

Key indexes:

NameColumnsType
UPI_FinanceAccount_…provider_productCode_accountNumber_environmentpartialUnique (live, provider+number present)
UPI_FinanceAccount_merchantId_type_environmentpartialUnique default per merchant/type (metadata.isDefault=true)
IDX_FinanceAccount_merchantId(_status / _provider_environment)-Btree

FinanceVoucher

PropertyValue
Tablefinance.FinanceVoucher
Sourcepackages/core/src/models/schemas/finance/finance-voucher/
Soft-deleteyes
Owner ID columnmerchantId

Fields (selected):

FieldTypeRequiredDefaultDescription
typetext-RECEIPT / PAYMENT / TRANSFER / ADJUSTMENT
statustextDRAFTDRAFT / ISSUED / VOIDED
voucherNumbertext-Assigned on issue (PT/PC/PCK/PKT + seq)
amountdecimal(15,4)-Denormalized header total
unittextVNDVoucher currency (all lines share it)
partyTypetext-CUSTOMER / VENDOR / EMPLOYEE / INTERNAL / EXTERNAL
partyId / partyName / partyAddresstext/i18npartyName ✓-Party snapshot at issue
reasoni18n-Narrative
sourceTypetext-MANUAL / SALE_ORDER / SALE_ORDER_REFUND / PURCHASE_ORDER / POS_SESSION / INVENTORY_ADJUSTMENT
sourceIdtext-Originating document id
sourceEventUidtext-Per-event idempotency key (Kafka redelivery)
sessionIdtext-POS shift attribution
transactionDatetimestamptz-Accounting date
reversalVoucherIdtext-Counter voucher on void
issuedAt/issuedBy/voidedAt/voidedBy/voidReason--Lifecycle audit
metadatajsonb-draftLines, adjustmentReason, inventoryIssuance, inventoryAdjustment, refundOf

Type / direction mapping:

Voucher typeLine direction ruleNumber prefix
RECEIPT (Phiếu thu)all lines DEBIT (inferred)PT
PAYMENT (Phiếu chi)all lines CREDIT (inferred)PC
TRANSFER (Phiếu chuyển khoản)explicit per line; DEBIT sum == CREDIT sumPCK
ADJUSTMENT (Phiếu kế toán)explicit per linePKT

Key indexes:

NameColumnsType
UPI_…merchantId_voucherNumberpartialUnique number per merchant (live)
UPI_…merchantId_type_sourceType_sourceIdpartialPer-source dedup (excludes MANUAL/POS_SESSION/SALE_ORDER)
UPI_…sourceType_sourceEventUidpartialPer-event dedup (live, non-void)

FinanceTransaction (ledger line)

PropertyValue
Tablefinance.FinanceTransaction
Sourcepackages/core/src/models/schemas/finance/finance-transaction/
Soft-deleteyes
Owner ID columnmerchantId

Fields (selected):

FieldTypeRequiredDefaultDescription
typetext-100_DEBIT (balance up) / 200_CREDIT (balance down)
statustextCOMPLETEDPENDING / COMPLETED / CANCELLED
amountdecimal(15,4)-Line amount
unittextVNDCurrency (= account unit)
exchangeRatedecimal(19,6)1Reserved (single-currency, always 1)
financeAccountIdtext-Account affected
financeCategoryIdtext-Optional classification
financeVoucherIdtext-Parent voucher
lineNumberinteger-Position within voucher
balanceBefore / balanceAfterdecimal(15,4)-Snapshot at post time
postingSequencebigint-Per-account monotonic posting cursor
referenceType / referenceIdtext-Polymorphic source link (mirrors voucher source)

Key index: UPI_FinanceTransaction_financeAccountId_postingSequence (partial unique - one line per account posting cursor).

FinanceCategory

PropertyValue
Tablefinance.FinanceCategory
Sourcepackages/core/src/models/schemas/finance/finance-category/
Soft-deleteyes
Owner ID columnmerchantId (null = system category)

Fields: identifier (unique), type (100_INCOME / 200_EXPENSE), name/description (i18n), parentId (hierarchy), status. See Configuration → Seeded Data for the 14 system categories.

FinanceVoucherSequence

PropertyValue
Tablefinance.FinanceVoucherSequence
Sourcepackages/core/src/models/schemas/finance/finance-voucher-sequence/
Unique(merchantId, type, yearMonth)

Per-merchant / per-type / per-month counter. lastValue (bigint) is incremented under lock to mint the next voucher number.

PaymentIntegration

PropertyValue
Tablefinance.PaymentIntegration
Sourcepackages/core/src/models/schemas/finance/payment-integration/
Soft-deleteyes
Owner ID columnprincipalId (Merchant / SaleChannel)

Fields (selected): financeAccountId, principalType (Merchant/SaleChannel), provider, productCode, integrationType (100_PAYMENT_PROVIDER / 200_MERCHANT_PAYMENT_CONFIG / 300_TERMINAL_PAYMENT_CONFIG), credential (encrypted), maskedValue, config (jsonb), status (ACTIVATED / ARCHIVED). A partial unique index enforces one ACTIVATED row per full credential coordinate.

3. Cross-entity Invariants

InvariantEnforcement
A voucher is balanced: TRANSFER Σdebit == Σcredit; PAYMENT-with-asset-leg balancesFinanceVoucherService.assertLineDirectionForVoucherType (1e-4 tolerance)
Every voucher line shares the voucher's currencyassertLinesCurrencyConsistent + per-line check in postLines
account.currentBalance == running sum of its line signed amountspostLines computes balanceAfter; adjustAccountState persists
postingSequenceLastValue strictly increases per accountadjustAccountState guard + partial UPI on (financeAccountId, postingSequence)
One default account per (merchant, type, environment)Partial unique index where metadata.isDefault=true
Auto vouchers are idempotent under Kafka redeliveryPer-source / per-event partial unique indexes + tryIdempotentReplay
INVENTORY + COGS control accounts exist before COGS posting_ensureInternalControlAccounts on merchant CDC; handler throws if missing

4. Soft-delete Behavior

BehaviorDetail
Read defaultdeletedAt IS NULL (model defaultFilter)
Hard-deleteOnly deleteDraft removes a DRAFT voucher; issued vouchers are never deleted (void instead)
RestoreNot exposed
Dedup indexesAll scoped WHERE deletedAt IS NULL so a soft-deleted row never blocks re-insert

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