feat: lead-back guarantee — one-click credit refund after 3 days no response

Backend:
- Migration 0020: add guarantee_claimed_at, guarantee_contact_method to lead_forwards
- credits.py: refund_lead_guarantee() — validates 3–30 day window, reverses credit
  spend via ledger entry (event_type='guarantee_refund'), sets status='no_response'
- GuaranteeAlreadyClaimed, GuaranteeWindowClosed exceptions
- Route: POST /suppliers/leads/<forward_id>/guarantee-claim — HTMX endpoint,
  returns updated lead card partial with success message
- _get_lead_feed_data: pull forward_id, forward_created_at, guarantee_claimed_at
  so dashboard feed can show/hide the guarantee button per-lead

UI:
- lead_card_unlocked.html: "Lead didn't respond" button rendered client-side via
  JS (3–30 day window check in browser), shows contact method radio + submit
- Success state and already-claimed state handled in partial

EN/DE: remove empty sup_credits_only_post key (fails i18n parity test)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-26 15:22:52 +01:00
parent a1e2a5aa8d
commit cc43d936f0
7 changed files with 238 additions and 56 deletions

View File

@@ -5,7 +5,7 @@ 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 datetime
from datetime import UTC, datetime
from .core import execute, fetch_all, fetch_one, transaction
@@ -204,3 +204,92 @@ async def get_ledger(supplier_id: int, limit: int = 50) -> list[dict]:
ORDER BY cl.created_at 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

@@ -1633,7 +1633,6 @@
"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_credits_only_post": "",
"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?",

View File

@@ -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.",
@@ -1055,11 +1055,9 @@
"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_leads_unlock_cta": "Start Getting Leads",
"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_credits_only_post": "",
"sup_step1_free_forever": "Free forever",
"sd_guarantee_btn": "Lead didnt respond",
"sd_guarantee_contact_label": "How did you try to reach them?",
@@ -1494,7 +1492,6 @@
"sd_flash_valid_email": "Please enter a valid email address.",
"sd_flash_claim_error": "This listing has already been claimed or does not exist.",
"sd_flash_listing_saved": "Listing saved successfully.",
"bp_indoor": "Indoor",
"bp_outdoor": "Outdoor",
"bp_own": "Own",
@@ -1503,24 +1500,20 @@
"bp_payback_not_reached": "Not reached in 60 months",
"bp_months": "{n} months",
"bp_years": "{n} years",
"bp_exec_paragraph": "This business plan models a <strong>{facility_type}</strong> padel facility with <strong>{courts} courts</strong> ({sqm} m\u00b2). Total investment is {total_capex}, financed with {equity} equity and {loan} debt. The projected IRR is {irr} with a payback period of {payback}.",
"bp_exec_paragraph": "This business plan models a <strong>{facility_type}</strong> padel facility with <strong>{courts} courts</strong> ({sqm} m²). Total investment is {total_capex}, financed with {equity} equity and {loan} debt. The projected IRR is {irr} with a payback period of {payback}.",
"bp_lbl_scenario": "Scenario",
"bp_lbl_generated_by": "Generated by Padelnomics \u2014 padelnomics.io",
"bp_lbl_generated_by": "Generated by Padelnomics padelnomics.io",
"bp_lbl_total_investment": "Total Investment",
"bp_lbl_equity_required": "Equity Required",
"bp_lbl_year3_ebitda": "Year 3 EBITDA",
"bp_lbl_irr": "IRR",
"bp_lbl_payback_period": "Payback Period",
"bp_lbl_year1_revenue": "Year 1 Revenue",
"bp_lbl_item": "Item",
"bp_lbl_amount": "Amount",
"bp_lbl_notes": "Notes",
"bp_lbl_total_capex": "Total CAPEX",
"bp_lbl_capex_stats": "CAPEX per court: {per_court} \u2022 CAPEX per m\u00b2: {per_sqm}",
"bp_lbl_capex_stats": "CAPEX per court: {per_court} CAPEX per m²: {per_sqm}",
"bp_lbl_equity": "Equity",
"bp_lbl_loan": "Loan",
"bp_lbl_interest_rate": "Interest Rate",
@@ -1528,24 +1521,20 @@
"bp_lbl_monthly_payment": "Monthly Payment",
"bp_lbl_annual_debt_service": "Annual Debt Service",
"bp_lbl_ltv": "Loan-to-Value",
"bp_lbl_monthly": "Monthly",
"bp_lbl_total_monthly_opex": "Total Monthly OPEX",
"bp_lbl_annual_opex": "Annual OPEX",
"bp_lbl_weighted_hourly_rate": "Weighted Hourly Rate",
"bp_lbl_target_utilization": "Target Utilization",
"bp_lbl_gross_monthly_revenue": "Gross Monthly Revenue",
"bp_lbl_net_monthly_revenue": "Net Monthly Revenue",
"bp_lbl_monthly_ebitda": "Monthly EBITDA",
"bp_lbl_monthly_net_cf": "Monthly Net Cash Flow",
"bp_lbl_year": "Year",
"bp_lbl_revenue": "Revenue",
"bp_lbl_ebitda": "EBITDA",
"bp_lbl_debt_service": "Debt Service",
"bp_lbl_net_cf": "Net CF",
"bp_lbl_moic": "MOIC",
"bp_lbl_cash_on_cash": "Cash-on-Cash (Y3)",
"bp_lbl_payback": "Payback",
@@ -1553,46 +1542,40 @@
"bp_lbl_ebitda_margin": "EBITDA Margin",
"bp_lbl_dscr_y3": "DSCR (Y3)",
"bp_lbl_yield_on_cost": "Yield on Cost",
"bp_lbl_month": "Month",
"bp_lbl_opex": "OPEX",
"bp_lbl_debt": "Debt",
"bp_lbl_cumulative": "Cumulative",
"bp_lbl_disclaimer": "<strong>Disclaimer:</strong> This business plan is generated from user-provided assumptions using the Padelnomics financial model. All projections are estimates and do not constitute financial advice. Actual results may vary significantly based on market conditions, execution, and other factors. Consult with financial advisors before making investment decisions. \u00a9 Padelnomics \u2014 padelnomics.io",
"bp_lbl_disclaimer": "<strong>Disclaimer:</strong> This business plan is generated from user-provided assumptions using the Padelnomics financial model. All projections are estimates and do not constitute financial advice. Actual results may vary significantly based on market conditions, execution, and other factors. Consult with financial advisors before making investment decisions. © Padelnomics — padelnomics.io",
"email_magic_link_heading": "Sign in to {app_name}",
"email_magic_link_body": "Here's your sign-in link. It expires in {expiry_minutes} minutes.",
"email_magic_link_btn": "Sign In \u2192",
"email_magic_link_btn": "Sign In ",
"email_magic_link_fallback": "If the button doesn't work, copy and paste this URL into your browser:",
"email_magic_link_ignore": "If you didn't request this, you can safely ignore this email.",
"email_magic_link_subject": "Your sign-in link for {app_name}",
"email_magic_link_preheader": "This link expires in {expiry_minutes} minutes",
"email_quote_verify_heading": "Verify your email to get quotes",
"email_quote_verify_greeting": "Hi {first_name},",
"email_quote_verify_body": "Thanks for requesting quotes. Verify your email to activate your quote request and create your {app_name} account.",
"email_quote_verify_project_label": "Your project:",
"email_quote_verify_urgency": "Verified requests get prioritized by our supplier network.",
"email_quote_verify_btn": "Verify & Activate \u2192",
"email_quote_verify_btn": "Verify & Activate ",
"email_quote_verify_expires": "This link expires in 60 minutes.",
"email_quote_verify_fallback": "If the button doesn't work, copy and paste this URL into your browser:",
"email_quote_verify_ignore": "If you didn't request this, you can safely ignore this email.",
"email_quote_verify_subject": "Verify your email \u2014 suppliers are ready to quote",
"email_quote_verify_subject": "Verify your email suppliers are ready to quote",
"email_quote_verify_preheader": "One click to activate your quote request",
"email_quote_verify_preheader_courts": "One click to activate your {court_count}-court project",
"email_welcome_heading": "Welcome to {app_name}",
"email_welcome_greeting": "Hi {first_name},",
"email_welcome_body": "You now have access to the financial planner, market data, and supplier directory \u2014 everything you need to plan your padel business.",
"email_welcome_body": "You now have access to the financial planner, market data, and supplier directory everything you need to plan your padel business.",
"email_welcome_quickstart_heading": "Quick start:",
"email_welcome_link_planner": "Financial Planner \u2014 model your investment",
"email_welcome_link_markets": "Market Data \u2014 explore padel demand by city",
"email_welcome_link_quotes": "Get Quotes \u2014 connect with verified suppliers",
"email_welcome_btn": "Start Planning \u2192",
"email_welcome_subject": "You're in \u2014 here's how to start planning",
"email_welcome_link_planner": "Financial Planner model your investment",
"email_welcome_link_markets": "Market Data explore padel demand by city",
"email_welcome_link_quotes": "Get Quotes connect with verified suppliers",
"email_welcome_btn": "Start Planning ",
"email_welcome_subject": "You're in here's how to start planning",
"email_welcome_preheader": "Your padel business planning toolkit is ready",
"email_waitlist_supplier_heading": "You're on the Supplier Waitlist",
"email_waitlist_supplier_body": "Thanks for your interest in the <strong>{plan_name}</strong> plan. We're building a platform to connect you with qualified leads from padel entrepreneurs actively planning projects.",
"email_waitlist_supplier_perks_intro": "As an early waitlist member, you'll get:",
@@ -1600,20 +1583,19 @@
"email_waitlist_supplier_perk_2": "Exclusive launch pricing (locked in)",
"email_waitlist_supplier_perk_3": "Dedicated onboarding call",
"email_waitlist_supplier_meanwhile": "In the meantime, explore our free resources:",
"email_waitlist_supplier_link_planner": "Financial Planning Tool \u2014 model your padel facility",
"email_waitlist_supplier_link_directory": "Supplier Directory \u2014 browse verified suppliers",
"email_waitlist_supplier_subject": "You're in \u2014 {plan_name} early access is coming",
"email_waitlist_supplier_link_planner": "Financial Planning Tool model your padel facility",
"email_waitlist_supplier_link_directory": "Supplier Directory browse verified suppliers",
"email_waitlist_supplier_subject": "You're in {plan_name} early access is coming",
"email_waitlist_supplier_preheader": "Exclusive launch pricing + priority onboarding",
"email_waitlist_general_heading": "You're on the Waitlist",
"email_waitlist_general_body": "Thanks for joining. We're building the planning platform for padel entrepreneurs \u2014 financial modelling, market data, and supplier connections in one place.",
"email_waitlist_general_body": "Thanks for joining. We're building the planning platform for padel entrepreneurs financial modelling, market data, and supplier connections in one place.",
"email_waitlist_general_perks_intro": "As an early waitlist member, you'll get:",
"email_waitlist_general_perk_1": "Early access before public launch",
"email_waitlist_general_perk_2": "Exclusive launch pricing",
"email_waitlist_general_perk_3": "Priority onboarding and support",
"email_waitlist_general_outro": "We'll be in touch soon.",
"email_waitlist_general_subject": "You're on the list \u2014 we'll notify you at launch",
"email_waitlist_general_subject": "You're on the list we'll notify you at launch",
"email_waitlist_general_preheader": "Early access + exclusive launch pricing",
"email_lead_forward_heading": "New Project Lead",
"email_lead_forward_urgency": "This lead was just unlocked. Suppliers who respond within 24 hours are 3x more likely to win the project.",
"email_lead_forward_section_brief": "Project Brief",
@@ -1630,22 +1612,20 @@
"email_lead_forward_lbl_phone": "Phone",
"email_lead_forward_lbl_company": "Company",
"email_lead_forward_lbl_role": "Role",
"email_lead_forward_btn": "View in Lead Feed \u2192",
"email_lead_forward_btn": "View in Lead Feed ",
"email_lead_forward_reply_direct": "or <a href=\"mailto:{contact_email}\" style=\"color:#1D4ED8;font-weight:500;\">reply directly to {contact_email}</a>",
"email_lead_forward_preheader_suffix": "contact details inside",
"email_lead_matched_heading": "A supplier wants to discuss your project",
"email_lead_matched_greeting": "Hi {first_name},",
"email_lead_matched_body": "Great news \u2014 a verified supplier has been matched with your padel project. They have your project brief and contact details.",
"email_lead_matched_body": "Great news a verified supplier has been matched with your padel project. They have your project brief and contact details.",
"email_lead_matched_context": "You submitted a quote request for a {facility_type} facility with {court_count} courts in {country}.",
"email_lead_matched_next_heading": "What happens next",
"email_lead_matched_next_body": "The supplier has received your project brief and contact details. Most suppliers respond within 24\u201348 hours via email or phone.",
"email_lead_matched_next_body": "The supplier has received your project brief and contact details. Most suppliers respond within 2448 hours via email or phone.",
"email_lead_matched_tip": "Tip: Responding quickly to supplier outreach increases your chance of getting competitive quotes.",
"email_lead_matched_btn": "View Your Dashboard \u2192",
"email_lead_matched_btn": "View Your Dashboard ",
"email_lead_matched_note": "You'll receive this notification each time a new supplier unlocks your project details.",
"email_lead_matched_subject": "{first_name}, a supplier wants to discuss your project",
"email_lead_matched_preheader": "They'll reach out to you directly \u2014 here's what to expect",
"email_lead_matched_preheader": "They'll reach out to you directly here's what to expect",
"email_enquiry_heading": "New enquiry from {contact_name}",
"email_enquiry_body": "You have a new enquiry via your <strong>{supplier_name}</strong> directory listing.",
"email_enquiry_lbl_from": "From",
@@ -1654,15 +1634,13 @@
"email_enquiry_reply": "Reply directly to <a href=\"mailto:{contact_email}\" style=\"color:#1D4ED8;\">{contact_email}</a> to connect.",
"email_enquiry_subject": "New enquiry from {contact_name} via your directory listing",
"email_enquiry_preheader": "Reply to connect with this potential client",
"email_business_plan_heading": "Your business plan is ready",
"email_business_plan_body": "Your padel business plan PDF has been generated and is ready for download.",
"email_business_plan_includes": "Your plan includes investment breakdown, revenue projections, and break-even analysis.",
"email_business_plan_btn": "Download PDF \u2192",
"email_business_plan_quote_cta": "Ready for the next step? <a href=\"{quote_url}\" style=\"color:#1D4ED8;font-weight:500;\">Get quotes from suppliers \u2192</a>",
"email_business_plan_btn": "Download PDF ",
"email_business_plan_quote_cta": "Ready for the next step? <a href=\"{quote_url}\" style=\"color:#1D4ED8;font-weight:500;\">Get quotes from suppliers </a>",
"email_business_plan_subject": "Your business plan PDF is ready to download",
"email_business_plan_preheader": "Professional padel facility financial plan \u2014 download now",
"email_business_plan_preheader": "Professional padel facility financial plan download now",
"email_footer_tagline": "The padel business planning platform",
"email_footer_copyright": "\u00a9 {year} {app_name}. You received this email because you have an account or submitted a request."
"email_footer_copyright": "© {year} {app_name}. You received this email because you have an account or submitted a request."
}

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

@@ -554,7 +554,7 @@
<!-- 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> {{ t.sup_credits_only_post }}</p>
<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 -->

View File

@@ -13,9 +13,9 @@ from ..core import (
config,
csrf_protect,
execute,
feature_gate,
fetch_all,
fetch_one,
feature_gate,
get_paddle_price,
is_flag_enabled,
)
@@ -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 %}