Platform bundle: marketing publish, IT ops, packaging, agents mesh.
Volledige Foodlinkk Command Center uitbreiding met social automatisering, reclamefolder filters, Proxmox monitoring en documentatie.
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
"""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 {})
|
||||
Reference in New Issue
Block a user