← back to AbramsEgo
spend-reviews: auto-skip own-business/infra merchants + fix gmail adapter alias
22d3236f1ec44add400028ba76c9d1b87429b8bb · 2026-09-10 14:47:25 -0700 · Steve
- gmail source ('gmail') now resolves to gmail-receipts.js via alias (was a silent no-op)
- EXCLUDE_MERCHANTS filter auto-skips Steve's own companies (self-review is prohibited) + infra/SaaS vendors at ingest, env-overridable via SPEND_REVIEW_EXCLUDE
- applied in both /ingest/:source and /upload-csv
- arm-spend-reviews.sh launcher for the gated activation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A lib/spend-reviews/adapters/gmail.jsM lib/spend-reviews/config.jsM lib/spend-reviews/resolver.jsM lib/spend-reviews/router.jsA scripts/arm-spend-reviews.sh
Diff
commit 22d3236f1ec44add400028ba76c9d1b87429b8bb
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Sep 10 14:47:25 2026 -0700
spend-reviews: auto-skip own-business/infra merchants + fix gmail adapter alias
- gmail source ('gmail') now resolves to gmail-receipts.js via alias (was a silent no-op)
- EXCLUDE_MERCHANTS filter auto-skips Steve's own companies (self-review is prohibited) + infra/SaaS vendors at ingest, env-overridable via SPEND_REVIEW_EXCLUDE
- applied in both /ingest/:source and /upload-csv
- arm-spend-reviews.sh launcher for the gated activation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
lib/spend-reviews/adapters/gmail.js | 3 +++
lib/spend-reviews/config.js | 20 ++++++++++++++++++++
lib/spend-reviews/resolver.js | 14 +++++++++++++-
lib/spend-reviews/router.js | 12 ++++++++++--
scripts/arm-spend-reviews.sh | 33 +++++++++++++++++++++++++++++++++
5 files changed, 79 insertions(+), 3 deletions(-)
diff --git a/lib/spend-reviews/adapters/gmail.js b/lib/spend-reviews/adapters/gmail.js
new file mode 100644
index 00000000..af467920
--- /dev/null
+++ b/lib/spend-reviews/adapters/gmail.js
@@ -0,0 +1,3 @@
+// Alias: SOURCES uses 'gmail' but the adapter file is gmail-receipts.js.
+// loadAdapter does require('./adapters/'+source), so 'gmail' must resolve here.
+module.exports = require('./gmail-receipts');
diff --git a/lib/spend-reviews/config.js b/lib/spend-reviews/config.js
index 80d42f6f..615c891c 100644
--- a/lib/spend-reviews/config.js
+++ b/lib/spend-reviews/config.js
@@ -50,4 +50,24 @@ module.exports = {
SOURCES: ['amazon', 'gmail', 'csv'],
TARGETS: ['google_review', 'amazon_review', 'seller_email'],
+
+ // Merchants to AUTO-SKIP at ingest: Steve's OWN businesses (a review of your own
+ // company is a prohibited fake review) + pure infra/SaaS vendors that aren't the
+ // consumer "businesses I spend money on" this feature is for. Matched as
+ // normalized substrings (case-insensitive). Extend/replace via the comma-sep
+ // env SPEND_REVIEW_EXCLUDE. NOTE: never add bare 'amazon' — Amazon.com purchases
+ // are the core use case; the normalizer already strips the amazon/amzn prefix.
+ EXCLUDE_MERCHANTS: (process.env.SPEND_REVIEW_EXCLUDE
+ ? process.env.SPEND_REVIEW_EXCLUDE.split(',')
+ : [
+ // Steve's own businesses / brands (self-review = prohibited)
+ 'designerwallcoverings', 'designer wallcoverings', 'novasuede', 'philipperomano',
+ 'phillipe romano', 'wallpapersback', "wallpaper's back", 'apartmentwallpaper',
+ 'agentabrams', 'abrams ego', 'abramsego',
+ // pure infra / SaaS vendors (not consumer businesses to review)
+ 'kamatera', 'cloudflare', 'godaddy', 'namecheap', 'porkbun', 'openai', 'anthropic',
+ 'replicate', 'elevenlabs', 'twilio', 'stripe', 'purelymail', 'browserbase',
+ 'moonshot', 'vercel', 'github', 'google workspace', 'google cloud', 'aws',
+ 'digitalocean', 'linode',
+ ]).map((s) => s.trim().toLowerCase()).filter(Boolean),
};
diff --git a/lib/spend-reviews/resolver.js b/lib/spend-reviews/resolver.js
index 34452d17..b303fe10 100644
--- a/lib/spend-reviews/resolver.js
+++ b/lib/spend-reviews/resolver.js
@@ -82,4 +82,16 @@ async function resolveItem(item, budget) {
return { google, amazon };
}
-module.exports = { resolveItem, resolveGoogle, resolveAmazon, cachePlaceId, mapsSearchUrl, writeReviewUrl };
+/**
+ * True if this merchant should be auto-skipped (Steve's own businesses or a pure
+ * infra/SaaS vendor — see config.EXCLUDE_MERCHANTS). Matches the normalized AND
+ * display merchant as case-insensitive substrings so both "Designerwallcoverings"
+ * and a raw "DESIGNER WALLCOVERINGS LLC" descriptor are caught.
+ */
+function isExcluded(item) {
+ const norm = (item.norm_merchant || store.normalizeMerchant(item.merchant) || '').toLowerCase();
+ const disp = (item.display_merchant || item.merchant || '').toLowerCase();
+ return (cfg.EXCLUDE_MERCHANTS || []).some((x) => x && (norm.includes(x) || disp.includes(x)));
+}
+
+module.exports = { resolveItem, resolveGoogle, resolveAmazon, cachePlaceId, mapsSearchUrl, writeReviewUrl, isExcluded };
diff --git a/lib/spend-reviews/router.js b/lib/spend-reviews/router.js
index 6c370145..18d0f883 100644
--- a/lib/spend-reviews/router.js
+++ b/lib/spend-reviews/router.js
@@ -81,8 +81,12 @@ router.post('/ingest/:source', async (req, res) => {
const adapter = loadAdapter(source);
const out = await adapter.ingest(Object.assign({}, req.body, { source }));
const rawItems = (out.items || []).map((i) => store.makeItem(Object.assign({ source }, i)));
+ let excluded = 0;
+ for (const it of rawItems) {
+ if (resolver.isExcluded(it)) { it.excluded = true; for (const t of cfg.TARGETS) it.targets[t] = 'skipped'; excluded++; }
+ }
const up = store.upsertItems(rawItems);
- res.json({ ok: out.ok !== false, source, meta: out.meta || {}, ingested: up, needsAction: out.meta && out.meta.needsAction || null });
+ res.json({ ok: out.ok !== false, source, meta: out.meta || {}, ingested: up, excluded, needsAction: out.meta && out.meta.needsAction || null });
} catch (e) { res.status(500).json({ ok: false, error: e.message }); }
});
@@ -93,8 +97,12 @@ router.post('/upload-csv', async (req, res) => {
const adapter = loadAdapter('csv');
const out = await adapter.ingest({ source: 'csv', csv: req.body && req.body.csv, filename: req.body && req.body.filename });
const rawItems = (out.items || []).map((i) => store.makeItem(Object.assign({ source: 'csv' }, i)));
+ let excluded = 0;
+ for (const it of rawItems) {
+ if (resolver.isExcluded(it)) { it.excluded = true; for (const t of cfg.TARGETS) it.targets[t] = 'skipped'; excluded++; }
+ }
const up = store.upsertItems(rawItems);
- res.json({ ok: out.ok !== false, meta: out.meta || {}, ingested: up });
+ res.json({ ok: out.ok !== false, meta: out.meta || {}, ingested: up, excluded });
} catch (e) { res.status(500).json({ ok: false, error: e.message }); }
});
diff --git a/scripts/arm-spend-reviews.sh b/scripts/arm-spend-reviews.sh
new file mode 100644
index 00000000..7312d67f
--- /dev/null
+++ b/scripts/arm-spend-reviews.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+# Spend->Reviews activation: wire George creds from secrets-manager + arm the
+# posting executors. Idempotent (safe to re-run). Steve runs this via `! bash ...`.
+# Arming posts NOTHING on its own — per-item approve + content-seal + confirm still gate every send.
+set -euo pipefail
+cd ~/Projects/AbramsEgo
+
+# .env may not exist yet (app runs on defaults) — create it so sed/append work
+touch .env
+
+# --- wire George creds from the canonical secrets store (value never printed) ---
+A=$(grep -m1 '^GEORGE_AUTH=' ~/Projects/secrets-manager/.env | cut -d= -f2-)
+U=${A%%:*}
+P=${A#*:}
+sed -i '' '/^GEORGE_\(URL\|AUTH\|USER\|PASS\)=/d' .env
+{
+ echo "GEORGE_URL=http://localhost:9850"
+ echo "GEORGE_AUTH=$A"
+ echo "GEORGE_USER=$U"
+ echo "GEORGE_PASS=$P"
+} >> .env
+
+# --- arm the posting edge ---
+if grep -q '^SPEND_REVIEW_EXECUTORS_LIVE=' .env; then
+ sed -i '' 's/^SPEND_REVIEW_EXECUTORS_LIVE=.*/SPEND_REVIEW_EXECUTORS_LIVE=1/' .env
+else
+ echo 'SPEND_REVIEW_EXECUTORS_LIVE=1' >> .env
+fi
+
+pm2 restart abramsego >/dev/null 2>&1 || true
+sleep 2
+curl -s -o /dev/null -w 'ready http=%{http_code}\n' -u admin:DW2024! http://127.0.0.1:9773/
+echo "george keys set: $(grep -cE '^GEORGE_(URL|AUTH|USER|PASS)=' .env)/4 ; executors_live=$(grep -m1 '^SPEND_REVIEW_EXECUTORS_LIVE=' .env | cut -d= -f2-)"
← 65195041 auto-data-snapshot: 2026-09-10T14:13:31 (1 data files) — dat
·
back to AbramsEgo
·
auto-data-snapshot: 2026-09-10T14:47:21 (1 data files) — dat 2697be6b →