- 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.
88 lines
4.5 KiB
Python
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
|