Appearance
Settlements
Three-step lifecycle on every Vecnet settlement: close → audit → confirm. The composer closes (open → closed in one transaction), finops audits (closed → accepted or closed → failed), and finops confirms once the SPEI receipt lands (accepted → paid).
State machine
| Status | UI label | Who flips it | What happens |
|---|---|---|---|
open | — | composer (skipped today) | Reserved; the composer goes straight to closed |
closed | "Pending audit" | composer | Settlement row created; entries linked; adjustments allowed |
accepted | "On the way" | finops via Accept button | Audit passed; SPEI initiated; no more adjustments |
paid | "Confirmed" | finops via Confirm with SPEI ref | Payout ledger pair written; merchant balance drops |
failed | "Rejected" | finops via Reject | Audit failed; settlement locked |
UI labels live in lib/settlements/status-labels.ts. Permission gate for all transitions: canEditFees (finops, superadmin).
Per-channel composition
composeSettlement runs once per channel per cycle. A merchant with mixed activity produces two Settlement rows per close — one for online (t1), one for terminal (t2) — each with its own displayId, its own ledger linkage, its own audit. The idempotency key is (merchantId, periodEnd, channel).
The settlement scheduler loops:
ts
for (const channel of ["online", "terminal"] as const) {
await composeSettlement({ merchantId, periodStart, periodEnd, channel });
}workers/settlement-scheduler.ts runs hourly. The merchant "Run now" button at /merchants/[id]/settlement runs the same logic on demand.
Synthetic readable display ids
lib/settlements/display-id.ts:synthDisplayId produces:
set-{t1|t2}-{merchantSlug}-{YYYYMMDD}-{msTimestamp}Example: set-t1-bitso-121-20260527-1779894104192.
The cuid stays as the primary key and route key; the displayId is the human-facing label everywhere (list, header, report filenames).
Audit phase — Accept / Reject
In closed state finops can:
- Accept → status
accepted. Single API call; no body. The settlement is now locked from adjustments. - Reject → status
failed. Requires areason(>= 2 chars). The reason is recorded as a $0SettlementAdjustmentfor audit trail.
Adjustments — only in closed
SettlementAdjustment rows write a balanced settlement_adjustment ledger pair against vecnet_clearing and merchant_pending (sign-aware: positive amount → merchant gets more, negative → less). The composer already cached Settlement.adjustmentsCents and Settlement.netCents; each adjustment increments them in the same transaction.
UI lives in components/settlements/adjustments-panel.tsx. The form is locked when status !== "closed".
Confirm — SPEI ref + payout pair
In accepted state finops enters the SPEI reference at the "Confirm payment" button. The API:
- Flips status
accepted → paid. - Stores
payoutReference,paidAt,payoutMarkedById. - Writes the
settlement_payoutledger pair ifnetCents > 0. - Opens a
settlement_confirmJournal for audit.
After confirm the merchant's merchant_pending balance is shorter by netCents — the journal entry that materialises the payout.
Reports
Three downloads per settlement:
| Download | File | Audience | Includes IN fees? |
|---|---|---|---|
lib/settlements/pdf.tsx | Merchant | No | |
| Merchant CSV | lib/settlements/csv.ts | Merchant | No |
| Resumen XLSX | lib/settlements/resumen-xlsx.ts | Merchant | No — Spanish "Resumen de Liquidación" format |
| Internal CSV | lib/settlements/internal-csv.ts | finops/superadmin only | Yes + IVA |
The Resumen XLSX maps the existing data to the same 2-column CONCEPTO / Cálculo layout used by Tonder's PSP for continuity. Sections: Resumen de Liquidación · Volumen procesado · Comisiones · Reserva · Ajustes · Liquidación.