Skip to main content
The Finance Portal (apps/finance-portal, oi-sys-finance) is a dedicated Next.js application for the Finance / Accounting team. It surfaces TigerBeetle double-entry ledger data, accounts receivable aging, statutory financials, reconciliation workflows, and a flexible report builder — all scoped behind the finance_analyst JWT role.
Note: The Finance Portal is intentionally isolated from the Admin and Compliance portals. A finance_analyst JWT cannot access platform ops routes (rate tables, programs, rules) or compliance data (producer licensing, filings).

URL & Access

EnvironmentURL
Productionhttps://finance.openinsure.dev
Local devhttp://localhost:3006

Sign In

Use the standard OpenInsure sign-in flow. The Finance Portal sets the oi_finance_token cookie, verified against FINANCE_JWT_SECRET.
GET https://auth-dev.openinsure.dev/api/auth/sign-in/microsoft
    ?callbackURL=https://finance.openinsure.dev/api/auth/callback

Roles & Authorization

JWT roleSpiceDB org relationDescription
finance_analystfinanceStandard finance team access
superadminFull access
systemMachine-to-machine (M2M)
The finance_analyst role is enforced in apps/finance-portal/lib/auth.ts. The middleware rejects any JWT that does not carry one of these three roles and redirects unauthenticated requests to /login.
SectionPageRoute
OverviewDashboard/
AnalyticsEntities/analytics/entities
AnalyticsPipeline/analytics/pipeline
AnalyticsCaptive/analytics/captive
ReportsAR Aging/reports/ar-aging
ReportsFinancials/reports/financials
ReportsReconciliation/reports/reconciliation
ReportsStatutory/reports/statutory
ReportsChart of Accounts/reports/chart-of-accounts
ReportsReport Builder/reports/builder

Pages

Dashboard (/)

Three-pane layout (flex h-full overflow-hidden) with period toggle and live non-pay queue: Left pane (w-64) — KPI sidebar:
  • Period toggle — YTD / MTD / QTD links (?period=ytd URL param), default ytd
  • KPICard for GWP (Gross Written Premium) — neutral status
  • KPICard for AR Outstandingstatus='warn' if balance > 0
  • KPICard for Loss Ratiostatus='warn' if > 65%, status='critical' if > 85%
  • Navigation links to A/R Aging, Pipeline, and Financials
Center pane (flex-1) — AR Aging visualization:
  • Fixed header with total outstanding balance
  • CSS horizontal bar chart — 5 aging buckets (Current / 31–60 / 61–90 / 91–120 / 120+), bars sized relative to the largest bucket, no chart library
  • Top 5 overdue accounts mini-table — Invoice #, Insured, Outstanding (cents), Days overdue
    • > 90d → red, > 30d → amber, else muted
    • “View all N invoices →” link to full AR Aging report
Right pane (w-72)NonPayQueue client component:
  • TanStack Query polling (refetchInterval: 60_000) with initialData from server render
  • Groups entries by dunning stage: Stage 3 Critical (red) first, Stage 2 Warning (amber) next
  • Each card shows policy number, insured name, amount due, and days remaining on grace period
  • Clicking a card opens a shadcn Dialog with amount due, grace period end, dunning stage, and optional note input
  • If queue empty: green checkmark (“Queue is clear”)
  • “Full A/R Aging Report” link at bottom
API calls (parallel via Promise.all):
  • GET /v1/analytics/mga?period=${period} — GWP + loss ratio
  • GET /v1/reports/ar-aging — bucket bars + top 5 overdue rows (amounts in cents)
  • GET /v1/billing/non-pay-queue — non-pay entries for right pane (amountDue in dollars)
Source: apps/finance-portal/app/(dashboard)/non-pay-queue.tsx (client island)

AR Aging (/reports/ar-aging)

Accounts receivable aging report grouped into 30/60/90/90+ day buckets. Fetches from GET /v1/reports/ar-aging. Exportable as CSV via the ExportToolbar component.

Financials (/reports/financials)

Income statement and balance sheet views sourced from the TigerBeetle ledger (GET /v1/reports/financials). Supports period selection (MTD, QTD, YTD, custom range). Income statement and balance sheet views are backed by getTrialBalance() from the TigerBeetle ledger client, which queries all 9 account types concurrently and returns debit/credit totals with a balanced boolean. The 9 account types queried are: Carrier Payable (1), MGA Fiduciary (2), MGA Revenue (3), Producer Payable (4), Tax Authority Payable (5), Loss Fund (6), Claims Paid (7), Reserves (8), and Reinsurer Payable (9). Each row in the trial balance reports total debits, total credits, and net balance in integer cents.

Reconciliation (/reports/reconciliation)

Displays unreconciled ledger entries with status badges. Individual entries can be marked reconciled via PATCH /v1/reports/reconciliation/:id. Changes optimistically update the UI.

Statutory (/reports/statutory)

State statutory financial exhibits for regulatory filing. Data from GET /v1/reports/statutory. Read-only; the actual filing workflow lives in the Compliance Portal.

Chart of Accounts (/reports/chart-of-accounts)

Full account hierarchy from TigerBeetle. Sortable and searchable.

Trial Balance & Journal Entries

The TigerBeetle ledger client provides two audit-grade reporting methods:
  • Trial Balance (getTrialBalance()) — Queries all 9 account types concurrently via Promise.all. Each row contains the account ID, account type code (1-9), human-readable name, total credits, total debits, and net balance — all in integer cents. Returns a balanced flag confirming debits equal credits across the organization’s subledger.
    CodeAccount Name
    1Carrier Payable
    2MGA Fiduciary
    3MGA Revenue
    4Producer Payable
    5Tax Authority Payable
    6Loss Fund
    7Claims Paid
    8Reserves
    9Reinsurer Payable
  • Journal Entries (getJournalEntries()) — Filtered transfer history through the MGA fiduciary hub account. Supports from/to date-range filtering (converted to TigerBeetle nanosecond timestamps) and a limit parameter (default 100). Each entry includes the transfer ID, debit/credit account IDs, amount in cents, transfer code, and timestamp.

Report Builder (/reports/builder)

Flexible pivot-style report builder. Saved report configurations are persisted via POST /v1/reports/builder. Results can be exported to CSV or PDF.

API Proxy Allowlist

The Finance Portal proxies API requests through apps/finance-portal/app/api/[...path]/route.ts, gated by lib/proxy-allowlist.ts:
GET  /v1/analytics/entities
GET  /v1/analytics/pipeline
GET  /v1/analytics/captive
GET  /v1/reports/ar-aging
GET  /v1/reports/statutory
GET  /v1/reports/financials
GET  /v1/reports/reconciliation
PATCH /v1/reports/reconciliation/:id
GET  /v1/reports/chart-of-accounts
GET  /v1/reports/builder
POST /v1/reports/builder
GET  /v1/billing/non-pay-queue
GET  /v1/ledger/accounts
GET  /v1/ledger/transfers
GET  /v1/ledger/trial-balance
GET  /v1/ledger/journal-entries
GET  /v1/ledger/lookup-transfers
GET  /v1/ledger/:orgId/transfers
Any request not in the allowlist returns 403 Forbidden.

Environment Variables

VariableDescription
FINANCE_JWT_SECRETJWT signing secret — set via wrangler secret put
API_URLAPI worker base URL (e.g., https://api.openinsure.dev)
NEXT_PUBLIC_APP_NAME"Finance Portal" — set in wrangler.toml [vars]
# Set the JWT secret (production)
wrangler secret put FINANCE_JWT_SECRET --name oi-sys-finance

# Local dev (.env.local)
FINANCE_JWT_SECRET=9d79c38aa7d57bd24a1afe213848b2b935519afb08a3923ac112aed71fd5bc21
API_URL=http://localhost:8787
DEMO_MODE=true

Deployment

The Finance Portal deploys as an OpenNext Cloudflare Worker (oi-sys-finance):
# Build + deploy
cd apps/finance-portal
npx opennextjs-cloudflare deploy -- --keep-vars

# Or via pnpm filter
pnpm --filter @openinsure/finance-portal deploy
CI deploys automatically from master when apps/finance-portal/ or shared packages change. See CI/CD for the full pipeline.

Period Close Workflow

The period close process manages the month-end accounting close lifecycle. It is implemented in @openinsure/billing/period-close and exposed through apps/api/src/routes/period-close.ts under /v1/period-close.

Close Lifecycle

open  -->  pending_review  -->  closed
  |                                 |
  └─────────  reopened  ◄───────────┘
StatusDescription
openClose created, checklist items being worked
in_progressClose work underway
pending_reviewAll items complete, awaiting dual-control approval
closedApproved and locked — no further changes
reopenedClosed period reopened for adjustments

Creating a Period Close

POST /v1/period-close
Authorization: Bearer <token>
Content-Type: application/json

{
  "period": "2025-06"
}
The period must be in YYYY-MM format. The system rejects duplicate closes for the same org and period (HTTP 409). On creation, a default 15-step checklist is automatically generated.

Default Checklist

The standard month-end checklist covers seven categories with dependency ordering (later items are blocked until predecessors complete):
OrderCategoryTitle
1PremiumPremium reconciliation
2PremiumEarned premium calculation review
3PremiumUPR balance verification
4LossLoss reserve review
5LossIBNR reserve review
6CommissionCommission calculation and posting
7CommissionProducer statement reconciliation
8ReinsuranceCeded premium calculation
9ReinsuranceReinsurer settlement
10ReconciliationBank reconciliation
11ReconciliationCarrier statement reconciliation
12AccrualExpense accruals
13AccrualRevenue accruals
14ReviewTrial balance review
15ReviewManagement sign-off
Each item tracks status (pending, in_progress, completed, skipped, blocked), assignee, completion metadata, and notes.

Updating Checklist Items

PATCH /v1/period-close/:id/items/:itemId
Authorization: Bearer <token>
Content-Type: application/json

{
  "status": "completed",
  "notes": "Reconciled against GL. No variances."
}
When marking an item completed, the system verifies that all blockedBy dependencies are already complete. If any dependency is incomplete, the request returns HTTP 409.

Progress Tracking

GET /v1/period-close/:id
Authorization: Bearer <token>
Returns the close record with the full checklist and a progress object calculated by calculateCloseProgress():
  • Total items, completed count, and completion percentage
  • Breakdown by category
  • List of blocked items

Accrual Entries

Generate month-end accrual journal entries for expenses and estimated losses:
POST /v1/period-close/:id/accruals
Authorization: Bearer <token>
Content-Type: application/json

{
  "expenses": [
    { "category": "legal", "amount": 12000, "accountCode": "6100" },
    { "category": "claims_adjustment", "amount": 8500 }
  ],
  "losses": {
    "estimatedIBNR": 285000,
    "estimatedULAE": 42000
  }
}
The generateAccrualEntries() function produces double-entry journal entries suitable for posting to the TigerBeetle ledger.

Reversing Accruals

Reverse prior period accruals at the start of the new period:
POST /v1/period-close/:id/reverse-accruals
Authorization: Bearer <token>
Content-Type: application/json

{
  "accrualResult": { "..." }
}
Pass the output from the generate accruals call to produce reversing entries.

Initiating Close

Move the period to pending_review status:
POST /v1/period-close/:id/initiate
Authorization: Bearer <token>
Can only be initiated from open or in_progress state. Records the initiating user for dual-control purposes.

Dual-Control Approval

POST /v1/period-close/:id/approve
Authorization: Bearer <token>
The approver must be a different user than the person who initiated the close. If the same user attempts to both initiate and approve, the request returns HTTP 403 with a dual-control violation error. On approval, the period status moves to closed and a closedAt timestamp is recorded. All period close endpoints require org_admin or billing_admin role.

Reconciliation

The reconciliation module matches carrier statement data against the system’s policy records to identify discrepancies. The API is at /v1/reconciliation.

Upload Carrier Statement

Carrier statements can be uploaded as JSON or CSV: JSON upload:
POST /v1/reconciliation/upload
Authorization: Bearer <token>
Content-Type: application/json

{
  "carrierName": "National General Insurance",
  "statementDate": "2025-06-30",
  "rows": [
    { "policyNumber": "MHC-CA-2025-001", "premium": 12500, "insuredName": "Acme Corp" },
    { "policyNumber": "MHC-FL-2025-042", "premium": 8750 }
  ]
}
CSV upload (multipart):
POST /v1/reconciliation/upload-csv
Content-Type: multipart/form-data

carrierName=National General Insurance
statementDate=2025-06-30
file=@carrier-statement.csv
The CSV must contain policyNumber and premium columns (header row required). An optional insuredName column is supported. The CSV file is archived to R2 storage for audit purposes.

Auto-Matching Logic

On upload, the system automatically matches carrier rows against all org policies:
  1. Exact match — Policy number matches (case-insensitive) and premium amounts agree within $0.01.
  2. Discrepancy — Policy number matches but premium amounts differ.
  3. Unmatched carrier — Carrier row has no corresponding system policy.
  4. Unmatched system — System policy has no corresponding carrier row.
Each upload creates a reconciliation session with summary counts:
FieldDescription
matchedCountPolicies that match exactly on premium
discrepancyCountPolicies that match on number but differ on premium
unmatchedCarrierCountCarrier rows with no system match
unmatchedSystemCountSystem policies missing from carrier statement

Session Management

List sessions:
GET /v1/reconciliation/sessions
Authorization: Bearer <token>
Get session detail with all rows:
GET /v1/reconciliation/sessions/:id
Authorization: Bearer <token>
Returns the session metadata and all matched/unmatched rows with full detail (carrier values, system values, and premium difference).

Manual Override

For discrepancies that have been investigated, rows can be manually accepted or disputed:
PATCH /v1/reconciliation/rows/:id/override
Authorization: Bearer <token>
Content-Type: application/json

{
  "overrideStatus": "accepted",
  "overrideNote": "Premium difference due to mid-term endorsement not yet on carrier statement"
}
Override statuses are accepted (variance explained) or disputed (requires carrier follow-up). The override records the acting user for audit. All reconciliation endpoints require org_admin or superadmin role.

Compliance Portal

Producer licensing, state filings, and compliance analytics.

Authentication

JWT roles, portal cookies, and SpiceDB authorization.