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 1323320fed
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") @router.get("/mission/webhook/telefoni/answered")
async def mission_telefoni_answered_get(request: Request, token: Optional[str] = Query(None)): 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) event = _event_from_query(request)
return await mission_telefoni_answered(event, request, token) 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") @router.get("/mission/webhook/telefoni/hangup")
async def mission_telefoni_hangup_get(request: Request, token: Optional[str] = Query(None)): 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) event = _event_from_query(request)
return await mission_telefoni_hangup(event, request, token) return await mission_telefoni_hangup(event, request, token)

View File

@ -26,6 +26,11 @@ logger = logging.getLogger(__name__)
router = APIRouter() 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: def _get_user_id_from_request(request: Request) -> int:
user_id = getattr(request.state, "user_id", None) user_id = getattr(request.state, "user_id", None)
if user_id is not 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).""" """List all sale items across cases (orders overview)."""
try: try:
if not _table_exists("sag_salgsvarer"):
logger.warning("⚠️ sag_salgsvarer table missing - returning empty sale items list")
return []
query = """ query = """
SELECT si.*, s.titel AS sag_titel, s.customer_id, c.name AS customer_name SELECT si.*, s.titel AS sag_titel, s.customer_id, c.name AS customer_name
FROM sag_salgsvarer si FROM sag_salgsvarer si
@ -1205,6 +1214,10 @@ async def get_varekob_salg(sag_id: int, include_subcases: bool = True):
if not check: if not check:
raise HTTPException(status_code=404, detail="Case not found") 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: if include_subcases:
case_tree_query = """ case_tree_query = """
WITH RECURSIVE normalized_relations AS ( WITH RECURSIVE normalized_relations AS (
@ -1268,6 +1281,7 @@ async def get_varekob_salg(sag_id: int, include_subcases: bool = True):
""" """
time_entries = execute_query(time_query, (sag_id,)) time_entries = execute_query(time_query, (sag_id,))
if has_sale_items_table:
sale_items_query = """ sale_items_query = """
WITH RECURSIVE normalized_relations AS ( WITH RECURSIVE normalized_relations AS (
SELECT SELECT
@ -1298,6 +1312,8 @@ async def get_varekob_salg(sag_id: int, include_subcases: bool = True):
ORDER BY si.line_date DESC NULLS LAST, si.id DESC ORDER BY si.line_date DESC NULLS LAST, si.id DESC
""" """
sale_items = execute_query(sale_items_query, (sag_id,)) sale_items = execute_query(sale_items_query, (sag_id,))
else:
sale_items = []
else: else:
case_tree = execute_query( case_tree = execute_query(
"SELECT id, titel FROM sag_sager WHERE id = %s AND deleted_at IS NULL", "SELECT id, titel FROM sag_sager WHERE id = %s AND deleted_at IS NULL",
@ -1312,6 +1328,7 @@ async def get_varekob_salg(sag_id: int, include_subcases: bool = True):
""" """
time_entries = execute_query(time_query, (sag_id,)) time_entries = execute_query(time_query, (sag_id,))
if has_sale_items_table:
sale_items_query = """ sale_items_query = """
SELECT si.*, s.titel AS source_sag_titel SELECT si.*, s.titel AS source_sag_titel
FROM sag_salgsvarer si FROM sag_salgsvarer si
@ -1320,6 +1337,8 @@ async def get_varekob_salg(sag_id: int, include_subcases: bool = True):
ORDER BY si.line_date DESC NULLS LAST, si.id DESC ORDER BY si.line_date DESC NULLS LAST, si.id DESC
""" """
sale_items = execute_query(sale_items_query, (sag_id,)) sale_items = execute_query(sale_items_query, (sag_id,))
else:
sale_items = []
total_entries = len(time_entries or []) total_entries = len(time_entries or [])
total_hours = 0 total_hours = 0
@ -1497,6 +1516,10 @@ async def list_sale_items(sag_id: int):
if not check: if not check:
raise HTTPException(status_code=404, detail="Case not found") 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 = """ query = """
SELECT si.*, s.titel AS source_sag_titel SELECT si.*, s.titel AS source_sag_titel
FROM sag_salgsvarer si FROM sag_salgsvarer si