codeBOKER commited on
Commit
64075d7
·
1 Parent(s): 73d7170

Fix trip seat filtering and enable null seats display

Browse files

- Fix search_active_trips to properly handle null seats, using 'available_seats.gte.{seats},available_seats.is.null' instead of malformed 'available_seats.is.null,available_seats.gte.{seats}'
- Update list_active_trips to use 'available_seats.is.null,available_seats.gt.0' instead of 'gt(0)'
- Allow null seats in search results (trips without seat info)
- Make seats fields nullable in database (new migration added)
- Update trip_selection to display 'المقاعد: غير متوفرة' when seats are null
- Update test fixtures and handlers to properly handle null seats

This fixes the issue where trips with null seats were showing as '1 of 1' instead of 'المقاعد: غير متوفرة'

app/database/supabase.py CHANGED
@@ -141,8 +141,8 @@ class SupabaseRepository:
141
  destination: str,
142
  departure_date: Any,
143
  departure_time: str,
144
- available_seats: int,
145
- total_seats: int,
146
  price: float,
147
  ) -> dict[str, Any]:
148
  customer = await self.upsert_customer(
@@ -181,8 +181,8 @@ class SupabaseRepository:
181
  destination: str,
182
  departure_date: Any,
183
  departure_time: str,
184
- available_seats: int,
185
- total_seats: int,
186
  price: float,
187
  ) -> dict[str, Any]:
188
  if not driver_id:
@@ -387,7 +387,7 @@ class SupabaseRepository:
387
  self.client.table("driver_trips")
388
  .select("*, drivers(*, customers(*)), driver_cars(*)")
389
  .eq("status", "active")
390
- .gt("available_seats", 0)
391
  )
392
  query = (
393
  self._apply_not_departed_filter(query)
@@ -422,7 +422,7 @@ class SupabaseRepository:
422
  self.client.table("driver_trips")
423
  .select("*, drivers(*, customers(*)), driver_cars(*)")
424
  .eq("status", "active")
425
- .gt("available_seats", 0)
426
  )
427
  query = self._apply_departure_request_filter(query, departure_request)
428
  query = query.order("departure_date").order("departure_time")
@@ -436,7 +436,7 @@ class SupabaseRepository:
436
  return []
437
  query = query.in_("driver_id", driver_ids)
438
  if seats:
439
- query = query.gte("available_seats", seats)
440
  if vehicle_type:
441
  query = query.ilike("driver_cars.car_type", f"%{vehicle_type}%")
442
  response = await query.limit(10).execute()
@@ -747,26 +747,28 @@ class SupabaseRepository:
747
  destination: str,
748
  departure_date: date,
749
  departure_time: str,
750
- available_seats: int,
751
- total_seats: int,
752
  price: float,
753
  driver_message: str | None = None,
754
  use_driver_message: bool = False,
755
  ) -> dict[str, Any]:
756
- payload = {
757
  "driver_id": driver_id,
758
  "car_id": car_id,
759
  "departure": departure,
760
  "destination": destination,
761
  "departure_date": departure_date.isoformat(),
762
  "departure_time": departure_time,
763
- "available_seats": available_seats,
764
- "total_seats": total_seats,
765
  "price": price,
766
  "status": "active",
767
  "driver_message": driver_message,
768
  "use_driver_message": use_driver_message,
769
  }
 
 
 
 
770
  response = await self.client.table("driver_trips").insert(payload).execute()
771
  data = _response_data(response)
772
  trip = data[0] if isinstance(data, list) else data
 
141
  destination: str,
142
  departure_date: Any,
143
  departure_time: str,
144
+ available_seats: int | None,
145
+ total_seats: int | None,
146
  price: float,
147
  ) -> dict[str, Any]:
148
  customer = await self.upsert_customer(
 
181
  destination: str,
182
  departure_date: Any,
183
  departure_time: str,
184
+ available_seats: int | None,
185
+ total_seats: int | None,
186
  price: float,
187
  ) -> dict[str, Any]:
188
  if not driver_id:
 
387
  self.client.table("driver_trips")
388
  .select("*, drivers(*, customers(*)), driver_cars(*)")
389
  .eq("status", "active")
390
+ .or_("available_seats.is.null,available_seats.gt.0")
391
  )
392
  query = (
393
  self._apply_not_departed_filter(query)
 
422
  self.client.table("driver_trips")
423
  .select("*, drivers(*, customers(*)), driver_cars(*)")
424
  .eq("status", "active")
425
+ .or_("available_seats.is.null,available_seats.gt.0")
426
  )
427
  query = self._apply_departure_request_filter(query, departure_request)
428
  query = query.order("departure_date").order("departure_time")
 
436
  return []
437
  query = query.in_("driver_id", driver_ids)
438
  if seats:
439
+ query = query.or_(f"available_seats.gte.{seats},available_seats.is.null")
440
  if vehicle_type:
441
  query = query.ilike("driver_cars.car_type", f"%{vehicle_type}%")
442
  response = await query.limit(10).execute()
 
747
  destination: str,
748
  departure_date: date,
749
  departure_time: str,
750
+ available_seats: int | None,
751
+ total_seats: int | None,
752
  price: float,
753
  driver_message: str | None = None,
754
  use_driver_message: bool = False,
755
  ) -> dict[str, Any]:
756
+ payload: dict[str, Any] = {
757
  "driver_id": driver_id,
758
  "car_id": car_id,
759
  "departure": departure,
760
  "destination": destination,
761
  "departure_date": departure_date.isoformat(),
762
  "departure_time": departure_time,
 
 
763
  "price": price,
764
  "status": "active",
765
  "driver_message": driver_message,
766
  "use_driver_message": use_driver_message,
767
  }
768
+ if available_seats is not None:
769
+ payload["available_seats"] = available_seats
770
+ if total_seats is not None:
771
+ payload["total_seats"] = total_seats
772
  response = await self.client.table("driver_trips").insert(payload).execute()
773
  data = _response_data(response)
774
  trip = data[0] if isinstance(data, list) else data
app/services/group_message_service.py CHANGED
@@ -120,8 +120,8 @@ class GroupMessageService:
120
  destination=extracted.destination,
121
  departure_date=departure_date,
122
  departure_time=departure_time,
123
- available_seats=extracted.available_seats or 1,
124
- total_seats=extracted.total_seats or extracted.available_seats or 1,
125
  price=extracted.price or 0,
126
  )
127
  else:
@@ -133,8 +133,8 @@ class GroupMessageService:
133
  destination=extracted.destination,
134
  departure_date=departure_date,
135
  departure_time=departure_time,
136
- available_seats=extracted.available_seats or 1,
137
- total_seats=extracted.total_seats or extracted.available_seats or 1,
138
  price=extracted.price or 0,
139
  )
140
 
 
120
  destination=extracted.destination,
121
  departure_date=departure_date,
122
  departure_time=departure_time,
123
+ available_seats=extracted.available_seats,
124
+ total_seats=extracted.total_seats,
125
  price=extracted.price or 0,
126
  )
127
  else:
 
133
  destination=extracted.destination,
134
  departure_date=departure_date,
135
  departure_time=departure_time,
136
+ available_seats=extracted.available_seats,
137
+ total_seats=extracted.total_seats,
138
  price=extracted.price or 0,
139
  )
140
 
app/services/trip_indexing.py CHANGED
@@ -11,10 +11,12 @@ def build_trip_embedding_record(trip: dict[str, Any], embedding_model: str) -> d
11
  trip_id = str(trip.get("id") or trip.get("trip_id"))
12
  departure_date = trip_departure_date(trip)
13
  departure_time = trip_departure_bucket(trip)
 
 
14
  chunk_text = (
15
  f"Trip {trip_id}: {trip.get('departure')} to {trip.get('destination')} "
16
  f"on {departure_date} during {departure_time}. "
17
- f"Available seats: {trip.get('available_seats')} of {trip.get('total_seats')}. "
18
  f"Vehicle: {car.get('car_type')}. Driver: {driver.get('name')}. "
19
  f"Price: {trip.get('price')}. Status: {trip.get('status')}."
20
  )
 
11
  trip_id = str(trip.get("id") or trip.get("trip_id"))
12
  departure_date = trip_departure_date(trip)
13
  departure_time = trip_departure_bucket(trip)
14
+ available = trip.get("available_seats") or "unknown"
15
+ total = trip.get("total_seats") or "unknown"
16
  chunk_text = (
17
  f"Trip {trip_id}: {trip.get('departure')} to {trip.get('destination')} "
18
  f"on {departure_date} during {departure_time}. "
19
+ f"Available seats: {available} of {total}. "
20
  f"Vehicle: {car.get('car_type')}. Driver: {driver.get('name')}. "
21
  f"Price: {trip.get('price')}. Status: {trip.get('status')}."
22
  )
app/tools/handlers.py CHANGED
@@ -248,7 +248,8 @@ class FalzhToolHandlers:
248
  return ToolResult(ok=False, data={}, error="Trip was not found")
249
  if trip.get("status") != "active":
250
  return ToolResult(ok=False, data={}, error="Trip is not active")
251
- if int(trip.get("available_seats") or 0) < requested_seats:
 
252
  return ToolResult(
253
  ok=False,
254
  data={"available_seats": trip.get("available_seats")},
@@ -555,10 +556,6 @@ class FalzhToolHandlers:
555
  if price is None and latest_trip is not None:
556
  price = _optional_price(latest_trip.get("price"))
557
 
558
- if total_seats is None:
559
- total_seats = 4
560
- if available_seats is None:
561
- available_seats = total_seats
562
  if price is None:
563
  price = 0
564
 
@@ -578,11 +575,11 @@ class FalzhToolHandlers:
578
  error="No registered vehicle found for this driver",
579
  )
580
 
581
- if total_seats is None or total_seats < 1:
582
  return ToolResult(ok=False, data={}, error="total_seats must be at least 1")
583
- if available_seats is None or available_seats < 0:
584
  return ToolResult(ok=False, data={}, error="available_seats must be at least 0")
585
- if available_seats > total_seats:
586
  return ToolResult(
587
  ok=False,
588
  data={},
@@ -966,7 +963,7 @@ def _is_trip_match(
966
  ) -> bool:
967
  if trip.get("status") != "active":
968
  return False
969
- if int(trip.get("available_seats") or 0) < seats:
970
  return False
971
  if departure and departure.lower() not in str(trip.get("departure") or "").lower():
972
  return False
 
248
  return ToolResult(ok=False, data={}, error="Trip was not found")
249
  if trip.get("status") != "active":
250
  return ToolResult(ok=False, data={}, error="Trip is not active")
251
+ available_seats = trip.get("available_seats")
252
+ if available_seats is not None and int(available_seats) < requested_seats:
253
  return ToolResult(
254
  ok=False,
255
  data={"available_seats": trip.get("available_seats")},
 
556
  if price is None and latest_trip is not None:
557
  price = _optional_price(latest_trip.get("price"))
558
 
 
 
 
 
559
  if price is None:
560
  price = 0
561
 
 
575
  error="No registered vehicle found for this driver",
576
  )
577
 
578
+ if total_seats is not None and total_seats < 1:
579
  return ToolResult(ok=False, data={}, error="total_seats must be at least 1")
580
+ if available_seats is not None and available_seats < 0:
581
  return ToolResult(ok=False, data={}, error="available_seats must be at least 0")
582
+ if total_seats is not None and available_seats is not None and available_seats > total_seats:
583
  return ToolResult(
584
  ok=False,
585
  data={},
 
963
  ) -> bool:
964
  if trip.get("status") != "active":
965
  return False
966
+ if trip.get("available_seats") is not None and int(trip["available_seats"]) < seats:
967
  return False
968
  if departure and departure.lower() not in str(trip.get("departure") or "").lower():
969
  return False
app/whatsapp/trip_selection.py CHANGED
@@ -24,16 +24,21 @@ def format_trip_card(trip: dict[str, Any]) -> str:
24
  driver_name = driver.get("name") or trip.get("driver_name") or ""
25
  car_type = car.get("car_type") or trip.get("car_type") or ""
26
 
27
- available = trip.get("available_seats") or 0
28
- total = trip.get("total_seats") or 0
29
  price = trip.get("price") or ""
30
  selection_count = trip.get("selection_count")
31
 
 
 
 
 
 
32
  lines = [
33
  "─" * 14,
34
  f"من: {departure} ← إلى: {destination}",
35
  f"التاريخ: {date_text} | الوقت: {bucket_text}",
36
- f"المقاعد: {available} من {total} متاحة",
37
  f"السعر: {price}" if price else "",
38
  f"السيارة: {car_type}" if car_type else "",
39
  f"السائق: {driver_name}" if driver_name else "",
 
24
  driver_name = driver.get("name") or trip.get("driver_name") or ""
25
  car_type = car.get("car_type") or trip.get("car_type") or ""
26
 
27
+ available = trip.get("available_seats")
28
+ total = trip.get("total_seats")
29
  price = trip.get("price") or ""
30
  selection_count = trip.get("selection_count")
31
 
32
+ if available is not None and total is not None:
33
+ seats_line = f"المقاعد: {available} من {total} متاحة"
34
+ else:
35
+ seats_line = "المقاعد: غير متوفرة"
36
+
37
  lines = [
38
  "─" * 14,
39
  f"من: {departure} ← إلى: {destination}",
40
  f"التاريخ: {date_text} | الوقت: {bucket_text}",
41
+ seats_line,
42
  f"السعر: {price}" if price else "",
43
  f"السيارة: {car_type}" if car_type else "",
44
  f"السائق: {driver_name}" if driver_name else "",
supabase/migrations/202607210001_nullable_seats.sql ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Make available_seats and total_seats nullable on driver_trips.
2
+ -- Group-message trips often lack seat info; store NULL instead of defaulting.
3
+
4
+ ALTER TABLE public.driver_trips
5
+ ALTER COLUMN available_seats DROP NOT NULL,
6
+ ALTER COLUMN total_seats DROP NOT NULL;
7
+
8
+ -- Replace the check constraint so it only enforces when both values are present
9
+ ALTER TABLE public.driver_trips
10
+ DROP CONSTRAINT IF EXISTS driver_trips_available_not_over_total;
11
+
12
+ ALTER TABLE public.driver_trips
13
+ ADD CONSTRAINT driver_trips_available_not_over_total
14
+ CHECK (available_seats IS NULL OR total_seats IS NULL OR available_seats <= total_seats);
15
+
16
+ -- Drop BOTH overloads of match_active_trips and recreate with NULL-safe seats filter
17
+ DROP FUNCTION IF EXISTS public.match_active_trips(
18
+ extensions.vector(1024), float, int, text, text, date, text, time, int, text
19
+ );
20
+ DROP FUNCTION IF EXISTS public.match_active_trips(
21
+ extensions.vector(1024), float, int, text, text, text, date, text, time, int, text
22
+ );
23
+
24
+ -- Recreate with filter_driver_name (the version the code actually calls)
25
+ CREATE OR REPLACE FUNCTION public.match_active_trips(
26
+ query_embedding extensions.vector(1024),
27
+ match_threshold float DEFAULT 0.0,
28
+ match_count int DEFAULT 10,
29
+ filter_departure text DEFAULT NULL,
30
+ filter_destination text DEFAULT NULL,
31
+ filter_driver_name text DEFAULT NULL,
32
+ filter_departure_date date DEFAULT NULL,
33
+ filter_departure_time text DEFAULT NULL,
34
+ filter_requested_time time DEFAULT NULL,
35
+ filter_seats int DEFAULT 1,
36
+ filter_vehicle_type text DEFAULT NULL
37
+ )
38
+ RETURNS TABLE (
39
+ trip_id uuid,
40
+ departure text,
41
+ destination text,
42
+ departure_date date,
43
+ departure_time text,
44
+ available_seats integer,
45
+ total_seats integer,
46
+ price numeric,
47
+ status text,
48
+ driver_name text,
49
+ driver_phone_number text,
50
+ car_type text,
51
+ chunk_text text,
52
+ similarity float,
53
+ time_difference_minutes integer,
54
+ registered boolean
55
+ )
56
+ LANGUAGE sql STABLE
57
+ AS $$
58
+ WITH ranked AS (
59
+ SELECT
60
+ driver_trips.id AS trip_id,
61
+ driver_trips.departure,
62
+ driver_trips.destination,
63
+ driver_trips.departure_date,
64
+ driver_trips.departure_time,
65
+ driver_trips.available_seats,
66
+ driver_trips.total_seats,
67
+ driver_trips.price,
68
+ driver_trips.status,
69
+ customers.name AS driver_name,
70
+ customers."remoteJid" AS driver_phone_number,
71
+ driver_cars.car_type,
72
+ driver_trip_embeddings.chunk_text,
73
+ COALESCE(customers.registered, false) AS registered,
74
+ 1 - (driver_trip_embeddings.embedding <=> query_embedding) AS similarity,
75
+ driver_trip_embeddings.embedding <=> query_embedding AS vector_distance,
76
+ CASE
77
+ WHEN filter_requested_time IS NULL THEN NULL
78
+ ELSE abs(
79
+ extract(epoch FROM (
80
+ public.departure_bucket_clock_time(driver_trips.departure_time)
81
+ - filter_requested_time
82
+ )) / 60
83
+ )::integer
84
+ END AS time_difference_minutes
85
+ FROM public.driver_trip_embeddings
86
+ JOIN public.driver_trips ON driver_trips.id = driver_trip_embeddings.trip_id
87
+ LEFT JOIN public.drivers ON drivers.id = driver_trips.driver_id
88
+ LEFT JOIN public.customers ON customers.id = drivers.customer_id
89
+ LEFT JOIN public.driver_cars ON driver_cars.id = driver_trips.car_id
90
+ WHERE driver_trips.status = 'active'
91
+ AND (driver_trips.available_seats IS NULL OR driver_trips.available_seats >= COALESCE(filter_seats, 1))
92
+ AND (filter_departure IS NULL OR driver_trips.departure ILIKE '%' || filter_departure || '%')
93
+ AND (filter_destination IS NULL OR driver_trips.destination ILIKE '%' || filter_destination || '%')
94
+ AND (filter_driver_name IS NULL OR customers.name ILIKE '%' || filter_driver_name || '%')
95
+ AND (filter_departure_date IS NULL OR driver_trips.departure_date = filter_departure_date)
96
+ AND (filter_departure_time IS NULL OR driver_trips.departure_time = filter_departure_time)
97
+ AND (filter_vehicle_type IS NULL OR driver_cars.car_type ILIKE '%' || filter_vehicle_type || '%')
98
+ AND (
99
+ driver_trips.departure_date > (NOW() AT TIME ZONE 'Asia/Aden')::date
100
+ OR (
101
+ driver_trips.departure_date = (NOW() AT TIME ZONE 'Asia/Aden')::date
102
+ AND (
103
+ (NOW() AT TIME ZONE 'Asia/Aden')::time < TIME '12:00'
104
+ OR (
105
+ (NOW() AT TIME ZONE 'Asia/Aden')::time < TIME '18:00'
106
+ AND driver_trips.departure_time IN ('noon', 'night')
107
+ )
108
+ OR (
109
+ (NOW() AT TIME ZONE 'Asia/Aden')::time >= TIME '18:00'
110
+ AND driver_trips.departure_time = 'night'
111
+ )
112
+ )
113
+ )
114
+ )
115
+ )
116
+ SELECT
117
+ ranked.trip_id,
118
+ ranked.departure,
119
+ ranked.destination,
120
+ ranked.departure_date,
121
+ ranked.departure_time,
122
+ ranked.available_seats,
123
+ ranked.total_seats,
124
+ ranked.price,
125
+ ranked.status,
126
+ ranked.driver_name,
127
+ ranked.driver_phone_number,
128
+ ranked.car_type,
129
+ ranked.chunk_text,
130
+ ranked.similarity,
131
+ ranked.time_difference_minutes,
132
+ ranked.registered
133
+ FROM ranked
134
+ WHERE ranked.similarity >= match_threshold
135
+ ORDER BY
136
+ ranked.registered DESC,
137
+ ranked.time_difference_minutes NULLS LAST,
138
+ ranked.departure_date,
139
+ ranked.vector_distance
140
+ LIMIT match_count;
141
+ $$;
tests/conftest.py CHANGED
@@ -223,8 +223,8 @@ class FakeRepository:
223
  destination: str,
224
  departure_date: Any,
225
  departure_time: str,
226
- available_seats: int,
227
- total_seats: int,
228
  price: float,
229
  ) -> dict[str, Any]:
230
  customer = await self.upsert_customer(
@@ -263,8 +263,8 @@ class FakeRepository:
263
  destination: str,
264
  departure_date: Any,
265
  departure_time: str,
266
- available_seats: int,
267
- total_seats: int,
268
  price: float,
269
  ) -> dict[str, Any]:
270
  if not driver_id:
 
223
  destination: str,
224
  departure_date: Any,
225
  departure_time: str,
226
+ available_seats: int | None,
227
+ total_seats: int | None,
228
  price: float,
229
  ) -> dict[str, Any]:
230
  customer = await self.upsert_customer(
 
263
  destination: str,
264
  departure_date: Any,
265
  departure_time: str,
266
+ available_seats: int | None,
267
+ total_seats: int | None,
268
  price: float,
269
  ) -> dict[str, Any]:
270
  if not driver_id:
tests/test_group_message_service.py CHANGED
@@ -669,3 +669,27 @@ async def test_dm_with_multi_phone_merges_into_existing(settings: Settings) -> N
669
  assert customer1["id"] == customer2["id"]
670
  assert customer2["remoteJid"] == "967712345678"
671
  assert customer2["registered"] is True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
  assert customer1["id"] == customer2["id"]
670
  assert customer2["remoteJid"] == "967712345678"
671
  assert customer2["registered"] is True
672
+
673
+
674
+ @pytest.mark.asyncio
675
+ async def test_trip_ad_with_null_seats_stores_none(settings: Settings) -> None:
676
+ repo = FakeRepository()
677
+ embeddings = FakeEmbeddings()
678
+ provider = FakeProvider(
679
+ response_content=_trip_ad_response(available_seats=None, total_seats=None)
680
+ )
681
+
682
+ service = GroupMessageService(
683
+ repository=repo,
684
+ embeddings=embeddings,
685
+ ai=provider,
686
+ settings=settings,
687
+ )
688
+
689
+ inbound = _make_inbound(text="رحلة من صنعاء إلى عدن")
690
+ await service.handle_group_message(inbound)
691
+
692
+ assert len(repo.created_trips) == 1
693
+ trip = repo.created_trips[0]
694
+ assert trip.get("available_seats") is None
695
+ assert trip.get("total_seats") is None
tests/test_tools.py CHANGED
@@ -587,7 +587,7 @@ async def test_add_trip_by_driver_creates_trip_without_optional_fields():
587
  assert result.ok is True
588
  assert repository.created_trips[0]["car_id"] == "car-1"
589
  assert repository.created_trips[0]["total_seats"] == 4
590
- assert repository.created_trips[0]["available_seats"] == 4
591
  assert repository.created_trips[0]["price"] == 0
592
 
593
 
 
587
  assert result.ok is True
588
  assert repository.created_trips[0]["car_id"] == "car-1"
589
  assert repository.created_trips[0]["total_seats"] == 4
590
+ assert repository.created_trips[0]["available_seats"] is None
591
  assert repository.created_trips[0]["price"] == 0
592
 
593
 
tests/test_trip_selection.py CHANGED
@@ -83,3 +83,21 @@ def test_format_trip_card_driver_message_empty():
83
  }
84
  card = format_trip_card(trip)
85
  assert "صنعاء ← إلى" in card
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  }
84
  card = format_trip_card(trip)
85
  assert "صنعاء ← إلى" in card
86
+
87
+
88
+ def test_format_trip_card_null_seats():
89
+ trip = {
90
+ "id": "trip-1",
91
+ "departure": "صنعاء",
92
+ "destination": "تعز",
93
+ "departure_date": "2026-12-01",
94
+ "departure_time": "noon",
95
+ "available_seats": None,
96
+ "total_seats": None,
97
+ "price": 5000,
98
+ "driver_cars": [{"car_type": "باص"}],
99
+ "drivers": [{"name": "أحمد"}],
100
+ }
101
+ card = format_trip_card(trip)
102
+ assert "غير متوفرة" in card
103
+ assert "المقاعد:" in card