183 lines
7.6 KiB
Python
183 lines
7.6 KiB
Python
|
|
"""City demographics and 360 entity workspace."""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from typing import Any, Optional
|
||
|
|
|
||
|
|
from app.connectors import cbs, pdok
|
||
|
|
from app.db import execute, execute_returning, fetch_all, fetch_one, json_param
|
||
|
|
|
||
|
|
|
||
|
|
def sync_city_demographics(limit: int = 100) -> dict[str, Any]:
|
||
|
|
cities = fetch_all(
|
||
|
|
"""SELECT DISTINCT city, province FROM supermarkets
|
||
|
|
WHERE city IS NOT NULL AND city <> 'Onbekend'
|
||
|
|
AND city NOT IN (SELECT city FROM city_demographics)
|
||
|
|
LIMIT %s""",
|
||
|
|
(limit,),
|
||
|
|
)
|
||
|
|
synced = 0
|
||
|
|
for row in cities:
|
||
|
|
pc_rows = fetch_all(
|
||
|
|
"SELECT postcode FROM supermarkets WHERE city = %s AND postcode <> '0000AA' LIMIT 1",
|
||
|
|
(row["city"],),
|
||
|
|
)
|
||
|
|
if not pc_rows:
|
||
|
|
continue
|
||
|
|
pd = pdok.lookup_postcode(pc_rows[0]["postcode"])
|
||
|
|
if not pd or not pd.get("municipality_code"):
|
||
|
|
continue
|
||
|
|
stats = cbs.fetch_gemeente_stats(pd["municipality_code"])
|
||
|
|
if not stats:
|
||
|
|
continue
|
||
|
|
rel = stats.get("religious_composition") or {}
|
||
|
|
execute_returning(
|
||
|
|
"""INSERT INTO city_demographics (city, province, gemeente_code, population, households,
|
||
|
|
avg_income, muslim_proxy_pct, data_source, last_updated)
|
||
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,'cbs+pdok',NOW())
|
||
|
|
ON CONFLICT (city, province) DO UPDATE SET
|
||
|
|
population=EXCLUDED.population, households=EXCLUDED.households,
|
||
|
|
avg_income=EXCLUDED.avg_income, muslim_proxy_pct=EXCLUDED.muslim_proxy_pct,
|
||
|
|
last_updated=NOW() RETURNING id""",
|
||
|
|
(
|
||
|
|
row["city"], row.get("province") or pd.get("province"),
|
||
|
|
pd.get("municipality_code"), stats.get("population"), stats.get("households"),
|
||
|
|
stats.get("avg_income"), rel.get("muslim_proxy_pct"),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
synced += 1
|
||
|
|
return {"synced": synced}
|
||
|
|
|
||
|
|
|
||
|
|
def get_city_context(city: str, province: Optional[str] = None) -> Optional[dict[str, Any]]:
|
||
|
|
if province:
|
||
|
|
row = fetch_one(
|
||
|
|
"SELECT * FROM city_demographics WHERE city ILIKE %s AND province ILIKE %s",
|
||
|
|
(city, province),
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
row = fetch_one("SELECT * FROM city_demographics WHERE city ILIKE %s LIMIT 1", (city,))
|
||
|
|
if not row:
|
||
|
|
return None
|
||
|
|
stores = fetch_one(
|
||
|
|
"SELECT COUNT(*) AS n FROM supermarkets WHERE city ILIKE %s", (city,)
|
||
|
|
)
|
||
|
|
out = dict(row)
|
||
|
|
out["stores_in_city"] = int((stores or {}).get("n") or 0)
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def get_store_360(supermarket_id: int) -> dict[str, Any]:
|
||
|
|
store = fetch_one("SELECT * FROM supermarkets WHERE id = %s", (supermarket_id,))
|
||
|
|
if not store:
|
||
|
|
raise ValueError("Store not found")
|
||
|
|
city_ctx = get_city_context(store["city"], store.get("province"))
|
||
|
|
notes = fetch_all(
|
||
|
|
"SELECT * FROM entity_notes WHERE entity_type='supermarket' AND entity_id=%s ORDER BY pinned DESC, created_at DESC",
|
||
|
|
(supermarket_id,),
|
||
|
|
)
|
||
|
|
media = fetch_all(
|
||
|
|
"SELECT * FROM entity_media WHERE entity_type='supermarket' AND entity_id=%s ORDER BY created_at DESC",
|
||
|
|
(supermarket_id,),
|
||
|
|
)
|
||
|
|
milestones = fetch_all(
|
||
|
|
"SELECT * FROM sales_milestones WHERE supermarket_id=%s ORDER BY sort_order, target_date NULLS LAST",
|
||
|
|
(supermarket_id,),
|
||
|
|
)
|
||
|
|
ownership = fetch_all(
|
||
|
|
"""SELECT * FROM ownership_changes
|
||
|
|
WHERE (entity_id=%s AND entity_type='supermarket') OR chain ILIKE %s
|
||
|
|
ORDER BY effective_date DESC NULLS LAST LIMIT 10""",
|
||
|
|
(supermarket_id, f"%{store['chain']}%"),
|
||
|
|
)
|
||
|
|
calendar = fetch_all(
|
||
|
|
"""SELECT * FROM calendar_events WHERE supermarket_id=%s OR (client_id=%s AND client_id IS NOT NULL)
|
||
|
|
ORDER BY starts_at DESC LIMIT 10""",
|
||
|
|
(supermarket_id, store.get("client_id")),
|
||
|
|
)
|
||
|
|
nearby_count = fetch_one(
|
||
|
|
"SELECT COUNT(*) AS n FROM supermarkets WHERE city ILIKE %s AND id <> %s",
|
||
|
|
(store["city"], supermarket_id),
|
||
|
|
)
|
||
|
|
area = fetch_one("SELECT * FROM area_analysis WHERE postcode = %s", (store.get("postcode"),))
|
||
|
|
weather = fetch_all(
|
||
|
|
"SELECT * FROM weather_data WHERE city ILIKE %s ORDER BY date DESC LIMIT 7",
|
||
|
|
(f"%{store.get('city', '')}%",),
|
||
|
|
)
|
||
|
|
return {
|
||
|
|
"store": dict(store),
|
||
|
|
"city": city_ctx,
|
||
|
|
"catchment": {
|
||
|
|
"city_population": (city_ctx or {}).get("population"),
|
||
|
|
"stores_in_city": int((nearby_count or {}).get("n") or 0),
|
||
|
|
"gemeente_population": (area or {}).get("population"),
|
||
|
|
"postcode_population_proxy": (area or {}).get("population"),
|
||
|
|
},
|
||
|
|
"notes": [dict(n) for n in notes],
|
||
|
|
"media": [dict(m) for m in media],
|
||
|
|
"milestones": [dict(m) for m in milestones],
|
||
|
|
"ownership_changes": [dict(o) for o in ownership],
|
||
|
|
"calendar": [dict(c) for c in calendar],
|
||
|
|
"area_analysis": dict(area) if area else None,
|
||
|
|
"weather": [dict(w) for w in weather],
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def add_note(entity_type: str, entity_id: int, body: str, title: Optional[str] = None, note_type: str = "general") -> dict[str, Any]:
|
||
|
|
row = execute_returning(
|
||
|
|
"""INSERT INTO entity_notes (entity_type, entity_id, title, body, note_type)
|
||
|
|
VALUES (%s,%s,%s,%s,%s) RETURNING *""",
|
||
|
|
(entity_type, entity_id, title, body, note_type),
|
||
|
|
)
|
||
|
|
return dict(row or {})
|
||
|
|
|
||
|
|
|
||
|
|
def add_milestone(supermarket_id: int, title: str, milestone_type: str = "custom", **kwargs: Any) -> dict[str, Any]:
|
||
|
|
row = execute_returning(
|
||
|
|
"""INSERT INTO sales_milestones (supermarket_id, client_id, deal_id, milestone_type, title,
|
||
|
|
status, target_date, value_eur, notes, sort_order)
|
||
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) RETURNING *""",
|
||
|
|
(
|
||
|
|
supermarket_id, kwargs.get("client_id"), kwargs.get("deal_id"), milestone_type, title,
|
||
|
|
kwargs.get("status", "pending"), kwargs.get("target_date"), kwargs.get("value_eur"),
|
||
|
|
kwargs.get("notes"), kwargs.get("sort_order", 0),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
return dict(row or {})
|
||
|
|
|
||
|
|
|
||
|
|
def add_ownership(**kwargs: Any) -> dict[str, Any]:
|
||
|
|
row = execute_returning(
|
||
|
|
"""INSERT INTO ownership_changes (entity_type, entity_id, chain, previous_owner, new_owner,
|
||
|
|
change_type, effective_date, source, notes)
|
||
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) RETURNING *""",
|
||
|
|
(
|
||
|
|
kwargs.get("entity_type", "supermarket"), kwargs.get("entity_id"), kwargs.get("chain"),
|
||
|
|
kwargs.get("previous_owner"), kwargs["new_owner"], kwargs.get("change_type", "acquisition"),
|
||
|
|
kwargs.get("effective_date"), kwargs.get("source"), kwargs.get("notes"),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
return dict(row or {})
|
||
|
|
|
||
|
|
|
||
|
|
def add_calendar_event(supermarket_id: int, title: str, starts_at: str, **kwargs: Any) -> dict[str, Any]:
|
||
|
|
row = execute_returning(
|
||
|
|
"""INSERT INTO calendar_events (title, description, starts_at, ends_at, client_id, deal_id,
|
||
|
|
supermarket_id, location, source)
|
||
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,'retail_360') RETURNING *""",
|
||
|
|
(
|
||
|
|
title, kwargs.get("description"), starts_at, kwargs.get("ends_at"),
|
||
|
|
kwargs.get("client_id"), kwargs.get("deal_id"), supermarket_id,
|
||
|
|
kwargs.get("location"),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
return dict(row or {})
|
||
|
|
|
||
|
|
|
||
|
|
def register_media(entity_type: str, entity_id: int, filename: str, storage_path: str, content_type: str, caption: Optional[str] = None) -> dict[str, Any]:
|
||
|
|
row = execute_returning(
|
||
|
|
"""INSERT INTO entity_media (entity_type, entity_id, filename, storage_path, content_type, caption)
|
||
|
|
VALUES (%s,%s,%s,%s,%s,%s) RETURNING *""",
|
||
|
|
(entity_type, entity_id, filename, storage_path, content_type, caption),
|
||
|
|
)
|
||
|
|
return dict(row or {})
|