22from typing import Optional
33
44import httpx
5- from fastapi import APIRouter , HTTPException , Request
5+ from fastapi import APIRouter , Depends , HTTPException , Request
66from fastapi .responses import Response
77from pydantic import BaseModel , Field
8+ from sqlalchemy .ext .asyncio import AsyncSession
89
910from app .config import settings
11+ from app .database import get_db
1012from app .integrations .phone import normalize_phone_number
1113from app .integrations .twilio_client import (
1214 TwilioConfigError ,
1517 start_outbound_call ,
1618)
1719from app .models import CompleteResponse , Session , Worker , new_id , utc_now_iso
20+ from app .services import store
1821from app .services .session_runtime import complete_session , create_session , require_session , update_session
19- from app .services .store import call_session_index , sessions , workers
22+ from app .services .store import call_session_index
2023
2124router = APIRouter (tags = ["call" ])
2225
@@ -35,23 +38,23 @@ class TwilioStatusResponse(BaseModel):
3538 status : Optional [str ] = None
3639
3740
38- def _find_worker_by_phone (phone_number : str ) -> Optional [Worker ]:
41+ async def _find_worker_by_phone (db : AsyncSession , phone_number : str ) -> Optional [Worker ]:
3942 normalized = normalize_phone_number (phone_number )
40- for worker in workers .values ():
41- if normalize_phone_number (worker .phone_number or "" ) == normalized :
42- return worker
43- return None
43+ if not normalized :
44+ return None
45+ return await store .get_worker_by_phone (db , normalized )
4446
4547
46- def _upsert_worker (
48+ async def _upsert_worker (
49+ db : AsyncSession ,
4750 * ,
4851 phone_number : Optional [str ],
4952 worker_name : Optional [str ],
5053 specialization : Optional [str ],
5154 experience_years : int ,
5255) -> Worker :
5356 normalized_phone = normalize_phone_number (phone_number )
54- existing = _find_worker_by_phone (normalized_phone or "" ) if normalized_phone else None
57+ existing = await _find_worker_by_phone (db , normalized_phone or "" ) if normalized_phone else None
5558 if existing :
5659 updates = {}
5760 if worker_name :
@@ -61,7 +64,7 @@ def _upsert_worker(
6164 updates ["experience_years" ] = max (existing .experience_years , experience_years )
6265 if updates :
6366 updated = existing .model_copy (update = updates )
64- workers [ existing . id ] = updated
67+ await store . update_worker ( db , updated )
6568 return updated
6669 return existing
6770
@@ -73,7 +76,7 @@ def _upsert_worker(
7376 phone_number = normalized_phone ,
7477 created_at = utc_now_iso (),
7578 )
76- workers [ worker . id ] = worker
79+ await store . create_worker ( db , worker )
7780 return worker
7881
7982
@@ -94,15 +97,20 @@ def _build_greeting_text(session: Session) -> str:
9497
9598
9699@router .post ("/calls/twilio/start" )
97- async def start_twilio_screening_call (payload : TwilioCallStartRequest ):
98- worker = _upsert_worker (
100+ async def start_twilio_screening_call (
101+ payload : TwilioCallStartRequest ,
102+ db : AsyncSession = Depends (get_db ),
103+ ):
104+ worker = await _upsert_worker (
105+ db ,
99106 phone_number = payload .phone_number ,
100107 worker_name = payload .worker_name ,
101108 specialization = payload .specialization ,
102109 experience_years = payload .experience_years ,
103110 )
104111 assignment = (payload .assignment or settings .twilio_default_assignment ).strip ()
105- session , first_question = create_session (
112+ session , first_question = await create_session (
113+ db ,
106114 worker ,
107115 assignment ,
108116 interview_mode = "call" ,
@@ -122,7 +130,8 @@ async def start_twilio_screening_call(payload: TwilioCallStartRequest):
122130 except httpx .HTTPError as exc :
123131 raise HTTPException (status_code = 502 , detail = f"Twilio call start failed: { exc } " ) from exc
124132
125- session = update_session (
133+ session = await update_session (
134+ db ,
126135 session .id ,
127136 external_call_id = outbound .get ("sid" ),
128137 external_call_status = outbound .get ("status" ) or "queued" ,
@@ -143,45 +152,46 @@ async def start_twilio_screening_call(payload: TwilioCallStartRequest):
143152
144153
145154@router .api_route ("/calls/twilio/twiml" , methods = ["GET" , "POST" ])
146- async def twilio_twiml (request : Request ) -> Response :
155+ async def twilio_twiml (
156+ request : Request ,
157+ db : AsyncSession = Depends (get_db ),
158+ ) -> Response :
147159 query = dict (request .query_params )
148160 body = dict (await request .form ()) if request .method == "POST" else {}
149161 payload = {** query , ** body }
150162 session_id = payload .get ("session_id" )
151163 if not session_id :
152164 raise HTTPException (status_code = 400 , detail = "session_id is required" )
153165
154- session = require_session (str (session_id ))
166+ session = await require_session (db , str (session_id ))
155167 return _twiml_response (_build_greeting_text (session ))
156168
157169
158170@router .api_route ("/calls/twilio/incoming" , methods = ["GET" , "POST" ])
159- async def twilio_incoming_webhook (request : Request ) -> Response :
160- """
161- Incoming voice webhook used when a caller dials your Twilio number directly.
162-
163- Creates a call-mode session on-the-fly and responds with TwiML that
164- speaks the phone-assessment greeting and the first interview question.
165- """
171+ async def twilio_incoming_webhook (
172+ request : Request ,
173+ db : AsyncSession = Depends (get_db ),
174+ ) -> Response :
166175 query = dict (request .query_params )
167176 body = dict (await request .form ()) if request .method == "POST" else {}
168177 payload : dict [str , object ] = {** query , ** body }
169178
170- # Twilio sends From (caller) + CallSid on voice webhooks.
171179 from_number = payload .get ("From" ) or payload .get ("from" ) or payload .get ("Caller" )
172180 call_sid = extract_twilio_sid (payload )
173181 if not from_number :
174182 raise HTTPException (status_code = 400 , detail = "From number is required" )
175183
176- worker = _upsert_worker (
184+ worker = await _upsert_worker (
185+ db ,
177186 phone_number = str (from_number ),
178187 worker_name = "श्रमिक" ,
179188 specialization = None ,
180189 experience_years = 0 ,
181190 )
182191
183192 assignment = settings .twilio_default_assignment .strip ()
184- session , _ = create_session (
193+ session , _ = await create_session (
194+ db ,
185195 worker ,
186196 assignment ,
187197 interview_mode = "call" ,
@@ -193,14 +203,16 @@ async def twilio_incoming_webhook(request: Request) -> Response:
193203 )
194204
195205 if call_sid :
196- # Enables /calls/twilio/status to resolve the session even without session_id query params.
197206 call_session_index [call_sid ] = session .id
198207
199208 return _twiml_response (_build_greeting_text (session ))
200209
201210
202211@router .api_route ("/calls/twilio/status" , methods = ["GET" , "POST" ], response_model = TwilioStatusResponse )
203- async def twilio_status_callback (request : Request ) -> TwilioStatusResponse :
212+ async def twilio_status_callback (
213+ request : Request ,
214+ db : AsyncSession = Depends (get_db ),
215+ ) -> TwilioStatusResponse :
204216 body = await request .form () if request .method == "POST" else {}
205217 payload = {** dict (request .query_params ), ** dict (body )}
206218 call_sid = extract_twilio_sid (payload )
@@ -212,7 +224,7 @@ async def twilio_status_callback(request: Request) -> TwilioStatusResponse:
212224 resolved_session_id = None
213225 if session_id :
214226 try :
215- require_session (str (session_id ))
227+ await require_session (db , str (session_id ))
216228 resolved_session_id = str (session_id )
217229 except HTTPException :
218230 resolved_session_id = None
@@ -226,18 +238,20 @@ async def twilio_status_callback(request: Request) -> TwilioStatusResponse:
226238 except ValueError :
227239 parsed_duration = None
228240
229- session = update_session (
241+ current = await require_session (db , resolved_session_id )
242+ session = await update_session (
243+ db ,
230244 resolved_session_id ,
231- external_call_id = call_sid or require_session ( resolved_session_id ) .external_call_id ,
232- external_call_status = str (status ) if status else require_session ( resolved_session_id ) .external_call_status ,
233- call_duration_seconds = parsed_duration or require_session ( resolved_session_id ) .call_duration_seconds ,
234- latest_call_recording_url = str (recording_url ) if recording_url else require_session ( resolved_session_id ) .latest_call_recording_url ,
245+ external_call_id = call_sid or current .external_call_id ,
246+ external_call_status = str (status ) if status else current .external_call_status ,
247+ call_duration_seconds = parsed_duration or current .call_duration_seconds ,
248+ latest_call_recording_url = str (recording_url ) if recording_url else current .latest_call_recording_url ,
235249 )
236250 if call_sid :
237251 call_session_index [call_sid ] = session .id
238252
239253 terminal_statuses = {"completed" , "busy" , "failed" , "no-answer" , "canceled" }
240254 if (session .external_call_status or "" ).lower () in terminal_statuses and session .status == "live" :
241- session = complete_session (session .id , locale = "hi" )
255+ session = await complete_session (db , session .id , locale = "hi" )
242256
243257 return TwilioStatusResponse (ok = True , session_id = session .id , status = session .external_call_status )
0 commit comments