diff --git a/src/pages/MarketingPage.test.tsx b/src/pages/MarketingPage.test.tsx index 1554616..454534e 100644 --- a/src/pages/MarketingPage.test.tsx +++ b/src/pages/MarketingPage.test.tsx @@ -121,6 +121,46 @@ describe('MarketingPage — claim consistency (T18 P1-4 / P1-6)', () => { expect(text).toMatch(/~60s/) }) + // TEAM-GATE reconciliation (2026-06-08): the homepage Team tile's CTA used + // to point at the self-serve /app/checkout?plan=team path — a tier the + // server-side gate rejects with 400 tier_not_yet_available, so the homepage + // was marketing a plan the platform can't actually sell. Team is + // contact-sales only until its delivery is proven built (CEO TEAM-GATE + // directive). These guards pin the homepage Team CTA to a contact-sales + // mailto and forbid the self-serve checkout link from creeping back, AND + // confirm Team is NOT badged "Most popular" (Pro is the highlighted tier). + it('homepage Team tile CTA is a contact-sales mailto, NOT a self-serve checkout', () => { + const { container } = render( + + + , + ) + const teamCta = findAnchorByText('Contact sales →') + expect(teamCta).not.toBeNull() + expect(teamCta!.getAttribute('href')).toContain('mailto:sales@instanode.dev') + // The retired self-serve Team checkout link must not exist anywhere on the page. + const hrefs = Array.from(container.querySelectorAll('a')).map((a) => a.getAttribute('href') ?? '') + expect(hrefs.some((h) => h.includes('plan=team'))).toBe(false) + // And the old buyable label must be gone. + expect(findAnchorByText('Start team →')).toBeNull() + }) + + it('only Pro is badged "Most popular" — Team must not be highlighted/badged as buyable', () => { + const { container } = render( + + + , + ) + const badges = Array.from(container.querySelectorAll('.mkt-featured-flag')).filter( + (el) => (el.textContent ?? '').trim() === 'Most popular', + ) + expect(badges.length).toBe(1) + // The badge sits inside the Pro card, not the Team card. + const card = badges[0].closest('.mkt-price-card') + expect(card?.textContent).toContain('Pro') + expect(card?.textContent).not.toContain('Team') + }) + // BIZ-3 (2026-05-29): the landing pricing tile shipped "1 small deployment" // and "10 medium deployments" copy from the days when /deploy/new had a // deployment_size field. The backend dropped that field; marketing diff --git a/src/pages/MarketingPage.tsx b/src/pages/MarketingPage.tsx index 9d3f5e6..365bbf5 100644 --- a/src/pages/MarketingPage.tsx +++ b/src/pages/MarketingPage.tsx @@ -94,6 +94,13 @@ const ROUTES = { playground: '#playground', } as const +// Team is sales-assisted only (TEAM-GATE, 2026-06-04 CEO directive): it is NOT +// self-serve and must not route to /app/checkout until its dedicated-infra +// delivery is proven built — the server-side gate rejects a Team checkout with +// 400 tier_not_yet_available. The Team CTA points here, matching the +// contact-sales action PricingPage.tsx and BillingPage.tsx already use. +const SALES_MAILTO_TEAM = 'mailto:sales@instanode.dev?subject=Team%20plan%20enquiry' + type Service = { id: 'pg' | 'rd' | 'mg' | 'vc' | 'qu' | 'st' | 'wh' | 'dp' name: string @@ -203,12 +210,13 @@ const PLANS: Plan[] = [ // Team tier — $199/mo. strict-80% margin redesign (2026-06-05): every // Team limit is now a finite plans.yaml cap (was -1/unlimited). Above // these caps = Enterprise (contact sales). - // NOTE (pre-existing, out of scope here): this CTA still points at the - // self-serve /app/checkout?plan=team path, which the server-side Team - // gate rejects with 400 tier_not_yet_available. The PricingPage Team CTA - // already uses a contact-sales mailto. This MarketingPage CTA should be - // reconciled with the gate in a follow-up (not changed here — the - // strict-margin task is explicitly copy-only and must not touch the gate). + // TEAM-GATE reconciliation (2026-06-08): the CTA previously pointed at the + // self-serve /app/checkout?plan=team path, which the server-side Team gate + // rejects with 400 tier_not_yet_available — so the homepage was offering a + // tier the platform can't sell. Now reconciled with PricingPage.tsx + + // BillingPage.tsx: Team is contact-sales only until its delivery is proven + // built. Do NOT re-point this at /app/checkout. Ref TEAM-GATE directive + // (2026-06-04) + docs/sessions/2026-06-04/TEAM-PLAN-GATE-AND-BUILD.md. id: 'team', name: 'Team', tagline: 'For the engineering org. Dedicated infra, RBAC + audit, with SSO & SLA on the way.', @@ -221,8 +229,8 @@ const PLANS: Plan[] = [ 'Need more? Enterprise — contact sales', ], cta: { - label: 'Start team →', - href: '/app/checkout?plan=team&frequency=monthly', + label: 'Contact sales →', + href: SALES_MAILTO_TEAM, variant: 'secondary', }, }, @@ -311,7 +319,7 @@ export function MarketingPage() {

Postgres, Redis, MongoDB, vectors, queues, storage, webhooks, and deployments.{' '} Provisioned in <2 seconds.{' '} - No signup, no Docker, no waitlist. + Self-serve from the first call to a paid plan — no signup, no Docker, no sales call.