From 2666b59327c601ac215d60dc264a46ef068dba4e Mon Sep 17 00:00:00 2001 From: Sagnik Ghosh Date: Fri, 1 May 2026 18:54:00 +0530 Subject: [PATCH] fix: wire Velocity-OS live CRM and inventory data --- core/api/api/routes_crm.py | 1 + core/api/api/routes_inventory.py | 93 ++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/core/api/api/routes_crm.py b/core/api/api/routes_crm.py index 62dbbdd..aa70953 100644 --- a/core/api/api/routes_crm.py +++ b/core/api/api/routes_crm.py @@ -1356,6 +1356,7 @@ async def get_kanban_board( @crm_router.get("/pipeline/kanban") +@crm_router.get("/crm/pipeline/kanban") async def get_pipeline_kanban( request: Request, user: UserPrincipal = Depends(get_current_user), diff --git a/core/api/api/routes_inventory.py b/core/api/api/routes_inventory.py index 73b4b8b..75d5d14 100644 --- a/core/api/api/routes_inventory.py +++ b/core/api/api/routes_inventory.py @@ -47,6 +47,91 @@ def _tenant_scope(user) -> str: return user.tenant_id +def _as_float(value: Any) -> float | None: + return float(value) if value is not None else None + + +def _canonical_project_payload(row: dict[str, Any]) -> dict[str, Any]: + location = row.get("location_json") + amenities = row.get("amenities_json") + unit_mix = row.get("unit_mix") + price_min = row.get("price_min") + price_max = row.get("price_max") + return { + "property_id": str(row["project_id"]), + "project_name": row["project_name"], + "developer_name": row["developer_name"], + "property_type": "project", + "location": { + "city": row.get("city"), + "district": row.get("micro_market"), + "area": row.get("micro_market"), + "address": row.get("address"), + **(location if isinstance(location, dict) else {}), + }, + "price_bands": [ + { + "label": "Current inventory", + "min": _as_float(price_min), + "max": _as_float(price_max), + "currency": "INR lakh", + } + ] if price_min is not None or price_max is not None else [], + "unit_mix": unit_mix if isinstance(unit_mix, list) else [], + "amenities": amenities if isinstance(amenities, list) else [], + "status": row.get("project_status"), + "ingested_at": row.get("created_at"), + "created_at": row.get("created_at"), + } + + +async def _fetch_canonical_project_rows(conn: Any, limit: int, offset: int, project_id: str | None = None) -> list[dict[str, Any]]: + filters = "" + params: list[Any] = [] + if project_id is not None: + filters = "WHERE p.project_id = $1" + params.append(project_id) + rows = await conn.fetch( + f""" + SELECT + p.project_id, + p.project_name, + p.developer_name, + p.city, + p.micro_market, + p.address, + p.project_status, + p.location_json, + p.amenities_json, + p.created_at, + MIN(u.price_current) AS price_min, + MAX(u.price_current) AS price_max, + COALESCE( + jsonb_agg( + DISTINCT jsonb_build_object( + 'configuration', u.configuration, + 'count', 1, + 'available', CASE WHEN u.status = 'available' THEN 1 ELSE 0 END, + 'area', CASE WHEN u.area_sqft IS NULL THEN NULL ELSE concat(u.area_sqft::text, ' sqft') END, + 'price', CASE WHEN u.price_current IS NULL THEN NULL ELSE concat('INR ', u.price_current::text, ' lakh') END + ) + ) FILTER (WHERE u.unit_id IS NOT NULL), + '[]'::jsonb + ) AS unit_mix + FROM inventory_projects p + LEFT JOIN inventory_units u ON u.project_id = p.project_id + {filters} + GROUP BY p.project_id + ORDER BY p.project_name ASC + LIMIT ${len(params) + 1} OFFSET ${len(params) + 2} + """, + *params, + limit, + offset, + ) + return [_canonical_project_payload(dict(row)) for row in rows] + + # ── Pydantic Models ─────────────────────────────────────────────────────────── VALID_SOURCE_TYPES = {"csv", "json", "api_push", "manual"} @@ -237,6 +322,10 @@ async def list_properties( total = await conn.fetchval( f"SELECT COUNT(*) FROM inventory_properties {where_clause}", *params, ) + if total == 0 and not status_filter and not property_type: + canonical_rows = await _fetch_canonical_project_rows(conn, limit, offset) + canonical_total = await conn.fetchval("SELECT COUNT(*) FROM inventory_projects") + return {"total": canonical_total, "limit": limit, "offset": offset, "properties": canonical_rows} return {"total": total, "limit": limit, "offset": offset, "properties": [dict(r) for r in rows]} @@ -252,6 +341,10 @@ async def get_property( "SELECT * FROM inventory_properties WHERE property_id=$1 AND tenant_id=$2", property_id, _tenant_scope(user), ) + if not row: + canonical_rows = await _fetch_canonical_project_rows(conn, limit=1, offset=0, project_id=property_id) + if canonical_rows: + return canonical_rows[0] if not row: raise HTTPException(404, "Property not found") return dict(row)