mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 18:20:57 +00:00
Merge branch 'alpha' of https://github.com/Tria-plc/edr-platform into alpha
This commit is contained in:
@@ -31,7 +31,7 @@ export class CurrenciesController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@PassengerAdmin()
|
||||
@PassengerStaff(PASSENGER_PERMS.currencies.manage)
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
deleteCurrency(@Param('id') id: string) {
|
||||
return this.currenciesService.deleteCurrency(id);
|
||||
|
||||
@@ -13,7 +13,7 @@ export class CurrenciesService {
|
||||
async getAllCurrencies() {
|
||||
const rates = await this.prisma.currencyExchangeRate.findMany({
|
||||
distinct: ['toCurrency'],
|
||||
orderBy: { toCurrency: 'asc' },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
const base = {
|
||||
@@ -83,12 +83,14 @@ export class CurrenciesService {
|
||||
throw new BadRequestException('Exchange rate must be positive');
|
||||
}
|
||||
|
||||
const updated = await this.prisma.currencyExchangeRate.update({
|
||||
where: { id },
|
||||
data: {
|
||||
rate: dto.exchangeRate,
|
||||
},
|
||||
});
|
||||
// Upsert today's record so getRateOrThrow (orderBy effectiveDate desc) picks it up
|
||||
const updated = await this.currencyService.upsertRate(
|
||||
existing.fromCurrency,
|
||||
existing.toCurrency,
|
||||
dto.exchangeRate ?? Number(existing.rate),
|
||||
undefined,
|
||||
'MANUAL',
|
||||
);
|
||||
|
||||
return {
|
||||
id: updated.id,
|
||||
@@ -112,8 +114,9 @@ export class CurrenciesService {
|
||||
throw new NotFoundException('Currency not found');
|
||||
}
|
||||
|
||||
await this.prisma.currencyExchangeRate.delete({
|
||||
where: { id },
|
||||
// Delete all records for this currency pair so no stale rates remain
|
||||
await this.prisma.currencyExchangeRate.deleteMany({
|
||||
where: { fromCurrency: existing.fromCurrency, toCurrency: existing.toCurrency },
|
||||
});
|
||||
|
||||
return { message: 'Currency deleted successfully' };
|
||||
|
||||
@@ -99,6 +99,8 @@ export class PaymentsService {
|
||||
priceTierId: true,
|
||||
adultCount: true,
|
||||
childCount: true,
|
||||
totalMinor: true,
|
||||
currency: true,
|
||||
priceTier: { select: { priceMinor: true } },
|
||||
},
|
||||
},
|
||||
@@ -129,7 +131,7 @@ export class PaymentsService {
|
||||
id: item.id,
|
||||
reference: item.id.substring(0, 8),
|
||||
bookingId: item.bookingId,
|
||||
booking: { bookingRef: b?.bookingRef },
|
||||
booking: { bookingRef: b?.bookingRef, totalMinor: b?.totalMinor, currency: b?.currency },
|
||||
amountMinor,
|
||||
currency: item.currency,
|
||||
method: item.method,
|
||||
|
||||
@@ -837,13 +837,16 @@ export class SeatsService {
|
||||
async removeSeat(seatId: string) {
|
||||
const seat = await this.prisma.seat.findUnique({ where: { id: seatId } });
|
||||
if (!seat) throw new NotFoundException('Seat not found');
|
||||
if (!seat.seatNumber) throw new BadRequestException('Seat already removed');
|
||||
if (!seat.seatNumber || seat.seatNumber.startsWith('-')) throw new BadRequestException('Seat already removed');
|
||||
|
||||
// Mark as removed, then renumber all active seats in the coach
|
||||
await this.prisma.seat.update({
|
||||
where: { id: seatId },
|
||||
data: { seatNumber: `-${seat.seatNumber}` },
|
||||
});
|
||||
|
||||
await this.renumberCoachSeats(seat.coachId);
|
||||
|
||||
return { removed: true, seatId, originalSeatNumber: seat.seatNumber };
|
||||
}
|
||||
|
||||
@@ -854,10 +857,38 @@ export class SeatsService {
|
||||
throw new BadRequestException('Seat is not removed');
|
||||
}
|
||||
|
||||
const originalNumber = seat.seatNumber.slice(1);
|
||||
await this.prisma.seat.update({ where: { id: seatId }, data: { seatNumber: originalNumber } });
|
||||
// Restore with a temporary placeholder number, then renumber
|
||||
await this.prisma.seat.update({ where: { id: seatId }, data: { seatNumber: `__restore__${seatId}` } });
|
||||
await this.renumberCoachSeats(seat.coachId);
|
||||
|
||||
return { restored: true, seatId, seatNumber: originalNumber };
|
||||
const restored = await this.prisma.seat.findUnique({ where: { id: seatId } });
|
||||
return { restored: true, seatId, seatNumber: restored?.seatNumber };
|
||||
}
|
||||
|
||||
/**
|
||||
* Renumbers all active (non-removed) seats in a coach sequentially starting from 1,
|
||||
* ordered by row then col. Removed seats (prefixed with "-") keep their slot but
|
||||
* are excluded from the numbering sequence so numbers remain continuous.
|
||||
*/
|
||||
private async renumberCoachSeats(coachId: string): Promise<void> {
|
||||
const allSeats = await this.prisma.seat.findMany({
|
||||
where: { coachId },
|
||||
orderBy: [{ row: 'asc' }, { col: 'asc' }],
|
||||
select: { id: true, seatNumber: true },
|
||||
});
|
||||
|
||||
const activeSeats = allSeats.filter(
|
||||
(s) => s.seatNumber && !s.seatNumber.startsWith('-') && !s.seatNumber.startsWith('__restore__'),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
activeSeats.map((s, idx) =>
|
||||
this.prisma.seat.update({
|
||||
where: { id: s.id },
|
||||
data: { seatNumber: String(idx + 1) },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PrismaService } from '../../common/prisma.service';
|
||||
export const CONFIG_KEYS = {
|
||||
SEAT_HOLD_DURATION_MINUTES: 'seat_hold_duration_minutes',
|
||||
HOLD_CUTOFF_HOURS_BEFORE_DEPARTURE: 'hold_cutoff_hours_before_departure',
|
||||
BOARDING_WINDOW_HOURS_BEFORE_DEPARTURE: 'boarding_window_hours_before_departure',
|
||||
THROTTLE_AUTH_LIMIT: 'throttle_auth_limit',
|
||||
THROTTLE_AUTH_TTL_MS: 'throttle_auth_ttl_ms',
|
||||
THROTTLE_STRICT_LIMIT: 'throttle_strict_limit',
|
||||
@@ -15,6 +16,7 @@ export const CONFIG_KEYS = {
|
||||
const DEFAULTS: Record<string, string> = {
|
||||
[CONFIG_KEYS.SEAT_HOLD_DURATION_MINUTES]: '5',
|
||||
[CONFIG_KEYS.HOLD_CUTOFF_HOURS_BEFORE_DEPARTURE]: '2',
|
||||
[CONFIG_KEYS.BOARDING_WINDOW_HOURS_BEFORE_DEPARTURE]: '4',
|
||||
[CONFIG_KEYS.THROTTLE_AUTH_LIMIT]: '5',
|
||||
[CONFIG_KEYS.THROTTLE_AUTH_TTL_MS]: '60000',
|
||||
[CONFIG_KEYS.THROTTLE_STRICT_LIMIT]: '20',
|
||||
|
||||
@@ -3,9 +3,10 @@ import { TicketsController } from './tickets.controller';
|
||||
import { TicketsService } from './tickets.service';
|
||||
import { JwtGuard } from '../../common/jwt.guard';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { SystemConfigModule } from '../system-config/system-config.module';
|
||||
|
||||
@Module({
|
||||
imports: [NotificationsModule],
|
||||
imports: [NotificationsModule, SystemConfigModule],
|
||||
controllers: [TicketsController],
|
||||
providers: [TicketsService, JwtGuard],
|
||||
exports: [TicketsService, JwtGuard],
|
||||
|
||||
@@ -3,6 +3,7 @@ import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { SystemConfigService, CONFIG_KEYS } from '../system-config/system-config.service';
|
||||
import * as QRCode from 'qrcode';
|
||||
|
||||
interface OfflineValidation {
|
||||
@@ -20,6 +21,7 @@ export class TicketsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly notifications: NotificationsService,
|
||||
private readonly systemConfig: SystemConfigService,
|
||||
@InjectDataSource() private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
@@ -423,26 +425,20 @@ export class TicketsService {
|
||||
|
||||
// Check if ticket date matches today
|
||||
const today = new Date();
|
||||
const todayDateStr = today.toISOString().split('T')[0]; // YYYY-MM-DD format
|
||||
|
||||
if ((booking as any).schedule?.departureAt) {
|
||||
const departureDate = new Date((booking as any).schedule.departureAt);
|
||||
const departureDateStr = departureDate.toISOString().split('T')[0];
|
||||
|
||||
// Check if ticket is for today
|
||||
if (departureDateStr !== todayDateStr) {
|
||||
if (departureDateStr < todayDateStr) {
|
||||
throw new BadRequestException('Ticket has expired - departure date has passed');
|
||||
} else {
|
||||
throw new BadRequestException('Ticket is for a future date - cannot board early');
|
||||
}
|
||||
}
|
||||
|
||||
// Additional check: ticket expires 4 hours after departure time
|
||||
const departureTime = new Date((booking as any).schedule.departureAt);
|
||||
const expiryTime = new Date(departureTime.getTime() + 4 * 60 * 60 * 1000); // 4 hours after departure
|
||||
if (today > expiryTime) {
|
||||
throw new BadRequestException('Ticket has expired - boarding window closed');
|
||||
const boardingWindowHours = await this.systemConfig.getNumber(CONFIG_KEYS.BOARDING_WINDOW_HOURS_BEFORE_DEPARTURE);
|
||||
const boardingOpenTime = new Date(departureTime.getTime() - boardingWindowHours * 60 * 60 * 1000);
|
||||
|
||||
if (today < boardingOpenTime) {
|
||||
throw new BadRequestException(
|
||||
`Boarding opens ${boardingWindowHours} hour(s) before departure at ${boardingOpenTime.toISOString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (today >= departureTime) {
|
||||
throw new BadRequestException('Boarding is closed — departure time has passed');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user