| import logging |
| from datetime import date, time |
| from typing import Any |
|
|
| from app.config import Settings |
| from app.utils.departure import ( |
| DepartureRequest, |
| not_departed_bucket_filter, |
| ) |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| async def create_supabase_client(settings: Settings) -> Any: |
| from supabase import acreate_client |
|
|
| return await acreate_client( |
| str(settings.supabase_url), |
| settings.supabase_service_role_key, |
| ) |
|
|
|
|
| def _response_data(response: Any) -> Any: |
| if hasattr(response, "data"): |
| return response.data |
| if isinstance(response, dict): |
| return response.get("data", response) |
| return response |
|
|
|
|
| class SupabaseRepository: |
| def __init__(self, client: Any) -> None: |
| self.client = client |
|
|
| async def upsert_customer( |
| self, |
| *, |
| remote_jid: str, |
| name: str | None = None, |
| preferred_language: str | None = None, |
| phone_number: str | None = None, |
| ) -> dict[str, Any]: |
| if phone_number: |
| phone_number = phone_number.split("@")[0] |
| payload = { |
| "remoteJid": remote_jid, |
| "name": name, |
| "preferred_language": preferred_language, |
| "phone_number": phone_number, |
| } |
| payload = {key: value for key, value in payload.items() if value is not None} |
| response = await ( |
| self.client.table("customers") |
| .upsert(payload, on_conflict="remoteJid") |
| .execute() |
| ) |
| data = _response_data(response) |
| return data[0] if isinstance(data, list) else data |
|
|
| async def update_customer_user_mode( |
| self, |
| *, |
| customer_id: str, |
| user_mode: str, |
| ) -> dict[str, Any]: |
| response = await ( |
| self.client.table("customers") |
| .update({"user_mode": user_mode}) |
| .eq("id", customer_id) |
| .execute() |
| ) |
| data = _response_data(response) |
| return data[0] if isinstance(data, list) else data |
|
|
| async def update_customer_name( |
| self, |
| *, |
| customer_id: str, |
| name: str, |
| ) -> dict[str, Any]: |
| response = await ( |
| self.client.table("customers") |
| .update({"name": name}) |
| .eq("id", customer_id) |
| .execute() |
| ) |
| data = _response_data(response) |
| return data[0] if isinstance(data, list) else data |
|
|
| async def get_customer_session(self, customer_id: str) -> dict[str, Any]: |
| response = await ( |
| self.client.table("customers") |
| .select("session_data") |
| .eq("id", customer_id) |
| .maybe_single() |
| .execute() |
| ) |
| data = _response_data(response) or {} |
| session_data = data.get("session_data") |
| return session_data if isinstance(session_data, dict) else {} |
|
|
| async def update_customer_session( |
| self, |
| *, |
| customer_id: str, |
| session_data: dict[str, Any], |
| ) -> dict[str, Any]: |
| response = await ( |
| self.client.table("customers") |
| .update({"session_data": session_data}) |
| .eq("id", customer_id) |
| .execute() |
| ) |
| data = _response_data(response) |
| return data[0] if isinstance(data, list) else data |
|
|
| async def set_customer_session_field( |
| self, |
| *, |
| customer_id: str, |
| key: str, |
| value: Any, |
| ) -> dict[str, Any]: |
| session_data = await self.get_customer_session(customer_id) |
| session_data[key] = value |
| return await self.update_customer_session( |
| customer_id=customer_id, |
| session_data=session_data, |
| ) |
|
|
| async def clear_customer_session_field( |
| self, |
| *, |
| customer_id: str, |
| key: str, |
| ) -> dict[str, Any]: |
| session_data = await self.get_customer_session(customer_id) |
| session_data.pop(key, None) |
| return await self.update_customer_session( |
| customer_id=customer_id, |
| session_data=session_data, |
| ) |
|
|
| async def message_exists(self, whatsapp_message_id: str) -> bool: |
| response = await ( |
| self.client.table("messages") |
| .select("id") |
| .eq("whatsapp_message_id", whatsapp_message_id) |
| .limit(1) |
| .execute() |
| ) |
| data = _response_data(response) |
| return bool(data) |
|
|
| async def create_message( |
| self, |
| *, |
| customer_id: str, |
| sender_type: str, |
| message: str, |
| whatsapp_message_id: str | None = None, |
| metadata: dict[str, Any] | None = None, |
| ) -> dict[str, Any]: |
| payload = { |
| "customer_id": customer_id, |
| "sender_type": sender_type, |
| "message": message, |
| "whatsapp_message_id": whatsapp_message_id, |
| "metadata": metadata or {}, |
| } |
| response = await self.client.table("messages").insert(payload).execute() |
| data = _response_data(response) |
| return data[0] if isinstance(data, list) else data |
|
|
| async def get_recent_context_messages( |
| self, |
| *, |
| customer_id: str, |
| current_message_id: str, |
| limit: int = 4, |
| ) -> list[dict[str, Any]]: |
| current_response = await ( |
| self.client.table("messages") |
| .select("*") |
| .eq("id", current_message_id) |
| .single() |
| .execute() |
| ) |
| current = _response_data(current_response) |
| current_created_at = current.get("created_at") |
|
|
| prior_query = ( |
| self.client.table("messages") |
| .select("*") |
| .eq("customer_id", customer_id) |
| .neq("id", current_message_id) |
| .order("created_at", desc=True) |
| .limit(limit) |
| ) |
| if current_created_at: |
| prior_query = prior_query.lt("created_at", current_created_at) |
|
|
| prior_response = await prior_query.execute() |
| prior = _response_data(prior_response) or [] |
| return list(reversed(prior)) + [current] |
|
|
| async def get_message_by_whatsapp_id( |
| self, |
| whatsapp_message_id: str, |
| ) -> dict[str, Any] | None: |
| response = await ( |
| self.client.table("messages") |
| .select("*") |
| .eq("whatsapp_message_id", whatsapp_message_id) |
| .maybe_single() |
| .execute() |
| ) |
| return _response_data(response) |
|
|
| async def list_active_trips(self) -> list[dict[str, Any]]: |
| query = ( |
| self.client.table("driver_trips") |
| .select("*, drivers(*, customers(*)), driver_cars(*)") |
| .eq("status", "active") |
| .gt("available_seats", 0) |
| ) |
| query = ( |
| self._apply_not_departed_filter(query) |
| .order("departure_date") |
| .order("departure_time") |
| ) |
| response = await query.execute() |
| return _response_data(response) or [] |
|
|
| async def get_trips_by_ids(self, trip_ids: list[str]) -> list[dict[str, Any]]: |
| if not trip_ids: |
| return [] |
| response = await ( |
| self.client.table("driver_trips") |
| .select("*, drivers(*, customers(*)), driver_cars(*)") |
| .in_("id", trip_ids) |
| .execute() |
| ) |
| return _response_data(response) or [] |
|
|
| async def search_active_trips( |
| self, |
| *, |
| departure: str | None = None, |
| destination: str | None = None, |
| seats: int | None = None, |
| vehicle_type: str | None = None, |
| departure_request: DepartureRequest | None = None, |
| ) -> list[dict[str, Any]]: |
| query = ( |
| self.client.table("driver_trips") |
| .select("*, drivers(*, customers(*)), driver_cars(*)") |
| .eq("status", "active") |
| .gt("available_seats", 0) |
| ) |
| query = self._apply_departure_request_filter(query, departure_request) |
| query = query.order("departure_date").order("departure_time") |
| if departure: |
| query = query.ilike("departure", f"%{departure}%") |
| if destination: |
| query = query.ilike("destination", f"%{destination}%") |
| if seats: |
| query = query.gte("available_seats", seats) |
| if vehicle_type: |
| query = query.ilike("driver_cars.car_type", f"%{vehicle_type}%") |
| response = await query.limit(10).execute() |
| return _response_data(response) or [] |
|
|
| async def search_info_chunks_by_vector( |
| self, |
| *, |
| query_embedding: list[float], |
| match_count: int = 5, |
| ) -> list[dict[str, Any]]: |
| response = await self.client.rpc( |
| "match_falsa_info", |
| { |
| "query_embedding": query_embedding, |
| "match_count": match_count, |
| "match_threshold": 0.0, |
| }, |
| ).execute() |
| return _response_data(response) or [] |
|
|
| async def search_trips_by_vector( |
| self, |
| *, |
| query_embedding: list[float], |
| departure: str | None = None, |
| destination: str | None = None, |
| departure_date: date | None = None, |
| departure_time: str | None = None, |
| requested_time: time | None = None, |
| seats: int = 1, |
| vehicle_type: str | None = None, |
| match_count: int = 10, |
| ) -> list[dict[str, Any]]: |
| try: |
| response = await self.client.rpc( |
| "match_active_trips", |
| { |
| "query_embedding": query_embedding, |
| "match_count": match_count, |
| "match_threshold": 0.0, |
| "filter_departure": departure, |
| "filter_destination": destination, |
| "filter_departure_date": ( |
| departure_date.isoformat() if departure_date else None |
| ), |
| "filter_departure_time": departure_time, |
| "filter_requested_time": ( |
| requested_time.isoformat(timespec="minutes") if requested_time else None |
| ), |
| "filter_seats": seats, |
| "filter_vehicle_type": vehicle_type, |
| }, |
| ).execute() |
| return _response_data(response) or [] |
| except Exception as exc: |
| logger.warning( |
| "Supabase match_active_trips RPC failed; falling back to regular active trips search: %s", |
| exc, |
| ) |
| return [] |
|
|
| async def upsert_info_chunks(self, chunks: list[dict[str, Any]]) -> int: |
| if not chunks: |
| return 0 |
| response = await self.client.table("falsa_info_chunks").upsert(chunks).execute() |
| data = _response_data(response) |
| return len(data) if isinstance(data, list) else len(chunks) |
|
|
| async def upsert_trip_embeddings(self, trip_embeddings: list[dict[str, Any]]) -> int: |
| if not trip_embeddings: |
| return 0 |
| response = await ( |
| self.client.table("driver_trip_embeddings") |
| .upsert(trip_embeddings, on_conflict="trip_id") |
| .execute() |
| ) |
| data = _response_data(response) |
| return len(data) if isinstance(data, list) else len(trip_embeddings) |
|
|
| async def delete_trip_embedding(self, trip_id: str) -> None: |
| await ( |
| self.client.table("driver_trip_embeddings") |
| .delete() |
| .eq("trip_id", trip_id) |
| .execute() |
| ) |
|
|
| def _apply_departure_request_filter( |
| self, |
| query: Any, |
| departure_request: DepartureRequest | None, |
| ) -> Any: |
| if not departure_request: |
| return self._apply_not_departed_filter(query) |
|
|
| today, remaining_buckets = not_departed_bucket_filter() |
| if departure_request.departure_date: |
| query = query.eq("departure_date", departure_request.departure_date.isoformat()) |
| if departure_request.departure_time: |
| return query.eq("departure_time", departure_request.departure_time) |
| if departure_request.departure_date == today: |
| return query.in_("departure_time", list(remaining_buckets)) |
| return query |
|
|
| query = self._apply_not_departed_filter(query) |
| if departure_request.departure_time: |
| return query.eq("departure_time", departure_request.departure_time) |
| return query |
|
|
| def _apply_not_departed_filter(self, query: Any) -> Any: |
| today, remaining_buckets = not_departed_bucket_filter() |
| bucket_list = ",".join(remaining_buckets) |
| return query.or_( |
| f"departure_date.gt.{today.isoformat()}," |
| f"and(departure_date.eq.{today.isoformat()},departure_time.in.({bucket_list}))" |
| ) |
|
|
| async def get_trip_by_id(self, trip_id: str) -> dict[str, Any] | None: |
| response = await ( |
| self.client.table("driver_trips") |
| .select("*, drivers(*, customers(*)), driver_cars(*)") |
| .eq("id", trip_id) |
| .maybe_single() |
| .execute() |
| ) |
| return _response_data(response) |
|
|
| async def get_driver_by_remoteJid(self, remote_jid: str) -> dict[str, Any] | None: |
| response = await ( |
| self.client.table("drivers") |
| .select("*, customers!inner(*)") |
| .eq("customers.remoteJid", remote_jid) |
| .maybe_single() |
| .execute() |
| ) |
| return _response_data(response) |
|
|
| async def create_driver(self, *, customer_id: str) -> dict[str, Any]: |
| response = await ( |
| self.client.table("drivers") |
| .insert({"customer_id": customer_id, "status": "active"}) |
| .execute() |
| ) |
| data = _response_data(response) |
| driver = data[0] if isinstance(data, list) else data |
| await ( |
| self.client.table("driver_wallet") |
| .insert({"driver_id": driver["id"], "balance": 0}) |
| .execute() |
| ) |
| driver_response = await ( |
| self.client.table("drivers") |
| .select("*, customers(*)") |
| .eq("id", str(driver["id"])) |
| .maybe_single() |
| .execute() |
| ) |
| return _response_data(driver_response) |
|
|
| async def get_driver_latest_trip(self, driver_id: str) -> dict[str, Any] | None: |
| response = await ( |
| self.client.table("driver_trips") |
| .select("*, driver_cars(*)") |
| .eq("driver_id", driver_id) |
| .order("created_at", desc=True) |
| .limit(1) |
| .maybe_single() |
| .execute() |
| ) |
| return _response_data(response) |
|
|
| async def get_driver_trip_by_datetime( |
| self, |
| driver_id: str, |
| departure_date: date, |
| departure_time: str, |
| ) -> dict[str, Any] | None: |
| response = await ( |
| self.client.table("driver_trips") |
| .select("id, departure, destination, departure_date, departure_time") |
| .eq("driver_id", driver_id) |
| .eq("status", "active") |
| .eq("departure_date", departure_date.isoformat()) |
| .eq("departure_time", departure_time) |
| .maybe_single() |
| .execute() |
| ) |
| return _response_data(response) |
|
|
| async def list_driver_cars(self, driver_id: str) -> list[dict[str, Any]]: |
| response = await ( |
| self.client.table("driver_cars") |
| .select("*") |
| .eq("driver_id", driver_id) |
| .execute() |
| ) |
| return _response_data(response) or [] |
|
|
| async def list_driver_trips(self, driver_id: str) -> list[dict[str, Any]]: |
| query = ( |
| self.client.table("driver_trips") |
| .select("*, driver_cars(*)") |
| .eq("driver_id", driver_id) |
| .eq("status", "active") |
| ) |
| query = self._apply_not_departed_filter(query).order("departure_date").order("departure_time") |
| response = await query.execute() |
| return _response_data(response) or [] |
|
|
| async def create_driver_car( |
| self, |
| *, |
| driver_id: str, |
| car_type: str, |
| plate_number: str | None = None, |
| seat_count: int | None = None, |
| ) -> dict[str, Any]: |
| payload: dict[str, Any] = { |
| "driver_id": driver_id, |
| "car_type": car_type, |
| } |
| if plate_number is not None: |
| payload["plate_number"] = plate_number |
| if seat_count is not None: |
| payload["seat_count"] = seat_count |
|
|
| response = await self.client.table("driver_cars").insert(payload).execute() |
| data = _response_data(response) |
| return data[0] if isinstance(data, list) else data |
|
|
| async def create_driver_trip( |
| self, |
| *, |
| driver_id: str, |
| car_id: str | None, |
| departure: str, |
| destination: str, |
| departure_date: date, |
| departure_time: str, |
| available_seats: int, |
| total_seats: int, |
| price: float, |
| ) -> dict[str, Any]: |
| payload = { |
| "driver_id": driver_id, |
| "car_id": car_id, |
| "departure": departure, |
| "destination": destination, |
| "departure_date": departure_date.isoformat(), |
| "departure_time": departure_time, |
| "available_seats": available_seats, |
| "total_seats": total_seats, |
| "price": price, |
| "status": "active", |
| } |
| response = await self.client.table("driver_trips").insert(payload).execute() |
| data = _response_data(response) |
| trip = data[0] if isinstance(data, list) else data |
| return await self.get_trip_by_id(str(trip["id"])) or trip |
|
|
| async def update_driver_trip( |
| self, |
| trip_id: str, |
| updates: dict[str, Any], |
| ) -> dict[str, Any]: |
| if not updates: |
| raise ValueError("updates must not be empty") |
| response = await ( |
| self.client.table("driver_trips") |
| .update(updates) |
| .eq("id", trip_id) |
| .execute() |
| ) |
| data = _response_data(response) |
| updated = data[0] if isinstance(data, list) else data |
| return await self.get_trip_by_id(str(updated.get("id") or trip_id)) or updated |
|
|
| async def cancel_driver_trip(self, trip_id: str) -> dict[str, Any]: |
| response = await ( |
| self.client.table("driver_trips") |
| .update({"status": "cancelled"}) |
| .eq("id", trip_id) |
| .execute() |
| ) |
| data = _response_data(response) |
| updated = data[0] if isinstance(data, list) else data |
| return await self.get_trip_by_id(str(updated.get("id") or trip_id)) or updated |
|
|
| async def create_booking_lead( |
| self, |
| *, |
| customer_id: str, |
| trip_id: str, |
| requested_seats: int, |
| notes: str | None, |
| ) -> dict[str, Any]: |
| payload = { |
| "customer_id": customer_id, |
| "trip_id": trip_id, |
| "requested_seats": requested_seats, |
| "status": "pending", |
| "notes": notes, |
| "driver_notification_status": "not_sent", |
| } |
| response = await self.client.table("booking_leads").insert(payload).execute() |
| data = _response_data(response) |
| return data[0] if isinstance(data, list) else data |
|
|
| async def update_booking_lead_notification( |
| self, |
| *, |
| lead_id: str, |
| status: str, |
| metadata: dict[str, Any] | None = None, |
| ) -> dict[str, Any]: |
| payload: dict[str, Any] = {"driver_notification_status": status} |
| if metadata is not None: |
| payload["metadata"] = metadata |
| response = ( |
| await self.client.table("booking_leads").update(payload).eq("id", lead_id).execute() |
| ) |
| data = _response_data(response) |
| return data[0] if isinstance(data, list) else data |
|
|