import logging from datetime import datetime from typing import Any, Optional from app.core.database import execute_query, execute_query_single logger = logging.getLogger(__name__) class TelefoniService: @staticmethod def find_user_by_extension(extension: Optional[str]) -> list[int]: """Find all users with the given extension - returns list of user_ids.""" if not extension: return [] rows = execute_query( "SELECT user_id FROM users WHERE telefoni_aktiv = TRUE AND telefoni_extension = %s ORDER BY user_id", (extension,), ) return [int(row["user_id"]) for row in rows if row.get("user_id") is not None] @staticmethod def find_contact_by_phone(number: Optional[str]) -> Optional[dict]: """Two-step lookup: full normalised number first, 8-digit suffix as fallback.""" from app.modules.telefoni.backend.utils import phone_digits_full, phone_suffix_8 full = phone_digits_full(number) suffix = phone_suffix_8(number) if not full and not suffix: return None _contact_cte = """ SELECT c.id, c.first_name, c.last_name, ( SELECT cu.id FROM contact_companies cc JOIN customers cu ON cu.id = cc.customer_id WHERE cc.contact_id = c.id ORDER BY cc.is_primary DESC NULLS LAST, cc.id ASC LIMIT 1 ) AS company_id, ( SELECT cu.name FROM contact_companies cc JOIN customers cu ON cu.id = cc.customer_id WHERE cc.contact_id = c.id ORDER BY cc.is_primary DESC NULLS LAST, cc.id ASC LIMIT 1 ) AS company, ( SELECT COUNT(*) FROM sag_kontakter sk JOIN sag_sager s ON s.id = sk.sag_id WHERE sk.contact_id = c.id AND sk.deleted_at IS NULL AND s.deleted_at IS NULL AND LOWER(COALESCE(s.status, '')) <> 'lukket' ) AS open_case_count, ( SELECT MAX(t.started_at) FROM telefoni_opkald t WHERE t.kontakt_id = c.id ) AS last_call_at FROM contacts c """ row = None # Step 1: exact full-digit match (strips country code first) if full: query_full = _contact_cte + """ WHERE regexp_replace(COALESCE(c.phone, ''), '\\D', '', 'g') LIKE %s OR regexp_replace(COALESCE(c.mobile, ''), '\\D', '', 'g') LIKE %s ORDER BY open_case_count DESC, last_call_at DESC NULLS LAST, c.id ASC LIMIT 1 """ # Match ending with full digits (covers both with and without country code stored) pattern = f"%{full}" row = execute_query_single(query_full, (pattern, pattern)) if row: logger.debug("πŸ“ž Phone lookup: full-digit match for %s β†’ contact %s", number, row["id"]) # Step 2: 8-digit suffix fallback if not row and suffix: query_suffix = _contact_cte + """ WHERE RIGHT(regexp_replace(COALESCE(c.phone, ''), '\\D', '', 'g'), 8) = %s OR RIGHT(regexp_replace(COALESCE(c.mobile, ''), '\\D', '', 'g'), 8) = %s ORDER BY open_case_count DESC, last_call_at DESC NULLS LAST, c.id ASC LIMIT 1 """ row = execute_query_single(query_suffix, (suffix, suffix)) if row: logger.debug("πŸ“ž Phone lookup: suffix-8 fallback for %s β†’ contact %s", number, row["id"]) if not row: return None return { "id": row["id"], "name": f"{(row.get('first_name') or '').strip()} {(row.get('last_name') or '').strip()}".strip(), "company_id": row.get("company_id"), "company": row.get("company"), } @staticmethod def find_contact_by_phone_suffix(suffix8: Optional[str]) -> Optional[dict]: """Deprecated: use find_contact_by_phone(). Kept for backward compatibility.""" return TelefoniService.find_contact_by_phone(suffix8) @staticmethod def upsert_call( *, callid: str, user_id: Optional[int], direction: str, ekstern_nummer: Optional[str], intern_extension: Optional[str], kontakt_id: Optional[int], raw_payload: Any, started_at: datetime, ) -> dict: query = """ INSERT INTO telefoni_opkald (callid, bruger_id, direction, ekstern_nummer, intern_extension, kontakt_id, started_at, raw_payload) VALUES (%s, %s, %s, %s, %s, %s, %s, %s::jsonb) ON CONFLICT (callid) DO UPDATE SET raw_payload = EXCLUDED.raw_payload, direction = EXCLUDED.direction, intern_extension = COALESCE(telefoni_opkald.intern_extension, EXCLUDED.intern_extension), ekstern_nummer = COALESCE(telefoni_opkald.ekstern_nummer, EXCLUDED.ekstern_nummer), bruger_id = COALESCE(telefoni_opkald.bruger_id, EXCLUDED.bruger_id), kontakt_id = COALESCE(telefoni_opkald.kontakt_id, EXCLUDED.kontakt_id), started_at = LEAST(telefoni_opkald.started_at, EXCLUDED.started_at) RETURNING * """ rows = execute_query( query, ( callid, user_id, direction, ekstern_nummer, intern_extension, kontakt_id, started_at, raw_payload, ), ) return rows[0] if rows else {} @staticmethod def terminate_call(callid: str, duration_sec: Optional[int]) -> bool: if not callid: return False rows = execute_query( """ UPDATE telefoni_opkald SET ended_at = NOW(), duration_sec = COALESCE( %s, CASE WHEN started_at IS NOT NULL THEN GREATEST(EXTRACT(EPOCH FROM (NOW() - started_at))::int, 0) ELSE NULL END ) WHERE callid = %s RETURNING id """, (duration_sec, callid), ) return bool(rows) @staticmethod def get_contact_details(contact_id: int, company_id: Optional[int] = None) -> dict: """Get extended contact details for telefoni popups and call notifications.""" if not contact_id: return { "recent_cases": [], "contact_cases": [], "company_cases": [], "related_contacts": [], "last_call": None, } contact_row = execute_query_single( """ SELECT c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile, c.title, c.department, c.is_active, c.user_company, ( SELECT cu.id FROM contact_companies cc JOIN customers cu ON cu.id = cc.customer_id WHERE cc.contact_id = c.id ORDER BY cc.is_primary DESC NULLS LAST, cc.id ASC LIMIT 1 ) AS company_id, ( SELECT cu.name FROM contact_companies cc JOIN customers cu ON cu.id = cc.customer_id WHERE cc.contact_id = c.id ORDER BY cc.is_primary DESC NULLS LAST, cc.id ASC LIMIT 1 ) AS company FROM contacts c WHERE c.id = %s """, (contact_id,), ) effective_company_id = company_id or (contact_row.get("company_id") if contact_row else None) open_cases_query = """ SELECT s.id, s.titel, s.created_at FROM sag_sager s INNER JOIN sag_kontakter sk ON s.id = sk.sag_id WHERE sk.contact_id = %s AND s.status = 'Γ₯ben' AND s.deleted_at IS NULL AND sk.deleted_at IS NULL ORDER BY s.created_at DESC LIMIT 3 """ recent_open_cases = execute_query(open_cases_query, (contact_id,)) or [] contact_cases_query = """ SELECT s.id, s.titel, s.status, s.customer_id, cu.name AS customer_name, s.created_at, s.updated_at FROM sag_sager s INNER JOIN sag_kontakter sk ON s.id = sk.sag_id LEFT JOIN customers cu ON cu.id = s.customer_id WHERE sk.contact_id = %s AND s.deleted_at IS NULL AND sk.deleted_at IS NULL ORDER BY COALESCE(s.updated_at, s.created_at) DESC LIMIT 5 """ contact_cases = execute_query(contact_cases_query, (contact_id,)) or [] company_cases = [] related_contacts = [] if effective_company_id: company_cases_query = """ SELECT s.id, s.titel, s.status, s.customer_id, cu.name AS customer_name, s.created_at, s.updated_at FROM sag_sager s LEFT JOIN customers cu ON cu.id = s.customer_id WHERE s.customer_id = %s AND s.deleted_at IS NULL ORDER BY COALESCE(s.updated_at, s.created_at) DESC LIMIT 5 """ company_cases = execute_query(company_cases_query, (effective_company_id,)) or [] related_contacts_query = """ SELECT c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile, c.title, c.department, c.is_active, c.created_at, c.updated_at, ARRAY_AGG(DISTINCT cu.name ORDER BY cu.name) FILTER (WHERE cu.name IS NOT NULL) AS company_names FROM contacts c INNER JOIN contact_companies cc ON c.id = cc.contact_id INNER JOIN customers cu ON cc.customer_id = cu.id WHERE cc.customer_id = %s AND c.id <> %s AND c.is_active = TRUE GROUP BY c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile, c.title, c.department, c.is_active, c.created_at, c.updated_at ORDER BY c.last_name, c.first_name LIMIT 8 """ related_contacts = execute_query(related_contacts_query, (effective_company_id, contact_id)) or [] last_call_query = """ SELECT t.started_at, t.bruger_id, t.duration_sec, u.full_name, u.username FROM telefoni_opkald t LEFT JOIN users u ON t.bruger_id = u.user_id WHERE t.kontakt_id = %s AND t.ended_at IS NOT NULL ORDER BY t.started_at DESC LIMIT 1 """ last_call_row = execute_query_single(last_call_query, (contact_id,)) last_call_data = None if last_call_row: last_call_data = { "started_at": last_call_row.get("started_at"), "bruger_navn": last_call_row.get("full_name") or last_call_row.get("username"), "duration_sec": last_call_row.get("duration_sec"), } return { "recent_cases": [ { "id": case["id"], "titel": case["titel"], "created_at": case["created_at"], } for case in recent_open_cases ], "contact_cases": [ { "id": case["id"], "titel": case["titel"], "status": case.get("status"), "customer_id": case.get("customer_id"), "customer_name": case.get("customer_name"), "created_at": case.get("created_at"), "updated_at": case.get("updated_at"), } for case in contact_cases ], "company_cases": [ { "id": case["id"], "titel": case["titel"], "status": case.get("status"), "customer_id": case.get("customer_id"), "customer_name": case.get("customer_name"), "created_at": case.get("created_at"), "updated_at": case.get("updated_at"), } for case in company_cases ], "related_contacts": [ { "id": contact["id"], "first_name": contact.get("first_name"), "last_name": contact.get("last_name"), "email": contact.get("email"), "phone": contact.get("phone"), "mobile": contact.get("mobile"), "title": contact.get("title"), "department": contact.get("department"), "is_active": contact.get("is_active"), "company_names": contact.get("company_names") or [], } for contact in related_contacts ], "last_call": last_call_data, }