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>
523 lines
34 KiB
Markdown
523 lines
34 KiB
Markdown
# 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
|
||
- **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 handler** — `BOOST_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 atomic** — `boost_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 0010** — `published_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 pipeline** — `POST /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 system** — `POST /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 prevention** — `RESERVED_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-setup** — `setup_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 broken** — `Verifier().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 crash** — `no 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 CRUD** — `GET/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 0008** — `paddle_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 export** — `businessplan.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 routes** — `GET /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 management** — `GET /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 script** — `scripts/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 0007** — `credit_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 management** — `GET /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 emails** — `send_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 submission** — `credit_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 flow** — `GET /suppliers/claim/<slug>` verifies unclaimed
|
||
and redirects to signup with pre-fill
|
||
- **Webhook handlers** — `subscription.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_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** — `<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
|