feat(seo): expand city coverage to 40 cities + DuckDB refresh script
- 22 new cities: Madrid, Barcelona, Valencia, Seville, Málaga (ES), Paris, Lyon, Marseille (FR), Milan, Rome (IT), Amsterdam (NL), Vienna (AT), Zurich (CH), Stockholm (SE), Lisbon, Porto (PT), Brussels (BE), Dubai (AE), Sydney, Melbourne (AU), Dublin (IE) - Total: 40 cities × EN + DE = 80 articles - refresh_from_daas.py: sync template_data from planner_defaults serving table; dry-run mode; graceful when analytics unavailable Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Expanded programmatic SEO city coverage from 18 to 40 cities (+22 cities across ES, FR,
|
||||||
|
IT, NL, AT, CH, SE, PT, BE, AE, AU, IE) — generates 80 articles (40 cities × EN + DE)
|
||||||
|
- `scripts/refresh_from_daas.py`: syncs template_data rows from DuckDB `planner_defaults`
|
||||||
|
serving table; supports `--dry-run` and `--generate` flags; graceful no-op when DuckDB unavailable
|
||||||
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- `analytics.py`: DuckDB read-only reader (`open_analytics_db`, `close_analytics_db`,
|
- `analytics.py`: DuckDB read-only reader (`open_analytics_db`, `close_analytics_db`,
|
||||||
`fetch_analytics`) registered in app lifecycle (startup/shutdown)
|
`fetch_analytics`) registered in app lifecycle (startup/shutdown)
|
||||||
|
|||||||
208
padelnomics/web/src/padelnomics/scripts/refresh_from_daas.py
Normal file
208
padelnomics/web/src/padelnomics/scripts/refresh_from_daas.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
"""
|
||||||
|
Refresh template_data rows from DuckDB analytics serving layer.
|
||||||
|
|
||||||
|
Reads per-city market data from the `padelnomics.planner_defaults` serving table
|
||||||
|
and overwrites matching static values in `template_data.data_json`. This keeps
|
||||||
|
article financial model inputs in sync with the real-world data pipeline output.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# Dry-run — show what would change without writing:
|
||||||
|
uv run python -m padelnomics.scripts.refresh_from_daas --dry-run
|
||||||
|
|
||||||
|
# Apply updates:
|
||||||
|
uv run python -m padelnomics.scripts.refresh_from_daas
|
||||||
|
|
||||||
|
# Also re-generate articles after updating:
|
||||||
|
uv run python -m padelnomics.scripts.refresh_from_daas --generate
|
||||||
|
|
||||||
|
Graceful degradation:
|
||||||
|
- If DUCKDB_PATH is unset or the file doesn't exist, exits 0 with a warning.
|
||||||
|
- Cities not found in the analytics layer are left unchanged.
|
||||||
|
- Only fields with non-null values in the analytics layer are updated.
|
||||||
|
|
||||||
|
Fields mapped (DuckDB → data_json camelCase key):
|
||||||
|
rate_peak → ratePeak
|
||||||
|
rate_off_peak → rateOffPeak
|
||||||
|
court_cost_dbl → courtCostDbl
|
||||||
|
court_cost_sgl → courtCostSgl
|
||||||
|
rent_sqm → rentSqm
|
||||||
|
insurance → insurance
|
||||||
|
electricity → electricity
|
||||||
|
maintenance → maintenance
|
||||||
|
marketing → marketing
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
DATABASE_PATH = os.getenv("DATABASE_PATH", "data/app.db")
|
||||||
|
DUCKDB_PATH = os.getenv("DUCKDB_PATH", "data/lakehouse.duckdb")
|
||||||
|
|
||||||
|
# DuckDB column → data_json camelCase key
|
||||||
|
_FIELD_MAP: dict[str, str] = {
|
||||||
|
"rate_peak": "ratePeak",
|
||||||
|
"rate_off_peak": "rateOffPeak",
|
||||||
|
"court_cost_dbl": "courtCostDbl",
|
||||||
|
"court_cost_sgl": "courtCostSgl",
|
||||||
|
"rent_sqm": "rentSqm",
|
||||||
|
"insurance": "insurance",
|
||||||
|
"electricity": "electricity",
|
||||||
|
"maintenance": "maintenance",
|
||||||
|
"marketing": "marketing",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_analytics(city_slugs: list[str]) -> dict[str, dict]:
|
||||||
|
"""Query DuckDB for planner_defaults rows matching the given city slugs.
|
||||||
|
|
||||||
|
Returns dict of city_slug → {camelCase key: value}.
|
||||||
|
Returns {} if DuckDB is unavailable.
|
||||||
|
"""
|
||||||
|
path = Path(DUCKDB_PATH)
|
||||||
|
if not path.exists():
|
||||||
|
print(f" [analytics] DuckDB not found at {path} — skipping analytics refresh.")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
import duckdb
|
||||||
|
except ImportError:
|
||||||
|
print(" [analytics] duckdb not installed — skipping analytics refresh.")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
result: dict[str, dict] = {}
|
||||||
|
try:
|
||||||
|
conn = duckdb.connect(str(path), read_only=True)
|
||||||
|
placeholders = ", ".join(["?"] * len(city_slugs))
|
||||||
|
rows = conn.execute(
|
||||||
|
f"SELECT * FROM padelnomics.planner_defaults WHERE city_slug IN ({placeholders})",
|
||||||
|
city_slugs,
|
||||||
|
).fetchall()
|
||||||
|
cols = [d[0] for d in conn.description]
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
row_dict = dict(zip(cols, row))
|
||||||
|
slug = row_dict["city_slug"]
|
||||||
|
overrides: dict = {}
|
||||||
|
for col, key in _FIELD_MAP.items():
|
||||||
|
val = row_dict.get(col)
|
||||||
|
if val is not None:
|
||||||
|
overrides[key] = round(float(val))
|
||||||
|
result[slug] = overrides
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
print(f" [analytics] DuckDB query failed: {exc}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def refresh(dry_run: bool = False) -> int:
|
||||||
|
"""Update template_data rows from analytics. Returns count of rows updated."""
|
||||||
|
conn = sqlite3.connect(DATABASE_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
|
# Fetch all template_data rows that have a city_slug field
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT id, template_id, data_json FROM template_data"
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
city_slug_to_ids: dict[str, list[int]] = {}
|
||||||
|
for row in rows:
|
||||||
|
try:
|
||||||
|
data = json.loads(row["data_json"])
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
continue
|
||||||
|
slug = data.get("city_slug", "")
|
||||||
|
if slug:
|
||||||
|
city_slug_to_ids.setdefault(slug, []).append(row["id"])
|
||||||
|
|
||||||
|
if not city_slug_to_ids:
|
||||||
|
print("No template_data rows with city_slug found.")
|
||||||
|
conn.close()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
analytics = _load_analytics(list(city_slug_to_ids.keys()))
|
||||||
|
if not analytics:
|
||||||
|
print("No analytics data found — nothing to update.")
|
||||||
|
conn.close()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
updated = 0
|
||||||
|
for slug, overrides in analytics.items():
|
||||||
|
ids = city_slug_to_ids.get(slug, [])
|
||||||
|
if not ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for row_id in ids:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT data_json FROM template_data WHERE id = ?", (row_id,)
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = json.loads(row["data_json"])
|
||||||
|
changed = {k: v for k, v in overrides.items() if data.get(k) != v}
|
||||||
|
if not changed:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data.update(overrides)
|
||||||
|
if dry_run:
|
||||||
|
print(f" [dry-run] id={row_id} city_slug={slug}: {changed}")
|
||||||
|
else:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE template_data SET data_json = ?, updated_at = datetime('now') WHERE id = ?",
|
||||||
|
(json.dumps(data), row_id),
|
||||||
|
)
|
||||||
|
print(f" Updated id={row_id} city_slug={slug}: {list(changed.keys())}")
|
||||||
|
updated += 1
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
def _trigger_generation() -> None:
|
||||||
|
"""Trigger article generation via admin endpoint (requires running app)."""
|
||||||
|
import urllib.request
|
||||||
|
admin_key = os.getenv("ADMIN_API_KEY", "")
|
||||||
|
base_url = os.getenv("BASE_URL", "http://localhost:5000")
|
||||||
|
url = f"{base_url}/admin/generate-all"
|
||||||
|
assert admin_key, "ADMIN_API_KEY env var required to trigger generation"
|
||||||
|
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
method="POST",
|
||||||
|
headers={"X-Admin-Key": admin_key},
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||||
|
print(f" Generation triggered: HTTP {resp.status}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--dry-run", action="store_true",
|
||||||
|
help="Print changes without writing to DB")
|
||||||
|
parser.add_argument("--generate", action="store_true",
|
||||||
|
help="Trigger article re-generation after updating")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print(f"{'[DRY RUN] ' if args.dry_run else ''}Refreshing template_data from DuckDB…")
|
||||||
|
count = refresh(dry_run=args.dry_run)
|
||||||
|
print(f"{'Would update' if args.dry_run else 'Updated'} {count} rows.")
|
||||||
|
|
||||||
|
if args.generate and count > 0 and not args.dry_run:
|
||||||
|
print("Triggering article generation…")
|
||||||
|
_trigger_generation()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -705,6 +705,609 @@ CITIES = [
|
|||||||
"rentSqm": 4, "electricity": 630, "heating": 440,
|
"rentSqm": 4, "electricity": 630, "heating": 440,
|
||||||
"staff": 2100, "permitsCompliance": 10000, "utilTarget": 38,
|
"staff": 2100, "permitsCompliance": 10000, "utilTarget": 38,
|
||||||
},
|
},
|
||||||
|
# ------------------------------------------------------------------ Spain
|
||||||
|
{
|
||||||
|
"city": "Madrid",
|
||||||
|
"city_slug_en": "madrid",
|
||||||
|
"city_slug_de": "madrid",
|
||||||
|
"country": "ES",
|
||||||
|
"country_name_en": "Spain",
|
||||||
|
"country_name_de": "Spanien",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "3.4M",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Madrid is the world's densest padel market, with over 5,000 courts "
|
||||||
|
"nationwide and the sport embedded in everyday culture. High utilisation "
|
||||||
|
"rates and a deeply padel-literate customer base make Madrid one of the "
|
||||||
|
"best-validated markets for new investment."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Madrid ist der weltweit dichteste Padelmarkt mit über 5.000 Courts "
|
||||||
|
"und dem Sport als fester Alltagskultur. Hohe Auslastungsraten und "
|
||||||
|
"eine padelerfahrene Kundschaft machen Madrid zu einem der "
|
||||||
|
"bestvalidiertesten Märkte für neue Investitionen."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 5, "sglCourts": 2,
|
||||||
|
"ratePeak": 44, "rateOffPeak": 30,
|
||||||
|
"rentSqm": 6, "electricity": 580, "heating": 50,
|
||||||
|
"staff": 2200, "permitsCompliance": 25000, "utilTarget": 60,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"city": "Barcelona",
|
||||||
|
"city_slug_en": "barcelona",
|
||||||
|
"city_slug_de": "barcelona",
|
||||||
|
"country": "ES",
|
||||||
|
"country_name_en": "Spain",
|
||||||
|
"country_name_de": "Spanien",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "1.6M",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Barcelona combines a mature padel culture with year-round outdoor "
|
||||||
|
"conditions and strong tourism demand. Premium zones near the seafront "
|
||||||
|
"command higher rates, while suburban clubs operate at lower cost with "
|
||||||
|
"equally strong fill rates."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Barcelona verbindet eine reife Padelkultur mit ganzjährig "
|
||||||
|
"günstigen Outdoor-Bedingungen und starker Tourismusnachfrage. "
|
||||||
|
"Premium-Zonen nahe der Küste erzielen höhere Preise, während "
|
||||||
|
"Vorortclubs bei niedrigeren Kosten gleichstarke Belegung aufweisen."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 5, "sglCourts": 2,
|
||||||
|
"ratePeak": 47, "rateOffPeak": 32,
|
||||||
|
"rentSqm": 8, "electricity": 590, "heating": 0,
|
||||||
|
"staff": 2300, "permitsCompliance": 22000, "utilTarget": 60,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"city": "Valencia",
|
||||||
|
"city_slug_en": "valencia",
|
||||||
|
"city_slug_de": "valencia",
|
||||||
|
"country": "ES",
|
||||||
|
"country_name_en": "Spain",
|
||||||
|
"country_name_de": "Spanien",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "800K",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Valencia offers a compelling cost-to-utilisation ratio. Commercial "
|
||||||
|
"rents are 30–40% below Madrid, warm winters allow year-round outdoor "
|
||||||
|
"play, and the padel culture is well established — making it one of "
|
||||||
|
"Spain's most attractive second-tier markets."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Valencia bietet ein überzeugendes Kosten-Auslastungs-Verhältnis. "
|
||||||
|
"Gewerbemieten liegen 30–40% unter Madrid, warme Winter ermöglichen "
|
||||||
|
"ganzjähriges Outdoor-Spiel, und die Padelkultur ist fest etabliert — "
|
||||||
|
"einer der attraktivsten spanischen Zweitplatzmärkte."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 4, "sglCourts": 2,
|
||||||
|
"ratePeak": 38, "rateOffPeak": 26,
|
||||||
|
"rentSqm": 5, "electricity": 550, "heating": 0,
|
||||||
|
"staff": 1800, "permitsCompliance": 18000, "utilTarget": 55,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"city": "Seville",
|
||||||
|
"city_slug_en": "seville",
|
||||||
|
"city_slug_de": "sevilla",
|
||||||
|
"country": "ES",
|
||||||
|
"country_name_en": "Spain",
|
||||||
|
"country_name_de": "Spanien",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "690K",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Seville's hot, dry climate enables year-round outdoor padel, dramatically "
|
||||||
|
"lowering operating costs versus indoor-only markets. Strong local sports "
|
||||||
|
"culture and lower land prices combine to produce attractive returns for "
|
||||||
|
"early-mover operators."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Sevillas heißes, trockenes Klima ermöglicht ganzjähriges Outdoor-Padel "
|
||||||
|
"und senkt die Betriebskosten gegenüber reinen Innenmarktstandorten erheblich. "
|
||||||
|
"Starke lokale Sportkultur und niedrigere Grundstückspreise ergeben "
|
||||||
|
"attraktive Renditen für Erstanbieter."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 4, "sglCourts": 2,
|
||||||
|
"ratePeak": 35, "rateOffPeak": 24,
|
||||||
|
"rentSqm": 4, "electricity": 520, "heating": 0,
|
||||||
|
"staff": 1600, "permitsCompliance": 20000, "utilTarget": 52,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"city": "Málaga",
|
||||||
|
"city_slug_en": "malaga",
|
||||||
|
"city_slug_de": "malaga",
|
||||||
|
"country": "ES",
|
||||||
|
"country_name_en": "Spain",
|
||||||
|
"country_name_de": "Spanien",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "580K",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Málaga's status as a major expat and tourism hub creates dual revenue "
|
||||||
|
"streams: local membership and high-rate tourist bookings. The city's "
|
||||||
|
"tech scene and growing digital-nomad community add a further demographic "
|
||||||
|
"of high-frequency casual players."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Málagas Status als bedeutendes Expat- und Tourismuszentrum schafft "
|
||||||
|
"zwei Einnahmequellen: lokale Mitgliedschaften und hochpreisige Touristenbuchungen. "
|
||||||
|
"Die Tech-Szene und wachsende Digital-Nomad-Gemeinschaft der Stadt schaffen "
|
||||||
|
"eine weitere Demografie hochfrequenter Gelegenheitsspieler."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 4, "sglCourts": 2,
|
||||||
|
"ratePeak": 42, "rateOffPeak": 28,
|
||||||
|
"rentSqm": 5, "electricity": 540, "heating": 0,
|
||||||
|
"staff": 1700, "permitsCompliance": 18000, "utilTarget": 53,
|
||||||
|
},
|
||||||
|
# ------------------------------------------------------------------ France
|
||||||
|
{
|
||||||
|
"city": "Paris",
|
||||||
|
"city_slug_en": "paris",
|
||||||
|
"city_slug_de": "paris",
|
||||||
|
"country": "FR",
|
||||||
|
"country_name_en": "France",
|
||||||
|
"country_name_de": "Frankreich",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "2.1M (metro 12M)",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Paris is France's largest and fastest-growing padel market, with demand "
|
||||||
|
"significantly outpacing supply. High disposable incomes, an appetite for "
|
||||||
|
"premium sports experiences, and tight commercial space keep prices elevated "
|
||||||
|
"but support strong utilisation."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Paris ist Frankreichs größter und am schnellsten wachsender Padelmarkt, "
|
||||||
|
"mit deutlich überschüssiger Nachfrage. Hohe verfügbare Einkommen, "
|
||||||
|
"Nachfrage nach Premium-Sporterfahrungen und knappes Gewerbeflächenangebot "
|
||||||
|
"halten die Preise hoch und stützen eine starke Auslastung."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 4, "sglCourts": 2,
|
||||||
|
"ratePeak": 58, "rateOffPeak": 40,
|
||||||
|
"rentSqm": 12, "electricity": 620, "heating": 330,
|
||||||
|
"staff": 3000, "permitsCompliance": 15000, "utilTarget": 47,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"city": "Lyon",
|
||||||
|
"city_slug_en": "lyon",
|
||||||
|
"city_slug_de": "lyon",
|
||||||
|
"country": "FR",
|
||||||
|
"country_name_en": "France",
|
||||||
|
"country_name_de": "Frankreich",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "520K (metro 2.3M)",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Lyon has emerged as one of France's most active provincial padel markets, "
|
||||||
|
"backed by a large student and professional population. Lower rents than "
|
||||||
|
"Paris and a growing club network make it an attractive second-city opportunity."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Lyon hat sich als einer der aktivsten provinziellen Padelmärkte Frankreichs "
|
||||||
|
"etabliert, getragen von einer großen Studenten- und Berufsschicht. "
|
||||||
|
"Günstigere Mieten als Paris und ein wachsendes Clubnetzwerk machen "
|
||||||
|
"es zu einer attraktiven Zweitstadtchance."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 4, "sglCourts": 2,
|
||||||
|
"ratePeak": 42, "rateOffPeak": 30,
|
||||||
|
"rentSqm": 6, "electricity": 560, "heating": 280,
|
||||||
|
"staff": 2000, "permitsCompliance": 12000, "utilTarget": 40,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"city": "Marseille",
|
||||||
|
"city_slug_en": "marseille",
|
||||||
|
"city_slug_de": "marseille",
|
||||||
|
"country": "FR",
|
||||||
|
"country_name_en": "France",
|
||||||
|
"country_name_de": "Frankreich",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "870K",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Marseille's Mediterranean climate reduces heating costs significantly "
|
||||||
|
"and extends the viable outdoor season. As France's second city by size, "
|
||||||
|
"it offers deep demographic reach at commercial rents well below Paris or Lyon."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Marseilles Mittelmeerklima senkt die Heizkosten erheblich und verlängert "
|
||||||
|
"die nutzbare Outdoor-Saison. Als zweitgrößte Stadt Frankreichs bietet sie "
|
||||||
|
"demographische Tiefe bei Gewerbemieten weit unter Paris oder Lyon."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 4, "sglCourts": 2,
|
||||||
|
"ratePeak": 38, "rateOffPeak": 27,
|
||||||
|
"rentSqm": 5, "electricity": 540, "heating": 100,
|
||||||
|
"staff": 1700, "permitsCompliance": 12000, "utilTarget": 40,
|
||||||
|
},
|
||||||
|
# ------------------------------------------------------------------ Italy
|
||||||
|
{
|
||||||
|
"city": "Milan",
|
||||||
|
"city_slug_en": "milan",
|
||||||
|
"city_slug_de": "mailand",
|
||||||
|
"country": "IT",
|
||||||
|
"country_name_en": "Italy",
|
||||||
|
"country_name_de": "Italien",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "1.4M (metro 3.2M)",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Milan leads Italy's padel expansion, fuelled by high disposable incomes "
|
||||||
|
"and a business culture that treats sport as a social currency. Demand from "
|
||||||
|
"corporate accounts is particularly strong, with firms booking courts "
|
||||||
|
"for team activities and client entertainment."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Mailand führt Italiens Padelexpansion an, angetrieben durch hohes "
|
||||||
|
"verfügbares Einkommen und eine Geschäftskultur, die Sport als soziale "
|
||||||
|
"Währung betrachtet. Die Nachfrage von Firmenkonten ist besonders stark, "
|
||||||
|
"da Unternehmen Courts für Teamaktivitäten und Kundenunterhaltung buchen."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 4, "sglCourts": 2,
|
||||||
|
"ratePeak": 52, "rateOffPeak": 36,
|
||||||
|
"rentSqm": 9, "electricity": 730, "heating": 400,
|
||||||
|
"staff": 2500, "permitsCompliance": 18000, "utilTarget": 46,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"city": "Rome",
|
||||||
|
"city_slug_en": "rome",
|
||||||
|
"city_slug_de": "rom",
|
||||||
|
"country": "IT",
|
||||||
|
"country_name_en": "Italy",
|
||||||
|
"country_name_de": "Italien",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "2.8M",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Rome's mild winters and vast population base underpin a rapidly growing "
|
||||||
|
"padel market. Outdoor courts operate profitably year-round, reducing "
|
||||||
|
"CAPEX versus fully enclosed facilities. Central government facilities "
|
||||||
|
"and sports club conversions dominate early supply."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Roms milde Winter und riesige Bevölkerungsbasis stützen einen schnell "
|
||||||
|
"wachsenden Padelmarkt. Outdoor-Courts arbeiten ganzjährig profitabel, "
|
||||||
|
"was den CAPEX gegenüber vollständig geschlossenen Anlagen reduziert. "
|
||||||
|
"Staatliche Anlagen und Sportclubumbauten dominieren das frühe Angebot."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 4, "sglCourts": 2,
|
||||||
|
"ratePeak": 46, "rateOffPeak": 32,
|
||||||
|
"rentSqm": 7, "electricity": 700, "heating": 200,
|
||||||
|
"staff": 2200, "permitsCompliance": 20000, "utilTarget": 43,
|
||||||
|
},
|
||||||
|
# ---------------------------------------------- Netherlands
|
||||||
|
{
|
||||||
|
"city": "Amsterdam",
|
||||||
|
"city_slug_en": "amsterdam",
|
||||||
|
"city_slug_de": "amsterdam",
|
||||||
|
"country": "NL",
|
||||||
|
"country_name_en": "Netherlands",
|
||||||
|
"country_name_de": "Niederlande",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "920K",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Amsterdam combines a sports-active, internationally minded population "
|
||||||
|
"with one of Europe's highest purchasing powers. The city's compact geography "
|
||||||
|
"and limited available space push operators toward efficient indoor multi-court "
|
||||||
|
"venues, justifying premium pricing."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Amsterdam verbindet eine sportaktive, international ausgerichtete Bevölkerung "
|
||||||
|
"mit einer der höchsten Kaufkräfte Europas. Die kompakte Geografie und begrenzte "
|
||||||
|
"Verfügbarkeit von Räumlichkeiten drängen Betreiber zu effizienten Indoor-"
|
||||||
|
"Mehrcourt-Anlagen und rechtfertigen Premium-Preise."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 4, "sglCourts": 2,
|
||||||
|
"ratePeak": 54, "rateOffPeak": 38,
|
||||||
|
"rentSqm": 10, "electricity": 660, "heating": 460,
|
||||||
|
"staff": 2800, "permitsCompliance": 10000, "utilTarget": 43,
|
||||||
|
},
|
||||||
|
# ----------------------------------------------------------------- Austria
|
||||||
|
{
|
||||||
|
"city": "Vienna",
|
||||||
|
"city_slug_en": "vienna",
|
||||||
|
"city_slug_de": "wien",
|
||||||
|
"country": "AT",
|
||||||
|
"country_name_en": "Austria",
|
||||||
|
"country_name_de": "Österreich",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "1.9M",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Vienna is Austria's dominant padel market and one of Central Europe's "
|
||||||
|
"fastest-growing cities for the sport. A large English-speaking expat "
|
||||||
|
"community and high real incomes drive premium bookings, particularly "
|
||||||
|
"in the first and third districts."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Wien ist Österreichs dominanter Padelmarkt und eine der am schnellsten "
|
||||||
|
"wachsenden Städte für den Sport in Mitteleuropa. Eine große englischsprachige "
|
||||||
|
"Expat-Gemeinschaft und hohe Realeinkommen treiben Premium-Buchungen an, "
|
||||||
|
"insbesondere im ersten und dritten Bezirk."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 4, "sglCourts": 2,
|
||||||
|
"ratePeak": 50, "rateOffPeak": 35,
|
||||||
|
"rentSqm": 7, "electricity": 610, "heating": 510,
|
||||||
|
"staff": 2400, "permitsCompliance": 14000, "utilTarget": 42,
|
||||||
|
},
|
||||||
|
# --------------------------------------------------------------- Switzerland
|
||||||
|
{
|
||||||
|
"city": "Zurich",
|
||||||
|
"city_slug_en": "zurich",
|
||||||
|
"city_slug_de": "zuerich",
|
||||||
|
"country": "CH",
|
||||||
|
"country_name_en": "Switzerland",
|
||||||
|
"country_name_de": "Schweiz",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "440K (metro 1.4M)",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Zurich is Europe's most expensive padel market, driven by the highest "
|
||||||
|
"average wages on the continent. Players expect premium facilities and "
|
||||||
|
"are willing to pay for them — average court rates are 60–80% above "
|
||||||
|
"comparable German cities. Staff costs are the single largest operating "
|
||||||
|
"line item."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Zürich ist Europas teuerster Padelmarkt, angetrieben durch die höchsten "
|
||||||
|
"Durchschnittslöhne des Kontinents. Spieler erwarten Premium-Anlagen und "
|
||||||
|
"sind bereit, dafür zu zahlen — durchschnittliche Courtpreise liegen 60–80% "
|
||||||
|
"über vergleichbaren deutschen Städten. Personalkosten sind die größte "
|
||||||
|
"Einzelposition im Betrieb."
|
||||||
|
),
|
||||||
|
"currency_note_en": _EUR_NOTE_EN,
|
||||||
|
"currency_note_de": _EUR_NOTE_DE,
|
||||||
|
"dblCourts": 4, "sglCourts": 1,
|
||||||
|
"ratePeak": 75, "rateOffPeak": 52,
|
||||||
|
"rentSqm": 15, "electricity": 500, "heating": 480,
|
||||||
|
"staff": 4500, "permitsCompliance": 15000, "utilTarget": 45,
|
||||||
|
},
|
||||||
|
# ------------------------------------------------------------------ Sweden
|
||||||
|
{
|
||||||
|
"city": "Stockholm",
|
||||||
|
"city_slug_en": "stockholm",
|
||||||
|
"city_slug_de": "stockholm",
|
||||||
|
"country": "SE",
|
||||||
|
"country_name_en": "Sweden",
|
||||||
|
"country_name_de": "Schweden",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "980K",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Stockholm has the highest padel court-per-capita ratio in the world outside "
|
||||||
|
"Spain. The sport is deeply embedded in Swedish corporate and social culture — "
|
||||||
|
"padel after work is a fixture for office teams. Indoor venues dominate "
|
||||||
|
"given the long winter, but cheap electricity (largely hydropower) keeps "
|
||||||
|
"operating costs manageable."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Stockholm hat das weltweit höchste Padel-Court-pro-Kopf-Verhältnis außerhalb "
|
||||||
|
"Spaniens. Der Sport ist tief in der schwedischen Unternehmens- und Sozialkultur "
|
||||||
|
"verankert. Indoor-Anlagen dominieren aufgrund des langen Winters, aber günstige "
|
||||||
|
"Elektrizität (größtenteils Wasserkraft) hält die Betriebskosten beherrschbar."
|
||||||
|
),
|
||||||
|
"currency_note_en": _EUR_NOTE_EN,
|
||||||
|
"currency_note_de": _EUR_NOTE_DE,
|
||||||
|
"dblCourts": 4, "sglCourts": 2,
|
||||||
|
"ratePeak": 62, "rateOffPeak": 44,
|
||||||
|
"rentSqm": 8, "electricity": 390, "heating": 650,
|
||||||
|
"staff": 3200, "permitsCompliance": 8000, "utilTarget": 46,
|
||||||
|
},
|
||||||
|
# ----------------------------------------------------------------- Portugal
|
||||||
|
{
|
||||||
|
"city": "Lisbon",
|
||||||
|
"city_slug_en": "lisbon",
|
||||||
|
"city_slug_de": "lissabon",
|
||||||
|
"country": "PT",
|
||||||
|
"country_name_en": "Portugal",
|
||||||
|
"country_name_de": "Portugal",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "550K (metro 2.8M)",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Lisbon is one of Europe's fastest-growing padel markets, driven by an "
|
||||||
|
"influx of international residents and a warm climate that minimises "
|
||||||
|
"heating costs. Court rates are rising rapidly as demand outpaces supply, "
|
||||||
|
"making early-mover advantage particularly valuable."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Lissabon ist einer der am schnellsten wachsenden Padelmärkte Europas, "
|
||||||
|
"angetrieben durch einen Zustrom internationaler Einwohner und ein warmes "
|
||||||
|
"Klima, das Heizkosten minimiert. Courtpreise steigen rapide, da die Nachfrage "
|
||||||
|
"das Angebot übersteigt — der Erstanbieter-Vorteil ist besonders wertvoll."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 4, "sglCourts": 2,
|
||||||
|
"ratePeak": 38, "rateOffPeak": 26,
|
||||||
|
"rentSqm": 6, "electricity": 540, "heating": 80,
|
||||||
|
"staff": 1700, "permitsCompliance": 12000, "utilTarget": 52,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"city": "Porto",
|
||||||
|
"city_slug_en": "porto",
|
||||||
|
"city_slug_de": "porto",
|
||||||
|
"country": "PT",
|
||||||
|
"country_name_en": "Portugal",
|
||||||
|
"country_name_de": "Portugal",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "240K (metro 1.7M)",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Porto offers the most accessible padel investment entry point in Western Europe. "
|
||||||
|
"Commercial rents are among the lowest of any major city in this report, "
|
||||||
|
"the growing tech sector is driving demand, and the mild Atlantic climate "
|
||||||
|
"supports year-round play without heating infrastructure."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Porto bietet den günstigsten Padelinvestitions-Einstiegspunkt in Westeuropa. "
|
||||||
|
"Gewerbemieten gehören zu den niedrigsten aller Großstädte in diesem Bericht, "
|
||||||
|
"der wachsende Tech-Sektor treibt die Nachfrage, und das milde Atlantikklima "
|
||||||
|
"unterstützt ganzjähriges Spiel ohne Heizinfrastruktur."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 3, "sglCourts": 2,
|
||||||
|
"ratePeak": 32, "rateOffPeak": 22,
|
||||||
|
"rentSqm": 4, "electricity": 500, "heating": 50,
|
||||||
|
"staff": 1400, "permitsCompliance": 10000, "utilTarget": 48,
|
||||||
|
},
|
||||||
|
# ----------------------------------------------------------------- Belgium
|
||||||
|
{
|
||||||
|
"city": "Brussels",
|
||||||
|
"city_slug_en": "brussels",
|
||||||
|
"city_slug_de": "bruessel",
|
||||||
|
"country": "BE",
|
||||||
|
"country_name_en": "Belgium",
|
||||||
|
"country_name_de": "Belgien",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "1.2M (metro 2.1M)",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Brussels benefits from a large international civil-servant and lobbying "
|
||||||
|
"community with high disposable incomes and a taste for premium leisure. "
|
||||||
|
"Belgium's electricity costs are among Europe's highest, making energy "
|
||||||
|
"efficiency a critical factor in site selection."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Brüssel profitiert von einer großen internationalen Beamten- und "
|
||||||
|
"Lobbyisten-Gemeinschaft mit hohem verfügbarem Einkommen. Belgiens "
|
||||||
|
"Stromkosten gehören zu den höchsten Europas — Energieeffizienz ist "
|
||||||
|
"ein entscheidender Faktor bei der Standortwahl."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 4, "sglCourts": 2,
|
||||||
|
"ratePeak": 50, "rateOffPeak": 35,
|
||||||
|
"rentSqm": 8, "electricity": 660, "heating": 450,
|
||||||
|
"staff": 2600, "permitsCompliance": 12000, "utilTarget": 41,
|
||||||
|
},
|
||||||
|
# ------------------------------------------------------------------- UAE
|
||||||
|
{
|
||||||
|
"city": "Dubai",
|
||||||
|
"city_slug_en": "dubai",
|
||||||
|
"city_slug_de": "dubai",
|
||||||
|
"country": "AE",
|
||||||
|
"country_name_en": "UAE",
|
||||||
|
"country_name_de": "Vereinigte Arabische Emirate",
|
||||||
|
"region": "Middle East",
|
||||||
|
"population": "3.7M",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Dubai is the fastest-growing padel market in the Middle East, "
|
||||||
|
"with investment from European club operators and locally founded chains "
|
||||||
|
"expanding rapidly. Year-round outdoor conditions, no income tax, "
|
||||||
|
"and a high-earning expat population create exceptional unit economics. "
|
||||||
|
"AC costs replace heating but are heavily subsidised."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Dubai ist der am schnellsten wachsende Padelmarkt im Nahen Osten, "
|
||||||
|
"mit Investitionen europäischer Clubbetreiber und lokaler Ketten. "
|
||||||
|
"Ganzjährige Outdoor-Bedingungen, keine Einkommensteuer und "
|
||||||
|
"eine gut verdienende Expat-Bevölkerung schaffen außergewöhnliche Unit Economics. "
|
||||||
|
"AC-Kosten ersetzen Heizung, sind aber stark subventioniert."
|
||||||
|
),
|
||||||
|
"currency_note_en": _EUR_NOTE_EN,
|
||||||
|
"currency_note_de": _EUR_NOTE_DE,
|
||||||
|
"dblCourts": 6, "sglCourts": 2,
|
||||||
|
"ratePeak": 85, "rateOffPeak": 58,
|
||||||
|
"rentSqm": 5, "electricity": 380, "heating": 0,
|
||||||
|
"staff": 4200, "permitsCompliance": 20000, "utilTarget": 58,
|
||||||
|
},
|
||||||
|
# ---------------------------------------------------------------- Australia
|
||||||
|
{
|
||||||
|
"city": "Sydney",
|
||||||
|
"city_slug_en": "sydney",
|
||||||
|
"city_slug_de": "sydney",
|
||||||
|
"country": "AU",
|
||||||
|
"country_name_en": "Australia",
|
||||||
|
"country_name_de": "Australien",
|
||||||
|
"region": "Asia Pacific",
|
||||||
|
"population": "5.3M",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Sydney is Australia's largest and most developed padel market, "
|
||||||
|
"with supply ramping rapidly since 2022. High commercial rents and "
|
||||||
|
"electricity costs require strong utilisation to break even, but the "
|
||||||
|
"city's affluent coastal suburbs produce premium booking rates and "
|
||||||
|
"membership uptake above European benchmarks."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Sydney ist Australiens größter und am weitesten entwickelter Padelmarkt, "
|
||||||
|
"mit rasant steigendem Angebot seit 2022. Hohe Gewerberaum- und Stromkosten "
|
||||||
|
"erfordern eine starke Auslastung, aber die wohlhabenden Küstenvorortee "
|
||||||
|
"der Stadt erzielen Premium-Buchungsraten über europäischen Benchmarks."
|
||||||
|
),
|
||||||
|
"currency_note_en": _EUR_NOTE_EN,
|
||||||
|
"currency_note_de": _EUR_NOTE_DE,
|
||||||
|
"dblCourts": 5, "sglCourts": 2,
|
||||||
|
"ratePeak": 68, "rateOffPeak": 48,
|
||||||
|
"rentSqm": 6, "electricity": 760, "heating": 80,
|
||||||
|
"staff": 3600, "permitsCompliance": 18000, "utilTarget": 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"city": "Melbourne",
|
||||||
|
"city_slug_en": "melbourne",
|
||||||
|
"city_slug_de": "melbourne",
|
||||||
|
"country": "AU",
|
||||||
|
"country_name_en": "Australia",
|
||||||
|
"country_name_de": "Australien",
|
||||||
|
"region": "Asia Pacific",
|
||||||
|
"population": "5.1M",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Melbourne's four-seasons climate and strong indoor-sport infrastructure "
|
||||||
|
"make it well suited to padel. The city trails Sydney in court density "
|
||||||
|
"but benefits from somewhat lower commercial rents and a sports culture "
|
||||||
|
"that is quick to adopt new racket sports."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Melbournes Vier-Jahreszeiten-Klima und starke Hallensport-Infrastruktur "
|
||||||
|
"machen es gut geeignet für Padel. Die Stadt liegt hinter Sydney in "
|
||||||
|
"der Court-Dichte, profitiert aber von etwas niedrigeren Gewerbemieten "
|
||||||
|
"und einer Sportkultur, die neue Racketsportarten schnell annimmt."
|
||||||
|
),
|
||||||
|
"currency_note_en": _EUR_NOTE_EN,
|
||||||
|
"currency_note_de": _EUR_NOTE_DE,
|
||||||
|
"dblCourts": 5, "sglCourts": 2,
|
||||||
|
"ratePeak": 62, "rateOffPeak": 44,
|
||||||
|
"rentSqm": 5, "electricity": 700, "heating": 100,
|
||||||
|
"staff": 3200, "permitsCompliance": 16000, "utilTarget": 47,
|
||||||
|
},
|
||||||
|
# ----------------------------------------------------------------- Ireland
|
||||||
|
{
|
||||||
|
"city": "Dublin",
|
||||||
|
"city_slug_en": "dublin",
|
||||||
|
"city_slug_de": "dublin",
|
||||||
|
"country": "IE",
|
||||||
|
"country_name_en": "Ireland",
|
||||||
|
"country_name_de": "Irland",
|
||||||
|
"region": "Europe",
|
||||||
|
"population": "1.4M",
|
||||||
|
"padel_context_en": (
|
||||||
|
"Dublin's booming tech sector and large young professional population "
|
||||||
|
"have driven rapid uptake of padel since 2022. Ireland's electricity "
|
||||||
|
"costs are among Europe's highest, significantly impacting operating "
|
||||||
|
"margins — energy-efficient site design is essential."
|
||||||
|
),
|
||||||
|
"padel_context_de": (
|
||||||
|
"Dublins boomender Tech-Sektor und eine große junge Berufsschicht "
|
||||||
|
"haben seit 2022 eine rasche Padeladoption angetrieben. Irlands Stromkosten "
|
||||||
|
"gehören zu den höchsten Europas und belasten die Betriebsmargen erheblich — "
|
||||||
|
"energieeffizientes Standortdesign ist unerlässlich."
|
||||||
|
),
|
||||||
|
"currency_note_en": "",
|
||||||
|
"currency_note_de": "",
|
||||||
|
"dblCourts": 4, "sglCourts": 2,
|
||||||
|
"ratePeak": 55, "rateOffPeak": 38,
|
||||||
|
"rentSqm": 9, "electricity": 800, "heating": 460,
|
||||||
|
"staff": 2900, "permitsCompliance": 12000, "utilTarget": 42,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Calc fields that get passed to the financial model (must match DEFAULTS keys)
|
# Calc fields that get passed to the financial model (must match DEFAULTS keys)
|
||||||
|
|||||||
Reference in New Issue
Block a user