import { apiClient } from '@/lib/api-client'; // ── Search (POST /search) ────────────────────────────────────────────────── export interface SearchTripsRequest { originStationId: string; destinationStationId: string; date: string; adultCount: number; childCount?: number; journeyType: 'ONE_WAY' | 'ROUND_TRIP'; /** Required when journeyType is ROUND_TRIP. */ returnDate?: string; /** Drives which fare tier (Local vs International) gets quoted — see fareTier in the page component. */ nationality?: string; /** Always 'GROUP_BOOKING' for this app — search returns ONLY schedules marked * isGroupBookingOnly (an exclusive partition, not additive): normal passenger-facing * schedules never show up here, and group-only schedules never show up in the portal. */ channel?: 'PORTAL' | 'GROUP_BOOKING'; } export interface ScheduleClassOption { name: string; baseFareMinor: number; displayCurrency: string; displayAmountMinor: number; available: number; } export interface ScheduleCoachType { coachTypeId: string; coachTypeName: string; coachTypeCode: string; coachId: string; classes: ScheduleClassOption[]; } export interface ScheduleResult { type: 'DIRECT'; scheduleId: string; trainNumber: string; trainName: string; origin: { id: string; code: string; name: string; city: string; sequence: number }; destination: { id: string; code: string; name: string; city: string; sequence: number }; departureAt: string; arrivalAt: string; durationMinutes: number; status: string; hasAvailability: boolean; displayCurrency: string; coachTypes: ScheduleCoachType[]; } export type SearchEmptyReasonCode = | 'NO_ROUTE' | 'NO_SCHEDULE_ON_DATE' | 'CANCELLED' | 'PACKAGE_ONLY' | 'GROUP_BOOKING_ONLY' | 'CHECKIN_CLOSED' | 'FULLY_BOOKED'; /** Structured, not a string — always render via a code→message lookup, never directly. */ export interface SearchEmptyReason { code: SearchEmptyReasonCode; originStationName: string; destinationStationName: string; } export interface SearchTripsResponse { journeyType: string; outbound: ScheduleResult[]; requestedDate: string; outboundReason?: SearchEmptyReason; /** Nearby schedules for the same station pair on a different date, offered when `outbound` is empty. */ alternativeOutbound?: ScheduleResult[]; /** Return-leg schedules — present when the request's journeyType was ROUND_TRIP. */ inbound?: ScheduleResult[]; requestedReturnDate?: string; inboundReason?: SearchEmptyReason; alternativeInbound?: ScheduleResult[]; } // ── Seat classes (GET /seat-classes) ─────────────────────────────────────── export interface SeatClassOption { id: string; name: string; } // ── Auto-assign + hold (POST /seats/auto-assign-hold) ───────────────────── export interface AutoAssignHoldRequest { scheduleId: string; originStationId: string; destinationStationId: string; seatClassName: string; adultCount: number; childCount?: number; /** Round-trip leg tag — omit for a one-way booking. */ journeyDirection?: 'OUTBOUND' | 'RETURN'; } export interface HeldPassengerSeat { passengerId: string; seat: { id: string; label?: string; seatNumber?: string; coach?: string; row?: number; col?: string; }; } export interface AutoAssignHoldResponse { holdId: string; expiresAt: string; ttlSeconds: number; schedule: { id: string; trainNumber: string; trainName: string; departureAt: string; arrivalAt: string } | null; passengers: HeldPassengerSeat[]; } // ── Group booking creation (POST /bookings/group) ────────────────────────── export interface GroupBookingPassengerInput { /** Omit for a free child (ONE_WAY only) — matches guest-booking.dto.ts's own optional seatId. */ seatId?: string; passengerName: string; dateOfBirth: string; idDocumentType: 'NATIONAL_ID' | 'PASSPORT' | 'DRIVING_LICENSE' | 'OTHER'; idDocumentNumber?: string; passportNumber?: string; passportCountry?: string; nationality?: string; phone?: string; email?: string; /** Return-leg seat ID — required when the booking is ROUND_TRIP. */ returnSeatId?: string; } export interface CreateGroupBookingRequest { scheduleId: string; holdId: string; originStationId: string; destinationStationId: string; seatClassId: string; bookingType: 'ONE_WAY' | 'ROUND_TRIP'; passengers: GroupBookingPassengerInput[]; /** ROUND_TRIP only. */ returnScheduleId?: string; returnHoldId?: string; returnOriginStationId?: string; returnDestinationStationId?: string; /** Falls back to seatClassId on the backend if omitted. */ returnSeatClassId?: string; } export interface GroupBookingSeat { seatId: string; passengerName: string; passengerCategory: 'ADULT' | 'CHILD'; /** 1 = outbound leg, 2 = return leg. Absent on a plain ONE_WAY booking. */ leg?: number; seat: { seatNumber: string; bedPosition?: string | null; coach: { number: string } }; } export interface CreateGroupBookingResponse { id: string; bookingRef: string; status: string; totalMinor: number; currency: string; adultCount: number; childCount: number; seats: GroupBookingSeat[]; schedule: { departureAt: string; arrivalAt: string; train: { number: string; name: string }; originStation: { name: string }; destinationStation: { name: string }; }; } // ── Payment (GET /payments/methods, POST /payments/initiate) ─────────────── export type PaymentMethodType = | 'TELEBIRR' | 'CBE_BIRR' | 'EBIRR' | 'WAAFI' | 'DMONEY' | 'CAC_BANK' | 'CARD' | 'WALLET' | 'CBE_BILL'; export interface SupportedPaymentMethod { id: string; type: PaymentMethodType; displayName: string; region: string; currency: string; enabled: boolean; } export interface InitiatePaymentRequest { bookingId: string; method: PaymentMethodType; paymentMethodId?: string; platform?: 'web' | 'mobile' | 'inapp'; } export interface PaymentClientAction { type: 'REDIRECT' | 'LAUNCH_APP' | 'INVOKE_BRIDGE' | 'COLLECT_OTP' | 'AWAIT_PUSH' | 'SHOW_BILL_REFERENCE'; url?: string; /** Set when type=SHOW_BILL_REFERENCE (CBE bill payment) — the number the payer enters at any CBE channel. */ billReference?: string; instructions?: string; expiresAt?: string; message?: string; payerAccountMasked?: string; } export interface InitiatePaymentResponse { intentId: string; status: string; clientAction?: PaymentClientAction; merchantOrderId?: string; failureCode?: string; failureMessage?: string; sessionExpiresAt?: string; paymentDeadline?: string; } export const groupBookingApi = { searchTrips: (dto: SearchTripsRequest) => apiClient.post('/search', dto), getSeatClasses: () => apiClient.get('/seat-classes'), autoAssignHold: (dto: AutoAssignHoldRequest) => apiClient.post('/seats/auto-assign-hold', dto), /** Best-effort early release — e.g. freeing an outbound hold when the return leg's auto-assign fails. */ releaseHold: (holdId: string) => apiClient.delete(`/seats/hold/${holdId}`), createGroupBooking: (dto: CreateGroupBookingRequest) => apiClient.post('/bookings/group', dto), // Guards against a non-array response the same way dashboardApi.getPaymentMethods / // paymentsApi.getMethods already do elsewhere in this app — never lets a bad/unexpected // response shape reach a caller expecting a plain array. getPaymentMethods: async (): Promise => { try { const response = await apiClient.get('/payments/methods'); return Array.isArray(response) ? response : []; } catch { return []; } }, initiatePayment: (dto: InitiatePaymentRequest) => apiClient.post('/payments/initiate', dto), };