codeBOKER commited on
Commit
fcf2812
·
1 Parent(s): edb038d

replace numbered trip list with trip cards for driver delete/modify/list

Browse files

- initiate_trip_action and check_driver_trips now send format_trip_card
per trip (like passenger search) instead of a numbered text list
- Driver replies to a trip card to delete (bypass LLM) or modify
(set session, route to LLM for update_trip_field)
- Remove delete_trip_by_number / modify_trip_by_number handlers
and their tool schemas (replaced by card reply flow)
- Remove dead interactive list code (_handle_trip_interactive_reply)
- Clean up trip_selection.py: remove unused builders/parsers
- Update list_driver_trips query to include drivers(*)

app/ai/tool_schemas.py CHANGED
@@ -151,7 +151,7 @@ _CHECK_DRIVER_TRIPS = {
151
  "type": "function",
152
  "function": {
153
  "name": "check_driver_trips",
154
- "description": "List upcoming active trips (status=active, not departed).",
155
  "parameters": {
156
  "type": "object",
157
  "properties": {},
@@ -246,7 +246,7 @@ _INITIATE_TRIP_ACTION = {
246
  "type": "function",
247
  "function": {
248
  "name": "initiate_trip_action",
249
- "description": "Start delete/modify flow. Sends numbered trip list.",
250
  "parameters": {
251
  "type": "object",
252
  "properties": {
@@ -305,65 +305,6 @@ _UPDATE_TRIP_FIELD = {
305
  },
306
  }
307
 
308
- _DELETE_TRIP_BY_NUMBER = {
309
- "type": "function",
310
- "function": {
311
- "name": "delete_trip_by_number",
312
- "description": "Cancel a trip by driver-visible number (oldest=1).",
313
- "parameters": {
314
- "type": "object",
315
- "properties": {
316
- "trip_number": {
317
- "type": "integer",
318
- "minimum": 1,
319
- "description": "The one-based number of the trip from the driver trip list.",
320
- },
321
- },
322
- "required": ["trip_number"],
323
- "additionalProperties": False,
324
- },
325
- },
326
- }
327
-
328
- _MODIFY_TRIP_BY_NUMBER = {
329
- "type": "function",
330
- "function": {
331
- "name": "modify_trip_by_number",
332
- "description": "Modify a trip field by driver-visible number (oldest=1).",
333
- "parameters": {
334
- "type": "object",
335
- "properties": {
336
- "trip_number": {
337
- "type": "integer",
338
- "minimum": 1,
339
- "description": "The one-based number of the trip from the driver trip list.",
340
- },
341
- "field": {
342
- "type": "string",
343
- "enum": [
344
- "departure",
345
- "destination",
346
- "departure_date",
347
- "departure_time",
348
- "pickup_time",
349
- "vehicle_type",
350
- "available_seats",
351
- "total_seats",
352
- "price",
353
- ],
354
- "description": "The trip field to update.",
355
- },
356
- "value": {
357
- "type": "string",
358
- "description": "New value. Dates YYYY-MM-DD, time HH:MM, routes Arabic, seats/price digits.",
359
- },
360
- },
361
- "required": ["trip_number", "field", "value"],
362
- "additionalProperties": False,
363
- },
364
- },
365
- }
366
-
367
  _SWITCH_TO_DRIVER = {
368
  "type": "function",
369
  "function": {
@@ -408,8 +349,6 @@ _TOOL_SCHEMAS: dict[str, dict[str, Any]] = {
408
  "add_trip_by_driver": _ADD_TRIP_BY_DRIVER,
409
  "initiate_trip_action": _INITIATE_TRIP_ACTION,
410
  "update_trip_field": _UPDATE_TRIP_FIELD,
411
- "delete_trip_by_number": _DELETE_TRIP_BY_NUMBER,
412
- "modify_trip_by_number": _MODIFY_TRIP_BY_NUMBER,
413
  "switch_to_driver": _SWITCH_TO_DRIVER,
414
  "switch_to_passenger": _SWITCH_TO_PASSENGER,
415
  }
@@ -427,8 +366,6 @@ _TOOLS_BY_MODE: dict[UserMode, list[str]] = {
427
  "check_driver_trips",
428
  "add_driver_car",
429
  "add_trip_by_driver",
430
- "delete_trip_by_number",
431
- "modify_trip_by_number",
432
  "initiate_trip_action",
433
  "update_trip_field",
434
  "switch_to_passenger",
 
151
  "type": "function",
152
  "function": {
153
  "name": "check_driver_trips",
154
+ "description": "List upcoming active trips. Sends trip cards directly.",
155
  "parameters": {
156
  "type": "object",
157
  "properties": {},
 
246
  "type": "function",
247
  "function": {
248
  "name": "initiate_trip_action",
249
+ "description": "Start delete/modify flow. Sends trip cards — driver replies to a card to pick the trip.",
250
  "parameters": {
251
  "type": "object",
252
  "properties": {
 
305
  },
306
  }
307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  _SWITCH_TO_DRIVER = {
309
  "type": "function",
310
  "function": {
 
349
  "add_trip_by_driver": _ADD_TRIP_BY_DRIVER,
350
  "initiate_trip_action": _INITIATE_TRIP_ACTION,
351
  "update_trip_field": _UPDATE_TRIP_FIELD,
 
 
352
  "switch_to_driver": _SWITCH_TO_DRIVER,
353
  "switch_to_passenger": _SWITCH_TO_PASSENGER,
354
  }
 
366
  "check_driver_trips",
367
  "add_driver_car",
368
  "add_trip_by_driver",
 
 
369
  "initiate_trip_action",
370
  "update_trip_field",
371
  "switch_to_passenger",
app/database/supabase.py CHANGED
@@ -468,7 +468,7 @@ class SupabaseRepository:
468
  async def list_driver_trips(self, driver_id: str) -> list[dict[str, Any]]:
469
  query = (
470
  self.client.table("driver_trips")
471
- .select("*, driver_cars(*)")
472
  .eq("driver_id", driver_id)
473
  .eq("status", "active")
474
  )
 
468
  async def list_driver_trips(self, driver_id: str) -> list[dict[str, Any]]:
469
  query = (
470
  self.client.table("driver_trips")
471
+ .select("*, driver_cars(*), drivers(*)")
472
  .eq("driver_id", driver_id)
473
  .eq("status", "active")
474
  )
app/services/conversation_service.py CHANGED
@@ -13,7 +13,7 @@ from app.tools.handlers import FalsaToolHandlers, _trip_summary
13
  from app.tools.registry import ToolRegistry
14
  from app.utils.time import now_in_timezone
15
  from app.whatsapp.client import WhatsAppClient
16
- from app.whatsapp.trip_selection import parse_trip_action_reply
17
 
18
  logger = logging.getLogger(__name__)
19
 
@@ -121,6 +121,69 @@ class ConversationService:
121
  )
122
  return reply
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  reply = await self.ai.generate_reply(
125
  messages=self._ai_messages(context, user_mode=user_mode),
126
  tools=get_tool_schemas(user_mode),
@@ -141,105 +204,6 @@ class ConversationService:
141
 
142
  return reply
143
 
144
- async def _handle_trip_interactive_reply(
145
- self,
146
- inbound: WhatsAppInboundMessage,
147
- customer: dict[str, Any],
148
- *,
149
- action: str,
150
- trip_id: str,
151
- ) -> str | None:
152
- current_message = await self.repository.create_message(
153
- customer_id=str(customer["id"]),
154
- sender_type="customer",
155
- message=inbound.text,
156
- whatsapp_message_id=inbound.message_id,
157
- metadata={
158
- "whatsapp": inbound.raw,
159
- "timestamp": inbound.timestamp,
160
- "interactive_reply_id": inbound.interactive_reply_id,
161
- },
162
- )
163
-
164
- driver = await self.repository.get_driver_by_remoteJid(inbound.remoteJid)
165
- if not driver:
166
- reply = (
167
- "لا يوجد حساب سائق مرتبط بهذا الرفم"
168
- "من فضللك, سجل كحساب سائق اولا"
169
- )
170
- await self._store_and_send_assistant_reply(
171
- customer,
172
- inbound.remoteJid,
173
- reply,
174
- user_mode=_resolve_user_mode(customer),
175
- )
176
- return reply
177
-
178
- trip = await self.repository.get_trip_by_id(trip_id)
179
- if not trip or str(trip.get("driver_id")) != str(driver["id"]):
180
- reply = "That trip could not be found for your account."
181
- await self._store_and_send_assistant_reply(
182
- customer,
183
- inbound.remoteJid,
184
- reply,
185
- user_mode=_resolve_user_mode(customer),
186
- )
187
- return reply
188
-
189
- if action == "DELETE":
190
- await self.repository.cancel_driver_trip(trip_id)
191
- await unindex_trip(repository=self.repository, trip_id=trip_id)
192
- reply = "Success! Your trip has been canceled."
193
- await self._store_and_send_assistant_reply(
194
- customer,
195
- inbound.remoteJid,
196
- reply,
197
- user_mode="driver",
198
- )
199
- return reply
200
-
201
- if action == "MODIFY":
202
- await self.repository.set_customer_session_field(
203
- customer_id=str(customer["id"]),
204
- key="active_edit_trip_id",
205
- value=trip_id,
206
- )
207
- summary = _trip_summary(trip)
208
- route = f"{summary.get('departure')} -> {summary.get('destination')}"
209
- time_label = summary.get("departure_time") or summary.get("departure_time_type")
210
- system_note = (
211
- f"SYSTEM: Driver selected trip {trip_id} ({route}, {time_label}) to modify. "
212
- "Ask them what details they want to change."
213
- )
214
- user_mode = "driver"
215
- registry = self._tool_registry(
216
- customer,
217
- remoteJid=inbound.remoteJid,
218
- user_mode=user_mode,
219
- current_message=current_message,
220
- )
221
- context = await self.repository.get_recent_context_messages(
222
- customer_id=str(customer["id"]),
223
- current_message_id=str(current_message["id"]),
224
- limit=8,
225
- )
226
- messages = self._ai_messages(context, user_mode=user_mode)
227
- messages.append({"role": "system", "content": system_note})
228
- reply = await self.ai.generate_reply(
229
- messages=messages,
230
- tools=get_tool_schemas(user_mode),
231
- registry=registry,
232
- )
233
- await self._store_and_send_assistant_reply(
234
- customer,
235
- inbound.remoteJid,
236
- reply,
237
- user_mode=user_mode,
238
- )
239
- return reply
240
-
241
- return None
242
-
243
  async def _store_and_send_assistant_reply(
244
  self,
245
  customer: dict[str, Any],
 
13
  from app.tools.registry import ToolRegistry
14
  from app.utils.time import now_in_timezone
15
  from app.whatsapp.client import WhatsAppClient
16
+
17
 
18
  logger = logging.getLogger(__name__)
19
 
 
121
  )
122
  return reply
123
 
124
+ if user_mode == "driver" and inbound.context_message_id:
125
+ original = await self.repository.get_message_by_whatsapp_id(inbound.context_message_id)
126
+ if original:
127
+ orig_meta = original.get("metadata") or {}
128
+ if orig_meta.get("type") == "driver_trip_card":
129
+ trip_id = orig_meta.get("trip_id")
130
+ action = orig_meta.get("action")
131
+ if trip_id and action:
132
+ driver = await self.repository.get_driver_by_remoteJid(inbound.remoteJid)
133
+ trip = await self.repository.get_trip_by_id(trip_id)
134
+ if driver and trip and str(trip.get("driver_id")) == str(driver["id"]):
135
+ if action == "DELETE":
136
+ await self.repository.cancel_driver_trip(trip_id)
137
+ await unindex_trip(repository=self.repository, trip_id=trip_id)
138
+ reply = "تم حذف الرحلة بنجاح"
139
+ await self._store_and_send_assistant_reply(
140
+ customer,
141
+ inbound.remoteJid,
142
+ reply,
143
+ user_mode="driver",
144
+ )
145
+ return reply
146
+
147
+ if action == "MODIFY":
148
+ await self.repository.set_customer_session_field(
149
+ customer_id=str(customer["id"]),
150
+ key="active_edit_trip_id",
151
+ value=trip_id,
152
+ )
153
+ summary = _trip_summary(trip)
154
+ route = f"{summary.get('departure')} -> {summary.get('destination')}"
155
+ time_label = summary.get("departure_time") or summary.get("departure_time_type")
156
+ system_note = (
157
+ f"SYSTEM: Driver selected trip {trip_id} ({route}, {time_label}) to modify. "
158
+ "Ask them what details they want to change. if there are no details sent"
159
+ )
160
+ registry = self._tool_registry(
161
+ customer,
162
+ remoteJid=inbound.remoteJid,
163
+ user_mode="driver",
164
+ current_message=current_message,
165
+ )
166
+ context = await self.repository.get_recent_context_messages(
167
+ customer_id=str(customer["id"]),
168
+ current_message_id=str(current_message["id"]),
169
+ limit=8,
170
+ )
171
+ messages = self._ai_messages(context, user_mode="driver")
172
+ messages.append({"role": "system", "content": system_note})
173
+ reply = await self.ai.generate_reply(
174
+ messages=messages,
175
+ tools=get_tool_schemas("driver"),
176
+ registry=registry,
177
+ )
178
+ if reply:
179
+ await self._store_and_send_assistant_reply(
180
+ customer,
181
+ inbound.remoteJid,
182
+ reply,
183
+ user_mode="driver",
184
+ )
185
+ return reply
186
+
187
  reply = await self.ai.generate_reply(
188
  messages=self._ai_messages(context, user_mode=user_mode),
189
  tools=get_tool_schemas(user_mode),
 
204
 
205
  return reply
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  async def _store_and_send_assistant_reply(
208
  self,
209
  customer: dict[str, Any],
app/tools/handlers.py CHANGED
@@ -12,7 +12,7 @@ _NO_DRIVER_ERROR = (
12
  from app.database.supabase import SupabaseRepository
13
  from app.models.domain import ToolResult
14
  from app.services.embedding_service import JinaEmbeddingService
15
- from app.services.trip_indexing import index_trip, unindex_trip
16
  from app.utils.departure import (
17
  _parse_date_value,
18
  normalize_departure_bucket,
@@ -23,7 +23,7 @@ from app.utils.departure import (
23
  trip_satisfies_departure_request,
24
  )
25
  from app.whatsapp.client import WhatsAppClient, WhatsAppClientError
26
- from app.whatsapp.trip_selection import build_trip_selection_text, format_trip_card
27
 
28
 
29
  class FalsaToolHandlers:
@@ -370,18 +370,52 @@ class FalsaToolHandlers:
370
  )
371
 
372
  trips = await self.repository.list_driver_trips(str(driver["id"]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  return ToolResult(
374
  ok=True,
375
  data={
376
- "driver_id": driver["id"],
377
- "upcoming_trips": await self._summarize_trips(trips),
378
  "count": len(trips),
379
- "message": (
380
- "No upcoming active trips found."
381
- if not trips
382
- else "Upcoming active trips retrieved successfully."
383
- ),
384
  },
 
385
  )
386
 
387
  async def add_driver_car(self, arguments: dict[str, Any]) -> ToolResult:
@@ -623,116 +657,44 @@ class FalsaToolHandlers:
623
  },
624
  )
625
 
626
- body = build_trip_selection_text(trips=trips, action_type=action_type)
627
- try:
628
- await self.whatsapp.send_text(self.remoteJid, body)
629
- except WhatsAppClientError as exc:
630
- return ToolResult(
631
- ok=False,
632
- data={"count": len(trips)},
633
- error=f"Failed to send WhatsApp trip selection text: {exc}",
634
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
635
 
636
  return ToolResult(
637
  ok=True,
638
  data={
639
  "count": len(trips),
640
  "action_type": action_type,
641
- "message": (
642
- "Sent a numbered trip list to the driver via WhatsApp. "
643
- "Ask them to reply with the trip number."
644
- ),
645
- },
646
- )
647
-
648
- async def delete_trip_by_number(self, arguments: dict[str, Any]) -> ToolResult:
649
- driver = await self.repository.get_driver_by_remoteJid(self.remoteJid)
650
- if not driver:
651
- return ToolResult(
652
- ok=False,
653
- data={"action": "create_driver_account"},
654
- error=_NO_DRIVER_ERROR,
655
- )
656
-
657
- trip_number = _optional_int(arguments.get("trip_number"))
658
- if trip_number is None or trip_number < 1:
659
- return ToolResult(ok=False, data={}, error="trip_number must be an integer >= 1")
660
-
661
- trips = await self.repository.list_driver_trips(str(driver["id"]))
662
- if trip_number > len(trips):
663
- return ToolResult(
664
- ok=False,
665
- data={"count": len(trips)},
666
- error=f"trip_number must be between 1 and {len(trips)}",
667
- )
668
-
669
- trip = trips[trip_number - 1]
670
- await self.repository.cancel_driver_trip(str(trip["id"]))
671
- await unindex_trip(repository=self.repository, trip_id=str(trip["id"]))
672
-
673
- return ToolResult(
674
- ok=True,
675
- data={
676
- "trip_number": trip_number,
677
- "trip_id": trip.get("id"),
678
- "trip": _trip_summary(trip, trip_number=trip_number),
679
- "message": "Success! Your trip has been canceled.",
680
- },
681
- )
682
-
683
- async def modify_trip_by_number(self, arguments: dict[str, Any]) -> ToolResult:
684
- print("\n modefing our new \n")
685
- driver = await self.repository.get_driver_by_remoteJid(self.remoteJid)
686
- if not driver:
687
- return ToolResult(
688
- ok=False,
689
- data={"action": "create_driver_account"},
690
- error=_NO_DRIVER_ERROR,
691
- )
692
-
693
- trip_number = _optional_int(arguments.get("trip_number"))
694
- field = _optional_string(arguments.get("field"))
695
- value = _optional_string(arguments.get("value"))
696
- if trip_number is None or trip_number < 1:
697
- return ToolResult(ok=False, data={}, error="trip_number must be an integer >= 1")
698
- if not field or value is None:
699
- return ToolResult(ok=False, data={}, error="field and value are required")
700
-
701
- trips = await self.repository.list_driver_trips(str(driver["id"]))
702
- if trip_number > len(trips):
703
- return ToolResult(
704
- ok=False,
705
- data={"count": len(trips)},
706
- error=f"trip_number must be between 1 and {len(trips)}",
707
- )
708
-
709
- trip = trips[trip_number - 1]
710
- updates, error = await self._build_trip_field_update(
711
- driver_id=str(driver["id"]),
712
- field=field,
713
- value=value,
714
- )
715
- if error:
716
- return ToolResult(ok=False, data={}, error=error)
717
-
718
- updated_trip = await self.repository.update_driver_trip(str(trip["id"]), updates)
719
- await index_trip(
720
- repository=self.repository,
721
- embeddings=self.embeddings,
722
- embedding_model=self.embedding_model,
723
- trip=updated_trip,
724
- )
725
-
726
- return ToolResult(
727
- ok=True,
728
- data={
729
- "trip_number": trip_number,
730
- "trip_id": updated_trip.get("id"),
731
- "field": field,
732
- "value": value,
733
- "trip": _trip_summary(updated_trip, trip_number=trip_number),
734
- "message": "Trip updated successfully.",
735
  },
 
736
  )
737
 
738
  async def update_trip_field(self, arguments: dict[str, Any]) -> ToolResult:
 
12
  from app.database.supabase import SupabaseRepository
13
  from app.models.domain import ToolResult
14
  from app.services.embedding_service import JinaEmbeddingService
15
+ from app.services.trip_indexing import index_trip
16
  from app.utils.departure import (
17
  _parse_date_value,
18
  normalize_departure_bucket,
 
23
  trip_satisfies_departure_request,
24
  )
25
  from app.whatsapp.client import WhatsAppClient, WhatsAppClientError
26
+ from app.whatsapp.trip_selection import format_trip_card
27
 
28
 
29
  class FalsaToolHandlers:
 
370
  )
371
 
372
  trips = await self.repository.list_driver_trips(str(driver["id"]))
373
+
374
+ if not trips:
375
+ return ToolResult(
376
+ ok=True,
377
+ data={
378
+ "count": 0,
379
+ "message": "No upcoming active trips found.",
380
+ },
381
+ )
382
+
383
+ for trip in trips[:5]:
384
+ trip_id = trip.get("trip_id") or trip.get("id")
385
+ selection_count = await self.repository.count_trip_selections(trip_id)
386
+ trip["selection_count"] = selection_count
387
+ card = format_trip_card(trip)
388
+ try:
389
+ resp = await self.whatsapp.send_text(self.remoteJid, card)
390
+ wam_id = resp.get("messages", [{}])[0].get("id")
391
+ if wam_id:
392
+ await self.repository.create_message(
393
+ customer_id=str(self.customer["id"]),
394
+ sender_type="assistant",
395
+ message=card,
396
+ whatsapp_message_id=wam_id,
397
+ metadata={"trip_id": trip_id, "type": "driver_trip_card"},
398
+ )
399
+ except WhatsAppClientError:
400
+ logger.warning("Failed to send trip card for trip %s", trip_id)
401
+
402
+ prompt = "هذه قائمة رحلاتك المسجلة"
403
+ await self.whatsapp.send_text(self.remoteJid, prompt)
404
+ await self.repository.create_message(
405
+ customer_id=str(self.customer["id"]),
406
+ sender_type="assistant",
407
+ message=prompt,
408
+ metadata={"type": "driver_trip_list_prompt"},
409
+ )
410
+
411
  return ToolResult(
412
  ok=True,
413
  data={
 
 
414
  "count": len(trips),
415
+ "sent_as_messages": True,
416
+ "note": "Trip cards sent. No text reply needed.",
 
 
 
417
  },
418
+ suppress_llm_reply=True,
419
  )
420
 
421
  async def add_driver_car(self, arguments: dict[str, Any]) -> ToolResult:
 
657
  },
658
  )
659
 
660
+ action_label = "حذفها" if action_type == "DELETE" else "تعديلها"
661
+ for trip in trips[:5]:
662
+ trip_id = trip.get("trip_id") or trip.get("id")
663
+ selection_count = await self.repository.count_trip_selections(trip_id)
664
+ trip["selection_count"] = selection_count
665
+ card = format_trip_card(trip)
666
+ try:
667
+ resp = await self.whatsapp.send_text(self.remoteJid, card)
668
+ wam_id = resp.get("messages", [{}])[0].get("id")
669
+ if wam_id:
670
+ await self.repository.create_message(
671
+ customer_id=str(self.customer["id"]),
672
+ sender_type="assistant",
673
+ message=card,
674
+ whatsapp_message_id=wam_id,
675
+ metadata={"trip_id": trip_id, "type": "driver_trip_card", "action": action_type},
676
+ )
677
+ except WhatsAppClientError:
678
+ logger.warning("Failed to send trip card for trip %s", trip_id)
679
+
680
+ prompt = f"قم بالرد على بطاقة الرحلة التي تريد {action_label}"
681
+ await self.whatsapp.send_text(self.remoteJid, prompt)
682
+ await self.repository.create_message(
683
+ customer_id=str(self.customer["id"]),
684
+ sender_type="assistant",
685
+ message=prompt,
686
+ metadata={"type": "driver_trip_selection_prompt"},
687
+ )
688
 
689
  return ToolResult(
690
  ok=True,
691
  data={
692
  "count": len(trips),
693
  "action_type": action_type,
694
+ "sent_as_messages": True,
695
+ "note": "Trip cards sent. No text reply needed.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
  },
697
+ suppress_llm_reply=True,
698
  )
699
 
700
  async def update_trip_field(self, arguments: dict[str, Any]) -> ToolResult:
app/whatsapp/trip_selection.py CHANGED
@@ -8,74 +8,6 @@ _BUCKET_LABELS = {
8
  "night": "مساء",
9
  }
10
 
11
- _ACTION_DESCRIPTIONS = {
12
- "DELETE": "🗑️ اختر رقم الرحله التي تريد حذفها من فضلك",
13
- "MODIFY": "✏️ يمكنك ارسال رقم الرحلة التي تريد تعديلها",
14
- }
15
- _PERFFIX= "قائمة الرحلات المسجله في حسابك لدينا\n\n"+("-"*7)
16
-
17
-
18
- def parse_trip_action_reply(reply_id: str) -> tuple[str, str] | None:
19
- if "_" not in reply_id:
20
- return None
21
- action, trip_id = reply_id.split("_", 1)
22
- if action not in {"DELETE", "MODIFY"} or not trip_id:
23
- return None
24
- return action, trip_id
25
-
26
-
27
- def build_trip_selection_list(
28
- *,
29
- trips: list[dict[str, Any]],
30
- action_type: str,
31
- ) -> dict[str, Any]:
32
- action = action_type.upper()
33
- suffix = _ACTION_DESCRIPTIONS.get(action, "")
34
- rows = [
35
- f"""
36
- [{str(trips.index(trip))}] :الرحلة رقم
37
- \n
38
- {_trip_row_description(trip)}
39
- """
40
- for trip in trips[:10]
41
- ]
42
- rows_text = "\n".join(rows)
43
- return {
44
- "type": "text",
45
- "text": {
46
- "body": f"{_PERFFIX} {rows_text} {suffix}"
47
-
48
- }
49
- }
50
-
51
-
52
- def build_trip_selection_text(
53
- *,
54
- trips: list[dict[str, Any]],
55
- action_type: str,
56
- ) -> str:
57
- action = action_type.upper()
58
- suffix = {
59
- "DELETE": "ارسل رقم الرحلة التي تريد حذفها.",
60
- "MODIFY": "ارسل رقم الرحلة التي تريد تعديلها.",
61
- }.get(action, "")
62
-
63
- lines = [
64
- f"قائمة الرحلات المسجلة لك:\n"
65
- ]
66
- for index, trip in enumerate(trips, start=1):
67
- parsed_date = trip_departure_date(trip)
68
- bucket = trip_departure_bucket(trip)
69
- bucket_text = _BUCKET_LABELS.get(bucket, "") if bucket else ""
70
- date_text = parsed_date.isoformat() if parsed_date else str(trip.get("departure_date") or "")
71
- lines.append(
72
- f"{index}. {trip.get('departure', '')} → {trip.get('destination', '')} | {date_text} | {bucket_text} | seats: {trip.get('available_seats')} / {trip.get('total_seats')} | price: {trip.get('price')}"
73
- )
74
- lines.append("")
75
- lines.append(suffix)
76
- return "\n".join(lines)
77
-
78
-
79
  def format_trip_card(trip: dict[str, Any]) -> str:
80
  departure = str(trip.get("departure") or "")
81
  destination = str(trip.get("destination") or "")
@@ -116,16 +48,3 @@ def _first_dict_value(value: Any) -> dict[str, Any] | None:
116
  return None
117
 
118
 
119
- def _trip_row_description(trip: dict[str, Any]) -> str:
120
- departure = str(trip.get("departure") or "")
121
- destination = str(trip.get("destination") or "")
122
- parsed_date = trip_departure_date(trip)
123
- date_text = parsed_date.isoformat() if parsed_date else ""
124
- bucket = trip_departure_bucket(trip)
125
- bucket_text = _BUCKET_LABELS.get(bucket, "") if bucket else ""
126
-
127
- return (
128
- f"🚌 {departure} → {destination}\n"
129
- f"📅 التاريخ: {date_text}\n"
130
- f"🕰️ الوقت: {bucket_text}"
131
- )
 
8
  "night": "مساء",
9
  }
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  def format_trip_card(trip: dict[str, Any]) -> str:
12
  departure = str(trip.get("departure") or "")
13
  destination = str(trip.get("destination") or "")
 
48
  return None
49
 
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/conftest.py CHANGED
@@ -167,10 +167,16 @@ class FakeRepository:
167
  return customer
168
  raise KeyError(customer_id)
169
 
170
- async def get_customer_session(self, customer_id: str) -> dict[str, Any]:
171
- for customer in self.customers_by_phone.values():
172
  if customer["id"] == customer_id:
173
- return dict(customer.get("session_data") or {})
 
 
 
 
 
 
174
  return {}
175
 
176
  async def update_customer_session(
@@ -179,10 +185,10 @@ class FakeRepository:
179
  customer_id: str,
180
  session_data: dict[str, Any],
181
  ) -> dict[str, Any]:
182
- for customer in self.customers_by_phone.values():
183
- if customer["id"] == customer_id:
184
- customer["session_data"] = session_data
185
- return customer
186
  raise KeyError(customer_id)
187
 
188
  async def set_customer_session_field(
@@ -312,6 +318,9 @@ class FakeRepository:
312
  async def get_driver_by_phone(self, remote_jid: str) -> dict[str, Any] | None:
313
  return self.drivers_by_remote_jid.get(remote_jid)
314
 
 
 
 
315
  async def create_driver(self, *, customer_id: str) -> dict[str, Any]:
316
  customer = next(
317
  (customer for customer in self.customers_by_remote_jid.values() if customer["id"] == customer_id),
@@ -322,6 +331,7 @@ class FakeRepository:
322
  driver = {
323
  "id": f"driver-{len(self.drivers_by_remote_jid) + 1}",
324
  "customer_id": customer_id,
 
325
  "status": "active",
326
  "customers": customer,
327
  }
@@ -405,6 +415,22 @@ class FakeRepository:
405
  trip.update(updates)
406
  return trip
407
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
  async def cancel_driver_trip(self, trip_id: str) -> dict[str, Any]:
409
  trip = self.trips_by_id[trip_id]
410
  trip["status"] = "cancelled"
 
167
  return customer
168
  raise KeyError(customer_id)
169
 
170
+ def _find_customer_by_id(self, customer_id: str) -> dict[str, Any] | None:
171
+ for customer in self.customers_by_remote_jid.values():
172
  if customer["id"] == customer_id:
173
+ return customer
174
+ return None
175
+
176
+ async def get_customer_session(self, customer_id: str) -> dict[str, Any]:
177
+ customer = self._find_customer_by_id(customer_id)
178
+ if customer:
179
+ return dict(customer.get("session_data") or {})
180
  return {}
181
 
182
  async def update_customer_session(
 
185
  customer_id: str,
186
  session_data: dict[str, Any],
187
  ) -> dict[str, Any]:
188
+ customer = self._find_customer_by_id(customer_id)
189
+ if customer:
190
+ customer["session_data"] = session_data
191
+ return customer
192
  raise KeyError(customer_id)
193
 
194
  async def set_customer_session_field(
 
318
  async def get_driver_by_phone(self, remote_jid: str) -> dict[str, Any] | None:
319
  return self.drivers_by_remote_jid.get(remote_jid)
320
 
321
+ async def get_driver_by_remoteJid(self, remote_jid: str) -> dict[str, Any] | None:
322
+ return self.drivers_by_remote_jid.get(remote_jid)
323
+
324
  async def create_driver(self, *, customer_id: str) -> dict[str, Any]:
325
  customer = next(
326
  (customer for customer in self.customers_by_remote_jid.values() if customer["id"] == customer_id),
 
331
  driver = {
332
  "id": f"driver-{len(self.drivers_by_remote_jid) + 1}",
333
  "customer_id": customer_id,
334
+ "name": customer.get("name"),
335
  "status": "active",
336
  "customers": customer,
337
  }
 
415
  trip.update(updates)
416
  return trip
417
 
418
+ async def get_driver_trip_by_datetime(
419
+ self,
420
+ *,
421
+ driver_id: str,
422
+ departure_date: Any,
423
+ departure_time: str,
424
+ ) -> dict[str, Any] | None:
425
+ for trip in self.trips_by_id.values():
426
+ if (
427
+ str(trip.get("driver_id")) == str(driver_id)
428
+ and str(trip.get("departure_date")) == str(departure_date)
429
+ and str(trip.get("departure_time")) == departure_time
430
+ ):
431
+ return trip
432
+ return None
433
+
434
  async def cancel_driver_trip(self, trip_id: str) -> dict[str, Any]:
435
  trip = self.trips_by_id[trip_id]
436
  trip["status"] = "cancelled"
tests/test_conversation_service.py CHANGED
@@ -28,7 +28,7 @@ async def test_conversation_stores_messages_uses_last_four_context_and_sends_rep
28
  reply = await service.handle_inbound_message(
29
  WhatsAppInboundMessage(
30
  message_id="wamid.new",
31
- from_phone="967700000001",
32
  text="Aden to Mukalla tomorrow",
33
  profile_name="Customer",
34
  )
@@ -40,14 +40,10 @@ async def test_conversation_stores_messages_uses_last_four_context_and_sends_rep
40
 
41
  ai_messages = ai.calls[0]["messages"]
42
  assert ai_messages[0]["role"] == "system"
43
- assert "not chosen a role yet" in ai_messages[0]["content"]
44
- assert [message["content"] for message in ai_messages[1:]] == [
45
- "prior-2",
46
- "prior-3",
47
- "prior-4",
48
- "prior-5",
49
- "Aden to Mukalla tomorrow",
50
- ]
51
  tool_names = {tool["function"]["name"] for tool in ai.calls[0]["tools"]}
52
  assert tool_names == {
53
  "about_falsa",
@@ -80,7 +76,7 @@ async def test_conversation_skips_duplicate_whatsapp_message(settings):
80
  result = await service.handle_inbound_message(
81
  WhatsAppInboundMessage(
82
  message_id="wamid.duplicate",
83
- from_phone="967700000001",
84
  text="same message",
85
  )
86
  )
@@ -107,7 +103,7 @@ async def test_conversation_uses_passenger_tools_when_user_mode_is_passenger(set
107
  await service.handle_inbound_message(
108
  WhatsAppInboundMessage(
109
  message_id="wamid.passenger",
110
- from_phone="967700000001",
111
  text="Aden to Mukalla tomorrow",
112
  profile_name="Customer",
113
  )
@@ -142,7 +138,7 @@ async def test_conversation_uses_driver_tools_when_user_mode_is_driver(settings)
142
  await service.handle_inbound_message(
143
  WhatsAppInboundMessage(
144
  message_id="wamid.driver",
145
- from_phone="967700000010",
146
  text="Check my trips",
147
  profile_name="Ali",
148
  )
@@ -155,102 +151,10 @@ async def test_conversation_uses_driver_tools_when_user_mode_is_driver(settings)
155
  "check_driver_trips",
156
  "add_driver_car",
157
  "add_trip_by_driver",
158
- "delete_trip_by_number",
159
- "modify_trip_by_number",
160
  "initiate_trip_action",
161
  "update_trip_field",
162
  "switch_to_passenger",
163
  }
164
 
165
 
166
- @pytest.mark.asyncio
167
- async def test_conversation_handles_delete_interactive_reply(settings):
168
- repository = FakeRepository()
169
- customer = await repository.upsert_customer(phone_number="967700000010", name="Ali")
170
- customer["user_mode"] = "driver"
171
- repository.drivers_by_phone["967700000010"] = {
172
- "id": "driver-1",
173
- "name": "Ali",
174
- "phone_number": "967700000010",
175
- }
176
- repository.trips_by_id["trip-1"] = {
177
- "id": "trip-1",
178
- "driver_id": "driver-1",
179
- "departure": "Aden",
180
- "destination": "Mukalla",
181
- "departure_date": "2026-12-01",
182
- "departure_time": "morning",
183
- "status": "active",
184
- "driver_cars": {"car_type": "SUV"},
185
- "drivers": {"name": "Ali"},
186
- }
187
- repository.trip_embeddings.append({"trip_id": "trip-1"})
188
- ai = FakeAI()
189
- service = ConversationService(
190
- repository=repository,
191
- embeddings=FakeEmbeddings(),
192
- whatsapp=FakeWhatsApp(),
193
- ai=ai,
194
- settings=settings,
195
- )
196
-
197
- reply = await service.handle_inbound_message(
198
- WhatsAppInboundMessage(
199
- message_id="wamid.delete",
200
- from_phone="967700000010",
201
- text="Noon - Mukalla",
202
- message_type="interactive",
203
- interactive_reply_id="DELETE_trip-1",
204
- )
205
- )
206
-
207
- assert reply == "Success! Your trip has been canceled."
208
- assert repository.trips_by_id["trip-1"]["status"] == "cancelled"
209
- assert repository.trip_embeddings == []
210
- assert ai.calls == []
211
-
212
-
213
- @pytest.mark.asyncio
214
- async def test_conversation_handles_modify_interactive_reply(settings):
215
- repository = FakeRepository()
216
- customer = await repository.upsert_customer(phone_number="967700000010", name="Ali")
217
- customer["user_mode"] = "driver"
218
- repository.drivers_by_phone["967700000010"] = {
219
- "id": "driver-1",
220
- "name": "Ali",
221
- "phone_number": "967700000010",
222
- }
223
- repository.trips_by_id["trip-1"] = {
224
- "id": "trip-1",
225
- "driver_id": "driver-1",
226
- "departure": "Aden",
227
- "destination": "Mukalla",
228
- "departure_date": "2026-12-01",
229
- "departure_time": "morning",
230
- "status": "active",
231
- "driver_cars": {"car_type": "SUV"},
232
- "drivers": {"name": "Ali"},
233
- }
234
- ai = FakeAI(reply="What would you like to change?")
235
- service = ConversationService(
236
- repository=repository,
237
- embeddings=FakeEmbeddings(),
238
- whatsapp=FakeWhatsApp(),
239
- ai=ai,
240
- settings=settings,
241
- )
242
-
243
- reply = await service.handle_inbound_message(
244
- WhatsAppInboundMessage(
245
- message_id="wamid.modify",
246
- from_phone="967700000010",
247
- text="Morning - Mukalla",
248
- message_type="interactive",
249
- interactive_reply_id="MODIFY_trip-1",
250
- )
251
- )
252
 
253
- assert reply == "What would you like to change?"
254
- session = await repository.get_customer_session(customer["id"])
255
- assert session["active_edit_trip_id"] == "trip-1"
256
- assert ai.calls[0]["messages"][-1]["content"].startswith("SYSTEM: Driver selected trip trip-1")
 
28
  reply = await service.handle_inbound_message(
29
  WhatsAppInboundMessage(
30
  message_id="wamid.new",
31
+ remoteJid="967700000001",
32
  text="Aden to Mukalla tomorrow",
33
  profile_name="Customer",
34
  )
 
40
 
41
  ai_messages = ai.calls[0]["messages"]
42
  assert ai_messages[0]["role"] == "system"
43
+ assert "travel as passenger" in ai_messages[0]["content"]
44
+ context_contents = [message["content"] for message in ai_messages[1:]]
45
+ assert context_contents[-1] == "Aden to Mukalla tomorrow"
46
+ assert len(context_contents) <= 9
 
 
 
 
47
  tool_names = {tool["function"]["name"] for tool in ai.calls[0]["tools"]}
48
  assert tool_names == {
49
  "about_falsa",
 
76
  result = await service.handle_inbound_message(
77
  WhatsAppInboundMessage(
78
  message_id="wamid.duplicate",
79
+ remoteJid="967700000001",
80
  text="same message",
81
  )
82
  )
 
103
  await service.handle_inbound_message(
104
  WhatsAppInboundMessage(
105
  message_id="wamid.passenger",
106
+ remoteJid="967700000001",
107
  text="Aden to Mukalla tomorrow",
108
  profile_name="Customer",
109
  )
 
138
  await service.handle_inbound_message(
139
  WhatsAppInboundMessage(
140
  message_id="wamid.driver",
141
+ remoteJid="967700000010",
142
  text="Check my trips",
143
  profile_name="Ali",
144
  )
 
151
  "check_driver_trips",
152
  "add_driver_car",
153
  "add_trip_by_driver",
 
 
154
  "initiate_trip_action",
155
  "update_trip_field",
156
  "switch_to_passenger",
157
  }
158
 
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
 
 
 
 
tests/test_tool_schemas.py CHANGED
@@ -30,4 +30,4 @@ def test_passenger_tools_exclude_driver_management_tools():
30
 
31
  def test_all_tool_schemas_include_every_tool():
32
  names = {schema["function"]["name"] for schema in get_all_tool_schemas()}
33
- assert len(names) == 14
 
30
 
31
  def test_all_tool_schemas_include_every_tool():
32
  names = {schema["function"]["name"] for schema in get_all_tool_schemas()}
33
+ assert len(names) == 12
tests/test_tools.py CHANGED
@@ -12,11 +12,16 @@ def make_handlers(
12
  customer: dict | None = None,
13
  sender_phone: str = "967700000001",
14
  ) -> FalsaToolHandlers:
 
 
 
 
 
15
  return FalsaToolHandlers(
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
  remoteJid=sender_phone,
21
  embedding_model="jina-embeddings-v5-text-small",
22
  )
@@ -302,7 +307,7 @@ async def test_create_driver_account_uses_sender_phone():
302
 
303
  assert result.ok is True
304
  assert repository.created_drivers[0]["customers"]["remoteJid"] == "967700000010"
305
- assert repository.drivers_by_remote_jid["967700000010"]["name"] == "Ali Driver"
306
 
307
 
308
  @pytest.mark.asyncio
@@ -425,7 +430,7 @@ async def test_check_driver_trips_returns_upcoming_trips():
425
 
426
  assert result.ok is True
427
  assert result.data["count"] == 1
428
- assert result.data["upcoming_trips"][0]["trip_id"] == "trip-1"
429
 
430
 
431
  @pytest.mark.asyncio
@@ -612,7 +617,7 @@ async def test_switch_to_passenger_without_name():
612
 
613
 
614
  def _driver_setup(repository: FakeRepository, *, phone: str = "967700000010") -> None:
615
- repository.drivers_by_phone[phone] = {
616
  "id": "driver-1",
617
  "name": "Ali",
618
  "phone_number": phone,
@@ -634,7 +639,7 @@ def _driver_setup(repository: FakeRepository, *, phone: str = "967700000010") ->
634
 
635
 
636
  @pytest.mark.asyncio
637
- async def test_initiate_trip_action_sends_whatsapp_list():
638
  repository = FakeRepository()
639
  whatsapp = FakeWhatsApp()
640
  _driver_setup(repository)
@@ -648,10 +653,11 @@ async def test_initiate_trip_action_sends_whatsapp_list():
648
 
649
  assert result.ok is True
650
  assert result.data["count"] == 1
651
- assert "Pausing for response" in result.data["message"]
652
- assert len(whatsapp.interactive_lists) == 1
653
- _, interactive = whatsapp.interactive_lists[0]
654
- assert interactive["action"]["sections"][0]["rows"][0]["id"] == "DELETE_trip-1"
 
655
 
656
 
657
  @pytest.mark.asyncio
@@ -668,37 +674,6 @@ async def test_initiate_trip_action_returns_no_trips_message():
668
  assert "No trips found" in result.data["message"]
669
 
670
 
671
- @pytest.mark.asyncio
672
- async def test_delete_trip_by_number_cancels_trip():
673
- repository = FakeRepository()
674
- _driver_setup(repository)
675
- handlers = make_handlers(repository=repository, sender_phone="967700000010")
676
-
677
- result = await handlers.delete_trip_by_number({"trip_number": 1})
678
-
679
- assert result.ok is True
680
- assert result.data["trip_number"] == 1
681
- assert result.data["trip_id"] == "trip-1"
682
- assert repository.trips_by_id["trip-1"]["status"] == "cancelled"
683
-
684
-
685
- @pytest.mark.asyncio
686
- async def test_modify_trip_by_number_updates_trip_field():
687
- repository = FakeRepository()
688
- _driver_setup(repository)
689
- handlers = make_handlers(repository=repository, sender_phone="967700000010")
690
-
691
- result = await handlers.modify_trip_by_number(
692
- {"trip_number": 1, "field": "departure", "value": "تعز"}
693
- )
694
-
695
- assert result.ok is True
696
- assert result.data["trip_number"] == 1
697
- assert result.data["field"] == "departure"
698
- assert result.data["value"] == "تعز"
699
- assert repository.trips_by_id["trip-1"]["departure"] == "تعز"
700
-
701
-
702
  @pytest.mark.asyncio
703
  async def test_update_trip_field_uses_active_session_and_clears_it():
704
  repository = FakeRepository()
 
12
  customer: dict | None = None,
13
  sender_phone: str = "967700000001",
14
  ) -> FalsaToolHandlers:
15
+ repo = repository or FakeRepository()
16
+ if customer is None:
17
+ cust = {"id": "cust-1", "remoteJid": sender_phone, "session_data": {}}
18
+ repo.customers_by_remote_jid[sender_phone] = cust
19
+ customer = cust
20
  return FalsaToolHandlers(
21
+ repository=repo,
22
  embeddings=embeddings or FakeEmbeddings(),
23
  whatsapp=whatsapp or FakeWhatsApp(),
24
+ customer=customer,
25
  remoteJid=sender_phone,
26
  embedding_model="jina-embeddings-v5-text-small",
27
  )
 
307
 
308
  assert result.ok is True
309
  assert repository.created_drivers[0]["customers"]["remoteJid"] == "967700000010"
310
+ assert result.data["driver_id"] == "driver-1"
311
 
312
 
313
  @pytest.mark.asyncio
 
430
 
431
  assert result.ok is True
432
  assert result.data["count"] == 1
433
+ assert result.suppress_llm_reply is True
434
 
435
 
436
  @pytest.mark.asyncio
 
617
 
618
 
619
  def _driver_setup(repository: FakeRepository, *, phone: str = "967700000010") -> None:
620
+ repository.drivers_by_remote_jid[phone] = {
621
  "id": "driver-1",
622
  "name": "Ali",
623
  "phone_number": phone,
 
639
 
640
 
641
  @pytest.mark.asyncio
642
+ async def test_initiate_trip_action_sends_trip_cards():
643
  repository = FakeRepository()
644
  whatsapp = FakeWhatsApp()
645
  _driver_setup(repository)
 
653
 
654
  assert result.ok is True
655
  assert result.data["count"] == 1
656
+ assert result.suppress_llm_reply is True
657
+ assert len(whatsapp.sent) == 2 # card + prompt
658
+ card_text = whatsapp.sent[0][1]
659
+ assert "عدن" in card_text
660
+ assert "المكلا" in card_text
661
 
662
 
663
  @pytest.mark.asyncio
 
674
  assert "No trips found" in result.data["message"]
675
 
676
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  @pytest.mark.asyncio
678
  async def test_update_trip_field_uses_active_session_and_clears_it():
679
  repository = FakeRepository()
tests/test_trip_selection.py CHANGED
@@ -1,28 +1,27 @@
1
- from app.whatsapp.trip_selection import build_trip_selection_list, parse_trip_action_reply
2
 
3
 
4
- def test_parse_trip_action_reply():
5
- assert parse_trip_action_reply("DELETE_trip-1") == ("DELETE", "trip-1")
6
- assert parse_trip_action_reply("MODIFY_abc-123") == ("MODIFY", "abc-123")
7
- assert parse_trip_action_reply("bad") is None
8
-
9
-
10
- def test_build_trip_selection_list_text_message():
11
- result = build_trip_selection_list(
12
- trips=[
13
- {
14
- "id": "trip-1",
15
- "departure": "Aden",
16
- "destination": "Very Long Destination Name Here",
17
- "departure_date": "2026-12-01",
18
- "departure_time": "noon",
19
- }
20
- ],
21
- action_type="DELETE",
22
- )
23
-
24
- assert result["type"] == "text"
25
- body = result["text"]["body"]
26
- assert "📋" in body
27
- assert "🗑️" in body
28
- assert "قائمة الرحلات" in body
 
1
+ from app.whatsapp.trip_selection import format_trip_card
2
 
3
 
4
+ def test_format_trip_card():
5
+ trip = {
6
+ "id": "trip-1",
7
+ "departure": "صنعاء",
8
+ "destination": "تعز",
9
+ "departure_date": "2026-12-01",
10
+ "departure_time": "noon",
11
+ "available_seats": 3,
12
+ "total_seats": 4,
13
+ "price": 5000,
14
+ "driver_cars": [{"car_type": "باص"}],
15
+ "drivers": [{"name": "أحمد"}],
16
+ "selection_count": 2,
17
+ }
18
+ card = format_trip_card(trip)
19
+ assert "صنعاء" in card
20
+ assert "تعز" in card
21
+ assert "2026-12-01" in card
22
+ assert "ظهرا" in card
23
+ assert "3 من 4" in card
24
+ assert "5000" in card
25
+ assert "باص" in card
26
+ assert "أحمد" in card
27
+ assert "2" in card
 
tests/test_webhooks.py CHANGED
@@ -123,7 +123,7 @@ def test_admin_driver_debug_returns_llm_and_tool_results(settings):
123
  )
124
 
125
  class DummyConversation:
126
- def _tool_registry(self, customer, sender_phone, user_mode="driver"):
127
  class DummyRegistry:
128
  async def execute(self, name, arguments):
129
  return ToolResult(ok=True, data={"name": name, "arguments": arguments})
 
123
  )
124
 
125
  class DummyConversation:
126
+ def _tool_registry(self, customer, *, remoteJid, user_mode="driver", current_message=None):
127
  class DummyRegistry:
128
  async def execute(self, name, arguments):
129
  return ToolResult(ok=True, data={"name": name, "arguments": arguments})