# SCRATCHPAD — QBO Slice 4: Payments Out, Class Tracking, Multi-Realm

## Decisions

- **2026-06-11 — Intuit webhooks excluded** (user-confirmed). 15-minute CDC
  polling is competitive; webhooks get their own plan only if hosted latency
  becomes a real complaint. Keeps hosted/appliance behavior identical.
- Deposit account defaults to Undeposited Funds, resolved at delivery time
  (not stored) so renamed/recreated accounts don't strand a stale ref.
- Class granularity v1 = per item mapping metadata + tenant default; no
  per-client/per-invoice overrides.
- `default_realm` becomes an explicit tenant setting; phase-1's
  first-stored-key ordering remains the fallback when unset.

## Key file paths

- Payment producer site: `recordExternalPayment` (slice-1 extraction from
  `ee/server/src/lib/payments/PaymentService.ts`)
- Echo-suppression mechanism: payment mapping rows in
  `tenant_external_entity_mappings` (slice-1 design)
- Catalog action pattern: `packages/integrations/src/actions/qboActions.ts`
- Realm defaulting to swap to setting-backed:
  `getDefaultQboRealmId` in `packages/integrations/src/lib/qbo/qboClientService.ts`
- Settings connection card to become a list:
  `packages/integrations/src/components/settings/integrations/QboIntegrationSettings.tsx`
- Batch dialog for the realm picker:
  `packages/billing/src/components/billing-dashboard/accounting/AccountingExportsTab.tsx`

## Gotchas

- QBO Payment currency must match the customer's currency; mismatches ride
  the slice-1 currency exception path — multi-currency tenants will hit it.
- DepositToAccountRef must reference an account of an allowed type
  (Other Current Asset/Bank); filter in getQboAccounts, not just in the UI.
- The double-entry scenario (bookkeeper manually re-keys a pushed Stripe
  payment) is detected at application time as an over-allocation on a
  settled invoice — exception, never auto-reversal.
- Disconnecting one realm of several must not deregister the other realm's
  cycle (singleton keys are per tenant×realm).

## Implementation notes (built 2026-06-11)

- Payment push producer lives INSIDE recordExternalPayment's success path
  with a provider !== 'quickbooks' echo guard, so any future provider pushes
  automatically; reverseExternalPayment pushes nothing (refunds out of scope).
- DepositToAccountRef omitted when unconfigured — QBO defaults to Undeposited
  Funds natively (no account lookup needed). PaymentRefNum truncated to QBO's
  21-char limit.
- Echo suppression verified end-to-end: push stores String(response.SyncToken)
  in the payment mapping; the CDC change for the same payment carries the same
  token → inbound applier no-ops.
- Double-entry guard: NEW external payments against fully-settled invoices
  file an accounting_sync_unmapped_payment exception with reason
  'over_application' and apply nothing; partial invoices accept normally.
- Realm UX landed in QboSyncHealthPanel (realm list + make-default via
  AccountingSyncHealth.realms + setDefaultQboRealm) instead of the
  integrations connection card — package-direction constraint. Batch dialog
  realm picker appears only with >1 realm. Wizard intentionally runs against
  the default realm (resolveDefaultRealm inside the onboarding actions).
- resolveDefaultRealm (settings.defaultRealm validated against the credential
  map, else first-stored-key) adopted by ALL billing call sites including the
  four syncProducers producers (follow-up TODO closed same day).
- Settings-dir contract suites (Microsoft/Xero/MspSso) have 13 pre-existing,
  shuffle-sensitive failures — identical on a clean tree (stash-verified).
