Files
padelnomics/CHANGELOG.md
Deeman df8a747463 add credit system and supplier webhook test suites, remove dead test
24 tests for credits.py (balance, spend, unlock, refill, ledger) and
10 integration tests for supplier webhook handlers (credit packs, sticky
boosts, subscription activation, business plan purchase). Removed
test_mobile_nav_no_overflow which never asserted its JS result.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 22:02:09 +01:00

34 KiB
Raw Blame History

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog.

[Unreleased]

Added

  • Credit system test suite (tests/test_credits.py — 24 tests) — covers get_balance, add_credits, spend_credits, compute_credit_cost, already_unlocked, unlock_lead, monthly_credit_refill, get_ledger; tests run against real in-memory SQLite with no mocking
  • Supplier webhook test suite (tests/test_supplier_webhooks.py — 10 tests) — integration tests POSTing signed payloads to /billing/webhook/paddle and verifying DB state for credit pack purchases, sticky boosts, supplier subscription activation (growth/pro tiers, boost items, noop on missing supplier_id), and business plan PDF export

Removed

  • Dead test test_mobile_nav_no_overflow from test_visual.py — evaluated JS but never asserted the result; always passed regardless of overflow state

Fixed

  • Supplier logo missing on public directory — directory cards and supplier detail page only checked logo_url (external URL); now checks logo_file first (uploaded via dashboard), falling back to logo_url
  • Admin forward history links to wrong page — supplier name in lead forward history linked to /directory/ (public index) instead of /admin/suppliers/<id> (admin detail page)
  • Dashboard lead country filter drops heat/timeline state — hidden inputs used mismatched names (heat_hidden/timeline_hidden vs heat/timeline), silently resetting other filters when country changed
  • Directory pagination drops region filter — paginating after filtering by region lost the region parameter; added to all pagination link templates
  • boost_card_color missing from webhook handlerBOOST_PRICE_KEYS omitted boost_card_color, so purchasing the card color boost via Paddle would silently fail to create the supplier_boosts record; also removed stale boost_newsletter entry (replaced by card color boost)
  • Sticky boost DB operations not atomicboost_sticky_week and boost_sticky_month handlers issued two bare execute() calls (INSERT + UPDATE) without a transaction; wrapped in db_transaction() to prevent partial writes

Added

  • Clickable admin list rows — entire row is clickable on leads, suppliers, and users admin list pages (data-href + JS click handler that skips links/buttons/forms); pointer cursor and hover highlight via CSS
  • Supplier impersonation — "Impersonate Owner" button on admin supplier detail page; reuses existing admin.impersonate route, shown only when the supplier has a claimed_by user
  • Seed data: supplier owner accounts — each claimed supplier now gets its own user + active subscription for realistic impersonation testing

Fixed

  • Insufficient credits UX — unlock error now shows credit balance, cost, and a "Buy Credits" CTA linking to Boost & Upsells tab (was a collapsed red error message with no action)
  • Supplier dashboard sidenav — active tab highlight now updates on click (was only correct on initial page load)
  • Boost/credit buy buttons used wrong price IDs — dashboard boosts tab passed internal keys (boost_logo, credits_25) as Paddle priceId instead of resolved Paddle price IDs; buttons now show "Not configured" when Paddle products aren't set up
  • Listing preview card stretched full width — constrained to 420px to match actual directory card proportions
  • Impersonate redirects to supplier dashboard — impersonating a user who owns a supplier now lands on /suppliers/dashboard instead of the generic demand-side dashboard

Added — Programmatic SEO: Content Generation Engine

  • Database migration 0010published_scenarios, article_templates, template_data, articles tables with FTS5 full-text search on articles; content-sync triggers for INSERT/UPDATE/DELETE
  • Article template system — parameterized content recipes with input_schema (JSON field definitions), url_pattern, title_pattern, body_template (Markdown + Jinja2); supports multiple content types (only calculator built now)
  • Template data rows — per-city/region input data that feeds the generation pipeline; manual add or bulk CSV upload
  • Bulk generation pipelinePOST /admin/templates/<id>/generate computes financial scenarios from data rows via validate_state() + calc(), renders Markdown with baked scenario cards, writes static HTML to data/content/_build/, creates articles with staggered publish dates
  • Published scenarios — standalone financial widgets with pre-computed calc_json; embeddable in articles via [scenario:slug] markers with section variants (:capex, :operating, :cashflow, :returns, :full)
  • Scenario widget templates — 6 partial templates (summary, CAPEX breakdown, revenue & OPEX, 5-year projection, returns & financing, full combined) with dark navy header bar, Commit Mono numbers, responsive metric grids, and "Try with your own numbers" CTA
  • Static file rendering — articles rendered to data/content/_build/ as pre-baked HTML; DB stores metadata only, no body content in DB
  • Content blueprint — catch-all route serves published articles by url_path; registered last to avoid path collisions
  • Markets hub (/markets) — public search + country/region filter page for discovering articles; HTMX live filtering via FTS5 queries
  • Admin template CRUD — list, create, edit, delete article templates; view/add/upload/delete data rows; bulk generation form with start date and articles-per-day controls
  • Admin scenario CRUD — list, create (with curated calculator form), edit + recalculate, delete, preview all widget sections
  • Admin article management — list with status badges (draft/published/ scheduled), create manual articles (Markdown textarea), edit, delete, publish/unpublish toggle, rebuild single or all articles
  • Rebuild systemPOST /admin/articles/<id>/rebuild and /admin/rebuild-all re-render article HTML from source (template+data or markdown file) with fresh scenario calc_json
  • Article detail page — SEO meta tags (description, og:title, og:description, og:type=article, og:image, canonical), article typography styles, bottom CTA linking to planner
  • Scenario widget CSS.scenario-widget component styles in input.css with responsive table scroll and metric grid collapse; .article-body typography for headings, paragraphs, lists, blockquotes, tables, code blocks
  • slugify() utility — added to core.py for URL-safe slug generation
  • mistune dependency — Markdown → HTML rendering for articles
  • Sitemap/markets and all published articles added to /sitemap.xml (only articles with published_at <= now)
  • Footer — "Markets" link added to Product column
  • Admin dashboard — Templates, Scenarios, Articles quick-link buttons
  • Path collision preventionRESERVED_PREFIXES validation rejects article url_path values that conflict with existing routes

Added

  • Dev setup script (scripts/dev_setup.sh) — interactive bootstrap that checks prerequisites, installs deps, creates .env with auto-generated SECRET_KEY, runs migrations, seeds test data, optionally sets up Paddle sandbox products, and builds CSS
  • Dev run script (scripts/dev_run.sh) — resets DB, runs migrations, seeds data, builds CSS, then starts app + worker + CSS watcher in parallel with colored/labeled output and clean Ctrl-C shutdown; when Paddle is configured and ngrok is installed, starts a tunnel and updates the Paddle webhook destination automatically for end-to-end checkout testing
  • Paddle webhook auto-setupsetup_paddle.py now creates a notification destination in Paddle with the 5 event types the webhook handler processes, and writes PADDLE_WEBHOOK_SECRET to .env automatically
  • Resend test email docs — documented Resend test addresses (delivered@resend.dev, bounced@resend.dev, etc.) in .env.example and README for testing email flows without a verified domain

Fixed

  • Webhook signature verification brokenVerifier().verify() was called with raw bytes instead of a request object, causing all signed webhooks to fail with 400; replaced with manual HMAC verification matching Paddle's ts=<unix>;h1=<hmac> format; also added JSON parse error guard (400 instead of 500 on malformed payloads)
  • Billing tests stale after SDK migration — webhook tests used plain HMAC instead of Paddle's ts=...;h1=... signature format; checkout tests expected redirect instead of JSON overlay response; manage/cancel tests mocked httpx instead of Paddle SDK; removed stale PADDLE_PRICES config test (prices now in DB)
  • Quote wizard state loss_accumulated hidden input used " attribute delimiters which broke on tojson output containing literal " characters; switched all 8 step templates to single-quote delimiters (value='...')
  • Admin dashboard crashno such column: amount in credit ledger query; corrected to delta (the actual column name)
  • Admin supplier detail — credit ledger table referenced entry.entry_type and entry.amount instead of entry.event_type and entry.delta
  • Nav bar semi-transparent — replaced rgba(255,255,255,0.85) + backdrop-filter: blur() with opaque #ffffff background
  • Supplier pricing card buttons misaligned — added flexbox to .pricing-card with flex-grow: 1 on feature list and margin-top: auto on buttons

Changed

  • Newsletter boost removed — replaced with "Custom Card Color" boost (€19/mo) across supplier pricing page, setup_paddle.py, and supplier dashboard; directory cards with active card_color boost render a custom border color from supplier_boosts.metadata JSON
  • Removed "Zillow-style" comments from base.html and input.css

Added

  • Migration 0009 — adds metadata TEXT column to supplier_boosts for card color boost configuration
  • Admin supplier/lead CRUDGET/POST /admin/suppliers/new and GET/POST /admin/leads/new routes with form templates; "New Supplier" and "New Lead" buttons on admin list pages
  • Dev seed data script (scripts/seed_dev_data.py) — creates 5 suppliers (mix of tiers), 10 leads (mix of heat scores), 1 dev user, credit ledger entries, and lead forwards for local testing
  • Playwright quote wizard tests (tests/test_quote_wizard.py) — full 9-step flow, back-navigation data preservation, and validation error tests

Added — Phase 2: Scale the Marketplace — Supplier Dashboard + Business Plan PDF

  • Paddle.js overlay checkout — migrated all checkout flows (billing, supplier signup, business plan) from server-side Paddle transaction creation + redirect to client-side Paddle.Checkout.open() overlay; PADDLE_CLIENT_TOKEN config; Paddle.js script in base.html with sandbox/production toggle
  • Paddle products in DB — new paddle_products table replaces 16 PADDLE_PRICE_* env vars; get_paddle_price(key) and get_all_paddle_prices() async helpers in core.py; setup_paddle.py rewritten to write product/price IDs directly to database
  • Migration 0008paddle_products, business_plan_exports, feedback tables; logo_file and tagline columns on suppliers
  • Umami analytics — tracking script in base.html; config vars UMAMI_API_URL, UMAMI_API_TOKEN, UMAMI_WEBSITE_ID; directory click tracking redirect routes (/<slug>/website, /<slug>/quote)
  • Supplier dashboard (/suppliers/dashboard) — tab-based HTMX dashboard with sidebar nav (Overview, Lead Feed, My Listing, Boost & Upsells); each tab loads via hx-get with hx-push-url for deep-linking
    • Overview tab — 4 KPI stat cards (profile views, leads unlocked, credit balance, directory rank), new leads alert banner, recent activity feed
    • Lead feed tab — refactored _get_lead_feed_data() shared function with bidder count; heat/country/timeline filter pills; region matching badges; "No other suppliers yet — be first!" messaging
    • My Listing tab — preview card + inline edit form (company info, categories, service area, logo upload); POST /suppliers/dashboard/listing saves changes
    • Boost & Upsells tab — current plan, active boosts, available boosts with Paddle.Checkout.open() purchase buttons, credit packs grid, summary sidebar with visibility multiplier
  • Business plan PDF exportbusinessplan.py with WeasyPrint PDF engine; plan.html + plan.css A4 templates (executive summary, CAPEX, OPEX, revenue model, 5-year P&L, 12-month cash flow, sensitivity, key metrics); bilingual EN/DE; generate_business_plan worker task
  • Business plan routesGET /planner/export (options page with scenario picker + Paddle checkout), POST /planner/export/checkout, GET /planner/export/success, GET /planner/export/<id> (download); export CTA in planner sidebar
  • Supplier landing page enhanced — live stats from DB (business plans created, avg project value, suppliers listed, monthly leads); real anonymized lead preview cards (fallback to example data); credit explainer (hot=35, warm=20, cool=8); "Most Popular" badge on Growth plan; expanded FAQ (8 questions including credits, countries, cancellation); social proof section
  • Admin supplier managementGET /admin/suppliers with tier/country/name filters (HTMX search), GET /admin/suppliers/<id> detail with profile info, credit balance + ledger, active boosts, lead forward history; POST /admin/suppliers/<id>/credits manual credit adjustment; POST /admin/suppliers/<id>/tier manual tier change; supplier stats on admin dashboard (claimed, growth, pro, credits spent, leads forwarded)
  • Feedback widget — compact "Feedback" button in navbar opens HTMX popover with textarea; POST /feedback rate-limited (5/hr per IP), inserts into feedback table; GET /admin/feedback paginated admin view with user email
    • page URL

Added — Phase 1: Lead Operations + Builder Directory Monetization

  • SDK migration — replaced raw httpx calls with official paddle-python-sdk and resend SDKs for type safety, built-in webhook verification, and cleaner code; send_email() now accepts from_addr parameter; all Paddle API calls use SDK client
  • EMAIL_ADDRESSES dict — hardcoded transactional, leads, and nurture from-addresses sharing the notification.padelnomics.io Resend domain
  • Paddle product setup scriptscripts/setup_paddle.py creates all 14 products/prices programmatically via SDK; outputs .env snippet for CI
  • Claude Code skills.claude/skills/paddle-integration/SKILL.md and .claude/skills/resend-emails/SKILL.md for consistent SDK usage patterns
  • Migration 0007credit_ledger, lead_forwards, supplier_boosts tables; 12 new columns on suppliers (profile, credits); credit_cost and unlock_count on lead_requests
  • Credit system (credits.py) — get_balance, add_credits, spend_credits, unlock_lead, compute_credit_cost, monthly_credit_refill, get_ledger; InsufficientCredits exception; heat-based pricing (hot=35, warm=20, cool=8 credits); refill_monthly_credits worker task + scheduler
  • Admin lead managementGET /admin/leads with status/heat/country filters (HTMX search), GET /admin/leads/<id> detail with project brief + forward history, POST /admin/leads/<id>/status update, POST /admin/leads/<id>/forward manual forward (no credit cost); lead funnel stats on admin dashboard (planner users → leads → verified → unlocked)
  • Lead forwarding emailssend_lead_forward_email worker task sends full project brief + contact details to supplier; send_lead_matched_notification notifies entrepreneur when a supplier unlocks their lead
  • Credit cost computed on submissioncredit_cost set from heat score both on verified-user submission and on email verification
  • Supplier signup wizard (/suppliers/signup) — 4-step HTMX wizard: plan selection (Growth €149/mo, Pro €399/mo), boost add-ons (logo, highlight, verified, newsletter), credit packs (25-250), account details + order summary; builds multi-item Paddle transaction; _accumulated hidden JSON pattern
  • Supplier claim flowGET /suppliers/claim/<slug> verifies unclaimed and redirects to signup with pre-fill
  • Webhook handlerssubscription.activated with supplier_* plan creates supplier record with tier, credits, and boosts; transaction.completed handles credit pack purchases and sticky boost purchases with expiry
  • Supplier profile page (/directory/<slug>) — public profile with logo, verified badge, description, service categories as pills, service area, years in business, project count, website; "Request Quote" and "Claim This Listing" CTAs
  • Directory card links — all directory cards now link to supplier profile pages; paid-tier cards show "Request Quote" mini-CTA
  • Supplier lead feed (/suppliers/leads) — requires login + paid supplier tier; shows anonymized lead cards with heat badge, facility type, courts, country, timeline, budget range, credit cost, unlock count; POST /suppliers/leads/<id>/unlock spends credits, creates lead_forward, sends emails, returns full-details card via HTMX swap
  • Email nurture via Resend Audiences — on first scenario save, user is added to "Planner Users" audience (triggers 3-email automation); on quote submission, user is removed from audience (stops nurture)
  • PADDLE_PRICES expanded — 13 new price keys for supplier plans, boosts, credit packs; PADDLE_ENVIRONMENT config for sandbox/production switching

Changed

  • Supplier marketing page CTAs link to /suppliers/signup instead of mailto
  • httpx removed from direct dependencies (transitive via paddle SDK)

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_verificationnew, 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 testtest_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 endpointPOST /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 calculatorcountry 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 variablesbudgetTarget (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 scoringcalculate_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<meta> 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