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

182 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.