merge: pricing-overhaul — Basic free, card color €59, BP PDF €149, supplier page CRO, lead-back guarantee

This commit is contained in:
Deeman
2026-02-26 15:49:57 +01:00
12 changed files with 495 additions and 198 deletions

View File

@@ -151,6 +151,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
2. Worker count auto-detected from proxy count (drops `EXTRACT_WORKERS`).
3. True crash resumption via `.partial.jsonl` sidecar: progress flushed every 50 venues,
resume skips already-fetched venues and merges prior results into the final file.
- **Lead-Back Guarantee** — suppliers can claim credits back for non-responding leads
with one click after 3 business days. Route `POST /suppliers/leads/<id>/guarantee-claim`,
`refund_lead_guarantee()` in credits.py, "Lead didn't respond" button on unlocked
lead cards (visible 330 days after unlock). Migration 0020 adds `guarantee_claimed_at`
and `guarantee_contact_method` columns to `lead_forwards`.
- **Supplier page CRO restructure** — `/suppliers` page reordered to lead with value
before pricing (Why Padelnomics → Lead-Back Guarantee → lead preview → social proof
→ pricing). All CTAs changed from "See Plans & Pricing" to "Get Started Free".
- **Static ROI line** — one-sentence ROI callout near pricing grounded in
`research/padel-hall-economics.md` data (4-court project = €30K+ contractor profit).
- **Credits-only callout** — below pricing grid: "Not ready for a subscription? Buy
a credit pack and unlock leads one at a time."
### Fixed
- **`datetime.utcnow()` deprecation warnings** — replaced all 94 occurrences
@@ -1197,6 +1210,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Pico CSS CDN dependency
- `custom.css` (replaced by Tailwind `input.css` with `@layer components`)
- JetBrains Mono font (replaced by self-hosted Commit Mono)
- **Basic tier is now free** — `supplier_basic` monthly/yearly price → €0. No Paddle
subscription required for Basic. Signup wizard shows "Free forever" instead of €39.
- **Card color boost** — price corrected €19/mo → €59/mo (aligns with MARKETING.md).
- **Business plan PDF** — price raised €99 → €149 (KfW-ready document supporting
€200K+ investment decision).
- **Hero and final CTA** — links changed from `#pricing` anchor to direct signup URL.
- **Comparison table** — €1,799/yr annotated with "(yearly plan)" for clarity.
- **EN+DE translations** — `sup_meta_desc` updated (removed "from €39/mo"); all
Basic-tier strings updated to reflect free tier; FAQ updated with guarantee mention.
- **`setup_paddle.py`** — Basic subscription products commented out (no longer needed);
`boost_card_color` 1900 → 5900 cents; `business_plan` 9900 → 14900 cents.
### 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

View File

@@ -59,8 +59,10 @@
- [x] Business Plan PDF purchase flow (Paddle one-time → webhook → async generation)
- [x] Boost purchases (logo, highlight, verified, card color, sticky week/month)
- [x] Credit pack purchases (25/50/100/250)
- [x] Supplier subscription tiers (Basic free / Growth €149 / Pro €399, monthly + annual)
- [x] Supplier subscription tiers (Basic free / Growth €199 / Pro €499, monthly + annual)
- [x] **Feature flags** (DB-backed, migration 0019) — `is_flag_enabled()` + `feature_gate()` decorator replace `WAITLIST_MODE`; 5 flags (markets, payments, planner_export, supplier_signup, lead_unlock); admin UI at `/admin/flags` with toggle
- [x] **Pricing overhaul** — Basic free (no Paddle sub), card color €59, BP PDF €149; supplier page restructured value-first (why → guarantee → leads → social proof → pricing); all CTAs "Get Started Free"; static ROI line; credits-only callout
- [x] **Lead-Back Guarantee** (migration 0020) — 1-click credit refund for non-responding leads (330 day window); `refund_lead_guarantee()` in credits.py; "Lead didn't respond" button on unlocked lead cards
- [x] **Python supervisor** (`src/padelnomics/supervisor.py`) + `workflows.toml` — replaces `supervisor.sh`; topological wave scheduling; croniter-based `is_due()`; systemd service updated
- [x] **Proxy rotation** (`extract/padelnomics_extract/proxy.py`) — round-robin + sticky hash-based selector via `PROXY_URLS` env var
- [x] Resend email integration (transactional: magic link, welcome, quote verify, lead forward, enquiry)
@@ -266,3 +268,6 @@
| 2026-02-22 | No soft email gate on planner | Planner already captures emails at natural points (scenario save → login, quote wizard step 9). Gate would add friction without meaningful list value. Revisit if data shows a gap. |
| 2026-02-22 | Wipe test suppliers before launch | 5 `example.com` entries from seed_dev_data.py — empty directory with "Be the first" CTA is better than obviously fake data |
| 2026-02-24 | Split market score into two branded scores | Marktreife-Score (existing market maturity, cities with ≥1 venue) vs Marktpotenzial-Score (greenfield opportunity, all GeoNames locations globally). SERP analysis confirmed zero competition for hyperlocal Gemeinde-level market intelligence pages. |
| 2026-02-26 | Basic tier free, no Paddle subscription | Simplest onboarding — signup without payment flow; MARKETING.md already said free, code said €39 |
| 2026-02-26 | Lead-Back Guarantee: supplier-initiated, credits back (not cash) | Risk reduction beats ROI projection (competitor research: #1 complaint is paying for silent leads). Credits-only keeps cash while removing the psychological barrier. |
| 2026-02-26 | Static ROI line, not interactive calculator | No lead marketplace uses inline ROI calcs; B2B SaaS trend away from them (Zendesk, Intercom, Gong all removed theirs). One bold number grounded in research beats a widget. |

View File

@@ -5,6 +5,8 @@ All balance mutations go through this module to keep credit_ledger (source of tr
and suppliers.credit_balance (denormalized cache) in sync within a single transaction.
"""
from datetime import UTC, datetime
from .core import execute, fetch_all, fetch_one, transaction, utcnow_iso
# Credit cost per heat tier
@@ -202,3 +204,92 @@ async def get_ledger(supplier_id: int, limit: int = 50) -> list[dict]:
ORDER BY cl.created_at DESC, cl.id DESC LIMIT ?""",
(supplier_id, limit),
)
class GuaranteeAlreadyClaimed(Exception):
"""Raised when supplier tries to claim guarantee twice for the same lead."""
class GuaranteeWindowClosed(Exception):
"""Raised when the 3-30 day guarantee window has passed."""
async def refund_lead_guarantee(
supplier_id: int,
forward_id: int,
contact_method: str,
) -> int:
"""Refund credits for a non-responding lead. Returns new balance.
Preconditions:
- forward must exist, belong to supplier_id, status='sent'
- guarantee_claimed_at must be NULL (not already claimed)
- created_at must be 330 days ago (calendar days)
- contact_method must be one of: 'email', 'phone', 'both'
The refund is a ledger credit entry (event_type='guarantee_refund') referencing
the forward_id. lead_forwards.status is updated to 'no_response'.
We keep the cash — only credits are returned to the supplier balance.
"""
assert contact_method in ("email", "phone", "both"), (
f"Invalid contact_method: {contact_method!r}"
)
forward = await fetch_one(
"SELECT * FROM lead_forwards WHERE id = ? AND supplier_id = ?",
(forward_id, supplier_id),
)
assert forward is not None, f"Forward {forward_id} not found for supplier {supplier_id}"
if forward["guarantee_claimed_at"] is not None:
raise GuaranteeAlreadyClaimed(
f"Guarantee already claimed for forward {forward_id}"
)
# Check 330 day window (calendar days)
created = datetime.fromisoformat(forward["created_at"].replace("Z", "+00:00"))
now = datetime.now(UTC)
age_days = (now - created).days
if age_days < 3 or age_days > 30:
raise GuaranteeWindowClosed(
f"Guarantee window closed: forward is {age_days} days old (must be 330)"
)
refund_amount = forward["credit_cost"]
now_iso = datetime.utcnow().isoformat()
async with transaction() as db:
row = await db.execute_fetchall(
"SELECT credit_balance FROM suppliers WHERE id = ?", (supplier_id,)
)
current = row[0][0] if row else 0
new_balance = current + refund_amount
await db.execute(
"""INSERT INTO credit_ledger
(supplier_id, delta, balance_after, event_type, reference_id, note, created_at)
VALUES (?, ?, ?, 'guarantee_refund', ?, ?, ?)""",
(
supplier_id,
refund_amount,
new_balance,
forward_id,
f"Lead-back guarantee refund for forward #{forward_id}",
now_iso,
),
)
await db.execute(
"UPDATE suppliers SET credit_balance = ? WHERE id = ?",
(new_balance, supplier_id),
)
await db.execute(
"""UPDATE lead_forwards
SET status = 'no_response',
guarantee_claimed_at = ?,
guarantee_contact_method = ?
WHERE id = ?""",
(now_iso, contact_method, forward_id),
)
return new_balance

View File

@@ -888,11 +888,11 @@
"month_nov": "Nov",
"month_dec": "Dez",
"sup_meta_title": "Für Anbieter Erreiche Padel-Unternehmer",
"sup_meta_desc": "Werde auf Padelnomics gelistet. Erreiche Unternehmer, die bereits einen Finanzplan für ihr Padel-Projekt erstellt haben. Basic, Growth und Pro ab €39/Monat.",
"sup_meta_desc": "Kostenloser Verzeichniseintrag auf Padelnomics. Qualifizierte Leads von Interessenten mit fertigem Businessplan. Growth- und Pro-Pläne ab €199/Monat.",
"sup_hero_h1a": "Kein Kaltakquise mehr.",
"sup_hero_h1b": "Triff Käufer, die bereits einen Businessplan haben.",
"sup_hero_sub": "Jeder Lead auf Padelnomics hat CAPEX, Umsatz und ROI bereits modelliert bevor er dich kontaktiert. Keine Zeitverschwender. Kein „ich schau mich nur um.“",
"sup_hero_cta": "Pläne & Preise ansehen",
"sup_hero_cta": "Kostenlos starten",
"sup_hero_trust_pre": "Vertrauen von Anbietern in",
"sup_hero_trust_post": "Ländern",
"sup_stat_plans": "erstellte Businesspläne",
@@ -932,7 +932,7 @@
"sup_lead_timeline": "Zeitplan",
"sup_lead_contact": "Kontakt",
"sup_leads_unlock_pre": "Vollständige Kontaktdaten und Projektspezifikationen mit Credits freischalten.",
"sup_leads_unlock_cta": "Jetzt starten →",
"sup_leads_unlock_cta": "Leads freischalten →",
"sup_leads_example": "Dies sind Beispiel-Leads. Echte Leads erscheinen, sobald Unternehmer Angebotsanfragen einreichen.",
"sup_why_h2": "Warum Padelnomics-Leads anders sind",
"sup_why_sub": "Jeder Lead hat bereits ein Finanzmodell für sein Projekt erstellt.",
@@ -948,14 +948,14 @@
"sup_billing_yearly": "Jährlich",
"sup_billing_save": "Bis zu 26% sparen",
"sup_basic_name": "Basic",
"sup_basic_dir": "Verzeichniseintrag",
"sup_basic_dir": "Dauerhaft kostenlos",
"sup_basic_f1": "Verifiziert ✓ Badge",
"sup_basic_f2": "Firmenlogo",
"sup_basic_f3": "Vollständige Beschreibung & Slogan",
"sup_basic_f4": "Website & Kontaktdaten",
"sup_basic_f5": "Checkliste der angebotenen Leistungen",
"sup_basic_f6": "Kontaktformular auf der Listing-Seite",
"sup_basic_cta": "Jetzt listen",
"sup_basic_cta": "Unternehmen kostenlos eintragen",
"sup_growth_name": "Growth",
"sup_growth_popular": "Beliebtester Plan",
"sup_growth_credits": "30 Credits/Monat inklusive",
@@ -975,11 +975,11 @@
"sup_pro_f5": "Bevorzugte Platzierung im Verzeichnis",
"sup_pro_f6": "100 Lead-Credits pro Monat",
"sup_pro_cta": "Jetzt starten",
"sup_yearly_note_basic": "€349 jährlich",
"sup_yearly_note_basic": "Dauerhaft kostenlos",
"sup_yearly_note_growth": "€1.799 jährlich",
"sup_yearly_note_pro": "€4.499 jährlich",
"sup_boosts_h3": "Boost Add-Ons",
"sup_boosts_sub": "Mit jedem kostenpflichtigen Plan verfügbar. Im Dashboard verwalten.",
"sup_boosts_sub": "Für jeden Plan verfügbar. Im Dashboard verwalten. Kartenfarbe ab €59/Monat.",
"sup_boost_logo": "Logo",
"sup_boost_highlight": "Hervorhebung",
"sup_boost_verified": "Verifiziert-Badge",
@@ -1026,13 +1026,13 @@
"sup_faq_a1_post": "und klicke auf „Ist das Dein Unternehmen?“ Wir prüfen Deine Identität und geben Dir Zugang, um einen Plan auszuwählen und Dein Profil zu aktualisieren.",
"sup_faq_dir_link": "Verzeichnis",
"sup_faq_q2": "Wie viel kostet es?",
"sup_faq_a2": "Wir bieten drei Pläne an: Basic (€39/Monat) für einen verifizierten Verzeichniseintrag mit Kontaktformular; Growth (€199/Monat, 30 Credits) mit vollem Lead-Zugang und Prioritätsplatzierung; und Pro (€499/Monat, 100 Credits) für maximale Sichtbarkeit und Lead-Volumen. Jährliche Abrechnung spart bis zu 26 % Basic bei €349/Jahr, Growth bei €1.799/Jahr, Pro bei €4.499/Jahr. Optionale Boost-Add-Ons sind zusätzlich erhältlich.",
"sup_faq_a2": "Basic ist kostenlos verifizierter Verzeichniseintrag mit Kontaktformular, kein Abo nötig. Growth (€199/Monat, 30 Credits) bietet vollen Lead-Zugang und Prioritätsplatzierung. Pro (€499/Monat, 100 Credits) maximiert Sichtbarkeit und Lead-Volumen. Jährliche Abrechnung spart bis zu 25 %: Growth bei €1.799/Jahr, Pro bei €4.499/Jahr. Optionale Boost-Add-Ons sind für jeden Plan verfügbar.",
"sup_faq_q3": "Was macht Padelnomics-Leads anders als andere Plattformen?",
"sup_faq_a3": "Jeder Lead auf Padelnomics hat unser Finanzplanungstool genutzt, um sein Projekt zu modellieren CAPEX, Umsatzprognosen, ROI und Schuldendienstdeckung bevor er sich meldet. Das bedeutet: sie sind ernst, haben ein realistisches Budget und sind bereit, mit Anbietern zu sprechen. Du bekommst keine Kaltanfragen, sondern vorqualifizierte Projektbriefings.",
"sup_faq_q4": "Wie sieht der Preisvergleich mit Alternativen aus?",
"sup_faq_a4": "Eine Messepräsenz kostet €10.000+ pro Veranstaltung und liefert meist nur Browsing-Kontakte. Google Ads für Padel-Baukeywords kosten €2080 pro Klick das sind €5.000+/Jahr, bevor du mit einem einzigen Interessenten sprichst. Ein typischer Kaltverzeichniseintrag kostet ~€600/Jahr ohne einen einzigen Lead. Padelnomics Growth bei €1.799/Jahr beinhaltet 30 Lead-Credits pro Monat mit vollständigen Projektbriefings.",
"sup_faq_q5": "Wie funktionieren Credits?",
"sup_faq_a5": "Mit Credits schaltest du die Kontaktdaten von Leads frei. Jeder Plan beinhaltet monatliche Credits (Growth: 30, Pro: 100). Heiße Leads kosten 35 Credits, warme 20 und coole 8. Du kannst jederzeit zusätzliche Credit-Pakete über dein Dashboard kaufen. Ungenutzte Credits werden auf den nächsten Monat übertragen.",
"sup_faq_q5": "Wie funktionieren Credits und was ist die Lead-Back-Garantie?",
"sup_faq_a5": "Credits schalten die Kontaktdaten von Leads frei. Growth enthält 30 Credits/Monat, Pro 100. Heiße Leads kosten 35 Credits, warme 20, coole 8. Credit-Pakete sind auch ohne Abo erhältlich. Ungenutzte Credits werden übertragen. Wenn ein Lead nicht antwortet, klickst du nach 3 Werktagen auf „Lead hat nicht geantwortet“ die Credits werden sofort zurückgebucht, ohne Support-Ticket.",
"sup_faq_q6": "Welche Informationen enthalten Leads?",
"sup_faq_a6": "Jeder Lead enthält: Anlagentyp (innen/außen), Court-Anzahl, Glas- und Beleuchtungsvorlieben, Land und Stadt, Budgetschätzung, Projektphase, Zeitplan, Finanzierungsstatus, Stakeholder-Typ, benötigte Leistungen und vollständige Kontaktdaten.",
"sup_faq_q7": "Wie werden Leads Anbietern zugeordnet?",
@@ -1560,23 +1560,22 @@
"bp_lbl_confidential": "Vertraulich",
"bp_lbl_table_of_contents": "Inhaltsverzeichnis",
"email_magic_link_heading": "Bei {app_name} anmelden",
"email_magic_link_body": "Hier ist dein Anmeldelink. Er läuft in {expiry_minutes} Minuten ab.",
"email_magic_link_btn": "Anmelden →",
"email_magic_link_fallback": "Wenn der Button nicht funktioniert, kopiere diese URL in deinen Browser:",
"email_magic_link_ignore": "Falls Du das nicht angefordert hast, kannst Du diese E-Mail ignorieren.",
"email_magic_link_ignore": "Wenn du das nicht angefordert hast, kannst du diese E-Mail ignorieren.",
"email_magic_link_subject": "Dein Anmeldelink für {app_name}",
"email_magic_link_preheader": "Dieser Link läuft in {expiry_minutes} Minuten ab",
"email_quote_verify_heading": "Bestätige deine E-Mail für Angebote",
"email_quote_verify_greeting": "Hallo {first_name},",
"email_quote_verify_body": "Danke für deine Angebotsanfrage. Bestätige deine E-Mail, um deine Anfrage zu aktivieren und dein {app_name}-Konto zu erstellen.",
"email_quote_verify_project_label": "Dein Projekt:",
"email_quote_verify_urgency": "Verifizierte Anfragen werden von unserem Anbieternetzwerk bevorzugt bearbeitet.",
"email_quote_verify_urgency": "Verifizierte Anfragen werden von unserem Anbieternetzwerk bevorzugt behandelt.",
"email_quote_verify_btn": "Bestätigen & Aktivieren →",
"email_quote_verify_expires": "Dieser Link läuft in 60 Minuten ab.",
"email_quote_verify_fallback": "Wenn der Button nicht funktioniert, kopiere diese URL in deinen Browser:",
"email_quote_verify_ignore": "Falls Du das nicht angefordert hast, kannst Du diese E-Mail ignorieren.",
"email_quote_verify_ignore": "Wenn du das nicht angefordert hast, kannst du diese E-Mail ignorieren.",
"email_quote_verify_subject": "Bestätige deine E-Mail — Anbieter sind bereit für Angebote",
"email_quote_verify_preheader": "Ein Klick, um deine Angebotsanfrage zu aktivieren",
"email_quote_verify_preheader_courts": "Ein Klick, um dein {court_count}-Court-Projekt zu aktivieren",
@@ -1588,7 +1587,7 @@
"email_welcome_link_markets": "Marktdaten — erkunde die Padel-Nachfrage nach Stadt",
"email_welcome_link_quotes": "Angebote einholen — verbinde dich mit verifizierten Anbietern",
"email_welcome_btn": "Jetzt planen →",
"email_welcome_subject": "Du bist dabei — so fängst Du an",
"email_welcome_subject": "Du bist dabei — so fängst du an",
"email_welcome_preheader": "Dein Padel-Planungstoolkit ist bereit",
"email_waitlist_supplier_heading": "Du stehst auf der Anbieter-Warteliste",
"email_waitlist_supplier_body": "Danke für dein Interesse am <strong>{plan_name}</strong>-Plan. Wir bauen eine Plattform, die dich mit qualifizierten Leads von Padel-Unternehmern verbindet, die aktiv Projekte planen.",
@@ -1607,12 +1606,11 @@
"email_waitlist_general_perk_1": "Frühen Zugang vor dem öffentlichen Launch",
"email_waitlist_general_perk_2": "Exklusive Launch-Preise",
"email_waitlist_general_perk_3": "Prioritäts-Onboarding und Support",
"email_waitlist_general_outro": "Wir melden uns in Kürze.",
"email_waitlist_general_outro": "Wir melden uns bald.",
"email_waitlist_general_subject": "Du stehst auf der Liste — wir benachrichtigen dich zum Launch",
"email_waitlist_general_preheader": "Früher Zugang + exklusive Launch-Preise",
"email_lead_forward_heading": "Neues Projekt-Lead",
"email_lead_forward_urgency": "Dieses Lead wurde gerade freigeschaltet. Anbieter, die innerhalb von 24 Stunden antworten, gewinnen das Projekt 3× häufiger.",
"email_lead_forward_section_brief": "Projektbeschreibung",
"email_lead_forward_urgency": "Dieses Lead wurde gerade freigeschaltet. Anbieter, die innerhalb von 24 Stunden antworten, gewinnen das Projekt 3× häufiger.", "email_lead_forward_section_brief": "Projektbeschreibung",
"email_lead_forward_section_contact": "Kontakt",
"email_lead_forward_lbl_facility": "Anlage",
"email_lead_forward_lbl_courts": "Plätze",
@@ -1631,15 +1629,15 @@
"email_lead_forward_preheader_suffix": "Kontaktdaten enthalten",
"email_lead_matched_heading": "Ein Anbieter möchte dein Projekt besprechen",
"email_lead_matched_greeting": "Hallo {first_name},",
"email_lead_matched_body": "Gute Neuigkeit — ein verifizierter Anbieter wurde mit Deinem Padel-Projekt abgeglichen. Er hat Dein Projektbriefing und Deine Kontaktdaten.",
"email_lead_matched_body": "Gute Nachrichten — ein verifizierter Anbieter wurde mit deinem Padel-Projekt abgeglichen. Er hat deine Projektbeschreibung und Kontaktdaten.",
"email_lead_matched_context": "Du hast eine Angebotsanfrage für eine {facility_type}-Anlage mit {court_count} Plätzen in {country} eingereicht.",
"email_lead_matched_next_heading": "Was passiert als Nächstes",
"email_lead_matched_next_body": "Der Anbieter hat Dein Projektbriefing und Deine Kontaktdaten erhalten. Die meisten Anbieter melden sich innerhalb von 2448 Stunden per E-Mail oder Telefon.",
"email_lead_matched_tip": "Tipp: Wer schnell auf Anbieter-Kontaktaufnahmen reagiert, erhöht seine Chancen auf wettbewerbsfähige Angebote.",
"email_lead_matched_next_body": "Der Anbieter hat deine Projektbeschreibung und Kontaktdaten erhalten. Die meisten Anbieter melden sich innerhalb von 2448 Stunden per E-Mail oder Telefon.",
"email_lead_matched_tip": "Tipp: Schnelles Reagieren auf Anbieter-Kontaktaufnahmen erhöht deine Chance auf wettbewerbsfähige Angebote.",
"email_lead_matched_btn": "Zum Dashboard →",
"email_lead_matched_note": "Du erhältst diese Benachrichtigung jedes Mal, wenn ein neuer Anbieter Deine Projektdetails freischaltet.",
"email_lead_matched_note": "Du erhältst diese Benachrichtigung jedes Mal, wenn ein neuer Anbieter deine Projektdetails freischaltet.",
"email_lead_matched_subject": "{first_name}, ein Anbieter möchte dein Projekt besprechen",
"email_lead_matched_preheader": "Der Anbieter meldet sich direkt bei Dir — das erwartet Dich",
"email_lead_matched_preheader": "Der Anbieter wird sich direkt bei dir melden — das erwartet dich",
"email_enquiry_heading": "Neue Anfrage von {contact_name}",
"email_enquiry_body": "Du hast eine neue Anfrage über deinen <strong>{supplier_name}</strong>-Verzeichniseintrag.",
"email_enquiry_lbl_from": "Von",
@@ -1647,8 +1645,7 @@
"email_enquiry_respond_fast": "Antworte innerhalb von 24 Stunden für den besten ersten Eindruck.",
"email_enquiry_reply": "Antworte direkt an <a href=\"mailto:{contact_email}\" style=\"color:#1D4ED8;\">{contact_email}</a>.",
"email_enquiry_subject": "Neue Anfrage von {contact_name} über deinen Verzeichniseintrag",
"email_enquiry_preheader": "Antworte, um mit diesem potenziellen Kunden in Kontakt zu kommen",
"email_business_plan_heading": "Dein Businessplan ist fertig",
"email_enquiry_preheader": "Antworte, um mit diesem potenziellen Kunden in Kontakt zu treten", "email_business_plan_heading": "Dein Businessplan ist fertig",
"email_business_plan_body": "Dein Padel-Businessplan wurde als PDF erstellt und steht zum Download bereit.",
"email_business_plan_includes": "Dein Plan enthält Investitionsübersicht, Umsatzprognosen und Break-Even-Analyse.",
"email_business_plan_btn": "PDF herunterladen →",
@@ -1730,5 +1727,26 @@
"mscore_faq_q6": "Was ist der Unterschied zwischen dem padelnomics Marktreife-Score und dem padelnomics Marktpotenzial-Score?",
"mscore_faq_a6": "Der padelnomics Marktreife-Score misst, wie etabliert und ausgereift ein bestehender Padel-Markt ist — er gilt nur für Städte mit mindestens einer Anlage. Der padelnomics Marktpotenzial-Score bewertet Investitionschancen in noch unbestellten Märkten und erfasst alle Standorte weltweit. Angebotslücken und unterversorgte Einzugsgebiete fließen positiv ein — auch dort, wo es noch gar keine Anlagen gibt.",
"mscore_faq_q7": "Warum hat mein Ort einen hohen padelnomics Marktpotenzial-Score, aber keine Padelanlagen?",
"mscore_faq_a7": "Genau darum geht es. Ein hoher padelnomics Marktpotenzial-Score signalisiert einen unterversorgten Standort: solide Bevölkerungsbasis, wirtschaftliche Kaufkraft, kein bestehendes Angebot und weite Entfernung zur nächsten Anlage. Das sind genau die Signale, die auf eine Pionierchance hinweisen — kein Zeichen für einen schwachen Markt."
}
"mscore_faq_a7": "Genau darum geht es. Ein hoher padelnomics Marktpotenzial-Score signalisiert einen unterversorgten Standort: solide Bevölkerungsbasis, wirtschaftliche Kaufkraft, kein bestehendes Angebot und weite Entfernung zur nächsten Anlage. Das sind genau die Signale, die auf eine Pionierchance hinweisen — kein Zeichen für einen schwachen Markt.",
"sup_cta_btn": "Kostenlos starten",
"sup_basic_free_label": "Kostenlos",
"sup_pricing_eur_note": "Alle Preise in EUR",
"sup_guarantee_h2": "Lead-Back-Garantie",
"sup_guarantee_p": "Wenn ein Lead nicht antwortet, Credits per Klick zurückbuchen. Kein Support-Ticket, kein Warten nach 3 Werktagen einfach im Dashboard auf „Lead hat nicht geantwortet“ klicken.",
"sup_guarantee_badge": "Garantie ohne Risiko",
"sup_leads_section_h2": "So sehen deine Interessenten aus",
"sup_leads_section_sub": "Jeder Lead hat unseren Finanzplaner genutzt. Kontaktdaten werden nach dem Freischalten sichtbar.",
"sup_roi_line": "Ein einziges 4-Court-Projekt = <strong>€30.000+ Gewinn</strong>. Growth-Plan: €2.388/Jahr. Die Rechnung ist einfach.",
"sup_credits_only_pre": "Noch nicht bereit für ein Abo? Kaufe ein Credit-Paket und schalte Leads einzeln frei. Keine Bindung, keine Monatsgebühr.",
"sup_credits_only_cta": "Credits kaufen →",
"sup_step1_free_forever": "Dauerhaft kostenlos",
"sd_guarantee_btn": "Lead hat nicht geantwortet",
"sd_guarantee_contact_label": "Wie hast du versucht, den Lead zu erreichen?",
"sd_guarantee_contact_email": "E-Mail",
"sd_guarantee_contact_phone": "Telefon",
"sd_guarantee_contact_both": "Beides E-Mail und Telefon",
"sd_guarantee_submit": "Credits zurückbuchen",
"sd_guarantee_success": "Credits wurden deinem Guthaben gutgeschrieben.",
"sd_guarantee_window_error": "Garantiezeitraum abgelaufen (nur 330 Tage nach dem Freischalten verfügbar).",
"sd_guarantee_already_claimed": "Du hast für diesen Lead bereits eine Rückerstattung beantragt."
}

View File

@@ -888,11 +888,11 @@
"month_nov": "Nov",
"month_dec": "Dec",
"sup_meta_title": "For Suppliers - Reach Padel Entrepreneurs",
"sup_meta_desc": "Get listed on Padelnomics. Reach entrepreneurs who've already built a financial model for their padel project. Basic, Growth and Pro plans from €39/mo.",
"sup_meta_desc": "Free directory listing on Padelnomics. Qualified leads from buyers with business plans. Growth and Pro plans from €199/mo.",
"sup_hero_h1a": "Stop Chasing Cold Leads.",
"sup_hero_h1b": "Meet Buyers Who Already Have a Business Plan.",
"sup_hero_sub": "Every lead on Padelnomics has modeled their CAPEX, projected revenue, and calculated ROI — before they contact you. No tire-kickers. No “just browsing.”",
"sup_hero_cta": "See Plans & Pricing",
"sup_hero_cta": "Get Started Free",
"sup_hero_trust_pre": "Trusted by suppliers in",
"sup_hero_trust_post": "countries",
"sup_stat_plans": "Business plans created",
@@ -932,7 +932,7 @@
"sup_lead_timeline": "Timeline",
"sup_lead_contact": "Contact",
"sup_leads_unlock_pre": "Unlock full contact details and project specs with credits.",
"sup_leads_unlock_cta": "Get started →",
"sup_leads_unlock_cta": "Start Getting Leads",
"sup_leads_example": "These are example leads. Real leads appear as entrepreneurs submit quote requests.",
"sup_why_h2": "Why Padelnomics Leads Are Different",
"sup_why_sub": "Every lead has already built a financial model for their project.",
@@ -948,14 +948,14 @@
"sup_billing_yearly": "Yearly",
"sup_billing_save": "Save up to 26%",
"sup_basic_name": "Basic",
"sup_basic_dir": "Directory listing",
"sup_basic_dir": "Free forever",
"sup_basic_f1": "Verified ✓ badge",
"sup_basic_f2": "Company logo",
"sup_basic_f3": "Full description & tagline",
"sup_basic_f4": "Website & contact details",
"sup_basic_f5": "Services offered checklist",
"sup_basic_f6": "Enquiry form on listing page",
"sup_basic_cta": "Get Listed",
"sup_basic_cta": "List Your Company Free",
"sup_growth_name": "Growth",
"sup_growth_popular": "Most Popular",
"sup_growth_credits": "30 credits/mo included",
@@ -975,11 +975,11 @@
"sup_pro_f5": "Priority placement in directory",
"sup_pro_f6": "100 lead credits per month",
"sup_pro_cta": "Get Started",
"sup_yearly_note_basic": "€349 billed yearly",
"sup_yearly_note_basic": "Free forever",
"sup_yearly_note_growth": "€1,799 billed yearly",
"sup_yearly_note_pro": "€4,499 billed yearly",
"sup_boosts_h3": "Boost Add-Ons",
"sup_boosts_sub": "Available with any paid plan. Manage from your dashboard.",
"sup_boosts_sub": "Available on any plan. Manage from your dashboard. Card color from €59/mo.",
"sup_boost_logo": "Logo",
"sup_boost_highlight": "Highlight",
"sup_boost_verified": "Verified Badge",
@@ -1026,13 +1026,13 @@
"sup_faq_a1_post": "and click “Is this your company?” Well verify your identity and give you access to choose a plan and upgrade your profile.",
"sup_faq_dir_link": "directory",
"sup_faq_q2": "How much does it cost?",
"sup_faq_a2": "We offer three plans: Basic (€39/mo) for a verified directory listing with enquiry form; Growth (€199/mo, 30 credits) with full lead access and priority placement; and Pro (€499/mo, 100 credits) for maximum visibility and lead volume. Yearly billing saves up to 26% — Basic at €349/yr, Growth at €1,799/yr, Pro at €4,499/yr. Optional boost add-ons are available on top.",
"sup_faq_a2": "Basic is free — a verified directory listing with an enquiry form, no subscription required. Growth (€199/mo, 30 credits) gives full lead access and priority placement. Pro (€499/mo, 100 credits) adds maximum visibility and lead volume. Yearly billing saves up to 25%: Growth at €1,799/yr, Pro at €4,499/yr. Optional boost add-ons are available on any plan.",
"sup_faq_q3": "What makes Padelnomics leads different from other platforms?",
"sup_faq_a3": "Every lead on Padelnomics has used our financial planning tool to model their project — CAPEX, revenue projections, ROI, and debt service coverage — before reaching out. This means theyre serious, they have a realistic budget, and theyre ready to talk to suppliers. Youre not getting cold enquiries; youre getting pre-qualified project briefs.",
"sup_faq_q4": "How does pricing compare to alternatives?",
"sup_faq_a4": "A trade show booth costs €10,000+ per event and delivers mostly browsing contacts. Google Ads for padel construction keywords run €2080 per click — thats €5,000+/yr before you talk to a single prospect. A typical cold directory listing charges ~€600/yr with no leads at all. Padelnomics Growth at €1,799/yr includes 30 lead credits per month with full project briefs.",
"sup_faq_q5": "How do credits work?",
"sup_faq_a5": "Credits are how you unlock lead contact details. Each plan includes monthly credits (Growth: 30, Pro: 100). Hot leads cost 35 credits, warm leads 20, and cool leads 8. You can buy additional credit packs anytime from your dashboard. Unused credits roll over month to month.",
"sup_faq_q5": "How do credits work — and what's the Lead-Back Guarantee?",
"sup_faq_a5": "Credits unlock lead contact details. Growth includes 30 credits/mo; Pro includes 100. Hot leads cost 35 credits, warm leads 20, cool leads 8. You can also buy credit packs without a subscription. Unused credits roll over. If a lead doesn't respond, click \"Lead didn't respond\" on the card after 3 business days and your credits are instantly returned to your balance — no support ticket, no waiting.",
"sup_faq_q6": "What information do leads include?",
"sup_faq_a6": "Every lead includes: facility type (indoor/outdoor), court count, glass and lighting preferences, country and city, budget estimate, project phase, timeline, financing status, stakeholder type, services needed, and full contact details.",
"sup_faq_q7": "How are leads matched to suppliers?",
@@ -1047,6 +1047,27 @@
"sup_faq_a10_post": "with your company details and well add you to the directory within 48 hours.",
"sup_cta_h2": "Your Next Client Is Already Building a Business Plan",
"sup_cta_p": "Theyve modeled the ROI. They know their budget. Theyre looking for a supplier like you.",
"sup_cta_btn": "Get Started Free",
"sup_basic_free_label": "Free",
"sup_pricing_eur_note": "All prices in EUR",
"sup_guarantee_h2": "Lead-Back Guarantee",
"sup_guarantee_p": "If a lead doesnt respond, claim your credits back with one click. No support tickets, no waiting — just click \"Lead didnt respond\" in your dashboard after 3 business days.",
"sup_guarantee_badge": "No-risk guarantee",
"sup_leads_section_h2": "See What Your Prospects Look Like",
"sup_leads_section_sub": "Every lead has used our financial planner. Contact details are blurred until you unlock.",
"sup_roi_line": "A single 4-court project = <strong>€30,000+ in profit</strong>. Growth plan costs €2,388/year. The math is simple.",
"sup_credits_only_pre": "Not ready for a subscription? Buy a credit pack and unlock leads one at a time. No commitment, no monthly fee.",
"sup_credits_only_cta": "Buy Credits →",
"sup_step1_free_forever": "Free forever",
"sd_guarantee_btn": "Lead didnt respond",
"sd_guarantee_contact_label": "How did you try to reach them?",
"sd_guarantee_contact_email": "Email",
"sd_guarantee_contact_phone": "Phone",
"sd_guarantee_contact_both": "Both email and phone",
"sd_guarantee_submit": "Claim credits back",
"sd_guarantee_success": "Credits returned to your balance.",
"sd_guarantee_window_error": "Guarantee window has closed (only available 330 days after unlock).",
"sd_guarantee_already_claimed": "Youve already claimed a refund for this lead.",
"scenario_cta_try_numbers": "Try with your own numbers →",
"scenario_payback_label": "Payback",
"scenario_months_unit": "months",
@@ -1731,4 +1752,4 @@
"mscore_faq_a6": "The padelnomics Marktreife-Score measures how established and mature an existing padel market is — it only applies to cities with at least one venue. The padelnomics Marktpotenzial-Score measures greenfield investment opportunity and covers all locations globally, rewarding supply gaps and underserved catchment areas where no courts exist yet.",
"mscore_faq_q7": "Why does my town have a high padelnomics Marktpotenzial-Score but no padel courts?",
"mscore_faq_a7": "That is exactly the point. A high padelnomics Marktpotenzial-Score indicates an underserved location: strong demographics, economic purchasing power, no existing supply, and distance from the nearest court. These are precisely the signals that suggest a greenfield opportunity — not a sign of a weak market."
}
}

View File

@@ -0,0 +1,16 @@
"""Add lead-back guarantee columns to lead_forwards.
guarantee_claimed_at: ISO timestamp when supplier claimed the guarantee refund.
guarantee_contact_method: how the supplier tried to reach the lead (email/phone/both).
Status 'no_response' added to lead_forwards.status by convention (no CHECK constraint change
needed — existing status TEXT column accepts any value).
"""
def up(conn):
conn.execute(
"ALTER TABLE lead_forwards ADD COLUMN guarantee_claimed_at TEXT"
)
conn.execute(
"ALTER TABLE lead_forwards ADD COLUMN guarantee_contact_method TEXT"
)

View File

@@ -43,7 +43,7 @@
<p>{{ t.export_subtitle }}</p>
</div>
<div class="exp-price">&euro;99 <span>one-time</span></div>
<div class="exp-price">&euro;149 <span>one-time</span></div>
<ul class="exp-features">
{% for key in ['planner_export_f1','planner_export_f2','planner_export_f3','planner_export_f4','planner_export_f5','planner_export_f6','planner_export_f7','planner_export_f8'] %}

View File

@@ -44,38 +44,35 @@
margin-top: 1.5rem; font-weight: 500;
}
/* How it works */
.sup-steps {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem;
max-width: 800px; margin: 0 auto;
/* Why section */
.sup-why { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }
.sup-why-card {
border: 1px solid #E2E8F0; border-radius: 14px; padding: 1.25rem;
text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.sup-step { text-align: center; }
.sup-step__num {
display: inline-flex; align-items: center; justify-content: center;
width: 48px; height: 48px; border-radius: 50%;
background: #EFF6FF; color: #1D4ED8; font-weight: 700; font-size: 1.25rem;
margin-bottom: 0.75rem;
}
.sup-step h3 { font-size: 1rem; margin-bottom: 0.25rem; }
.sup-step p { font-size: 0.8125rem; color: #64748B; }
.sup-why-card h3 { font-size: 0.9375rem; margin-bottom: 0.25rem; }
.sup-why-card p { font-size: 0.8125rem; color: #64748B; }
/* Credit explainer */
.credit-explainer {
background: #F8FAFC; border: 1px solid #E2E8F0; border-radius: 16px;
padding: 1.5rem; max-width: 600px; margin: 2rem auto 0;
/* Lead-back guarantee */
.sup-guarantee {
background: linear-gradient(135deg, #F0FDF4 0%, #ECFDF5 100%);
border: 2px solid #16A34A; border-radius: 20px; padding: 2rem;
max-width: 680px; margin: 0 auto; text-align: center;
}
.credit-explainer h3 { font-size: 1rem; margin-bottom: 1rem; text-align: center; }
.credit-tiers { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; }
.credit-tier {
text-align: center; padding: 0.75rem; background: white;
border: 1px solid #E2E8F0; border-radius: 10px;
.sup-guarantee__icon {
font-size: 2.5rem; margin-bottom: 0.5rem; display: block; line-height: 1;
}
.sup-guarantee h2 {
font-size: 1.375rem; color: #14532D; margin-bottom: 0.5rem; text-align: center;
}
.sup-guarantee p {
color: #166534; font-size: 0.9375rem; line-height: 1.65; max-width: 520px; margin: 0 auto;
}
.sup-guarantee__badge {
display: inline-block; background: #16A34A; color: white; font-size: 0.6875rem;
font-weight: 700; padding: 4px 12px; border-radius: 999px; margin-top: 1rem;
text-transform: uppercase; letter-spacing: 0.04em;
}
.credit-tier .tier-heat { font-weight: 700; font-size: 0.875rem; margin-bottom: 4px; }
.credit-tier .tier-cost { font-size: 1.25rem; font-weight: 800; color: #1D4ED8; }
.credit-tier .tier-label { font-size: 0.6875rem; color: #64748B; }
.heat-hot { color: #DC2626; }
.heat-warm { color: #D97706; }
.heat-cool { color: #3B82F6; }
/* Lead preview */
.sup-lead-preview {
@@ -100,14 +97,39 @@
.lead-preview-card dd { color: #1E293B; font-weight: 500; margin: 0; }
.lead-preview-card .lp-blur { color: transparent; text-shadow: 0 0 8px rgba(30,41,59,0.5); user-select: none; }
/* Why section */
.sup-why { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }
.sup-why-card {
border: 1px solid #E2E8F0; border-radius: 14px; padding: 1.25rem;
text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.04);
/* How it works */
.sup-steps {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem;
max-width: 800px; margin: 0 auto;
}
.sup-why-card h3 { font-size: 0.9375rem; margin-bottom: 0.25rem; }
.sup-why-card p { font-size: 0.8125rem; color: #64748B; }
.sup-step { text-align: center; }
.sup-step__num {
display: inline-flex; align-items: center; justify-content: center;
width: 48px; height: 48px; border-radius: 50%;
background: #EFF6FF; color: #1D4ED8; font-weight: 700; font-size: 1.25rem;
margin-bottom: 0.75rem;
}
.sup-step h3 { font-size: 1rem; margin-bottom: 0.25rem; }
.sup-step p { font-size: 0.8125rem; color: #64748B; }
/* Social proof */
.sup-proof-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem;
max-width: 720px; margin: 0 auto;
}
.sup-proof-card {
background: #F8FAFC; border: 1px solid #E2E8F0; border-radius: 16px;
padding: 1.75rem; text-align: left; position: relative;
}
.sup-proof-card::before {
content: "\201C"; font-size: 4rem; color: #DBEAFE; line-height: 1;
position: absolute; top: 1rem; left: 1.25rem; font-family: Georgia, serif;
}
.sup-proof-card blockquote {
font-size: 0.9375rem; color: #334155; font-style: italic;
line-height: 1.6; margin: 1.5rem 0 0.75rem; padding: 0;
}
.sup-proof-card cite { font-size: 0.8125rem; color: #94A3B8; font-style: normal; }
/* Billing toggle — CSS-only via radio sibling trick */
input[name="billing"] { position: absolute; opacity: 0; pointer-events: none; width: 1px; height: 1px; }
@@ -167,6 +189,23 @@
}
.pricing-card li::before { content: "\2713"; color: #16A34A; font-weight: 700; flex-shrink: 0; }
.pricing-card .btn, .pricing-card .btn-outline { width: 100%; text-align: center; margin-top: auto; }
.pricing-card__eur-note { font-size: 0.6875rem; color: #94A3B8; text-align: center; margin-top: 0.5rem; }
/* ROI callout */
.sup-roi {
background: #0F172A; border-radius: 14px; padding: 1.25rem 2rem;
text-align: center; max-width: 720px; margin: 1.75rem auto 0;
}
.sup-roi p { color: #E2E8F0; font-size: 1rem; font-weight: 600; margin: 0; line-height: 1.5; }
.sup-roi strong { color: #34D399; }
/* Credits-only callout */
.sup-credits-only {
background: #F8FAFC; border: 1px solid #E2E8F0; border-radius: 14px;
padding: 1.25rem 1.5rem; text-align: center; max-width: 640px; margin: 1.5rem auto 0;
}
.sup-credits-only p { font-size: 0.875rem; color: #475569; margin: 0; }
.sup-credits-only a { color: #1D4ED8; font-weight: 600; }
/* Boosts */
.boost-grid {
@@ -198,25 +237,6 @@
margin: 0.75rem auto 0; max-width: 560px;
}
/* Social proof */
.sup-proof-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem;
max-width: 720px; margin: 0 auto;
}
.sup-proof-card {
background: #F8FAFC; border: 1px solid #E2E8F0; border-radius: 16px;
padding: 1.75rem; text-align: left; position: relative;
}
.sup-proof-card::before {
content: "\201C"; font-size: 4rem; color: #DBEAFE; line-height: 1;
position: absolute; top: 1rem; left: 1.25rem; font-family: Georgia, serif;
}
.sup-proof-card blockquote {
font-size: 0.9375rem; color: #334155; font-style: italic;
line-height: 1.6; margin: 1.5rem 0 0.75rem; padding: 0;
}
.sup-proof-card cite { font-size: 0.8125rem; color: #94A3B8; font-style: normal; }
/* FAQ */
.sup-faq { max-width: 640px; margin: 0 auto; }
.sup-faq details { border-bottom: 1px solid #E2E8F0; padding: 1rem 0; }
@@ -240,7 +260,6 @@
.sup-stats { grid-template-columns: repeat(2, 1fr); }
.sup-steps, .sup-why, .sup-problem-grid, .sup-proof-grid { grid-template-columns: 1fr; }
.pricing-grid { grid-template-columns: 1fr; }
.credit-tiers { grid-template-columns: 1fr; }
.lead-preview-grid { grid-template-columns: 1fr; }
.sup-hero h1 { font-size: 1.75rem; }
}
@@ -254,7 +273,7 @@
<div class="sup-hero">
<h1>{{ t.sup_hero_h1a }}<br>{{ t.sup_hero_h1b }}</h1>
<p>{{ t.sup_hero_sub }}</p>
<a href="#pricing" class="btn">{{ t.sup_hero_cta }}</a>
<a href="{{ url_for('suppliers.signup') }}" class="btn">{{ t.sup_hero_cta }}</a>
<p class="sup-hero__proof">{{ t.sup_hero_trust_pre }} {{ total_countries }} {{ t.sup_hero_trust_post }}</p>
</div>
@@ -304,54 +323,40 @@
</div>
</section>
<!-- How it works -->
<!-- Why Padelnomics — moved above lead preview -->
<section class="sup-section">
<h2>{{ t.sup_how_h2 }}</h2>
<p class="sub">{{ t.sup_how_sub }}</p>
<div class="sup-steps">
<div class="sup-step">
<div class="sup-step__num">1</div>
<h3>{{ t.sup_how_step1_h3 }}</h3>
<p>{{ t.sup_how_step1_p }}</p>
<h2>{{ t.sup_why_h2 }}</h2>
<p class="sub">{{ t.sup_why_sub }}</p>
<div class="sup-why">
<div class="sup-why-card">
<h3>{{ t.sup_why_card1_h3 }}</h3>
<p>{{ t.sup_why_card1_p }}</p>
</div>
<div class="sup-step">
<div class="sup-step__num">2</div>
<h3>{{ t.sup_how_step2_h3 }}</h3>
<p>{{ t.sup_how_step2_p }}</p>
<div class="sup-why-card">
<h3>{{ t.sup_why_card2_h3 }}</h3>
<p>{{ t.sup_why_card2_p }}</p>
</div>
<div class="sup-step">
<div class="sup-step__num">3</div>
<h3>{{ t.sup_how_step3_h3 }}</h3>
<p>{{ t.sup_how_step3_p }}</p>
<div class="sup-why-card">
<h3>{{ t.sup_why_card3_h3 }}</h3>
<p>{{ t.sup_why_card3_p }}</p>
</div>
</div>
</section>
<!-- Credit explainer -->
<div class="credit-explainer">
<h3>{{ t.sup_credits_h3 }}</h3>
<p style="text-align:center;font-size:0.8125rem;color:#64748B;margin-bottom:1rem">
{{ t.sup_credits_sub }}
</p>
<div class="credit-tiers">
<div class="credit-tier">
<div class="tier-heat heat-hot">{{ t.sup_credits_hot }}</div>
<div class="tier-cost">35</div>
<div class="tier-label">{{ t.sup_credits_hot_label }}</div>
</div>
<div class="credit-tier">
<div class="tier-heat heat-warm">{{ t.sup_credits_warm }}</div>
<div class="tier-cost">20</div>
<div class="tier-label">{{ t.sup_credits_warm_label }}</div>
</div>
<div class="credit-tier">
<div class="tier-heat heat-cool">{{ t.sup_credits_cool }}</div>
<div class="tier-cost">8</div>
<div class="tier-label">{{ t.sup_credits_cool_label }}</div>
</div>
</div>
<!-- Lead-Back Guarantee -->
<section class="sup-section">
<div class="sup-guarantee">
<span class="sup-guarantee__icon">&#x1F6E1;&#xFE0F;</span>
<h2>{{ t.sup_guarantee_h2 }}</h2>
<p>{{ t.sup_guarantee_p }}</p>
<span class="sup-guarantee__badge">{{ t.sup_guarantee_badge }}</span>
</div>
</section>
<!-- Live lead preview -->
<!-- Live lead preview -->
<section class="sup-section">
<h2>{{ t.sup_leads_section_h2 }}</h2>
<p class="sub">{{ t.sup_leads_section_sub }}</p>
<div class="sup-lead-preview">
<h4>{{ t.sup_leads_heading }}</h4>
{% if preview_leads %}
@@ -376,7 +381,7 @@
</div>
<p style="text-align:center;margin-top:1rem;font-size:0.8125rem;color:#64748B">
{{ t.sup_leads_unlock_pre }}
<a href="#pricing" style="color:#1D4ED8;font-weight:600">{{ t.sup_leads_unlock_cta }}</a>
<a href="{{ url_for('suppliers.signup') }}" style="color:#1D4ED8;font-weight:600">{{ t.sup_leads_unlock_cta }}</a>
</p>
{% else %}
<div class="lead-preview-grid">
@@ -418,22 +423,41 @@
</div>
</section>
<!-- Why Padelnomics -->
<!-- How it works -->
<section class="sup-section">
<h2>{{ t.sup_why_h2 }}</h2>
<p class="sub">{{ t.sup_why_sub }}</p>
<div class="sup-why">
<div class="sup-why-card">
<h3>{{ t.sup_why_card1_h3 }}</h3>
<p>{{ t.sup_why_card1_p }}</p>
<h2>{{ t.sup_how_h2 }}</h2>
<p class="sub">{{ t.sup_how_sub }}</p>
<div class="sup-steps">
<div class="sup-step">
<div class="sup-step__num">1</div>
<h3>{{ t.sup_how_step1_h3 }}</h3>
<p>{{ t.sup_how_step1_p }}</p>
</div>
<div class="sup-why-card">
<h3>{{ t.sup_why_card2_h3 }}</h3>
<p>{{ t.sup_why_card2_p }}</p>
<div class="sup-step">
<div class="sup-step__num">2</div>
<h3>{{ t.sup_how_step2_h3 }}</h3>
<p>{{ t.sup_how_step2_p }}</p>
</div>
<div class="sup-why-card">
<h3>{{ t.sup_why_card3_h3 }}</h3>
<p>{{ t.sup_why_card3_p }}</p>
<div class="sup-step">
<div class="sup-step__num">3</div>
<h3>{{ t.sup_how_step3_h3 }}</h3>
<p>{{ t.sup_how_step3_p }}</p>
</div>
</div>
</section>
<!-- Social proof — moved before pricing -->
<section class="sup-section">
<h2>{{ t.sup_proof_h2 }}</h2>
<p class="sub">{{ calc_requests }}+ {{ t.sup_proof_stat1 }} &middot; {{ total_suppliers }}+ {{ t.sup_proof_stat2 }} &middot; {{ total_countries }} {{ t.sup_proof_stat3 }}</p>
<div class="sup-proof-grid">
<div class="sup-proof-card">
<blockquote>&ldquo;{{ t.sup_proof_q1 }}&rdquo;</blockquote>
<cite>{{ t.sup_proof_cite1 }}</cite>
</div>
<div class="sup-proof-card">
<blockquote>&ldquo;{{ t.sup_proof_q2 }}&rdquo;</blockquote>
<cite>{{ t.sup_proof_cite2 }}</cite>
</div>
</div>
</section>
@@ -453,15 +477,14 @@
</div>
<div class="pricing-grid">
<!-- Basic -->
<!-- Basic — Free -->
<div class="pricing-card">
<h3>{{ t.sup_basic_name }}</h3>
<div class="price-monthly">
<div class="price">&euro;39 <span>/mo</span></div>
<div class="price">{{ t.sup_basic_free_label }}</div>
</div>
<div class="price-yearly">
<div class="price">&euro;29 <span>/mo</span></div>
<span class="yearly-note">{{ t.sup_yearly_note_basic }}</span>
<div class="price">{{ t.sup_basic_free_label }}</div>
</div>
<div class="credits-inc credits-inc--muted">{{ t.sup_basic_dir }}</div>
<ul>
@@ -473,6 +496,7 @@
<li>{{ t.sup_basic_f6 }}</li>
</ul>
<a href="{{ url_for('suppliers.signup') }}?plan=supplier_basic" class="btn-outline" style="display:block;text-align:center">{{ t.sup_basic_cta }}</a>
<p class="pricing-card__eur-note">{{ t.sup_pricing_eur_note }}</p>
</div>
<!-- Growth -->
@@ -496,6 +520,7 @@
<li>{{ t.sup_growth_f6 }}</li>
</ul>
<a href="{{ url_for('suppliers.signup') }}?plan=supplier_growth" class="btn" style="display:block;text-align:center">{{ t.sup_growth_cta }}</a>
<p class="pricing-card__eur-note">{{ t.sup_pricing_eur_note }}</p>
</div>
<!-- Pro -->
@@ -518,9 +543,20 @@
<li>{{ t.sup_pro_f6 }}</li>
</ul>
<a href="{{ url_for('suppliers.signup') }}?plan=supplier_pro" class="btn-outline" style="display:block;text-align:center">{{ t.sup_pro_cta }}</a>
<p class="pricing-card__eur-note">{{ t.sup_pricing_eur_note }}</p>
</div>
</div>
<!-- Static ROI line -->
<div class="sup-roi">
<p>{{ t.sup_roi_line }}</p>
</div>
<!-- Credits-only callout -->
<div class="sup-credits-only">
<p>{{ t.sup_credits_only_pre }} <a href="{{ url_for('suppliers.signup') }}?plan=supplier_basic#credits">{{ t.sup_credits_only_cta }}</a></p>
</div>
<!-- Boost add-ons -->
<h3 style="text-align:center;font-size:1rem;margin-top:2rem;margin-bottom:0.25rem">{{ t.sup_boosts_h3 }}</h3>
<p style="text-align:center;color:#64748B;font-size:0.8125rem;margin-bottom:1rem">{{ t.sup_boosts_sub }}</p>
@@ -543,7 +579,7 @@
</div>
<div class="boost-card">
<strong>{{ t.sup_boost_color }}</strong>
<span class="boost-price">&euro;19/mo</span>
<span class="boost-price">&euro;59/mo</span>
</div>
</div>
</section>
@@ -566,7 +602,7 @@
<tbody>
<tr>
<td>{{ t.sup_cmp_row1 }}</td>
<td class="col-us">&euro;1,799/yr</td>
<td class="col-us">&euro;1,799/yr <small style="font-weight:400;font-size:0.6875rem">(yearly plan)</small></td>
<td>&euro;10,000+/event</td>
<td>&euro;5,000+/yr*</td>
<td>&euro;600/yr</td>
@@ -612,22 +648,6 @@
<p class="comparison-footnote">{{ t.sup_cmp_footnote }}</p>
</section>
<!-- Social proof -->
<section class="sup-section">
<h2>{{ t.sup_proof_h2 }}</h2>
<p class="sub">{{ calc_requests }}+ {{ t.sup_proof_stat1 }} &middot; {{ total_suppliers }}+ {{ t.sup_proof_stat2 }} &middot; {{ total_countries }} {{ t.sup_proof_stat3 }}</p>
<div class="sup-proof-grid">
<div class="sup-proof-card">
<blockquote>&ldquo;{{ t.sup_proof_q1 }}&rdquo;</blockquote>
<cite>{{ t.sup_proof_cite1 }}</cite>
</div>
<div class="sup-proof-card">
<blockquote>&ldquo;{{ t.sup_proof_q2 }}&rdquo;</blockquote>
<cite>{{ t.sup_proof_cite2 }}</cite>
</div>
</div>
</section>
<!-- FAQ -->
<section class="sup-section">
<h2>{{ t.sup_faq_h2 }}</h2>
@@ -679,7 +699,7 @@
<section class="sup-cta">
<h2>{{ t.sup_cta_h2 }}</h2>
<p>{{ t.sup_cta_p }}</p>
<a href="#pricing" class="btn">{{ t.sup_hero_cta }}</a>
<a href="{{ url_for('suppliers.signup') }}" class="btn">{{ t.sup_cta_btn }}</a>
</section>
</main>

View File

@@ -44,23 +44,15 @@ if not PADDLE_API_KEY:
# Maps our internal key -> product name in Paddle.
# The name is used to match existing products on sync.
PRODUCTS = [
# Subscriptions — Basic tier (new)
{
"key": "supplier_basic_monthly",
"name": "Supplier Basic (Monthly)",
"price": 3900,
"currency": CurrencyCode.EUR,
"interval": "month",
"billing_type": "subscription",
},
{
"key": "supplier_basic_yearly",
"name": "Supplier Basic (Yearly)",
"price": 34900,
"currency": CurrencyCode.EUR,
"interval": "year",
"billing_type": "subscription",
},
# NOTE: Basic tier is free — no Paddle subscription product needed.
# Suppliers select Basic during signup without a payment flow.
# These entries are kept as dead/legacy references only; do not create them.
# {
# "key": "supplier_basic_monthly",
# "name": "Supplier Basic (Monthly)",
# "price": 0,
# ...
# },
# Subscriptions — Growth tier (existing monthly + new yearly)
{
"key": "supplier_growth",
@@ -123,7 +115,7 @@ PRODUCTS = [
{
"key": "boost_card_color",
"name": "Boost: Custom Card Color",
"price": 1900,
"price": 5900,
"currency": CurrencyCode.EUR,
"interval": "month",
"billing_type": "subscription",
@@ -176,7 +168,7 @@ PRODUCTS = [
{
"key": "business_plan",
"name": "Padel Business Plan (PDF)",
"price": 9900,
"price": 14900,
"currency": CurrencyCode.EUR,
"billing_type": "one_time",
},

View File

@@ -36,9 +36,9 @@ bp = Blueprint(
PLAN_FEATURES = {
"supplier_basic": {
"name": "Basic",
"monthly_price": 39,
"yearly_price": 349,
"yearly_monthly_equivalent": 29,
"monthly_price": 0,
"yearly_price": 0,
"yearly_monthly_equivalent": 0,
"monthly_credits": 0,
"paddle_key_monthly": "supplier_basic_monthly",
"paddle_key_yearly": "supplier_basic_yearly",
@@ -112,7 +112,7 @@ BOOST_OPTIONS = [
"key": "boost_card_color",
"type": "card_color",
"name_key": "sd_boost_card_color_name",
"price": 19,
"price": 59,
"desc_key": "sd_boost_card_color_desc",
},
]
@@ -535,12 +535,15 @@ async def _get_lead_feed_data(supplier, country="", heat="", timeline="", q="",
leads = await fetch_all(
f"""SELECT lr.*,
EXISTS(SELECT 1 FROM lead_forwards lf WHERE lf.lead_id = lr.id AND lf.supplier_id = ?) as is_unlocked,
(SELECT lf.id FROM lead_forwards lf WHERE lf.lead_id = lr.id AND lf.supplier_id = ? LIMIT 1) as forward_id,
(SELECT lf.created_at FROM lead_forwards lf WHERE lf.lead_id = lr.id AND lf.supplier_id = ? LIMIT 1) as forward_created_at,
(SELECT lf.guarantee_claimed_at FROM lead_forwards lf WHERE lf.lead_id = lr.id AND lf.supplier_id = ? LIMIT 1) as guarantee_claimed_at,
(SELECT lf.id FROM lead_forwards lf WHERE lf.lead_id = lr.id AND lf.supplier_id = ? LIMIT 1) IS NOT NULL as is_unlocked,
(SELECT COUNT(*) FROM lead_forwards lf2 WHERE lf2.lead_id = lr.id) as bidder_count
FROM lead_requests lr
WHERE {where}
ORDER BY lr.created_at DESC LIMIT ?""",
(supplier["id"], *params),
(supplier["id"], supplier["id"], supplier["id"], supplier["id"], *params),
)
countries = await fetch_all(
@@ -643,6 +646,57 @@ async def unlock_lead(token: str):
supplier=updated_supplier,
credit_cost=result["credit_cost"],
scenario_id=scenario_id,
forward_id=result["forward_id"],
)
@bp.route("/leads/<int:forward_id>/guarantee-claim", methods=["POST"])
@_lead_tier_required
@csrf_protect
async def guarantee_claim(forward_id: int):
"""Claim lead-back guarantee: return credits for a non-responding lead.
Validates the 330 day window and contact method, then calls
credits.refund_lead_guarantee(). Returns an updated lead card partial.
"""
from ..credits import GuaranteeAlreadyClaimed, GuaranteeWindowClosed, refund_lead_guarantee
t = get_translations(g.get("lang") or "en")
supplier = g.supplier
form = await request.form
contact_method = form.get("contact_method", "")
if contact_method not in ("email", "phone", "both"):
return t["sd_guarantee_window_error"], 400
try:
await refund_lead_guarantee(
supplier["id"], forward_id, contact_method
)
except GuaranteeAlreadyClaimed:
return t["sd_guarantee_already_claimed"], 409
except GuaranteeWindowClosed:
return t["sd_guarantee_window_error"], 409
except AssertionError:
return "Lead not found.", 404
updated_supplier = await fetch_one(
"SELECT * FROM suppliers WHERE id = ?", (supplier["id"],)
)
forward = await fetch_one(
"""SELECT lf.*, lr.*
FROM lead_forwards lf
JOIN lead_requests lr ON lf.lead_id = lr.id
WHERE lf.id = ?""",
(forward_id,),
)
return await render_template(
"suppliers/partials/lead_card_unlocked.html",
lead=forward,
supplier=updated_supplier,
guarantee_claimed=True,
guarantee_success_msg=t["sd_guarantee_success"],
)

View File

@@ -95,6 +95,52 @@
{% if credit_cost is defined %}
<p style="font-size:0.6875rem;color:#94A3B8;margin-top:0.5rem;text-align:center">{{ credit_cost }} {{ t.sd_unlocked_credits_used }} &middot; {{ supplier.credit_balance }} {{ t.sd_unlocked_remaining }}</p>
{% endif %}
{# --- Lead-Back Guarantee button ---
Show after 3 days, hide after 30 days, hide if already claimed.
forward_id comes from routes (unlock response) or from lead.forward_id (dashboard feed).
#}
{% set _fid = forward_id if forward_id is defined else lead.get('forward_id') %}
{% set _claimed_at = (guarantee_claimed_at if guarantee_claimed_at is defined else lead.get('guarantee_claimed_at')) %}
{% set _forward_created = lead.get('forward_created_at') or lead.get('created_at') %}
{% if guarantee_claimed %}
<p style="font-size:0.8125rem;color:#16A34A;font-weight:600;margin-top:0.75rem;text-align:center">
&#10003; {{ guarantee_success_msg or t.sd_guarantee_success }}
</p>
{% elif _fid and not _claimed_at %}
{# JS calculates age client-side to avoid server-side timezone math in template #}
<div id="guarantee-btn-{{ _fid }}" style="margin-top:0.75rem">
<script>
(function() {
var fwd = "{{ _fid }}";
var createdAt = "{{ _forward_created }}";
if (!createdAt) return;
var ageDays = (Date.now() - new Date(createdAt).getTime()) / 86400000;
if (ageDays < 3 || ageDays > 30) return;
var container = document.getElementById("guarantee-btn-" + fwd);
if (!container) return;
container.innerHTML = '<details style="border:1px solid #D1FAE5;border-radius:10px;padding:0.75rem;">' +
'<summary style="cursor:pointer;font-size:0.8125rem;color:#16A34A;font-weight:600;list-style:none">' +
'&#x1F6E1;&#xFE0F; {{ t.sd_guarantee_btn | e }}</summary>' +
'<form hx-post="{{ url_for("suppliers.guarantee_claim", forward_id=0) }}'.replace("/0/", "/" + fwd + "/") +
'" hx-target="#lead-card-' + (typeof lead_card_id !== "undefined" ? lead_card_id : fwd) + '" hx-swap="innerHTML"' +
' style="margin-top:0.75rem">' +
'<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">' +
'<p style="font-size:0.8125rem;color:#475569;margin:0 0 0.5rem">{{ t.sd_guarantee_contact_label | e }}</p>' +
'<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:0.75rem">' +
'<label style="font-size:0.8125rem"><input type="radio" name="contact_method" value="email" required> {{ t.sd_guarantee_contact_email | e }}</label>' +
'<label style="font-size:0.8125rem"><input type="radio" name="contact_method" value="phone"> {{ t.sd_guarantee_contact_phone | e }}</label>' +
'<label style="font-size:0.8125rem"><input type="radio" name="contact_method" value="both"> {{ t.sd_guarantee_contact_both | e }}</label>' +
'</div>' +
'<button type="submit" style="width:100%;padding:8px;font-size:0.8125rem;font-weight:600;background:#16A34A;color:white;border:none;border-radius:8px;cursor:pointer;font-family:inherit">{{ t.sd_guarantee_submit | e }}</button>' +
'</form></details>';
})();
</script>
</div>
{% elif _claimed_at %}
<p style="font-size:0.75rem;color:#94A3B8;margin-top:0.5rem;text-align:center">{{ t.sd_guarantee_already_claimed }}</p>
{% endif %}
</div>
{% if credit_cost is defined %}

View File

@@ -64,12 +64,22 @@
{% if key == 'supplier_growth' %}<div class="s-plan-card__popular">{{ t.sup_step1_popular }}</div>{% endif %}
<h3>{{ plan.name }}</h3>
<div class="price-yearly">
{% if plan.yearly_price == 0 %}
<div class="price">{{ t.sup_basic_free_label }}</div>
<div style="font-size:0.6875rem;color:#16A34A;margin-top:2px">{{ t.sup_step1_free_forever }}</div>
{% else %}
<div class="price">&euro;{{ plan.yearly_monthly_equivalent }} <span>/mo</span></div>
<div style="font-size:0.6875rem;color:#94A3B8;margin-top:2px">{{ t.sup_step1_billed_yearly | tformat(price=plan.yearly_price) }}</div>
{% endif %}
</div>
<div class="price-monthly">
{% if plan.monthly_price == 0 %}
<div class="price">{{ t.sup_basic_free_label }}</div>
<div style="font-size:0.6875rem;color:#16A34A;margin-top:2px">{{ t.sup_step1_free_forever }}</div>
{% else %}
<div class="price">&euro;{{ plan.monthly_price }} <span>/mo</span></div>
<div style="font-size:0.6875rem;color:#94A3B8;margin-top:2px">{{ t.sup_step1_billed_monthly }}</div>
{% endif %}
</div>
<ul>
{% for key in plan.feature_keys %}