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