# Accounting, User Management & Customer Portal — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. **Goal:** Extend Zappier with three surfaces: (a) company accounting & system management, (b) internal user management, and (c) an end-user customer portal — with full documentation and screenshot walkthroughs for each. **Architecture:** Same ports-and-adapters style as the existing codebase. New domain modules (`src/invoicing.ts`, `src/reports.ts`, `src/accounts.ts`) expose pure logic + repo interfaces; SQLite adapters live in `src/db/`; HTTP wiring goes into `src/admin.ts` (company side) and a new `src/portal.ts` (customer side). The admin SPA gains tabs; the portal is a second dependency-free SPA under `portal/`. No new runtime deps in Phase 1–2; Phase 3 adds `otplib`-style TOTP only if unavoidable — prefer `node:crypto` scrypt for passwords and a minimal TOTP implementation per RFC 6238. **Tech Stack:** Node 20 · TS strict · Express 4 · better-sqlite3 · jest/supertest. ## Global Constraints - Money is integer cents everywhere; multipliers applied once, `Math.round`. - Money/date logic must be pure functions, unit-tested without HTTP or SQLite. - All SQL in `src/db/` repos only; all tables `CREATE TABLE IF NOT EXISTS`; schema changes for existing tables use idempotent `ALTER TABLE ... ADD COLUMN` guarded by `PRAGMA table_info` checks. - Existing 87 jest tests + 4 mocha tests stay green; `npx tsc --noEmit` clean after every task. - No servers left running after any verification; walkthrough screenshots use a throwaway `ZAPPIER_DB`. - PDFs: Phase 1–2 deliver **print-ready HTML report/invoice pages** (browser Print → PDF, zero deps) plus CSV; binary PDF generation is deferred unless the owner asks. - The demo password `$$$Adm1n###` and `admin-dev-key` remain dev defaults, overridable by env. ## Surface map | Surface | URL | Auth | Audience | |---|---|---|---| | Admin SPA (extended) | `/admin` | login token / `x-admin-key` | Company ops & accounting | | Customer portal SPA (new) | `/portal` | customer session token (+TOTP 2FA) | End-user customers | | Public API | `/v1` | `x-api-key` | Unchanged | --- ## Phase 1 — Accounting backend ### Task 1: Schema + customer billing identity **Files:** - Modify: `src/auth.ts` — `Customer` gains `billingType: 'stripe' | 'purchase_order'` (default `'stripe'`), `email?: string` - Modify: `src/db/customer-repo.ts` — idempotent `ALTER TABLE customers ADD COLUMN billing_type TEXT NOT NULL DEFAULT 'stripe'`; same for `email TEXT`; map in `toCustomer` - Modify: `src/admin.ts` — `PUT /customers/:id` accepts `billingType` (validated) and `email` - Test: `tests/db-customer.test.ts`, `tests/admin.test.ts` (extend) **Interfaces:** - Consumes: existing `CustomerRepo` - Produces: `Customer.billingType`, `Customer.email` used by all later tasks - [x] Extend `tests/db-customer.test.ts`: migration on existing DB adds columns with `'stripe'` default; round-trip `billingType`/`email` - [x] Extend `tests/admin.test.ts`: `PUT /customers/:id` sets `billingType: 'purchase_order'`; invalid value → 400 - [x] Implement; run suites; commit `feat(accounting): customer billing types and email` ### Task 2: Invoice engine **Files:** - Create: `src/invoicing.ts` — types + pure generation logic + repo interface - Create: `src/db/invoice-repo.ts` — SQLite adapter - Test: `tests/invoicing.test.ts`, `tests/db-invoice.test.ts` **Interfaces:** ```ts export interface Invoice { id: string; // INV-2026-07- customerId: string; period: string; // YYYY-MM status: 'draft' | 'issued' | 'paid'; lines: InvoiceLine[]; // { endpointId, calls, cents } totalCents: number; // sum of lines (gross usage) creditCents: number; // monthly credit applied billableCents: number; // totalCents - creditCents, floored at 0 billingType: 'stripe' | 'purchase_order'; poNumber?: string; // PO billing only issuedAtMs?: number; dueAtMs?: number; paidAtMs?: number; } export interface InvoiceRepo { save(invoice: Invoice): void; get(id: string): Invoice | undefined; list(filter: { customerId?: string; period?: string; status?: Invoice['status'] }): Invoice[]; nextSequence(period: string): number; } export function buildInvoice(args: { customer: Customer; period: string; sequence: number; entries: UsageEntry[]; tier: TierConfig; poNumber?: string; }): Invoice; // groups entries by endpoint; status 'draft' ``` - [x] Tests: line grouping, credit math (partial/zero/excess), id format, PO fields - [x] Repo tests: save/get/list filters, sequence increments per period - [x] Implement; commit `feat(accounting): invoice engine` ### Task 3: Reports service (billing + trends, CSV/JSON) **Files:** - Create: `src/reports.ts` — `billingRows(entries, customers, tiers, range)` → rows `{customerId, name, billingType, calls, totalCents, creditCents, billableCents}`; `usageTrend(entries, bucket: 'day'|'week')` → `[{bucket, calls, cents}]`; `toCsv(rows)` with RFC-4180 escaping - Test: `tests/reports.test.ts` - [x] Tests: date-range filtering (inclusive from, exclusive to), per-customer vs all, per-billingType filter, trend bucketing across month boundary, CSV quoting of commas/quotes/newlines - [x] Implement; commit `feat(accounting): reports service` ### Task 4: Admin accounting API **Files:** - Modify: `src/admin.ts` — new routes (all behind existing admin auth): - `POST /invoices/generate { period, customerId?, poNumber? }` → builds draft invoices for the period (all customers or one; idempotent per customer+period — regenerating replaces the draft) - `POST /invoices/:id/issue`, `POST /invoices/:id/paid` - `GET /invoices?customerId&period&status` - `GET /invoices/:id` (+ `?format=html` print-ready invoice page) - `GET /reports/billing?from&to&customerId&billingType&format=json|csv` - `GET /reports/usage-trend?from&to&bucket&customerId` - `GET /zapier/status` → `{ published: boolean, triggerCount, actionCount, baseUrl }` read from `zapier-app/` files (static inspection, no network) - Test: `tests/admin-accounting.test.ts` - [x] Tests per route incl. CSV content-type, filter combos, invoice lifecycle transitions (draft→issued→paid; illegal transitions → 409) - [x] Implement; commit `feat(accounting): admin accounting API` --- ## Phase 2 — Admin UI: Accounting, Reports, Users tabs ### Task 5: Invoices tab (generate, filter, lifecycle, print page) ### Task 6: Reports tab (date-range pickers, customer + billing-type filters, CSV download, trend charts as inline SVG) ### Task 7: Users tab (admin account management: list/create/deactivate admin users backed by a new `admin_users` table replacing the static two-account map; login endpoint reads the table; env seed preserved) ### Task 8: System tab (Zapier connection status, billing job last-run info from `billing_reports`/`job_locks`) Each: admin UI section + `tests/` coverage for any new API + screenshot verification. Commit per task. --- ## Phase 3 — Customer portal (`/portal`) ### Task 9: Customer identity - `customers` += `password_hash`, `totp_secret`, `totp_enabled`, `email_verified` - `src/accounts.ts`: scrypt hash/verify (`node:crypto`), session tokens (new `portal_sessions` table), signup `POST /portal/api/signup` (creates customer on `free` tier + issues API key), login `POST /portal/api/login` - Tests: hash round-trip, signup/login flows, session expiry ### Task 10: TOTP 2FA - RFC 6238 TOTP (HMAC-SHA1, 30 s step, 6 digits) implemented in `src/accounts.ts` (no dep): `generateTotpSecret`, `totpUri(secret, email)`, `verifyTotp(secret, code, window=1)` - Routes: `POST /portal/api/2fa/setup` (returns secret + otpauth URI; QR rendered client-side via a tiny inline QR lib or Google-Charts-free canvas QR — decision: render otpauth URI as text + QR via `qrcode` npm dep, portal-side only), `POST /portal/api/2fa/enable`, `POST /portal/api/2fa/verify` (login second step), `POST /portal/api/2fa/disable` - Tests: known RFC vectors, window tolerance, login requires second factor when enabled ### Task 11: Portal dashboard API - `GET /portal/api/me` (profile, tier, apiKey, regenerate key `POST /portal/api/api-key`) - `GET /portal/api/usage` (month-to-date + credit) - `GET /portal/api/invoices` (own invoices only, scoped by session customer) - `POST /portal/api/reload { amountCents }` — prepaid balance: `customers` += `balance_cents`; Stripe PaymentIntent via existing SDK (test mode); balance drawn down at invoice issue before metered reporting - `PUT /portal/api/email-invoicing { enabled, email }` — stored prefs; billing job emails PO invoices (send via SMTP env config; dev: log-only transport) - Tests: session scoping (cannot read other customers' invoices), reload math, key regeneration invalidates old key ### Task 12: Portal SPA - `portal/index.html` + `portal/app.js` in the same design language as the new admin: login/signup/2FA screens, dashboard (usage + balance), invoices (history + print), billing (reload, email invoicing), API docs + pricing info pages (rendered from `openapi.yaml` + live rate card) - Screenshot verification of every screen --- ## Phase 4 — Documentation ### Task 13: `docs/ACCOUNTING.md` + PDF — company accounting walkthrough (invoices, PO billing, reports, trends, CSV export) with screenshots ### Task 14: `docs/USER-MANAGEMENT.md` + PDF — admin users walkthrough ### Task 15: `docs/CUSTOMER-PORTAL.md` + PDF — end-user walkthrough (signup, 2FA, reload, invoices, API docs) ### Task 16: Refresh `USER-MANUAL.md`, `DEVELOPER.md`, `README.md`; re-export all PDFs --- ## Self-review notes - Spec coverage: Stripe **and** PO billing (Tasks 1–2, 11), invoicing histories (2, 4, 5), CSV/PDF + date range + all/individual/by-type (3, 4, 6), usage/billing trends (3, 6), management reports (4, 6, 8), Zapier connections (4, 8), user management (7), customer login/setup/2FA/invoices/reload/email invoicing/API docs/pricing (9–12), docs for all three surfaces (13–16). - Type consistency: `Invoice`, `InvoiceRepo`, `billingRows`, `usageTrend`, `toCsv` signatures are defined once above and reused in every later task.