wulf-pulse/scripts/verify-pax8-invoice-items.ts

106 lines
3.9 KiB
TypeScript
Raw Normal View History

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