[object Object]

← back to Govarbitrage

DigestSnapshot model: freeze each honesty-gated Top-10 at send time (public-safe fields only); capture wired into digest send + standalone; seed script with marked fabricated edition

343863bbc119b8fc706a0f10bff21d3a3be2f65b · 2026-07-13 01:38:44 -0700 · Steve Abrams

Files touched

Diff

commit 343863bbc119b8fc706a0f10bff21d3a3be2f65b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 13 01:38:44 2026 -0700

    DigestSnapshot model: freeze each honesty-gated Top-10 at send time (public-safe fields only); capture wired into digest send + standalone; seed script with marked fabricated edition
---
 prisma/schema.prisma             |  22 +++++++
 scripts/seed-digest-snapshots.ts | 117 +++++++++++++++++++++++++++++++++++
 src/lib/digest-snapshot.ts       | 128 +++++++++++++++++++++++++++++++++++++++
 src/lib/send-digest.ts           |   6 ++
 4 files changed, 273 insertions(+)

diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 870ab84..04de4aa 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -463,3 +463,25 @@ model AuditLog {
   @@index([createdAt])
   @@index([action])
 }
+
+// ------------------------------------------------------------------ digest archive
+
+enum DigestSlot {
+  AM
+  PM
+}
+
+// Frozen record of each digest run. The public /deals archive pages render
+// these snapshots so archive URLs stay stable and indexable instead of
+// re-querying live listings that expire out from under a crawled page.
+model DigestSnapshot {
+  id        String     @id @default(cuid())
+  date      String // YYYY-MM-DD in America/Los_Angeles (the digest's home timezone)
+  slot      DigestSlot
+  dealsJson Json // public-safe serialized Top-10 (see src/lib/digest-snapshot.ts)
+  dealCount Int
+  createdAt DateTime   @default(now())
+
+  @@unique([date, slot])
+  @@index([date])
+}
diff --git a/scripts/seed-digest-snapshots.ts b/scripts/seed-digest-snapshots.ts
new file mode 100644
index 0000000..bfae08d
--- /dev/null
+++ b/scripts/seed-digest-snapshots.ts
@@ -0,0 +1,117 @@
+// Seed the public /deals archive:
+//   1. A REAL capture for today's slot via captureDigestSnapshot() — freezes
+//      whatever currently clears the hot-deal gate (may be 0 deals; the
+//      archive renders that honestly).
+//   2. A clearly-marked FABRICATED edition for 2026-07-12 with realistic
+//      deals so the teaser rendering (confidence labels, ROI bands, withheld
+//      numbers) is actually exercised before launch. Every fabricated deal's
+//      disclaimer says SEED DATA. Delete before go-live:
+//        DELETE FROM "DigestSnapshot" WHERE date = '2026-07-12';
+//
+// Run: npx tsx scripts/seed-digest-snapshots.ts
+
+import { readFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import { dirname, join } from "node:path";
+import type { Prisma } from "@prisma/client";
+
+function loadEnv() {
+  const root = join(dirname(fileURLToPath(import.meta.url)), "..");
+  try {
+    const raw = readFileSync(join(root, ".env"), "utf8");
+    for (const line of raw.split("\n")) {
+      const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/i);
+      if (!m) continue;
+      const key = m[1];
+      let val = m[2].trim();
+      if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
+        val = val.slice(1, -1);
+      }
+      if (process.env[key] === undefined) process.env[key] = val;
+    }
+  } catch {
+    /* no .env — rely on ambient env */
+  }
+}
+loadEnv();
+
+const { prisma } = await import("../src/lib/db");
+const { captureDigestSnapshot, currentSlot } = await import("../src/lib/digest-snapshot");
+type SnapshotDeal = import("../src/lib/digest-snapshot").SnapshotDeal;
+
+const SEED_DATE = "2026-07-12";
+const SEED_DISCLAIMER = "SEED DATA — fabricated example for pre-launch rendering tests, not a real listing.";
+
+const seedDeals: SnapshotDeal[] = [
+  {
+    rank: 1,
+    title: "2018 John Deere 320G Skid Steer (1,240 hrs) — County Fleet Surplus",
+    source: "GOVDEALS",
+    locationCity: "Bakersfield",
+    locationState: "CA",
+    currentBid: 6200,
+    recMax: 14800,
+    expectedSaleLow: 21500,
+    confidence: "HIGH",
+    roiConservative: 0.58,
+    roiCapped: false,
+    disclaimer: SEED_DISCLAIMER,
+    closingAt: "2026-07-14T19:00:00.000Z",
+    sourceUrl: null,
+  },
+  {
+    rank: 2,
+    title: "Pallet of 24 Dell Latitude 5520 Laptops (i5/16GB, wiped) — School District IT Refresh",
+    source: "PUBLICSURPLUS",
+    locationCity: "Mesa",
+    locationState: "AZ",
+    currentBid: 1150,
+    recMax: 3400,
+    expectedSaleLow: 5300,
+    confidence: "MEDIUM",
+    roiConservative: 0.49,
+    roiCapped: false,
+    disclaimer: SEED_DISCLAIMER,
+    closingAt: "2026-07-13T22:30:00.000Z",
+    sourceUrl: null,
+  },
+  {
+    rank: 3,
+    title: "2015 Ford F-250 XL 4x4 Utility Truck w/ Liftgate — Municipal Water Dept.",
+    source: "MUNICIBID",
+    locationCity: "Spokane",
+    locationState: "WA",
+    currentBid: 3875,
+    recMax: 9200,
+    expectedSaleLow: 12800,
+    confidence: "LOW",
+    roiConservative: 0.41,
+    roiCapped: true,
+    disclaimer: SEED_DISCLAIMER,
+    closingAt: null, // make-offer, no deadline
+    sourceUrl: null,
+  },
+];
+
+async function main() {
+  const real = await captureDigestSnapshot(currentSlot());
+  console.log(`Real capture: ${real.date} ${real.slot} — ${real.dealCount} deal(s)`);
+
+  const dealsJson = seedDeals as unknown as Prisma.InputJsonValue;
+  const fabricated = await prisma.digestSnapshot.upsert({
+    where: { date_slot: { date: SEED_DATE, slot: "AM" } },
+    create: { date: SEED_DATE, slot: "AM", dealsJson, dealCount: seedDeals.length },
+    update: { dealsJson, dealCount: seedDeals.length },
+  });
+  console.log(`Fabricated seed edition: ${fabricated.date} ${fabricated.slot} — ${fabricated.dealCount} deal(s) (SEED DATA)`);
+}
+
+main()
+  .then(async () => {
+    await prisma.$disconnect();
+  })
+  .catch(async (e) => {
+    console.error(e);
+    await prisma.$disconnect();
+    process.exit(1);
+  });
diff --git a/src/lib/digest-snapshot.ts b/src/lib/digest-snapshot.ts
new file mode 100644
index 0000000..b3dc35d
--- /dev/null
+++ b/src/lib/digest-snapshot.ts
@@ -0,0 +1,128 @@
+import { prisma } from "@/lib/db";
+import { findHotDeals, type HotDeal, type Confidence } from "@/lib/hot-deals";
+import type { DigestSlot, Prisma } from "@prisma/client";
+
+// Freezes each digest run into a DigestSnapshot so the public /deals archive
+// renders a stable, indexable record instead of re-querying live listings
+// that expire. Only PUBLIC-SAFE display fields are serialized — exactly what
+// the free-tier digest email already shows, never internal cost math.
+
+export interface SnapshotDeal {
+  rank: number;
+  title: string;
+  source: string;
+  locationCity: string | null;
+  locationState: string | null;
+  currentBid: number;
+  recMax: number;
+  expectedSaleLow: number;
+  confidence: Confidence;
+  roiConservative: number;
+  roiCapped: boolean;
+  disclaimer: string;
+  closingAt: string | null;
+  sourceUrl: string | null;
+}
+
+export function digestDateString(d = new Date()): string {
+  // en-CA renders YYYY-MM-DD; PT is the digest's home timezone (6am/5pm sends).
+  return d.toLocaleDateString("en-CA", { timeZone: "America/Los_Angeles" });
+}
+
+export function currentSlot(d = new Date()): DigestSlot {
+  const hour = Number(
+    d.toLocaleString("en-US", { timeZone: "America/Los_Angeles", hour: "numeric", hour12: false }),
+  );
+  return hour < 12 ? "AM" : "PM";
+}
+
+export function toSnapshotDeals(deals: HotDeal[]): SnapshotDeal[] {
+  return deals.map((d, i) => ({
+    rank: i + 1,
+    title: d.listing.title,
+    source: d.listing.source,
+    locationCity: d.listing.locationCity,
+    locationState: d.listing.locationState,
+    currentBid: d.currentBid,
+    recMax: d.recMax,
+    expectedSaleLow: d.expectedSaleLow,
+    confidence: d.confidence,
+    roiConservative: d.roiConservative,
+    roiCapped: d.roiCapped,
+    disclaimer: d.disclaimer,
+    closingAt: d.closingAt ? new Date(d.closingAt).toISOString() : null,
+    sourceUrl: d.listing.sourceUrl,
+  }));
+}
+
+/** Persist a snapshot from already-fetched deals (used at digest send time). */
+export async function persistDigestSnapshot(slot: DigestSlot, deals: HotDeal[], date = digestDateString()) {
+  const snapshotDeals = toSnapshotDeals(deals);
+  const dealsJson = snapshotDeals as unknown as Prisma.InputJsonValue;
+  return prisma.digestSnapshot.upsert({
+    where: { date_slot: { date, slot } },
+    create: { date, slot, dealsJson, dealCount: snapshotDeals.length },
+    update: { dealsJson, dealCount: snapshotDeals.length },
+  });
+}
+
+/** Standalone capture: run the honesty-gated Top-10 and freeze it. */
+export async function captureDigestSnapshot(slot: DigestSlot = currentSlot()) {
+  const deals = await findHotDeals({ limit: 10 });
+  return persistDigestSnapshot(slot, deals);
+}
+
+// ── read side for the /deals archive pages ─────────────────────────────────
+
+export interface DigestEdition {
+  date: string;
+  slot: DigestSlot;
+  dealCount: number;
+  createdAt: Date;
+  deals: SnapshotDeal[];
+}
+
+function parseDeals(dealsJson: Prisma.JsonValue): SnapshotDeal[] {
+  return Array.isArray(dealsJson) ? (dealsJson as unknown as SnapshotDeal[]) : [];
+}
+
+export async function listDigestEditions(limit = 60): Promise<DigestEdition[]> {
+  const rows = await prisma.digestSnapshot.findMany({
+    orderBy: [{ date: "desc" }, { slot: "asc" }],
+    take: limit,
+  });
+  return rows.map((r) => ({
+    date: r.date,
+    slot: r.slot,
+    dealCount: r.dealCount,
+    createdAt: r.createdAt,
+    deals: parseDeals(r.dealsJson),
+  }));
+}
+
+export async function getDigestEditionsForDate(date: string): Promise<DigestEdition[]> {
+  const rows = await prisma.digestSnapshot.findMany({
+    where: { date },
+    orderBy: { slot: "asc" },
+  });
+  return rows.map((r) => ({
+    date: r.date,
+    slot: r.slot,
+    dealCount: r.dealCount,
+    createdAt: r.createdAt,
+    deals: parseDeals(r.dealsJson),
+  }));
+}
+
+export async function listDigestDates(): Promise<{ date: string; lastModified: Date }[]> {
+  const rows = await prisma.digestSnapshot.findMany({
+    select: { date: true, createdAt: true },
+    orderBy: { date: "desc" },
+  });
+  const byDate = new Map<string, Date>();
+  for (const r of rows) {
+    const prev = byDate.get(r.date);
+    if (!prev || r.createdAt > prev) byDate.set(r.date, r.createdAt);
+  }
+  return [...byDate.entries()].map(([date, lastModified]) => ({ date, lastModified }));
+}
diff --git a/src/lib/send-digest.ts b/src/lib/send-digest.ts
index 0cef9e8..87d6761 100644
--- a/src/lib/send-digest.ts
+++ b/src/lib/send-digest.ts
@@ -1,6 +1,7 @@
 import { prisma } from "@/lib/db";
 import { findHotDeals, type HotDeal } from "@/lib/hot-deals";
 import { sendNewsletterEmail, newsletterSendMode } from "@/lib/newsletter";
+import { persistDigestSnapshot, currentSlot } from "@/lib/digest-snapshot";
 
 // Digest send-to-list. Renders the same honesty-gated Top-10 the paid alerts
 // use and fans it out to every CONFIRMED subscriber, each with their own
@@ -55,6 +56,11 @@ export interface DigestSendResult {
 export async function sendDigestToSubscribers(opts: { baseUrl?: string; limit?: number } = {}): Promise<DigestSendResult> {
   const baseUrl = opts.baseUrl || process.env.NEWSLETTER_BASE_URL || "http://localhost:3737";
   const deals = await findHotDeals({ limit: opts.limit ?? 10 });
+
+  // Freeze this edition for the public /deals archive — even if every send
+  // below fails, the archive records what the digest showed at send time.
+  await persistDigestSnapshot(currentSlot(), deals);
+
   const subscribers = await prisma.subscriber.findMany({ where: { status: "CONFIRMED" } });
 
   let sent = 0;

← 8d98349 Contrarian fixes: digest send-to-list engine (TEST-logged),  ·  back to Govarbitrage  ·  Public SEO deal archive: /deals index + /deals/[date] teaser 5e22c50 →