Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

9.0 KiB
Raw Permalink Blame History

PRD: QBO Closed-Loop Sync — Slice 1

  • Status: Draft
  • Owner: Robert Isaacs
  • Created: 2026-06-11
  • Design: ./design.md (architecture authority; this PRD details slice 1)
  • Branch: feature branches off release/qbo-online-integration

1. Problem statement & user value

Phase 1 shipped a one-way, operator-driven invoice push to QuickBooks Online. The moment a client pays an invoice in QBO, Alga is wrong: AR status, the client portal, and collections all show unpaid until someone reconciles by hand. There is also no automation (an operator must remember to create export batches), no per-invoice answer to "is this in QuickBooks?", and a dead connection is discovered only when an export fails.

Slice 1 closes the loop for the highest-value flows: a scheduled sync engine pulls QBO payments into Alga as real AR records, pushes newly finalized invoices out automatically, detects drift on exported invoices, surfaces sync state on every invoice, and warns billing admins before the connection breaks.

2. Goals (slice 1)

  • A per-tenant×realm sync cycle runs every ~15 minutes unattended, on Temporal or pg-boss via the IJobRunner abstraction, identically on hosted and appliance.
  • Payments recorded in QBO (including edits and deletions) are applied to Alga invoices within one cycle: invoice_payments row, payment transaction, status flip to paid/partially_applied.
  • Finalizing an invoice auto-enqueues its export; the cycle delivers it through the existing batch pipeline (auto-batches, origin: 'scheduled').
  • Exported invoices changed or voided in QBO are flagged as drift with re-export / accept resolutions.
  • Every invoice shows a sync badge (Not synced | Queued | Synced | Drift | Error | Voided) with a deep link into QBO; the QBO settings page shows cycle health and pending work.
  • Refresh-token expiry and auth failures notify billing admins (14/7/2-day countdown; immediate on failure).
  • Sync exceptions land in the existing workflow-task inbox, deduplicated to one open task per entity+type.

3. Non-goals (slice 1)

  • Credit memo export, credit reshape, voids (slice 2 — outlined in §9).
  • Customer mapping UI, reconciliation wizard, go-live cutoff (slice 3).
  • Outward Stripe-payment push, class/department tracking, multi-realm UX, Intuit webhooks (slice 4).
  • Importing QBO-originated documents; RefundReceipt handling (surface only).
  • Monitoring/metrics beyond the cycle stats already in the design.

4. Personas & primary flows

  • MSP billing admin: connects QBO (phase 1), leaves auto-sync on; finalized invoices appear in QBO within a cycle; when clients pay, Alga flips to paid by itself. Checks the settings health panel when something looks off; works sync exceptions from the task inbox.
  • MSP bookkeeper (in QBO): records, edits, or deletes payments in QBO as the book of record; Alga follows without being told.
  • Operator (existing flow): can still create manual export batches; they ride the same pipeline and satisfy any queued auto-export ops.

5. Functional scope (slice 1 detail)

5.1 Sync engine

Per design.md §Sync engine: accounting_sync_cycles (cursor + run history), accounting_sync_operations (outbound queue), new adapter capabilities supportsChangePolling/fetchChanges(since) (QBO: CDC) and supportsPaymentRecording (declared now, used in slice 4). Cycle order: token health → inbound (customers, payments, invoice drift) → outbound drain → cursor advance (inbound success only; outbound failures retry with capped backoff, then become exceptions). 5-minute cursor overlap; all appliers idempotent against the mapping ledger. Scheduling registered on connect / deregistered on disconnect / re-registered at startup; singleton-keyed per tenant×realm. "Sync now" triggers an immediate cycle.

5.2 Inbound payments

Per-allocation application from Payment.Line[].LinkedTxn via a shared recordExternalPayment service refactored out of PaymentService.recordPaymentFromWebhook (Stripe behavior unchanged). Idempotency/echo-suppression via payment mapping rows (alga_entity_type: 'invoice_payment'). Payment edits reverse-and-reapply; deletions write payment_reversal and recompute status. Unmapped invoice, currency mismatch → exceptions. Unapplied amounts → cycle stats only. Status flip uses a computeBalanceDue helper that matches current behavior (slice 2 swaps its internals for the credit reshape).

5.3 Invoice drift detection

Deliver snapshots exported total alongside the existing sync token in mapping metadata. Inbound invoice changes with a moved sync token compare totals / void state / doc number; material drift sets sync_status: 'drift' and files an exception with both versions and two actions: re-export (sparse update QBO to Alga's truth) or accept (refresh snapshot). Balance-only movement is not drift. Unmapped QBO invoices are counted, not imported.

5.4 Auto-export on finalize

finalizeInvoice enqueues export_invoice when a realm is connected and the tenant's auto-sync setting is on (default off until slice 1 is validated live, then on). The cycle groups pending ops into one scheduled batch through the existing validate→transform→deliver pipeline; validation failures become inbox exceptions. Manual batches mark matching ops done.

5.5 Exceptions & notifications

New system task definitions (folding in the unwired qbo_mapping_error): accounting_sync_drift, accounting_sync_unmapped_payment, accounting_sync_export_error, accounting_connection_expired. One open task per entity+type; cycles update rather than duplicate. Notifications (internal + email, to users with billing_settings update): connection expired/auth failure immediately; token expiry at 14/7/2 days; per-cycle summary only when new exceptions appeared.

5.6 UI

  • Invoice sync badge (list + detail; InvoiceTaxSourceBadge pattern) fed by mapping ledger + ops queue; tooltip with QBO doc number, last-synced time, environment-aware deep link; detail actions Sync now / View in QuickBooks.
  • Settings health panel in QboIntegrationSettings: last cycle result, next run, pending/exception/drift counts (deep-linked to inbox), token expiry countdown, Sync Now, auto-sync toggle.

6. Data model & API notes

  • New tables accounting_sync_cycles, accounting_sync_operations (columns in design.md); Citus-distributed on tenant like the accounting_export_* tables.
  • tenant_external_entity_mappings gains payment mappings (no schema change; new alga_entity_type value) and the exported-total metadata snapshot.
  • accounting_export_batches gains an origin discriminator (manual | scheduled).
  • Tenant settings: auto-sync flag (+ slice-4 placeholders deferred).
  • QBO API surface: ChangeDataCapture endpoint added to QboClientService; no other new external calls in slice 1.

7. Risks & open questions

  • scheduleRecurringJob coerces intervals to 24h — use the short-interval path or fix the coercion (see SCRATCHPAD gotchas).
  • Payment reverse-and-reapply must be transactional per payment; partial application crashes mid-cycle must not double-apply (idempotency tests cover this).
  • Refactoring recordPaymentFromWebhook touches the live Stripe path — regression risk is mitigated by keeping its tests green and behavior byte-identical.
  • Open: exact deep-link URL format for QBO sandbox vs production (resolve during implementation; environment is already known per connection).

8. Acceptance criteria / definition of done

  • All slice-1 features in features.json implemented; automated tests in tests.json (mode automated) green; existing Stripe payment and Xero suites unaffected.
  • Live Intuit-sandbox smoke (mode live-smoke in tests.json) executed and passing: connect → finalize → auto-export within a cycle → pay in QBO → Alga flips paid → edit payment → amounts follow → delete payment → reversal → drift (edit invoice total in QBO) → exception with working re-export → token alert fires when expiry forced.
  • Auto-sync default flipped on only after the smoke passes.

9. Later slices (planned separately)

Each later slice has its own full plan: ../2026-06-11-qbo-phase2-slice2-credits-voids/, ../2026-06-11-qbo-phase2-slice3-onboarding/, ../2026-06-11-qbo-phase2-slice4-polish/.

  • Slice 2 — Credits & voids: credit reshape (immutable totals + backfill, invoice_type, CM- numbering, balance-due read-site audit incl. Stripe payment-link amount), CreditMemo export, apply_credit zero-dollar Payment linkage, voidInvoice action + QBO void propagation, delete-blocking.
  • Slice 3 — Onboarding: customer mapping tab (getQboCustomers), first-connect reconciliation wizard (customers → historical invoice matching without export → go-live cutoff).
  • Slice 4 — Polish: Stripe payments pushed to QBO (deposit account picker via getQboAccounts), class/department tracking via mapping metadata + tenant defaults, multi-realm UX, optional Intuit webhooks for hosted latency.