---
phase: 12-orders-invoices-company-matching
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - migrations/093_pax8_orders_company_matching.sql
  - lib/types/pax8.ts
autonomous: true
requirements: [PAX8-06, PAX8-10, PAX8-11]

must_haves:
  truths:
    - "pg_trgm extension is enabled in the dev Postgres database"
    - "pax8_order_items has per-company id, billing period, and dual-cost columns"
    - "pax8_companies has auto-match columns (autotask_company_id, match_confidence, match_method, matched_at)"
    - "TypeScript exposes Pax8Invoice and Pax8InvoiceItem types matching the live PAX8 /invoices field shape"
  artifacts:
    - path: "migrations/093_pax8_orders_company_matching.sql"
      provides: "pg_trgm + additive columns on pax8_order_items and pax8_companies"
      contains: "CREATE EXTENSION IF NOT EXISTS pg_trgm"
    - path: "lib/types/pax8.ts"
      provides: "Pax8Invoice / Pax8InvoiceItem API types"
      contains: "Pax8InvoiceItem"
  key_links:
    - from: "migrations/093_pax8_orders_company_matching.sql"
      to: "pax8_order_items"
      via: "ALTER TABLE ADD COLUMN IF NOT EXISTS"
      pattern: "ALTER TABLE pax8_order_items"
    - from: "migrations/093_pax8_orders_company_matching.sql"
      to: "pax8_companies"
      via: "ALTER TABLE ADD COLUMN IF NOT EXISTS"
      pattern: "ALTER TABLE pax8_companies"
---

<objective>
Lay the schema + type foundation for both halves of Phase 12: historical
invoice/line-item cost storage (PAX8-06) and fuzzy company matching
(PAX8-10, PAX8-11).

Live PAX8 verification (12-RESEARCH.md) proved two schema gaps in the
already-committed migration 091: `pax8_order_items` has no per-company id and
no billing-period columns (invoice headers carry no per-customer data —
`companyId` is always NULL on the header), and `pax8_companies` has nowhere to
store a confident auto-match. This plan closes both gaps with a new additive
migration and enables the `pg_trgm` extension the matcher needs.

Purpose: Everything downstream (client methods, matcher, sync wiring) depends
on these columns and types existing first.
Output: migrations/093_pax8_orders_company_matching.sql applied to the dev DB;
Pax8Invoice / Pax8InvoiceItem types in lib/types/pax8.ts.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/12-orders-invoices-company-matching/12-CONTEXT.md
@.planning/phases/12-orders-invoices-company-matching/12-RESEARCH.md
@.planning/phases/12-orders-invoices-company-matching/12-PATTERNS.md

<interfaces>
Existing schema this migration extends (migrations/091_pax8_tables.sql):
- pax8_order_items already has: id UUID PK, order_id UUID NOT NULL REFERENCES
  pax8_orders(id) ON DELETE CASCADE, product_id UUID, quantity INTEGER,
  unit_price NUMERIC(12,2), line_total NUMERIC(12,2), currency CHAR(3),
  raw_payload JSONB, synced_at, is_deleted, deleted_at.
- pax8_companies already has: id UUID PK, name TEXT NOT NULL, external_id,
  website, status, city, state_or_province, postal_code, country,
  raw_payload, synced_at, is_deleted, deleted_at.
- companies (Autotask, migrations/001): id BIGINT PK, company_name
  VARCHAR(255), is_active BOOLEAN.

Extension-enable precedent (migrations/069, 070): `CREATE EXTENSION IF NOT
EXISTS pgcrypto;`

Existing stale stubs to replace in lib/types/pax8.ts (currently unused — no
file imports Pax8Order/Pax8OrderItem): Pax8Order (lines 65-75),
Pax8OrderItem (lines 79-88). Keep Pax8EntitySyncResult / Pax8SyncResult
unchanged.
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Write migration 093 (pg_trgm + additive columns) and apply to dev DB</name>
  <files>migrations/093_pax8_orders_company_matching.sql</files>
  <read_first>
    - migrations/091_pax8_tables.sql (current pax8_order_items and pax8_companies definitions this migration ALTERs — confirm which columns already exist so ADD COLUMN IF NOT EXISTS is correct)
    - migrations/069_create_analyzer_tables.sql (CREATE EXTENSION IF NOT EXISTS pgcrypto precedent — copy this exact statement shape for pg_trgm)
    - .planning/phases/12-orders-invoices-company-matching/12-RESEARCH.md (Code Examples > migration skeleton; Pitfall 1 and Pitfall 2 explain why each column exists)
    - scripts/apply-migrations.sh (how migrations are applied to the running container; DB_USER default pulse_user, DB_NAME default pulse_autotask)
  </read_first>
  <action>
    Create migrations/093_pax8_orders_company_matching.sql, additive-only, using
    CREATE EXTENSION IF NOT EXISTS and ADD COLUMN IF NOT EXISTS / CREATE INDEX IF
    NOT EXISTS throughout (never edit committed migration 091). Include a header
    comment stating this is Phase 12, that it enables pg_trgm and closes the
    per-company/period gap on pax8_order_items (invoice headers carry no
    per-customer data — see 12-RESEARCH.md Pitfall 1), and that
    pax8_orders.pax8_company_id will intentionally stay NULL for every real row.

    Statement 1: `CREATE EXTENSION IF NOT EXISTS pg_trgm;`

    Statement 2: ALTER TABLE pax8_order_items ADD COLUMN IF NOT EXISTS these nine
    columns: pax8_company_id UUID (soft ref to pax8_companies(id), no hard FK —
    matches the soft-ref convention on pax8_subscriptions.pax8_company_id),
    subscription_id UUID (soft ref to pax8_subscriptions(id)), item_type TEXT
    (free-form — 'subscription'/'prorate'/'one-time' and possibly others; NO
    CHECK constraint, matching pax8_subscriptions.status), sku TEXT, description
    TEXT, start_period TIMESTAMPTZ, end_period TIMESTAMPTZ, partner_cost
    NUMERIC(12,2), partner_cost_total NUMERIC(12,2).

    Statement 3: CREATE INDEX IF NOT EXISTS idx_pax8_order_items_company ON
    pax8_order_items(pax8_company_id); and idx_pax8_order_items_period ON
    pax8_order_items(start_period, end_period).

    Statement 4: ALTER TABLE pax8_companies ADD COLUMN IF NOT EXISTS
    autotask_company_id BIGINT (soft ref to companies(id), no hard FK),
    match_confidence NUMERIC(4,3) (raw pg_trgm score 0.000-1.000), match_method
    TEXT ('pg_trgm' | 'manual'), matched_at TIMESTAMPTZ.

    Statement 5: CREATE INDEX IF NOT EXISTS idx_pax8_companies_autotask ON
    pax8_companies(autotask_company_id).

    Do NOT touch pax8_orders, pax8_order_items existing columns, or
    pax8_company_match_review — they are already correctly shaped by migration 091.

    Then apply to the running dev database (existing volume — Postgres init does
    not re-run migrations per CLAUDE.md): run
    `docker exec -i pulse-postgres psql -U pulse_user -d pulse_autotask < migrations/093_pax8_orders_company_matching.sql`.
    If the container name or credentials differ, read scripts/apply-migrations.sh
    and adapt.
  </action>
  <acceptance_criteria>
    - File migrations/093_pax8_orders_company_matching.sql exists and contains the literal string `CREATE EXTENSION IF NOT EXISTS pg_trgm`
    - `grep -c "ADD COLUMN IF NOT EXISTS" migrations/093_pax8_orders_company_matching.sql` returns 13 (9 on pax8_order_items + 4 on pax8_companies)
    - After apply, `docker exec pulse-postgres psql -U pulse_user -d pulse_autotask -tAc "SELECT 1 FROM pg_extension WHERE extname='pg_trgm'"` prints `1`
    - After apply, `docker exec pulse-postgres psql -U pulse_user -d pulse_autotask -tAc "SELECT count(*) FROM information_schema.columns WHERE table_name='pax8_order_items' AND column_name IN ('pax8_company_id','subscription_id','item_type','sku','description','start_period','end_period','partner_cost','partner_cost_total')"` prints `9`
    - After apply, the same query for table_name='pax8_companies' and column_name IN ('autotask_company_id','match_confidence','match_method','matched_at') prints `4`
    - No ALTER or DROP against pax8_orders / pax8_company_match_review appears in the file
  </acceptance_criteria>
  <verify>
    <automated>docker exec pulse-postgres psql -U pulse_user -d pulse_autotask -tAc "SELECT (SELECT count(*) FROM pg_extension WHERE extname='pg_trgm') || '/' || (SELECT count(*) FROM information_schema.columns WHERE table_name='pax8_order_items' AND column_name IN ('pax8_company_id','subscription_id','item_type','sku','description','start_period','end_period','partner_cost','partner_cost_total')) || '/' || (SELECT count(*) FROM information_schema.columns WHERE table_name='pax8_companies' AND column_name IN ('autotask_company_id','match_confidence','match_method','matched_at'))"</automated>
    Expected output: 1/9/4
  </verify>
  <done>pg_trgm enabled; all 13 additive columns and 3 indexes present in the dev DB; migration is additive-only and does not edit migration 091.</done>
</task>

<task type="auto">
  <name>Task 2: Add Pax8Invoice / Pax8InvoiceItem types to lib/types/pax8.ts</name>
  <files>lib/types/pax8.ts</files>
  <read_first>
    - lib/types/pax8.ts (current Pax8Order/Pax8OrderItem stubs at lines 65-88 to replace; Pax8Company/Pax8Subscription convention with the `[key: string]: unknown` escape hatch to mirror)
    - .planning/phases/12-orders-invoices-company-matching/12-RESEARCH.md (Pitfall 2 — the live-verified invoice item JSON shape is the source of truth for field names/types)
  </read_first>
  <action>
    Replace the two stale speculative stubs Pax8Order and Pax8OrderItem (currently
    unused — grep confirms no importers) with live-verified types named
    Pax8Invoice and Pax8InvoiceItem (favor these names per 12-RESEARCH.md
    Recommended Project Structure, matching the client methods listAllInvoices /
    listAllInvoiceItems added in Plan 02). Keep the `[key: string]: unknown`
    escape hatch on both.

    Pax8Invoice (header — the partner's consolidated monthly bill): id: string;
    companyId: string | null (add a comment: always null on the header for this
    single-tenant reseller account — per-company data lives on items); invoiceDate:
    string | null; total: number | null; status: string | null; currencyCode:
    string | null; plus the escape hatch.

    Pax8InvoiceItem (line item — per-company, per-period cost): id: string; type:
    string | null; externalId: string | null; companyId: string | null;
    forCompanyId: string | null; companyName: string | null; startPeriod: string |
    null; endPeriod: string | null; quantity: number | null; unitOfMeasure: string
    | null; term: string | null; sku: string | null; description: string | null;
    rateType: string | null; chargeType: string | null; price: number | null;
    subTotal: number | null; cost: number | null; costTotal: number | null; total:
    number | null; amountDue: number | null; productId: string | null;
    productName: string | null; vendorName: string | null; currencyCode: string |
    null; subscriptionId: string | null; plus the escape hatch.

    Do not change Pax8Company, Pax8Subscription, Pax8Product, Pax8PageEnvelope,
    Pax8EntitySyncResult, or Pax8SyncResult.
  </action>
  <acceptance_criteria>
    - lib/types/pax8.ts exports interfaces named exactly `Pax8Invoice` and `Pax8InvoiceItem`
    - `grep -c "Pax8Order" lib/types/pax8.ts` returns 0 (stale stubs removed)
    - Pax8InvoiceItem declares fields `amountDue`, `cost`, `costTotal`, `subscriptionId`, `startPeriod`, `endPeriod`, `companyId`, `type`
    - `npx tsc --noEmit --pretty` passes with no new errors
  </acceptance_criteria>
  <verify>
    <automated>npx tsc --noEmit --pretty 2>&1 | tail -5; grep -c "Pax8Order" lib/types/pax8.ts</automated>
  </verify>
  <done>Pax8Invoice and Pax8InvoiceItem exist with the live-verified field set; no Pax8Order/Pax8OrderItem references remain; type-check passes.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| migration file → dev Postgres | DDL applied to a live database with existing PAX8 data |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-12-01 | Tampering | migration 093 DDL | mitigate | Additive-only (ADD COLUMN IF NOT EXISTS, CREATE INDEX IF NOT EXISTS, CREATE EXTENSION IF NOT EXISTS); no DROP/ALTER of existing columns; re-runnable without data loss |
| T-12-SC | Tampering | package installs | accept | No new npm/pip/cargo packages this phase; pg_trgm is a Postgres 16 contrib extension verified live against the project's own container (12-RESEARCH.md) — not a registry package |
</threat_model>

<verification>
- Migration is idempotent: re-applying it against the dev DB produces no error and no duplicate columns.
- `npx tsc --noEmit --pretty` passes.
</verification>

<success_criteria>
pg_trgm is enabled; pax8_order_items and pax8_companies carry the new columns; Pax8Invoice/Pax8InvoiceItem types match the live PAX8 field shape; type-check green.
</success_criteria>

<output>
Create `.planning/phases/12-orders-invoices-company-matching/12-01-SUMMARY.md` when done.
</output>
