mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 23:00:57 +00:00
259 lines
8.0 KiB
TypeScript
259 lines
8.0 KiB
TypeScript
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<SearchTripsResponse>('/search', dto),
|
|
|
|
getSeatClasses: () => apiClient.get<SeatClassOption[]>('/seat-classes'),
|
|
|
|
autoAssignHold: (dto: AutoAssignHoldRequest) =>
|
|
apiClient.post<AutoAssignHoldResponse>('/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<CreateGroupBookingResponse>('/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<SupportedPaymentMethod[]> => {
|
|
try {
|
|
const response = await apiClient.get<SupportedPaymentMethod[]>('/payments/methods');
|
|
return Array.isArray(response) ? response : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
},
|
|
|
|
initiatePayment: (dto: InitiatePaymentRequest) =>
|
|
apiClient.post<InitiatePaymentResponse>('/payments/initiate', dto),
|
|
};
|