[object Object]

← back to Ads Dashboard

combined revenue: /api/revenue merges Shopify + FMPro (Mac2-pushed) + PayPal placeholder = $115k real; KPIs + Revenue-by-stream panel

914f356b1224c448b4f85b6c1a800839bd76da43 · 2026-08-03 07:20:40 -0700 · Steve Abrams

Files touched

Diff

commit 914f356b1224c448b4f85b6c1a800839bd76da43
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 3 07:20:40 2026 -0700

    combined revenue: /api/revenue merges Shopify + FMPro (Mac2-pushed) + PayPal placeholder = $115k real; KPIs + Revenue-by-stream panel
---
 data/fmpro.json   | 10 ++++++++++
 public/index.html | 39 ++++++++++++++++++++++++++++++++++-----
 server.js         | 26 ++++++++++++++++++++++++++
 3 files changed, 70 insertions(+), 5 deletions(-)

diff --git a/data/fmpro.json b/data/fmpro.json
new file mode 100644
index 0000000..d895f45
--- /dev/null
+++ b/data/fmpro.json
@@ -0,0 +1,10 @@
+{
+  "source": "fmpro-invoicing",
+  "present": true,
+  "truncated": false,
+  "window": "07/04/2026–08/03/2026 (last 30d)",
+  "total": 72706.19,
+  "orders": 251,
+  "open_quotes": 368,
+  "generated_at": "2026-08-03T14:19:02.822Z"
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index ff9d26a..4780e69 100644
--- a/public/index.html
+++ b/public/index.html
@@ -50,6 +50,13 @@
 <main>
   <div class="grid" id="kpis"></div>
 
+  <div class="section-h">Revenue by stream <span style="text-transform:none;font-weight:400">(trailing 30d, net)</span></div>
+  <div class="card">
+    <table id="revTable"><thead><tr><th>Stream</th><th>Revenue</th><th>Orders</th><th>Status</th></tr></thead>
+    <tbody id="revBody"></tbody></table>
+    <div class="when" id="revNote" style="margin-top:8px"></div>
+  </div>
+
   <div class="section-h">Daily orders <span style="text-transform:none;font-weight:400">(net, last 30d)</span></div>
   <div class="card">
     <div id="trend" style="display:flex;align-items:flex-end;gap:3px;height:120px;overflow-x:auto"></div>
@@ -101,6 +108,24 @@
 const fmtDate = (iso) => { try { return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); } catch { return iso||'—'; } };
 const $ = (id)=>document.getElementById(id);
 
+function renderRevenue(rev){
+  if (!rev || !rev.present){ $('revBody').innerHTML='<tr><td colspan="4" class="empty">Revenue unavailable (source: /api/revenue)</td></tr>'; return; }
+  const fmt$ = n => '$'+Number(n||0).toLocaleString();
+  const rows = (rev.streams||[]).map(s=>{
+    let status;
+    if (!s.present) status = '<span class="badge">'+(s.reason||'not connected')+'</span>';
+    else if (s.stale) status = '<span class="badge" style="border-color:#c0392b;color:#c0392b">⚠ stale '+s.age_hours+'h — as of '+fmtDate(s.as_of)+'</span>';
+    else status = '<span class="badge">live'+(s.as_of?' · '+fmtDate(s.as_of):'')+'</span>';
+    return `<tr${s.present?'':' style="opacity:.55"'}>
+      <td><a href="/api/revenue">${s.stream}</a></td>
+      <td>${s.present?fmt$(s.total):'—'}</td>
+      <td>${s.orders!=null?s.orders:'—'}</td>
+      <td>${status}</td></tr>`;
+  }).join('');
+  $('revBody').innerHTML = rows + `<tr style="font-weight:680"><td>Combined</td><td>${fmt$(rev.combined_total)}</td><td></td><td></td></tr>`;
+  $('revNote').textContent = rev.note || '';
+}
+
 let campaigns = [];
 async function boot(){
   const brand = await fetch('/api/brand').then(r=>r.json());
@@ -110,21 +135,25 @@ async function boot(){
   $('hostChip').textContent = location.host;
   document.title = brand.short || 'Ad-Ops';
 
-  const [acq, camps] = await Promise.all([
+  const [acq, camps, rev] = await Promise.all([
     fetch('/api/acquisition').then(r=>r.json()).catch(()=>({present:false})),
     fetch('/api/campaigns').then(r=>r.json()),
+    fetch('/api/revenue').then(r=>r.json()).catch(()=>({present:false})),
   ]);
+  renderRevenue(rev);
 
   // KPIs — driven by REAL acquisition data (30d Shopify orders), every number hrefs to its source
   const topCh = (acq.channels||[])[0];
   const tagPct = acq.present && acq.totals.orders ? Math.round((acq.totals.utm_tagged||0)/acq.totals.orders*100) : 0;
+  const shopS = (rev.streams||[]).find(s=>s.stream==='Shopify');
+  const fmS = (rev.streams||[]).find(s=>s.stream==='FMPro invoicing');
   const kpis = acq.present ? [
-    { h:'Orders (30d)', v: acq.totals.orders, href:'/api/acquisition', when: acq.window },
-    { h:'Shopify rev (30d)', v: '$'+acq.totals.revenue.toLocaleString(), href:'/api/acquisition', when: 'Shopify only — FMPro + PayPal separate' },
+    { h:'Revenue (30d, all streams)', v: rev.present ? '$'+rev.combined_total.toLocaleString() : '$'+acq.totals.revenue.toLocaleString(), href:'/api/revenue', when: rev.present ? 'Shopify + FMPro'+((rev.streams||[]).some(s=>s.stream==='PayPal'&&s.present)?' + PayPal':' (PayPal pending)') : 'Shopify only' },
+    { h:'Shopify (30d)', v: '$'+(shopS?shopS.total:acq.totals.revenue).toLocaleString(), href:'/api/acquisition', when: acq.totals.orders+' orders' },
+    { h:'FMPro invoicing (30d)', v: fmS&&fmS.present ? '$'+fmS.total.toLocaleString() : '—', href:'/api/revenue', when: fmS&&fmS.present ? fmS.orders+' booked'+(fmS.stale?' · ⚠ stale':'') : (fmS?fmS.reason:'') },
     { h:'Top channel', v: topCh ? topCh.channel : '—', href:'/api/acquisition', when: topCh ? topCh.orders+' orders' : '' },
-    { h:'UTM-tagged', v: tagPct+'%', href:'/api/acquisition', when: (acq.totals.utm_tagged||0)+' of '+acq.totals.orders },
   ] : [
-    { h:'Orders (30d)', v:'—', href:'/api/acquisition', when:'acquisition unavailable' },
+    { h:'Revenue', v:'—', href:'/api/revenue', when:'unavailable' },
   ];
   $('kpis').innerHTML = kpis.map(k=>`<div class="card"><h2>${k.h}</h2>
      <div class="kpi"><a href="${k.href}">${k.v}</a></div>
diff --git a/server.js b/server.js
index d339d63..d21ca71 100644
--- a/server.js
+++ b/server.js
@@ -252,6 +252,32 @@ const server = http.createServer((req, res) => {
     return sendJSON(res, 200, Array.isArray(c) ? { campaigns: c } : c);
   }
   if (pathname === '/api/brand') return sendJSON(res, 200, brand);
+  if (pathname === '/api/revenue') {
+    if (brand.key !== 'dw') return sendJSON(res, 200, { present: false, reason: 'revenue is DW-only' });
+    return fetchAcquisition().then(acq => {
+      const fm = readJSON('fmpro.json', null);
+      const streams = [];
+      // Shopify — live
+      streams.push({ stream: 'Shopify', present: !!acq.present, total: acq.present ? acq.totals.revenue : 0,
+        orders: acq.present ? acq.totals.orders : 0, window: acq.window || null, as_of: acq.generated_at || null });
+      // FMPro — pushed from Mac2 (aggregate only), stamped with freshness
+      if (fm && fm.present) {
+        const ageH = fm.generated_at ? (Date.now() - Date.parse(fm.generated_at)) / 36e5 : null;
+        streams.push({ stream: 'FMPro invoicing', present: true, total: fm.total, orders: fm.orders,
+          window: fm.window, as_of: fm.generated_at, stale: ageH != null && ageH > 25, age_hours: ageH != null ? Math.round(ageH * 10) / 10 : null });
+      } else {
+        streams.push({ stream: 'FMPro invoicing', present: false, total: 0, reason: fm ? 'truncated fetch' : 'no push yet (Mac2→Kamatera)' });
+      }
+      // PayPal — not connected (source TBD, Steve to confirm)
+      streams.push({ stream: 'PayPal', present: false, total: 0, reason: 'not connected — source pending' });
+      const combined = streams.filter(s => s.present).reduce((a, s) => a + (s.total || 0), 0);
+      return sendJSON(res, 200, {
+        present: true, generated_at: new Date().toISOString(),
+        note: 'Shopify + FMPro are confirmed non-overlapping (Steve 2026-08-03). PayPal not yet connected.',
+        combined_total: Math.round(combined * 100) / 100, streams,
+      });
+    }).catch(e => sendJSON(res, 200, { present: false, reason: 'error: ' + e.message }));
+  }
   if (pathname === '/api/acquisition') {
     if (brand.key !== 'dw') return sendJSON(res, 200, { present: false, reason: 'acquisition data is DW-only' });
     return fetchAcquisition()

← 33b3919 honest relabel: 'Shopify rev (30d)' (FMPro $72.7k + PayPal a  ·  back to Ads Dashboard  ·  href-drill: daily trend bars now href to /api/acquisition (e c0ed05e →