# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] ### Added - **Double opt-in email verification for quote requests** — guest quote submissions now require email verification before the lead goes live; verification click also creates a user account and logs them in automatically (GDPR-friendly consent trail) - `GET /leads/verify` route — validates token, activates lead (`pending_verification` → `new`, sets `verified_at`), logs user in, sends admin notification and welcome email - `send_quote_verification` worker task — branded verification email with project details and "Verify & Activate Quote" CTA button (DEBUG mode prints link to console) - `quote_verify_sent.html` template — "Check your email" page shown after guest quote submission - Migration 0006 — adds `verified_at TEXT` column to `lead_requests` - 9 new tests in `TestQuoteVerification` class covering the full verification flow, expired tokens, duplicate verification, and user creation ### Changed - **Inline CTA full copy** — mobile/narrow-screen inline quote CTA now matches sidebar: "Next Step" label, full title, description, 4 checkmark benefits, "Get Supplier Quotes" button, and "Takes ~2 minutes" hint - **Signup bar simplified** — removed `×` close button from guest signup bar; now a non-dismissable nudge (still only shown on results tabs via JS) - **Investment tab narrower** — CAPEX tab content constrained to 800px max-width so 3-column card grid, table, and chart don't stretch across full 1100px on wide screens ### Changed - **Quote form → standalone 9-step HTMX wizard** — extracted "Get Quotes" from planner Step 5 into a standalone multi-step wizard at `/leads/quote` using server-rendered HTMX partials; each step validates server-side and swaps via `hx-post`/`hx-get` with OOB dot progress updates; accumulated state passed forward as hidden JSON field (no JS state management) - **Planner reduced to 4 steps** — removed embedded quote form (Step 5) from planner wizard; Step 4 "Get Quotes →" now navigates to `/leads/quote` with pre-filled params (venue, courts, glass, lighting, country, budget) - **Planner sidebar CTA** — "Get Supplier Quotes" button now links to standalone quote wizard instead of scrolling to embedded Step 5; sidebar now visible on all tabs including assumptions (was results-only) - **Sticky wizard nav** — planner preview bar (CAPEX/CF/IRR) and back/next buttons now stick to the bottom of the viewport so users don't have to scroll to navigate between steps - **Mobile quote CTA** — inline "Get Quotes" card shown below main content on screens narrower than 1400px (where the fixed sidebar is hidden) - **Step 4 → "Show Results"** — final planner wizard step now says "Show Results" instead of "Get Quotes" since quote flow is a separate standalone wizard - **Removed "2-5 suppliers" cap language** — replaced specific supplier count promises with "matched suppliers" across landing page, supplier FAQ, planner sidebar, and quote form privacy box ### Removed - Inline quote form from planner (Step 5 HTML, `#wizSuccess`, hidden inputs) - `populateWizAutoFill()`, `submitQuote()`, `COUNTRY_NAMES` from planner.js - `__PADELNOMICS_QUOTE_URL__` JS variable from planner template - Step 5 scoped CSS (~155 lines): `#wizQuoteForm`, `.wiz-autofill-summary`, `.wiz-input`, `.wiz-privacy-box`, `.consent-group`, `.wiz-success`, `.wiz-signup-nudge`, `.wiz-checkbox-label` ### Added - **Supplier tier system** — Migration 0005 adds `tier` (free/growth/pro), `logo_url`, `is_verified`, `highlight`, `sticky_until`, `sticky_country` columns to suppliers table for paid listing support - **HTMX live search** — directory search input and filters update results via `hx-get` with 300ms debounce; new `/directory/results` endpoint returns swappable partial - **Directory card tiers** — three-tier card design: Pro (green border, logo, verified badge, website), Growth (description, blue badge), Free (muted, unverified, "Is this your company?" CTA); sticky/featured suppliers pinned to top with blue border - **Supplier pricing page** — `/suppliers/` now shows Growth (€149/mo) and Pro (€399/mo) plan cards with feature lists, boost add-ons grid (Logo, Highlight, Verified Badge, Sticky Top, Newsletter Feature), updated FAQ - **Mandatory form fields** — country, timeline, and stakeholder_type now required on quote request form with server-side 422 validation - **Validation test** — `test_quote_validation_rejects_missing_fields` verifies server returns 422 JSON errors for missing mandatory fields ### Changed - **Nav redesign** — Zillow-style sticky nav with backdrop-blur: demand-side links (Planner, Directory) left, supply-side (For Suppliers, Help) after separator, Sign In right; removed "Get Started Free" button - **CTA text sweep** — "Get Matched" → "Get Quotes" across planner, landing, and lead forms; removed all "Free" qualifiers from CTAs and badges - **ROI calculator fix** — realistic cost model: €35K/court (was €25K), staff costs, €8/sqm rent (was €4); payback and ROI now based on total investment (was equity only); defaults: €40/hr rate, 35% utilization; shows ~3.9yr payback, ~26% ROI (was 0.1yr/1255%) - **Directory route refactor** — shared `_build_directory_query()` helper with tier-based SQL ordering (sticky → pro → growth → free → alphabetical) ### Added - **Supplier directory** — public searchable directory at `/directory/` with 279 padel court suppliers across 31 countries; FTS5 full-text search, country and category filters, pagination, category-colored badges, unclaimed listing model - **Supplier landing page** — `/suppliers/` marketing page for suppliers: hero, how-it-works steps, example lead preview, FAQ, "Claim Your Listing" CTAs - **Migration 0004** — creates `suppliers` table with FTS5 virtual table, content-sync triggers, and seeds 279 suppliers from PadelDirectory.md - **Quick ROI calculator** — landing page now features an interactive 3-slider calculator (courts, rate, utilization) showing investment, monthly cash flow, payback period, and annual ROI in real time - **Supplier matching section** — landing page "Find the Right Suppliers" section with 3-step flow and link to directory - **FAQ accordion** — landing page FAQ covering planner features, signup requirements, supplier matching, directory pricing, and projection accuracy ### Changed - **Visual refresh** — adopted React prototype color palette and aesthetic site-wide: royal blue primary (#1D4ED8), green (#16A34A), gold (#D97706); elevated cards with soft shadows, rounder corners (rounded-2xl cards, rounded-xl buttons/inputs), frosted-glass planner nav, highlighted CTA regions with blue-tinted backgrounds, pill-shaped toggle/filter controls, polished buttons with colored shadows, stronger hover lift on directory cards - **Landing hero redesigned** — two-column layout with headline + CTAs on left and interactive Quick ROI calculator on right (matching React prototype); green badge pill, feature check bullets, "Open Full Planner" CTA inside calculator card; responsive single-column on mobile - **Landing page redesigned** — replaced screenshot card with Quick ROI calculator; added supplier matching section, FAQ, and live supplier stats; CTAs renamed "Open the Planner — Free"; Build journey card updated with live supplier/country counts - **Navbar** — Planner and Directory links now visible for all users (not just logged-in); footer updated with Directory and For Suppliers links - **Planner CTAs** — removed sticky "Get Builder Quotes" footer bar; CAPEX and Returns tab CTAs now navigate to wizard step 5 (integrated lead qualifier) instead of redirecting to standalone `/leads/quote` form - **Sitemap** — added `/planner/`, `/directory/`, and `/suppliers/` URLs ### Changed - **Planner wizard** — Assumptions tab reorganized into 5 guided steps (Your Venue → Pricing → Costs → Finance → Get Matched) with live preview bar and step navigation; reduces cognitive load from 60 sliders to ~6-15 per step - **Integrated lead qualifier** — Step 5 "Get Matched" embeds the supplier quote form directly in the planner; auto-fills venue, courts, glass, lighting, country, budget from planner state; submits inline via fetch - **JSON quote endpoint** — `POST /leads/quote` now accepts `application/json` and returns `{"ok": true, "heat": "..."}` for inline planner submissions; standalone HTML form unchanged ### Added — Phase 0 Round 2: Polish & Country-Specific Calculator - **Country-specific calculator** — `country` selector (DE/ES/IT/FR/NL/SE/UK/Other) and `permitsCompliance` CAPEX item for Indoor Rent and Outdoor scenarios; country presets auto-adjust permit costs - **Permits & Compliance** — new CAPEX line item for building permits, noise studies, and regulatory compliance (default €12K for Germany); excluded from Indoor Buy where Planning + Permits already covers this - **Quote form redesign** — elevated white card on gradient background, green gradient CTA buttons, progress labels (Project/Details/Contact), privacy info box, mandatory consent checkbox - **Project phase** (replaces location_status options) — 7-stage progression: still searching → location found → converting existing → lease signed → permit not filed → permit pending → permit granted; updated heat scoring - **Stakeholder type** field — "You are..." selector (Entrepreneur, Tennis Club, Municipality, Developer, Operator, Architect) with `stakeholder_type` DB column (migration 0003) - **Build context** — added "Need Help Finding a Venue / Land" (`venue_search`) option - **Quote submitted page redesign** — "You're matched!" flow with next-steps timeline, email confirmation box, and signup CTA for guests - **Migration 0003** — adds `stakeholder_type TEXT` column to `lead_requests` ### Changed - **Landing page** — replaced teaser calculator with planner screenshot in browser-frame card + "Start Planning — Free" CTA; all CTAs now point to `/planner/` (no signup gate) - **Heat score** — updated `calculate_heat_score()` for new project phase values (`permit_granted` +4, `lease_signed`/`permit_pending` +3, `converting_existing`/`permit_not_filed` +2, `location_found` +1) - **Quote URL** — planner now passes `country` parameter to quote form prefill - **Admin email** — includes stakeholder type and updated field labels ### Added — Phase 0: Ungate & Validate - **Guest mode planner** — removed auth gate from `/planner/` and `/planner/calculate`; scenarios still require login - **New calculator variables** — `budgetTarget` (budget vs CAPEX comparison), `glassType` (standard/panoramic, 1.4x multiplier), `lightingType` (LED standard/competition/natural, 1.5x/0x multipliers) - **Pill select UI component** — reusable `pillSelect()` helper in planner.js with matching `.pill-btn` CSS for multi-option inputs - **Budget indicator card** — shows over/under budget with variance amount and percentage on the Investment tab - **3-step "Get Builder Quotes" flow** — `/leads/quote` with project specs, details, and contact steps; no login required - **Lead heat scoring** — `calculate_heat_score()` rates leads as hot/warm/cool based on timeline, financing, location readiness, and budget signals - **PDF export CTA** — "Export Business Plan (PDF) — €99" wired to Paddle checkout (`business_plan` price in PADDLE_PRICES) - **SEO meta tags** — `` description, og:title, og:description, og:image on planner page - **Migration 0002** — expands `lead_requests` with 17 new columns for quote qualification flow; makes `user_id` nullable for guest leads - **Phase 0 test suite** (`tests/test_phase0.py`) — 47 tests covering guest mode, glass/lighting/budget variables, heat scoring, quote submission, schema validation - Updated Hypothesis strategy in `test_calculator.py` with `budgetTarget`, `glassType`, `lightingType` ### Changed - Planner CTA links now point to `/leads/quote` with pre-filled calculator state params (venue, courts, glass, lighting, budget) - Sticky footer bar updated: "Get Builder Quotes" + "Export Business Plan (PDF)" replace old supplier/financing links ### Changed - Landing page journey section: renamed "From Idea to Operating Hall" → "Your Journey", expanded from 4 cards to 5 (Explore → Plan → Finance → Build → Grow) with "Coming Soon" badges on unreleased stages - Added `.grid-5` CSS helper for 5-column grid layout ### Changed - **Pico CSS → Tailwind CSS v4** — full design system migration across all templates (except planner, which keeps its own CSS) - Standalone Tailwind CLI binary (no Node.js) with `make css-build` / `make css-watch` - Court Tech brand theme: navy/charcoal/electric/accent color palette - Component classes (`.btn`, `.card`, `.form-input`, `.table`, `.badge`, `.flash`, etc.) in `input.css` for consistent styling - Self-hosted Commit Mono font (replaces JetBrains Mono) for monospace data display - Docker multi-stage build: CSS compiled in dedicated stage before Python build - Landing page teaser calculator restyled with Tailwind utilities and brand colors ### Removed - Pico CSS CDN dependency - `custom.css` (replaced by Tailwind `input.css` with `@layer components`) - JetBrains Mono font (replaced by self-hosted Commit Mono) ### Fixed - Empty env vars (e.g. `SECRET_KEY=`) now fall back to defaults instead of silently using `""` — fixes 500 on every request when `.env` has blank values ### Added - Comprehensive migration test suite (`tests/test_migrations.py` — 20 tests) covering fresh DB, existing DB, up-to-date DB, idempotent migration, version discovery, `_is_fresh_db`, migration 0001 correctness, and ordering - Expanded `migrate.py` module docstring documenting the 8-step algorithm, protocol for adding migrations, and design decisions - Sequential migration system (`migrations/migrate.py`) — tracks applied versions in `_migrations` table, auto-detects fresh vs existing DBs, runs pending migrations in order - `migrations/versions/0001_rename_ls_to_paddle.py` — first versioned migration (absorbed from `scripts/migrate_to_paddle.py`) - Server-side financial calculator (`planner/calculator.py`) — ported JS `calc()`, `pmt()`, `calcIRR()` to Python so the full financial model is no longer exposed in client-side JavaScript - `POST /planner/calculate` endpoint for server-side computation - Pre-computed initial data (`window.__PADELNOMICS_INITIAL_D__`) injected on page load for instant first render - Debounced API fetch pattern in `planner.js` with `AbortController` for in-flight request cancellation - Computing indicator CSS (`.planner-app--computing`) with subtle "computing..." text - Comprehensive test suite for calculator (`tests/test_calculator.py` — 227 tests) covering all 4 venue/ownership combos, edge cases, and Hypothesis property-based fuzzing - Comprehensive billing test suite (371 tests total): - `tests/conftest.py` — shared fixtures (DB, app, clients, subscriptions, webhook helpers) - `tests/test_billing_helpers.py` — unit tests for SQL helpers, feature/limit access, plan determination (60+ tests + parameterized + Hypothesis) - `tests/test_billing_webhooks.py` — integration tests for LemonSqueezy webhooks (signature verification, all lifecycle events, Hypothesis fuzzing) - `tests/test_billing_routes.py` — route tests (pricing, checkout, manage, cancel, resume, subscription_required decorator) - Added `hypothesis>=6.100.0` and `respx>=0.22.0` to dev dependencies for property-based testing and httpx mocking - **Factored into Copier template** — all billing tests now generate as `.jinja` templates with provider-specific conditionals for Stripe, Paddle, and LemonSqueezy - GitLab CI/CD pipeline (`.gitlab-ci.yml`) — runs pytest + ruff on master/MRs, auto-deploys on master - Blue-green deployment with Docker Compose profiles (`docker-compose.prod.yml`, `deploy.sh`) - nginx router on port 5000 proxies to active blue/green slot - Zero-downtime: new slot health-checked before traffic switch - Automatic rollback on failed health check ### Removed - `scripts/migrate_to_paddle.py` — superseded by `versions/0001_rename_ls_to_paddle.py` ### Changed - `planner.js` no longer contains `calc()`, `pmt()`, or `calcIRR()` functions — computation moved server-side - `render()` split into `render()` (tab switching + schedule calc) and `renderWith(d)` (DOM updates from data) - Tab switching now renders from `_lastD` cache (instant, no API call) - Slider input triggers 200ms debounced server call instead of synchronous client-side calc