Domain Model
Schema source:
packages/core/src/models/schemas/finance/. PostgreSQL schemafinance, 6 tables.
1. Full ERD
2. Entities
FinanceAccount
| Property | Value |
|---|---|
| Table | finance.FinanceAccount |
| Source | packages/core/src/models/schemas/finance/finance-account/ |
| Soft-delete | yes |
| Owner ID column | merchantId |
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | text | ✓ | Snowflake | Primary key |
merchantId | text | ✓ | — | Owner merchant |
status | text | ✓ | ACTIVATED | See status enum |
name | i18n | ✓ | — | { default, en, vi } |
type | text | ✓ | — | See account-type enum |
provider | text | ✓ | BANA | BANA / VNPAY / e-wallet / bank |
productCode | text | — | e.g. QR_MMS, PHONE_POS, INTERNAL_ACCOUNT | |
accountNumber / accountHolder | text | — | Bank/e-wallet identity | |
details | jsonb | — | Type-specific (branch, swift, last4, drawer…) | |
unit | text | ✓ | VND | Account currency |
currentBalance | numeric(15,4) | ✓ | 0 | Running balance maintained per posting |
postingSequenceLastValue | bigint | ✓ | 0 | Monotonic per-account ledger cursor |
environment | text | ✓ | production | Env partition |
metadata | jsonb | — | isDefault, isExcluded, isPayable, isInternal |
Account-type enum (FinanceAccountTypes):
| Value | Meaning |
|---|---|
100_CASH | Physical cash drawer |
200_BANK | Bank account |
300_QR_CODE | QR acceptance (e.g. VNPAY QR_MMS) |
400_MOBILE_POS | Mobile/phone POS terminal (e.g. VNPAY PHONE_POS) |
998_COGS | Internal control — cost of goods sold (isInternal=true) |
999_INVENTORY | Internal control — inventory asset (isInternal=true) |
Status enum: ACTIVATED · DEACTIVATED · ARCHIVED.
Key indexes:
| Name | Columns | Type |
|---|---|---|
UPI_FinanceAccount_…provider_productCode_accountNumber_environment | partial | Unique (live, provider+number present) |
UPI_FinanceAccount_merchantId_type_environment | partial | Unique default per merchant/type (metadata.isDefault=true) |
IDX_FinanceAccount_merchantId(_status / _provider_environment) | — | Btree |
FinanceVoucher
| Property | Value |
|---|---|
| Table | finance.FinanceVoucher |
| Source | packages/core/src/models/schemas/finance/finance-voucher/ |
| Soft-delete | yes |
| Owner ID column | merchantId |
Fields (selected):
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type | text | ✓ | — | RECEIPT / PAYMENT / TRANSFER / ADJUSTMENT |
status | text | ✓ | DRAFT | DRAFT / ISSUED / VOIDED |
voucherNumber | text | — | Assigned on issue (PT/PC/PCK/PKT + seq) | |
amount | decimal(15,4) | ✓ | — | Denormalized header total |
unit | text | ✓ | VND | Voucher currency (all lines share it) |
partyType | text | ✓ | — | CUSTOMER / VENDOR / EMPLOYEE / INTERNAL / EXTERNAL |
partyId / partyName / partyAddress | text/i18n | partyName ✓ | — | Party snapshot at issue |
reason | i18n | — | Narrative | |
sourceType | text | ✓ | — | MANUAL / SALE_ORDER / SALE_ORDER_REFUND / PURCHASE_ORDER / POS_SESSION / INVENTORY_ADJUSTMENT |
sourceId | text | — | Originating document id | |
sourceEventUid | text | — | Per-event idempotency key (Kafka redelivery) | |
sessionId | text | — | POS shift attribution | |
transactionDate | timestamptz | ✓ | — | Accounting date |
reversalVoucherId | text | — | Counter voucher on void | |
issuedAt/issuedBy/voidedAt/voidedBy/voidReason | — | — | Lifecycle audit | |
metadata | jsonb | — | draftLines, adjustmentReason, inventoryIssuance, inventoryAdjustment, refundOf |
Type / direction mapping:
| Voucher type | Line direction rule | Number 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 sum | PCK |
ADJUSTMENT (Phiếu kế toán) | explicit per line | PKT |
Key indexes:
| Name | Columns | Type |
|---|---|---|
UPI_…merchantId_voucherNumber | partial | Unique number per merchant (live) |
UPI_…merchantId_type_sourceType_sourceId | partial | Per-source dedup (excludes MANUAL/POS_SESSION/SALE_ORDER) |
UPI_…sourceType_sourceEventUid | partial | Per-event dedup (live, non-void) |
FinanceTransaction (ledger line)
| Property | Value |
|---|---|
| Table | finance.FinanceTransaction |
| Source | packages/core/src/models/schemas/finance/finance-transaction/ |
| Soft-delete | yes |
| Owner ID column | merchantId |
Fields (selected):
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type | text | ✓ | — | 100_DEBIT (balance up) / 200_CREDIT (balance down) |
status | text | ✓ | COMPLETED | PENDING / COMPLETED / CANCELLED |
amount | decimal(15,4) | ✓ | — | Line amount |
unit | text | ✓ | VND | Currency (= account unit) |
exchangeRate | decimal(19,6) | ✓ | 1 | Reserved (single-currency, always 1) |
financeAccountId | text | ✓ | — | Account affected |
financeCategoryId | text | — | Optional classification | |
financeVoucherId | text | — | Parent voucher | |
lineNumber | integer | — | Position within voucher | |
balanceBefore / balanceAfter | decimal(15,4) | — | Snapshot at post time | |
postingSequence | bigint | — | Per-account monotonic posting cursor | |
referenceType / referenceId | text | — | Polymorphic source link (mirrors voucher source) |
Key index: UPI_FinanceTransaction_financeAccountId_postingSequence (partial unique — one line per account posting cursor).
FinanceCategory
| Property | Value |
|---|---|
| Table | finance.FinanceCategory |
| Source | packages/core/src/models/schemas/finance/finance-category/ |
| Soft-delete | yes |
| Owner ID column | merchantId (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
| Property | Value |
|---|---|
| Table | finance.FinanceVoucherSequence |
| Source | packages/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
| Property | Value |
|---|---|
| Table | finance.PaymentIntegration |
| Source | packages/core/src/models/schemas/finance/payment-integration/ |
| Soft-delete | yes |
| Owner ID column | principalId (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
| Invariant | Enforcement |
|---|---|
A voucher is balanced: TRANSFER Σdebit == Σcredit; PAYMENT-with-asset-leg balances | FinanceVoucherService.assertLineDirectionForVoucherType (1e-4 tolerance) |
| Every voucher line shares the voucher's currency | assertLinesCurrencyConsistent + per-line check in postLines |
account.currentBalance == running sum of its line signed amounts | postLines computes balanceAfter; adjustAccountState persists |
postingSequenceLastValue strictly increases per account | adjustAccountState 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 redelivery | Per-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
| Behavior | Detail |
|---|---|
| Read default | deletedAt IS NULL (model defaultFilter) |
| Hard-delete | Only deleteDraft removes a DRAFT voucher; issued vouchers are never deleted (void instead) |
| Restore | Not exposed |
| Dedup indexes | All scoped WHERE deletedAt IS NULL so a soft-deleted row never blocks re-insert |