Skip to content

Ledger tables

The double-entry core: Account, Journal, LedgerEntry. Plus BalanceSnapshot for day-end aggregates and Settlement for the payout header. balance is never stored on Account — it's always derived from the ledger.

These four models replaced the original LedgerAccount enum + flat LedgerEntry table during Phase 1 of the Tonder usrv-finances port. The schema lives at prisma/schema.prisma.

Account — chart of accounts

Replaces the old enum. Per-merchant accounts and Vecnet house accounts coexist in this single table. Resolution is lazy via lib/accounts/resolve.ts:resolveAccountId({ kind, merchantId?, acquirer? })(kind, merchantId, acquirer, currency) is the unique compound key.

ColumnTypeNotes
idcuidPK
accountCodeString @uniqueStable human label: MERCHANT_PENDING:cuid:MXN, VECNET_REVENUE::MXN, ACQUIRER_CLEARING:TONDER:MXN
nameStringDisplay label
typeAccountTypeasset · liability · revenue · expense · equity
kindAccountKindSee enum below
merchantIdString?Null for house accounts
acquirerTransactionProvider?Only set for per-acquirer house accounts
currencyString"MXN" default
activeBool

AccountKind:

KindTypeWho owns it
merchant_pendingliabilityper-merchant
merchant_availableliabilityper-merchant
merchant_rolling_reserveliabilityper-merchant
vecnet_revenuerevenueVecnet house
vecnet_costexpenseVecnet house
vecnet_clearingassetVecnet house
chargeback_reserveliabilityVecnet house
acquirer_clearingassetper-acquirer house (tonder, menta)
vecnet_iva_payableliabilityVecnet house — IVA owed to SAT

Type → balance convention:

  • asset / expense: balance = DR − CR
  • liability / revenue / equity: balance = CR − DR

Journal — event header

One row per financial event. Groups the LedgerEntries it wrote so reports never re-aggregate. Summary fields are denormalised at write time and never mutated (the db-extensions.ts immutability check doesn't touch Journals — they're not append-only, but in practice composers update them once and forget).

ColumnTypeNotes
idcuidPK
categoryJournalCategorySee enum
typeJournalTypein (merchant gains value) or out (loses)
merchantIdStringRequired — every event belongs to a merchant
transactionIdString?If the event was triggered by a Transaction
settlementIdString?If the event belongs to a Settlement
grossCents, feeCents, ivaCents, netCents, rrCentsBigIntDenormalised summary
descriptionString?Free text
generatedAtDateTimeWall-clock when row was inserted
createdAtDateTimeWall-clock the event represents (defaults to generatedAt; differs during bulk reprocess)

JournalCategory: payment · refund · chargeback · void · dispute_in_review · dispute_won · withdrawal · topup · settlement_approve · settlement_confirm · settlement_adjustment · rolling_reserve_held · rolling_reserve_released · fee · internal_transfer.

Helper: lib/journals/open.ts:openJournal() returns the new id; callers attach it to every LedgerEntry they write.

LedgerEntry — debit / credit line

Append-only. Every event writes 2+ rows whose debits sum equals credits sum.

ColumnTypeNotes
idcuidPK
transactionIdString?If linked to a Transaction
merchantIdStringRequired
accountIdString?FK → Account (resolved lazily)
journalIdString?FK → Journal (set at write time)
directionLedgerDirectiondebit · credit
amountCentsBigIntThe leg amount
ivaCentsBigInt @default(0)Denormalised IVA portion for fee/IVA legs
currencyString @default("MXN")
categoryLedgerCategorySee below
releasedAtDateTime?For rolling-reserve held entries — when they become due
settlementIdString?Set by the composer when entries get linked to a Settlement
createdAtDateTime @default(now())

LedgerCategory: gross · fee_acceptance_(out\|in) · fee_refund_(out\|in) · fee_chargeback_(out\|in) · fee_decline_(out\|in) · fee_three_ds_(out\|in) · fee_antifraud_(out\|in) · fee_settlement_(out\|in) · fee_iva_out (NEW) · rolling_reserve_held · rolling_reserve_released · settlement_payout · settlement_adjustment.

Immutability

lib/db-extensions.ts:applyImmutableLedger blocks update / updateMany unless the data payload sets only one of: settlementId, journalId, accountId. upsert, delete, deleteMany always throw on LedgerEntry.

Corrections are new entries, never edits.

BalanceSnapshot — day-end aggregates

Written by workers/daily-snapshots.ts every night at 23:30 America/Mexico_City. One row per (accountId, date)@@unique. The worker also supports --run-once for manual triggers and tests.

ColumnTypeNotes
accountIdStringFK → Account
dateDateTimeLocal midnight
balanceCentsBigIntEnd-of-day balance (DR − CR or CR − DR per type)
inflowsCentsBigInt @default(0)Day's inflow magnitude
outflowsCentsBigInt @default(0)Day's outflow magnitude

Treasury KPIs at /finances and the per-account sparkline at /finances/accounts/[id] both read from this table.

Settlement — payout header

The aggregate row created when a settlement cycle closes. One per (merchantId, periodEnd, channel) triple — that's the composer's idempotency key.

ColumnTypeNotes
idcuidPK
displayIdString? @uniqueHuman-readable: set-t1-bitso-121-20260527-1779894104192
merchantIdString
channelTransactionChannelonline or terminal — drives t1/t2
periodStart, periodEndDateTimeCycle window
statusSettlementStatusopenclosed (Pending audit) → accepted (On the way) → paid (Confirmed) — or closedfailed (Rejected)
grossCents, totalOutFeesCents, totalInFeesCents, marginCents, rollingReserveHeldCents, rollingReserveReleasedCents, netCents, adjustmentsCentsBigIntCached at close time from the aggregated ledger
transactionCountInt
payoutReferenceString?SPEI reference filled at "Confirm payment"
paidAtDateTime?
payoutMarkedByIdString?Actor (AdminUser.id)
closedAt, createdAtDateTime

Settlement has a journals back-relation — the settlement-approve, fee, adjustment, and payout journals all link via journal.settlementId.

SettlementAdjustment — manual signed corrections

Append-only. Only allowed while a Settlement is closed. Each adjustment writes a balanced LedgerEntry pair + a Journal with category settlement_adjustment, then bumps Settlement.adjustmentsCents and Settlement.netCents.

prisma
model SettlementAdjustment {
  id            String   @id @default(cuid())
  settlementId  String
  amountCents   BigInt   // signed: positive = increase merchant payout
  reason        String
  createdById   String   // AdminUser cuid
  createdAt     DateTime @default(now())
}

Vecnet — Build Spec v0.2 · Obsidian Terminal