Files
padelnomics/web/src/padelnomics/scripts/seed_dev_data.py
Deeman 0984657e72 fix(affiliate): sidebar active state, subnav order, dev seed data
- base_admin.html: add 'affiliate_dashboard' to _section_map so Dashboard
  page stays under the Affiliate section (was falling through to 'overview')
- base_admin.html: sidebar Affiliate link now points to dashboard (first tab)
- base_admin.html: subnav order Dashboard | Products (was Products | Dashboard)
- seed_dev_data.py: add 10 affiliate products (4 rackets, 2 shoes, 1 ball,
  1 grip, 1 bag) + 236 click events spread over 30 days for dashboard charts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 21:32:20 +01:00

739 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Seed realistic test data for local development.
Creates suppliers, leads, a dev user, credit ledger entries, and lead forwards.
Usage:
uv run python -m padelnomics.scripts.seed_dev_data
"""
import logging
import os
import sqlite3
import sys
from datetime import UTC, datetime, timedelta
from pathlib import Path
from dotenv import load_dotenv
logger = logging.getLogger(__name__)
load_dotenv()
DATABASE_PATH = os.getenv("DATABASE_PATH", "data/app.db")
def slugify(name: str) -> str:
return name.lower().replace(" ", "-").replace("&", "and").replace(",", "")
SUPPLIERS = [
{
"name": "PadelTech GmbH",
"slug": "padeltech-gmbh",
"country_code": "DE",
"city": "Munich",
"region": "Europe",
"website": "https://padeltech.example.com",
"description": "Premium padel court manufacturer with 15+ years experience in Central Europe.",
"category": "manufacturer",
"tier": "pro",
"credit_balance": 80,
"monthly_credits": 100,
"contact_name": "Hans Weber",
"contact_email": "hans@padeltech.example.com",
"years_in_business": 15,
"project_count": 120,
"service_area": "DE,AT,CH",
},
{
"name": "CourtBuild Spain",
"slug": "courtbuild-spain",
"country_code": "ES",
"city": "Madrid",
"region": "Europe",
"website": "https://courtbuild.example.com",
"description": "Turnkey padel facility builder serving Spain and Portugal.",
"category": "turnkey",
"tier": "growth",
"credit_balance": 25,
"monthly_credits": 30,
"contact_name": "Maria Garcia",
"contact_email": "maria@courtbuild.example.com",
"years_in_business": 8,
"project_count": 45,
"service_area": "ES,PT",
},
{
"name": "Nordic Padel Solutions",
"slug": "nordic-padel-solutions",
"country_code": "SE",
"city": "Stockholm",
"region": "Europe",
"website": "https://nordicpadel.example.com",
"description": "Indoor padel specialists for the Nordic climate.",
"category": "manufacturer",
"tier": "free",
"credit_balance": 0,
"monthly_credits": 0,
"contact_name": "Erik Lindqvist",
"contact_email": "erik@nordicpadel.example.com",
"years_in_business": 5,
"project_count": 20,
"service_area": "SE,DK,FI,NO",
},
{
"name": "PadelLux Consulting",
"slug": "padellux-consulting",
"country_code": "NL",
"city": "Amsterdam",
"region": "Europe",
"website": "https://padellux.example.com",
"description": "Independent padel consultants helping investors plan profitable facilities.",
"category": "consultant",
"tier": "growth",
"credit_balance": 18,
"monthly_credits": 30,
"contact_name": "Jan de Vries",
"contact_email": "jan@padellux.example.com",
"years_in_business": 3,
"project_count": 12,
"service_area": "NL,BE,DE",
},
{
"name": "Desert Padel FZE",
"slug": "desert-padel-fze",
"country_code": "AE",
"city": "Dubai",
"region": "Middle East",
"website": "https://desertpadel.example.com",
"description": "Outdoor and covered padel courts designed for extreme heat environments.",
"category": "turnkey",
"tier": "pro",
"credit_balance": 90,
"monthly_credits": 100,
"contact_name": "Ahmed Al-Rashid",
"contact_email": "ahmed@desertpadel.example.com",
"years_in_business": 6,
"project_count": 30,
"service_area": "AE,SA",
},
]
LEADS = [
{
"facility_type": "indoor",
"court_count": 6,
"country": "DE",
"city": "Berlin",
"timeline": "3-6mo",
"budget_estimate": 450000,
"heat_score": "hot",
"status": "new",
"contact_name": "Thomas Mueller",
"contact_email": "thomas@example.com",
"stakeholder_type": "entrepreneur",
"financing_status": "loan_approved",
"location_status": "lease_signed",
"decision_process": "solo",
},
{
"facility_type": "outdoor",
"court_count": 4,
"country": "ES",
"city": "Barcelona",
"timeline": "asap",
"budget_estimate": 280000,
"heat_score": "hot",
"status": "new",
"contact_name": "Carlos Ruiz",
"contact_email": "carlos@example.com",
"stakeholder_type": "tennis_club",
"financing_status": "self_funded",
"location_status": "permit_granted",
"decision_process": "partners",
},
{
"facility_type": "both",
"court_count": 8,
"country": "SE",
"city": "Gothenburg",
"timeline": "6-12mo",
"budget_estimate": 720000,
"heat_score": "warm",
"status": "new",
"contact_name": "Anna Svensson",
"contact_email": "anna@example.com",
"stakeholder_type": "developer",
"financing_status": "seeking",
"location_status": "location_found",
"decision_process": "committee",
},
{
"facility_type": "indoor",
"court_count": 4,
"country": "NL",
"city": "Rotterdam",
"timeline": "3-6mo",
"budget_estimate": 350000,
"heat_score": "warm",
"status": "new",
"contact_name": "Pieter van Dijk",
"contact_email": "pieter@example.com",
"stakeholder_type": "entrepreneur",
"financing_status": "self_funded",
"location_status": "converting_existing",
"decision_process": "solo",
},
{
"facility_type": "indoor",
"court_count": 10,
"country": "DE",
"city": "Hamburg",
"timeline": "asap",
"budget_estimate": 900000,
"heat_score": "hot",
"status": "forwarded",
"contact_name": "Lisa Braun",
"contact_email": "lisa@example.com",
"stakeholder_type": "operator",
"financing_status": "loan_approved",
"location_status": "permit_pending",
"decision_process": "partners",
},
{
"facility_type": "outdoor",
"court_count": 3,
"country": "AE",
"city": "Abu Dhabi",
"timeline": "12+mo",
"budget_estimate": 200000,
"heat_score": "cool",
"status": "new",
"contact_name": "Fatima Hassan",
"contact_email": "fatima@example.com",
"stakeholder_type": "municipality",
"financing_status": "not_started",
"location_status": "still_searching",
"decision_process": "committee",
},
{
"facility_type": "indoor",
"court_count": 6,
"country": "FR",
"city": "Lyon",
"timeline": "6-12mo",
"budget_estimate": 500000,
"heat_score": "warm",
"status": "new",
"contact_name": "Jean Dupont",
"contact_email": "jean@example.com",
"stakeholder_type": "entrepreneur",
"financing_status": "seeking",
"location_status": "location_found",
"decision_process": "solo",
},
{
"facility_type": "both",
"court_count": 12,
"country": "IT",
"city": "Milan",
"timeline": "3-6mo",
"budget_estimate": 1200000,
"heat_score": "hot",
"status": "new",
"contact_name": "Marco Rossi",
"contact_email": "marco@example.com",
"stakeholder_type": "developer",
"financing_status": "loan_approved",
"location_status": "permit_granted",
"decision_process": "partners",
},
{
"facility_type": "indoor",
"court_count": 4,
"country": "GB",
"city": "Manchester",
"timeline": "6-12mo",
"budget_estimate": 400000,
"heat_score": "warm",
"status": "pending_verification",
"contact_name": "James Wilson",
"contact_email": "james@example.com",
"stakeholder_type": "tennis_club",
"financing_status": "seeking",
"location_status": "converting_existing",
"decision_process": "committee",
},
{
"facility_type": "outdoor",
"court_count": 2,
"country": "PT",
"city": "Lisbon",
"timeline": "12+mo",
"budget_estimate": 150000,
"heat_score": "cool",
"status": "new",
"contact_name": "Sofia Costa",
"contact_email": "sofia@example.com",
"stakeholder_type": "entrepreneur",
"financing_status": "not_started",
"location_status": "still_searching",
"decision_process": "solo",
},
]
AFFILIATE_PRODUCTS = [
# Rackets
{
"slug": "bullpadel-vertex-04-amazon",
"name": "Bullpadel Vertex 04",
"brand": "Bullpadel",
"category": "racket",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST01?tag=padelnomics-21",
"price_cents": 17999,
"rating": 4.7,
"pros": '["Carbon-Rahmen für maximale Power", "Diamant-Form für aggressive Spieler", "Sehr gute Balance"]',
"cons": '["Nur für fortgeschrittene Spieler", "Höherer Preis"]',
"description": "Der Vertex 04 ist der Flaggschiff-Schläger von Bullpadel für Power-Spieler.",
"status": "active",
"language": "de",
"sort_order": 1,
},
{
"slug": "head-delta-pro-amazon",
"name": "HEAD Delta Pro",
"brand": "HEAD",
"category": "racket",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST02?tag=padelnomics-21",
"price_cents": 14999,
"rating": 4.5,
"pros": '["Sehr kontrollorientiert", "Ideal für Defensivspieler", "Leicht"]',
"cons": '["Weniger Power als Diamant-Formen"]',
"description": "Runde Form mit perfekter Kontrolle — ideal für Einsteiger und Defensivspieler.",
"status": "active",
"language": "de",
"sort_order": 2,
},
{
"slug": "adidas-metalbone-30-amazon",
"name": "Adidas Metalbone 3.0",
"brand": "Adidas",
"category": "racket",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST03?tag=padelnomics-21",
"price_cents": 18999,
"rating": 4.8,
"pros": '["Brutale Power", "Hochwertige Verarbeitung", "Sehr beliebt auf Pro-Tour"]',
"cons": '["Teuer", "Gewöhnungsbedürftig"]',
"description": "Das Flaggschiff von Adidas Padel — getragen von den besten Profis der Welt.",
"status": "active",
"language": "de",
"sort_order": 3,
},
{
"slug": "wilson-bela-pro-v2-amazon",
"name": "Wilson Bela Pro v2",
"brand": "Wilson",
"category": "racket",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST04?tag=padelnomics-21",
"price_cents": 16999,
"rating": 4.6,
"pros": '["Bekannter Signature-Schläger", "Gute Mischung aus Power und Kontrolle"]',
"cons": '["Fortgeschrittene bevorzugt"]',
"description": "Der Schläger von Fernando Belasteguín — einer der meistgekauften Schläger weltweit.",
"status": "active",
"language": "de",
"sort_order": 4,
},
# Beginner racket — draft (tests that draft products are excluded from public)
{
"slug": "dunlop-aero-star-amazon",
"name": "Dunlop Aero Star",
"brand": "Dunlop",
"category": "racket",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST05?tag=padelnomics-21",
"price_cents": 8999,
"rating": 4.2,
"pros": '["Günstig", "Für Einsteiger ideal"]',
"cons": '["Wenig Power für Fortgeschrittene"]',
"description": "Solider Einsteigerschläger für unter 90 Euro.",
"status": "draft",
"language": "de",
"sort_order": 5,
},
# Shoes
{
"slug": "adidas-adipower-ctrl-amazon",
"name": "Adidas Adipower Ctrl",
"brand": "Adidas",
"category": "shoe",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST10?tag=padelnomics-21",
"price_cents": 9999,
"rating": 4.4,
"pros": '["Hervorragender Halt auf Sand", "Leicht und atmungsaktiv"]',
"cons": '["Größenfehler möglich — eine Größe größer bestellen"]',
"description": "Professioneller Padelschuh mit optimierter Sohle für Sand- und Kunstrasencourts.",
"status": "active",
"language": "de",
"sort_order": 1,
},
{
"slug": "babolat-jet-premura-amazon",
"name": "Babolat Jet Premura",
"brand": "Babolat",
"category": "shoe",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST11?tag=padelnomics-21",
"price_cents": 11999,
"rating": 4.6,
"pros": '["Sehr leicht", "Gute Dämpfung", "Stylisches Design"]',
"cons": '["Teurer als Mitbewerber"]',
"description": "Ultraleichter Padelschuh von Babolat — ideal für schnelle Spieler.",
"status": "active",
"language": "de",
"sort_order": 2,
},
# Balls
{
"slug": "head-padel-pro-balls-amazon",
"name": "HEAD Padel Pro Bälle (3er-Dose)",
"brand": "HEAD",
"category": "ball",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST20?tag=padelnomics-21",
"price_cents": 799,
"rating": 4.5,
"pros": '["Offizieller Turnierball", "Guter Druckerhalt", "Günstig"]',
"cons": '["Bei intensivem Spiel nach 45 Sessions platter"]',
"description": "Offizieller Turnierball von HEAD — der am häufigsten gespielte Padelball in Europa.",
"status": "active",
"language": "de",
"sort_order": 1,
},
# Grips/Accessories
{
"slug": "bullpadel-overgrip-3er-amazon",
"name": "Bullpadel Overgrip (3er-Pack)",
"brand": "Bullpadel",
"category": "grip",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST30?tag=padelnomics-21",
"price_cents": 499,
"rating": 4.3,
"pros": '["Günstig", "Guter Halt auch bei Schweiß", "Einfach zu wechseln"]',
"cons": '["Hält weniger lang als Originalgriff"]',
"description": "Günstiges Overgrip-Set — jeder Padelspieler sollte regelmäßig wechseln.",
"status": "active",
"language": "de",
"sort_order": 1,
},
{
"slug": "nox-padel-bag-amazon",
"name": "NOX ML10 Schläger-Tasche",
"brand": "NOX",
"category": "accessory",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST40?tag=padelnomics-21",
"price_cents": 5999,
"rating": 4.4,
"pros": '["Platz für 2 Schläger", "Gepolstertes Schlägerfach", "Robustes Material"]',
"cons": '["Kein Schuhfach"]',
"description": "Praktische Padelschläger-Tasche mit Platz für 2 Schläger und Zubehör.",
"status": "active",
"language": "de",
"sort_order": 1,
},
]
# Article slugs for realistic click referrers
_ARTICLE_SLUGS = [
"beste-padelschlaeger-2026",
"padelschlaeger-anfaenger",
"padelschuhe-test",
"padelbaelle-vergleich",
"padel-zubehoer",
]
def main():
db_path = DATABASE_PATH
if not Path(db_path).exists():
logger.error("Database not found at %s. Run migrations first.", db_path)
sys.exit(1)
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
conn.row_factory = sqlite3.Row
now = datetime.now(UTC)
# 1. Create dev user
logger.info("Creating dev user (dev@localhost)...")
existing = conn.execute("SELECT id FROM users WHERE email = 'dev@localhost'").fetchone()
if existing:
dev_user_id = existing["id"]
logger.info(" Already exists (id=%s)", dev_user_id)
else:
cursor = conn.execute(
"INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)",
("dev@localhost", "Dev User", now.strftime("%Y-%m-%d %H:%M:%S")),
)
dev_user_id = cursor.lastrowid
logger.info(" Created (id=%s)", dev_user_id)
# Grant admin role to dev user
conn.execute(
"INSERT OR IGNORE INTO user_roles (user_id, role) VALUES (?, 'admin')",
(dev_user_id,),
)
logger.info(" Admin role granted")
# 2. Seed suppliers
logger.info("Seeding %s suppliers...", len(SUPPLIERS))
supplier_ids = {}
for s in SUPPLIERS:
existing = conn.execute("SELECT id FROM suppliers WHERE slug = ?", (s["slug"],)).fetchone()
if existing:
supplier_ids[s["slug"]] = existing["id"]
logger.info(" %s already exists (id=%s)", s["name"], existing["id"])
continue
cursor = conn.execute(
"""INSERT INTO suppliers
(name, slug, country_code, city, region, website, description, category,
tier, credit_balance, monthly_credits, contact_name, contact_email,
years_in_business, project_count, service_area, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
s["name"], s["slug"], s["country_code"], s["city"], s["region"],
s["website"], s["description"], s["category"], s["tier"],
s["credit_balance"], s["monthly_credits"], s["contact_name"],
s["contact_email"], s["years_in_business"], s["project_count"],
s["service_area"], now.strftime("%Y-%m-%d %H:%M:%S"),
),
)
supplier_ids[s["slug"]] = cursor.lastrowid
logger.info(" %s -> id=%s", s["name"], cursor.lastrowid)
# 3. Claim paid suppliers — each gets its own owner user + subscription
logger.info("Claiming paid suppliers with owner accounts...")
claimed_suppliers = [
("padeltech-gmbh", "supplier_pro", "hans@padeltech.example.com", "Hans Weber"),
("courtbuild-spain", "supplier_growth", "maria@courtbuild.example.com", "Maria Garcia"),
("desert-padel-fze", "supplier_pro", "ahmed@desertpadel.example.com", "Ahmed Al-Rashid"),
]
period_end = (now + timedelta(days=30)).strftime("%Y-%m-%d %H:%M:%S")
for slug, plan, email, name in claimed_suppliers:
sid = supplier_ids.get(slug)
if not sid:
continue
# Create or find owner user
existing_owner = conn.execute(
"SELECT id FROM users WHERE email = ?", (email,)
).fetchone()
if existing_owner:
owner_id = existing_owner["id"]
else:
cursor = conn.execute(
"INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)",
(email, name, now.strftime("%Y-%m-%d %H:%M:%S")),
)
owner_id = cursor.lastrowid
# Claim the supplier
conn.execute(
"UPDATE suppliers SET claimed_by = ?, claimed_at = ? WHERE id = ? AND claimed_by IS NULL",
(owner_id, now.strftime("%Y-%m-%d %H:%M:%S"), sid),
)
# Create billing customer record
existing_bc = conn.execute(
"SELECT id FROM billing_customers WHERE user_id = ?", (owner_id,)
).fetchone()
if not existing_bc:
conn.execute(
"""INSERT INTO billing_customers (user_id, provider_customer_id, created_at)
VALUES (?, ?, ?)""",
(owner_id, f"ctm_dev_{slug}", now.strftime("%Y-%m-%d %H:%M:%S")),
)
# Create active subscription
existing_sub = conn.execute(
"SELECT id FROM subscriptions WHERE user_id = ?", (owner_id,)
).fetchone()
if not existing_sub:
conn.execute(
"""INSERT INTO subscriptions
(user_id, plan, status, provider_subscription_id,
current_period_end, created_at)
VALUES (?, ?, 'active', ?, ?, ?)""",
(owner_id, plan, f"sub_dev_{slug}",
period_end, now.strftime("%Y-%m-%d %H:%M:%S")),
)
logger.info(" %s -> owner %s (%s)", slug, email, plan)
# 4. Seed leads
logger.info("Seeding %s leads...", len(LEADS))
lead_ids = []
for i, lead in enumerate(LEADS):
from padelnomics.credits import HEAT_CREDIT_COSTS
credit_cost = HEAT_CREDIT_COSTS.get(lead["heat_score"], 8)
verified_at = (now - timedelta(hours=i * 2)).isoformat() if lead["status"] not in ("pending_verification",) else None
cursor = conn.execute(
"""INSERT INTO lead_requests
(user_id, lead_type, facility_type, court_count, country, location, timeline,
budget_estimate, heat_score, status, contact_name, contact_email,
stakeholder_type, financing_status, location_status, decision_process,
credit_cost, verified_at, created_at)
VALUES (?, 'quote', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
dev_user_id, lead["facility_type"], lead["court_count"], lead["country"],
lead["city"], lead["timeline"], lead["budget_estimate"],
lead["heat_score"], lead["status"], lead["contact_name"],
lead["contact_email"], lead["stakeholder_type"],
lead["financing_status"], lead["location_status"],
lead["decision_process"], credit_cost, verified_at,
(now - timedelta(hours=i * 3)).isoformat(),
),
)
lead_ids.append(cursor.lastrowid)
logger.info(" Lead #%s: %s (%s, %s)", cursor.lastrowid, lead["contact_name"], lead["heat_score"], lead["country"])
# 5. Add credit ledger entries for claimed suppliers
logger.info("Adding credit ledger entries...")
for slug in ("padeltech-gmbh", "courtbuild-spain", "desert-padel-fze"):
sid = supplier_ids.get(slug)
if not sid:
continue
is_pro = slug in ("padeltech-gmbh", "desert-padel-fze")
monthly = 100 if is_pro else 30
# Monthly allocation
conn.execute(
"""INSERT INTO credit_ledger (supplier_id, delta, balance_after, event_type, note, created_at)
VALUES (?, ?, ?, 'monthly_allocation', 'Monthly refill', ?)""",
(sid, monthly, monthly, (now - timedelta(days=28)).isoformat()),
)
# Admin adjustment
conn.execute(
"""INSERT INTO credit_ledger (supplier_id, delta, balance_after, event_type, note, created_at)
VALUES (?, ?, ?, 'admin_adjustment', 'Welcome bonus', ?)""",
(sid, 10, monthly + 10, (now - timedelta(days=25)).isoformat()),
)
logger.info(" %s: 2 ledger entries", slug)
# 6. Add lead forwards for testing
logger.info("Adding lead forwards...")
padeltech_id = supplier_ids.get("padeltech-gmbh")
if padeltech_id and len(lead_ids) >= 2:
for lead_id in lead_ids[:2]:
existing = conn.execute(
"SELECT 1 FROM lead_forwards WHERE lead_id = ? AND supplier_id = ?",
(lead_id, padeltech_id),
).fetchone()
if not existing:
conn.execute(
"""INSERT INTO lead_forwards (lead_id, supplier_id, credit_cost, status, created_at)
VALUES (?, ?, ?, 'sent', ?)""",
(lead_id, padeltech_id, 35, (now - timedelta(hours=6)).isoformat()),
)
conn.execute(
"UPDATE lead_requests SET unlock_count = unlock_count + 1 WHERE id = ?",
(lead_id,),
)
# Ledger entry for spend
conn.execute(
"""INSERT INTO credit_ledger (supplier_id, delta, balance_after, event_type, reference_id, note, created_at)
VALUES (?, -35, ?, 'lead_unlock', ?, ?, ?)""",
(padeltech_id, 80, lead_id, f"Unlocked lead #{lead_id}",
(now - timedelta(hours=6)).isoformat()),
)
logger.info(" PadelTech unlocked lead #%s", lead_id)
# 7. Seed affiliate products
logger.info("Seeding %s affiliate products...", len(AFFILIATE_PRODUCTS))
product_ids: dict[str, int] = {}
for p in AFFILIATE_PRODUCTS:
existing = conn.execute(
"SELECT id FROM affiliate_products WHERE slug = ? AND language = ?",
(p["slug"], p["language"]),
).fetchone()
if existing:
product_ids[p["slug"]] = existing["id"]
logger.info(" %s already exists (id=%s)", p["name"], existing["id"])
continue
cursor = conn.execute(
"""INSERT INTO affiliate_products
(slug, name, brand, category, retailer, affiliate_url,
price_cents, currency, rating, pros, cons, description,
status, language, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, 'EUR', ?, ?, ?, ?, ?, ?, ?)""",
(
p["slug"], p["name"], p["brand"], p["category"], p["retailer"],
p["affiliate_url"], p["price_cents"], p["rating"],
p["pros"], p["cons"], p["description"],
p["status"], p["language"], p["sort_order"],
),
)
product_ids[p["slug"]] = cursor.lastrowid
logger.info(" %s -> id=%s (%s)", p["name"], cursor.lastrowid, p["status"])
# 8. Seed affiliate clicks (realistic 30-day spread for dashboard charts)
logger.info("Seeding affiliate clicks...")
import random
rng = random.Random(42)
# click distribution: more on popular rackets, fewer on accessories
click_weights = [
("bullpadel-vertex-04-amazon", "beste-padelschlaeger-2026", 52),
("adidas-metalbone-30-amazon", "beste-padelschlaeger-2026", 41),
("head-delta-pro-amazon", "padelschlaeger-anfaenger", 38),
("wilson-bela-pro-v2-amazon", "padelschlaeger-anfaenger", 29),
("adidas-adipower-ctrl-amazon", "padelschuhe-test", 24),
("babolat-jet-premura-amazon", "padelschuhe-test", 18),
("head-padel-pro-balls-amazon", "padelbaelle-vergleich", 15),
("bullpadel-overgrip-3er-amazon", "padel-zubehoer", 11),
("nox-padel-bag-amazon", "padel-zubehoer", 8),
]
existing_click_count = conn.execute("SELECT COUNT(*) FROM affiliate_clicks").fetchone()[0]
if existing_click_count == 0:
for slug, article_slug, count in click_weights:
pid = product_ids.get(slug)
if not pid:
continue
for _ in range(count):
days_ago = rng.randint(0, 29)
hours_ago = rng.randint(0, 23)
clicked_at = (now - timedelta(days=days_ago, hours=hours_ago)).strftime("%Y-%m-%d %H:%M:%S")
ip_hash = f"dev_{slug}_{_:04d}" # stable fake hash (not real SHA256)
conn.execute(
"""INSERT INTO affiliate_clicks
(product_id, article_slug, referrer, ip_hash, clicked_at)
VALUES (?, ?, ?, ?, ?)""",
(pid, article_slug, f"https://padelnomics.io/de/blog/{article_slug}", ip_hash, clicked_at),
)
total_clicks = sum(c for _, _, c in click_weights)
logger.info(" Inserted %s click events across 9 products", total_clicks)
else:
logger.info(" Clicks already seeded (%s rows), skipping", existing_click_count)
conn.commit()
conn.close()
logger.info("Done! Seed data written to %s", db_path)
logger.info(" Login: /auth/dev-login?email=dev@localhost")
logger.info(" Admin: set ADMIN_EMAILS=dev@localhost in .env, then dev-login grants admin role")
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s")
main()