codeBOKER commited on
Commit
d4f6fe7
·
1 Parent(s): 102dd4f

change to (remoteJid)

Browse files
app/api/routes.py CHANGED
@@ -136,7 +136,7 @@ async def llm_tool_call_debug(
136
  tool_calls: list[dict[str, Any]] = []
137
  tool_results: list[dict[str, Any]] = []
138
  registry = container.conversation._tool_registry(
139
- {"id": "admin-debug", "phone_number": "", "name": "admin-debug"},
140
  sender_phone="admin-debug",
141
  user_mode="passenger",
142
  )
@@ -187,7 +187,7 @@ async def driver_service_debug(
187
  tool_results: list[dict[str, Any]] = []
188
  customer = {
189
  "id": "debug",
190
- "phone_number": request.client_number,
191
  "name": "driver-debug",
192
  }
193
  registry = container.conversation._tool_registry(
 
136
  tool_calls: list[dict[str, Any]] = []
137
  tool_results: list[dict[str, Any]] = []
138
  registry = container.conversation._tool_registry(
139
+ {"id": "admin-debug", "remoteJid": "", "name": "admin-debug"},
140
  sender_phone="admin-debug",
141
  user_mode="passenger",
142
  )
 
187
  tool_results: list[dict[str, Any]] = []
188
  customer = {
189
  "id": "debug",
190
+ "remoteJid": request.client_number,
191
  "name": "driver-debug",
192
  }
193
  registry = container.conversation._tool_registry(
app/database/supabase.py CHANGED
@@ -35,19 +35,19 @@ class SupabaseRepository:
35
  async def upsert_customer(
36
  self,
37
  *,
38
- phone_number: str,
39
  name: str | None = None,
40
  preferred_language: str | None = None,
41
  ) -> dict[str, Any]:
42
  payload = {
43
- "phone_number": phone_number,
44
  "name": name,
45
  "preferred_language": preferred_language,
46
  }
47
  payload = {key: value for key, value in payload.items() if value is not None}
48
  response = await (
49
  self.client.table("customers")
50
- .upsert(payload, on_conflict="phone_number")
51
  .execute()
52
  )
53
  data = _response_data(response)
@@ -149,7 +149,7 @@ class SupabaseRepository:
149
  async def list_active_trips(self) -> list[dict[str, Any]]:
150
  query = (
151
  self.client.table("driver_trips")
152
- .select("*, drivers(*), driver_cars(*)")
153
  .eq("status", "active")
154
  .gt("available_seats", 0)
155
  )
@@ -166,7 +166,7 @@ class SupabaseRepository:
166
  return []
167
  response = await (
168
  self.client.table("driver_trips")
169
- .select("*, drivers(*), driver_cars(*)")
170
  .in_("id", trip_ids)
171
  .execute()
172
  )
@@ -183,7 +183,7 @@ class SupabaseRepository:
183
  ) -> list[dict[str, Any]]:
184
  query = (
185
  self.client.table("driver_trips")
186
- .select("*, drivers(*), driver_cars(*)")
187
  .eq("status", "active")
188
  .gt("available_seats", 0)
189
  )
@@ -308,27 +308,27 @@ class SupabaseRepository:
308
  async def get_trip_by_id(self, trip_id: str) -> dict[str, Any] | None:
309
  response = await (
310
  self.client.table("driver_trips")
311
- .select("*, drivers(*), driver_cars(*)")
312
  .eq("id", trip_id)
313
  .maybe_single()
314
  .execute()
315
  )
316
  return _response_data(response)
317
 
318
- async def get_driver_by_phone(self, phone_number: str) -> dict[str, Any] | None:
319
  response = await (
320
  self.client.table("drivers")
321
- .select("*")
322
- .eq("phone_number", phone_number)
323
  .maybe_single()
324
  .execute()
325
  )
326
  return _response_data(response)
327
 
328
- async def create_driver(self, *, name: str, phone_number: str) -> dict[str, Any]:
329
  response = await (
330
  self.client.table("drivers")
331
- .insert({"name": name, "phone_number": phone_number, "status": "active"})
332
  .execute()
333
  )
334
  data = _response_data(response)
@@ -338,7 +338,14 @@ class SupabaseRepository:
338
  .insert({"driver_id": driver["id"], "balance": 0})
339
  .execute()
340
  )
341
- return driver
 
 
 
 
 
 
 
342
 
343
  async def get_driver_latest_trip(self, driver_id: str) -> dict[str, Any] | None:
344
  response = await (
 
35
  async def upsert_customer(
36
  self,
37
  *,
38
+ remote_jid: str,
39
  name: str | None = None,
40
  preferred_language: str | None = None,
41
  ) -> dict[str, Any]:
42
  payload = {
43
+ "remoteJid": remote_jid,
44
  "name": name,
45
  "preferred_language": preferred_language,
46
  }
47
  payload = {key: value for key, value in payload.items() if value is not None}
48
  response = await (
49
  self.client.table("customers")
50
+ .upsert(payload, on_conflict="remoteJid")
51
  .execute()
52
  )
53
  data = _response_data(response)
 
149
  async def list_active_trips(self) -> list[dict[str, Any]]:
150
  query = (
151
  self.client.table("driver_trips")
152
+ .select("*, drivers(*, customers(*)), driver_cars(*)")
153
  .eq("status", "active")
154
  .gt("available_seats", 0)
155
  )
 
166
  return []
167
  response = await (
168
  self.client.table("driver_trips")
169
+ .select("*, drivers(*, customers(*)), driver_cars(*)")
170
  .in_("id", trip_ids)
171
  .execute()
172
  )
 
183
  ) -> list[dict[str, Any]]:
184
  query = (
185
  self.client.table("driver_trips")
186
+ .select("*, drivers(*, customers(*)), driver_cars(*)")
187
  .eq("status", "active")
188
  .gt("available_seats", 0)
189
  )
 
308
  async def get_trip_by_id(self, trip_id: str) -> dict[str, Any] | None:
309
  response = await (
310
  self.client.table("driver_trips")
311
+ .select("*, drivers(*, customers(*)), driver_cars(*)")
312
  .eq("id", trip_id)
313
  .maybe_single()
314
  .execute()
315
  )
316
  return _response_data(response)
317
 
318
+ async def get_driver_by_remoteJid(self, remote_jid: str) -> dict[str, Any] | None:
319
  response = await (
320
  self.client.table("drivers")
321
+ .select("*, customers!inner(*)")
322
+ .eq("customers.remoteJid", remote_jid)
323
  .maybe_single()
324
  .execute()
325
  )
326
  return _response_data(response)
327
 
328
+ async def create_driver(self, *, customer_id: str) -> dict[str, Any]:
329
  response = await (
330
  self.client.table("drivers")
331
+ .insert({"customer_id": customer_id, "status": "active"})
332
  .execute()
333
  )
334
  data = _response_data(response)
 
338
  .insert({"driver_id": driver["id"], "balance": 0})
339
  .execute()
340
  )
341
+ driver_response = await (
342
+ self.client.table("drivers")
343
+ .select("*, customers(*)")
344
+ .eq("id", str(driver["id"]))
345
+ .maybe_single()
346
+ .execute()
347
+ )
348
+ return _response_data(driver_response)
349
 
350
  async def get_driver_latest_trip(self, driver_id: str) -> dict[str, Any] | None:
351
  response = await (
app/models/domain.py CHANGED
@@ -8,7 +8,7 @@ UserMode = Literal["new_user", "driver", "passenger"]
8
  @dataclass(slots=True)
9
  class WhatsAppInboundMessage:
10
  message_id: str
11
- from_phone: str
12
  text: str
13
  timestamp: str | None = None
14
  profile_name: str | None = None
 
8
  @dataclass(slots=True)
9
  class WhatsAppInboundMessage:
10
  message_id: str
11
+ remoteJid: str
12
  text: str
13
  timestamp: str | None = None
14
  profile_name: str | None = None
app/services/conversation_service.py CHANGED
@@ -70,7 +70,7 @@ class ConversationService:
70
  return None
71
 
72
  customer = await self.repository.upsert_customer(
73
- phone_number=inbound.from_phone,
74
  name=inbound.profile_name,
75
  )
76
  current_message = await self.repository.create_message(
@@ -88,7 +88,7 @@ class ConversationService:
88
  )
89
 
90
  user_mode = _resolve_user_mode(customer)
91
- registry = self._tool_registry(customer, sender_phone=inbound.from_phone, user_mode=user_mode)
92
  reply = await self.ai.generate_reply(
93
  messages=self._ai_messages(context, user_mode=user_mode),
94
  tools=get_tool_schemas(user_mode),
@@ -101,14 +101,14 @@ class ConversationService:
101
  message=reply,
102
  metadata={"provider_flow": "groq_primary_hf_fallback", "user_mode": user_mode},
103
  )
104
- # await self.whatsapp.send_text(inbound.from_phone, reply)
105
  return reply
106
 
107
  def _tool_registry(
108
  self,
109
  customer: dict[str, Any],
110
  *,
111
- sender_phone: str,
112
  user_mode: UserMode,
113
  ) -> ToolRegistry:
114
  handlers = FalsaToolHandlers(
@@ -116,7 +116,7 @@ class ConversationService:
116
  embeddings=self.embeddings,
117
  whatsapp=self.whatsapp,
118
  customer=customer,
119
- sender_phone=sender_phone,
120
  embedding_model=self.settings.jina_embedding_model,
121
  )
122
  registry = ToolRegistry()
 
70
  return None
71
 
72
  customer = await self.repository.upsert_customer(
73
+ remote_jid=inbound.remoteJid,
74
  name=inbound.profile_name,
75
  )
76
  current_message = await self.repository.create_message(
 
88
  )
89
 
90
  user_mode = _resolve_user_mode(customer)
91
+ registry = self._tool_registry(customer, remoteJid=inbound.remoteJid, user_mode=user_mode)
92
  reply = await self.ai.generate_reply(
93
  messages=self._ai_messages(context, user_mode=user_mode),
94
  tools=get_tool_schemas(user_mode),
 
101
  message=reply,
102
  metadata={"provider_flow": "groq_primary_hf_fallback", "user_mode": user_mode},
103
  )
104
+ await self.whatsapp.send_text(inbound.remoteJid, reply)
105
  return reply
106
 
107
  def _tool_registry(
108
  self,
109
  customer: dict[str, Any],
110
  *,
111
+ remoteJid: str,
112
  user_mode: UserMode,
113
  ) -> ToolRegistry:
114
  handlers = FalsaToolHandlers(
 
116
  embeddings=self.embeddings,
117
  whatsapp=self.whatsapp,
118
  customer=customer,
119
+ remoteJid=remoteJid,
120
  embedding_model=self.settings.jina_embedding_model,
121
  )
122
  registry = ToolRegistry()
app/tools/handlers.py CHANGED
@@ -25,14 +25,14 @@ class FalsaToolHandlers:
25
  embeddings: JinaEmbeddingService,
26
  whatsapp: WhatsAppClient,
27
  customer: dict[str, Any],
28
- sender_phone: str,
29
  embedding_model: str,
30
  ) -> None:
31
  self.repository = repository
32
  self.embeddings = embeddings
33
  self.whatsapp = whatsapp
34
  self.customer = customer
35
- self.sender_phone = sender_phone
36
  self.embedding_model = embedding_model
37
 
38
  async def about_falsa(self, arguments: dict[str, Any]) -> ToolResult:
@@ -188,11 +188,13 @@ class FalsaToolHandlers:
188
  notification_status = "sent"
189
  notification_error = None
190
  try:
191
- driver_phone = (_first_or_dict(trip.get("drivers")) or {}).get("phone_number")
192
- if not driver_phone:
193
- raise WhatsAppClientError("Driver phone number is missing")
 
 
194
  await self.whatsapp.send_text(
195
- driver_phone,
196
  _driver_notification_text(
197
  customer=self.customer,
198
  trip=trip,
@@ -226,8 +228,8 @@ class FalsaToolHandlers:
226
  if not name:
227
  return ToolResult(ok=False, data={}, error="name is required")
228
 
229
- phone = self.sender_phone
230
- existing = await self.repository.get_driver_by_phone(phone)
231
  if existing:
232
  return ToolResult(
233
  ok=False,
@@ -235,19 +237,19 @@ class FalsaToolHandlers:
235
  error="Driver account already exists for this WhatsApp number",
236
  )
237
 
238
- driver = await self.repository.create_driver(name=name, phone_number=phone)
239
  return ToolResult(
240
  ok=True,
241
  data={
242
  "driver_id": driver["id"],
243
- "name": driver.get("name"),
244
- "phone_number": driver.get("phone_number"),
245
  "message": "Driver account created successfully.",
246
  },
247
  )
248
 
249
  async def check_driver_info(self, arguments: dict[str, Any]) -> ToolResult:
250
- driver = await self.repository.get_driver_by_phone(self.sender_phone)
251
  if not driver:
252
  return ToolResult(
253
  ok=False,
@@ -258,6 +260,7 @@ class FalsaToolHandlers:
258
  ),
259
  )
260
 
 
261
  cars = await self.repository.list_driver_cars(str(driver["id"]))
262
  upcoming_trips = await self.repository.list_driver_trips(str(driver["id"]))
263
 
@@ -265,8 +268,8 @@ class FalsaToolHandlers:
265
  ok=True,
266
  data={
267
  "driver_id": driver["id"],
268
- "name": driver.get("name"),
269
- "phone_number": driver.get("phone_number"),
270
  "status": driver.get("status"),
271
  "vehicle_count": len(cars),
272
  "active_trip_count": len(upcoming_trips),
@@ -286,7 +289,7 @@ class FalsaToolHandlers:
286
  )
287
 
288
  async def check_driver_trips(self, arguments: dict[str, Any]) -> ToolResult:
289
- driver = await self.repository.get_driver_by_phone(self.sender_phone)
290
  if not driver:
291
  return ToolResult(
292
  ok=False,
@@ -313,7 +316,7 @@ class FalsaToolHandlers:
313
  )
314
 
315
  async def add_driver_car(self, arguments: dict[str, Any]) -> ToolResult:
316
- driver = await self.repository.get_driver_by_phone(self.sender_phone)
317
  if not driver:
318
  return ToolResult(
319
  ok=False,
@@ -352,7 +355,7 @@ class FalsaToolHandlers:
352
  )
353
 
354
  async def add_trip_by_driver(self, arguments: dict[str, Any]) -> ToolResult:
355
- driver = await self.repository.get_driver_by_phone(self.sender_phone)
356
  if not driver:
357
  return ToolResult(
358
  ok=False,
@@ -503,7 +506,7 @@ class FalsaToolHandlers:
503
  )
504
 
505
  async def switch_to_driver(self, arguments: dict[str, Any]) -> ToolResult:
506
- driver = await self.repository.get_driver_by_phone(self.sender_phone)
507
  if not driver:
508
  return ToolResult(
509
  ok=False,
@@ -732,8 +735,8 @@ def _driver_notification_text(
732
  ) -> str:
733
  return (
734
  "New FALSA booking lead\n"
735
- f"Customer: {customer.get('name') or customer.get('phone_number')}\n"
736
- f"Phone: {customer.get('phone_number')}\n"
737
  f"Trip: {trip.get('departure')} -> {trip.get('destination')}\n"
738
  f"Departure: {trip_departure_date(trip)} {trip_departure_bucket(trip)}\n"
739
  f"Seats requested: {requested_seats}\n"
 
25
  embeddings: JinaEmbeddingService,
26
  whatsapp: WhatsAppClient,
27
  customer: dict[str, Any],
28
+ remoteJid: str,
29
  embedding_model: str,
30
  ) -> None:
31
  self.repository = repository
32
  self.embeddings = embeddings
33
  self.whatsapp = whatsapp
34
  self.customer = customer
35
+ self.remoteJid = remoteJid
36
  self.embedding_model = embedding_model
37
 
38
  async def about_falsa(self, arguments: dict[str, Any]) -> ToolResult:
 
188
  notification_status = "sent"
189
  notification_error = None
190
  try:
191
+ driver_record = _first_or_dict(trip.get("drivers")) or {}
192
+ driver_customer = driver_record.get("customers") or {}
193
+ driver_remote_jid = driver_customer.get("remoteJid") or driver_record.get("remoteJid")
194
+ if not driver_remote_jid:
195
+ raise WhatsAppClientError("Driver remoteJid is missing")
196
  await self.whatsapp.send_text(
197
+ driver_remote_jid,
198
  _driver_notification_text(
199
  customer=self.customer,
200
  trip=trip,
 
228
  if not name:
229
  return ToolResult(ok=False, data={}, error="name is required")
230
 
231
+ remote_jid = self.remoteJid
232
+ existing = await self.repository.get_driver_by_remoteJid(remote_jid)
233
  if existing:
234
  return ToolResult(
235
  ok=False,
 
237
  error="Driver account already exists for this WhatsApp number",
238
  )
239
 
240
+ driver = await self.repository.create_driver(customer_id=str(self.customer["id"]))
241
  return ToolResult(
242
  ok=True,
243
  data={
244
  "driver_id": driver["id"],
245
+ "name": self.customer.get("name"),
246
+ "remoteJid": self.customer.get("remoteJid"),
247
  "message": "Driver account created successfully.",
248
  },
249
  )
250
 
251
  async def check_driver_info(self, arguments: dict[str, Any]) -> ToolResult:
252
+ driver = await self.repository.get_driver_by_remoteJid(self.remoteJid)
253
  if not driver:
254
  return ToolResult(
255
  ok=False,
 
260
  ),
261
  )
262
 
263
+ customer_info = driver.get("customers", {})
264
  cars = await self.repository.list_driver_cars(str(driver["id"]))
265
  upcoming_trips = await self.repository.list_driver_trips(str(driver["id"]))
266
 
 
268
  ok=True,
269
  data={
270
  "driver_id": driver["id"],
271
+ "name": customer_info.get("name") or driver.get("name"),
272
+ "remoteJid": customer_info.get("remoteJid") or driver.get("remoteJid"),
273
  "status": driver.get("status"),
274
  "vehicle_count": len(cars),
275
  "active_trip_count": len(upcoming_trips),
 
289
  )
290
 
291
  async def check_driver_trips(self, arguments: dict[str, Any]) -> ToolResult:
292
+ driver = await self.repository.get_driver_by_remoteJid(self.remoteJid)
293
  if not driver:
294
  return ToolResult(
295
  ok=False,
 
316
  )
317
 
318
  async def add_driver_car(self, arguments: dict[str, Any]) -> ToolResult:
319
+ driver = await self.repository.get_driver_by_remoteJid(self.remoteJid)
320
  if not driver:
321
  return ToolResult(
322
  ok=False,
 
355
  )
356
 
357
  async def add_trip_by_driver(self, arguments: dict[str, Any]) -> ToolResult:
358
+ driver = await self.repository.get_driver_by_remoteJid(self.remoteJid)
359
  if not driver:
360
  return ToolResult(
361
  ok=False,
 
506
  )
507
 
508
  async def switch_to_driver(self, arguments: dict[str, Any]) -> ToolResult:
509
+ driver = await self.repository.get_driver_by_remoteJid(self.remoteJid)
510
  if not driver:
511
  return ToolResult(
512
  ok=False,
 
735
  ) -> str:
736
  return (
737
  "New FALSA booking lead\n"
738
+ f"Customer: {customer.get('name') or customer.get('remoteJid')}\n"
739
+ f"Remote JID: {customer.get('remoteJid')}\n"
740
  f"Trip: {trip.get('departure')} -> {trip.get('destination')}\n"
741
  f"Departure: {trip_departure_date(trip)} {trip_departure_bucket(trip)}\n"
742
  f"Seats requested: {requested_seats}\n"
app/whatsapp/parser.py CHANGED
@@ -22,19 +22,19 @@ def parse_inbound_messages(payload: dict[str, Any]) -> list[WhatsAppInboundMessa
22
  if message_type not in SUPPORTED_MESSAGE_TYPES:
23
  continue
24
 
25
- from_phone = message.get("from")
26
  message_id = message.get("id")
27
  text = message.get("text", {}).get("body")
28
- if not from_phone or not message_id or text is None:
29
  continue
30
 
31
  messages.append(
32
  WhatsAppInboundMessage(
33
  message_id=message_id,
34
- from_phone=from_phone,
35
  text=text,
36
  timestamp=message.get("timestamp"),
37
- profile_name=contacts_by_wa_id.get(from_phone),
38
  phone_number_id=phone_number_id,
39
  raw=message,
40
  )
 
22
  if message_type not in SUPPORTED_MESSAGE_TYPES:
23
  continue
24
 
25
+ remoteJid = message.get("from")
26
  message_id = message.get("id")
27
  text = message.get("text", {}).get("body")
28
+ if not remoteJid or not message_id or text is None:
29
  continue
30
 
31
  messages.append(
32
  WhatsAppInboundMessage(
33
  message_id=message_id,
34
+ remoteJid=remoteJid,
35
  text=text,
36
  timestamp=message.get("timestamp"),
37
+ profile_name=contacts_by_wa_id.get(remoteJid),
38
  phone_number_id=phone_number_id,
39
  raw=message,
40
  )
supabase/migrations/202605210001_initial_schema.sql CHANGED
@@ -13,7 +13,7 @@ $$;
13
  create table if not exists public.customers (
14
  id uuid primary key default gen_random_uuid(),
15
  name text,
16
- phone_number text not null unique,
17
  preferred_language text check (preferred_language in ('ar', 'en')),
18
  created_at timestamptz not null default now(),
19
  updated_at timestamptz not null default now()
@@ -21,8 +21,7 @@ create table if not exists public.customers (
21
 
22
  create table if not exists public.drivers (
23
  id uuid primary key default gen_random_uuid(),
24
- name text not null,
25
- phone_number text not null unique,
26
  status text not null default 'active' check (status in ('active', 'inactive', 'suspended')),
27
  rating numeric(3,2),
28
  created_at timestamptz not null default now(),
 
13
  create table if not exists public.customers (
14
  id uuid primary key default gen_random_uuid(),
15
  name text,
16
+ "remoteJid" text not null unique,
17
  preferred_language text check (preferred_language in ('ar', 'en')),
18
  created_at timestamptz not null default now(),
19
  updated_at timestamptz not null default now()
 
21
 
22
  create table if not exists public.drivers (
23
  id uuid primary key default gen_random_uuid(),
24
+ customer_id uuid not null unique references public.customers(id) on delete cascade,
 
25
  status text not null default 'active' check (status in ('active', 'inactive', 'suspended')),
26
  rating numeric(3,2),
27
  created_at timestamptz not null default now(),
supabase/migrations/202606020001_supabase_jina_vectors.sql CHANGED
@@ -117,8 +117,8 @@ as $$
117
  driver_trips.total_seats,
118
  driver_trips.price,
119
  driver_trips.status,
120
- drivers.name as driver_name,
121
- drivers.phone_number as driver_phone_number,
122
  driver_cars.car_type,
123
  driver_trip_embeddings.chunk_text,
124
  1 - (driver_trip_embeddings.embedding <=> query_embedding) as similarity,
@@ -137,6 +137,7 @@ as $$
137
  from public.driver_trip_embeddings
138
  join public.driver_trips on driver_trips.id = driver_trip_embeddings.trip_id
139
  left join public.drivers on drivers.id = driver_trips.driver_id
 
140
  left join public.driver_cars on driver_cars.id = driver_trips.car_id
141
  where driver_trips.status = 'active'
142
  and driver_trips.available_seats >= coalesce(filter_seats, 1)
 
117
  driver_trips.total_seats,
118
  driver_trips.price,
119
  driver_trips.status,
120
+ customers.name as driver_name,
121
+ customers."remoteJid" as driver_phone_number,
122
  driver_cars.car_type,
123
  driver_trip_embeddings.chunk_text,
124
  1 - (driver_trip_embeddings.embedding <=> query_embedding) as similarity,
 
137
  from public.driver_trip_embeddings
138
  join public.driver_trips on driver_trips.id = driver_trip_embeddings.trip_id
139
  left join public.drivers on drivers.id = driver_trips.driver_id
140
+ left join public.customers on customers.id = drivers.customer_id
141
  left join public.driver_cars on driver_cars.id = driver_trips.car_id
142
  where driver_trips.status = 'active'
143
  and driver_trips.available_seats >= coalesce(filter_seats, 1)
supabase/migrations/202606170001_driver_customer_identity.sql ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Migration: driver records now reference customers as primary identity
2
+ -- 1) Add customer_id foreign key to drivers
3
+ -- 2) Populate customer_id from existing customer records when possible
4
+ -- 3) Enforce uniqueness and non-nullability
5
+ -- 4) Drop redundant driver identity columns
6
+
7
+ ALTER TABLE public.drivers
8
+ ADD COLUMN customer_id uuid;
9
+
10
+ ALTER TABLE public.customers
11
+ RENAME COLUMN phone_number TO "remoteJid";
12
+
13
+ UPDATE public.drivers
14
+ SET customer_id = public.customers.id
15
+ FROM public.customers
16
+ WHERE public.drivers.phone_number = public.customers."remoteJid";
17
+
18
+ ALTER TABLE public.drivers
19
+ ADD CONSTRAINT drivers_customer_id_key UNIQUE (customer_id);
20
+
21
+ ALTER TABLE public.drivers
22
+ ALTER COLUMN customer_id SET NOT NULL;
23
+
24
+ ALTER TABLE public.drivers
25
+ ADD CONSTRAINT drivers_customer_id_fkey
26
+ FOREIGN KEY (customer_id)
27
+ REFERENCES public.customers(id)
28
+ ON DELETE CASCADE;
29
+
30
+ ALTER TABLE public.drivers
31
+ DROP COLUMN name,
32
+ DROP COLUMN phone_number;
tests/conftest.py CHANGED
@@ -98,8 +98,8 @@ def test_app(settings: Settings) -> Any:
98
 
99
  class FakeRepository:
100
  def __init__(self) -> None:
101
- self.customers_by_phone: dict[str, dict[str, Any]] = {}
102
- self.drivers_by_phone: dict[str, dict[str, Any]] = {}
103
  self.driver_cars_by_driver: dict[str, list[dict[str, Any]]] = {}
104
  self.latest_trips_by_driver: dict[str, dict[str, Any]] = {}
105
  self.created_drivers: list[dict[str, Any]] = []
@@ -117,20 +117,20 @@ class FakeRepository:
117
  async def upsert_customer(
118
  self,
119
  *,
120
- phone_number: str,
121
  name: str | None = None,
122
  preferred_language: str | None = None,
123
  ) -> dict[str, Any]:
124
- customer = self.customers_by_phone.get(phone_number)
125
  if customer is None:
126
  customer = {
127
- "id": f"cust-{len(self.customers_by_phone) + 1}",
128
- "phone_number": phone_number,
129
  "name": name,
130
  "preferred_language": preferred_language,
131
  "user_mode": None,
132
  }
133
- self.customers_by_phone[phone_number] = customer
134
  elif name:
135
  customer["name"] = name
136
  return customer
@@ -141,7 +141,7 @@ class FakeRepository:
141
  customer_id: str,
142
  user_mode: str,
143
  ) -> dict[str, Any]:
144
- for customer in self.customers_by_phone.values():
145
  if customer["id"] == customer_id:
146
  customer["user_mode"] = user_mode
147
  return customer
@@ -153,7 +153,7 @@ class FakeRepository:
153
  customer_id: str,
154
  name: str,
155
  ) -> dict[str, Any]:
156
- for customer in self.customers_by_phone.values():
157
  if customer["id"] == customer_id:
158
  customer["name"] = name
159
  return customer
@@ -253,17 +253,23 @@ class FakeRepository:
253
  self.notification_updates.append(update)
254
  return update
255
 
256
- async def get_driver_by_phone(self, phone_number: str) -> dict[str, Any] | None:
257
- return self.drivers_by_phone.get(phone_number)
258
 
259
- async def create_driver(self, *, name: str, phone_number: str) -> dict[str, Any]:
 
 
 
 
 
 
260
  driver = {
261
- "id": f"driver-{len(self.drivers_by_phone) + 1}",
262
- "name": name,
263
- "phone_number": phone_number,
264
  "status": "active",
 
265
  }
266
- self.drivers_by_phone[phone_number] = driver
267
  self.created_drivers.append(driver)
268
  return driver
269
 
@@ -312,7 +318,7 @@ class FakeRepository:
312
  car_id = kwargs.get("car_id")
313
  cars = self.driver_cars_by_driver.get(driver_id, [])
314
  driver = next(
315
- (row for row in self.drivers_by_phone.values() if row["id"] == driver_id),
316
  {"name": "Driver"},
317
  )
318
  matched_car = next(
 
98
 
99
  class FakeRepository:
100
  def __init__(self) -> None:
101
+ self.customers_by_remote_jid: dict[str, dict[str, Any]] = {}
102
+ self.drivers_by_remote_jid: dict[str, dict[str, Any]] = {}
103
  self.driver_cars_by_driver: dict[str, list[dict[str, Any]]] = {}
104
  self.latest_trips_by_driver: dict[str, dict[str, Any]] = {}
105
  self.created_drivers: list[dict[str, Any]] = []
 
117
  async def upsert_customer(
118
  self,
119
  *,
120
+ remote_jid: str,
121
  name: str | None = None,
122
  preferred_language: str | None = None,
123
  ) -> dict[str, Any]:
124
+ customer = self.customers_by_remote_jid.get(remote_jid)
125
  if customer is None:
126
  customer = {
127
+ "id": f"cust-{len(self.customers_by_remote_jid) + 1}",
128
+ "remoteJid": remote_jid,
129
  "name": name,
130
  "preferred_language": preferred_language,
131
  "user_mode": None,
132
  }
133
+ self.customers_by_remote_jid[remote_jid] = customer
134
  elif name:
135
  customer["name"] = name
136
  return customer
 
141
  customer_id: str,
142
  user_mode: str,
143
  ) -> dict[str, Any]:
144
+ for customer in self.customers_by_remote_jid.values():
145
  if customer["id"] == customer_id:
146
  customer["user_mode"] = user_mode
147
  return customer
 
153
  customer_id: str,
154
  name: str,
155
  ) -> dict[str, Any]:
156
+ for customer in self.customers_by_remote_jid.values():
157
  if customer["id"] == customer_id:
158
  customer["name"] = name
159
  return customer
 
253
  self.notification_updates.append(update)
254
  return update
255
 
256
+ async def get_driver_by_phone(self, remote_jid: str) -> dict[str, Any] | None:
257
+ return self.drivers_by_remote_jid.get(remote_jid)
258
 
259
+ async def create_driver(self, *, customer_id: str) -> dict[str, Any]:
260
+ customer = next(
261
+ (customer for customer in self.customers_by_remote_jid.values() if customer["id"] == customer_id),
262
+ None,
263
+ )
264
+ if customer is None:
265
+ raise KeyError(customer_id)
266
  driver = {
267
+ "id": f"driver-{len(self.drivers_by_remote_jid) + 1}",
268
+ "customer_id": customer_id,
 
269
  "status": "active",
270
+ "customers": customer,
271
  }
272
+ self.drivers_by_remote_jid[customer["remoteJid"]] = driver
273
  self.created_drivers.append(driver)
274
  return driver
275
 
 
318
  car_id = kwargs.get("car_id")
319
  cars = self.driver_cars_by_driver.get(driver_id, [])
320
  driver = next(
321
+ (row for row in self.drivers_by_remote_jid.values() if row["id"] == driver_id),
322
  {"name": "Driver"},
323
  )
324
  matched_car = next(
tests/test_conversation_service.py CHANGED
@@ -8,7 +8,7 @@ from tests.conftest import FakeAI, FakeEmbeddings, FakeRepository, FakeWhatsApp
8
  @pytest.mark.asyncio
9
  async def test_conversation_stores_messages_uses_last_four_context_and_sends_reply(settings):
10
  repository = FakeRepository()
11
- customer = await repository.upsert_customer(phone_number="967700000001", name="Customer")
12
  for index in range(6):
13
  await repository.create_message(
14
  customer_id=customer["id"],
@@ -60,7 +60,7 @@ async def test_conversation_stores_messages_uses_last_four_context_and_sends_rep
60
  @pytest.mark.asyncio
61
  async def test_conversation_skips_duplicate_whatsapp_message(settings):
62
  repository = FakeRepository()
63
- customer = await repository.upsert_customer(phone_number="967700000001")
64
  await repository.create_message(
65
  customer_id=customer["id"],
66
  sender_type="customer",
@@ -93,7 +93,7 @@ async def test_conversation_skips_duplicate_whatsapp_message(settings):
93
  @pytest.mark.asyncio
94
  async def test_conversation_uses_passenger_tools_when_user_mode_is_passenger(settings):
95
  repository = FakeRepository()
96
- customer = await repository.upsert_customer(phone_number="967700000001", name="Customer")
97
  customer["user_mode"] = "passenger"
98
  ai = FakeAI(reply="Passenger reply")
99
  service = ConversationService(
 
8
  @pytest.mark.asyncio
9
  async def test_conversation_stores_messages_uses_last_four_context_and_sends_reply(settings):
10
  repository = FakeRepository()
11
+ customer = await repository.upsert_customer(remote_jid="967700000001", name="Customer")
12
  for index in range(6):
13
  await repository.create_message(
14
  customer_id=customer["id"],
 
60
  @pytest.mark.asyncio
61
  async def test_conversation_skips_duplicate_whatsapp_message(settings):
62
  repository = FakeRepository()
63
+ customer = await repository.upsert_customer(remote_jid="967700000001")
64
  await repository.create_message(
65
  customer_id=customer["id"],
66
  sender_type="customer",
 
93
  @pytest.mark.asyncio
94
  async def test_conversation_uses_passenger_tools_when_user_mode_is_passenger(settings):
95
  repository = FakeRepository()
96
+ customer = await repository.upsert_customer(remote_jid="967700000001", name="Customer")
97
  customer["user_mode"] = "passenger"
98
  ai = FakeAI(reply="Passenger reply")
99
  service = ConversationService(
tests/test_tools.py CHANGED
@@ -16,7 +16,7 @@ def make_handlers(
16
  repository=repository or FakeRepository(),
17
  embeddings=embeddings or FakeEmbeddings(),
18
  whatsapp=whatsapp or FakeWhatsApp(),
19
- customer=customer or {"id": "cust-1", "phone_number": sender_phone},
20
  sender_phone=sender_phone,
21
  embedding_model="jina-embeddings-v5-text-small",
22
  )
@@ -43,7 +43,7 @@ def trip(
43
  "total_seats": 4,
44
  "price": "50.00",
45
  "status": status,
46
- "drivers": {"name": "Ali", "phone_number": "967700000009"},
47
  "driver_cars": {"car_type": car_type},
48
  }
49
 
@@ -162,7 +162,7 @@ async def test_create_booking_lead_notifies_driver():
162
  handlers = make_handlers(
163
  repository=repository,
164
  whatsapp=whatsapp,
165
- customer={"id": "cust-1", "phone_number": "967700000001", "name": "Mona"},
166
  )
167
 
168
  result = await handlers.create_booking_lead(
@@ -210,22 +210,20 @@ async def test_create_driver_account_uses_sender_phone():
210
  repository = FakeRepository()
211
  handlers = make_handlers(repository=repository, sender_phone="967700000010")
212
 
213
- result = await handlers.create_driver_account(
214
- {"name": "Ali Driver", "phone_number": "967700000099"}
215
- )
216
 
217
  assert result.ok is True
218
- assert repository.created_drivers[0]["phone_number"] == "967700000010"
219
- assert repository.drivers_by_phone["967700000010"]["name"] == "Ali Driver"
220
 
221
 
222
  @pytest.mark.asyncio
223
  async def test_create_driver_account_rejects_duplicate():
224
  repository = FakeRepository()
225
- repository.drivers_by_phone["967700000010"] = {
226
  "id": "driver-1",
227
  "name": "Ali",
228
- "phone_number": "967700000010",
229
  }
230
  handlers = make_handlers(repository=repository, sender_phone="967700000010")
231
 
@@ -257,10 +255,10 @@ async def test_add_trip_by_driver_requires_account():
257
  @pytest.mark.asyncio
258
  async def test_add_driver_car_accepts_name_only():
259
  repository = FakeRepository()
260
- repository.drivers_by_phone["967700000010"] = {
261
  "id": "driver-1",
262
  "name": "Ali",
263
- "phone_number": "967700000010",
264
  }
265
  handlers = make_handlers(repository=repository, sender_phone="967700000010")
266
 
@@ -276,10 +274,10 @@ async def test_add_driver_car_accepts_name_only():
276
  @pytest.mark.asyncio
277
  async def test_check_driver_info_returns_account_summary():
278
  repository = FakeRepository()
279
- repository.drivers_by_phone["967700000010"] = {
280
  "id": "driver-1",
281
  "name": "Ali",
282
- "phone_number": "967700000010",
283
  "status": "active",
284
  }
285
  repository.driver_cars_by_driver["driver-1"] = [
@@ -314,10 +312,10 @@ async def test_check_driver_info_returns_account_summary():
314
  @pytest.mark.asyncio
315
  async def test_check_driver_trips_returns_upcoming_trips():
316
  repository = FakeRepository()
317
- repository.drivers_by_phone["967700000010"] = {
318
  "id": "driver-1",
319
  "name": "Ali",
320
- "phone_number": "967700000010",
321
  }
322
  repository.trips_by_id["trip-1"] = {
323
  "id": "trip-1",
@@ -367,10 +365,10 @@ async def test_check_driver_info_requests_driver_registration_if_unregistered():
367
  @pytest.mark.asyncio
368
  async def test_add_trip_by_driver_uses_latest_trip_defaults():
369
  repository = FakeRepository()
370
- repository.drivers_by_phone["967700000010"] = {
371
  "id": "driver-1",
372
  "name": "Ali",
373
- "phone_number": "967700000010",
374
  }
375
  repository.driver_cars_by_driver["driver-1"] = [
376
  {"id": "car-1", "car_type": "SUV", "seat_count": 4},
@@ -409,10 +407,10 @@ async def test_add_trip_by_driver_uses_latest_trip_defaults():
409
  @pytest.mark.asyncio
410
  async def test_add_trip_by_driver_resolves_vehicle_type_by_name():
411
  repository = FakeRepository()
412
- repository.drivers_by_phone["967700000010"] = {
413
  "id": "driver-1",
414
  "name": "Ali",
415
- "phone_number": "967700000010",
416
  }
417
  repository.driver_cars_by_driver["driver-1"] = [
418
  {"id": "car-1", "car_type": "باص", "plate_number": "1234", "seat_count": 14},
@@ -440,10 +438,10 @@ async def test_add_trip_by_driver_resolves_vehicle_type_by_name():
440
  @pytest.mark.asyncio
441
  async def test_add_trip_by_driver_indexes_new_trip():
442
  repository = FakeRepository()
443
- repository.drivers_by_phone["967700000010"] = {
444
  "id": "driver-1",
445
  "name": "Ali",
446
- "phone_number": "967700000010",
447
  }
448
  repository.driver_cars_by_driver["driver-1"] = [
449
  {"id": "car-1", "car_type": "SUV", "seat_count": 4},
@@ -471,7 +469,7 @@ async def test_add_trip_by_driver_indexes_new_trip():
471
  @pytest.mark.asyncio
472
  async def test_switch_to_driver_requires_existing_driver_account():
473
  repository = FakeRepository()
474
- customer = {"id": "cust-1", "phone_number": "967700000001"}
475
  handlers = make_handlers(repository=repository, customer=customer)
476
 
477
  result = await handlers.switch_to_driver({})
@@ -483,11 +481,11 @@ async def test_switch_to_driver_requires_existing_driver_account():
483
  @pytest.mark.asyncio
484
  async def test_switch_to_driver_updates_customer_mode():
485
  repository = FakeRepository()
486
- customer = await repository.upsert_customer(phone_number="967700000001")
487
- repository.drivers_by_phone["967700000001"] = {
488
  "id": "driver-1",
489
  "name": "Ali",
490
- "phone_number": "967700000001",
491
  }
492
  handlers = make_handlers(repository=repository, customer=customer)
493
 
@@ -501,7 +499,7 @@ async def test_switch_to_driver_updates_customer_mode():
501
  @pytest.mark.asyncio
502
  async def test_switch_to_passenger_updates_mode_and_optional_name():
503
  repository = FakeRepository()
504
- customer = await repository.upsert_customer(phone_number="967700000001")
505
  handlers = make_handlers(repository=repository, customer=customer)
506
 
507
  result = await handlers.switch_to_passenger({"name": "Sara"})
@@ -515,7 +513,7 @@ async def test_switch_to_passenger_updates_mode_and_optional_name():
515
  @pytest.mark.asyncio
516
  async def test_switch_to_passenger_without_name():
517
  repository = FakeRepository()
518
- customer = await repository.upsert_customer(phone_number="967700000001", name="Existing")
519
  handlers = make_handlers(repository=repository, customer=customer)
520
 
521
  result = await handlers.switch_to_passenger({})
 
16
  repository=repository or FakeRepository(),
17
  embeddings=embeddings or FakeEmbeddings(),
18
  whatsapp=whatsapp or FakeWhatsApp(),
19
+ customer=customer or {"id": "cust-1", "remoteJid": sender_phone},
20
  sender_phone=sender_phone,
21
  embedding_model="jina-embeddings-v5-text-small",
22
  )
 
43
  "total_seats": 4,
44
  "price": "50.00",
45
  "status": status,
46
+ "drivers": {"name": "Ali", "remoteJid": "967700000009"},
47
  "driver_cars": {"car_type": car_type},
48
  }
49
 
 
162
  handlers = make_handlers(
163
  repository=repository,
164
  whatsapp=whatsapp,
165
+ customer={"id": "cust-1", "remoteJid": "967700000001", "name": "Mona"},
166
  )
167
 
168
  result = await handlers.create_booking_lead(
 
210
  repository = FakeRepository()
211
  handlers = make_handlers(repository=repository, sender_phone="967700000010")
212
 
213
+ result = await handlers.create_driver_account({"name": "Ali Driver"})
 
 
214
 
215
  assert result.ok is True
216
+ assert repository.created_drivers[0]["customers"]["remoteJid"] == "967700000010"
217
+ assert repository.drivers_by_remote_jid["967700000010"]["name"] == "Ali Driver"
218
 
219
 
220
  @pytest.mark.asyncio
221
  async def test_create_driver_account_rejects_duplicate():
222
  repository = FakeRepository()
223
+ repository.drivers_by_remote_jid["967700000010"] = {
224
  "id": "driver-1",
225
  "name": "Ali",
226
+ "remoteJid": "967700000010",
227
  }
228
  handlers = make_handlers(repository=repository, sender_phone="967700000010")
229
 
 
255
  @pytest.mark.asyncio
256
  async def test_add_driver_car_accepts_name_only():
257
  repository = FakeRepository()
258
+ repository.drivers_by_remote_jid["967700000010"] = {
259
  "id": "driver-1",
260
  "name": "Ali",
261
+ "remoteJid": "967700000010",
262
  }
263
  handlers = make_handlers(repository=repository, sender_phone="967700000010")
264
 
 
274
  @pytest.mark.asyncio
275
  async def test_check_driver_info_returns_account_summary():
276
  repository = FakeRepository()
277
+ repository.drivers_by_remote_jid["967700000010"] = {
278
  "id": "driver-1",
279
  "name": "Ali",
280
+ "remoteJid": "967700000010",
281
  "status": "active",
282
  }
283
  repository.driver_cars_by_driver["driver-1"] = [
 
312
  @pytest.mark.asyncio
313
  async def test_check_driver_trips_returns_upcoming_trips():
314
  repository = FakeRepository()
315
+ repository.drivers_by_remote_jid["967700000010"] = {
316
  "id": "driver-1",
317
  "name": "Ali",
318
+ "remoteJid": "967700000010",
319
  }
320
  repository.trips_by_id["trip-1"] = {
321
  "id": "trip-1",
 
365
  @pytest.mark.asyncio
366
  async def test_add_trip_by_driver_uses_latest_trip_defaults():
367
  repository = FakeRepository()
368
+ repository.drivers_by_remote_jid["967700000010"] = {
369
  "id": "driver-1",
370
  "name": "Ali",
371
+ "remoteJid": "967700000010",
372
  }
373
  repository.driver_cars_by_driver["driver-1"] = [
374
  {"id": "car-1", "car_type": "SUV", "seat_count": 4},
 
407
  @pytest.mark.asyncio
408
  async def test_add_trip_by_driver_resolves_vehicle_type_by_name():
409
  repository = FakeRepository()
410
+ repository.drivers_by_remote_jid["967700000010"] = {
411
  "id": "driver-1",
412
  "name": "Ali",
413
+ "remoteJid": "967700000010",
414
  }
415
  repository.driver_cars_by_driver["driver-1"] = [
416
  {"id": "car-1", "car_type": "باص", "plate_number": "1234", "seat_count": 14},
 
438
  @pytest.mark.asyncio
439
  async def test_add_trip_by_driver_indexes_new_trip():
440
  repository = FakeRepository()
441
+ repository.drivers_by_remote_jid["967700000010"] = {
442
  "id": "driver-1",
443
  "name": "Ali",
444
+ "remoteJid": "967700000010",
445
  }
446
  repository.driver_cars_by_driver["driver-1"] = [
447
  {"id": "car-1", "car_type": "SUV", "seat_count": 4},
 
469
  @pytest.mark.asyncio
470
  async def test_switch_to_driver_requires_existing_driver_account():
471
  repository = FakeRepository()
472
+ customer = {"id": "cust-1", "remoteJid": "967700000001"}
473
  handlers = make_handlers(repository=repository, customer=customer)
474
 
475
  result = await handlers.switch_to_driver({})
 
481
  @pytest.mark.asyncio
482
  async def test_switch_to_driver_updates_customer_mode():
483
  repository = FakeRepository()
484
+ customer = await repository.upsert_customer(remote_jid="967700000001")
485
+ repository.drivers_by_remote_jid["967700000001"] = {
486
  "id": "driver-1",
487
  "name": "Ali",
488
+ "remoteJid": "967700000001",
489
  }
490
  handlers = make_handlers(repository=repository, customer=customer)
491
 
 
499
  @pytest.mark.asyncio
500
  async def test_switch_to_passenger_updates_mode_and_optional_name():
501
  repository = FakeRepository()
502
+ customer = await repository.upsert_customer(remote_jid="967700000001")
503
  handlers = make_handlers(repository=repository, customer=customer)
504
 
505
  result = await handlers.switch_to_passenger({"name": "Sara"})
 
513
  @pytest.mark.asyncio
514
  async def test_switch_to_passenger_without_name():
515
  repository = FakeRepository()
516
+ customer = await repository.upsert_customer(remote_jid="967700000001", name="Existing")
517
  handlers = make_handlers(repository=repository, customer=customer)
518
 
519
  result = await handlers.switch_to_passenger({})