[object Object]

← back to Filemaker Mcp

fix: idempotency guard — skip invoice creation when FileMaker already has one for this Cust PO (order#)

65eb72c98eb59d80a6f14d66ec97f2b04448479e · 2026-07-14 10:28:35 -0700 · Steve Abrams

Prevents the systemic double-billing (e.g. #32609). Dedup was local-only (state.processed[]); any re-process minted a duplicate invoice. Now checks FileMaker truth by Cust PO before creating.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 65eb72c98eb59d80a6f14d66ec97f2b04448479e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 14 10:28:35 2026 -0700

    fix: idempotency guard — skip invoice creation when FileMaker already has one for this Cust PO (order#)
    
    Prevents the systemic double-billing (e.g. #32609). Dedup was local-only (state.processed[]); any re-process minted a duplicate invoice. Now checks FileMaker truth by Cust PO before creating.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 lib/invoice.js | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/lib/invoice.js b/lib/invoice.js
index 0127910..3731a6e 100644
--- a/lib/invoice.js
+++ b/lib/invoice.js
@@ -136,6 +136,28 @@ export async function createInvoiceForOrder(order, accountNumber, { commit = tru
       stubRecords: items.slice(1).map((li) => ({ sku: li.sku, qty: li.quantity || 1, dollars: 0 })) };
   }
 
+  // ── Idempotency guard (2026-07-14): never create a second invoice for an
+  // order FileMaker already has one for. Dedup was previously local-only
+  // (state.processed[] in shopify-sync.js), so any re-process — a state reset,
+  // manual keying, a second machine, or a run overlapping the daemon — minted a
+  // duplicate invoice (e.g. #32609 → #101633 + #102735). Check FileMaker truth
+  // by Cust PO (= unique Shopify order number) and return the existing invoice.
+  const existing = await fm
+    .findRecords('invoice', HDR, { 'Cust PO': '==' + order.order_number }, { limit: 10 })
+    .catch(() => ({ records: [] }));
+  const prior = (existing.records || [])
+    .map((r) => r.fieldData || {})
+    .filter((f) => /^\d+$/.test(String(f['Invoice'] || '').trim()))
+    .map((f) => parseInt(f['Invoice'], 10))
+    .sort((a, b) => a - b);
+  if (prior.length) {
+    return {
+      invoiceNumber: String(prior[0]), recordId: null, grandTotal: 0,
+      cust_po: order.order_number, account: accountNumber, invoiceCount: prior.length,
+      invoices: prior.map((n) => ({ invoiceNumber: String(n) })), skipped: 'already-invoiced',
+    };
+  }
+
   const sharedHeader = {
     'Cust PO': String(order.order_number), 'Account #': String(accountNumber || ''),
     'SOLD NAME': name, 'SOLD COMPANY': s.company || '', 'SOLD ADDRESS': s.address1 || '', 'SOLD CITY': s.city || '',

← aa0cddd fix: resolve George basic-auth from live fleet cred (was emp  ·  back to Filemaker Mcp  ·  Fix client double-entry + Company/Name field swap in Shopify de6eb81 →