bmc_hub/app/modules/remote/telephony.py
Christian 85e019783b Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00

88 lines
4.5 KiB
Python

"""Contact lookup and restricted control of the owner's existing VoIP phone."""
from fastapi import HTTPException
from psycopg2.extras import Json
from app.core.database import atomic_transaction, execute_query, execute_query_single
from app.dashboard.backend.mission_service import MissionService
from .security import actor, remote_device
def permitted(user):
return bool(user.get('is_superadmin') or 'customers.view' in (user.get('permissions') or []))
def require_access(user):
if not permitted(user):
raise HTTPException(403, 'Du har ikke adgang til kontakter og telefoni')
async def contacts(user, q, offset):
require_access(user)
from app.contacts.backend.router_simple import get_contacts
data = await get_contacts(search=q, customer_id=None, is_active=True, limit=50, offset=offset)
fields = ('id', 'first_name', 'last_name', 'email', 'phone', 'mobile', 'company_names')
return {'items': [{key: row.get(key) for key in fields} for row in data['contacts']],
'has_more': offset + len(data['contacts']) < data['total']}
def status(user):
if not permitted(user):
return {'allowed': False, 'incoming': []}
# Match Mission Control's ring window without changing shared call state.
rows = MissionService.get_active_calls()
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
now = datetime.now(timezone.utc)
incoming = []
for row in rows:
updated = row.get('updated_at') or row.get('started_at')
if not updated:
continue
if updated.tzinfo is None:
updated = updated.replace(tzinfo=ZoneInfo('Europe/Copenhagen'))
if (now - updated).total_seconds() > MissionService.get_ring_timeout_seconds():
continue
incoming.append({key: row.get(key) for key in ('call_id', 'caller_number', 'contact_name', 'company_name', 'ringing_phones')})
return {'allowed': True, 'incoming': incoming}
async def call(payload, request):
"""Persist a reservation before external I/O; never automatically redial on uncertainty."""
from app.modules.telefoni.backend.router import click_to_call
from app.modules.telefoni.backend.schemas import TelefoniClickToCallRequest
user = actor(remote_device(request))
require_access(user)
uid = user['id']
value = {'action': 'call', **payload.model_dump(mode='json')}
with atomic_transaction():
execute_query('SELECT user_id FROM users WHERE user_id=%s FOR UPDATE', (uid,))
user = actor(remote_device(request)); require_access(user)
old = execute_query_single('SELECT payload,result FROM remote_commands WHERE user_id=%s AND request_id=%s', (uid, str(payload.request_id)))
if old:
if old['payload'] != value:
raise HTTPException(409, 'Handlingen har allerede andre oplysninger')
return old['result']
contact = execute_query_single('SELECT id,phone,mobile FROM contacts WHERE id=%s AND is_active=TRUE', (payload.contact_id,))
if not contact or not contact.get(payload.field):
raise HTTPException(404, 'Kontakten har ikke det valgte telefonnummer')
# Only stored phone numbers can reach the existing action URL template.
import re
number = str(contact[payload.field]).strip()
if not re.fullmatch(r'\+?[0-9][0-9 () .-]{1,39}', number):
raise HTTPException(400, 'Kontaktens telefonnummer skal rettes i Hub')
pending = {'status': 'uncertain', 'message': 'Opkald er afsendt eller under behandling. Kontrollér telefonen før et nyt opkald.'}
execute_query('INSERT INTO remote_commands(user_id,request_id,payload,result) VALUES(%s,%s,%s,%s)',
(uid, str(payload.request_id), Json(value), Json(pending)))
request.state.user_id = uid
try:
await click_to_call(TelefoniClickToCallRequest(number=number, user_id=uid), request)
result = {'status': 'ok', 'message': 'Opkald startet på din VoIP-telefon'}
except HTTPException as exc:
# A timeout can mean that the phone received the command. Never redial it.
result = {'status': 'failed' if exc.status_code < 500 else 'uncertain',
'message': 'Opkald kunne ikke startes. Kontrollér telefonopsætningen i Hub.' if exc.status_code < 500 else pending['message']}
except Exception:
result = pending
execute_query('UPDATE remote_commands SET result=%s WHERE user_id=%s AND request_id=%s',
(Json(result), uid, str(payload.request_id)))
return result