Overdue Invoice Chaser
Module 21 · business · intermediate
normalize.js
// One clean invoice list, whatever the source: simple JSON from the API, raw Stripe invoices
// from the API (for testing the mapping), or the Stripe REST response (GET /v1/invoices).
const viaApi = $('Chase Run API').isExecuted;
const body = viaApi ? $('Chase Run API').first().json.body ?? {} : {};
const fromStripeHttp = $('Get Stripe Invoices').isExecuted;
const http = fromStripeHttp ? $('Get Stripe Invoices').first().json : null;
const fromStripe = (inv) => ({
invoice_id: inv.id,
number: inv.number ?? inv.id,
customer: inv.customer_name ?? inv.customer_email ?? 'customer',
email: (inv.customer_email ?? '').toLowerCase(),
amount_remaining: (inv.amount_remaining ?? inv.amount_due ?? 0) / 100, // Stripe amounts are in cents
currency: (inv.currency ?? 'usd').toLowerCase(),
due_date: inv.due_date ? new Date(inv.due_date * 1000).toISOString().slice(0, 10) : null,
status: inv.status,
pay_url: inv.hosted_invoice_url ?? '',
source: 'stripe',
});
const fromSimple = (inv) => ({
invoice_id: String(inv.id ?? inv.invoice_id ?? ''),
number: String(inv.number ?? inv.id ?? ''),
customer: inv.customer ?? inv.email ?? 'customer',
email: String(inv.email ?? '').trim().toLowerCase(),
amount_remaining: Number(inv.amount_remaining ?? inv.amount ?? 0),
currency: String(inv.currency ?? $('Settings (edit me)').first().json.currency).toLowerCase(),
due_date: inv.due_date ?? null,
status: inv.status ?? 'open',
pay_url: inv.pay_url ?? '',
source: 'api',
});
let invoices = [], source_error = null, has_more = false;
if (fromStripeHttp) {
if (Array.isArray(http.data)) { invoices = http.data.map(fromStripe); has_more = Boolean(http.has_more); }
else source_error = String(http.error?.message ?? http.error ?? 'Stripe returned no invoice list (check the Stripe credential)').slice(0, 300);
} else {
invoices = [...(body.invoices ?? []).map(fromSimple), ...(body.stripe_invoices ?? []).map(fromStripe)];
}
return [{
json: {
invoices: invoices.filter((i) => i.invoice_id),
source: fromStripeHttp ? 'stripe' : 'api',
source_error, has_more,
now: body.now || new Date().toISOString(), // tests inject the clock
dry_run: body.dry_run === true,
record_only: body.record_only === true, // mark reminders as sent without emailing (first-time import, or already chased by hand)
force_summary: body.summary === true,
via_api: viaApi,
},
}];