---
phase: 12-orders-invoices-company-matching
plan: 02
type: execute
wave: 2
depends_on: ["12-01"]
files_modified:
  - lib/services/pax8-client.ts
  - lib/services/pax8-client.test.ts
  - scripts/verify-pax8-invoice-items.ts
autonomous: true
requirements: [PAX8-06]

must_haves:
  truths:
    - "Pax8Client can page through every invoice header via listAllInvoices()"
    - "Pax8Client can page through a single invoice's line items via listAllInvoiceItems(invoiceId)"
    - "Both new methods are GET-only (no mutating verb), preserving PAX8-08"
    - "The live invoice-item cost field mapping (unit_price/line_total/partner_cost/partner_cost_total) is confirmed against real prorate + one-time + subscription items"
  artifacts:
    - path: "lib/services/pax8-client.ts"
      provides: "listAllInvoices + listAllInvoiceItems read methods"
      contains: "listAllInvoiceItems"
    - path: "lib/services/pax8-client.test.ts"
      provides: "pagination + GET-only tests for the new methods"
      contains: "listAllInvoices"
    - path: "scripts/verify-pax8-invoice-items.ts"
      provides: "live field-mapping spot-check resolving RESEARCH Open Question 1"
  key_links:
    - from: "lib/services/pax8-client.ts"
      to: "paginateAll"
      via: "reuse existing private helper for both new methods"
      pattern: "paginateAll<Pax8Invoice"
---

<objective>
Add the two read-only PAX8 client methods this phase's invoice sync needs
(PAX8-06), and resolve the one open field-mapping question from research before
the sync service (Plan 04) writes any cost columns.

`/invoices` is a flat list (94 headers, full history since 2019). Invoice items
are a per-invoice child resource (`/invoices/{id}/items`) — there is no flat
items endpoint (12-RESEARCH.md Pitfall 3; `/orders` is unreliable, returns 504
— do NOT add it). Both methods reuse the existing generic `paginateAll<T>`
helper unchanged.

The spot-check script closes RESEARCH Open Question 1: confirm that the billed
line amount is `amountDue` (not `total`), and that partner cost maps from
`cost`/`costTotal`, across all three observed item types.

Purpose: Correct client surface + confirmed cost mapping before Plan 04 codes
the upsert SQL.
Output: two new client methods, their tests, and a verification script.
</objective>

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

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

<interfaces>
Existing pax8-client.ts (Plan 01 added Pax8Invoice/Pax8InvoiceItem to lib/types/pax8.ts):
- private paginateAll<T>((page, size) => Promise<Pax8PageEnvelope<T>>): Promise<T[]>
  — size=200, loops until page.number >= page.totalPages - 1; inherits fetchJson's
  429/Retry-After backoff. GET-only.
- Existing analog to copy: listAllProducts() (lines 117-122) returns
  paginateAll<Pax8Product>((page, size) => this.fetchJson<Pax8PageEnvelope<Pax8Product>>(`/products?page=${page}&size=${size}`)).
- Import Pax8Invoice, Pax8InvoiceItem from '@/lib/types/pax8'.

Existing test helpers (pax8-client.test.ts):
- makeMultiPageFetchMock({ resource, pages }) — matches url.includes(`/${resource}`),
  keys page body by `page=N`. Works for `/invoices` and for `/invoices/{id}/items`
  (both contain `/invoices`).
- SECRET constant + the GET-only / secret-never-leaked assertion patterns.

verify script precedent: scripts/verify-pax8-auth.ts — dotenv from ../.env.local,
getPax8Client(), prints ONLY a summary, never the token/secret. Run with npx tsx.
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add listAllInvoices() and listAllInvoiceItems(invoiceId) to Pax8Client</name>
  <files>lib/services/pax8-client.ts</files>
  <read_first>
    - lib/services/pax8-client.ts (paginateAll helper lines 86-101; listAllProducts lines 117-122 as the exact shape to copy; the import line 7 to extend)
    - lib/types/pax8.ts (Pax8Invoice / Pax8InvoiceItem added in Plan 01)
    - .planning/phases/12-orders-invoices-company-matching/12-PATTERNS.md (pax8-client.ts section — nested per-invoice method shape)
  </read_first>
  <action>
    Extend the type import on line 7 to include Pax8Invoice and Pax8InvoiceItem.

    Add listAllInvoices(): Promise&lt;Pax8Invoice[]&gt; — identical shape to
    listAllProducts but hitting `/invoices?page=${page}&size=${size}`, returning
    paginateAll&lt;Pax8Invoice&gt;. Add a doc comment marking it read-only / GET-only
    (PAX8-08), consistent with the other listAll* comments.

    Add listAllInvoiceItems(invoiceId: string): Promise&lt;Pax8InvoiceItem[]&gt; —
    paginateAll&lt;Pax8InvoiceItem&gt; hitting the nested child path
    `/invoices/${invoiceId}/items?page=${page}&size=${size}`. Same GET-only doc
    comment. Do NOT add any `/orders` method (12-RESEARCH.md Pitfall 3 — the
    endpoint is unreliable and not part of the design).

    paginateAll and fetchJson need no changes — both new methods are purely
    additive call sites.
  </action>
  <acceptance_criteria>
    - pax8-client.ts declares `async listAllInvoices(): Promise<Pax8Invoice[]>` and `async listAllInvoiceItems(invoiceId: string): Promise<Pax8InvoiceItem[]>`
    - listAllInvoiceItems interpolates the invoiceId into the path segment `/invoices/${invoiceId}/items`
    - `grep -c "/orders" lib/services/pax8-client.ts` returns 0
    - No `method:` other than the default GET appears in either new method
    - `npx tsc --noEmit --pretty` passes
  </acceptance_criteria>
  <verify>
    <automated>npx tsc --noEmit --pretty 2>&1 | tail -5; grep -c "/orders" lib/services/pax8-client.ts</automated>
  </verify>
  <done>Both read-only methods exist, reuse paginateAll, and type-check passes; no /orders call added.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Add pagination + GET-only tests for the new client methods</name>
  <files>lib/services/pax8-client.test.ts</files>
  <behavior>
    - listAllInvoices() concatenates content across all pages in order (multi-page mock, resource 'invoices'), and every request is size=200
    - listAllInvoiceItems(invoiceId) requests the nested path containing that invoiceId (e.g. `/invoices/inv-123/items`) and concatenates its pages
    - Every data request issued by both methods is a GET with an Authorization: Bearer header, none use a mutating method (extends the existing PAX8-08 assertion to the new methods)
  </behavior>
  <read_first>
    - lib/services/pax8-client.test.ts (makeMultiPageFetchMock lines 49-98; the listAllSubscriptions/listAllProducts test cases lines 165-223 to copy; the GET-only test lines 225-240)
  </read_first>
  <action>
    Add test cases mirroring the existing listAllSubscriptions/listAllProducts
    tests. For listAllInvoices use makeMultiPageFetchMock with resource: 'invoices'
    and assert the concatenated order plus size=200 on each call.

    For listAllInvoiceItems, the shared helper's url.includes('/invoices') already
    routes the nested path, but it does not assert the invoiceId segment — add an
    explicit assertion that at least one issued data-call URL contains
    `/invoices/inv-123/items` (use invoiceId 'inv-123'). If the shared helper's
    page-keying does not cleanly disambiguate the nested path, write a small
    bespoke fetch mock for this one case following makeMultiPageFetchMock's shape.

    Extend the existing GET-only / no-mutating-method assertion to also cover a
    listAllInvoiceItems call so PAX8-08 stays proven for the new surface.
  </action>
  <acceptance_criteria>
    - Test file contains cases titled to cover listAllInvoices and listAllInvoiceItems
    - The listAllInvoiceItems test asserts a data-call URL includes the literal substring `/invoices/inv-123/items`
    - `npx vitest run lib/services/pax8-client.test.ts` passes with the new cases included
  </acceptance_criteria>
  <verify>
    <automated>npx vitest run lib/services/pax8-client.test.ts</automated>
  </verify>
  <done>New tests pass, proving in-order pagination, the nested invoice-item path, and GET-only behavior for both methods.</done>
</task>

<task type="auto">
  <name>Task 3: Live field-mapping spot-check script (resolves RESEARCH Open Question 1)</name>
  <files>scripts/verify-pax8-invoice-items.ts</files>
  <read_first>
    - scripts/verify-pax8-auth.ts (the dotenv-from-.env.local + getPax8Client() + secret-safe logging pattern to copy)
    - .planning/phases/12-orders-invoices-company-matching/12-RESEARCH.md (Open Question 1 and Pitfall 2 — the exact fields and the hypothesized mapping to confirm)
    - lib/services/pax8-client.ts (the listAllInvoices / listAllInvoiceItems methods from Task 1)
  </read_first>
  <action>
    Create scripts/verify-pax8-invoice-items.ts (run via `npx tsx`), copying the
    dotenv + getPax8Client + secret-safe logging discipline from
    verify-pax8-auth.ts (never print the token or client secret — summary output
    only).

    The script fetches the first invoice via listAllInvoices(), then its items via
    listAllInvoiceItems(invoice.id). It locates one item of each observed type
    ('subscription', 'prorate', 'one-time') where present, and for each prints:
    type, quantity, price, subTotal, cost, costTotal, total, amountDue,
    companyId (non-null?), subscriptionId, startPeriod, endPeriod. It then prints
    a one-line CONFIRM/DIVERGENCE verdict for the mapping the sync service (Plan 04)
    will use: unit_price ← price, line_total ← amountDue, partner_cost ← cost,
    partner_cost_total ← costTotal — flagging any type where amountDue is not the
    plausible billed amount so Plan 04 can adjust before writing the upsert SQL.

    The script must run read-only (it only calls listAll* GET methods). Print a
    final summary line with the count of item types inspected.
  </action>
  <acceptance_criteria>
    - Running `npx tsx scripts/verify-pax8-invoice-items.ts` prints, for at least the 'subscription' type, the fields price/subTotal/cost/costTotal/total/amountDue and a CONFIRM or DIVERGENCE verdict line for the unit_price/line_total/partner_cost/partner_cost_total mapping
    - The script output never contains the client secret or access token
    - The script issues only GET calls (uses only listAllInvoices / listAllInvoiceItems)
    - The SUMMARY (12-02-SUMMARY.md) records the confirmed mapping (or any divergence) so Plan 04 can rely on it
  </acceptance_criteria>
  <verify>
    <automated>npx tsx scripts/verify-pax8-invoice-items.ts 2>&1 | tail -20</automated>
    Manual read of output: confirm the mapping verdict line and that no secret is printed.
  </verify>
  <done>Live field mapping confirmed (or divergence documented) for the invoice-item cost columns; result recorded in the SUMMARY for Plan 04 to consume.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| PAX8 API → Pulse client | External JSON responses (untrusted) parsed into typed objects |
| PAX8 credentials → logs/output | Client secret + bearer token must never surface in logs or script output |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-12-03 | Information Disclosure | verify-pax8-invoice-items.ts, new client error paths | mitigate | Copy verify-pax8-auth.ts's summary-only logging; error messages include HTTP status but never the secret (existing pax8-client.test.ts asserts `rejects.not.toThrow(new RegExp(SECRET))`) |
| T-12-05 | Elevation of Privilege | new client methods | mitigate | Both methods are GET-only via paginateAll; no mutating verb ever set (PAX8-08); covered by the extended GET-only test |
| T-12-SC | Tampering | package installs | accept | No new packages; methods reuse existing client internals |
</threat_model>

<verification>
- `npx vitest run lib/services/pax8-client.test.ts` green.
- `npx tsc --noEmit --pretty` passes.
- Spot-check script runs read-only and prints the mapping verdict with no secret leakage.
</verification>

<success_criteria>
listAllInvoices + listAllInvoiceItems exist, are GET-only, tested; the invoice-item cost field mapping is confirmed live and recorded for Plan 04.
</success_criteria>

<output>
Create `.planning/phases/12-orders-invoices-company-matching/12-02-SUMMARY.md` when done. Record the confirmed invoice-item field mapping (or any divergence) explicitly.
</output>
