"""Retail intelligence map page + API proxy.""" from __future__ import annotations import os from typing import Any, Optional import httpx from fastapi import APIRouter, Query, Request from fastapi.responses import JSONResponse, StreamingResponse from fastapi.templating import Jinja2Templates from pathlib import Path from pydantic import BaseModel router = APIRouter() BASE = Path(__file__).resolve().parent.parent.parent templates = Jinja2Templates(directory=str(BASE / "templates")) TOOLS = os.getenv("TOOLS_API_URL", "http://tools-api:8700").rstrip("/") class CrmLinkBody(BaseModel): client_id: int deal_id: Optional[int] = None relationship_type: str = "prospect" partnership_status: Optional[str] = None notes: Optional[str] = None class SupermarketCreateBody(BaseModel): name: str chain: str address: str postcode: str = "0000AA" city: str province: Optional[str] = None phone: Optional[str] = None email: Optional[str] = None website: Optional[str] = None manager_name: Optional[str] = None employee_count: Optional[int] = None store_type: Optional[str] = None partnership_status: str = "none" halal_certified: bool = False has_halal_section: bool = False halal_certifier: Optional[str] = None class SupermarketPatchBody(BaseModel): name: Optional[str] = None chain: Optional[str] = None address: Optional[str] = None postcode: Optional[str] = None city: Optional[str] = None province: Optional[str] = None phone: Optional[str] = None email: Optional[str] = None website: Optional[str] = None manager_name: Optional[str] = None employee_count: Optional[int] = None store_type: Optional[str] = None size_m2: Optional[int] = None partnership_status: Optional[str] = None halal_certified: Optional[bool] = None has_halal_section: Optional[bool] = None halal_certifier: Optional[str] = None halal_certificate_number: Optional[str] = None organic_section: Optional[bool] = None alcohol_section: Optional[bool] = None class NoteBody(BaseModel): body: str title: Optional[str] = None note_type: str = "general" class MilestoneBody(BaseModel): title: str milestone_type: str = "custom" client_id: Optional[int] = None deal_id: Optional[int] = None target_date: Optional[str] = None value_eur: Optional[float] = None notes: Optional[str] = None class OwnershipBody(BaseModel): new_owner: str previous_owner: Optional[str] = None change_type: str = "acquisition" effective_date: Optional[str] = None source: Optional[str] = None notes: Optional[str] = None class CalendarBody(BaseModel): title: str starts_at: str description: Optional[str] = None ends_at: Optional[str] = None client_id: Optional[int] = None deal_id: Optional[int] = None location: Optional[str] = None class MediaBody(BaseModel): filename: str storage_path: str content_type: str = "image/jpeg" caption: Optional[str] = None async def _tools_get(path: str, params: Optional[dict] = None) -> Any: async with httpx.AsyncClient(timeout=120) as client: resp = await client.get(f"{TOOLS}{path}", params=params or {}) resp.raise_for_status() return resp.json() async def _tools_post(path: str, params: Optional[dict] = None, json_body: Optional[dict] = None) -> Any: async with httpx.AsyncClient(timeout=300) as client: resp = await client.post(f"{TOOLS}{path}", params=params or {}, json=json_body) resp.raise_for_status() return resp.json() async def _tools_delete(path: str) -> Any: async with httpx.AsyncClient(timeout=60) as client: resp = await client.delete(f"{TOOLS}{path}") resp.raise_for_status() return resp.json() async def _tools_patch(path: str, json_body: Optional[dict] = None) -> Any: async with httpx.AsyncClient(timeout=120) as client: resp = await client.patch(f"{TOOLS}{path}", json=json_body or {}) resp.raise_for_status() return resp.json() @router.get("/retail") async def retail_page(request: Request): stats = await _tools_get("/retail/stats") filters = await _tools_get("/retail/filters") schema = await _tools_get("/retail/schema") trends = await _tools_get("/retail/trends", {"limit": 8}) crm = await _tools_get("/retail/crm/options") return templates.TemplateResponse( "retail.html", { "request": request, "stats": stats, "filters": filters, "schema": schema, "trends": trends.get("items", []), "crm_options": crm, }, ) @router.get("/api/retail/stats") async def api_retail_stats(request: Request): return JSONResponse(await _tools_get("/retail/stats", dict(request.query_params))) @router.get("/api/retail/filters") async def api_retail_filters(): return JSONResponse(await _tools_get("/retail/filters")) @router.get("/api/retail/map") async def api_retail_map(request: Request): return JSONResponse(await _tools_get("/retail/map", dict(request.query_params))) @router.get("/api/retail/list") async def api_retail_list(request: Request): return JSONResponse(await _tools_get("/retail/supermarkets", dict(request.query_params))) @router.get("/api/retail/supermarkets/{store_id}") async def api_retail_store(store_id: int): return JSONResponse(await _tools_get(f"/retail/supermarkets/{store_id}")) @router.get("/api/retail/opportunities") async def api_retail_opportunities(request: Request): return JSONResponse(await _tools_get("/retail/opportunities", dict(request.query_params))) @router.get("/api/retail/trends") async def api_retail_trends(request: Request): return JSONResponse(await _tools_get("/retail/trends", dict(request.query_params))) @router.get("/api/retail/crm/options") async def api_crm_options(): return JSONResponse(await _tools_get("/retail/crm/options")) @router.post("/api/retail/supermarkets/{store_id}/link") async def api_link_store(store_id: int, body: CrmLinkBody): return JSONResponse(await _tools_post(f"/retail/supermarkets/{store_id}/link", json_body=body.model_dump())) @router.post("/api/retail/supermarkets") async def api_create_supermarket(body: SupermarketCreateBody): return JSONResponse(await _tools_post("/retail/supermarkets", json_body=body.model_dump())) @router.patch("/api/retail/supermarkets/{store_id}") async def api_patch_supermarket(store_id: int, body: SupermarketPatchBody): return JSONResponse(await _tools_patch( f"/retail/supermarkets/{store_id}", json_body=body.model_dump(exclude_unset=True), )) @router.delete("/api/retail/supermarkets/{store_id}/link/{client_id}") async def api_unlink_store(store_id: int, client_id: int): return JSONResponse(await _tools_delete(f"/retail/supermarkets/{store_id}/link/{client_id}")) @router.post("/api/retail/enrich") async def api_retail_enrich(limit: int = Query(100, ge=1, le=200)): return JSONResponse(await _tools_post("/retail/enrich", params={"limit": limit})) @router.post("/api/retail/sync/{action}") async def api_retail_sync(action: str, limit: int = Query(100, ge=1, le=300)): paths = { "halal": "/retail/sync/halal", "contacts": f"/retail/sync/contacts?limit={limit}", "trends": "/retail/sync/trends", "opportunities": "/retail/compute-opportunities", } if action not in paths: return JSONResponse({"error": "unknown action"}, status_code=400) return JSONResponse(await _tools_post(paths[action])) @router.get("/api/retail/export") async def api_retail_export(request: Request): async with httpx.AsyncClient(timeout=120) as client: resp = await client.get(f"{TOOLS}/retail/export", params=dict(request.query_params)) resp.raise_for_status() return StreamingResponse( iter([resp.text]), media_type="text/csv", headers={"Content-Disposition": "attachment; filename=retail_export.csv"}, ) # --- 360 workspace proxies --- @router.get("/api/retail/360/{store_id}") async def api_retail_360(store_id: int): return JSONResponse(await _tools_get(f"/retail/360/{store_id}")) @router.post("/api/retail/360/{store_id}/notes") async def api_retail_note(store_id: int, body: NoteBody): return JSONResponse(await _tools_post(f"/retail/360/{store_id}/notes", json_body=body.model_dump())) @router.post("/api/retail/360/{store_id}/milestones") async def api_retail_milestone(store_id: int, body: MilestoneBody): return JSONResponse(await _tools_post(f"/retail/360/{store_id}/milestones", json_body=body.model_dump())) @router.post("/api/retail/360/{store_id}/ownership") async def api_retail_ownership(store_id: int, body: OwnershipBody): return JSONResponse(await _tools_post(f"/retail/360/{store_id}/ownership", json_body=body.model_dump())) @router.post("/api/retail/360/{store_id}/calendar") async def api_retail_calendar(store_id: int, body: CalendarBody): return JSONResponse(await _tools_post(f"/retail/360/{store_id}/calendar", json_body=body.model_dump())) @router.post("/api/retail/360/{store_id}/media") async def api_retail_media(store_id: int, body: MediaBody): return JSONResponse(await _tools_post(f"/retail/360/{store_id}/media", json_body=body.model_dump())) @router.get("/api/retail/cities") async def api_retail_cities(request: Request): return JSONResponse(await _tools_get("/retail/cities", dict(request.query_params))) @router.post("/api/retail/cities/sync") async def api_retail_cities_sync(limit: int = Query(50, ge=1, le=200)): return JSONResponse(await _tools_post("/retail/cities/sync", params={"limit": limit})) @router.get("/api/retail/wholesalers") async def api_retail_wholesalers(request: Request): return JSONResponse(await _tools_get("/retail/wholesalers", dict(request.query_params))) @router.post("/api/retail/wholesalers/import") async def api_retail_wholesalers_import(): return JSONResponse(await _tools_post("/retail/wholesalers/import")) @router.get("/api/retail/rss/live") async def api_retail_rss(request: Request): return JSONResponse(await _tools_get("/retail/rss/live", dict(request.query_params))) @router.post("/api/retail/rss/refresh") async def api_retail_rss_refresh(): return JSONResponse(await _tools_post("/retail/rss/refresh")) @router.get("/api/retail/rss/bookmarks") async def api_rss_bookmarks(request: Request): return JSONResponse(await _tools_get("/retail/rss/bookmarks", dict(request.query_params))) @router.post("/api/retail/rss/bookmarks") async def api_rss_bookmark_add(request: Request): body = await request.json() return JSONResponse(await _tools_post("/retail/rss/bookmarks", json_body=body)) @router.delete("/api/retail/rss/bookmarks/{rss_item_id}") async def api_rss_bookmark_delete(rss_item_id: int): return JSONResponse(await _tools_delete(f"/retail/rss/bookmarks/{rss_item_id}")) @router.get("/api/retail/wholesalers/meta") async def api_wholesalers_meta(): return JSONResponse(await _tools_get("/retail/wholesalers/meta")) @router.get("/api/retail/wholesalers/{wh_id}/contacts") async def api_wholesaler_contacts(wh_id: int): return JSONResponse(await _tools_get(f"/retail/wholesalers/{wh_id}/contacts")) @router.post("/api/retail/wholesalers/{wh_id}/contacts") async def api_wholesaler_contact_add(wh_id: int, request: Request): body = await request.json() return JSONResponse(await _tools_post(f"/retail/wholesalers/{wh_id}/contacts", json_body=body)) @router.get("/api/retail/promo-campaigns") async def api_promo_campaigns(request: Request): return JSONResponse(await _tools_get("/retail/promo-campaigns", dict(request.query_params))) @router.post("/api/retail/promo-campaigns") async def api_promo_campaign_add(request: Request): body = await request.json() return JSONResponse(await _tools_post("/retail/promo-campaigns", json_body=body)) @router.post("/api/retail/reclamefolder/refresh") async def api_reclamefolder_refresh(): return JSONResponse(await _tools_post("/retail/reclamefolder/refresh", json_body={})) @router.get("/api/retail/reclamefolder/live") async def api_reclamefolder_live(request: Request): return JSONResponse(await _tools_get("/retail/reclamefolder/live", dict(request.query_params))) @router.get("/api/retail/reclamefolder/chains") async def api_reclamefolder_chains(): return JSONResponse(await _tools_get("/retail/reclamefolder/chains")) @router.get("/api/retail/market/supermarkets") async def api_supermarket_market(): return JSONResponse(await _tools_get("/retail/market/supermarkets")) @router.get("/api/retail/market/food-trends") async def api_food_trends(): return JSONResponse(await _tools_get("/retail/market/food-trends")) @router.get("/api/retail/market/concepts") async def api_market_concepts(request: Request): return JSONResponse(await _tools_get("/retail/market/concepts", dict(request.query_params))) @router.get("/api/retail/live-dashboard") async def api_retail_live_dashboard(): return JSONResponse(await _tools_get("/retail/live-dashboard")) @router.get("/api/retail/market/stocks") async def api_retail_market_stocks(): return JSONResponse(await _tools_get("/retail/market/stocks")) @router.get("/api/retail/regulations") async def api_retail_regulations(request: Request): return JSONResponse(await _tools_get("/retail/regulations", dict(request.query_params)))