Files
foodlinkk-command-center/tools-api/app/retail_360.py
T

183 lines
7.6 KiB
Python
Raw Normal View History

"""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 {})