[object Object]

← back to Filemaker Mcp

fix: consolidate multi-item orders to single line (exact total), round cents, WC PATTERN=base pattern, delete API, Slack→#new-order, error-resilient runner; recovered 32476 ($34)

7525e58d4580eb3f0ab2a8367c83037328f2d01d · 2026-06-29 12:50:45 -0700 · Steve

Files touched

Diff

commit 7525e58d4580eb3f0ab2a8367c83037328f2d01d
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jun 29 12:50:45 2026 -0700

    fix: consolidate multi-item orders to single line (exact total), round cents, WC PATTERN=base pattern, delete API, Slack→#new-order, error-resilient runner; recovered 32476 ($34)
---
 bin/shopify-sync.js | 10 +++++++++-
 lib/invoice.js      | 44 +++++++++++++++++++-------------------------
 2 files changed, 28 insertions(+), 26 deletions(-)

diff --git a/bin/shopify-sync.js b/bin/shopify-sync.js
index 047c2b0..e78fa80 100644
--- a/bin/shopify-sync.js
+++ b/bin/shopify-sync.js
@@ -86,7 +86,15 @@ async function main() {
       state.processed = [...new Set([...(state.processed || []), num])].slice(-500);
       state.maxProcessed = Math.max(state.maxProcessed || 0, num);
       saveState(state);
-    } catch (e) { log(`#${num} ERROR:`, e.message); }
+    } catch (e) {
+      // record the failure and mark the order seen so it neither retries forever
+      // nor silently disappears — surfaces in the ledger for manual reprocessing
+      log(`#${num} ERROR:`, e.message);
+      appendFileSync(LEDGER, JSON.stringify({ ts: new Date().toISOString(), orderNumber: num, action: 'error', error: e.message }) + '\n');
+      state.processed = [...new Set([...(state.processed || []), num])].slice(-500);
+      state.maxProcessed = Math.max(state.maxProcessed || 0, num);
+      saveState(state);
+    }
   }
   await reportProcessed(results);
 }
diff --git a/lib/invoice.js b/lib/invoice.js
index 60a678d..fa9ea6f 100644
--- a/lib/invoice.js
+++ b/lib/invoice.js
@@ -92,31 +92,25 @@ export async function createInvoiceForOrder(order, accountNumber, { commit = tru
   await patch(id, 'SALES TAX YN', orderTax > 0 ? 'Y' : 'N');
   await patch(id, 'check Number', today());
 
-  // line items -> repetitions 1..MAX_LINES. If more line items than slots,
-  // write the first MAX_LINES-1 individually and CONSOLIDATE the rest into the
-  // last slot (qty 1 × summed $) so the merchandise total stays exact.
-  const individual = items.length <= MAX_LINES ? items.length : MAX_LINES - 1;
-  for (let i = 0; i < individual; i++) {
-    const li = items[i], r = i + 1, price = r2(li.price);
-    await patch(id, `DETAIL 1(${r})`, `${li.sku || ''} — ${li.name || ''}`.trim());
-    await patch(id, `Q1(${r})`, li.quantity);
-    await patch(id, `Unit 1(${r})`, uom(li));
-    await patch(id, `RETAIL 1(${r})`, price);
-    await patch(id, `NET 1(${r})`, price);
-    await patch(id, `Sidemark 1(${r})`, s.company || '');
-  }
-  if (items.length > MAX_LINES) {
-    const rest = items.slice(individual);
-    const restTotal = r2(rest.reduce((a, li) => a + (Number(li.price)||0) * (li.quantity||1), 0));
-    const skus = rest.map((li) => baseSku(li.sku)).join(', ');
-    const r = MAX_LINES;
-    await patch(id, `DETAIL 1(${r})`, `+${rest.length} more items: ${skus}`.slice(0, 250));
-    await patch(id, `Q1(${r})`, 1);
-    await patch(id, `Unit 1(${r})`, 'lot');
-    await patch(id, `RETAIL 1(${r})`, restTotal);
-    await patch(id, `NET 1(${r})`, restTotal);
-    await patch(id, `Sidemark 1(${r})`, s.company || '');
-  }
+  // Consolidate to ONE billable line (line rep 1) — the legacy invoice's
+  // MERCHANDISE TOTAL reliably reflects only rep 1, so a single line keeps the
+  // total exact. Same unit price -> Qty = total units × that price; mixed
+  // prices -> Qty 1 × merchandise subtotal. SKUs listed in the description.
+  const merch = r2(items.reduce((a, li) => a + (Number(li.price) || 0) * (li.quantity || 1), 0));
+  const totalQty = items.reduce((a, li) => a + (li.quantity || 1), 0);
+  const skus = items.map((li) => baseSku(li.sku));
+  const samePrice = new Set(items.map((li) => r2(li.price))).size === 1;
+  const qty = samePrice ? totalQty : 1;
+  const unitPrice = samePrice ? r2(items[0].price) : merch;
+  const desc = items.length === 1
+    ? `${skus[0]} — ${items[0].name || ''}`.trim()
+    : `${items.length} items: ${skus.join(', ')}`;
+  await patch(id, 'DETAIL 1(1)', desc.slice(0, 250));
+  await patch(id, 'Q1(1)', qty);
+  await patch(id, 'Unit 1(1)', uom(items[0] || {}));
+  await patch(id, 'RETAIL 1(1)', unitPrice);
+  await patch(id, 'NET 1(1)', unitPrice);
+  await patch(id, 'Sidemark 1(1)', s.company || '');
 
   // cross-table fields (different base-table layouts; share the record id)
   try { await fm.updateRecord('invoice', SKU_LAYOUT, id, { 'JS Easy SKU #': baseSku(primarySku) }, { dryRun: false }); } catch {}

← 1894d57 auto-save: 2026-06-29T12:49:09 (2 files) — lib/invoice.js sr  ·  back to Filemaker Mcp  ·  invoice: populate 'WC Pattern Number AKA DW SKU' from primar 707cde7 →