mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 04:08:11 +00:00
Initial commit of edr-passenger-api alpha version
This commit is contained in:
@@ -1,26 +0,0 @@
|
||||
import { BaseEntity } from "@edr/api-common";
|
||||
import { Passenger } from "@edr/types";
|
||||
import { Column, Entity } from "typeorm";
|
||||
|
||||
@Entity({ name: "seats" })
|
||||
export class Seat extends BaseEntity {
|
||||
@Column({ name: "schedule_id", type: "uuid" })
|
||||
scheduleId!: string;
|
||||
|
||||
@Column({ name: "seat_number", type: "varchar", length: 16 })
|
||||
seatNumber!: string;
|
||||
|
||||
@Column({ name: "seat_class", type: "enum", enum: Passenger.SeatClass })
|
||||
seatClass!: Passenger.SeatClass;
|
||||
|
||||
@Column({
|
||||
name: "status",
|
||||
type: "enum",
|
||||
enum: Passenger.SeatStatus,
|
||||
default: Passenger.SeatStatus.Available,
|
||||
})
|
||||
status!: Passenger.SeatStatus;
|
||||
|
||||
@Column({ name: "price", type: "numeric", precision: 10, scale: 2 })
|
||||
price!: number;
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Controller, Get, Param, ParseUUIDPipe } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { SeatsService } from './seats.service';
|
||||
import { HoldSeatsDto } from './seats.dto';
|
||||
import { JwtGuard } from '../../common/jwt.guard';
|
||||
|
||||
import { SeatsService } from "./seats.service";
|
||||
|
||||
@ApiTags("seats")
|
||||
// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth
|
||||
@Controller("seats")
|
||||
@ApiTags('Seats')
|
||||
@Controller('seats')
|
||||
export class SeatsController {
|
||||
constructor(private readonly seatsService: SeatsService) {}
|
||||
|
||||
@Get("schedule/:scheduleId")
|
||||
@ApiOperation({ summary: "List seats for a schedule" })
|
||||
findBySchedule(@Param("scheduleId", ParseUUIDPipe) scheduleId: string) {
|
||||
return this.seatsService.findBySchedule(scheduleId);
|
||||
}
|
||||
constructor(private service: SeatsService) {}
|
||||
@Get('seatmap/:tripId') @ApiOperation({ summary: 'Get seat map for a trip' })
|
||||
getSeatMap(@Param('tripId') tripId: string, @Query('coachId') coachId?: string) { return this.service.getSeatMap(tripId, coachId); }
|
||||
@Post('hold') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Hold seats for 15 minutes' })
|
||||
holdSeats(@Body() dto: HoldSeatsDto) { return this.service.holdSeats(dto); }
|
||||
@Delete('hold/:holdId') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Release a seat hold' })
|
||||
releaseHold(@Param('holdId') holdId: string) { return this.service.releaseHold(holdId); }
|
||||
}
|
||||
|
||||
9
apps/edr-passenger-api/src/modules/seats/seats.dto.ts
Normal file
9
apps/edr-passenger-api/src/modules/seats/seats.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { IsString, IsArray } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class HoldSeatsDto {
|
||||
@ApiProperty() @IsString() tripId: string;
|
||||
@ApiProperty() @IsString() passengerId: string;
|
||||
@ApiProperty({ type: [String] }) @IsArray() seatIds: string[];
|
||||
@ApiProperty({ required: false }) fareQuoteId?: string;
|
||||
}
|
||||
@@ -1,14 +1,6 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SeatsController } from './seats.controller';
|
||||
import { SeatsService } from './seats.service';
|
||||
|
||||
import { Seat } from "./entities/seat.entity";
|
||||
import { SeatsController } from "./seats.controller";
|
||||
import { SeatsService } from "./seats.service";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Seat])],
|
||||
controllers: [SeatsController],
|
||||
providers: [SeatsService],
|
||||
exports: [SeatsService],
|
||||
})
|
||||
@Module({ controllers: [SeatsController], providers: [SeatsService], exports: [SeatsService] })
|
||||
export class SeatsModule {}
|
||||
|
||||
@@ -1,21 +1,50 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
|
||||
import { Seat } from "./entities/seat.entity";
|
||||
import { Injectable, ConflictException, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
import { HoldSeatsDto } from './seats.dto';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
|
||||
@Injectable()
|
||||
export class SeatsService {
|
||||
constructor(
|
||||
@InjectRepository(Seat)
|
||||
private readonly seatsRepository: Repository<Seat>,
|
||||
) {}
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
/** List every seat on a given schedule, ordered by seat number. */
|
||||
findBySchedule(scheduleId: string): Promise<Seat[]> {
|
||||
return this.seatsRepository.find({
|
||||
where: { scheduleId },
|
||||
order: { seatNumber: "ASC" },
|
||||
async getSeatMap(tripId: string, coachId?: string) {
|
||||
const coaches = await this.prisma.coach.findMany({ where: { tripId, ...(coachId ? { id: coachId } : {}) }, include: { seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] } } });
|
||||
return {
|
||||
coaches: coaches.map((coach) => ({
|
||||
id: coach.id,
|
||||
name: `Coach ${coach.label}`,
|
||||
type: coach.serviceClass,
|
||||
seats: coach.seats.map((s) => ({ id: s.id, number: s.label, status: s.status, kind: s.kind })),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async holdSeats(dto: HoldSeatsDto) {
|
||||
const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
|
||||
const hold = await this.prisma.$transaction(async (tx) => {
|
||||
const seats = await tx.seat.findMany({ where: { id: { in: dto.seatIds } }, select: { id: true, status: true, heldUntil: true } });
|
||||
const unavailable = seats.filter((s) => s.status === 'BOOKED' || s.status === 'BLOCKED' || (s.status === 'HELD' && s.heldUntil && s.heldUntil > new Date()));
|
||||
if (unavailable.length > 0) throw new ConflictException('One or more seats unavailable');
|
||||
await tx.seat.updateMany({ where: { id: { in: dto.seatIds } }, data: { status: 'HELD', heldUntil: expiresAt } });
|
||||
return tx.seatHold.create({ data: { tripId: dto.tripId, passengerId: dto.passengerId, seatIds: dto.seatIds, fareQuoteId: dto.fareQuoteId, expiresAt } });
|
||||
});
|
||||
return { id: hold.id, tripId: dto.tripId, seatIds: dto.seatIds, expiresAt };
|
||||
}
|
||||
|
||||
async releaseHold(holdId: string) {
|
||||
const hold = await this.prisma.seatHold.findUnique({ where: { id: holdId } });
|
||||
if (!hold) throw new NotFoundException('Hold not found');
|
||||
await this.prisma.seat.updateMany({ where: { id: { in: hold.seatIds }, status: 'HELD' }, data: { status: 'AVAILABLE', heldUntil: null } });
|
||||
await this.prisma.seatHold.delete({ where: { id: holdId } });
|
||||
return { released: true };
|
||||
}
|
||||
|
||||
async confirmSeats(seatIds: string[]) { await this.prisma.seat.updateMany({ where: { id: { in: seatIds } }, data: { status: 'BOOKED', heldUntil: null } }); }
|
||||
async releaseSeats(seatIds: string[]) { await this.prisma.seat.updateMany({ where: { id: { in: seatIds } }, data: { status: 'AVAILABLE', heldUntil: null } }); }
|
||||
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
async expireHolds() {
|
||||
const expired = await this.prisma.seatHold.findMany({ where: { expiresAt: { lt: new Date() } } });
|
||||
for (const hold of expired) { await this.releaseSeats(hold.seatIds); await this.prisma.seatHold.delete({ where: { id: hold.id } }); }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user