Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
6.3 KiB
PRD: QBO Closed-Loop Sync — Slice 3: Onboarding & Reconciliation
- Status: Draft
- Owner: Robert Isaacs
- Created: 2026-06-11
- Design:
../2026-06-11-qbo-phase2-closed-loop/design.md - Depends on: Slice 1 (engine, payment applier, mapping ledger); benefits from slice 2 but does not require it
1. Problem statement & user value
Nearly every tenant connecting QBO connects to an established company file: customers, invoices, and payment history already live there. Today the integration auto-provisions customers by display name on first export — which duplicates "Acme Corporation" when Alga says "Acme Corp" — and offers no way to link existing QBO invoices, so a re-export of history would double-book it. Payments recorded before connecting never replay through CDC, so linked history would read unpaid in Alga forever.
This slice makes first connect safe and deliberate: explicit customer mapping with auto-match assistance, historical invoice linking without export, a one-time payment-status backfill for matched history, and a go-live cutoff that fences which invoices auto-sync.
2. Goals
- A customer mapping surface where every Alga client can be linked to an existing QBO customer, created in QBO on demand, or left for first-export auto-provision — with exact-name matches bulk-acceptable and fuzzy matches human-confirmed.
- A re-runnable first-connect wizard: customers → historical invoice matching (mappings written, nothing exported) → go-live cutoff.
- Matched historical invoices get a one-time payment backfill through
recordExternalPayment, so their paid state and AR are correct from day one. - The go-live cutoff guarantees connecting never sprays history into QBO: only invoices finalized after the cutoff auto-enqueue.
3. Non-goals
- Importing QBO customers as new Alga clients (link-only; creation flows Alga→QBO).
- Importing unmatched QBO invoices into Alga.
- Continuous two-way customer field sync (name/address propagation beyond the cached display name from slice 1).
- Multi-realm wizard flows (single/default realm this slice; realm picker arrives in slice 4).
4. Personas & primary flows
- MSP billing admin (new connection): completes OAuth, lands in the wizard. Accepts 40 exact customer matches in one click, reviews 6 fuzzy ones, links 200 historical invoices by doc number, opts into the payment backfill, sets go-live to today. First scheduled cycle exports nothing historical; the books simply agree.
- MSP billing admin (established connection): opens the Customers mapping tab to fix one mis-linked client, or re-runs the wizard from settings after a QBO file cleanup.
5. Functional scope
5.1 Customer mapping surface
getQboCustomersserver action (realm-scoped, cached like the existing catalog actions; EE +billing_settingsread gated).- A Customers tab in the live mapping manager — a bespoke component (the generic module pattern fits dropdown mappings; this needs match suggestions and per-row actions): each Alga client row shows its mapping state and offers link to existing (searchable QBO customer picker), create in QBO now (runs the company-sync adapter immediately), or leave.
- Auto-match: normalized display-name comparison (case, punctuation, whitespace, common suffixes like Inc/LLC folded). Exact matches → bulk-accept bar ("Accept N exact matches"); near matches → suggested but individually confirmed; everything else unmatched.
- Mapping writes go to
tenant_external_entity_mappings(alga_entity_type: 'client', realm-scoped), the same rows the exporter's customer resolution already consults.
5.2 Reconciliation wizard
- Launches after the first successful OAuth connect (no completed-wizard
record for the realm); re-runnable from QBO settings. Steps:
- Customers — embeds the mapping surface (5.1) in wizard chrome.
- Historical invoices — fetch QBO invoices (paged), candidate-match
against Alga invoices by doc number + total + (when mapped) customer.
Confident matches are listed for one-click bulk link: mapping rows
written with sync-token/total snapshot, nothing exported. Ambiguous
candidates (number collision, total mismatch) go to a review list and
are never auto-linked. Includes the payment backfill option
(default on): for each linked invoice, query its QBO payments once and
apply through
recordExternalPayment(skipping invoices alreadypaid), giving history real payment records and correct status. - Go-live cutoff — set
auto_sync_start_date(default today): the slice-1 finalize producer only enqueues invoices finalized on/after this date. Existing unexported invoices before the cutoff remain manual-batch only.
- Wizard completion recorded per tenant×realm; settings shows completed/last-run state next to the re-run entry point.
6. Data model & API notes
- Tenant settings:
auto_sync_start_date, wizard completion record (per realm). No new tables — match candidates are computed live so the wizard is naturally idempotent and re-runnable. - Slice-1 producer gains the cutoff check (one condition).
- QBO API surface: Customer query (paged), Invoice query by date window
(paged), Payment query by invoice — all read-only additions to
QboClientService.
7. Risks & open questions
- Name normalization quality drives the wizard's first impression; ship the folding rules with table-driven tests and keep "exact" strict (normalized equality only — similarity scoring stays suggestion-tier).
- Payment backfill volume: a tenant with years of history triggers many Payment queries — run inside the wizard as a progress-reporting batch, not in the 15-minute cycle.
- Doc-number matching assumes Alga invoice numbers were used in QBO historically; where they weren't, matching legitimately finds nothing — the wizard must make "0 matches" a normal outcome, not an error.
8. Acceptance criteria / definition of done
- Features implemented; automated tests green; slice-1 suites unaffected.
- Live sandbox smoke against a pre-seeded QBO file (existing customers + invoices + payments): wizard links exact customers in bulk, links history without exporting, backfills paid status correctly, and a post-wizard finalize+cycle exports only the new invoice; re-running the wizard is a no-op.