From 1323320fed7a88580f07225963d372d4816e9def Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 4 Mar 2026 07:40:18 +0100 Subject: [PATCH] Release v2.2.48: sag sale-item fallback and mission webhook ping fixes --- RELEASE_NOTES_v2.2.48.md | 21 ++++++ VERSION | 2 +- app/dashboard/backend/mission_router.py | 12 +++ app/modules/sag/backend/router.py | 99 +++++++++++++++---------- 4 files changed, 95 insertions(+), 39 deletions(-) create mode 100644 RELEASE_NOTES_v2.2.48.md diff --git a/RELEASE_NOTES_v2.2.48.md b/RELEASE_NOTES_v2.2.48.md new file mode 100644 index 0000000..fe4866f --- /dev/null +++ b/RELEASE_NOTES_v2.2.48.md @@ -0,0 +1,21 @@ +# Release Notes v2.2.48 + +Dato: 4. marts 2026 + +## Fixes +- `sag` aggregering fejler ikke længere hvis tabellen `sag_salgsvarer` mangler; API returnerer fortsat tidsdata og tom salgsliste i stedet for `500`. +- Salgsliste-endpoints i `sag` returnerer nu tom liste med advarsel i log, hvis `sag_salgsvarer` ikke findes. +- Mission webhooks for `answered` og `hangup` accepterer nu også token-only `GET` ping uden `call_id` (samme kompatibilitet som `ringing`). + +## Ændrede filer +- `app/modules/sag/backend/router.py` +- `app/dashboard/backend/mission_router.py` +- `VERSION` +- `RELEASE_NOTES_v2.2.48.md` + +## Drift +- Deploy: `./updateto.sh v2.2.48` +- Valider webhook ping: + - `curl -i "http://localhost:8001/api/v1/mission/webhook/telefoni/ringing?token="` + - `curl -i "http://localhost:8001/api/v1/mission/webhook/telefoni/answered?token="` + - `curl -i "http://localhost:8001/api/v1/mission/webhook/telefoni/hangup?token="` diff --git a/VERSION b/VERSION index 4e97bd6..b5c8a81 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.47 +2.2.48 diff --git a/app/dashboard/backend/mission_router.py b/app/dashboard/backend/mission_router.py index 2e96543..375aa89 100644 --- a/app/dashboard/backend/mission_router.py +++ b/app/dashboard/backend/mission_router.py @@ -268,6 +268,12 @@ async def mission_telefoni_answered(event: MissionCallEvent, request: Request, t @router.get("/mission/webhook/telefoni/answered") async def mission_telefoni_answered_get(request: Request, token: Optional[str] = Query(None)): + _validate_mission_webhook_token(request, token) + + if not _first_query_param(request, "call_id", "callid", "id", "session_id", "uuid"): + logger.info("✅ Mission webhook answered ping method=%s", request.method) + return {"status": "ok", "mode": "ping"} + event = _event_from_query(request) return await mission_telefoni_answered(event, request, token) @@ -311,6 +317,12 @@ async def mission_telefoni_hangup(event: MissionCallEvent, request: Request, tok @router.get("/mission/webhook/telefoni/hangup") async def mission_telefoni_hangup_get(request: Request, token: Optional[str] = Query(None)): + _validate_mission_webhook_token(request, token) + + if not _first_query_param(request, "call_id", "callid", "id", "session_id", "uuid"): + logger.info("📴 Mission webhook hangup ping method=%s", request.method) + return {"status": "ok", "mode": "ping"} + event = _event_from_query(request) return await mission_telefoni_hangup(event, request, token) diff --git a/app/modules/sag/backend/router.py b/app/modules/sag/backend/router.py index 125e740..b296d1f 100644 --- a/app/modules/sag/backend/router.py +++ b/app/modules/sag/backend/router.py @@ -26,6 +26,11 @@ logger = logging.getLogger(__name__) router = APIRouter() +def _table_exists(table_name: str) -> bool: + row = execute_query_single("SELECT to_regclass(%s) AS table_name", (f"public.{table_name}",)) + return bool(row and row.get("table_name")) + + def _get_user_id_from_request(request: Request) -> int: user_id = getattr(request.state, "user_id", None) if user_id is not None: @@ -213,6 +218,10 @@ async def list_all_sale_items( ): """List all sale items across cases (orders overview).""" try: + if not _table_exists("sag_salgsvarer"): + logger.warning("⚠️ sag_salgsvarer table missing - returning empty sale items list") + return [] + query = """ SELECT si.*, s.titel AS sag_titel, s.customer_id, c.name AS customer_name FROM sag_salgsvarer si @@ -1205,6 +1214,10 @@ async def get_varekob_salg(sag_id: int, include_subcases: bool = True): if not check: raise HTTPException(status_code=404, detail="Case not found") + has_sale_items_table = _table_exists("sag_salgsvarer") + if not has_sale_items_table: + logger.warning("⚠️ sag_salgsvarer table missing - sale item aggregation skipped for sag_id=%s", sag_id) + if include_subcases: case_tree_query = """ WITH RECURSIVE normalized_relations AS ( @@ -1268,36 +1281,39 @@ async def get_varekob_salg(sag_id: int, include_subcases: bool = True): """ time_entries = execute_query(time_query, (sag_id,)) - sale_items_query = """ - WITH RECURSIVE normalized_relations AS ( - SELECT - CASE - WHEN LOWER(relationstype) IN ('afledt af', 'afledt_af') THEN målsag_id - WHEN LOWER(relationstype) IN ('årsag til', 'årsag_til') THEN kilde_sag_id - ELSE kilde_sag_id - END AS parent_id, - CASE - WHEN LOWER(relationstype) IN ('afledt af', 'afledt_af') THEN kilde_sag_id - WHEN LOWER(relationstype) IN ('årsag til', 'årsag_til') THEN målsag_id - ELSE målsag_id - END AS child_id - FROM sag_relationer - WHERE deleted_at IS NULL - ), - case_tree AS ( - SELECT id FROM sag_sager WHERE id = %s AND deleted_at IS NULL - UNION - SELECT nr.child_id - FROM normalized_relations nr - JOIN case_tree ct ON nr.parent_id = ct.id - ) - SELECT si.*, s.titel AS source_sag_titel - FROM sag_salgsvarer si - JOIN case_tree ct ON si.sag_id = ct.id - LEFT JOIN sag_sager s ON s.id = si.sag_id - ORDER BY si.line_date DESC NULLS LAST, si.id DESC - """ - sale_items = execute_query(sale_items_query, (sag_id,)) + if has_sale_items_table: + sale_items_query = """ + WITH RECURSIVE normalized_relations AS ( + SELECT + CASE + WHEN LOWER(relationstype) IN ('afledt af', 'afledt_af') THEN målsag_id + WHEN LOWER(relationstype) IN ('årsag til', 'årsag_til') THEN kilde_sag_id + ELSE kilde_sag_id + END AS parent_id, + CASE + WHEN LOWER(relationstype) IN ('afledt af', 'afledt_af') THEN kilde_sag_id + WHEN LOWER(relationstype) IN ('årsag til', 'årsag_til') THEN målsag_id + ELSE målsag_id + END AS child_id + FROM sag_relationer + WHERE deleted_at IS NULL + ), + case_tree AS ( + SELECT id FROM sag_sager WHERE id = %s AND deleted_at IS NULL + UNION + SELECT nr.child_id + FROM normalized_relations nr + JOIN case_tree ct ON nr.parent_id = ct.id + ) + SELECT si.*, s.titel AS source_sag_titel + FROM sag_salgsvarer si + JOIN case_tree ct ON si.sag_id = ct.id + LEFT JOIN sag_sager s ON s.id = si.sag_id + ORDER BY si.line_date DESC NULLS LAST, si.id DESC + """ + sale_items = execute_query(sale_items_query, (sag_id,)) + else: + sale_items = [] else: case_tree = execute_query( "SELECT id, titel FROM sag_sager WHERE id = %s AND deleted_at IS NULL", @@ -1312,14 +1328,17 @@ async def get_varekob_salg(sag_id: int, include_subcases: bool = True): """ time_entries = execute_query(time_query, (sag_id,)) - sale_items_query = """ - SELECT si.*, s.titel AS source_sag_titel - FROM sag_salgsvarer si - LEFT JOIN sag_sager s ON s.id = si.sag_id - WHERE si.sag_id = %s - ORDER BY si.line_date DESC NULLS LAST, si.id DESC - """ - sale_items = execute_query(sale_items_query, (sag_id,)) + if has_sale_items_table: + sale_items_query = """ + SELECT si.*, s.titel AS source_sag_titel + FROM sag_salgsvarer si + LEFT JOIN sag_sager s ON s.id = si.sag_id + WHERE si.sag_id = %s + ORDER BY si.line_date DESC NULLS LAST, si.id DESC + """ + sale_items = execute_query(sale_items_query, (sag_id,)) + else: + sale_items = [] total_entries = len(time_entries or []) total_hours = 0 @@ -1497,6 +1516,10 @@ async def list_sale_items(sag_id: int): if not check: raise HTTPException(status_code=404, detail="Case not found") + if not _table_exists("sag_salgsvarer"): + logger.warning("⚠️ sag_salgsvarer table missing - returning empty sale items list for sag_id=%s", sag_id) + return [] + query = """ SELECT si.*, s.titel AS source_sag_titel FROM sag_salgsvarer si