← back to Filemaker Mcp
invoice(B): real SKU on every line (not 'Samples') + atomic line-write so no empty-shell invoices
0a0ebbacaa8a530a2cc08709c4bd2e7ff05c1701 · 2026-08-04 11:05:28 -0700 · Steve
- moneyLine: always headline the real SKU on the money line; money still rides the
first record only, every other SKU = $0 stub (Steve 2026-08-04, choice B).
- create loop: write DETAIL/Q/Unit/NET/RETAIL as an immediate post-create step and
DELETE the record if that write fails, so an interrupted/racing run can never leave
a blank invoice shell (the 103152/103153/103154 defect).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit 0a0ebbacaa8a530a2cc08709c4bd2e7ff05c1701
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Aug 4 11:05:28 2026 -0700
invoice(B): real SKU on every line (not 'Samples') + atomic line-write so no empty-shell invoices
- moneyLine: always headline the real SKU on the money line; money still rides the
first record only, every other SKU = $0 stub (Steve 2026-08-04, choice B).
- create loop: write DETAIL/Q/Unit/NET/RETAIL as an immediate post-create step and
DELETE the record if that write fails, so an interrupted/racing run can never leave
a blank invoice shell (the 103152/103153/103154 defect).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
lib/invoice.js | 56 ++++++++++++++++++++++++++++++--------------------------
1 file changed, 30 insertions(+), 26 deletions(-)
diff --git a/lib/invoice.js b/lib/invoice.js
index 3731a6e..407b7cd 100644
--- a/lib/invoice.js
+++ b/lib/invoice.js
@@ -54,8 +54,11 @@ const isSample = (li) => /sample|memo|swatch/i.test(`${li.variant_title||''} ${l
// the SKU), so all the money stays on the first invoice.
function moneyLine(items, merch) {
const totalQty = items.reduce((a, li) => a + (li.quantity || 1), 0) || 1;
- const allSamples = items.every(isSample);
- const detail = allSamples ? 'Samples' : `${baseSku(items[0].sku)} — ${items[0].name || ''}`.trim().slice(0, 250);
+ // B (Steve 2026-08-04): ALWAYS headline the real SKU on the money line — never the
+ // generic "Samples" — so every invoice shows its own SKU. Money still rides this first
+ // record only (the rest are $0 SKU stubs). Falls back to bare SKU, then "Samples",
+ // only if there is no SKU/name at all (never leaves the line blank).
+ const detail = (`${baseSku(items[0].sku)} — ${items[0].name || ''}`.trim() || baseSku(items[0].sku) || 'Samples').slice(0, 250);
return { detail, qty: totalQty, unit: uom(items[0]), price: r2(merch / totalQty) };
}
function resale(s) {
@@ -174,13 +177,34 @@ export async function createInvoiceForOrder(order, accountNumber, { commit = tru
const li = items[i];
const isFirst = i === 0;
- // allocate a free invoice number + create the header record
+ // This record's ONE line, computed BEFORE create. First record = the money line (all
+ // the order's dollars); every other record = its own SKU at $0. Both ALWAYS carry the
+ // real SKU in DETAIL (B, Steve 2026-08-04).
+ const line = isFirst
+ ? moneyLine(items, merch)
+ : { detail: (`${baseSku(li.sku)} — ${li.name || ''}`.trim() || baseSku(li.sku) || 'Samples').slice(0, 250),
+ qty: li.quantity || 1, unit: uom(li), price: 0 };
+ const lineFields = {
+ 'DETAIL 1(1)': line.detail, 'Q1(1)': line.qty, 'Unit 1(1)': line.unit,
+ 'NET 1(1)': line.price, 'RETAIL 1(1)': line.price, 'Sidemark 1(1)': s.company || '',
+ };
+
+ // allocate a free invoice number + create the header record, then IMMEDIATELY write
+ // the line. Empty-shell guard (Steve 2026-08-04): if the line write fails, DELETE the
+ // just-created record so an interrupted/failed run can never leave a blank invoice,
+ // and let the loop retry a fresh number.
let id = null, createdInv = null;
for (let tries = 0; tries < 8 && !id; tries++, nextNum++) {
const dup = await fm.findRecords('invoice', HDR, { 'Invoice': '==' + nextNum }, { limit: 1 }).catch(() => ({ records: [] }));
if (dup.records.length) continue;
const res = await fm.createRecord('invoice', HDR, { 'Invoice': String(nextNum), ...sharedHeader }, { dryRun: false }).catch(() => null);
- if (res) { id = res.recordId; createdInv = nextNum; } // for-loop's single nextNum++ then advances to the next free number (no gaps)
+ if (!res) continue;
+ try {
+ await fm.updateRecord('invoice', HDR, res.recordId, lineFields, { dryRun: false });
+ id = res.recordId; createdInv = nextNum; // line committed — the record is complete
+ } catch {
+ await fm.deleteRecord('invoice', HDR, res.recordId).catch(() => {}); // no empty shell survives
+ }
}
if (!id) throw new Error('could not allocate an invoice number');
@@ -194,28 +218,8 @@ export async function createInvoiceForOrder(order, accountNumber, { commit = tru
await patch(id, 'SHIP VIA', isFirst ? shipVia : '');
await patch(id, 'SHIPPING HANDLING', isFirst ? shipping : 0);
await patch(id, 'SALES TAX YN', (isFirst && orderTax > 0) ? 'Y' : 'N');
-
- // The FIRST record carries ALL the order's money on ONE line: total units ×
- // per-unit price (GRAND TOTAL = the whole merch subtotal), with real units +
- // a real per-unit price. Every OTHER record shows JUST its SKU at $0, so all
- // the dollars stay on the first invoice and each pattern still gets a record.
- if (isFirst) {
- const L = moneyLine(items, merch);
- await patch(id, 'DETAIL 1(1)', L.detail);
- await patch(id, 'Q1(1)', L.qty);
- await patch(id, 'Unit 1(1)', L.unit);
- await patch(id, 'RETAIL 1(1)', L.price);
- await patch(id, 'NET 1(1)', L.price);
- await patch(id, 'Sidemark 1(1)', s.company || '');
- } else {
- const desc = `${baseSku(li.sku)} — ${li.name || ''}`.trim();
- await patch(id, 'DETAIL 1(1)', desc.slice(0, 250));
- await patch(id, 'Q1(1)', li.quantity || 1);
- await patch(id, 'Unit 1(1)', uom(li));
- await patch(id, 'RETAIL 1(1)', 0);
- await patch(id, 'NET 1(1)', 0);
- await patch(id, 'Sidemark 1(1)', s.company || '');
- }
+ // (DETAIL/Q/Unit/NET/RETAIL/Sidemark for this line were written atomically at
+ // create above — first record = money line, every other = its SKU at $0.)
// cross-table SKU fields for THIS sku (FileMaker pattern lookup keys off these)
try { await fm.updateRecord('invoice', SKU_LAYOUT, id, { 'JS Easy SKU #': baseSku(li.sku) }, { dryRun: false }); } catch {}
← ccd79b3 chore: bump v0.2.6 (session close — TK-10083 fail-closed par
·
back to Filemaker Mcp
·
test: smoke-invoice-b — dry-run regression for SKU-on-line + 35cf4d3 →