Bookkeeping Module — Integration Plan
Status: Draft for review Author: Architecture assessment Date: 2026-04-23 Related: SOP_AI_Bookkeeping_Automation.md
1. Context
Spade is a consultancy offering two distinct services:
- Payroll (currently implemented) — monthly payroll cycle orchestration for Singapore clients
- Bookkeeping (proposed) — monthly/annual AI-assisted document ingestion, journal entry generation, and bank reconciliation per the SOP
Some clients will consume payroll only, some bookkeeping only, some both. A client using both should experience one product, one login, one contact list.
2. Decision: Same Repo, New Product Module
Build bookkeeping as a first-class product module inside breezycorp — do not fork into a new repo.
Rationale
- Same customer, same portal session. A client using both services logs in once and sees both engagements in one place. A split repo forces a shared-auth / shared-client-master package and you'll fight cross-repo drift.
- ~40% of the stack is genuinely reusable. OCR adapter, File/S3, notifications, magic-link portal,
AuditEvent,OutboxEvent, staff auth, observability — all product-neutral today. - One consultancy team. Two repos = two CI pipelines, two deploys, two schema-migration workflows, duplicate infra. Overhead is not justified.
- UX coherence. Payroll Executive ↔ Bookkeeper cross-staffing is likely. Shared design language and component library keep the product feeling like one thing.
What this does NOT mean
- Bookkeeping is not stuffed into existing payroll tables with a discriminator column. Payroll aggregates (
PayrollCycle,Submission,ApprovalRound) stay payroll. Bookkeeping gets its own aggregates (JournalBatch,BankStatement,ReconciliationRun). - Shared infrastructure (auth, files, OCR, notifications) is the integration layer, not the domain layer.
3. Reuse vs. Build — Inventory
| Layer | Reusable as-is | Payroll-locked | Net-new for bookkeeping |
|---|---|---|---|
| Auth / magic link | JWT tokens, session, staff MFA | — | Token payload extension for product + resource |
| Client master | Client, ClientContact | Schema lacks enabledProducts | Add discriminator |
| RBAC | Role-action matrix shape | PAYROLL_EXECUTIVE, PAYROLL_LEAD names | BOOKKEEPER, SENIOR_ACCOUNTANT roles + product-scoped actions |
| File storage | S3 client, File model, retention | Bucket is single-namespace | Key-prefix scoping (payroll/, bookkeeping/) |
| OCR | OcrAdapter interface, Claude/Google/Mock impls, DocumentClassification, ExtractedField, confidence scoring | Field schemas are payroll-specific | Invoice/receipt/bank-statement field schemas |
| Notifications | Adapter, SMTP/mock | Payroll-copy templates | Bookkeeping templates (magic link, reconciliation summary) |
| Audit / outbox | AuditEvent, OutboxEvent | — | Bookkeeping event types |
| Portal shell | /portal/[token] token verification, save-and-resume pattern | Wizard steps (upload → review → declare) are payroll | New bookkeeping wizard (per-transaction respond/upload) |
| Exports | — | packages/excel is Infotech-only | New packages/exports or packages/ledger-exports with QuickBooks IIF/CSV, Xero CSV, Zoho CSV, Tally XML |
| Domain | Status-machine pattern | All 12 services payroll | JournalEntryService, ReconciliationService, MatchingEngine |
| API routes | Middleware, auth | Flat /ops/, /portal/ | Product-scoped routes |
| Worker handlers | Factory pattern, S3, observability | All 14 handlers | 5–7 new handlers (parse-bank-statement, run-matcher, generate-upload-file, etc.) |
| Web | Layout, auth scaffold, dashboard shell | /dashboard/cycles, /portal wizard | New dashboard + portal pages under bookkeeping/ |
4. Target Architecture
4.1 Directory layout (post-refactor)
apps/
api/
src/routes/
portal/ ← product-aware (token carries product + resource)
payroll/ ← existing portal wizard
bookkeeping/ ← per-transaction response UI
ops/
payroll/ ← moved from flat /ops/
bookkeeping/ ← new
admin/ ← shared (clients, staff, templates)
worker/
src/handlers/
payroll/ ← moved
bookkeeping/ ← new
shared/ ← ocr-process, delete-s3-object, retention-purge, outbox-poller
web/
src/app/
dashboard/
payroll/ ← moved from /dashboard/cycles
bookkeeping/ ← new
portal/[token]/
(product-aware router based on token payload)
packages/
contracts/
src/
enums/ ← Product, Role, Action (extended)
schemas/
common/ ← Client, Contact, File, AuditEvent, PortalInvitation
payroll/ ← Cycle, Submission, Export
bookkeeping/ ← JournalBatch, BankStatement, Reconciliation
domain/
src/
shared/ ← portal-token, file, client, audit
payroll/ ← existing services (moved)
bookkeeping/ ← JournalEntryService, MatchingEngine, ReconciliationService
auth/ ← extended role + action enums
db/ ← one Prisma schema, new models added
documents/ ← OCR adapter unchanged; add bookkeeping field schemas
notifications/ ← add bookkeeping templates
ledger-exports/ ← NEW: QuickBooks IIF/CSV, Xero CSV, Zoho CSV, Tally XML
excel/ ← stays Infotech-payroll-specific (consider renaming)
observability/ ← unchanged4.2 Schema changes — cross-cutting
// New enum (contracts + db)
enum Product {
PAYROLL
BOOKKEEPING
}
model Client {
// existing fields...
enabledProducts Product[] // e.g. [PAYROLL, BOOKKEEPING]
bookkeepingConfig Json? // COA ref, tax codes, bank accounts, accounting platform
}
// Generalize CycleRequest -> PortalInvitation
model PortalInvitation {
id String @id @default(uuid())
clientId String
contactId String
product Product
resourceType String // "PAYROLL_CYCLE" | "BOOKKEEPING_BATCH" | "RECONCILIATION_RUN"
resourceId String
tokenHash String
expiresAt DateTime
consumedAt DateTime?
// ...
}4.3 New bookkeeping models (high-level)
model BookkeepingDocument {
id String @id @default(uuid())
clientId String
sourceChannel String // EMAIL | WHATSAPP | FOLDER | CLIENT_PORTAL
fileId String // references shared File model
ingestedAt DateTime
documentType String? // INVOICE | RECEIPT | BILL | CREDIT_NOTE | BANK_STATEMENT
extractionId String? // references shared DocumentExtraction
// ...
}
model JournalBatch {
id String @id @default(uuid())
clientId String
periodStart DateTime
periodEnd DateTime
status String // DRAFT | FLAGGED | UNDER_REVIEW | APPROVED | EXPORTED
uploadFileId String? // generated platform-specific export
// ...
}
model JournalEntry {
id String @id @default(uuid())
batchId String
documentId String?
entryDate DateTime
reference String
description String
debitAccount String
creditAccount String
amount Decimal
taxAmount Decimal
totalAmount Decimal
currency String
exchangeRate Decimal?
status String // DRAFT | FLAGGED | APPROVED | REJECTED
flags String[]
// ...
}
model VendorMaster {
id String @id @default(uuid())
clientId String
name String
defaultAccountCode String?
// ...
}
model ChartOfAccountsEntry {
id String @id @default(uuid())
clientId String
accountCode String
accountName String
accountType String
// ...
}
model BankStatement {
id String @id @default(uuid())
clientId String
bankAccountRef String
fileId String
periodStart DateTime
periodEnd DateTime
openingBalance Decimal
closingBalance Decimal
// ...
}
model BankTransaction {
id String @id @default(uuid())
statementId String
txnDate DateTime
description String
debitAmount Decimal?
creditAmount Decimal?
balance Decimal
// ...
}
model ReconciliationRun {
id String @id @default(uuid())
statementId String
status String // IN_PROGRESS | AWAITING_CLIENT | COMPLETE
completedAt DateTime?
reportFileId String?
}
model ReconciliationMatch {
id String @id @default(uuid())
runId String
bankTxnId String
journalEntryId String?
matchType String // EXACT | PROPOSED | MANUAL
matchedAt DateTime
matchedBy String // SYSTEM | staffUserId
}
model UnreconciledItem {
id String @id @default(uuid())
runId String
bankTxnId String
clientResponse String? // SUPPORTING_UPLOADED | NO_SUPPORTING
clientNote String?
respondedAt DateTime?
}5. RBAC Changes
5.1 Role additions (option A — recommended)
Keep existing payroll role names; add bookkeeping roles. Permissions become (product, action) tuples.
Existing:
CLIENT_SUBMITTER, CLIENT_APPROVER (product-neutral — reuse for both)
PAYROLL_EXECUTIVE, PAYROLL_LEAD (payroll only)
PLATFORM_ADMIN (cross-product)
New:
BOOKKEEPER (bookkeeping equivalent of PAYROLL_EXECUTIVE)
SENIOR_ACCOUNTANT (bookkeeping equivalent of PAYROLL_LEAD)A single StaffUser can hold multiple roles across products. When a user logs in, the dashboard shows only product sections their roles grant access to.
5.2 Action enum split
Instead of flat actions (GENERATE_EXPORT), use (product, action):
(PAYROLL, GENERATE_EXPORT)
(BOOKKEEPING, GENERATE_UPLOAD_FILE)
(BOOKKEEPING, APPROVE_JOURNAL)
(BOOKKEEPING, RECONCILE_BANK_TXN)Permission check: can(user, product, action, clientId).
5.3 Client-side roles
CLIENT_SUBMITTER and CLIENT_APPROVER already exist and are product-neutral by function. Rename any payroll-specific copy; leave the enum alone.
6. Phased Rollout
Phase 0 — Product scoping refactor (prerequisite, ~1 week)
Goal: Introduce product as a first-class concept without changing behavior.
- [ ] Add
Productenum topackages/contracts - [ ] Add
Client.enabledProductscolumn (migration); backfill existing clients to[PAYROLL] - [ ] Rename
CycleRequest→PortalInvitationwithproduct+resourceType+resourceIdcolumns; migration keeps existing rows withproduct = PAYROLL,resourceType = PAYROLL_CYCLE - [ ] Move
apps/api/src/routes/ops/*→apps/api/src/routes/ops/payroll/*(URL migration, update web callers) - [ ] Move
apps/web/src/app/dashboard/cycles→apps/web/src/app/dashboard/payroll/cycles - [ ] Re-home domain services:
packages/domain/src/services/*→packages/domain/src/payroll/services/*; createpackages/domain/src/shared/for portal-token, file, client, audit - [ ] Re-home contracts:
packages/contracts/src/schemas/*→packages/contracts/src/schemas/payroll/*; createcommon/sibling - [ ] Extend
Roleenum withBOOKKEEPER,SENIOR_ACCOUNTANT(even if unused yet) and introduce(product, action)permission tuples - [ ] Dashboard nav: add product-switcher; hide bookkeeping for clients without it enabled
Exit criteria: Full test suite green. Existing payroll flows identical. Zero new functionality shipped.
Phase 1 — Bookkeeping Phase 1 (SOP §4): Doc → Journal (4–6 weeks)
- [ ] Prisma models:
BookkeepingDocument,JournalBatch,JournalEntry,VendorMaster,ChartOfAccountsEntry - [ ] Field schemas for invoice / receipt / bill in
packages/documents/src/schemas/bookkeeping/ - [ ] Domain service
JournalEntryServicewith status machineDRAFT → FLAGGED → APPROVED → EXPORTED - [ ] Classification engine (vendor master → COA keyword → category inference → fallback)
- [ ] Worker handlers:
bookkeeping/ingest-document,bookkeeping/classify-and-draft,bookkeeping/generate-upload-file - [ ] New package
packages/ledger-exports:- QuickBooks IIF + CSV (Import Transactions template)
- Xero CSV (Manual Journal Import)
- Zoho Books CSV (Manual Journal)
- Tally XML (VOUCHER / ALLLEDGERENTRIES)
- [ ] API:
/ops/bookkeeping/batches,/ops/bookkeeping/journal-entries/:id/approve|edit|reject|escalate - [ ] Web:
/dashboard/bookkeeping/batcheslist + per-entry review UI (mirror existing approval UI patterns) - [ ] Notification templates: batch ready for review, upload file generated
- [ ] Input channel: email inbox monitor (IMAP or provider API). WhatsApp + folder monitoring deferred to a later phase — email is enough to validate the loop.
Exit criteria: End-to-end flow for email-ingested document → draft entry → reviewer approval → downloaded upload file for QuickBooks.
Phase 2 — Bookkeeping Phase 2 (SOP §5): Bank Rec + Client Portal (4–6 weeks)
- [ ] Prisma models:
BankStatement,BankTransaction,ReconciliationRun,ReconciliationMatch,UnreconciledItem - [ ] Bank statement parser (PDF + CSV/XLSX). Leverage existing OCR adapter for PDF
- [ ] Matching engine as worker handler
bookkeeping/run-matcherwith configurable date tolerance - [ ] Client portal:
/portal/[token]/bookkeeping/unreconciled— per-transaction respond UI (upload supporting OR declare no-supporting) - [ ] Portal save-and-resume wired to
UnreconciledItem.respondedAt - [ ] Worker handler
bookkeeping/process-client-response— routes uploads into Phase 1 pipeline - [ ] Reconciliation report generation (CSV + PDF)
- [ ] Notification templates: magic link for unreconciled items, reconciliation complete
Exit criteria: Client receives statement link → responds to all items → reviewer approves new entries → reconciliation report generated.
Phase 3 — Annual engagements & polish (2–3 weeks)
- [ ]
engagement_start_date/engagement_end_dateon client config - [ ] Bulk folder ingestion (100+ doc batches) — queue management, rate limiting
- [ ] Per-month or per-quarter upload-file packaging
- [ ] Portal filtering by month + sorting, extended 60-day token expiry
- [ ] Exception queue UI for no-supporting materiality review
- [ ] Full audit export (CSV) per SOP §8
Phase 4 — Additional input channels (deferred)
- [ ] WhatsApp Business API ingestion
- [ ] Google Drive / SharePoint / Dropbox folder monitoring
- [ ] Channel routing + dedup across channels
7. Risks & Tradeoffs
| Risk | Mitigation |
|---|---|
Payroll vocabulary leaks into shared code. cycle, submission, approval round are baked into variable names and API shapes. Phase 0 must hunt these down. | Budget the full week for Phase 0 hygiene. Use grep audits + code review focus. |
| Multi-currency + FX accuracy. Bookkeeping is more sensitive to FX than payroll. | Dedicated FX adapter in Phase 1; record rate source + timestamp on every multi-currency entry. |
| OCR confidence on accounting documents may be lower than payroll forms (unstructured invoices). | Field-level confidence gating + explicit review UI. Same pattern as payroll; start with Claude adapter. |
| Accounting platform export format drift (QuickBooks/Xero/Zoho/Tally change templates). | Versioned export templates per platform, similar to ExportTemplateVersion for payroll. Snapshot template with each upload file. |
Schema migration blast radius. Phase 0 touches CycleRequest → PortalInvitation rename, which is a load-bearing table. | Write migration with ALTER + RENAME, not DROP; ship with rollback.sql per project runbook. |
| Dashboard complexity for dual-product clients. One client with both services needs clear context switching. | Product switcher in top nav; URL encodes product (/dashboard/payroll/... vs /dashboard/bookkeeping/...); never auto-switch. |
| Role explosion. Adding products will compound roles. | Keep permission layer (product, action) — don't create PAYROLL_REVIEWER and BOOKKEEPING_REVIEWER as separate roles; one role, scoped by product assignment. |
8. Open Questions for Product
- Accounting platform posting. SOP §4.5 says "no direct API connection — manual upload only." Is that a permanent stance or MVP-only? Direct API would unlock automation but requires per-client OAuth and complicates audit.
- Retention policy per product. Payroll retention is likely 7 years per statutory. Bookkeeping same, but per jurisdiction. Should
Client.enabledProductscarry per-product retention overrides? - Billing / engagement. Do bookkeeping engagements have a concept equivalent to payroll cycles (monthly locked period) or is it a rolling ledger? Affects whether we need a
BookkeepingPeriodaggregate. - Single reviewer vs. multiple. SOP mentions "assigned human reviewer." Is this one person per client, a pool, or a routing rule? Affects notification targets and workload balancing.
- Client portal branding per product. Does a dual-product client see one portal with two sections, or two separate magic-link portals? Recommendation: one portal, tabbed. Confirm.
- WhatsApp priority. Is WhatsApp ingestion a P0 or deferrable? SOP lists it as a first-class channel but email covers 80% of use cases.
9. Success Metrics (for bookkeeping module)
- Time from document ingestion → draft journal entry: < 5 min p95
- Reviewer review time per batch of 50 entries: < 30 min (vs. fully manual bookkeeping)
- Auto-reconciliation rate (exact matches): > 60% of bank transactions
- Client portal response time (magic link → all items addressed): < 5 business days p50
- Zero payroll regressions during Phase 0 refactor (full test suite + manual smoke gate)
10. Non-Goals
- Replacing human accountants or approvers. System remains human-in-the-loop.
- Direct posting to accounting platforms in MVP (see open question 1).
- Tax filing or statutory submission automation.
- Supporting accounting platforms beyond QuickBooks / Xero / Zoho / Tally in MVP.
- Real-time reconciliation. Batch-per-statement is sufficient.
Appendix A — Phase 0 file move map
| From | To |
|---|---|
apps/api/src/routes/ops/cycles/ | apps/api/src/routes/ops/payroll/cycles/ |
apps/api/src/routes/portal/ | apps/api/src/routes/portal/payroll/ (router dispatches by token product) |
apps/web/src/app/dashboard/cycles/ | apps/web/src/app/dashboard/payroll/cycles/ |
apps/web/src/app/portal/[token]/ | stays; internal switch on token.product |
packages/domain/src/services/cycle-service.ts | packages/domain/src/payroll/services/cycle-service.ts |
packages/domain/src/rules/ | packages/domain/src/payroll/rules/ |
packages/contracts/src/schemas/cycle.ts | packages/contracts/src/schemas/payroll/cycle.ts |
packages/contracts/src/schemas/client.ts | packages/contracts/src/schemas/common/client.ts |
packages/auth/src/roles.ts | stays; extend enum |
apps/worker/src/handlers/cycle-initiate.ts | apps/worker/src/handlers/payroll/cycle-initiate.ts |
apps/worker/src/handlers/ocr-process.ts | apps/worker/src/handlers/shared/ocr-process.ts |
Appendix B — Decision log
| Decision | Chosen | Alternatives considered | Why |
|---|---|---|---|
| Same repo vs new repo | Same repo | Separate repo; hybrid (shared npm packages) | Shared infra genuinely reusable; one customer, one portal, one team. |
| Product discriminator | Product enum on Client + aggregates | Discriminator column on Cycle/Submission | Cleaner separation; bookkeeping gets own aggregates, not overloaded payroll tables. |
| Role strategy | Extend Role enum + (product, action) tuples | Per-product duplicated roles; client-level product-role matrix | Simpler to implement; matches current RBAC shape; supports cross-product staffing. |
| Portal architecture | Single /portal/[token], dispatches on token.product | Separate /portal-payroll, /portal-bookkeeping | One URL for clients with both products; shared session, save-and-resume. |
| Exports package | New packages/ledger-exports | Extend packages/excel | excel is Infotech-specific; ledger exports are a distinct domain (4 platforms, IIF/XML/CSV mix). |
| Worker handler organization | Flat dir per product + shared/ | Single flat dir with naming convention | Clarity scales; shared/ makes cross-product handlers explicit. |
End of plan