[object Object]

← 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 →