← back to Sample Followup Sweep
5x sweep 5 (Cody FIX FIRST #2): refuse empty follow-up letters — compose() throws on 0 rows, send-one 409s a 0-item vendor, no send button on empty rows (TK-12320)
5181a26f1df5a5421fbedd9bfa4c5d1fde8153f5 · 2026-09-26 12:46:59 -0700 · Steve Abrams
Files touched
M lib/compose.jsM server.jsA test/compose-empty.test.js
Diff
commit 5181a26f1df5a5421fbedd9bfa4c5d1fde8153f5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Sep 26 12:46:59 2026 -0700
5x sweep 5 (Cody FIX FIRST #2): refuse empty follow-up letters — compose() throws on 0 rows, send-one 409s a 0-item vendor, no send button on empty rows (TK-12320)
---
lib/compose.js | 2 ++
server.js | 7 ++++++-
test/compose-empty.test.js | 8 ++++++++
3 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/lib/compose.js b/lib/compose.js
index 98f7028..3208edd 100644
--- a/lib/compose.js
+++ b/lib/compose.js
@@ -7,6 +7,8 @@ function esc(s) {
}
function compose(vendor, rows) {
+ // TK-12320 (Cody): never emit a follow-up letter with no items in it.
+ if (!Array.isArray(rows) || rows.length === 0) throw new Error('compose: refusing an empty follow-up letter (0 outstanding items)');
const account = vendor.account_number || '‹CONFIRM DW ACCOUNT #›';
// Steve 8/20: fall back to the vendor's main email when no sample email exists.
const to = vendor.sample_email || vendor.main_email || '‹CONFIRM SAMPLE EMAIL›';
diff --git a/server.js b/server.js
index 0adfaec..2926c88 100644
--- a/server.js
+++ b/server.js
@@ -146,6 +146,11 @@ const server = http.createServer(async (req, res) => {
const c = contacts();
const cc = c[b.slug] || {};
if (!cc.sample_email || !cc.account_number) return send(res, 400, { error: 'vendor not ready (needs sample email + account #)' });
+ // TK-12320 (Cody): a vendor with 0 outstanding items has nothing to chase — refuse server-side,
+ // including a stale staged draft, so no click can send an empty/obsolete letter.
+ const _v = vendorBySlug(fleet().vendors, b.slug);
+ if (!_v) return send(res, 404, { error: 'no vendor' });
+ if (!_v.items || !_v.items.length) return send(res, 409, { error: 'no outstanding items for this vendor — nothing to chase', noItems: true });
const st = stagedFor(b.slug);
let payload;
if (st) payload = { account: 'info', to: st.to, subject: st.subject, body: st.body };
@@ -278,7 +283,7 @@ async function load(){const r=await fetch('/api/state');S=await r.json();if(!S.s
span.style.color=hrs>24?'#9a6b12':'#999';span.title='sent.json harvested '+S.sent.harvested;
span.textContent=(hrs>24?'⚠ ':'')+'· Sent data '+label+(hrs>24?' — re-harvest recommended':'');
meta.appendChild(span);}}
-function ready(v){const c=S.contacts[v.slug]||{};return c.sample_email&&c.account_number;}
+function ready(v){const c=S.contacts[v.slug]||{};return !!(c.sample_email&&c.account_number&&v.items&&v.items.length);}
// Resolve whether this vendor's follow-up was actually emailed, from the Sent-folder snapshot.
// Matches the vendor's recipient address(es) (contacts.sample_email) against sent.byEmail.
function sentInfo(v){const c=S.contacts[v.slug]||{};const be=(S.sent&&S.sent.byEmail)||{};
diff --git a/test/compose-empty.test.js b/test/compose-empty.test.js
new file mode 100644
index 0000000..e036a58
--- /dev/null
+++ b/test/compose-empty.test.js
@@ -0,0 +1,8 @@
+// TK-12320 (Cody FIX FIRST #2): compose() must refuse an empty follow-up letter.
+const assert = require('assert');
+const { compose } = require('../lib/compose.js');
+const v = { name: 'X', account_number: '1', sample_email: 'a@b.com', ship_to: 'Y' };
+assert.throws(() => compose(v, []), /empty follow-up/);
+assert.throws(() => compose(v, undefined), /empty follow-up/);
+assert.ok(compose(v, [{ mfr: 'ABC-1' }]).html.includes('ABC-1'));
+console.log('compose-empty: ALL PASS');
← 18d9697 auto-data-snapshot: 2026-09-26T12:44:57 (1 data files) — dat
·
back to Sample Followup Sweep
·
5x report: sweep 5 + Cody findings (TK-12320) 60334c7 →