Rules-driven invoicing platform with operator and client portals
Auto-Invoice
A full-stack TypeScript platform that ingests usage spreadsheets from Google Drive, runs them through a declarative pricing-rules engine, and turns the result into invoices, ledger entries and versioned PDFs, with separate operator and client portals behind role-based access.
- Context
- Professional · National Sales Partners
- Role
- Primary developer
- Period
- 2025 — 2026
- Stack
- TypeScript, Next.js 16, React 19, Supabase (Postgres · Auth · Storage), Google OAuth
At a glance
- 01 · Problem
Turning daily usage spreadsheets into invoices under different rules for every client is repetitive, and an invoice that is wrong, duplicated or missing a day is expensive to unwind.
- 02 · Solution
A Next.js and Supabase platform that ingests spreadsheets from Google Drive, prices them with declarative per-client rules, and produces invoices, ledger entries and versioned PDFs.
- 03 · Impact
One repeatable flow from raw spreadsheet to finished invoice. Imports and invoice creation are safe to re-run, rule changes are template edits rather than code changes, and clients only ever see their own documents.
- Framework
- Next.js 16 App Router · React 19
- Language
- TypeScript end to end
- Data
- Supabase Postgres · Storage buckets
- Auth
- Supabase Auth · SSR cookie sessions
- Ingest
- Google OAuth 2.0 · Drive API · XLSX
- Engine
- Declarative JSON pricing rules
- Documents
- pdf-lib rendering · versioned storage
- Access
- Operator / client roles · route + API guards
The problem
Invoicing from usage data starts with a folder of daily spreadsheets and ends with a document a client will check line by line. Every client is billed under different rules, spreadsheets arrive late or get re-exported, and an invoice that is wrong, duplicated or missing a day is expensive to unwind. The job was to make the whole path from raw file to finished invoice repeatable, auditable and safe to re-run.
Architecture
Engineering patterns
Approach
Usage data comes straight from Google Drive. Each user connects Drive with OAuth; an ingest route lists the folder, parses each workbook with SheetJS, normalizes rows and upserts them in chunks keyed on (user, date, campaign, source file), so importing the same file twice changes nothing.
Imports are incremental: a per-user cursor on file modification time means only new or re-exported files are read. The cursor only advances after a run that actually imported something, so an empty or failed run can never skip data (a bug I found and fixed in production: cursor poisoning).
Billing rules are data, not code. Each client has a JSON template the engine evaluates in five stages: header aliases, filters, grouping, metrics (COUNT, COUNT_WHERE, SUM_COLUMN) and pricing blocks whose quantities are tokens such as {{billable_events}}, with minimum floors, rounding and tax.
Creating an invoice previews first with the same engine, refuses a second invoice for the same client and period (HTTP 409), then writes the invoice, its lines and a ledger entry, compensating (deleting) earlier writes if a later one fails. The PDF is rendered with pdf-lib and stored as a new document version.
What was built
- 01Google Drive connection with OAuth and folder selection
- 02Incremental and full-resync spreadsheet ingest
- 03Rules-driven pricing templates per client
- 04Campaign-to-client mapping: exact, prefix and regex rules
- 05Live invoice preview before anything is written
- 06Duplicate-period guard and compensating rollback
- 07Ledger entries and recorded payments per invoice
- 08Versioned PDF documents in object storage
- 09Operator portal: clients, team, templates, generation
- 10Client portal: their invoices and documents only
The hard parts
Idempotent by construction
Re-running an import or retrying a request must never double-count. Composite conflict keys on upsert and the duplicate-period guard make every write safe to repeat.
No partial invoices
Postgres writes go through a REST client without one wrapping transaction, so the create flow is a small saga: each step has a compensating delete that runs if a later step fails.
Rules people can change
Pricing differs per client and changes over time. Putting it in a typed JSON DSL with a pure evaluator means new rules are a template edit, and the same function powers preview and final invoice.
Two audiences, one app
Operators and clients share one codebase but get different areas: sessions refresh in a Next.js proxy, pages are guarded by role, and API handlers re-check the caller before touching data.
Outcome
- From raw spreadsheet to finished invoice PDF in one flow
- Imports and invoice creation safe to re-run
- Separate operator and client portals on one codebase