10 KiB
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 tablesCREATE TABLE IF NOT EXISTS; schema changes for existing tables use idempotentALTER TABLE ... ADD COLUMNguarded byPRAGMA table_infochecks. - Existing 87 jest tests + 4 mocha tests stay green;
npx tsc --noEmitclean 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###andadmin-dev-keyremain 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—CustomergainsbillingType: 'stripe' | 'purchase_order'(default'stripe'),email?: string - Modify:
src/db/customer-repo.ts— idempotentALTER TABLE customers ADD COLUMN billing_type TEXT NOT NULL DEFAULT 'stripe'; same foremail TEXT; map intoCustomer - Modify:
src/admin.ts—PUT /customers/:idacceptsbillingType(validated) andemail - Test:
tests/db-customer.test.ts,tests/admin.test.ts(extend)
Interfaces:
-
Consumes: existing
CustomerRepo -
Produces:
Customer.billingType,Customer.emailused by all later tasks -
Extend
tests/db-customer.test.ts: migration on existing DB adds columns with'stripe'default; round-tripbillingType/email -
Extend
tests/admin.test.ts:PUT /customers/:idsetsbillingType: 'purchase_order'; invalid value → 400 -
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:
export interface Invoice {
id: string; // INV-2026-07-<customerSeq>
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'
- Tests: line grouping, credit math (partial/zero/excess), id format, PO fields
- Repo tests: save/get/list filters, sequence increments per period
- 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 -
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
-
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/paidGET /invoices?customerId&period&statusGET /invoices/:id(+?format=htmlprint-ready invoice page)GET /reports/billing?from&to&customerId&billingType&format=json|csvGET /reports/usage-trend?from&to&bucket&customerIdGET /zapier/status→{ published: boolean, triggerCount, actionCount, baseUrl }read fromzapier-app/files (static inspection, no network)
-
Test:
tests/admin-accounting.test.ts -
Tests per route incl. CSV content-type, filter combos, invoice lifecycle transitions (draft→issued→paid; illegal transitions → 409)
-
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_verifiedsrc/accounts.ts: scrypt hash/verify (node:crypto), session tokens (newportal_sessionstable), signupPOST /portal/api/signup(creates customer onfreetier + issues API key), loginPOST /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 viaqrcodenpm 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 keyPOST /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 reportingPUT /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.jsin 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 fromopenapi.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,toCsvsignatures are defined once above and reused in every later task.