diff --git a/CHANGELOG.md b/CHANGELOG.md index acebf73..6c49a71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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//guarantee-claim`, + `refund_lead_guarantee()` in credits.py, "Lead didn't respond" button on unlocked + lead cards (visible 3–30 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 diff --git a/PROJECT.md b/PROJECT.md index 6f8e556..0d542f9 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -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 (3–30 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. | diff --git a/web/src/padelnomics/credits.py b/web/src/padelnomics/credits.py index fa8c855..127e884 100644 --- a/web/src/padelnomics/credits.py +++ b/web/src/padelnomics/credits.py @@ -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 3–30 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 3–30 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 3–30)" + ) + + 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 diff --git a/web/src/padelnomics/locales/de.json b/web/src/padelnomics/locales/de.json index 883af37..0de3885 100644 --- a/web/src/padelnomics/locales/de.json +++ b/web/src/padelnomics/locales/de.json @@ -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 €20–80 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 {plan_name}-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 24–48 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 24–48 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 {supplier_name}-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 {contact_email}.", "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 = €30.000+ Gewinn. 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 3–30 Tage nach dem Freischalten verfügbar).", + "sd_guarantee_already_claimed": "Du hast für diesen Lead bereits eine Rückerstattung beantragt." +} \ No newline at end of file diff --git a/web/src/padelnomics/locales/en.json b/web/src/padelnomics/locales/en.json index 811003e..3bcdc71 100644 --- a/web/src/padelnomics/locales/en.json +++ b/web/src/padelnomics/locales/en.json @@ -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?” We’ll 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 they’re serious, they have a realistic budget, and they’re ready to talk to suppliers. You’re not getting cold enquiries; you’re 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 €20–80 per click — that’s €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 we’ll add you to the directory within 48 hours.", "sup_cta_h2": "Your Next Client Is Already Building a Business Plan", "sup_cta_p": "They’ve modeled the ROI. They know their budget. They’re 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 doesn’t respond, claim your credits back with one click. No support tickets, no waiting — just click \"Lead didn’t 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 = €30,000+ in profit. 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 didn’t 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 3–30 days after unlock).", + "sd_guarantee_already_claimed": "You’ve 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." -} +} \ No newline at end of file diff --git a/web/src/padelnomics/migrations/versions/0020_lead_guarantee_columns.py b/web/src/padelnomics/migrations/versions/0020_lead_guarantee_columns.py new file mode 100644 index 0000000..f3c132d --- /dev/null +++ b/web/src/padelnomics/migrations/versions/0020_lead_guarantee_columns.py @@ -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" + ) diff --git a/web/src/padelnomics/planner/templates/export.html b/web/src/padelnomics/planner/templates/export.html index 9d9d9af..70f99fa 100644 --- a/web/src/padelnomics/planner/templates/export.html +++ b/web/src/padelnomics/planner/templates/export.html @@ -43,7 +43,7 @@

{{ t.export_subtitle }}

-
€99 one-time
+
€149 one-time
{{ t.sup_growth_cta }} +

{{ t.sup_pricing_eur_note }}

@@ -518,9 +543,20 @@
  • {{ t.sup_pro_f6 }}
  • {{ t.sup_pro_cta }} +

    {{ t.sup_pricing_eur_note }}

    + +
    +

    {{ t.sup_roi_line }}

    +
    + + +
    +

    {{ t.sup_credits_only_pre }} {{ t.sup_credits_only_cta }}

    +
    +

    {{ t.sup_boosts_h3 }}

    {{ t.sup_boosts_sub }}

    @@ -543,7 +579,7 @@
    {{ t.sup_boost_color }} - €19/mo + €59/mo
    @@ -566,7 +602,7 @@ {{ t.sup_cmp_row1 }} - €1,799/yr + €1,799/yr (yearly plan) €10,000+/event €5,000+/yr* €600/yr @@ -612,22 +648,6 @@

    {{ t.sup_cmp_footnote }}

    - -
    -

    {{ t.sup_proof_h2 }}

    -

    {{ calc_requests }}+ {{ t.sup_proof_stat1 }} · {{ total_suppliers }}+ {{ t.sup_proof_stat2 }} · {{ total_countries }} {{ t.sup_proof_stat3 }}

    -
    -
    -
    “{{ t.sup_proof_q1 }}”
    - {{ t.sup_proof_cite1 }} -
    -
    -
    “{{ t.sup_proof_q2 }}”
    - {{ t.sup_proof_cite2 }} -
    -
    -
    -

    {{ t.sup_faq_h2 }}

    @@ -679,7 +699,7 @@

    {{ t.sup_cta_h2 }}

    {{ t.sup_cta_p }}

    - {{ t.sup_hero_cta }} + {{ t.sup_cta_btn }}
    diff --git a/web/src/padelnomics/scripts/setup_paddle.py b/web/src/padelnomics/scripts/setup_paddle.py index 849d457..fce1488 100644 --- a/web/src/padelnomics/scripts/setup_paddle.py +++ b/web/src/padelnomics/scripts/setup_paddle.py @@ -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", }, diff --git a/web/src/padelnomics/suppliers/routes.py b/web/src/padelnomics/suppliers/routes.py index f38c442..2d51faf 100644 --- a/web/src/padelnomics/suppliers/routes.py +++ b/web/src/padelnomics/suppliers/routes.py @@ -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//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 3–30 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"], ) diff --git a/web/src/padelnomics/suppliers/templates/suppliers/partials/lead_card_unlocked.html b/web/src/padelnomics/suppliers/templates/suppliers/partials/lead_card_unlocked.html index 4fdbe52..2219c1f 100644 --- a/web/src/padelnomics/suppliers/templates/suppliers/partials/lead_card_unlocked.html +++ b/web/src/padelnomics/suppliers/templates/suppliers/partials/lead_card_unlocked.html @@ -95,6 +95,52 @@ {% if credit_cost is defined %}

    {{ credit_cost }} {{ t.sd_unlocked_credits_used }} · {{ supplier.credit_balance }} {{ t.sd_unlocked_remaining }}

    {% 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 %} +

    + ✓ {{ guarantee_success_msg or t.sd_guarantee_success }} +

    + {% elif _fid and not _claimed_at %} + {# JS calculates age client-side to avoid server-side timezone math in template #} +
    + +
    + {% elif _claimed_at %} +

    {{ t.sd_guarantee_already_claimed }}

    + {% endif %} {% if credit_cost is defined %} diff --git a/web/src/padelnomics/suppliers/templates/suppliers/partials/signup_step_1.html b/web/src/padelnomics/suppliers/templates/suppliers/partials/signup_step_1.html index 8afd96c..cd48e4d 100644 --- a/web/src/padelnomics/suppliers/templates/suppliers/partials/signup_step_1.html +++ b/web/src/padelnomics/suppliers/templates/suppliers/partials/signup_step_1.html @@ -64,12 +64,22 @@ {% if key == 'supplier_growth' %}{% endif %}

    {{ plan.name }}

    + {% if plan.yearly_price == 0 %} +
    {{ t.sup_basic_free_label }}
    +
    {{ t.sup_step1_free_forever }}
    + {% else %}
    €{{ plan.yearly_monthly_equivalent }} /mo
    {{ t.sup_step1_billed_yearly | tformat(price=plan.yearly_price) }}
    + {% endif %}
    + {% if plan.monthly_price == 0 %} +
    {{ t.sup_basic_free_label }}
    +
    {{ t.sup_step1_free_forever }}
    + {% else %}
    €{{ plan.monthly_price }} /mo
    {{ t.sup_step1_billed_monthly }}
    + {% endif %}
      {% for key in plan.feature_keys %}