/** * 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); });