Skip to content

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

  1. 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.
  2. ~40% of the stack is genuinely reusable. OCR adapter, File/S3, notifications, magic-link portal, AuditEvent, OutboxEvent, staff auth, observability — all product-neutral today.
  3. One consultancy team. Two repos = two CI pipelines, two deploys, two schema-migration workflows, duplicate infra. Overhead is not justified.
  4. 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

LayerReusable as-isPayroll-lockedNet-new for bookkeeping
Auth / magic linkJWT tokens, session, staff MFAToken payload extension for product + resource
Client masterClient, ClientContactSchema lacks enabledProductsAdd discriminator
RBACRole-action matrix shapePAYROLL_EXECUTIVE, PAYROLL_LEAD namesBOOKKEEPER, SENIOR_ACCOUNTANT roles + product-scoped actions
File storageS3 client, File model, retentionBucket is single-namespaceKey-prefix scoping (payroll/, bookkeeping/)
OCROcrAdapter interface, Claude/Google/Mock impls, DocumentClassification, ExtractedField, confidence scoringField schemas are payroll-specificInvoice/receipt/bank-statement field schemas
NotificationsAdapter, SMTP/mockPayroll-copy templatesBookkeeping templates (magic link, reconciliation summary)
Audit / outboxAuditEvent, OutboxEventBookkeeping event types
Portal shell/portal/[token] token verification, save-and-resume patternWizard steps (upload → review → declare) are payrollNew bookkeeping wizard (per-transaction respond/upload)
Exportspackages/excel is Infotech-onlyNew packages/exports or packages/ledger-exports with QuickBooks IIF/CSV, Xero CSV, Zoho CSV, Tally XML
DomainStatus-machine patternAll 12 services payrollJournalEntryService, ReconciliationService, MatchingEngine
API routesMiddleware, authFlat /ops/, /portal/Product-scoped routes
Worker handlersFactory pattern, S3, observabilityAll 14 handlers5–7 new handlers (parse-bank-statement, run-matcher, generate-upload-file, etc.)
WebLayout, auth scaffold, dashboard shell/dashboard/cycles, /portal wizardNew 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/                 ← unchanged

4.2 Schema changes — cross-cutting

prisma
// 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)

prisma
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

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 Product enum to packages/contracts
  • [ ] Add Client.enabledProducts column (migration); backfill existing clients to [PAYROLL]
  • [ ] Rename CycleRequestPortalInvitation with product + resourceType + resourceId columns; migration keeps existing rows with product = 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/cyclesapps/web/src/app/dashboard/payroll/cycles
  • [ ] Re-home domain services: packages/domain/src/services/*packages/domain/src/payroll/services/*; create packages/domain/src/shared/ for portal-token, file, client, audit
  • [ ] Re-home contracts: packages/contracts/src/schemas/*packages/contracts/src/schemas/payroll/*; create common/ sibling
  • [ ] Extend Role enum with BOOKKEEPER, 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 JournalEntryService with status machine DRAFT → 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/batches list + 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-matcher with 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_date on 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

RiskMitigation
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 CycleRequestPortalInvitation 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

  1. 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.
  2. Retention policy per product. Payroll retention is likely 7 years per statutory. Bookkeeping same, but per jurisdiction. Should Client.enabledProducts carry per-product retention overrides?
  3. 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 BookkeepingPeriod aggregate.
  4. 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.
  5. 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.
  6. 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

FromTo
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.tspackages/domain/src/payroll/services/cycle-service.ts
packages/domain/src/rules/packages/domain/src/payroll/rules/
packages/contracts/src/schemas/cycle.tspackages/contracts/src/schemas/payroll/cycle.ts
packages/contracts/src/schemas/client.tspackages/contracts/src/schemas/common/client.ts
packages/auth/src/roles.tsstays; extend enum
apps/worker/src/handlers/cycle-initiate.tsapps/worker/src/handlers/payroll/cycle-initiate.ts
apps/worker/src/handlers/ocr-process.tsapps/worker/src/handlers/shared/ocr-process.ts

Appendix B — Decision log

DecisionChosenAlternatives consideredWhy
Same repo vs new repoSame repoSeparate repo; hybrid (shared npm packages)Shared infra genuinely reusable; one customer, one portal, one team.
Product discriminatorProduct enum on Client + aggregatesDiscriminator column on Cycle/SubmissionCleaner separation; bookkeeping gets own aggregates, not overloaded payroll tables.
Role strategyExtend Role enum + (product, action) tuplesPer-product duplicated roles; client-level product-role matrixSimpler to implement; matches current RBAC shape; supports cross-product staffing.
Portal architectureSingle /portal/[token], dispatches on token.productSeparate /portal-payroll, /portal-bookkeepingOne URL for clients with both products; shared session, save-and-resume.
Exports packageNew packages/ledger-exportsExtend packages/excelexcel is Infotech-specific; ledger exports are a distinct domain (4 platforms, IIF/XML/CSV mix).
Worker handler organizationFlat dir per product + shared/Single flat dir with naming conventionClarity scales; shared/ makes cross-product handlers explicit.

End of plan

Internal use only — BreezyCorp