From ab145327677d3dd368c8e55416a31048b67dc1ca Mon Sep 17 00:00:00 2001 From: New1Direction <285551516+New1Direction@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:42:34 -0600 Subject: [PATCH] feat(impulse): Calculator Pro ($4.99 portfolio tool) + guide/Pro CTAs on /tools /drops /guides --- worker/src/articles.ts | 2 + worker/src/digital.ts | 129 +++++++++++++++++++++++++++++++++--- worker/src/drops_seo.ts | 4 ++ worker/src/index.ts | 17 +++++ worker/src/tools.ts | 9 +++ worker/test/digital.test.ts | 29 ++++++++ 6 files changed, 181 insertions(+), 9 deletions(-) diff --git a/worker/src/articles.ts b/worker/src/articles.ts index 80b1e42..8f0bdbd 100644 --- a/worker/src/articles.ts +++ b/worker/src/articles.ts @@ -3754,6 +3754,8 @@ ${uiNav(origin)} ${relatedHtml(a)} +

📘 The Drop-Day Playbook — the restock-day checklist ($2.99)

+ diff --git a/worker/src/digital.ts b/worker/src/digital.ts index adf6439..6b2ee1b 100644 --- a/worker/src/digital.ts +++ b/worker/src/digital.ts @@ -18,14 +18,17 @@ type Env = { KEYS: KVNamespace; STRIPE_SECRET_KEY?: string; STRIPE_PRICE_GUIDE?: string; + STRIPE_PRICE_CALCPRO?: string; }; -type ProductId = "guide"; +type ProductId = "guide" | "calcpro"; interface ProductDef { priceEnv: keyof Env; price: string; // display only title: string; + tagline: string; // sales-page sub-headline + bullets: string[]; // "what's inside" render: () => string; // the gated content (HTML body) } @@ -34,8 +37,30 @@ const PRODUCTS: Record = { priceEnv: "STRIPE_PRICE_GUIDE", price: "$2.99", title: "The Pokémon Drop-Day Playbook", + tagline: "Stop paying resellers. The exact restock-day checklist to land Pokémon sealed product at retail — on your phone, no app, no subscription.", + bullets: [ + "The 90-second pre-drop checklist", + "Store-by-store setup (Pokémon Center, Walmart, Target, Best Buy)", + "Retail-vs-resale margin math (skip the markup)", + "Is-it-worth-grading decision framework", + "Restock timing + scalper-trap avoidance", + ], render: () => GUIDE_HTML, }, + calcpro: { + priceEnv: "STRIPE_PRICE_CALCPRO", + price: "$4.99", + title: "Pro Portfolio Calculator", + tagline: "The free calculator does one item. This does your whole collection — track every sealed box and card, see your total savings and resale profit at a glance. Saves on your device, no account.", + bullets: [ + "Add unlimited items — your whole binder/haul", + "Running totals: spend, savings, resale profit, blended margin", + "Saves to your device automatically (no login)", + "Print or screenshot a clean summary", + "Works on mobile, reopen anytime from your private link", + ], + render: () => CALCPRO_HTML, + }, }; const esc = (s: string) => @@ -117,6 +142,96 @@ If that number is close to retail, just buy retail and skip the markup. If a sea
That's the whole method. Save this page or print it (your browser's Share → Print). You can reopen it anytime from the link below.
`; +// Calculator Pro — a self-contained multi-item portfolio tool. Lives entirely on +// the unlock page (the free /tools calculator is untouched). State persists in +// localStorage, so it works with no account and survives reloads. +const CALCPRO_HTML = ` + +

Pro Portfolio Calculator

+

Add every sealed box and card you're tracking. It keeps a running tally of what you'd spend at retail, what you save versus resale, and your profit if you flip — saved right on this device.

+ +
+
+
+
+
+
+
+
+
+ + + + +

No items yet — add your first above. Savings/profit = (resale − retail) × qty. Resale prices move, so update them as you go.

+ + +`; + // ------------------------------- handlers ------------------------------- // POST /api/v1//checkout — no email pre-form (Stripe collects it); @@ -179,7 +294,7 @@ export async function digitalRead(c: Context<{ Bindings: Env }>, product: Produc const raw = t ? await c.env.KEYS.get(`dlg:${t}`) : null; let ok = false; try { ok = !!raw && JSON.parse(raw).product === product; } catch {} - if (!ok) return c.html(shell(def.title, `

${esc(def.title)}

That access link isn't valid. Get the playbook

`), 403); + if (!ok) return c.html(shell(def.title, `

${esc(def.title)}

That access link isn't valid. Get ${esc(def.title)}

`), 403); return c.html(renderUnlocked(def, product, t)); } @@ -194,16 +309,12 @@ export function digitalSalesHtml(origin: string, product: ProductId, canceled: b if (!def) return shell("Not found", "

Not found

"); const body = `

${esc(def.title)}

-

Stop paying resellers. The exact restock-day checklist to land Pokémon sealed product at retail — on your phone, no app, no subscription.

-${canceled ? `
No worries — checkout was canceled. The playbook's still here when you want it.
` : ""} +

${esc(def.tagline)}

+${canceled ? `
No worries — checkout was canceled. It's still here when you want it.
` : ""}

What's inside

    -
  • The 90-second pre-drop checklist
  • -
  • Store-by-store setup (Pokémon Center, Walmart, Target, Best Buy)
  • -
  • Retail-vs-resale margin math (skip the markup)
  • -
  • Is-it-worth-grading decision framework
  • -
  • Restock timing + scalper-trap avoidance
  • +${def.bullets.map((b) => `
  • ${esc(b)}
  • `).join("\n")}
diff --git a/worker/src/drops_seo.ts b/worker/src/drops_seo.ts index 9f1edcf..ff5759b 100644 --- a/worker/src/drops_seo.ts +++ b/worker/src/drops_seo.ts @@ -1447,6 +1447,10 @@ ${localizable ? hreflangTags(origin, p.slug) + "\n" : ""} + 📘 The Drop-Day Playbook — land restocks at retail ($2.99) + +
${localizable ? langSwitcher(origin, p.slug, useLang) : ""} diff --git a/worker/src/index.ts b/worker/src/index.ts index 3b0300e..45a2ee6 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -217,6 +217,23 @@ app.get("/guide/read", async (c) => { const { digitalRead } = await import("./digital"); return digitalRead(c as any, "guide"); }); +// Calculator Pro ($4.99) — second impulse product on the same engine. +app.get("/calcpro", async (c) => { + const { digitalSalesHtml } = await import("./digital"); + return c.html(digitalSalesHtml(new URL(c.req.url).origin, "calcpro", c.req.query("canceled") === "1")); +}); +app.post("/api/v1/calcpro/checkout", async (c) => { + const { createDigitalCheckout } = await import("./digital"); + return createDigitalCheckout(c as any, "calcpro"); +}); +app.get("/calcpro/unlock", async (c) => { + const { digitalUnlock } = await import("./digital"); + return digitalUnlock(c as any, "calcpro"); +}); +app.get("/calcpro/read", async (c) => { + const { digitalRead } = await import("./digital"); + return digitalRead(c as any, "calcpro"); +}); app.get("/api/v1/health", (c) => c.json({ ok: true, version: "0.1.0", env: c.env.ENVIRONMENT }) diff --git a/worker/src/tools.ts b/worker/src/tools.ts index 4b4998c..c81bac0 100644 --- a/worker/src/tools.ts +++ b/worker/src/tools.ts @@ -457,6 +457,15 @@ ${alternates(origin, "calc")}
+
+

Do your whole collection — Pro Portfolio Calculator

+

The free tool above does one item. Pro tallies every sealed box and card you own — total spend, savings, and resale profit at a glance, saved on your device. One-time, no app.

+ +
+

${esc(c.ctaH)}

${esc(c.ctaP)}

diff --git a/worker/test/digital.test.ts b/worker/test/digital.test.ts index 645c4db..0bc3242 100644 --- a/worker/test/digital.test.ts +++ b/worker/test/digital.test.ts @@ -52,4 +52,33 @@ describe("digital impulse products", () => { expect(html).toContain("Get instant access"); expect(html).toContain("no signup"); }); + + it("calcpro checkout is fail-closed without its price (503)", async () => { + const c = makeCtx({ env: envMock({ STRIPE_SECRET_KEY: "sk_test" }), method: "POST", body: {}, url: "https://wmcp.sh/api/v1/calcpro/checkout" }); + const r: any = await createDigitalCheckout(c, "calcpro"); + expect(r.status).toBe(503); + expect(r.body.error).toBe("calcpro_not_configured"); + }); + + it("calcpro sales page renders at \$4.99 with its own copy", () => { + const html = digitalSalesHtml("https://wmcp.sh", "calcpro", false); + expect(html).toContain("$4.99"); + expect(html).toContain('data-product="calcpro"'); + expect(html).toContain("Portfolio"); + }); + + it("calcpro unlock content serves the portfolio tool for a valid token", async () => { + const env = envMock(); + await env.KEYS.put("dlg:cptoken", JSON.stringify({ product: "calcpro", ts: 1 })); + const c = makeCtx({ env, method: "GET", query: { t: "cptoken" }, url: "https://wmcp.sh/calcpro/read?t=cptoken" }); + const r: any = await digitalRead(c, "calcpro"); + expect(r.status).toBe(200); + expect(String(r.body)).toContain("Pro Portfolio Calculator"); + // a guide token must not open calcpro + const env2 = envMock(); + await env2.KEYS.put("dlg:gtok", JSON.stringify({ product: "guide", ts: 1 })); + const c2 = makeCtx({ env: env2, method: "GET", query: { t: "gtok" }, url: "https://wmcp.sh/calcpro/read?t=gtok" }); + const r2: any = await digitalRead(c2, "calcpro"); + expect(r2.status).toBe(403); + }); });