Merge pull request #385 from Tria-plc/alpha

Alpha
This commit is contained in:
Abubeker Yasin
2026-06-30 20:09:01 +03:00
committed by GitHub
7 changed files with 238 additions and 173 deletions

View File

@@ -15,6 +15,10 @@ export class DynamicThrottlerGuard extends ThrottlerGuard {
}
async canActivate(context: ExecutionContext): Promise<boolean> {
if (context.getType() !== 'http') {
return true;
}
const [authLimit, authTtl, strictLimit, strictTtl, defaultLimit, defaultTtl] =
await Promise.all([
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_AUTH_LIMIT),

View File

@@ -1,5 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { Nack, RabbitSubscribe } from '@golevelup/nestjs-rabbitmq';
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
import {
PAYMENT_EVENTS_DLX,
PAYMENT_EVENTS_EXCHANGE,
@@ -19,6 +20,7 @@ export class PaymentEventsConsumer {
constructor(private readonly paymentsService: PaymentsService) {}
@IsPublic()
@RabbitSubscribe({
exchange: PAYMENT_EVENTS_EXCHANGE,
routingKey: paymentServiceBindingPattern(PaymentService.PASSENGER), // payment.passenger.*

View File

@@ -9,6 +9,30 @@ import { Currency } from '@prisma/client';
const POINTS_TO_MINOR = 10;
// Shape returned by the heavy schedule include used throughout this service
type ScheduleWithIncludes = {
id: string;
routeId: string | null;
departureAt: Date;
arrivalAt: Date;
status: string;
train: any;
originStation: any;
destinationStation: any;
stopTimes: Array<{ stationId: string; sequence: number; plannedArrivalAt: Date | null; plannedDepartureAt: Date | null; station: any }>;
coachAssignments: Array<{ coach: { id: string; seats: any[]; coachType: { id: string; name: string; code: string; seatClasses: any[] } | null } }>;
};
const SCHEDULE_INCLUDE = {
train: true,
originStation: true,
destinationStation: true,
stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
coachAssignments: {
include: { coach: { include: { seats: true, coachType: { include: { seatClasses: true } } } } },
},
} as const;
@Injectable()
export class SearchService {
constructor(
@@ -124,7 +148,7 @@ export class SearchService {
if (windowStart < now) windowStart.setTime(now.getTime());
const windowEnd = new Date(requestedDate);
windowEnd.setDate(windowEnd.getDate() + daysAfter + 1); // exclusive upper bound
windowEnd.setDate(windowEnd.getDate() + daysAfter + 1);
const totalPassengers = adultCount + (childCount ?? 0);
@@ -139,30 +163,16 @@ export class SearchService {
],
stopTimes: { some: { stationId: originStationId } },
},
include: {
train: true,
originStation: true,
destinationStation: true,
stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
coachAssignments: {
include: { coach: { include: { seats: true, coachType: { include: { seatClasses: true } } } } },
},
},
include: SCHEDULE_INCLUDE,
orderBy: { departureAt: 'asc' },
});
const results: any[] = [];
for (const schedule of schedules) {
const result = await this.buildScheduleResult(
schedule,
originStationId,
destinationStationId,
totalPassengers,
nationality,
);
if (result) results.push(result);
}
return results;
const results = await Promise.all(
schedules.map(schedule =>
this.buildScheduleResult(schedule as any, originStationId, destinationStationId, totalPassengers, nationality)
)
);
return results.filter(Boolean);
}
private async searchSchedules(
@@ -185,29 +195,18 @@ export class SearchService {
departureAt: { gte: date < now ? now : date, lt: nextDay },
stopTimes: { some: { stationId: originStationId } },
},
include: {
train: true,
originStation: true,
destinationStation: true,
stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
coachAssignments: {
include: { coach: { include: { seats: true, coachType: { include: { seatClasses: true } } } } },
},
},
include: SCHEDULE_INCLUDE,
});
const results: any[] = [];
for (const schedule of schedules) {
const result = await this.buildScheduleResult(schedule, originStationId, destinationStationId, totalPassengers, nationality);
if (result) results.push(result);
}
return results;
const results = await Promise.all(
schedules.map(schedule =>
this.buildScheduleResult(schedule as any, originStationId, destinationStationId, totalPassengers, nationality)
)
);
return results.filter(Boolean);
}
// ── Transit search ─────────────────────────────────────────────────────────
// Finds pairs of schedules (leg1: origin→transit, leg2: transit→destination)
// where the passenger has between MIN_CONNECTION and MAX_CONNECTION minutes
// to change trains at the transit station.
private readonly MIN_CONNECTION_MINUTES = 30;
private readonly MAX_CONNECTION_MINUTES = 360;
@@ -219,82 +218,67 @@ export class SearchService {
childCount?: number,
nationality?: string,
) {
// Find all stations that can serve as transit points:
// they must be a stop after origin on some schedule AND
// a stop before destination on another schedule on the same day.
const [y, m, d] = dateStr.split('-').map(Number);
const dayStart = new Date(y, m - 1, d, 0, 0, 0, 0);
const dayEnd = new Date(y, m - 1, d + 1, 0, 0, 0, 0);
const leg2WindowEnd = new Date(dayEnd.getTime() + this.MAX_CONNECTION_MINUTES * 60_000);
const totalPassengers = adultCount + (childCount ?? 0);
// Load all schedules on this date that pass through origin
const leg1Schedules = await this.prisma.trainSchedule.findMany({
where: {
status: { in: ['SCHEDULED', 'BOARDING'] },
departureAt: { gte: dayStart, lt: dayEnd },
stopTimes: { some: { stationId: originStationId } },
},
include: {
train: true,
originStation: true,
destinationStation: true,
stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
coachAssignments: {
include: { coach: { include: { seats: true, coachType: { include: { seatClasses: true } } } } },
// Load leg1 and all potential leg2 candidates in one parallel round-trip
// instead of firing a separate DB query per transit stop.
const [leg1Schedules, allCandidates] = await Promise.all([
this.prisma.trainSchedule.findMany({
where: {
status: { in: ['SCHEDULED', 'BOARDING'] },
departureAt: { gte: dayStart, lt: dayEnd },
stopTimes: { some: { stationId: originStationId } },
},
},
});
include: SCHEDULE_INCLUDE,
}),
this.prisma.trainSchedule.findMany({
where: {
status: { in: ['SCHEDULED', 'BOARDING'] },
departureAt: { gte: dayStart, lt: leg2WindowEnd },
},
include: SCHEDULE_INCLUDE,
}),
]);
const results: any[] = [];
for (const leg1 of leg1Schedules) {
const originStop = leg1.stopTimes.find((s: any) => s.stationId === originStationId);
for (const leg1 of leg1Schedules as ScheduleWithIncludes[]) {
const originStop = leg1.stopTimes.find(s => s.stationId === originStationId);
if (!originStop) continue;
// Every stop after origin on leg1 is a candidate transit station
const candidateTransitStops = leg1.stopTimes.filter(
(s: any) => s.sequence > originStop.sequence,
s => s.sequence > originStop.sequence,
);
for (const transitStop of candidateTransitStops) {
// leg1 must NOT already contain the final destination
const leg1HasDest = leg1.stopTimes.some((s: any) => s.stationId === destinationStationId);
if (leg1HasDest) continue; // direct route exists — already returned by searchSchedules
const leg1HasDest = leg1.stopTimes.some(s => s.stationId === destinationStationId);
if (leg1HasDest) continue;
const transitStationId = transitStop.stationId;
const leg1ArrivalAt = transitStop.plannedArrivalAt ?? transitStop.plannedDepartureAt ?? leg1.arrivalAt;
// Find leg2 schedules departing from the transit station within the connection window,
// and reaching the final destination. Search up to the next calendar day to handle
// overnight connections.
const connWindowStart = new Date(new Date(leg1ArrivalAt).getTime() + this.MIN_CONNECTION_MINUTES * 60_000);
const connWindowEnd = new Date(new Date(leg1ArrivalAt).getTime() + this.MAX_CONNECTION_MINUTES * 60_000);
const leg2Schedules = await this.prisma.trainSchedule.findMany({
where: {
status: { in: ['SCHEDULED', 'BOARDING'] },
departureAt: { gte: connWindowStart, lte: connWindowEnd },
stopTimes: { some: { stationId: transitStationId } },
},
include: {
train: true,
originStation: true,
destinationStation: true,
stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
coachAssignments: {
include: { coach: { include: { seats: true, coachType: { include: { seatClasses: true } } } } },
},
},
// Filter from pre-loaded candidates in memory — no extra DB query
const leg2Schedules = (allCandidates as ScheduleWithIncludes[]).filter(s => {
const dep = new Date(s.departureAt).getTime();
return dep >= connWindowStart.getTime()
&& dep <= connWindowEnd.getTime()
&& s.stopTimes.some(st => st.stationId === transitStationId);
});
for (const leg2 of leg2Schedules) {
const leg2TransitStop = leg2.stopTimes.find((s: any) => s.stationId === transitStationId);
const leg2DestStop = leg2.stopTimes.find((s: any) => s.stationId === destinationStationId);
const leg2TransitStop = leg2.stopTimes.find(s => s.stationId === transitStationId);
const leg2DestStop = leg2.stopTimes.find(s => s.stationId === destinationStationId);
if (!leg2TransitStop || !leg2DestStop) continue;
if (leg2TransitStop.sequence >= leg2DestStop.sequence) continue;
// Build individual leg result objects (reuse existing per-schedule logic)
const [leg1Result, leg2Result] = await Promise.all([
this.buildScheduleResult(leg1, originStationId, transitStationId, totalPassengers, nationality),
this.buildScheduleResult(leg2, transitStationId, destinationStationId, totalPassengers, nationality),
@@ -326,7 +310,6 @@ export class SearchService {
displayCurrency,
combinedMinFareMinor,
combinedMinFareDisplay,
// Convenience top-level fields so round-trip filter can read them uniformly
departureAt: leg1Result.departureAt,
arrivalAt: leg2Result.arrivalAt,
totalDurationMinutes:
@@ -339,19 +322,37 @@ export class SearchService {
return results;
}
// Builds the same result shape as searchSchedules for a single schedule+leg,
// extracted so both direct and transit paths share identical output.
private async buildScheduleResult(
schedule: any,
schedule: ScheduleWithIncludes,
originStationId: string,
destinationStationId: string,
totalPassengers: number,
nationality?: string,
) {
const originStop = schedule.stopTimes.find((s: any) => s.stationId === originStationId);
const destStop = schedule.stopTimes.find((s: any) => s.stationId === destinationStationId);
const originStop = schedule.stopTimes.find(s => s.stationId === originStationId);
const destStop = schedule.stopTimes.find(s => s.stationId === destinationStationId);
if (!originStop || !destStop || originStop.sequence >= destStop.sequence) return null;
// Collect all valid seat IDs upfront for a single batch availability check
const allValidSeatIds = schedule.coachAssignments.flatMap(a =>
a.coach.seats
.filter((s: any) => s.status !== 'BLOCKED' && s.seatNumber?.trim())
.map((s: any) => s.id as string)
);
// Run availability batch and fare calculation in parallel
const [freeSeats, faresByClass] = await Promise.all([
this.segmentsService.getFreeSeatIds(
schedule.id,
allValidSeatIds,
schedule.stopTimes,
originStop.sequence,
destStop.sequence,
),
this.calculateFaresForSegment(schedule, originStationId, destinationStationId, nationality),
]);
// Compute per-class availability using the pre-computed free seat set
const availabilityByClass: Record<string, number> = {};
for (const assignment of schedule.coachAssignments) {
const seatClassNames = assignment.coach.coachType?.seatClasses?.map((sc: any) => sc.name) || ['Standard'];
@@ -362,8 +363,7 @@ export class SearchService {
let count = 0;
for (const seat of assignment.coach.seats) {
if (seat.bedPosition !== bedPosition || seat.status === 'BLOCKED' || !seat.seatNumber?.trim()) continue;
const free = await this.segmentsService.isSeatFreeForLeg(schedule.id, seat.id, originStop.sequence, destStop.sequence);
if (free) count++;
if (freeSeats.has(seat.id)) count++;
}
if (count > 0) {
const matchingClass = seatClassNames.find((n: string) => n.toLowerCase().includes(bedPosition));
@@ -374,15 +374,13 @@ export class SearchService {
let available = 0;
for (const seat of assignment.coach.seats) {
if (seat.status === 'BLOCKED' || !seat.seatNumber?.trim()) continue;
const free = await this.segmentsService.isSeatFreeForLeg(schedule.id, seat.id, originStop.sequence, destStop.sequence);
if (free) available++;
if (freeSeats.has(seat.id)) available++;
}
for (const name of seatClassNames) availabilityByClass[name] = (availabilityByClass[name] ?? 0) + available;
}
}
const faresByClass = await this.calculateFaresForSegment(schedule, originStationId, destinationStationId, nationality);
const coachTypes = await this.buildCoachTypeDetails(schedule, faresByClass);
const coachTypes = this.buildCoachTypeDetails(schedule, faresByClass);
const legDepartureAt = originStop.plannedDepartureAt ?? schedule.departureAt;
const legArrivalAt = destStop.plannedArrivalAt ?? schedule.arrivalAt;
@@ -400,8 +398,8 @@ export class SearchService {
durationMinutes: Math.round((new Date(legArrivalAt).getTime() - new Date(legDepartureAt).getTime()) / 60_000),
status: schedule.status,
stops: schedule.stopTimes
.filter((st: any) => st.sequence >= originStop.sequence && st.sequence <= destStop.sequence)
.map((st: any) => ({ stationId: st.stationId, stationName: st.station.name, sequence: st.sequence, plannedArrivalAt: st.plannedArrivalAt, plannedDepartureAt: st.plannedDepartureAt })),
.filter(st => st.sequence >= originStop.sequence && st.sequence <= destStop.sequence)
.map(st => ({ stationId: st.stationId, stationName: st.station.name, sequence: st.sequence, plannedArrivalAt: st.plannedArrivalAt, plannedDepartureAt: st.plannedDepartureAt })),
availabilityByClass,
hasAvailability: Object.values(availabilityByClass).some(n => n >= totalPassengers),
displayCurrency,
@@ -500,46 +498,31 @@ export class SearchService {
}
private async calculateFaresForSegment(
schedule: any,
schedule: ScheduleWithIncludes,
originStationId: string,
destinationStationId: string,
nationality?: string,
): Promise<Array<{ seatClassName: string; baseFareMinor: number; displayCurrency: Currency; displayAmountMinor: number }>> {
const displayCurrency = resolveCurrencyFromNationality(nationality);
const seatClassIds: string[] = Array.from(
new Set(
schedule.coachAssignments
.flatMap((a: any) => a.coach.coachType?.seatClasses || [])
.map((sc: any) => sc.id)
.filter((id: any) => id)
)
);
if (seatClassIds.length === 0) {
console.log(`No seat classes assigned to schedule ${schedule.id}`);
return [];
// Use seat class data already loaded in the schedule include — avoids an extra seatClass.findMany
const seatClassMap = new Map<string, any>();
for (const a of schedule.coachAssignments) {
for (const sc of (a.coach.coachType?.seatClasses ?? [])) {
if (sc.isActive && !seatClassMap.has(sc.id)) seatClassMap.set(sc.id, sc);
}
}
const seatClasses = Array.from(seatClassMap.values())
.sort((a: any, b: any) => a.baseFareMinor - b.baseFareMinor);
const seatClasses = await this.prisma.seatClass.findMany({
where: {
isActive: true,
id: { in: seatClassIds }
},
orderBy: { baseFareMinor: 'asc' },
});
if (seatClasses.length === 0) {
console.log(`No active seat classes for schedule ${schedule.id}`);
return [];
}
if (seatClasses.length === 0) return [];
if (schedule.routeId) {
const results = await Promise.all(
seatClasses.map(async (sc) => {
try {
const fare = await this.fareEngine.calculate({
routeId: schedule.routeId,
routeId: schedule.routeId!,
originStationId,
destinationStationId,
seatClassId: sc.id,
@@ -552,8 +535,7 @@ export class SearchService {
displayCurrency: fare.billingCurrency as Currency,
displayAmountMinor: Math.round(fare.baseFarePerPassengerMinor * fare.exchangeRate),
};
} catch (error) {
console.error(`Failed to calculate fare for ${sc.name}:`, (error as Error).message);
} catch {
return null;
}
}),
@@ -562,36 +544,32 @@ export class SearchService {
const validResults = results.filter(
(r): r is { seatClassName: string; baseFareMinor: number; displayCurrency: Currency; displayAmountMinor: number } => r !== null,
);
if (validResults.length > 0) {
return validResults;
}
if (validResults.length > 0) return validResults;
}
const originStation = await this.prisma.station.findUnique({ where: { id: originStationId } });
const destStation = await this.prisma.station.findUnique({ where: { id: destinationStationId } });
// Fallback: use station codes from already-loaded stopTimes when available
const originStop = schedule.stopTimes.find(st => st.stationId === originStationId);
const destStop = schedule.stopTimes.find(st => st.stationId === destinationStationId);
const originCode = originStop?.station?.code;
const destCode = destStop?.station?.code;
if (originStation && destStation) {
const segmentRoute = `${originStation.code}-${destStation.code}`;
if (originCode && destCode) {
const segmentRoute = `${originCode}-${destCode}`;
const now = new Date();
const fareRules = await this.prisma.fareRule.findMany({
where: {
route: segmentRoute,
seatClassId: { in: seatClassIds },
seatClassId: { in: seatClasses.map((sc: any) => sc.id) },
validFrom: { lte: now },
OR: [
{ validUntil: null },
{ validUntil: { gte: now } },
],
OR: [{ validUntil: null }, { validUntil: { gte: now } }],
},
});
if (fareRules.length > 0) {
console.log(`Found ${fareRules.length} fare rules for segment ${segmentRoute}`);
const seatClassMap = Object.fromEntries(seatClasses.map(sc => [sc.id, sc.name]));
const exchangeRate = await this.currencyService.getExchangeRate(Currency.ETB, displayCurrency);
return fareRules.map(rule => ({
seatClassName: seatClassMap[rule.seatClassId] || 'Unknown',
seatClassName: seatClassMap.get(rule.seatClassId)?.name ?? 'Unknown',
baseFareMinor: rule.baseFareMinor,
displayCurrency,
displayAmountMinor: Math.round(rule.baseFareMinor * exchangeRate),
@@ -599,20 +577,20 @@ export class SearchService {
}
}
console.log(`No fares found via engine or rules for ${originStationId} to ${destinationStationId}`);
return [];
}
private async buildCoachTypeDetails(
schedule: any,
// buildCoachTypeDetails is pure in-memory — no async needed
private buildCoachTypeDetails(
schedule: ScheduleWithIncludes,
faresByClass: Array<{ seatClassName: string; baseFareMinor: number; displayCurrency: Currency; displayAmountMinor: number }>,
): Promise<Array<{
): Array<{
coachTypeId: string;
coachTypeName: string;
coachTypeCode: string;
coachId: string;
classes: Array<{ name: string; baseFareMinor: number; displayCurrency: Currency; displayAmountMinor: number }>;
}>> {
}> {
const coachTypeMap = new Map<
string,
{ coachType: any; classNames: Set<string>; coachId: string }
@@ -682,14 +660,6 @@ export class SearchService {
return fare.baseFarePerPassengerMinor;
}
private getDefaultFareForClass(_className: string): never {
throw new Error('getDefaultFareForClass should not be called — use resolveScheduleFare instead');
}
private defaultFare(_seatClassName: string): never {
throw new Error('defaultFare should not be called — use resolveScheduleFare instead');
}
private selectBestFareRule(
candidates: any[],
scheduleId: string,

View File

@@ -146,6 +146,96 @@ export class SegmentsService {
return true;
}
/**
* Batch availability check for multiple seats on a single schedule.
* Replaces N×isSeatFreeForLeg calls with 2 queries total.
* Returns a Set of seat IDs that are free for [reqFrom, reqTo).
*/
async getFreeSeatIds(
scheduleId: string,
seatIds: string[],
stopTimesForSeqLookup: ReadonlyArray<{ stationId: string; sequence: number }>,
reqFrom: number,
reqTo: number,
): Promise<Set<string>> {
if (seatIds.length === 0) return new Set();
const seqOf = (stationId: string) =>
stopTimesForSeqLookup.find(s => s.stationId === stationId)?.sequence;
const seatIdSet = new Set(seatIds);
const now = new Date();
const [allHolds, bookedLegs] = await Promise.all([
this.prisma.seatHold.findMany({
where: { scheduleId, expiresAt: { gt: now } },
select: { seatIds: true, createdBy: true },
}),
this.prisma.journeySegment.findMany({
where: {
scheduleId,
seatId: { in: seatIds },
journey: { status: { in: ['CONFIRMED', 'PENDING_PAYMENT'] } },
},
select: { seatId: true, journeyId: true, departureStationId: true, arrivalStationId: true },
}),
]);
// Determine which seats are blocked by active holds
const holdBlockedSeats = new Set<string>();
for (const hold of allHolds) {
let holdFrom: number | undefined;
let holdTo: number | undefined;
try {
if (hold.createdBy) {
const meta = JSON.parse(hold.createdBy as string);
holdFrom = seqOf(meta.originStationId);
holdTo = seqOf(meta.destinationStationId);
}
} catch { /* ignore */ }
for (const sid of hold.seatIds) {
if (!seatIdSet.has(sid)) continue;
// Conservative block if leg can't be resolved; otherwise check overlap
if (holdFrom === undefined || holdTo === undefined || (holdFrom < reqTo && reqFrom < holdTo)) {
holdBlockedSeats.add(sid);
}
}
}
// Build full journey ranges per seat (group multi-leg journeys)
const journeyRangesBySeat = new Map<string, Map<string, { from: number; to: number }>>();
for (const leg of bookedLegs) {
if (!leg.seatId || !leg.journeyId || !leg.departureStationId || !leg.arrivalStationId) continue;
const depSeq = seqOf(leg.departureStationId);
const arrSeq = seqOf(leg.arrivalStationId);
if (depSeq === undefined || arrSeq === undefined) continue;
let rangeMap = journeyRangesBySeat.get(leg.seatId);
if (!rangeMap) { rangeMap = new Map(); journeyRangesBySeat.set(leg.seatId, rangeMap); }
const existing = rangeMap.get(leg.journeyId);
rangeMap.set(leg.journeyId, existing
? { from: Math.min(existing.from, depSeq), to: Math.max(existing.to, arrSeq) }
: { from: depSeq, to: arrSeq });
}
const freeSeats = new Set<string>();
for (const seatId of seatIds) {
if (holdBlockedSeats.has(seatId)) continue;
let blocked = false;
const rangeMap = journeyRangesBySeat.get(seatId);
if (rangeMap) {
for (const { from, to } of rangeMap.values()) {
if (from < reqTo && reqFrom < to) { blocked = true; break; }
}
}
if (!blocked) freeSeats.add(seatId);
}
return freeSeats;
}
/** Legacy wrapper used by EnhancedSeatsService.getOverlappingReservations */
async getOverlappingReservations(
scheduleId: string,

View File

@@ -259,20 +259,14 @@ export default function SeatsPage() {
})),
});
const coachesWithSeats = coaches.filter(
(c: any) => c.seats && c.seats.length > 0,
);
if (!currentSchedule?.selectedSeatClass) {
console.log(
"✅ No filter applied, returning all coaches:",
coachesWithSeats.length,
);
return coachesWithSeats;
}
const coachesWithSeats = coaches.filter((c: any) => {
// Bed coaches store occupants in rooms.beds, not seats
if (c.rooms?.length > 0) return c.rooms.some((r: any) => r.beds?.length > 0);
return c.seats && c.seats.length > 0;
});
console.log(
"✅ No seat class filter - returning all coaches with seats:",
"✅ Returning all coaches with seats/beds:",
coachesWithSeats.length,
);
return coachesWithSeats;
@@ -333,6 +327,8 @@ export default function SeatsPage() {
return seatLabel && !seatLabel.startsWith("-");
});
const isBedCoach =
selectedCoachData?.isBedCoach === true ||
seats.some((s: any) => s.bedPosition) ||
selectedCoachData?.seatClass?.toLowerCase().includes("bed") ||
selectedCoachData?.mode?.toLowerCase().includes("bed");
@@ -1071,6 +1067,8 @@ export default function SeatsPage() {
const allSelected = selectedSeats.length === passengers.length;
const isBedCoach =
selectedCoachData?.isBedCoach === true ||
selectedCoachData?.rooms?.length > 0 ||
selectedCoachData?.seatClass?.toLowerCase().includes("bed") ||
selectedCoachData?.mode?.toLowerCase().includes("bed");
@@ -1314,6 +1312,8 @@ export default function SeatsPage() {
}
const isBed =
coach.isBedCoach === true ||
coach.rooms?.length > 0 ||
coach.seatClass?.toLowerCase().includes("bed") ||
coach.mode?.toLowerCase().includes("bed");

View File

@@ -4,7 +4,6 @@ import { Menu, X, Moon, Sun, HelpCircle } from "lucide-react";
import Link from "next/link";
import Image from "next/image";
import { useEffect, useState } from "react";
import { LanguageSwitcher } from "./LanguageSwitcher";
export default function AppHeader() {
const [isOpen, setIsOpen] = useState(false);
@@ -72,9 +71,6 @@ export default function AppHeader() {
<HelpCircle className="w-5 h-5" />
</Link>
{/* Language Switcher */}
<LanguageSwitcher />
{/* Theme Toggler */}
<button
onClick={toggleTheme}

View File

@@ -61,6 +61,9 @@ export class DMoneyProvider implements PaymentProvider {
): Promise<ProviderInitiationResult> {
const fabricToken = await this.applyFabricToken();
const requestBody = this.buildPreOrderRequest(input);
this.logger.log(
`D-Money preOrder send request merchOrderId=${input.merchantOrderId} body=${JSON.stringify(this.sanitize(requestBody))}`,
);
const response = await this.postJson<DMoneyPreOrderResponse>(
`${this.baseUrl}/apiaccess/payment/gateway/payment/v1/merchant/preOrder`,
requestBody,