← back to Sample Followup Sweep
TK-11409: the 2nd request now updates the WALLPAPER file
f69c9f78a00e45f405a08d757572f4657ffb940d · 2026-09-10 10:32:04 -0700 · Steve Abrams
The resend called the stamper but wrote nothing, silently. matchPageSku hard-coded
[FIELD_SENT]: '=' (FileMaker '=' matches an EMPTY field) on every send, so on a resend
every SKU was already stamped from request #1, the find returned zero records, and
nothing was written. stampFmpro spawns detached with stdio:'ignore', so the no-op was
indistinguishable from success.
- matchPageSku/planVendor/stampVendor take a (default 1, so existing callers
are unchanged). pass 2 finds rows where FIELD_SENT is non-empty ('*') and the
2nd-request field is still empty.
- pass 1 now writes ONLY 'Date Email Sent to Vendor after 10 Days'. It used to write
the 2nd-request field at the same time, which left request #2 nowhere to record.
- pass 2 writes ONLY 'Date Sample Request Letter Sent', preserving the 1st-chase date.
- server.js derives isResend from sent.json (the record of fact) rather than b.force,
so a forced genuine first send stays pass 1.
Not yet exercised against live FileMaker — no records written.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Files touched
M scripts/fmpro.mjsM server.js
Diff
commit f69c9f78a00e45f405a08d757572f4657ffb940d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 10 10:32:04 2026 -0700
TK-11409: the 2nd request now updates the WALLPAPER file
The resend called the stamper but wrote nothing, silently. matchPageSku hard-coded
[FIELD_SENT]: '=' (FileMaker '=' matches an EMPTY field) on every send, so on a resend
every SKU was already stamped from request #1, the find returned zero records, and
nothing was written. stampFmpro spawns detached with stdio:'ignore', so the no-op was
indistinguishable from success.
- matchPageSku/planVendor/stampVendor take a (default 1, so existing callers
are unchanged). pass 2 finds rows where FIELD_SENT is non-empty ('*') and the
2nd-request field is still empty.
- pass 1 now writes ONLY 'Date Email Sent to Vendor after 10 Days'. It used to write
the 2nd-request field at the same time, which left request #2 nowhere to record.
- pass 2 writes ONLY 'Date Sample Request Letter Sent', preserving the 1st-chase date.
- server.js derives isResend from sent.json (the record of fact) rather than b.force,
so a forced genuine first send stays pass 1.
Not yet exercised against live FileMaker — no records written.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
scripts/fmpro.mjs | 34 +++++++++++++++++++++++++---------
server.js | 21 ++++++++++++++-------
2 files changed, 39 insertions(+), 16 deletions(-)
diff --git a/scripts/fmpro.mjs b/scripts/fmpro.mjs
index 4ab60d3..c5d4e32 100644
--- a/scripts/fmpro.mjs
+++ b/scripts/fmpro.mjs
@@ -70,16 +70,25 @@ function windowRange(today = new Date()) {
// find is begins-with-word, so vid over-matches: "SAN"→SANDB, "GREEN"→unrelated rows).
// `==` forces an exact match; `"="` matches an EMPTY field, so already-stamped/fulfilled
// rows are excluded (idempotent). Disambiguated by request date when a SKU repeats.
-async function matchPageSku(dw, requested) {
+async function matchPageSku(dw, requested, pass = 1) {
// Require BOTH the exact DW# and the exact request date. A DW# alone maps to many
// records (the live memo + blank pattern-masters + year-old orders); the request
// date pins it to the one current memo. No request date = don't guess, skip.
if (!dw || !String(dw).trim() || !requested) return [];
+ // TK-11409 (Steve, 2026-09-10): the 2nd request must also update the WALLPAPER file.
+ // pass 1 (first chase) -> rows whose FIELD_SENT is still EMPTY.
+ // pass 2 (the resend) -> rows already stamped by pass 1 whose 2nd-request field is
+ // still EMPTY. Keying pass 2 off `FIELD_SENT: '*'` (non-empty)
+ // is what makes the resend visible at all: the old query hard-
+ // coded `FIELD_SENT: '='` for every send, so on a resend it
+ // matched ZERO records and silently wrote nothing.
const query = {
'combo sku': `==${dw}`,
'today for client': `==${requested}`, // exact page-row request date
'Date WP Sample Sent': '=', // still outstanding
- [FIELD_SENT]: '=', // not yet stamped
+ ...(pass === 2
+ ? { [FIELD_SENT]: '*', [FIELD_2ND_DATE]: '=' } // chased once, not yet 2nd-requested
+ : { [FIELD_SENT]: '=' }), // not yet stamped
};
try {
const { records } = await fm.findRecords(DB, LAYOUT, query, { limit: 50 });
@@ -96,11 +105,11 @@ async function matchPageSku(dw, requested) {
}
// --- plan/stamp one vendor from its exact page SKUs (fleet.json items) ---
-async function planVendor(v) {
+async function planVendor(v, pass = 1) {
const seen = new Set();
const rows = [];
for (const it of (v.items || [])) {
- const matches = await matchPageSku(it.dw, it.initialReq || it.req);
+ const matches = await matchPageSku(it.dw, it.initialReq || it.req, pass);
for (const m of matches) {
if (seen.has(m.recordId)) continue;
seen.add(m.recordId);
@@ -109,14 +118,18 @@ async function planVendor(v) {
}
return rows;
}
-async function stampVendor(v) {
- const rows = await planVendor(v);
+async function stampVendor(v, pass = 1) {
+ const rows = await planVendor(v, pass);
const stamped = [];
for (const r of rows) {
- const res = await fm.updateRecord(DB, LAYOUT, r.recordId, { [FIELD_SENT]: r.date, [FIELD_2ND_DATE]: r.date }, { dryRun: false });
+ // Pass 1 writes ONLY the 10-day chase date, so the 2nd-request field stays free as a
+ // real slot. (It used to write both on the first send, which left request #2 nowhere
+ // to record itself.) Pass 2 writes ONLY the 2nd-request field, preserving the 1st date.
+ const fields = pass === 2 ? { [FIELD_2ND_DATE]: r.date } : { [FIELD_SENT]: r.date };
+ const res = await fm.updateRecord(DB, LAYOUT, r.recordId, fields, { dryRun: false });
if (res.committed) stamped.push(r);
}
- return { vid: v.vid, date: v.date, count: stamped.length, found: rows.length, stamped };
+ return { vid: v.vid, date: v.date, pass, count: stamped.length, found: rows.length, stamped };
}
// --- write a plain-language reply note to a specific memo record ---
@@ -196,10 +209,13 @@ if (cmd === 'plan') {
// Single vendor, keyed by --slug (server.js passes this at send time). Matches
// that vendor's exact page SKUs — never vid — so it can't over-match other vendors.
const slug = arg('slug'); const date = arg('date') || fmtDate(new Date());
+ // --pass 2 = this send was a RESEND (the 2nd request). Default 1 keeps the original
+ // first-chase behaviour, so nothing that calls `stamp` without --pass changes.
+ const pass = String(arg('pass') || '1') === '2' ? 2 : 1;
if (!slug) { console.error('need --slug'); process.exit(1); }
const fleetV = JSON.parse(readFileSync(join(ROOT, 'data', 'fleet.json'), 'utf8')).vendors.find((x) => x.slug === slug);
if (!fleetV) { console.error(`unknown --slug ${slug}`); process.exit(1); }
- const r = await stampVendor({ slug: fleetV.slug, vid: fleetV.vid, name: fleetV.name, items: fleetV.items, date });
+ const r = await stampVendor({ slug: fleetV.slug, vid: fleetV.vid, name: fleetV.name, items: fleetV.items, date }, pass);
if (r.count) recordPosted(fleetV.vid, r);
console.log(JSON.stringify(r));
} else if (cmd === 'note') {
diff --git a/server.js b/server.js
index 530e479..c9ce23f 100644
--- a/server.js
+++ b/server.js
@@ -27,9 +27,11 @@ const replies = () => readJSON(p('data', 'replies.json'), { byVid: {} });
// After a send, stamp "Date Email Sent to Vendor after 10 Days" on that vendor's exact
// page SKUs in FileMaker (WALLPAPER2) and record it for the console chip. Fire-and-forget:
// the email already went out, so a FileMaker hiccup must never fail the send response.
-function stampFmpro(slug) {
+function stampFmpro(slug, pass = 1) {
try {
- const child = spawn(process.execPath, [p('scripts', 'fmpro.mjs'), 'stamp', '--slug', slug],
+ const args = [p('scripts', 'fmpro.mjs'), 'stamp', '--slug', slug];
+ if (pass === 2) args.push('--pass', '2'); // TK-11409: the resend writes the 2nd-request field
+ const child = spawn(process.execPath, args,
{ cwd: ROOT, stdio: 'ignore', detached: true });
child.on('error', () => {});
child.unref();
@@ -130,11 +132,14 @@ const server = http.createServer(async (req, res) => {
let payload;
if (st) payload = { account: 'info', to: st.to, subject: st.subject, body: st.body };
else { const v = fleet().vendors.find(x => x.slug === b.slug); if (!v) return send(res, 404, { error: 'no vendor' }); const d = draftFor(v, c); payload = { account: 'info', to: d.to, subject: d.subject, body: d.html }; }
+ // Was this vendor already emailed BEFORE this send? Computed from sent.json (the record of
+ // fact), not from b.force — someone can force a genuine first send, and that must stay pass 1.
+ const _addrs = String(payload.to).split(',').map(a => a.trim().toLowerCase()).filter(a => a.includes('@'));
+ const _be = (sent().byEmail) || {};
+ const isResend = _addrs.length > 0 && _addrs.every(a => _be[a]);
// Resend guard (Cody FIX FIRST): refuse if this vendor's recipients were already emailed, unless {force:true}.
- if (!b.force) {
- const be = (sent().byEmail) || {};
- const addrs = String(payload.to).split(',').map(a => a.trim().toLowerCase()).filter(a => a.includes('@'));
- if (addrs.length && addrs.every(a => be[a])) return send(res, 409, { error: 'already sent to this vendor — click again to force-resend', to: payload.to, alreadySent: true });
+ if (!b.force && isResend) {
+ return send(res, 409, { error: 'already sent to this vendor — click again to force-resend', to: payload.to, alreadySent: true });
}
// George's canonical creds live in the DW-MCP .env (the file George's server loads into `creds`);
// GEORGE_EXTERNAL_SEND_TOKEN lives in george-gmail/.env. Search both, canonical first. (2026-08-15 fix:
@@ -148,7 +153,9 @@ const server = http.createServer(async (req, res) => {
let ok = false, mid = ''; try { const j = JSON.parse(gb); ok = !!j.success; mid = j.messageId || ''; } catch (e) {}
if (ok) {
try { const s = sent(); s.byEmail = s.byEmail || {}; const iso = new Date().toISOString(); for (let a of String(payload.to).split(',')) { a = a.trim().toLowerCase(); if (!a.includes('@')) continue; const cur = s.byEmail[a]; if (!cur) s.byEmail[a] = { lastSent: iso, count: 1 }; else { cur.count++; cur.lastSent = iso; } } fs.writeFileSync(p('data', 'sent.json'), JSON.stringify(s, null, 2)); } catch (e) {}
- stampFmpro(b.slug); // write the sent-date back to FileMaker + light the FMPro chip
+ // 1st chase -> "Date Email Sent to Vendor after 10 Days"; resend (2nd request) ->
+ // "Date Sample Request Letter Sent". Both light the FMPro chip. (TK-11409)
+ stampFmpro(b.slug, isResend ? 2 : 1);
return send(res, 200, { ok: true, messageId: mid, to: payload.to });
}
return send(res, 502, { error: 'George send blocked/failed', detail: gb.slice(0, 200) });
← c613b79 auto-data-snapshot: 2026-09-10T08:12:39 (1 data files) — dat
·
back to Sample Followup Sweep
·
auto-data-snapshot: 2026-09-10T10:34:55 (2 data files) — dat 0603d8e →