Appearance
Vecnet Admin — operator platform
The back-office where Vecnet personnel configure merchants, manage fees and rolling reserves, audit settlements, and watch treasury. Built as a Next.js 16 App Router app at /Users/yuyo/vecnet-admin. Auth is Clerk (see auth).
Sidebar shape
Home /
Merchants /merchants
Finances → /finances (Treasury landing — gated)
Accounts /finances/accounts
Journals /finances/journals
Snapshots /finances/snapshots
Settlements /finances/settlements
Admin users /admin-users (superadmin only)Every group label is itself a clickable link. Children render whether or not the user is currently on the section.
Merchant detail tabs
Each merchant has its own detail surface at /merchants/[id]. Tabs (some gated):
| Tab | URL | Role gate | What it does |
|---|---|---|---|
| Overview | /merchants/[id] | none | Identity, MIDs (onlineMid / terminalMid), status with state-machine switcher (activate / pause / terminate) |
| Features | /features | none | Payment-method toggles: Online · Terminal · Card · SPEI · OXXO · MercadoPago · Cash vouchers (Paysafe) · Payment links |
| Checkout | /checkout-config | none | Logo URL + 4 brand colors (rest of the config was dropped during the Tonder-scope trim) |
| Fees | /fees | finops, superadmin | Per-merchant MerchantFee rows: OUT (charged to merchant) + IN (charged to Vecnet). Rate basis points + fixed cents + IVA. Effective windows. The one and only fee editor. |
| Rolling reserve | /rolling-reserve | finops, superadmin | MerchantRollingReserveConfig per channel + liability summary + force-release for superadmin |
| Settlement | /settlement | none | Cycle type (daily / weekly / biweekly / monthly / t_plus_n) + cutoff hour + minimum payout + "Run now" action |
| Finance config | /finance-config | finops, superadmin | The 4 MerchantFinanceConfig module toggles — Fees · Ledger · Settlements · Daily snapshots. Mirrors Tonder's ConfigTable. |
| API keys | /api-keys | none | Live + test key generation, bcrypt-hashed at rest |
Permissions
lib/permissions.ts is the source of truth. Three role values:
| Role | What they see |
|---|---|
superadmin | Everything. Only role that can manage admin users + force-release reserves. |
finops | Fees, rolling reserve, settlements, treasury, finance-config. Cannot edit checkout or create merchants. |
integrations | Merchants list + create, features, checkout config, settlement view. Hidden from /finances entirely. |
Helpers: canViewMargin, canViewInFees, canViewOutFees, canEditFees, canViewRollingReserve, canEditRollingReserve, canForceReleaseReserve, canEditCheckoutConfig, canCreateMerchant, canManageAdminUsers, canViewTreasury. Each is one line over AdminRole.
Tech stack
- Next.js 16.2.6 (App Router, Turbopack)
- Prisma 7 client-extensions over PostgreSQL 14 (local via Homebrew)
- Tailwind v4 + shadcn/ui (Base UI variant)
- Clerk for auth (replaces NextAuth v5)
- BullMQ + Redis-optional for background workers
@react-pdf/rendererfor settlement PDFsexceljsfor the Spanish XLSX "Resumen de Liquidación"dayjs+ tz plugin for Mexico City cycle math (UTC-6, no DST)- Vitest for unit tests (54 passing)
Background workers
Three workers under /workers, each Redis-optional with a --run-once flag for manual triggers and tests:
| Worker | Cron | What it does |
|---|---|---|
settlement-scheduler.ts | hourly | Finds merchants whose cycle just crossed cutoff, composes one Settlement per channel that had activity |
rolling-release.ts | daily | Releases reserve held entries whose releasedAt has passed |
daily-snapshots.ts | 23:30 America/Mexico_City | Upserts BalanceSnapshot rows for every active account |
Without REDIS_URL set the workers log a no-op message and exit, so a fresh clone works without Redis until you need scheduling.
Conventions
- All money in BigInt cents. Never floats. Rates in basis points (Int).
- Double-entry: every event opens a
Journal, writes 2+LedgerEntryrows whose debits == credits. - LedgerEntry is append-only — enforced by
lib/db-extensions.tswhich only permitssettlementId/journalId/accountIdupdates on existing rows. - Account resolution is lazy via
lib/accounts/resolve.ts:resolveAccountId— every event writer goes through it.