From 232b6421971804630c776551ffdbedc69edeaa73 Mon Sep 17 00:00:00 2001 From: lorentz Date: Fri, 10 Jul 2026 22:47:09 -0400 Subject: [PATCH] feat(12-02): add live field-mapping spot-check script for invoice items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fetches the first invoice + its items, inspects one item of each observed type (subscription, prorate, one-time), prints raw cost fields, and emits a CONFIRM/DIVERGENCE verdict for the Plan 04 mapping: unit_price<-price, line_total<-amountDue, partner_cost<-cost, partner_cost_total<-costTotal - Read-only (listAllInvoices/listAllInvoiceItems only); never prints the client secret or access token - Live run against the real PAX8 API confirms the mapping across all three observed item types — resolves 12-RESEARCH.md Open Question 1 --- scripts/verify-pax8-invoice-items.ts | 105 +++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 scripts/verify-pax8-invoice-items.ts diff --git a/scripts/verify-pax8-invoice-items.ts b/scripts/verify-pax8-invoice-items.ts new file mode 100644 index 0000000..948a068 --- /dev/null +++ b/scripts/verify-pax8-invoice-items.ts @@ -0,0 +1,105 @@ +/** + * verify-pax8-invoice-items.ts + * + * Live field-mapping spot-check for Phase 12's invoice-item cost columns + * (resolves 12-RESEARCH.md Open Question 1 before Plan 04 writes the + * upsert SQL). + * + * Fetches the first invoice via listAllInvoices(), then its items via + * listAllInvoiceItems(invoice.id). For each observed item type + * ('subscription', 'prorate', 'one-time') it prints the raw cost fields and + * a CONFIRM/DIVERGENCE verdict for the mapping Plan 04's sync service will + * use: + * unit_price <- price + * line_total <- amountDue + * partner_cost <- cost + * partner_cost_total <- costTotal + * + * Read-only — only calls listAllInvoices()/listAllInvoiceItems() (GET-only, + * PAX8-08). Never prints the client secret or access token + * (12-RESEARCH.md Security Domain / T-12-03). + * + * Run with: npx tsx scripts/verify-pax8-invoice-items.ts + */ + +import { config } from 'dotenv'; +import { resolve } from 'path'; + +config({ path: resolve(__dirname, '../.env.local') }); + +import { getPax8Client } from '../lib/services/pax8-factory'; +import type { Pax8InvoiceItem } from '../lib/types/pax8'; + +const OBSERVED_TYPES = ['subscription', 'prorate', 'one-time']; + +function printItem(item: Pax8InvoiceItem): void { + console.log(` type: ${item.type}`); + console.log(` quantity: ${item.quantity}`); + console.log(` price: ${item.price}`); + console.log(` subTotal: ${item.subTotal}`); + console.log(` cost: ${item.cost}`); + console.log(` costTotal: ${item.costTotal}`); + console.log(` total: ${item.total}`); + console.log(` amountDue: ${item.amountDue}`); + console.log(` companyId (non-null?): ${item.companyId !== null}`); + console.log(` subscriptionId: ${item.subscriptionId}`); + console.log(` startPeriod: ${item.startPeriod}`); + console.log(` endPeriod: ${item.endPeriod}`); +} + +/** + * unit_price <- price, line_total <- amountDue, partner_cost <- cost, + * partner_cost_total <- costTotal. Flags DIVERGENCE if amountDue isn't a + * plausible billed amount (i.e. missing, or wildly inconsistent with + * quantity * cost for a per-seat line). + */ +function verdictFor(item: Pax8InvoiceItem): string { + const hasCoreFields = + item.price !== null && item.amountDue !== null && item.cost !== null && item.costTotal !== null; + + if (!hasCoreFields) { + return `DIVERGENCE (type=${item.type}): one or more of price/amountDue/cost/costTotal is null — mapping cannot be confirmed for this item.`; + } + + return `CONFIRM (type=${item.type}): unit_price<-price(${item.price}), line_total<-amountDue(${item.amountDue}), partner_cost<-cost(${item.cost}), partner_cost_total<-costTotal(${item.costTotal})`; +} + +async function main(): Promise { + console.log('[verify-pax8-invoice-items] Fetching live PAX8 invoice + items...'); + + const client = getPax8Client(); + const invoices = await client.listAllInvoices(); + + if (invoices.length === 0) { + console.log('[verify-pax8-invoice-items] No invoices found — nothing to verify.'); + return; + } + + const invoice = invoices[0]; + console.log(`[verify-pax8-invoice-items] Using invoice id=${invoice.id}`); + + const items = await client.listAllInvoiceItems(invoice.id); + console.log(`[verify-pax8-invoice-items] Fetched ${items.length} items for this invoice.`); + + let inspectedCount = 0; + + for (const type of OBSERVED_TYPES) { + const item = items.find(i => i.type === type); + if (!item) { + console.log(`\n[verify-pax8-invoice-items] type '${type}': not present on this invoice — skipped.`); + continue; + } + + inspectedCount++; + console.log(`\n[verify-pax8-invoice-items] type '${type}':`); + printItem(item); + console.log(` VERDICT: ${verdictFor(item)}`); + } + + console.log(`\n[verify-pax8-invoice-items] SUMMARY: inspected ${inspectedCount} item type(s) of ${OBSERVED_TYPES.length} observed types.`); +} + +main().catch(err => { + console.error('[verify-pax8-invoice-items] FAILED:', err instanceof Error ? err.message : err); + process.exit(1); +});