[object Object]

← 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

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 →