mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
Initial commit of edr-passenger-api alpha version
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { BookingsService } from './bookings.service';
|
||||
import { CreateBookingDto } from './bookings.dto';
|
||||
import { JwtGuard } from '../../common/jwt.guard';
|
||||
|
||||
@ApiTags('Booking')
|
||||
@Controller('bookings')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
export class BookingsController {
|
||||
constructor(private service: BookingsService) {}
|
||||
@Post() @ApiOperation({ summary: 'Create booking from seat hold' }) create(@Body() dto: CreateBookingDto) { return this.service.create(dto); }
|
||||
@Get(':bookingRef') @ApiOperation({ summary: 'Get booking by reference' }) getByRef(@Param('bookingRef') ref: string) { return this.service.getByRef(ref); }
|
||||
@Delete(':bookingRef')@ApiOperation({ summary: 'Cancel booking' }) cancel(@Param('bookingRef') ref: string) { return this.service.cancel(ref); }
|
||||
}
|
||||
22
apps/edr-passenger-api/src/modules/bookings/bookings.dto.ts
Normal file
22
apps/edr-passenger-api/src/modules/bookings/bookings.dto.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { IsString, IsArray, ValidateNested, IsOptional, IsInt, IsEnum } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class PassengerInputDto {
|
||||
@ApiProperty() @IsString() fullName: string;
|
||||
@ApiProperty() @IsString() phone: string;
|
||||
@ApiProperty() @IsString() email: string;
|
||||
@ApiProperty() @IsString() seatId: string;
|
||||
@ApiPropertyOptional() @IsOptional() @IsString() idDocumentType?: string;
|
||||
@ApiPropertyOptional() @IsOptional() @IsString() idDocumentNumber?: string;
|
||||
}
|
||||
|
||||
export class CreateBookingDto {
|
||||
@ApiProperty() @IsString() passengerId: string;
|
||||
@ApiProperty() @IsString() tripId: string;
|
||||
@ApiProperty() @IsString() holdId: string;
|
||||
@ApiProperty({ type: [PassengerInputDto] }) @IsArray() @ValidateNested({ each: true }) @Type(() => PassengerInputDto) passengers: PassengerInputDto[];
|
||||
@ApiPropertyOptional({ example: 'ECONOMY', enum: ['ECONOMY', 'BUSINESS', 'FIRST'] }) @IsOptional() @IsString() serviceClass?: string;
|
||||
@ApiPropertyOptional() @IsOptional() @IsString() promoCode?: string;
|
||||
@ApiPropertyOptional() @IsOptional() @IsInt() loyaltyRedemptionPoints?: number;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BookingsController } from './bookings.controller';
|
||||
import { BookingsService } from './bookings.service';
|
||||
import { SeatsModule } from '../seats/seats.module';
|
||||
import { SearchModule } from '../search/search.module';
|
||||
|
||||
@Module({ imports: [SeatsModule, SearchModule], controllers: [BookingsController], providers: [BookingsService], exports: [BookingsService] })
|
||||
export class BookingsModule {}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
import { SeatsService } from '../seats/seats.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { CreateBookingDto } from './bookings.dto';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { SearchService } from '../search/search.service';
|
||||
|
||||
function generateRef(): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
return Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class BookingsService {
|
||||
constructor(private prisma: PrismaService, private seatsService: SeatsService, private eventEmitter: EventEmitter2, private searchService: SearchService) {}
|
||||
|
||||
async create(dto: CreateBookingDto) {
|
||||
const hold = await this.prisma.seatHold.findUnique({ where: { id: dto.holdId } });
|
||||
if (!hold || hold.expiresAt < new Date()) throw new BadRequestException('Seat hold expired');
|
||||
const trip = await this.prisma.trip.findUnique({ where: { id: dto.tripId }, include: { originStation: true, destinationStation: true } });
|
||||
if (!trip) throw new NotFoundException('Trip not found');
|
||||
const fareQuote = await this.searchService.getFareQuote({ tripId: dto.tripId, serviceClass: dto.serviceClass ?? 'ECONOMY', passengerCount: dto.passengers.length, promoCode: dto.promoCode, loyaltyRedemptionPoints: dto.loyaltyRedemptionPoints });
|
||||
const booking = await this.prisma.booking.create({
|
||||
data: { bookingRef: generateRef(), passengerId: dto.passengerId, tripId: dto.tripId, status: 'PENDING_PAYMENT', totalMinor: fareQuote.totalMinor, seats: { create: dto.passengers.map((p) => ({ seatId: p.seatId, passengerName: p.fullName, idDocumentType: p.idDocumentType, idDocumentNumber: p.idDocumentNumber })) } },
|
||||
include: { seats: { include: { seat: true } }, trip: { include: { originStation: true, destinationStation: true, service: true } } },
|
||||
});
|
||||
this.eventEmitter.emit('booking.created', { booking });
|
||||
return booking;
|
||||
}
|
||||
|
||||
async getByRef(bookingRef: string) {
|
||||
const booking = await this.prisma.booking.findUnique({ where: { bookingRef }, include: { trip: { include: { originStation: true, destinationStation: true, service: true } }, seats: { include: { seat: { include: { coach: true } } } }, paymentIntent: true, ticket: true } });
|
||||
if (!booking) throw new NotFoundException('Booking not found');
|
||||
return {
|
||||
id: booking.id,
|
||||
bookingRef: booking.bookingRef,
|
||||
status: booking.status,
|
||||
totalFare: booking.totalMinor / 100,
|
||||
createdAt: booking.createdAt,
|
||||
trip: {
|
||||
number: booking.trip.service.number,
|
||||
origin: { id: booking.trip.originStation.id, name: booking.trip.originStation.name, code: booking.trip.originStation.code, city: booking.trip.originStation.city },
|
||||
destination: { id: booking.trip.destinationStation.id, name: booking.trip.destinationStation.name, code: booking.trip.destinationStation.code, city: booking.trip.destinationStation.city },
|
||||
departureAt: booking.trip.departureAt,
|
||||
arrivalAt: booking.trip.arrivalAt,
|
||||
},
|
||||
passengers: booking.seats.map((bs) => ({
|
||||
fullName: bs.passengerName,
|
||||
seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.serviceClass },
|
||||
})),
|
||||
payment: booking.paymentIntent ? { method: booking.paymentIntent.method, status: booking.paymentIntent.status } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async cancel(bookingRef: string) {
|
||||
const booking = await this.prisma.booking.findUnique({ where: { bookingRef }, include: { seats: true } });
|
||||
if (!booking) throw new NotFoundException('Booking not found');
|
||||
if (booking.status === 'CONFIRMED') throw new BadRequestException('Use refund for confirmed bookings');
|
||||
await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId));
|
||||
return this.prisma.booking.update({ where: { bookingRef }, data: { status: 'CANCELLED' } });
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
async expirePendingBookings() {
|
||||
const cutoff = new Date(Date.now() - 20 * 60 * 1000);
|
||||
const expired = await this.prisma.booking.findMany({ where: { status: 'PENDING_PAYMENT', createdAt: { lt: cutoff } }, include: { seats: true } });
|
||||
for (const b of expired) { await this.seatsService.releaseSeats(b.seats.map((s) => s.seatId)); await this.prisma.booking.update({ where: { id: b.id }, data: { status: 'CANCELLED' } }); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user