Release v2.2.48: sag sale-item fallback and mission webhook ping fixes

This commit is contained in:
Christian 2026-03-04 07:40:18 +01:00
parent 9fc57feda4
commit 500bb5eaf3
4 changed files with 95 additions and 39 deletions

21
RELEASE_NOTES_v2.2.48.md Normal file
View File

@ -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=<TOKEN>"`
- `curl -i "http://localhost:8001/api/v1/mission/webhook/telefoni/answered?token=<TOKEN>"`
- `curl -i "http://localhost:8001/api/v1/mission/webhook/telefoni/hangup?token=<TOKEN>"`

View File

@ -1 +1 @@
2.2.47
2.2.48

View File

@ -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)

View File

@ -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