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

rename booking leads to trip selections

Browse files

- create_booking_lead -> select_trip across tools, schemas, handlers, prompts
- booking_leads table -> trip_selections (with migration)
- update Arabic notifications and prompts to reflect 'interest' not 'booking'
- add selection_count to trip cards and summaries
- update README and falsa_info.md to reflect new coordination model

README.md CHANGED
@@ -1,6 +1,6 @@
1
  # FALSA
2
 
3
- > **AI-powered travel booking assistant — on WhatsApp.**
4
 
5
  > **🚧 Work in progress — not yet published. Coming soon.**
6
 
@@ -8,7 +8,7 @@
8
  <img src="assets/Falsa.png" alt="FALSA logo" width="100%">
9
  </p>
10
 
11
- FALSA lets passengers search trips and book seats, and lets drivers publish, update, and manage their trips — all through a natural conversation on WhatsApp. No app to install, no complex UI to learn. Just type what you want.
12
 
13
  ---
14
 
@@ -25,8 +25,8 @@ Travel booking platforms in my country are poorly adopted. Not because people do
25
 
26
  FALSA removes all of that. The entire interface is a single chat conversation. **You say where you're going and when — FALSA handles the rest.**
27
 
28
- - **Passengers** describe their trip in natural language; FALSA searches available trips, shows options, and helps book.
29
- - **Drivers** publish trips, manage schedules, and receive booking leads — all by chatting.
30
  - No app install, no account creation flow, no confusing dashboard.
31
 
32
  ---
@@ -35,8 +35,8 @@ FALSA removes all of that. The entire interface is a single chat conversation. *
35
 
36
  - **Natural-language trip search** — "I want to go from Sana'a to Taiz tomorrow morning"
37
  - **WhatsApp-native interaction** — interactive lists, text replies, no extra UI
38
- - **Driver tools** — publish trips, add cars, modify/delete trips, receive booking notifications
39
- - **Passenger tools** — search trips, create booking leads, get FALSA information
40
  - **AI-powered orchestration** — tool-calling LLM (Groq primary, Hugging Face fallback) routes intent to the right action
41
  - **RAG knowledge base** — company info, pricing, policies embedded via Jina AI and retrieved on demand
42
  - **Trip vector search** — semantic search over driver trips for flexible departure matching
 
1
  # FALSA
2
 
3
+ > **AI-powered travel service — on WhatsApp.**
4
 
5
  > **🚧 Work in progress — not yet published. Coming soon.**
6
 
 
8
  <img src="assets/Falsa.png" alt="FALSA logo" width="100%">
9
  </p>
10
 
11
+ FALSA lets passengers search trips and connect with drivers, and lets drivers publish, update, and manage their trips — all through a natural conversation on WhatsApp. No app to install, no complex UI to learn. Just type what you want.
12
 
13
  ---
14
 
 
25
 
26
  FALSA removes all of that. The entire interface is a single chat conversation. **You say where you're going and when — FALSA handles the rest.**
27
 
28
+ - **Passengers** describe their trip in natural language; FALSA searches available trips, shows options, and connects them with drivers.
29
+ - **Drivers** publish trips, manage schedules, and receive trip interest notifications — all by chatting.
30
  - No app install, no account creation flow, no confusing dashboard.
31
 
32
  ---
 
35
 
36
  - **Natural-language trip search** — "I want to go from Sana'a to Taiz tomorrow morning"
37
  - **WhatsApp-native interaction** — interactive lists, text replies, no extra UI
38
+ - **Driver tools** — publish trips, add cars, modify/delete trips, receive trip interest notifications
39
+ - **Passenger tools** — search trips, select trips, get driver contact, get FALSA information
40
  - **AI-powered orchestration** — tool-calling LLM (Groq primary, Hugging Face fallback) routes intent to the right action
41
  - **RAG knowledge base** — company info, pricing, policies embedded via Jina AI and retrieved on demand
42
  - **Trip vector search** — semantic search over driver trips for flexible departure matching
app/ai/tool_schemas.py CHANGED
@@ -85,12 +85,13 @@ _SEARCH_TRIPS = {
85
  },
86
  }
87
 
88
- _CREATE_BOOKING_LEAD = {
89
  "type": "function",
90
  "function": {
91
- "name": "create_booking_lead",
92
  "description": (
93
- "Create pending booking (default 1 seat), notify driver. "
 
94
  "Call on trip-card reply. Does not reserve or confirm."
95
  ),
96
  "parameters": {
@@ -399,7 +400,7 @@ _SWITCH_TO_PASSENGER = {
399
  _TOOL_SCHEMAS: dict[str, dict[str, Any]] = {
400
  "about_falsa": _ABOUT_FALSA,
401
  "search_trips": _SEARCH_TRIPS,
402
- "create_booking_lead": _CREATE_BOOKING_LEAD,
403
  "create_driver_account": _CREATE_DRIVER_ACCOUNT,
404
  "check_driver_info": _CHECK_DRIVER_INFO,
405
  "check_driver_trips": _CHECK_DRIVER_TRIPS,
@@ -435,7 +436,7 @@ _TOOLS_BY_MODE: dict[UserMode, list[str]] = {
435
  "passenger": [
436
  "about_falsa",
437
  "search_trips",
438
- "create_booking_lead",
439
  "create_driver_account",
440
  "switch_to_driver",
441
  ],
 
85
  },
86
  }
87
 
88
+ _SELECT_TRIP = {
89
  "type": "function",
90
  "function": {
91
+ "name": "select_trip",
92
  "description": (
93
+ "Record customer interest in a trip (default 1 seat), notify driver, "
94
+ "and return driver phone number so they can coordinate directly. "
95
  "Call on trip-card reply. Does not reserve or confirm."
96
  ),
97
  "parameters": {
 
400
  _TOOL_SCHEMAS: dict[str, dict[str, Any]] = {
401
  "about_falsa": _ABOUT_FALSA,
402
  "search_trips": _SEARCH_TRIPS,
403
+ "select_trip": _SELECT_TRIP,
404
  "create_driver_account": _CREATE_DRIVER_ACCOUNT,
405
  "check_driver_info": _CHECK_DRIVER_INFO,
406
  "check_driver_trips": _CHECK_DRIVER_TRIPS,
 
436
  "passenger": [
437
  "about_falsa",
438
  "search_trips",
439
+ "select_trip",
440
  "create_driver_account",
441
  "switch_to_driver",
442
  ],
app/database/supabase.py CHANGED
@@ -555,7 +555,7 @@ class SupabaseRepository:
555
  updated = data[0] if isinstance(data, list) else data
556
  return await self.get_trip_by_id(str(updated.get("id") or trip_id)) or updated
557
 
558
- async def create_booking_lead(
559
  self,
560
  *,
561
  customer_id: str,
@@ -571,14 +571,14 @@ class SupabaseRepository:
571
  "notes": notes,
572
  "driver_notification_status": "not_sent",
573
  }
574
- response = await self.client.table("booking_leads").insert(payload).execute()
575
  data = _response_data(response)
576
  return data[0] if isinstance(data, list) else data
577
 
578
- async def update_booking_lead_notification(
579
  self,
580
  *,
581
- lead_id: str,
582
  status: str,
583
  metadata: dict[str, Any] | None = None,
584
  ) -> dict[str, Any]:
@@ -586,7 +586,17 @@ class SupabaseRepository:
586
  if metadata is not None:
587
  payload["metadata"] = metadata
588
  response = (
589
- await self.client.table("booking_leads").update(payload).eq("id", lead_id).execute()
590
  )
591
  data = _response_data(response)
592
  return data[0] if isinstance(data, list) else data
 
 
 
 
 
 
 
 
 
 
 
555
  updated = data[0] if isinstance(data, list) else data
556
  return await self.get_trip_by_id(str(updated.get("id") or trip_id)) or updated
557
 
558
+ async def create_trip_selection(
559
  self,
560
  *,
561
  customer_id: str,
 
571
  "notes": notes,
572
  "driver_notification_status": "not_sent",
573
  }
574
+ response = await self.client.table("trip_selections").insert(payload).execute()
575
  data = _response_data(response)
576
  return data[0] if isinstance(data, list) else data
577
 
578
+ async def update_selection_notification(
579
  self,
580
  *,
581
+ selection_id: str,
582
  status: str,
583
  metadata: dict[str, Any] | None = None,
584
  ) -> dict[str, Any]:
 
586
  if metadata is not None:
587
  payload["metadata"] = metadata
588
  response = (
589
+ await self.client.table("trip_selections").update(payload).eq("id", selection_id).execute()
590
  )
591
  data = _response_data(response)
592
  return data[0] if isinstance(data, list) else data
593
+
594
+ async def count_trip_selections(self, trip_id: str) -> int:
595
+ response = await (
596
+ self.client.table("trip_selections")
597
+ .select("id", count="exact")
598
+ .eq("trip_id", trip_id)
599
+ .eq("status", "pending")
600
+ .execute()
601
+ )
602
+ return response.count if hasattr(response, "count") and response.count is not None else 0
app/services/conversation_service.py CHANGED
@@ -97,18 +97,18 @@ class ConversationService:
97
  embedding_model=self.settings.jina_embedding_model,
98
  current_message=current_message,
99
  )
100
- result = await handlers.create_booking_lead(
101
  {"trip_id": trip_id, "requested_seats": 1}
102
  )
103
  if result.ok:
104
  driver_phone = result.data.get("driver_phone")
105
  reply = (
106
- f"تم تأكيد الحجز! يمكنك التواصل مع السائق على الرقم: {driver_phone}"
107
  if driver_phone
108
- else "تم تأكيد الحجز! سيتم إشعار السائق."
109
  )
110
  else:
111
- reply = f"عذراً، لم يتم تأكيد الحجز: {result.error}"
112
  await self.whatsapp.send_text(inbound.remoteJid, reply)
113
  await self.repository.create_message(
114
  customer_id=str(customer["id"]),
 
97
  embedding_model=self.settings.jina_embedding_model,
98
  current_message=current_message,
99
  )
100
+ result = await handlers.select_trip(
101
  {"trip_id": trip_id, "requested_seats": 1}
102
  )
103
  if result.ok:
104
  driver_phone = result.data.get("driver_phone")
105
  reply = (
106
+ f"تم إرسال معلوماتك إلى السائق. يمكنك التواصل معه على الرقم: {driver_phone}"
107
  if driver_phone
108
+ else "تم إرسال معلوماتك إلى السائق. سيتم التواصل معك."
109
  )
110
  else:
111
+ reply = f"عذراً، لم يتم إرسال الطلب: {result.error}"
112
  await self.whatsapp.send_text(inbound.remoteJid, reply)
113
  await self.repository.create_message(
114
  customer_id=str(customer["id"]),
app/tools/handlers.py CHANGED
@@ -171,7 +171,10 @@ class FalsaToolHandlers:
171
  if top_trips:
172
  for trip_summary in top_trips:
173
  trip_id = trip_summary["trip_id"]
 
 
174
  trip = next((t for t in trips if (t.get("trip_id") or t.get("id")) == trip_id), {})
 
175
  card = format_trip_card(trip)
176
  try:
177
  resp = await self.whatsapp.send_text(self.remoteJid, card)
@@ -219,7 +222,7 @@ class FalsaToolHandlers:
219
  },
220
  )
221
 
222
- async def create_booking_lead(self, arguments: dict[str, Any]) -> ToolResult:
223
  trip_id = _optional_string(arguments.get("trip_id"))
224
  requested_seats = _optional_int(arguments.get("requested_seats")) or 1
225
  notes = _optional_string(arguments.get("notes"))
@@ -243,7 +246,7 @@ class FalsaToolHandlers:
243
  error="Not enough available seats",
244
  )
245
 
246
- lead = await self.repository.create_booking_lead(
247
  customer_id=str(self.customer["id"]),
248
  trip_id=trip_id,
249
  requested_seats=requested_seats,
@@ -273,8 +276,8 @@ class FalsaToolHandlers:
273
  notification_status = "failed"
274
  notification_error = str(exc)
275
 
276
- await self.repository.update_booking_lead_notification(
277
- lead_id=str(lead["id"]),
278
  status=notification_status,
279
  metadata={"error": notification_error} if notification_error else None,
280
  )
@@ -282,12 +285,12 @@ class FalsaToolHandlers:
282
  return ToolResult(
283
  ok=True,
284
  data={
285
- "lead_id": lead["id"],
286
  "status": "pending",
287
  "driver_notification_status": notification_status,
288
  "driver_notification_error": notification_error,
289
  "driver_phone": driver_phone,
290
- "message": "Booking lead created. Seats are not reserved until confirmed.",
291
  },
292
  )
293
 
@@ -1019,6 +1022,7 @@ def _trip_summary(trip: dict[str, Any], *, trip_number: int | None = None) -> di
1019
  "status": trip.get("status"),
1020
  "similarity": trip.get("similarity"),
1021
  "time_difference_minutes": trip.get("time_difference_minutes"),
 
1022
  }
1023
  if trip_number is not None:
1024
  summary["trip_number"] = trip_number
@@ -1076,12 +1080,12 @@ def _driver_notification_text(
1076
  notes: str | None,
1077
  ) -> str:
1078
  return (
1079
- "🔔 حجز جديد في فلسا\n"
1080
  f"العميل: {customer.get('name') or 'عميل جديد'}\n"
1081
  f"رقم العميل: {customer.get('phone_number') or 'غير متوفر'}\n"
1082
  f"الرحلة: {trip.get('departure')} ← {trip.get('destination')}\n"
1083
  f"التاريخ: {trip_departure_date(trip)} {trip_departure_bucket(trip)}\n"
1084
  f"المقاعد المطلوبة: {requested_seats}\n"
1085
  f"ملاحظات: {notes or 'لا يوجد'}\n"
1086
- "الحالة: قيد الانتظار"
1087
  )
 
171
  if top_trips:
172
  for trip_summary in top_trips:
173
  trip_id = trip_summary["trip_id"]
174
+ selection_count = await self.repository.count_trip_selections(trip_id)
175
+ trip_summary["selection_count"] = selection_count
176
  trip = next((t for t in trips if (t.get("trip_id") or t.get("id")) == trip_id), {})
177
+ trip["selection_count"] = selection_count
178
  card = format_trip_card(trip)
179
  try:
180
  resp = await self.whatsapp.send_text(self.remoteJid, card)
 
222
  },
223
  )
224
 
225
+ async def select_trip(self, arguments: dict[str, Any]) -> ToolResult:
226
  trip_id = _optional_string(arguments.get("trip_id"))
227
  requested_seats = _optional_int(arguments.get("requested_seats")) or 1
228
  notes = _optional_string(arguments.get("notes"))
 
246
  error="Not enough available seats",
247
  )
248
 
249
+ selection = await self.repository.create_trip_selection(
250
  customer_id=str(self.customer["id"]),
251
  trip_id=trip_id,
252
  requested_seats=requested_seats,
 
276
  notification_status = "failed"
277
  notification_error = str(exc)
278
 
279
+ await self.repository.update_selection_notification(
280
+ selection_id=str(selection["id"]),
281
  status=notification_status,
282
  metadata={"error": notification_error} if notification_error else None,
283
  )
 
285
  return ToolResult(
286
  ok=True,
287
  data={
288
+ "selection_id": selection["id"],
289
  "status": "pending",
290
  "driver_notification_status": notification_status,
291
  "driver_notification_error": notification_error,
292
  "driver_phone": driver_phone,
293
+ "message": "Customer interest recorded. Driver has been notified share their number so the two parties can coordinate directly.",
294
  },
295
  )
296
 
 
1022
  "status": trip.get("status"),
1023
  "similarity": trip.get("similarity"),
1024
  "time_difference_minutes": trip.get("time_difference_minutes"),
1025
+ "selection_count": trip.get("selection_count", 0),
1026
  }
1027
  if trip_number is not None:
1028
  summary["trip_number"] = trip_number
 
1080
  notes: str | None,
1081
  ) -> str:
1082
  return (
1083
+ "🔔 اهتمام جديد برحلة في فلسا\n"
1084
  f"العميل: {customer.get('name') or 'عميل جديد'}\n"
1085
  f"رقم العميل: {customer.get('phone_number') or 'غير متوفر'}\n"
1086
  f"الرحلة: {trip.get('departure')} ← {trip.get('destination')}\n"
1087
  f"التاريخ: {trip_departure_date(trip)} {trip_departure_bucket(trip)}\n"
1088
  f"المقاعد المطلوبة: {requested_seats}\n"
1089
  f"ملاحظات: {notes or 'لا يوجد'}\n"
1090
+ "يرجى التواصل مع العميل للاتفاق على الحجز"
1091
  )
app/whatsapp/trip_selection.py CHANGED
@@ -92,6 +92,7 @@ def format_trip_card(trip: dict[str, Any]) -> str:
92
  available = trip.get("available_seats") or 0
93
  total = trip.get("total_seats") or 0
94
  price = trip.get("price") or ""
 
95
 
96
  lines = [
97
  "─" * 14,
@@ -101,6 +102,7 @@ def format_trip_card(trip: dict[str, Any]) -> str:
101
  f"السعر: {price}" if price else "",
102
  f"السيارة: {car_type}" if car_type else "",
103
  f"السائق: {driver_name}" if driver_name else "",
 
104
  "─" * 14,
105
  ]
106
  return "\n".join(line for line in lines if line)
 
92
  available = trip.get("available_seats") or 0
93
  total = trip.get("total_seats") or 0
94
  price = trip.get("price") or ""
95
+ selection_count = trip.get("selection_count")
96
 
97
  lines = [
98
  "─" * 14,
 
102
  f"السعر: {price}" if price else "",
103
  f"السيارة: {car_type}" if car_type else "",
104
  f"السائق: {driver_name}" if driver_name else "",
105
+ f"عدد المهتمين: {selection_count}" if selection_count is not None else "",
106
  "─" * 14,
107
  ]
108
  return "\n".join(line for line in lines if line)
prompts/falsa_info.md CHANGED
@@ -1,23 +1,25 @@
1
  # FALSA Information Seed
2
 
3
  ## Company
4
- FALSA is an AI-powered travel booking customer service platform. Customers can ask
5
- about car and bus trips through WhatsApp, search available routes, and request a
6
- booking handoff to the driver or support team.
7
 
8
  ## How The Service Works
9
  Customers send a message on WhatsApp. FALSA checks available active trips, shares
10
- matching options, and creates a pending booking lead when the customer chooses a
11
- trip. Seats are not reserved until the driver or support team confirms the booking.
 
12
 
13
  ## Pricing
14
  Trip prices depend on route, vehicle type, seat availability, and driver pricing.
15
  FALSA should only share prices returned by the trip search results.
16
 
17
  ## Policies
18
- FALSA does not confirm a reservation or payment automatically in v1. Any booking
19
- lead remains pending until human or driver confirmation. Customers should provide
20
- accurate travel details, preferred time, number of seats, and pickup notes.
 
21
 
22
  ## Support
23
  If FALSA cannot find a suitable answer or trip, the assistant should collect the
 
1
  # FALSA Information Seed
2
 
3
  ## Company
4
+ FALSA is an AI-powered travel service platform on WhatsApp. Customers can ask
5
+ about car and bus trips through WhatsApp, search available routes, and express
6
+ interest in a trip — the driver and customer then coordinate directly.
7
 
8
  ## How The Service Works
9
  Customers send a message on WhatsApp. FALSA checks available active trips, shares
10
+ matching options, and records the customer's interest when they select a trip.
11
+ Seats are not reserved. The driver is notified and the customer receives the
12
+ driver's phone number to arrange the booking directly between themselves.
13
 
14
  ## Pricing
15
  Trip prices depend on route, vehicle type, seat availability, and driver pricing.
16
  FALSA should only share prices returned by the trip search results.
17
 
18
  ## Policies
19
+ FALSA does not handle reservations, confirmations, or payments. Customers select
20
+ a trip, receive the driver's contact, and coordinate directly with the driver.
21
+ Customers should provide accurate travel details, preferred time, number of seats,
22
+ and pickup notes.
23
 
24
  ## Support
25
  If FALSA cannot find a suitable answer or trip, the assistant should collect the
pyproject.toml CHANGED
@@ -1,7 +1,7 @@
1
  [project]
2
  name = "falsa"
3
  version = "0.1.0"
4
- description = "AI-powered WhatsApp travel booking customer service backend"
5
  requires-python = ">=3.12"
6
  dependencies = [
7
  "fastapi>=0.115.0",
 
1
  [project]
2
  name = "falsa"
3
  version = "0.1.0"
4
+ description = "AI-powered WhatsApp travel service backend"
5
  requires-python = ">=3.12"
6
  dependencies = [
7
  "fastapi>=0.115.0",
tests/conftest.py CHANGED
@@ -106,7 +106,7 @@ class FakeRepository:
106
  self.created_trips: list[dict[str, Any]] = []
107
  self.trip_embeddings: list[dict[str, Any]] = []
108
  self.messages: list[dict[str, Any]] = []
109
- self.booking_leads: list[dict[str, Any]] = []
110
  self.notification_updates: list[dict[str, Any]] = []
111
  self.trips_by_id: dict[str, dict[str, Any]] = {}
112
  self.active_search_results: list[dict[str, Any]] = []
@@ -277,7 +277,7 @@ class FakeRepository:
277
  match_count = int(kwargs.get("match_count") or 10)
278
  return self.trip_vector_search_results[:match_count]
279
 
280
- async def create_booking_lead(
281
  self,
282
  *,
283
  customer_id: str,
@@ -285,27 +285,30 @@ class FakeRepository:
285
  requested_seats: int,
286
  notes: str | None,
287
  ) -> dict[str, Any]:
288
- lead = {
289
- "id": f"lead-{len(self.booking_leads) + 1}",
290
  "customer_id": customer_id,
291
  "trip_id": trip_id,
292
  "requested_seats": requested_seats,
293
  "notes": notes,
294
  }
295
- self.booking_leads.append(lead)
296
- return lead
297
 
298
- async def update_booking_lead_notification(
299
  self,
300
  *,
301
- lead_id: str,
302
  status: str,
303
  metadata: dict[str, Any] | None = None,
304
  ) -> dict[str, Any]:
305
- update = {"lead_id": lead_id, "status": status, "metadata": metadata}
306
  self.notification_updates.append(update)
307
  return update
308
 
 
 
 
309
  async def get_driver_by_phone(self, remote_jid: str) -> dict[str, Any] | None:
310
  return self.drivers_by_remote_jid.get(remote_jid)
311
 
 
106
  self.created_trips: list[dict[str, Any]] = []
107
  self.trip_embeddings: list[dict[str, Any]] = []
108
  self.messages: list[dict[str, Any]] = []
109
+ self.trip_selections: list[dict[str, Any]] = []
110
  self.notification_updates: list[dict[str, Any]] = []
111
  self.trips_by_id: dict[str, dict[str, Any]] = {}
112
  self.active_search_results: list[dict[str, Any]] = []
 
277
  match_count = int(kwargs.get("match_count") or 10)
278
  return self.trip_vector_search_results[:match_count]
279
 
280
+ async def create_trip_selection(
281
  self,
282
  *,
283
  customer_id: str,
 
285
  requested_seats: int,
286
  notes: str | None,
287
  ) -> dict[str, Any]:
288
+ selection = {
289
+ "id": f"sel-{len(self.trip_selections) + 1}",
290
  "customer_id": customer_id,
291
  "trip_id": trip_id,
292
  "requested_seats": requested_seats,
293
  "notes": notes,
294
  }
295
+ self.trip_selections.append(selection)
296
+ return selection
297
 
298
+ async def update_selection_notification(
299
  self,
300
  *,
301
+ selection_id: str,
302
  status: str,
303
  metadata: dict[str, Any] | None = None,
304
  ) -> dict[str, Any]:
305
+ update = {"selection_id": selection_id, "status": status, "metadata": metadata}
306
  self.notification_updates.append(update)
307
  return update
308
 
309
+ async def count_trip_selections(self, trip_id: str) -> int:
310
+ return sum(1 for s in self.trip_selections if s["trip_id"] == trip_id)
311
+
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
 
tests/test_conversation_service.py CHANGED
@@ -117,7 +117,7 @@ async def test_conversation_uses_passenger_tools_when_user_mode_is_passenger(set
117
  assert tool_names == {
118
  "about_falsa",
119
  "search_trips",
120
- "create_booking_lead",
121
  "create_driver_account",
122
  "switch_to_driver",
123
  }
 
117
  assert tool_names == {
118
  "about_falsa",
119
  "search_trips",
120
+ "select_trip",
121
  "create_driver_account",
122
  "switch_to_driver",
123
  }
tests/test_tool_schemas.py CHANGED
@@ -14,7 +14,7 @@ def test_new_user_tools_are_onboarding_only():
14
  def test_driver_tools_exclude_passenger_booking_tools():
15
  names = {schema["function"]["name"] for schema in get_tool_schemas("driver")}
16
  assert "search_trips" not in names
17
- assert "create_booking_lead" not in names
18
  assert "initiate_trip_action" in names
19
  assert "update_trip_field" in names
20
  assert "switch_to_passenger" in names
 
14
  def test_driver_tools_exclude_passenger_booking_tools():
15
  names = {schema["function"]["name"] for schema in get_tool_schemas("driver")}
16
  assert "search_trips" not in names
17
+ assert "select_trip" not in names
18
  assert "initiate_trip_action" in names
19
  assert "update_trip_field" in names
20
  assert "switch_to_passenger" in names
tests/test_tools.py CHANGED
@@ -222,7 +222,7 @@ async def test_search_trips_includes_alternate_alert_when_first_result_is_over_o
222
 
223
 
224
  @pytest.mark.asyncio
225
- async def test_create_booking_lead_notifies_driver():
226
  repository = FakeRepository()
227
  repository.trips_by_id["trip-1"] = trip()
228
  whatsapp = FakeWhatsApp()
@@ -232,18 +232,18 @@ async def test_create_booking_lead_notifies_driver():
232
  customer={"id": "cust-1", "remoteJid": "967700000001", "name": "Mona"},
233
  )
234
 
235
- result = await handlers.create_booking_lead(
236
  {"trip_id": "trip-1", "requested_seats": 2, "notes": "Window seat"}
237
  )
238
 
239
  assert result.ok is True
240
  assert result.data["driver_notification_status"] == "sent"
241
- assert repository.booking_leads[0]["requested_seats"] == 2
242
  assert whatsapp.sent[0][0] == "967700000009"
243
 
244
 
245
  @pytest.mark.asyncio
246
- async def test_create_booking_lead_uses_driver_phone_number():
247
  repository = FakeRepository()
248
  repository.trips_by_id["trip-1"] = trip(driver_phone_number="967700000099")
249
  whatsapp = FakeWhatsApp()
@@ -253,7 +253,7 @@ async def test_create_booking_lead_uses_driver_phone_number():
253
  customer={"id": "cust-1", "remoteJid": "967700000001", "name": "Mona"},
254
  )
255
 
256
- result = await handlers.create_booking_lead(
257
  {"trip_id": "trip-1", "requested_seats": 1}
258
  )
259
 
@@ -264,7 +264,7 @@ async def test_create_booking_lead_uses_driver_phone_number():
264
 
265
 
266
  @pytest.mark.asyncio
267
- async def test_create_booking_lead_keeps_pending_when_driver_notification_fails():
268
  repository = FakeRepository()
269
  repository.trips_by_id["trip-1"] = trip()
270
  handlers = make_handlers(
@@ -272,7 +272,7 @@ async def test_create_booking_lead_keeps_pending_when_driver_notification_fails(
272
  whatsapp=FakeWhatsApp(fail=True),
273
  )
274
 
275
- result = await handlers.create_booking_lead({"trip_id": "trip-1", "requested_seats": 1})
276
 
277
  assert result.ok is True
278
  assert result.data["status"] == "pending"
@@ -281,16 +281,16 @@ async def test_create_booking_lead_keeps_pending_when_driver_notification_fails(
281
 
282
 
283
  @pytest.mark.asyncio
284
- async def test_create_booking_lead_rejects_insufficient_seats():
285
  repository = FakeRepository()
286
  repository.trips_by_id["trip-1"] = trip(available_seats=1)
287
  handlers = make_handlers(repository=repository)
288
 
289
- result = await handlers.create_booking_lead({"trip_id": "trip-1", "requested_seats": 2})
290
 
291
  assert result.ok is False
292
  assert result.error == "Not enough available seats"
293
- assert repository.booking_leads == []
294
 
295
 
296
  @pytest.mark.asyncio
 
222
 
223
 
224
  @pytest.mark.asyncio
225
+ async def test_select_trip_notifies_driver():
226
  repository = FakeRepository()
227
  repository.trips_by_id["trip-1"] = trip()
228
  whatsapp = FakeWhatsApp()
 
232
  customer={"id": "cust-1", "remoteJid": "967700000001", "name": "Mona"},
233
  )
234
 
235
+ result = await handlers.select_trip(
236
  {"trip_id": "trip-1", "requested_seats": 2, "notes": "Window seat"}
237
  )
238
 
239
  assert result.ok is True
240
  assert result.data["driver_notification_status"] == "sent"
241
+ assert repository.trip_selections[0]["requested_seats"] == 2
242
  assert whatsapp.sent[0][0] == "967700000009"
243
 
244
 
245
  @pytest.mark.asyncio
246
+ async def test_select_trip_uses_driver_phone_number():
247
  repository = FakeRepository()
248
  repository.trips_by_id["trip-1"] = trip(driver_phone_number="967700000099")
249
  whatsapp = FakeWhatsApp()
 
253
  customer={"id": "cust-1", "remoteJid": "967700000001", "name": "Mona"},
254
  )
255
 
256
+ result = await handlers.select_trip(
257
  {"trip_id": "trip-1", "requested_seats": 1}
258
  )
259
 
 
264
 
265
 
266
  @pytest.mark.asyncio
267
+ async def test_select_trip_keeps_pending_when_driver_notification_fails():
268
  repository = FakeRepository()
269
  repository.trips_by_id["trip-1"] = trip()
270
  handlers = make_handlers(
 
272
  whatsapp=FakeWhatsApp(fail=True),
273
  )
274
 
275
+ result = await handlers.select_trip({"trip_id": "trip-1", "requested_seats": 1})
276
 
277
  assert result.ok is True
278
  assert result.data["status"] == "pending"
 
281
 
282
 
283
  @pytest.mark.asyncio
284
+ async def test_select_trip_rejects_insufficient_seats():
285
  repository = FakeRepository()
286
  repository.trips_by_id["trip-1"] = trip(available_seats=1)
287
  handlers = make_handlers(repository=repository)
288
 
289
+ result = await handlers.select_trip({"trip_id": "trip-1", "requested_seats": 2})
290
 
291
  assert result.ok is False
292
  assert result.error == "Not enough available seats"
293
+ assert repository.trip_selections == []
294
 
295
 
296
  @pytest.mark.asyncio