mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
Update payment methods and cron job for payment cancellation
This commit is contained in:
@@ -483,8 +483,8 @@ export class BookingsService {
|
||||
}
|
||||
|
||||
const loyaltyMinor = (dto.loyaltyRedemptionPoints ?? 0) * 10;
|
||||
const taxesMinor = Math.round(combinedBaseFareMinor * 0.05);
|
||||
const totalMinor = Math.max(0, combinedBaseFareMinor - discountMinor - loyaltyMinor + taxesMinor);
|
||||
const taxesMinor = 0;
|
||||
const totalMinor = Math.max(0, combinedBaseFareMinor - discountMinor - loyaltyMinor);
|
||||
|
||||
const displayCurrency = dto.displayCurrency || Currency.ETB;
|
||||
let displayTotalMinor = totalMinor;
|
||||
@@ -660,8 +660,8 @@ export class BookingsService {
|
||||
}
|
||||
}
|
||||
const loyaltyMinor = (dto.loyaltyRedemptionPoints ?? 0) * 10;
|
||||
const taxesMinor = Math.round(combinedBase * 0.05);
|
||||
const totalMinor = Math.max(0, combinedBase - discountMinor - loyaltyMinor + taxesMinor);
|
||||
const taxesMinor = 0;
|
||||
const totalMinor = Math.max(0, combinedBase - discountMinor - loyaltyMinor);
|
||||
const displayCurrency = dto.displayCurrency || Currency.ETB;
|
||||
const displayTotalMinor = displayCurrency !== Currency.ETB
|
||||
? await this.currencyService.convertAmount(totalMinor, Currency.ETB, displayCurrency)
|
||||
@@ -854,8 +854,8 @@ export class BookingsService {
|
||||
}
|
||||
}
|
||||
const loyaltyMinor = (dto.loyaltyRedemptionPoints ?? 0) * 10;
|
||||
const taxesMinor = Math.round(combinedBase * 0.05);
|
||||
const totalMinor = Math.max(0, combinedBase - discountMinor - loyaltyMinor + taxesMinor);
|
||||
const taxesMinor = 0;
|
||||
const totalMinor = Math.max(0, combinedBase - discountMinor - loyaltyMinor);
|
||||
const displayCurrency = dto.displayCurrency || Currency.ETB;
|
||||
const displayTotalMinor = displayCurrency !== Currency.ETB
|
||||
? await this.currencyService.convertAmount(totalMinor, Currency.ETB, displayCurrency)
|
||||
@@ -1060,7 +1060,7 @@ export class BookingsService {
|
||||
loyaltyRedemptionPoints?: number
|
||||
) {
|
||||
const segmentRoute = `${originStop.station.code}-${destStop.station.code}`;
|
||||
const baseFareMinor = await this.getBaseFare(scheduleId, seatClassId, segmentRoute, undefined, nationality, originStop.sequence, destStop.sequence);
|
||||
const baseFareMinor = await this.getBaseFare(scheduleId, seatClassId, segmentRoute, undefined, nationality, originStop.sequence, destStop.sequence, originStop.stationId, destStop.stationId);
|
||||
|
||||
const adultFareMinor = baseFareMinor * adultCount;
|
||||
const paidChildrenCount = Math.max(0, childCount - 1);
|
||||
@@ -1076,8 +1076,8 @@ export class BookingsService {
|
||||
}
|
||||
|
||||
const loyaltyMinor = (loyaltyRedemptionPoints ?? 0) * 10;
|
||||
const taxesMinor = Math.round(totalBaseFareMinor * 0.05);
|
||||
const totalMinor = Math.max(0, totalBaseFareMinor - discountMinor - loyaltyMinor + taxesMinor);
|
||||
const taxesMinor = 0;
|
||||
const totalMinor = Math.max(0, totalBaseFareMinor - discountMinor - loyaltyMinor);
|
||||
|
||||
return {
|
||||
baseFareMinor,
|
||||
@@ -1103,6 +1103,8 @@ export class BookingsService {
|
||||
nationality?: string,
|
||||
originStopSeq?: number,
|
||||
destStopSeq?: number,
|
||||
originStationId?: string,
|
||||
destinationStationId?: string,
|
||||
): Promise<number> {
|
||||
const now = new Date();
|
||||
|
||||
@@ -1149,13 +1151,13 @@ export class BookingsService {
|
||||
const bestMatch = this.selectBestFareRule(candidates, scheduleId, segmentRoute, fullRoute, nationality);
|
||||
if (bestMatch) return bestMatch.baseFareMinor;
|
||||
|
||||
// 3. FareEngine — distance × rate-per-km from the schedule's route
|
||||
// 3. FareEngine — distance × rate-per-km from the booking's actual segment stations
|
||||
if (schedule?.routeId) {
|
||||
try {
|
||||
const fare = await this.fareEngine.calculate({
|
||||
routeId: schedule.routeId,
|
||||
originStationId: schedule.originStationId,
|
||||
destinationStationId: schedule.destinationStationId,
|
||||
originStationId: originStationId ?? schedule.originStationId,
|
||||
destinationStationId: destinationStationId ?? schedule.destinationStationId,
|
||||
seatClassId,
|
||||
nationality,
|
||||
});
|
||||
|
||||
@@ -9,6 +9,9 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { CreateGuestBookingDto, SavedPassengerProfileDto } from './guest-booking.dto';
|
||||
import { Currency, PassengerCategory, IdDocumentType } from '@prisma/client';
|
||||
|
||||
/** Booking cutoff: reject new bookings within this many ms of departure. */
|
||||
const BOOKING_CUTOFF_MS = 30 * 60 * 1000;
|
||||
|
||||
function generateRef(): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
return 'EDR-' + Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
|
||||
@@ -74,6 +77,10 @@ export class GuestBookingService {
|
||||
});
|
||||
if (!schedule) throw new NotFoundException('Schedule not found');
|
||||
|
||||
if (Date.now() >= schedule.departureAt.getTime() - BOOKING_CUTOFF_MS) {
|
||||
throw new BadRequestException('Bookings are not accepted within 30 minutes of departure');
|
||||
}
|
||||
|
||||
const originStop = schedule.stopTimes.find(s => s.stationId === dto.originStationId);
|
||||
const destStop = schedule.stopTimes.find(s => s.stationId === dto.destinationStationId);
|
||||
if (!originStop || !destStop) throw new NotFoundException('Origin or destination not found');
|
||||
@@ -142,7 +149,9 @@ export class GuestBookingService {
|
||||
dto.seatClassId,
|
||||
segmentRoute,
|
||||
fullRoute,
|
||||
primaryNationality
|
||||
primaryNationality,
|
||||
dto.originStationId,
|
||||
dto.destinationStationId,
|
||||
);
|
||||
|
||||
const adultFareMinor = baseFareMinor * adultCount;
|
||||
@@ -160,8 +169,8 @@ export class GuestBookingService {
|
||||
}
|
||||
}
|
||||
|
||||
const taxesMinor = Math.round(totalBaseFareMinor * 0.05);
|
||||
const totalMinor = Math.max(0, totalBaseFareMinor - discountMinor + taxesMinor);
|
||||
const taxesMinor = 0;
|
||||
const totalMinor = Math.max(0, totalBaseFareMinor - discountMinor);
|
||||
|
||||
const displayCurrency = dto.displayCurrency || Currency.ETB;
|
||||
let displayTotalMinor = totalMinor;
|
||||
@@ -293,6 +302,10 @@ export class GuestBookingService {
|
||||
if (!outboundSchedule) throw new NotFoundException('Outbound schedule not found');
|
||||
if (!returnSchedule) throw new NotFoundException('Return schedule not found');
|
||||
|
||||
if (Date.now() >= outboundSchedule.departureAt.getTime() - BOOKING_CUTOFF_MS) {
|
||||
throw new BadRequestException('Bookings are not accepted within 30 minutes of departure');
|
||||
}
|
||||
|
||||
const outboundOriginStop = outboundSchedule.stopTimes.find(s => s.stationId === dto.originStationId);
|
||||
const outboundDestStop = outboundSchedule.stopTimes.find(s => s.stationId === dto.destinationStationId);
|
||||
const returnOriginStop = returnSchedule.stopTimes.find(s => s.stationId === dto.returnOriginStationId);
|
||||
@@ -350,8 +363,8 @@ export class GuestBookingService {
|
||||
const primaryNationality = passengersData[0]?.nationality;
|
||||
|
||||
const [outboundBaseFare, returnBaseFare] = await Promise.all([
|
||||
this.getBaseFare(dto.scheduleId, dto.seatClassId, outboundSegmentRoute, outboundFullRoute, primaryNationality),
|
||||
this.getBaseFare(dto.returnScheduleId, returnSeatClassId, returnSegmentRoute, returnFullRoute, primaryNationality),
|
||||
this.getBaseFare(dto.scheduleId, dto.seatClassId, outboundSegmentRoute, outboundFullRoute, primaryNationality, dto.originStationId, dto.destinationStationId),
|
||||
this.getBaseFare(dto.returnScheduleId, returnSeatClassId, returnSegmentRoute, returnFullRoute, primaryNationality, dto.returnOriginStationId, dto.returnDestinationStationId),
|
||||
]);
|
||||
|
||||
const paidChildrenCount = Math.max(0, childCount - 1);
|
||||
@@ -369,8 +382,8 @@ export class GuestBookingService {
|
||||
}
|
||||
}
|
||||
|
||||
const taxesMinor = Math.round(combinedBaseFareMinor * 0.05);
|
||||
const totalMinor = Math.max(0, combinedBaseFareMinor - discountMinor + taxesMinor);
|
||||
const taxesMinor = 0;
|
||||
const totalMinor = Math.max(0, combinedBaseFareMinor - discountMinor);
|
||||
|
||||
const displayCurrency = dto.displayCurrency || Currency.ETB;
|
||||
const displayTotalMinor = displayCurrency !== Currency.ETB
|
||||
@@ -505,6 +518,10 @@ export class GuestBookingService {
|
||||
if (!leg1Schedule) throw new NotFoundException('Leg-1 schedule not found');
|
||||
if (!leg2Schedule) throw new NotFoundException('Leg-2 schedule not found');
|
||||
|
||||
if (Date.now() >= leg1Schedule.departureAt.getTime() - BOOKING_CUTOFF_MS) {
|
||||
throw new BadRequestException('Bookings are not accepted within 30 minutes of departure');
|
||||
}
|
||||
|
||||
const leg1OriginStop = leg1Schedule.stopTimes.find(s => s.stationId === dto.originStationId);
|
||||
const leg1DestStop = leg1Schedule.stopTimes.find(s => s.stationId === dto.transitStationId);
|
||||
const leg2OriginStop = leg2Schedule.stopTimes.find(s => s.stationId === dto.transitStationId);
|
||||
@@ -551,11 +568,11 @@ export class GuestBookingService {
|
||||
this.getBaseFare(dto.scheduleId, dto.seatClassId,
|
||||
`${leg1OriginStop.station.code}-${leg1DestStop.station.code}`,
|
||||
`${leg1Schedule.originStation.code}-${leg1Schedule.destinationStation.code}`,
|
||||
primaryNationality),
|
||||
primaryNationality, dto.originStationId, dto.transitStationId),
|
||||
this.getBaseFare(dto.leg2ScheduleId, leg2SeatClassId,
|
||||
`${leg2OriginStop.station.code}-${leg2DestStop.station.code}`,
|
||||
`${leg2Schedule.originStation.code}-${leg2Schedule.destinationStation.code}`,
|
||||
primaryNationality),
|
||||
primaryNationality, dto.transitStationId, dto.leg2DestinationStationId),
|
||||
]);
|
||||
|
||||
const leg1Total = leg1BaseFare * adultCount + leg1BaseFare * paidChildrenCount;
|
||||
@@ -569,8 +586,8 @@ export class GuestBookingService {
|
||||
discountMinor = promo.percentOff ? Math.round(combinedBase * promo.percentOff / 100) : (promo.amountOffMinor ?? 0);
|
||||
}
|
||||
}
|
||||
const taxesMinor = Math.round(combinedBase * 0.05);
|
||||
const totalMinor = Math.max(0, combinedBase - discountMinor + taxesMinor);
|
||||
const taxesMinor = 0;
|
||||
const totalMinor = Math.max(0, combinedBase - discountMinor);
|
||||
|
||||
const displayCurrency = dto.displayCurrency || Currency.ETB;
|
||||
const displayTotalMinor = displayCurrency !== Currency.ETB
|
||||
@@ -702,6 +719,10 @@ export class GuestBookingService {
|
||||
if (!retL1Sched) throw new NotFoundException('Return leg-1 schedule not found');
|
||||
if (!retL2Sched) throw new NotFoundException('Return leg-2 schedule not found');
|
||||
|
||||
if (Date.now() >= obL1Sched.departureAt.getTime() - BOOKING_CUTOFF_MS) {
|
||||
throw new BadRequestException('Bookings are not accepted within 30 minutes of departure');
|
||||
}
|
||||
|
||||
const obL1Origin = obL1Sched.stopTimes.find(s => s.stationId === dto.originStationId);
|
||||
const obL1Dest = obL1Sched.stopTimes.find(s => s.stationId === dto.transitStationId);
|
||||
const obL2Origin = obL2Sched.stopTimes.find(s => s.stationId === dto.transitStationId);
|
||||
@@ -750,10 +771,10 @@ export class GuestBookingService {
|
||||
const retL2ClassId = dto.returnLeg2SeatClassId ?? dto.seatClassId;
|
||||
|
||||
const [obL1Fare, obL2Fare, retL1Fare, retL2Fare] = await Promise.all([
|
||||
this.getBaseFare(dto.scheduleId, dto.seatClassId, `${obL1Origin.station.code}-${obL1Dest.station.code}`, `${obL1Sched.originStation.code}-${obL1Sched.destinationStation.code}`, nat),
|
||||
this.getBaseFare(dto.leg2ScheduleId!, obL2ClassId, `${obL2Origin.station.code}-${obL2Dest.station.code}`, `${obL2Sched.originStation.code}-${obL2Sched.destinationStation.code}`, nat),
|
||||
this.getBaseFare(dto.returnScheduleId!, retL1ClassId, `${retL1Origin.station.code}-${retL1Dest.station.code}`, `${retL1Sched.originStation.code}-${retL1Sched.destinationStation.code}`, nat),
|
||||
this.getBaseFare(dto.returnLeg2ScheduleId!,retL2ClassId, `${retL2Origin.station.code}-${retL2Dest.station.code}`, `${retL2Sched.originStation.code}-${retL2Sched.destinationStation.code}`, nat),
|
||||
this.getBaseFare(dto.scheduleId, dto.seatClassId, `${obL1Origin.station.code}-${obL1Dest.station.code}`, `${obL1Sched.originStation.code}-${obL1Sched.destinationStation.code}`, nat, dto.originStationId, dto.transitStationId),
|
||||
this.getBaseFare(dto.leg2ScheduleId!, obL2ClassId, `${obL2Origin.station.code}-${obL2Dest.station.code}`, `${obL2Sched.originStation.code}-${obL2Sched.destinationStation.code}`, nat, dto.transitStationId, dto.leg2DestinationStationId),
|
||||
this.getBaseFare(dto.returnScheduleId!, retL1ClassId, `${retL1Origin.station.code}-${retL1Dest.station.code}`, `${retL1Sched.originStation.code}-${retL1Sched.destinationStation.code}`, nat, dto.returnOriginStationId, dto.returnTransitStationId),
|
||||
this.getBaseFare(dto.returnLeg2ScheduleId!,retL2ClassId, `${retL2Origin.station.code}-${retL2Dest.station.code}`, `${retL2Sched.originStation.code}-${retL2Sched.destinationStation.code}`, nat, dto.returnTransitStationId, dto.returnLeg2DestinationStationId),
|
||||
]);
|
||||
|
||||
const combinedBase = (obL1Fare + obL2Fare + retL1Fare + retL2Fare) * adultCount +
|
||||
@@ -951,17 +972,28 @@ export class GuestBookingService {
|
||||
segmentRoute?: string,
|
||||
fullRoute?: string,
|
||||
nationality?: string,
|
||||
originStationId?: string,
|
||||
destinationStationId?: string,
|
||||
): Promise<number> {
|
||||
const now = new Date();
|
||||
|
||||
// 1. FareRule table — explicit override rules
|
||||
const candidates = await this.prisma.fareRule.findMany({
|
||||
where: {
|
||||
seatClassId,
|
||||
validFrom: { lte: now },
|
||||
OR: [{ validUntil: null }, { validUntil: { gte: now } }],
|
||||
},
|
||||
});
|
||||
// 1. FareRule table — explicit override rules (same priority logic as the fare engine)
|
||||
const [candidates, seatClass] = await Promise.all([
|
||||
this.prisma.fareRule.findMany({
|
||||
where: {
|
||||
seatClassId,
|
||||
validFrom: { lte: now },
|
||||
OR: [{ validUntil: null }, { validUntil: { gte: now } }],
|
||||
},
|
||||
}),
|
||||
this.prisma.seatClass.findUnique({
|
||||
where: { id: seatClassId },
|
||||
select: { premiumMinor: true, insuranceFeeMinor: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
const premiumMinor = seatClass?.premiumMinor ?? 0;
|
||||
const insuranceMinor = seatClass?.insuranceFeeMinor ?? 0;
|
||||
|
||||
const priorities = [
|
||||
{ tripId: scheduleId, route: segmentRoute, nationality },
|
||||
@@ -982,10 +1014,11 @@ export class GuestBookingService {
|
||||
const match = candidates.find(
|
||||
(c) => c.tripId === priority.tripId && c.route === priority.route && c.nationality === priority.nationality,
|
||||
);
|
||||
if (match) return match.baseFareMinor;
|
||||
// Return base fare + seat-class surcharges so the booking total matches the quoted fare
|
||||
if (match) return match.baseFareMinor + premiumMinor + insuranceMinor;
|
||||
}
|
||||
|
||||
// 2. FareEngine — distance × rate-per-km from the schedule's route
|
||||
// 2. FareEngine — distance × rate-per-km from the booking's actual segment stations
|
||||
const schedule = await this.prisma.trainSchedule.findUnique({
|
||||
where: { id: scheduleId },
|
||||
select: { routeId: true, originStationId: true, destinationStationId: true },
|
||||
@@ -995,12 +1028,15 @@ export class GuestBookingService {
|
||||
try {
|
||||
const fare = await this.fareEngine.calculate({
|
||||
routeId: schedule.routeId,
|
||||
originStationId: schedule.originStationId,
|
||||
destinationStationId: schedule.destinationStationId,
|
||||
// Use the booking's boarding/alighting stations so the distance reflects the
|
||||
// passenger's actual segment, not the full schedule route.
|
||||
originStationId: originStationId ?? schedule.originStationId,
|
||||
destinationStationId: destinationStationId ?? schedule.destinationStationId,
|
||||
seatClassId,
|
||||
nationality,
|
||||
});
|
||||
return fare.baseFarePerPassengerMinor;
|
||||
// farePerPassengerMinor already includes base + premiumMinor + insuranceFeeMinor
|
||||
return fare.farePerPassengerMinor;
|
||||
} catch {
|
||||
// FareEngine throws if distanceKm is missing; fall through to error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user