# SCRATCHPAD — QBO Phase 2: Closed-Loop Accounting Sync ## Decisions (with rationale) - **2026-06-10 — QBO is authoritative for payments; Alga pushes both ways** (user-confirmed). Bookkeepers live in QBO; payments recorded there flow into Alga as real AR records, and Alga-originated (Stripe portal) payments push out so the books agree. Conflicts resolve in QBO's favor. - **2026-06-10 — CDC polling through the `IJobRunner` abstraction, no Intuit webhooks this phase** (user-confirmed, with correction: "pg-boss is usually not used, usually temporal is, but the abstraction should handle it properly"). On-prem appliances cannot receive Intuit webhooks, so polling must exist regardless; one code path for hosted + appliance. - **2026-06-10 — Credit semantics reshape lands before credit sync** (user-confirmed after impedance-mismatch review). `applyCreditToInvoice` decrements `invoices.total_amount` (`creditActions.ts:910`) — posted documents must be immutable; application moves derived balance-due. Reshape now while the credit system is barely used; backfill is recoverable (`total_amount += credit_applied`). - **2026-06-10 — Real void semantics** (user-confirmed). `cancelled` status and `invoice_cancelled` transaction type exist in the schema but are unused; a `voidInvoice` action adopts them. Hard delete blocked for exported invoices. - **2026-06-10 — Architecture B: central sync engine** (user-confirmed) over per-feature jobs (fragmented state) and over resurrecting the event-bus push model deliberately removed in Oct 2025 (migration `20251031121500_remove_qbo_workflow_automation.cjs`). - **2026-06-11 — Engine is adapter-agnostic by construction.** Shared tables carry `adapter_type`; no `qbo_` prefixes. Xero adopts the engine in a later phase ~for free. - **2026-06-11 — Test planning follows the 80/20 rule** (user-directed): automated tests target ~80% confidence; the rest is the live Intuit-sandbox smoke checklist. `tests.json` marks live-smoke entries with `"mode": "live-smoke"`. ## Key file paths - Design (scope authority for architecture): `./design.md` - Phase 1 feature commit: `01ff24f662`; design commit: `91fc97b5b6` (branch `release/qbo-online-integration`) - Adapter: `packages/billing/src/adapters/accounting/quickBooksOnlineAdapter.ts` - Client service: `packages/integrations/src/lib/qbo/qboClientService.ts` - Payment landing (to refactor into shared service): `ee/server/src/lib/payments/PaymentService.ts:598` (`recordPaymentFromWebhook`) - Credit application mutation site: `packages/billing/src/actions/creditActions.ts:910` - Job abstraction: `packages/jobs/src/lib/jobs/interfaces/IJobRunner.ts`; startup registration `server/src/lib/jobs/initializeScheduledJobs.ts` - Workflow task inbox: `shared/workflow/persistence/workflowTaskModel.ts:119`; unwired `qbo_mapping_error` task type from migration `20250511215231_consolidate_qbo_workflow_schema.cjs:126` - Badge precedent: `packages/billing/src/components/invoices/InvoiceTaxSourceBadge.tsx` ## Gotchas - `JobScheduler.scheduleRecurringJob` (`server/src/lib/jobs/jobScheduler.ts:184`) coerces every interval to 24 hours. The 15-minute cycle must use the shorter-interval path (precedent: `renew-google-gmail-watch` every 30 min) or fix the coercion. - CDC overlap window (5 min) makes duplicate delivery routine — every inbound applier must be a no-op for already-mapped, unchanged entities. - `invoice_payments` is EE-only today; acceptable because the QBO integration is EE-gated, but the shared `recordExternalPayment` service lives in EE. - Pre-existing local test failures unrelated to this work (verified via `git stash` on clean tree): `xeroAdapter.spec.ts` (needs live test DB; local `.env.localtest` creds fail), `pageTitles.metadata.test.ts` T006/T007/T024/T026, `XeroIntegrationSettings.contract.test.tsx` 3/6. - Run package tests from `server/`: `npx vitest run ../packages/...` (root `vitest.config.ts` include-globs don't match package paths). ## Commands - Targeted suites: `cd server && npx vitest run src/test/unit/api/qboOAuthRoutes.test.ts` - Typecheck a package: `cd packages/ && npx tsc --noEmit -p tsconfig.json` - Pseudo-locales after en changes: `node scripts/generate-pseudo-locales.cjs` ## Implementation notes (slice 1 built 2026-06-11) - **Scheduling deviation (F011):** instead of connect/disconnect registration, `accounting-sync-cycle` is scheduled for EVERY tenant at startup (`initializeScheduledJobs`) like the platform's other per-tenant jobs, with a cheap no-op guard for CE/unconnected/auto-sync-off tenants. Cron `*/15 * * * *` goes through `getJobRunner()` (PgBossJobRunner uses real `boss.schedule()` per-schedule queues; Temporal in EE) — NOT the legacy `initializeScheduler()` JobScheduler, which coerces intervals to 24h. - The auto-sync toggle is the engine's master switch: cycles skip when off; `force` (Sync Now) bypasses. First-run cursor = now − 5min (never imports history; the slice-3 wizard is the deliberate path for that). - One job tick per tenant enumerates realms and runs sequential per-realm cycles (multi-realm scheduling needs no extra registration in slice 4). - `RefundReceipt` added to the CDC entity set + change-entity union (stats-only). - Health panel = `QboSyncHealthPanel` in packages/billing, injected into `QboIntegrationSettings` via a `syncHealthSlot` prop threaded from `server/src/components/settings/SettingsPage.tsx` — avoids the billing↔integrations package cycle (nx cycles broke appliance builds before). - `recordExternalPayment` transaction metadata is a superset of the old Stripe metadata (adds `reference_number`); PaymentService webhook path now delegates to it (validation + PAYMENT_RECORDED/APPLIED publication unchanged). - Notifications: `system-announcement` internal template; recipients = active internal users with role_name 'admin'. Token thresholds remembered in tenant_settings.settings.accountingSync.tokenNotices[realm]. - Sync Now runs the cycle inline in the server action (no job round-trip). - T032 (DB-backed integration cycle test) + T033 (migration round-trip) deferred: local test DB unavailable (pre-existing). Stripe integration tests are DB-skipped locally; T016 verified via typecheck + unchanged validation semantics. Live-smoke T034-T041 pending an Intuit sandbox session.