docs(12): capture phase context

This commit is contained in:
lorentz 2026-07-10 21:35:33 -04:00
parent 5044c2de8a
commit 410652dedb
2 changed files with 282 additions and 0 deletions

View file

@ -0,0 +1,198 @@
# 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*

View file

@ -0,0 +1,84 @@
# Phase 12: Orders/Invoices & Company Matching - Discussion Log
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
**Date:** 2026-07-11
**Phase:** 12-orders-invoices-company-matching
**Areas discussed:** Match confidence threshold, No-match & ambiguous-candidate handling
---
## Match Confidence Threshold
| Option | Description | Selected |
|--------|-------------|----------|
| Exact name match only | Only auto-link on byte-identical names (case/whitespace-insensitive). Safest but likely means most companies land in review. | |
| High-confidence fuzzy match | Auto-link when similarity clears a high bar (e.g. ≥90-95%), covering near-identical names. | ✓ |
| You decide | Planner picks a threshold based on research. | |
**User's choice:** High-confidence fuzzy match
**Follow-up: threshold tuning approach**
| Option | Description | Selected |
|--------|-------------|----------|
| Set an initial number, tune later | Planner picks ~90-95% as a starting point, documented for later adjustment. | |
| Be conservative — fewer auto-matches | Bias toward flagging borderline cases; accept more manual review since this feeds cost/billing reconciliation. | ✓ |
| You decide | No strong preference. | |
**User's choice:** Be conservative — fewer auto-matches
**Notes:** User explicitly prioritized avoiding wrong auto-links over reducing review burden, since this data feeds cost/billing reconciliation.
**Follow-up: duplicate/tie edge case**
| Option | Description | Selected |
|--------|-------------|----------|
| Yes — always flag on any tie | Even an exact name match gets flagged if ambiguous against multiple same/similar-named Autotask companies. | ✓ |
| No — exact match always wins | Exact string equality is strong enough signal on its own, even with a second similarly-named company present. | |
**User's choice:** Yes — always flag on any tie
**Notes:** Reinforces the conservative stance — never silently pick between look-alike companies (e.g. franchise locations).
---
## No-Match & Ambiguous-Candidate Handling
| Option | Description | Selected |
|--------|-------------|----------|
| Flag with an empty candidate list | Still create a review row with an empty candidate array for true no-match cases, so it surfaces in the admin queue. | ✓ |
| You decide | Planner decides mechanics as long as PAX8-11 (never silently dropped) is satisfied. | |
**User's choice:** Flag with an empty candidate list
**Follow-up: ambiguous-candidate count**
| Option | Description | Selected |
|--------|-------------|----------|
| Top 3 | Cap review candidates at the 3 highest-scoring Autotask companies, matching `device_link_review` precedent. | ✓ |
| All above a "plausible" floor | Show every candidate clearing a low relevance floor, regardless of count. | |
| You decide | Planner picks a reasonable cap. | |
**User's choice:** Top 3
**Follow-up: re-scoring cadence for unresolved reviews**
| Option | Description | Selected |
|--------|-------------|----------|
| Re-score every sync until resolved | Keep re-evaluating unresolved PAX8 companies each full sync; resolved matches still never overwritten. | ✓ |
| Match once, don't re-touch until resolved | Leave flagged companies alone until an admin resolves them — simpler, stable candidate list between admin visits. | |
**User's choice:** Re-score every sync until resolved
**Notes:** Candidates can improve over time (e.g. a renamed/newly-created Autotask company scores better later) without requiring a manual re-trigger. Does not affect SC#4's idempotency guarantee for already-resolved matches.
---
## Claude's Discretion
- Exact numeric fuzzy-match threshold and matching library/approach (e.g. Postgres `pg_trgm` vs. a JS similarity library) — user wants "conservative," not a specific number; planner researches and documents the chosen value.
- Company name normalization nuances (legal suffixes like LLC/Inc/Corp, punctuation, abbreviations) — gray area was surfaced but not selected for discussion this session.
- Orders/invoices historical lookback window (full history vs. bounded window) — gray area was surfaced but not selected for discussion this session; `migrations/091`'s inline comment indicates full history was the original intent, to be confirmed during research.
## Deferred Ideas
None — discussion stayed within Phase 12's scope. No new capabilities were proposed; the two undiscussed gray areas above were left to Claude's discretion within this phase, not deferred to a future phase.