← back to Designer Wallcoverings
snapshot before restart: Lilycolor enrichment complete (2634/2634) + readiness 320→2543 + full Sangetsu scrape (623 patterns, 13358 SKUs)
46bfc170fabdac9f9d01084a2fdf2e7f8fd3ab98 · 2026-07-02 06:37:19 -0700 · Steve
Files touched
A DW-MCP/daily-sales-summary.js
Diff
commit 46bfc170fabdac9f9d01084a2fdf2e7f8fd3ab98
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jul 2 06:37:19 2026 -0700
snapshot before restart: Lilycolor enrichment complete (2634/2634) + readiness 320→2543 + full Sangetsu scrape (623 patterns, 13358 SKUs)
---
DW-MCP/daily-sales-summary.js | 110 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 110 insertions(+)
diff --git a/DW-MCP/daily-sales-summary.js b/DW-MCP/daily-sales-summary.js
new file mode 100644
index 00000000..ebbb84f9
--- /dev/null
+++ b/DW-MCP/daily-sales-summary.js
@@ -0,0 +1,110 @@
+/*
+ * daily-sales-summary.js — End-of-day sales recap for Slack #new-order.
+ *
+ * Posts, once per day, a total for TODAY and a MONTH-TO-DATE total:
+ * • Today: N orders · $X.XX
+ * • Month-to-date: M orders · $Y.YY
+ *
+ * "Sales" = non-cancelled Shopify orders, bucketed by created_at in Pacific Time.
+ * Pairs with monitor-new-orders.js (per-sale pings, same #new-order channel).
+ *
+ * Env (DW-MCP/.env): SHOPIFY_STORE_DOMAIN, SHOPIFY_ACCESS_TOKEN, SLACK_BOT_TOKEN.
+ * Usage: node daily-sales-summary.js # compute + POST to Slack
+ * node daily-sales-summary.js --dry-run # compute + print only (no post)
+ */
+const axios = require('axios');
+const { WebClient } = require('@slack/web-api');
+require('dotenv').config();
+
+const TZ = 'America/Los_Angeles';
+const CHANNEL = process.env.SALES_SUMMARY_CHANNEL || 'new-order';
+const DRY_RUN = process.argv.includes('--dry-run');
+const API_VERSION = process.env.SHOPIFY_API_VERSION || '2024-10';
+
+const pad = (n) => String(n).padStart(2, '0');
+
+// Y-M-D of an instant, as seen in Pacific Time.
+function ptYMD(d) {
+ const parts = new Intl.DateTimeFormat('en-CA', {
+ timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit',
+ }).formatToParts(d);
+ const g = (t) => parts.find((p) => p.type === t).value;
+ return { y: +g('year'), m: +g('month'), d: +g('day') };
+}
+
+// Pacific offset (e.g. "-07:00") active at instant d, so ISO strings anchor to PT midnight.
+function ptOffset(d) {
+ const name = new Intl.DateTimeFormat('en-US', { timeZone: TZ, timeZoneName: 'longOffset' })
+ .formatToParts(d).find((p) => p.type === 'timeZoneName').value; // "GMT-07:00"
+ return name.replace('GMT', '') || '+00:00';
+}
+
+async function fetchOrdersSince(sinceISO) {
+ const base = `https://${process.env.SHOPIFY_STORE_DOMAIN}/admin/api/${API_VERSION}`;
+ let url = `${base}/orders.json?status=any&limit=250&created_at_min=${encodeURIComponent(sinceISO)}`;
+ const out = [];
+ while (url) {
+ const res = await axios.get(url, {
+ headers: { 'X-Shopify-Access-Token': process.env.SHOPIFY_ACCESS_TOKEN, 'Content-Type': 'application/json' },
+ });
+ out.push(...(res.data.orders || []));
+ // Shopify cursor pagination via the Link header (rel="next").
+ const link = res.headers.link || res.headers.Link || '';
+ const m = link.match(/<([^>]+)>;\s*rel="next"/);
+ url = m ? m[1] : null;
+ }
+ return out;
+}
+
+function summarize(orders, sinceInstant) {
+ let dayCount = 0, dayTotal = 0, mtdCount = 0, mtdTotal = 0, currency = 'USD';
+ for (const o of orders) {
+ if (o.cancelled_at) continue; // don't count cancelled orders as sales
+ const amt = parseFloat(o.total_price || '0') || 0;
+ if (o.currency) currency = o.currency;
+ mtdCount += 1; mtdTotal += amt;
+ if (new Date(o.created_at) >= sinceInstant) { dayCount += 1; dayTotal += amt; }
+ }
+ return { dayCount, dayTotal, mtdCount, mtdTotal, currency };
+}
+
+async function main() {
+ const now = new Date();
+ const { y, m, d } = ptYMD(now);
+ const off = ptOffset(now);
+ const startOfTodayISO = `${y}-${pad(m)}-${pad(d)}T00:00:00${off}`;
+ const startOfMonthISO = `${y}-${pad(m)}-01T00:00:00${off}`;
+ const startOfTodayInstant = new Date(startOfTodayISO);
+
+ const orders = await fetchOrdersSince(startOfMonthISO);
+ const s = summarize(orders, startOfTodayInstant);
+
+ const money = (n) => `${s.currency} ${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+ const dayLabel = new Intl.DateTimeFormat('en-US', { timeZone: TZ, weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' }).format(now);
+ const monthLabel = new Intl.DateTimeFormat('en-US', { timeZone: TZ, month: 'long', year: 'numeric' }).format(now);
+
+ const text = [
+ `*📊 Daily Sales Summary — ${dayLabel}*`,
+ ``,
+ `🗓️ *Today:* ${s.dayCount} order${s.dayCount === 1 ? '' : 's'} · *${money(s.dayTotal)}*`,
+ `📈 *Month-to-date (${monthLabel}):* ${s.mtdCount} order${s.mtdCount === 1 ? '' : 's'} · *${money(s.mtdTotal)}*`,
+ ``,
+ `_Sales = non-cancelled orders by order date (Pacific Time)._`,
+ ].join('\n');
+
+ if (DRY_RUN) {
+ console.log('--- DRY RUN (not posted) ---\n' + text);
+ return;
+ }
+ const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
+ const r = await slack.chat.postMessage({ channel: CHANNEL, text });
+ console.log(`✓ Posted daily summary to #${CHANNEL} (ts ${r.ts})`);
+ console.log(` Today: ${s.dayCount} / ${money(s.dayTotal)} · MTD: ${s.mtdCount} / ${money(s.mtdTotal)}`);
+}
+
+main().catch((e) => {
+ console.error('daily-sales-summary error:', e.message);
+ if (e.response) console.error(' HTTP', e.response.status, JSON.stringify(e.response.data).slice(0, 300));
+ if (e.data) console.error(' Slack:', JSON.stringify(e.data).slice(0, 300));
+ process.exit(1);
+});
← 0dadf149 audit: readiness re-run post-enrichment-complete (Lilycolor
·
back to Designer Wallcoverings
·
auto-save: 2026-07-02T06:41:33 (6 files) — pending-approval/ 005e77b9 →