# Phase 12: Orders/Invoices & Company Matching - Context

**Gathered:** 2026-07-11
**Status:** Ready for planning

<domain>
## Phase Boundary

Pulse gains historical PAX8 cost data for reconciliation over time (order/invoice
line items into `pax8_orders`/`pax8_order_items`), and every PAX8 company is
automatically linked to its Autotask counterpart by fuzzy name matching, or
explicitly flagged for review rather than silently guessed. No `/pax8` UI (Phase
14 — this phase only populates `pax8_company_match_review`), no scheduler/cron
wiring (Phase 13), no manual-resolution admin flow (Phase 14 — PAX8-12).

</domain>

<decisions>
## Implementation Decisions

### Match Confidence Threshold
- **D-01:** Auto-link (no human review) only on a high-confidence fuzzy match
  — not exact-string-only, but a high similarity bar (near-identical names:
  punctuation/case/whitespace differences, minor typos). The user explicitly
  chose "be conservative — fewer auto-matches" over a looser threshold or
  deferring the number to the planner's judgment alone: bias toward flagging
  borderline cases for review rather than risking a wrong auto-link, since
  this data feeds cost/billing reconciliation. The planner should document
  the exact numeric threshold chosen (and the library/approach) directly in
  the plan so it's easy to find and tune later — this is an initial number,
  not a permanently fixed one.
- **D-02:** Even an otherwise-exact name match must be flagged for review
  (not auto-linked) if it's ambiguous against more than one Autotask company
  sharing that same/very-similar name (e.g. franchise locations, "Acme Inc"
  vs "Acme Holdings Inc"). No silent tie-breaking — ever, even when the
  string match itself looks perfect.

### No-Match & Ambiguous-Candidate Handling
- **D-03:** When a PAX8 company has zero reasonably-similar Autotask
  candidates at all, still create a `pax8_company_match_review` row with an
  empty `candidate_company_ids` array — never silently drop it (PAX8-11).
  This flags it into the admin queue so Phase 14's UI can offer a manual
  search/pick, or confirm there truly isn't a matching Autotask company yet.
- **D-04:** Ambiguous-match review rows carry the **top 3** highest-scoring
  Autotask candidates (matches the existing `device_link_review` precedent's
  style of showing a small ranked list, not every plausible match).
- **D-05:** PAX8 companies still sitting unresolved in the review queue are
  **re-scored on every subsequent full sync**, not matched once and left
  alone — candidates can improve over time (e.g. a renamed/newly-created
  Autotask company scores better later) without requiring a manual
  re-trigger. This does NOT apply to already-resolved matches: SC#4's
  idempotency guarantee (a manually-confirmed match is never overwritten by
  a later sync) still holds — only rows with `resolved_at IS NULL` are
  eligible for re-scoring.

### Claude's Discretion
- **Exact threshold value / library choice** — user wants "conservative,"
  not a specific number. The planner should research common fuzzy
  name-matching approaches (e.g., trigram similarity via Postgres
  `pg_trgm`, or a JS library) and pick/document a concrete high threshold,
  erring toward fewer auto-matches per D-01.
- **Company name normalization nuances** (legal suffixes like LLC/Inc/Corp,
  punctuation, abbreviations) — not discussed in depth this session (user
  deselected this gray area). Planner/researcher should investigate whether
  Autotask company names in this instance commonly carry legal suffixes
  PAX8 names don't (or vice versa) and decide normalization rules
  accordingly; err toward the conservative stance (D-01/D-02) if uncertain.
- **Orders/invoices historical lookback window** — not discussed in depth
  this session (user deselected this gray area). `migrations/091`'s comment
  states "sync pulls full order history on first sync in a later phase"
  (this phase) — the planner should confirm this is still the intent (full
  history, no bounded window) during research, and flag to the user if
  PAX8's invoices API makes "full history" impractical (e.g., no
  pagination limit safety, or a very large per-company invoice count).

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Project scope & requirements
- `.planning/PROJECT.md` — Current Milestone: v2.0 PAX8 Integration section
- `.planning/REQUIREMENTS.md` — PAX8-06, PAX8-10, PAX8-11 (this phase's
  requirement IDs); note PAX8-12/13/14 (manual resolution, `/pax8` UI) are
  Phase 14, not this phase
- `.planning/ROADMAP.md` — Phase 12 section (goal, 4 success criteria,
  depends on Phase 11)

### Prior phase foundation this phase builds on
- `migrations/091_pax8_tables.sql` — `pax8_orders` (header, Invoice-shaped
  per its own inline comment: total/status/currency mirror PAX8's Invoice
  object, sourced from PAX8's `/invoices` resource not `/orders` — confirm
  exact field mapping against the live API during research),
  `pax8_order_items` (line items, hard FK to `pax8_orders`, soft ref to
  `pax8_products`), and `pax8_company_match_review` (copied field-for-field
  from `device_link_review` — `candidate_company_ids BIGINT[]`,
  `match_confidences TEXT[]`, partial unique index enforcing one open
  review per PAX8 company). All three tables already exist — this phase
  populates them, does not alter schema unless research finds a genuine gap
  (e.g., no column yet exists on `pax8_companies` to record a confident
  auto-match — the planner must decide where a non-reviewed, auto-linked
  match gets stored)
- `.planning/phases/11-company-catalog-subscription-sync/11-SUMMARY.md` /
  `11-02-SUMMARY.md` — `Pax8SyncService.fullSync` shape, soft-delete
  tombstone pattern (`id <> ALL($1::uuid[])`), `Pax8EntitySyncResult`/
  `Pax8SyncResult` types this phase's new entity syncs should conform to
- `lib/services/pax8-client.ts` — read-only pagination helpers
  (`listAllCompanies`/`listAllSubscriptions`/`listAllProducts`); this phase
  needs an equivalent for invoices (`listAllInvoices` or similar) following
  the same `paginateAll<T>()` shape
- `lib/services/pax8-sync-service.ts` — existing `Pax8SyncService` class
  this phase extends (or a sibling matching service) with orders/invoices
  sync + company-matching logic

### Existing patterns to follow
- `lib/services/device-link-reconciler.ts` — direct precedent for the
  match/review/confidence pattern this phase implements for companies:
  cascading match strategies ranked by confidence, `LINK_CONFIDENCE_RANK`
  ordering, `device_link_review` candidate storage shape. D-04's "top 3"
  and D-02's tie-flagging should follow this file's structure/conventions
  closely.
- `migrations/080_device_xref_company_id.sql` — schema `device_link_review`
  was copied from; useful for understanding the review-row lifecycle
  (detected_at, resolved_at, resolved_by_user_id, resolved_to_company_id,
  resolution_note) that `pax8_company_match_review` mirrors
- Soft-delete / tombstone convention (`is_deleted`, `deleted_at`,
  `id <> ALL($1::uuid[])`) — established in Phase 11, applies to
  `pax8_orders`/`pax8_order_items` too

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- `lib/services/device-link-reconciler.ts` — confidence-ranked matching
  logic to adapt for company name fuzzy matching (different matching
  criteria — name similarity instead of serial/MAC/hostname — but same
  candidate-ranking and review-row-write shape)
- `lib/services/pax8-sync-service.ts` — existing tombstone/upsert
  primitives from Phase 11 to extend for orders/order_items
- `postgresClient` singleton — parameterized queries, no ORM

### Established Patterns
- Sync services live at `lib/services/<name>-sync-service.ts`; this phase
  likely extends the existing `pax8-sync-service.ts` rather than creating a
  new file, since it's the same integration's sync orchestration
- `*_match_review` tables follow a detect → flag → admin-resolves lifecycle;
  `resolved_at`/`resolved_to_company_id` gate re-scoring per D-05

### Integration Points
- Extends `lib/services/pax8-sync-service.ts` (or a new
  `pax8-matching-service.ts` — planner's call) with orders/invoices sync +
  company matching, invoked from the same `fullSync()` orchestration Phase
  11 built
- No route/nav/UI integration in this phase (`UI hint: no` per ROADMAP.md)
  — `pax8_company_match_review` rows are written here, consumed by Phase
  14's UI

</code_context>

<specifics>
## Specific Ideas

No specific UI or behavioral references beyond the discussed decisions.
The five numbered decisions above (D-01 through D-05) are the concrete
specifics: conservative high-confidence auto-match threshold (tune-able,
not a fixed permanent number), always-flag-on-any-tie even for exact
string matches, empty-candidate-list flagging for true no-match cases,
top-3 candidate cap for ambiguous cases, and continuous re-scoring of
unresolved (not yet human-confirmed) review rows on every sync.

</specifics>

<deferred>
## Deferred Ideas

Two gray areas were surfaced but not discussed this session (user chose to
focus on match confidence and no-match/ambiguous handling instead) — not
deferred to a future phase, just left as Claude's Discretion within this
phase (see above):
- Company name normalization nuances (legal suffixes, punctuation,
  abbreviations)
- Orders/invoices historical lookback window (full history vs. bounded)

No capabilities were deferred outside this phase's boundary — discussion
stayed within Phase 12's scope. (The `/pax8` UI, manual resolution flow, and
scheduler cron wiring are already sequenced into Phases 13-14 per
ROADMAP.md/REQUIREMENTS.md, not deferred from this discussion.)

</deferred>

---

*Phase: 12-orders-invoices-company-matching*
*Context gathered: 2026-07-11*
