Files
edr-platform/apps/edr-passenger-web/portal/src/lib/booking-store.ts
2026-07-07 16:52:58 +03:00

226 lines
8.2 KiB
TypeScript

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
export interface SearchCriteria {
originStationId: string;
destinationStationId: string;
departureDate: string;
returnDate?: string;
tripType: 'ONE_WAY' | 'ROUND_TRIP';
adultCount: number;
childCount: number;
nationality: 'ETHIOPIAN' | 'DJIBOUTIAN' | 'OTHER';
promoCode?: string;
}
export interface PassengerDetail {
name: string;
dateOfBirth: string;
nationality: string;
faydaVerified?: boolean;
nationalId?: string;
faydaSub?: string;
passportNumber?: string;
passportCountry?: string;
passportIssueDate?: string;
passportExpiryDate?: string;
passportIssuingAuthority?: string;
idDocumentType?: string;
isPrimaryPassenger: boolean;
seatId?: string;
seatNumber?: string;
coachNumber?: string;
// Fare for this passenger's actual assigned seat (minor units). Bed coaches price
// Upper/Middle/Lower differently, so this can differ from the schedule's flat
// baseFareAdult (which is only the coach type's cheapest class) — review/payment
// should prefer this per-seat fare when it's available.
seatFareMinor?: number;
// Raw bed position ("lower"/"middle"/"upper") of the assigned seat, if it's a berth —
// used to resolve this passenger's actual seat class server-side (see review page's
// fare-breakdown request), since a shared coach-level class can't distinguish berths.
bedPosition?: string;
phone?: string;
email?: string;
gender?: string;
// Round-trip specific seat assignments
outboundSeatId?: string;
outboundSeatNumber?: string;
outboundCoachNumber?: string;
outboundSeatFareMinor?: number;
outboundBedPosition?: string;
inboundSeatId?: string;
inboundSeatNumber?: string;
inboundCoachNumber?: string;
inboundSeatFareMinor?: number;
inboundBedPosition?: string;
returnSeatId?: string;
returnSeatNumber?: string;
}
export interface SelectedSchedule {
id: string;
trainNumber: string;
origin: string;
destination: string;
originStationId?: string;
destinationStationId?: string;
departureTime: string;
arrivalTime: string;
duration: string;
baseFareAdult: number;
baseFareChild: number;
displayCurrency: string;
selectedSeatClass?: string;
selectedSeatClassName?: string;
seatClassName?: string;
selectedCoachTypeId?: string;
selectedCoachTypeCode?: string;
selectedCoachTypeName?: string;
// All coach types (with per-class fares) offered on this schedule at selection time —
// kept so the seat map's coach preview can compute the fare difference before letting
// a passenger switch to a different coach type.
coachTypes?: Array<{
coachId: string;
coachTypeId: string;
coachTypeName: string;
coachTypeCode: string;
classes: Array<{
name: string;
baseFareMinor: number;
displayCurrency?: string;
displayAmountMinor?: number;
}>;
}>;
}
export interface SeatHold {
holdId: string;
expiresAt: string;
returnHoldId?: string;
returnExpiresAt?: string;
}
interface BookingState {
searchCriteria: SearchCriteria | null;
selectedSchedule: SelectedSchedule | null;
outboundSchedule: SelectedSchedule | null;
inboundSchedule: SelectedSchedule | null;
passengers: PassengerDetail[];
seatHold: SeatHold | null;
bookingId: string | null;
pnr: string | null;
// The hold id(s) the current bookingId was actually created with — lets the review
// page detect "user came back from the payment gateway with the same booking still
// valid" and reuse it instead of creating a duplicate booking.
bookingHoldId: string | null;
bookingReturnHoldId: string | null;
selectedPaymentMethod: string | null;
createAccount: boolean;
passengerId: string | null;
packageId: string | null;
packageName: string | null;
priceTierId: string | null;
packageTierPriceMinor: number | null;
packageDepartureStationId: string | null;
packageDepartureStationName: string | null;
// The exact total (minor units, ETB) computed on the review page — used as the
// single source of truth on payment and confirmation pages to prevent drift.
reviewedTotalMinor: number | null;
// Per-passenger fare breakdown computed on the review page — guarantees line items
// on the payment page sum to exactly reviewedTotalMinor.
reviewedPassengerFares: Array<{ fareMinor: number; isFree: boolean }> | null;
setSearchCriteria: (criteria: SearchCriteria) => void;
setSelectedSchedule: (schedule: SelectedSchedule) => void;
setOutboundSchedule: (schedule: SelectedSchedule) => void;
setInboundSchedule: (schedule: SelectedSchedule) => void;
setPassengers: (passengers: PassengerDetail[]) => void;
setSeatHold: (hold: SeatHold | null) => void;
setBookingId: (id: string) => void;
setPNR: (pnr: string) => void;
setBookingHoldReference: (holdId: string | null, returnHoldId?: string | null) => void;
setPaymentMethod: (method: string) => void;
setCreateAccount: (create: boolean) => void;
setPassengerId: (id: string | null) => void;
setPackageContext: (packageId: string, priceTierId: string, priceMinor: number, packageName?: string, departureStationId?: string, departureStationName?: string) => void;
setReviewedTotal: (totalMinor: number, passengerFares: Array<{ fareMinor: number; isFree: boolean }>) => void;
clearBooking: () => void;
}
export const useBookingStore = create<BookingState>()(persist(
(set) => (({
searchCriteria: null,
selectedSchedule: null,
outboundSchedule: null,
inboundSchedule: null,
passengers: [],
seatHold: null,
bookingId: null,
pnr: null,
bookingHoldId: null,
bookingReturnHoldId: null,
selectedPaymentMethod: null,
createAccount: false,
passengerId: null,
packageId: null,
packageName: null,
priceTierId: null,
packageTierPriceMinor: null,
packageDepartureStationId: null,
packageDepartureStationName: null,
reviewedTotalMinor: null,
reviewedPassengerFares: null,
setSearchCriteria: (criteria) => set({ searchCriteria: criteria }),
setPackageContext: (packageId, priceTierId, priceMinor, packageName, departureStationId, departureStationName) => set({ packageId, packageName: packageName ?? null, priceTierId, packageTierPriceMinor: priceMinor, packageDepartureStationId: departureStationId ?? null, packageDepartureStationName: departureStationName ?? null }),
setSelectedSchedule: (schedule) => set({ selectedSchedule: schedule }),
setOutboundSchedule: (schedule) => set({ outboundSchedule: schedule }),
setInboundSchedule: (schedule) => set({ inboundSchedule: schedule }),
setPassengers: (passengers) => set({ passengers }),
setSeatHold: (hold) => set({ seatHold: hold }),
setBookingId: (id) => set({ bookingId: id }),
setPNR: (pnr) => set({ pnr }),
setBookingHoldReference: (holdId, returnHoldId) => set({ bookingHoldId: holdId, bookingReturnHoldId: returnHoldId ?? null }),
setPaymentMethod: (method) => set({ selectedPaymentMethod: method }),
setCreateAccount: (create) => set({ createAccount: create }),
setPassengerId: (id) => set({ passengerId: id }),
setReviewedTotal: (totalMinor, passengerFares) => set({ reviewedTotalMinor: totalMinor, reviewedPassengerFares: passengerFares }),
clearBooking: () => set({
searchCriteria: null,
selectedSchedule: null,
outboundSchedule: null,
inboundSchedule: null,
passengers: [],
seatHold: null,
bookingId: null,
pnr: null,
bookingHoldId: null,
bookingReturnHoldId: null,
selectedPaymentMethod: null,
createAccount: false,
passengerId: null,
packageId: null,
packageName: null,
priceTierId: null,
packageTierPriceMinor: null,
packageDepartureStationId: null,
packageDepartureStationName: null,
reviewedTotalMinor: null,
reviewedPassengerFares: null,
}),
} as BookingState)),
{
name: 'booking-storage',
storage: createJSONStorage(() => {
if (typeof window !== 'undefined') {
return localStorage;
}
return {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
};
}),
}
));