From b6678cf591180ac030982b265fe03f86360b6316 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 23 Aug 2026 20:24:42 -0700 Subject: [PATCH] DC-061: remove superseded status/pricing/index.html and obsolete pricing-page-catalog test --- BACKLOG.md | 8 +- .../billing/pricing-page-catalog.test.js | 134 -------------- status/pricing/index.html | 165 ------------------ 3 files changed, 4 insertions(+), 303 deletions(-) delete mode 100644 dashcaddy-api/__tests__/billing/pricing-page-catalog.test.js delete mode 100644 status/pricing/index.html diff --git a/BACKLOG.md b/BACKLOG.md index 39c6ba9..b63d36f 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -353,10 +353,10 @@ Sami explicitly stated he wants email auth as an OPTION alongside TOTP, not a re - **owner:** hermes ### DC-061: Remove superseded status/pricing/index.html — dead weight since dashcaddy.net pricing page -- **status:** in-progress -- **owner:** hermes -- **details:** The in-repo `status/pricing/index.html` is served by the status.sami SPA catch-all but duplicates the canonical pricing page now living on the dedicated Next.js marketing site at `dashcaddy.net/pricing`. It has 0 Stripe refs in the current codebase (the marketing site handles checkout). Remove the file to avoid confusion and reduce surface area. No Caddy config change needed — the SPA fallback will serve index.html for /pricing, which is correct behavior (dashboard app handles unknown routes). -- **impact:** Cleaner repo, single source of truth for pricing. Eliminates a stale page that could mislead users who hit status.sami/pricing directly. +- **status:** done +- **owner:** hermes +- **details:** The in-repo `status/pricing/index.html` was served by the status.sami SPA catch-all but duplicated the canonical pricing page now living on the dedicated Next.js marketing site at `dashcaddy.net/pricing`. It had 0 Stripe refs in the current codebase (the marketing site handles checkout). Removed the file and its parent directory. Also deleted the obsolete test `__tests__/billing/pricing-page-catalog.test.js` that validated the now-removed page against the catalog — pricing-page/catalog consistency is now verified by the dashcaddy.net marketing site's own test suite. No Caddy config change needed — the SPA fallback serves index.html for /pricing, which is correct behavior (dashboard app handles unknown routes). +- **result:** Removed `status/pricing/index.html` and `status/pricing/` directory. Deleted `__tests__/billing/pricing-page-catalog.test.js` (9 tests). All 2854 remaining tests pass, zero new ESLint warnings. - **details:** Two static pages at `/legal/tos` and `/legal/privacy`. ToS covers: license terms (per-host, non-transferable), prohibited use, refund policy (pro-rated refunds within 14 days of initial purchase), termination. Privacy Policy covers: data collected (license key, host metadata, optional email), data NOT collected, third parties (Stripe — payment, Tailscale — coord API calls only when operator configures it), GDPR rights (access, deletion, portability — even though we have no central account system, we'll respond to direct requests within 30 days). No SOC2/HIPAA — that's a v2 conversation. - **impact:** Legal compliance for taking money. Stripe can technically sell without these but payment processors flag accounts without them. - **prerequisite:** None. diff --git a/dashcaddy-api/__tests__/billing/pricing-page-catalog.test.js b/dashcaddy-api/__tests__/billing/pricing-page-catalog.test.js deleted file mode 100644 index bae3eaf..0000000 --- a/dashcaddy-api/__tests__/billing/pricing-page-catalog.test.js +++ /dev/null @@ -1,134 +0,0 @@ -/** - * DC-057 pricing-page catalog consistency test. - * - * The pricing page at status/pricing/index.html hard-codes the 4 product - * IDs, prices, and labels. This test asserts that those hard-coded values - * exactly match the catalog in src/billing/catalog.js — preventing drift - * between the two sources. - * - * If a new tier is added to the catalog, this test will fail until the - * pricing page is updated. If the pricing page is updated, the catalog - * must change in lockstep (or this test fails the other way). - */ - -const fs = require('fs'); -const path = require('path'); -const catalog = require('../../src/billing/catalog'); - -const PRICING_PAGE_PATH = path.join(__dirname, '..', '..', '..', 'status', 'pricing', 'index.html'); - -function extractTiersFromPage(html) { - // Extract each `
` block, then - // pull out the dollar amount in the `
` element and - // the durationDays from the "N-day Pro license" string. The regex is - // anchored on the tier-class open + the matching buy-btn close so we - // capture the full body of each tier card regardless of how many inner - // divs it has. - const tierRe = /
([\s\S]*?)]*class="buy-btn"[^>]*>\s*Buy/g; - const tierBlocks = [...html.matchAll(tierRe)]; - return tierBlocks.map(([, productId, body]) => { - const priceMatch = body.match(/
\$(\d+)<\/div>/); - const durMatch = body.match(/(\d+)-day Pro license/); - return { - productId, - priceDollars: priceMatch ? parseInt(priceMatch[1], 10) : null, - durationDays: durMatch ? parseInt(durMatch[1], 10) : null, - }; - }); -} - -/** - * Extract the HTML body for one specific tier (from open div through the - * buy-btn). Used by per-tier assertions that must NOT bleed across cards. - */ -function extractTierBody(html, productId) { - const re = new RegExp( - `
([\\s\\S]*?)]*class="buy-btn"[^>]*>\\s*Buy`, - 'i' - ); - const m = html.match(re); - return m ? m[1] : null; -} - -describe('pricing page <-> catalog consistency (DC-057)', () => { - let html; - let pageTiers; - - beforeAll(() => { - html = fs.readFileSync(PRICING_PAGE_PATH, 'utf8'); - pageTiers = extractTiersFromPage(html); - }); - - test('pricing page exists and is readable', () => { - expect(html.length).toBeGreaterThan(1000); - expect(pageTiers.length).toBeGreaterThan(0); - }); - - test('every catalog product is rendered on the pricing page', () => { - const catalogIds = catalog.PRODUCTS.map((p) => p.id).sort(); - const pageIds = pageTiers.map((t) => t.productId).sort(); - expect(pageIds).toEqual(catalogIds); - }); - - test('every pricing-page productId appears in the catalog', () => { - for (const tier of pageTiers) { - const product = catalog.getProduct(tier.productId); - expect(product).not.toBeNull(); - } - }); - - test('pricing-page dollar amounts match catalog amountCents', () => { - for (const tier of pageTiers) { - const product = catalog.getProduct(tier.productId); - const expectedDollars = product.amountCents / 100; - expect(tier.priceDollars).toBe(expectedDollars); - } - }); - - test('pricing-page duration strings match catalog durationDays', () => { - for (const tier of pageTiers) { - const product = catalog.getProduct(tier.productId); - expect(tier.durationDays).toBe(product.durationDays); - } - }); - - test('catalog and pricing page agree on price label (scoped per tier card)', () => { - // Per-tier priceLabel assertion: each tier card must include its - // own catalog.priceLabel. A swap or misplaced label fails immediately - // because the assertion checks the tier's own HTML body, not the page. - for (const tier of pageTiers) { - const product = catalog.getProduct(tier.productId); - const body = extractTierBody(html, tier.productId); - expect(body).not.toBeNull(); - // The priceLabel appears in the price div of THIS tier only, - // immediately followed by the closing
+ the duration block. - const labelRegex = new RegExp(`
\\s*\\${product.priceLabel}\\s*
\\s*
{ - // DC-057 acceptance: locked spec is ONE-TIME 30/90/180/365-day licenses - // at $20/$50/$70/$99. The old monthly/annual subscription toggle - // would contradict the spec. - expect(html).not.toMatch(/period-monthly|period-annual/); - expect(html).not.toMatch(/Subscribe to Pro/); - }); - - test('pricing page references the success-page endpoint', () => { - // The success URL is constructed server-side in stripe-client.js - // (${origin}/billing/success?session_id=...). The pricing page itself - // doesn't need to embed it — but the FOOTER must reference it so the - // customer knows where to go after Stripe redirects. - expect(html.toLowerCase()).toContain('after payment'); - expect(html).toContain('/admin/license'); - expect(html).toContain('/api/v1/billing/checkout'); - }); - - test('success page (status/billing/success.html) exists and references the lookup endpoint', () => { - const successPath = path.join(__dirname, '..', '..', '..', 'status', 'billing', 'success.html'); - const successHtml = fs.readFileSync(successPath, 'utf8'); - expect(successHtml).toContain('/api/v1/billing/lookup/'); - expect(successHtml.length).toBeGreaterThan(1000); - }); -}); diff --git a/status/pricing/index.html b/status/pricing/index.html deleted file mode 100644 index 332f26b..0000000 --- a/status/pricing/index.html +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - DashCaddy Pricing — Free & Pro - - - - - -
-
DashCaddy
-

Simple pricing. Self-hosted either way.

-

DashCaddy runs on your hardware. Free is enough for most homelabs. Pro unlocks multi-host fleets, public sharing, and email support.

- -
-
-

Free

-
$0/forever
-
Unlimited duration
-
    -
  • Single host
  • -
  • Up to 3 users
  • -
  • TOTP login (single-user)
  • -
  • Docker / Caddy / DNS management
  • -
  • Community support (GitHub issues)
  • -
- -
- -
-

1 month

-
$20
-
30-day Pro license
-
    -
  • Unlimited users
  • -
  • Public share links
  • -
  • Tailscale-mediated share
  • -
  • Email support
  • -
- -
- -
-

3 months

-
$50
-
90-day Pro license (17% off)
-
    -
  • Unlimited users
  • -
  • Public share links
  • -
  • Tailscale-mediated share
  • -
  • Email support
  • -
- -
- -
-

6 months

-
$70
-
180-day Pro license (42% off)
-
    -
  • Unlimited users
  • -
  • Public share links
  • -
  • Tailscale-mediated share
  • -
  • Email support
  • -
- -
- -
-

12 months

-
$99
-
365-day Pro license (59% off)
-
    -
  • Unlimited users
  • -
  • Public share links
  • -
  • Tailscale-mediated share
  • -
  • Email support
  • -
- -
-
- - -

Payments are processed by Stripe. Your card details never touch DashCaddy servers. After payment you receive a Pro license code on the success page AND by email — keep it safe; you'll paste it into /admin/license on your host. 14-day pro-rated refunds. By purchasing you agree to the Terms of Service and Privacy Policy.

-
- - - -