mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
Updated seat class and coach mangement
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "SeatClass" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"basePrice" INTEGER NOT NULL,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "SeatClass_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "SeatClass_name_key" ON "SeatClass"("name");
|
||||||
|
|
||||||
|
-- Seed default seat classes so existing coaches can be migrated
|
||||||
|
INSERT INTO "SeatClass" ("id", "name", "description", "basePrice", "isActive", "createdAt", "updatedAt")
|
||||||
|
VALUES
|
||||||
|
('sc_economy', 'Economy Seat', 'Standard economy seating', 45000, true, NOW(), NOW()),
|
||||||
|
('sc_business', 'Business Seat','Comfortable business class', 90000, true, NOW(), NOW()),
|
||||||
|
('sc_first', 'VIP Bed', 'First class VIP bed', 135000, true, NOW(), NOW());
|
||||||
|
|
||||||
|
-- Add seatClassId column to Coach (nullable first for migration safety)
|
||||||
|
ALTER TABLE "Coach" ADD COLUMN "seatClassId" TEXT;
|
||||||
|
|
||||||
|
-- Map existing serviceClass enum values to new SeatClass ids
|
||||||
|
UPDATE "Coach" SET "seatClassId" = 'sc_economy' WHERE "serviceClass" = 'ECONOMY';
|
||||||
|
UPDATE "Coach" SET "seatClassId" = 'sc_business' WHERE "serviceClass" = 'BUSINESS';
|
||||||
|
UPDATE "Coach" SET "seatClassId" = 'sc_first' WHERE "serviceClass" = 'FIRST';
|
||||||
|
|
||||||
|
-- Make seatClassId NOT NULL now that all rows are populated
|
||||||
|
ALTER TABLE "Coach" ALTER COLUMN "seatClassId" SET NOT NULL;
|
||||||
|
|
||||||
|
-- Drop old serviceClass column
|
||||||
|
ALTER TABLE "Coach" DROP COLUMN "serviceClass";
|
||||||
|
|
||||||
|
-- Add seatClassId to FareRule
|
||||||
|
ALTER TABLE "FareRule" ADD COLUMN "seatClassId" TEXT;
|
||||||
|
|
||||||
|
UPDATE "FareRule" SET "seatClassId" = 'sc_economy' WHERE "serviceClass" = 'ECONOMY';
|
||||||
|
UPDATE "FareRule" SET "seatClassId" = 'sc_business' WHERE "serviceClass" = 'BUSINESS';
|
||||||
|
UPDATE "FareRule" SET "seatClassId" = 'sc_first' WHERE "serviceClass" = 'FIRST';
|
||||||
|
|
||||||
|
ALTER TABLE "FareRule" ALTER COLUMN "seatClassId" SET NOT NULL;
|
||||||
|
ALTER TABLE "FareRule" DROP COLUMN "serviceClass";
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Coach" ADD CONSTRAINT "Coach_seatClassId_fkey"
|
||||||
|
FOREIGN KEY ("seatClassId") REFERENCES "SeatClass"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE "FareRule" ADD CONSTRAINT "FareRule_seatClassId_fkey"
|
||||||
|
FOREIGN KEY ("seatClassId") REFERENCES "SeatClass"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "SeatClass" ALTER COLUMN "updatedAt" SET DEFAULT NOW();
|
||||||
@@ -35,10 +35,16 @@ enum SeatStatus {
|
|||||||
BLOCKED
|
BLOCKED
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ServiceClass {
|
model SeatClass {
|
||||||
ECONOMY
|
id String @id @default(uuid())
|
||||||
BUSINESS
|
name String @unique
|
||||||
FIRST
|
description String?
|
||||||
|
basePrice Int
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
coaches Coach[]
|
||||||
|
fareRules FareRule[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BookingStatus {
|
enum BookingStatus {
|
||||||
@@ -249,12 +255,13 @@ model TripLiveStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Coach {
|
model Coach {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
tripId String
|
tripId String
|
||||||
label String
|
label String
|
||||||
serviceClass ServiceClass
|
seatClassId String
|
||||||
trip Trip @relation(fields: [tripId], references: [id])
|
trip Trip @relation(fields: [tripId], references: [id])
|
||||||
seats Seat[]
|
seatClass SeatClass @relation(fields: [seatClassId], references: [id])
|
||||||
|
seats Seat[]
|
||||||
@@unique([tripId, label])
|
@@unique([tripId, label])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,11 +290,12 @@ model SeatHold {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model FareRule {
|
model FareRule {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
tripId String?
|
tripId String?
|
||||||
route String?
|
route String?
|
||||||
serviceClass ServiceClass
|
seatClassId String
|
||||||
baseFareMinor Int
|
baseFareMinor Int
|
||||||
|
seatClass SeatClass @relation(fields: [seatClassId], references: [id])
|
||||||
currency String @default("ETB")
|
currency String @default("ETB")
|
||||||
refundable Boolean @default(true)
|
refundable Boolean @default(true)
|
||||||
validFrom DateTime
|
validFrom DateTime
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { LiveModule } from './modules/live/live.module';
|
|||||||
import { SupportModule } from './modules/support/support.module';
|
import { SupportModule } from './modules/support/support.module';
|
||||||
import { DashboardModule } from './modules/dashboard/dashboard.module';
|
import { DashboardModule } from './modules/dashboard/dashboard.module';
|
||||||
import { SegmentsModule } from './modules/segments/segments.module';
|
import { SegmentsModule } from './modules/segments/segments.module';
|
||||||
|
import { SeatClassesModule } from './modules/seat-classes/seat-classes.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -48,6 +49,7 @@ import { SegmentsModule } from './modules/segments/segments.module';
|
|||||||
SupportModule,
|
SupportModule,
|
||||||
DashboardModule,
|
DashboardModule,
|
||||||
SegmentsModule,
|
SegmentsModule,
|
||||||
|
SeatClassesModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ async function bootstrap() {
|
|||||||
.addBearerAuth({ type: 'http', scheme: 'bearer', bearerFormat: 'JWT', in: 'header' }, 'JWT-auth')
|
.addBearerAuth({ type: 'http', scheme: 'bearer', bearerFormat: 'JWT', in: 'header' }, 'JWT-auth')
|
||||||
.addTag('Auth', 'Registration and login')
|
.addTag('Auth', 'Registration and login')
|
||||||
.addTag('Stations', 'Station directory')
|
.addTag('Stations', 'Station directory')
|
||||||
.addTag('Fleet', 'Train services and coaches')
|
.addTag('Fleet', 'Train services, coaches and seat batches')
|
||||||
|
.addTag('Seat Classes', 'Seat class management')
|
||||||
.addTag('Schedule', 'Trips and fare rules')
|
.addTag('Schedule', 'Trips and fare rules')
|
||||||
.addTag('Search', 'Trip search and fare quotes')
|
.addTag('Search', 'Trip search and fare quotes')
|
||||||
.addTag('Seats', 'Seat maps and holds')
|
.addTag('Seats', 'Seat maps and holds')
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export class CreateBookingDto {
|
|||||||
@ApiProperty() @IsString() tripId: string;
|
@ApiProperty() @IsString() tripId: string;
|
||||||
@ApiProperty() @IsString() holdId: string;
|
@ApiProperty() @IsString() holdId: string;
|
||||||
@ApiProperty({ type: [PassengerInputDto] }) @IsArray() @ValidateNested({ each: true }) @Type(() => PassengerInputDto) passengers: PassengerInputDto[];
|
@ApiProperty({ type: [PassengerInputDto] }) @IsArray() @ValidateNested({ each: true }) @Type(() => PassengerInputDto) passengers: PassengerInputDto[];
|
||||||
@ApiPropertyOptional({ example: 'ECONOMY', enum: ['ECONOMY', 'BUSINESS', 'FIRST'] }) @IsOptional() @IsString() serviceClass?: string;
|
@ApiPropertyOptional({ example: 'seat-class-uuid' }) @IsOptional() @IsString() seatClassId?: string;
|
||||||
@ApiPropertyOptional() @IsOptional() @IsString() promoCode?: string;
|
@ApiPropertyOptional() @IsOptional() @IsString() promoCode?: string;
|
||||||
@ApiPropertyOptional() @IsOptional() @IsInt() loyaltyRedemptionPoints?: number;
|
@ApiPropertyOptional() @IsOptional() @IsInt() loyaltyRedemptionPoints?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class BookingsService {
|
|||||||
if (!hold || hold.expiresAt < new Date()) throw new BadRequestException('Seat hold expired');
|
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 } });
|
const trip = await this.prisma.trip.findUnique({ where: { id: dto.tripId }, include: { originStation: true, destinationStation: true } });
|
||||||
if (!trip) throw new NotFoundException('Trip not found');
|
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 fareQuote = await this.searchService.getFareQuote({ tripId: dto.tripId, seatClassId: dto.seatClassId ?? '', passengerCount: dto.passengers.length, promoCode: dto.promoCode, loyaltyRedemptionPoints: dto.loyaltyRedemptionPoints });
|
||||||
const booking = await this.prisma.booking.create({
|
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 })) } },
|
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 } } },
|
include: { seats: { include: { seat: true } }, trip: { include: { originStation: true, destinationStation: true, service: true } } },
|
||||||
@@ -47,7 +47,7 @@ export class BookingsService {
|
|||||||
},
|
},
|
||||||
passengers: booking.seats.map((bs) => ({
|
passengers: booking.seats.map((bs) => ({
|
||||||
fullName: bs.passengerName,
|
fullName: bs.passengerName,
|
||||||
seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.serviceClass },
|
seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.seatClassId },
|
||||||
})),
|
})),
|
||||||
payment: booking.paymentIntent ? { method: booking.paymentIntent.method, status: booking.paymentIntent.status } : undefined,
|
payment: booking.paymentIntent ? { method: booking.paymentIntent.method, status: booking.paymentIntent.status } : undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiResponse, ApiBody } from '@nestjs/swagger';
|
||||||
import { FleetService } from './fleet.service';
|
import { FleetService } from './fleet.service';
|
||||||
import { CreateTrainServiceDto, CreateCoachDto, CreateSeatBatchDto } from './fleet.dto';
|
import { CreateTrainServiceDto, CreateCoachDto, CreateSeatBatchDto, UpdateCoachDto } from './fleet.dto';
|
||||||
import { JwtGuard } from '../../common/jwt.guard';
|
import { JwtGuard } from '../../common/jwt.guard';
|
||||||
|
|
||||||
@ApiTags('Fleet')
|
@ApiTags('Fleet')
|
||||||
@@ -10,9 +10,47 @@ import { JwtGuard } from '../../common/jwt.guard';
|
|||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth('JWT-auth')
|
||||||
export class FleetController {
|
export class FleetController {
|
||||||
constructor(private service: FleetService) {}
|
constructor(private service: FleetService) {}
|
||||||
@Get('services') @ApiOperation({ summary: 'List train services' }) getServices() { return this.service.getServices(); }
|
|
||||||
@Post('services') @ApiOperation({ summary: 'Create train service' }) createService(@Body() dto: CreateTrainServiceDto) { return this.service.createService(dto); }
|
@Get('services')
|
||||||
@Post('coaches') @ApiOperation({ summary: 'Add coach to trip' }) createCoach(@Body() dto: CreateCoachDto) { return this.service.createCoach(dto); }
|
@ApiOperation({ summary: 'List train services' })
|
||||||
@Post('seats/batch')@ApiOperation({ summary: 'Batch-create seats for coach' }) createSeatBatch(@Body() dto: CreateSeatBatchDto) { return this.service.createSeatBatch(dto); }
|
@ApiResponse({ status: 200, description: 'Returns all train services with recent trips' })
|
||||||
@Get('analytics') @ApiOperation({ summary: 'Fleet analytics' }) getAnalytics() { return this.service.getAnalytics(); }
|
getServices() { return this.service.getServices(); }
|
||||||
|
|
||||||
|
@Post('services')
|
||||||
|
@ApiOperation({ summary: 'Create a train service' })
|
||||||
|
@ApiBody({ type: CreateTrainServiceDto })
|
||||||
|
@ApiResponse({ status: 201, description: 'Train service created' })
|
||||||
|
createService(@Body() dto: CreateTrainServiceDto) { return this.service.createService(dto); }
|
||||||
|
|
||||||
|
@Get('coaches')
|
||||||
|
@ApiOperation({ summary: 'List coaches' })
|
||||||
|
@ApiQuery({ name: 'tripId', required: false, description: 'Filter by trip UUID' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Returns coaches with seat class and seat count' })
|
||||||
|
listCoaches(@Query('tripId') tripId?: string) { return this.service.listCoaches(tripId); }
|
||||||
|
|
||||||
|
@Post('coaches')
|
||||||
|
@ApiOperation({ summary: 'Add a coach to a trip' })
|
||||||
|
@ApiBody({ type: CreateCoachDto })
|
||||||
|
@ApiResponse({ status: 201, description: 'Coach created' })
|
||||||
|
createCoach(@Body() dto: CreateCoachDto) { return this.service.createCoach(dto); }
|
||||||
|
|
||||||
|
@Patch('coaches/:id')
|
||||||
|
@ApiOperation({ summary: 'Update a coach' })
|
||||||
|
@ApiParam({ name: 'id', description: 'Coach UUID' })
|
||||||
|
@ApiBody({ type: UpdateCoachDto })
|
||||||
|
@ApiResponse({ status: 200, description: 'Coach updated' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Coach not found' })
|
||||||
|
updateCoach(@Param('id') id: string, @Body() dto: UpdateCoachDto) { return this.service.updateCoach(id, dto); }
|
||||||
|
|
||||||
|
@Post('seats/batch')
|
||||||
|
@ApiOperation({ summary: 'Batch-create seats for a coach' })
|
||||||
|
@ApiBody({ type: CreateSeatBatchDto })
|
||||||
|
@ApiResponse({ status: 201, description: 'Seats created' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Coach not found' })
|
||||||
|
createSeatBatch(@Body() dto: CreateSeatBatchDto) { return this.service.createSeatBatch(dto); }
|
||||||
|
|
||||||
|
@Get('analytics')
|
||||||
|
@ApiOperation({ summary: 'Fleet analytics' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Returns fleet occupancy analytics' })
|
||||||
|
getAnalytics() { return this.service.getAnalytics(); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { IsString, IsEnum, IsInt } from 'class-validator';
|
import { IsString, IsInt, IsOptional } from 'class-validator';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional, PartialType, OmitType } from '@nestjs/swagger';
|
||||||
import { ServiceClass } from '@prisma/client';
|
|
||||||
|
|
||||||
export class CreateTrainServiceDto {
|
export class CreateTrainServiceDto {
|
||||||
@ApiProperty({ example: '301' }) @IsString() number: string;
|
@ApiProperty({ example: '301' }) @IsString() number: string;
|
||||||
@@ -8,13 +7,15 @@ export class CreateTrainServiceDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CreateCoachDto {
|
export class CreateCoachDto {
|
||||||
@ApiProperty() @IsString() tripId: string;
|
@ApiProperty({ example: 'trip-uuid' }) @IsString() tripId: string;
|
||||||
@ApiProperty({ example: 'A' }) @IsString() label: string;
|
@ApiProperty({ example: 'A' }) @IsString() label: string;
|
||||||
@ApiProperty({ enum: ServiceClass }) @IsEnum(ServiceClass) serviceClass: ServiceClass;
|
@ApiProperty({ example: 'seat-class-uuid' }) @IsString() seatClassId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UpdateCoachDto extends PartialType(OmitType(CreateCoachDto, ['tripId'] as const)) {}
|
||||||
|
|
||||||
export class CreateSeatBatchDto {
|
export class CreateSeatBatchDto {
|
||||||
@ApiProperty() @IsString() coachId: string;
|
@ApiProperty({ example: 'coach-uuid' }) @IsString() coachId: string;
|
||||||
@ApiProperty({ example: 10 }) @IsInt() rows: number;
|
@ApiProperty({ example: 10 }) @IsInt() rows: number;
|
||||||
@ApiProperty({ example: ['A', 'B', 'C', 'D'] }) cols: string[];
|
@ApiProperty({ example: ['A', 'B', 'C', 'D'] }) cols: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { PrismaService } from '../../common/prisma.service';
|
import { PrismaService } from '../../common/prisma.service';
|
||||||
import { CreateTrainServiceDto, CreateCoachDto, CreateSeatBatchDto } from './fleet.dto';
|
import { CreateTrainServiceDto, CreateCoachDto, CreateSeatBatchDto, UpdateCoachDto } from './fleet.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FleetService {
|
export class FleetService {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
getServices() { return this.prisma.trainService.findMany({ include: { trips: { take: 5, orderBy: { departureAt: 'desc' } } } }); }
|
getServices() { return this.prisma.trainService.findMany({ include: { trips: { take: 5, orderBy: { departureAt: 'desc' } } } }); }
|
||||||
createService(dto: CreateTrainServiceDto) { return this.prisma.trainService.create({ data: dto }); }
|
createService(dto: CreateTrainServiceDto) { return this.prisma.trainService.create({ data: dto }); }
|
||||||
createCoach(dto: CreateCoachDto) { return this.prisma.coach.create({ data: dto }); }
|
|
||||||
|
listCoaches(tripId?: string) {
|
||||||
|
return this.prisma.coach.findMany({
|
||||||
|
where: tripId ? { tripId } : undefined,
|
||||||
|
include: { seatClass: { select: { id: true, name: true, basePrice: true } }, _count: { select: { seats: true } } },
|
||||||
|
orderBy: { label: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createCoach(dto: CreateCoachDto) { return this.prisma.coach.create({ data: dto, include: { seatClass: { select: { id: true, name: true, basePrice: true } } } }); }
|
||||||
|
|
||||||
|
async updateCoach(id: string, dto: UpdateCoachDto) {
|
||||||
|
const coach = await this.prisma.coach.findUnique({ where: { id } });
|
||||||
|
if (!coach) throw new NotFoundException('Coach not found');
|
||||||
|
return this.prisma.coach.update({ where: { id }, data: dto, include: { seatClass: { select: { id: true, name: true, basePrice: true } } } });
|
||||||
|
}
|
||||||
|
|
||||||
async createSeatBatch(dto: CreateSeatBatchDto) {
|
async createSeatBatch(dto: CreateSeatBatchDto) {
|
||||||
const coach = await this.prisma.coach.findUnique({ where: { id: dto.coachId } });
|
const coach = await this.prisma.coach.findUnique({ where: { id: dto.coachId } });
|
||||||
if (!coach) throw new NotFoundException('Coach not found');
|
if (!coach) throw new NotFoundException('Coach not found');
|
||||||
@@ -16,6 +33,7 @@ export class FleetService {
|
|||||||
await this.prisma.seat.createMany({ data: seats, skipDuplicates: true });
|
await this.prisma.seat.createMany({ data: seats, skipDuplicates: true });
|
||||||
return { created: seats.length };
|
return { created: seats.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAnalytics() {
|
async getAnalytics() {
|
||||||
const [totalServices, totalTrips, totalSeats, bookedSeats] = await Promise.all([
|
const [totalServices, totalTrips, totalSeats, bookedSeats] = await Promise.all([
|
||||||
this.prisma.trainService.count(), this.prisma.trip.count(),
|
this.prisma.trainService.count(), this.prisma.trip.count(),
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export class PassengersService {
|
|||||||
destination: { id: b.trip.destinationStation.id, name: b.trip.destinationStation.name, code: b.trip.destinationStation.code, city: b.trip.destinationStation.city },
|
destination: { id: b.trip.destinationStation.id, name: b.trip.destinationStation.name, code: b.trip.destinationStation.code, city: b.trip.destinationStation.city },
|
||||||
departureAt: b.trip.departureAt,
|
departureAt: b.trip.departureAt,
|
||||||
},
|
},
|
||||||
passengers: b.seats.map((bs) => ({ fullName: bs.passengerName, seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.serviceClass } })),
|
passengers: b.seats.map((bs) => ({ fullName: bs.passengerName, seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.seatClassId } })),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { IsString, IsDateString, IsInt, IsOptional, IsEnum } from 'class-validator';
|
import { IsString, IsDateString, IsInt, IsOptional } from 'class-validator';
|
||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { ServiceClass } from '@prisma/client';
|
|
||||||
|
|
||||||
export class CreateTripDto {
|
export class CreateTripDto {
|
||||||
@ApiProperty() @IsString() serviceId: string;
|
@ApiProperty() @IsString() serviceId: string;
|
||||||
@@ -14,7 +13,7 @@ export class CreateTripDto {
|
|||||||
export class CreateFareRuleDto {
|
export class CreateFareRuleDto {
|
||||||
@ApiPropertyOptional() @IsOptional() @IsString() tripId?: string;
|
@ApiPropertyOptional() @IsOptional() @IsString() tripId?: string;
|
||||||
@ApiPropertyOptional() @IsOptional() @IsString() route?: string;
|
@ApiPropertyOptional() @IsOptional() @IsString() route?: string;
|
||||||
@ApiProperty({ enum: ServiceClass }) @IsEnum(ServiceClass) serviceClass: ServiceClass;
|
@ApiProperty({ example: 'seat-class-uuid' }) @IsString() seatClassId: string;
|
||||||
@ApiProperty({ example: 45000 }) @IsInt() baseFareMinor: number;
|
@ApiProperty({ example: 45000 }) @IsInt() baseFareMinor: number;
|
||||||
@ApiProperty({ example: '2026-01-01T00:00:00Z' }) @IsDateString() validFrom: string;
|
@ApiProperty({ example: '2026-01-01T00:00:00Z' }) @IsDateString() validFrom: string;
|
||||||
@ApiPropertyOptional() @IsOptional() @IsDateString() validUntil?: string;
|
@ApiPropertyOptional() @IsOptional() @IsDateString() validUntil?: string;
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ export class SchedulesService {
|
|||||||
return this.prisma.fareRule.create({ data: { ...dto, validFrom: new Date(dto.validFrom), validUntil: dto.validUntil ? new Date(dto.validUntil) : null } });
|
return this.prisma.fareRule.create({ data: { ...dto, validFrom: new Date(dto.validFrom), validUntil: dto.validUntil ? new Date(dto.validUntil) : null } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFare(tripId: string, serviceClass: string) {
|
async getFare(tripId: string, seatClassId: string) {
|
||||||
const trip = await this.prisma.trip.findUnique({ where: { id: tripId }, include: { originStation: true, destinationStation: true } });
|
const trip = await this.prisma.trip.findUnique({ where: { id: tripId }, include: { originStation: true, destinationStation: true } });
|
||||||
if (!trip) throw new NotFoundException('Trip not found');
|
if (!trip) throw new NotFoundException('Trip not found');
|
||||||
const route = `${trip.originStation.code}-${trip.destinationStation.code}`;
|
const route = `${trip.originStation.code}-${trip.destinationStation.code}`;
|
||||||
const rule = await this.prisma.fareRule.findFirst({
|
const rule = await this.prisma.fareRule.findFirst({
|
||||||
where: { serviceClass: serviceClass as any, validFrom: { lte: new Date() }, OR: [{ tripId }, { route }, { tripId: null, route: null }], AND: [{ OR: [{ validUntil: null }, { validUntil: { gte: new Date() } }] }] },
|
where: { seatClassId, validFrom: { lte: new Date() }, OR: [{ tripId }, { route }, { tripId: null, route: null }], AND: [{ OR: [{ validUntil: null }, { validUntil: { gte: new Date() } }] }] },
|
||||||
orderBy: { validFrom: 'desc' },
|
orderBy: { validFrom: 'desc' },
|
||||||
});
|
});
|
||||||
return rule ?? { baseFareMinor: 45000, currency: 'ETB', serviceClass };
|
return rule ?? { baseFareMinor: 45000, currency: 'ETB', seatClassId };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export class SearchTripsDto {
|
|||||||
|
|
||||||
export class FareQuoteDto {
|
export class FareQuoteDto {
|
||||||
@ApiProperty() @IsString() tripId: string;
|
@ApiProperty() @IsString() tripId: string;
|
||||||
@ApiProperty({ example: 'ECONOMY' }) @IsString() serviceClass: string;
|
@ApiProperty({ example: 'uuid-of-seat-class' }) @IsString() seatClassId: string;
|
||||||
@ApiPropertyOptional({ example: 1 }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) passengerCount?: number;
|
@ApiPropertyOptional({ example: 1 }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) passengerCount?: number;
|
||||||
@ApiPropertyOptional({ example: 'WEEKEND15' }) @IsOptional() @IsString() promoCode?: string;
|
@ApiPropertyOptional({ example: 'WEEKEND15' }) @IsOptional() @IsString() promoCode?: string;
|
||||||
@ApiPropertyOptional({ example: 450 }) @IsOptional() @Type(() => Number) @IsInt() loyaltyRedemptionPoints?: number;
|
@ApiPropertyOptional({ example: 450 }) @IsOptional() @Type(() => Number) @IsInt() loyaltyRedemptionPoints?: number;
|
||||||
|
|||||||
@@ -12,19 +12,19 @@ export class SearchService {
|
|||||||
const date = new Date(dto.date), nextDay = new Date(date.getTime() + 86400000);
|
const date = new Date(dto.date), nextDay = new Date(date.getTime() + 86400000);
|
||||||
const trips = await this.prisma.trip.findMany({
|
const trips = await this.prisma.trip.findMany({
|
||||||
where: { originStationId: dto.originStationId, destinationStationId: dto.destinationStationId, departureAt: { gte: date, lt: nextDay }, status: { in: ['SCHEDULED', 'BOARDING'] } },
|
where: { originStationId: dto.originStationId, destinationStationId: dto.destinationStationId, departureAt: { gte: date, lt: nextDay }, status: { in: ['SCHEDULED', 'BOARDING'] } },
|
||||||
include: { service: true, originStation: true, destinationStation: true, coaches: { include: { seats: true } } },
|
include: { service: true, originStation: true, destinationStation: true, coaches: { include: { seats: true, seatClass: true } } },
|
||||||
});
|
});
|
||||||
return trips.map((trip) => {
|
return trips.map((trip) => {
|
||||||
const seatsByClass = (cls: string) => trip.coaches.filter((c) => c.serviceClass === cls).flatMap((c) => c.seats);
|
const seatsByClass = (id: string) => trip.coaches.filter((c) => c.seatClassId === id).flatMap((c) => c.seats);
|
||||||
const avail = (cls: string) => seatsByClass(cls).filter((s) => s.status === 'AVAILABLE').length;
|
const uniqueClasses = [...new Map(trip.coaches.map(c => [c.seatClassId, c.seatClass])).values()];
|
||||||
return {
|
return {
|
||||||
id: trip.id,
|
id: trip.id,
|
||||||
number: trip.service.number,
|
number: trip.service.number,
|
||||||
origin: { id: trip.originStation.id, code: trip.originStation.code, name: trip.originStation.name, city: trip.originStation.city },
|
origin: { id: trip.originStation.id, code: trip.originStation.code, name: trip.originStation.name, city: trip.originStation.city },
|
||||||
destination: { id: trip.destinationStation.id, code: trip.destinationStation.code, name: trip.destinationStation.name, city: trip.destinationStation.city },
|
destination: { id: trip.destinationStation.id, code: trip.destinationStation.code, name: trip.destinationStation.name, city: trip.destinationStation.city },
|
||||||
departureAt: trip.departureAt, arrivalAt: trip.arrivalAt, status: trip.status,
|
departureAt: trip.departureAt, arrivalAt: trip.arrivalAt, status: trip.status,
|
||||||
availability: { ECONOMY: avail('ECONOMY'), BUSINESS: avail('BUSINESS'), FIRST: avail('FIRST') },
|
availability: Object.fromEntries(uniqueClasses.map(sc => [sc.name, seatsByClass(sc.id).filter(s => s.status === 'AVAILABLE').length])),
|
||||||
fares: { ECONOMY: this.defaultFare('ECONOMY') / 100, BUSINESS: this.defaultFare('BUSINESS') / 100, FIRST: this.defaultFare('FIRST') / 100 },
|
fares: Object.fromEntries(uniqueClasses.map(sc => [sc.name, sc.basePrice / 100])),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -32,8 +32,10 @@ export class SearchService {
|
|||||||
async getFareQuote(dto: FareQuoteDto) {
|
async getFareQuote(dto: FareQuoteDto) {
|
||||||
const trip = await this.prisma.trip.findUnique({ where: { id: dto.tripId } });
|
const trip = await this.prisma.trip.findUnique({ where: { id: dto.tripId } });
|
||||||
if (!trip) throw new NotFoundException('Trip not found');
|
if (!trip) throw new NotFoundException('Trip not found');
|
||||||
|
const seatClass = await this.prisma.seatClass.findUnique({ where: { id: dto.seatClassId } });
|
||||||
|
if (!seatClass) throw new NotFoundException('SeatClass not found');
|
||||||
const count = dto.passengerCount ?? 1;
|
const count = dto.passengerCount ?? 1;
|
||||||
const baseFareMinor = this.defaultFare(dto.serviceClass) * count;
|
const baseFareMinor = seatClass.basePrice * count;
|
||||||
let discountMinor = 0;
|
let discountMinor = 0;
|
||||||
if (dto.promoCode) {
|
if (dto.promoCode) {
|
||||||
const promo = await this.prisma.promotion.findUnique({ where: { code: dto.promoCode } });
|
const promo = await this.prisma.promotion.findUnique({ where: { code: dto.promoCode } });
|
||||||
@@ -41,10 +43,7 @@ export class SearchService {
|
|||||||
}
|
}
|
||||||
const loyaltyMinor = (dto.loyaltyRedemptionPoints ?? 0) * POINTS_TO_MINOR;
|
const loyaltyMinor = (dto.loyaltyRedemptionPoints ?? 0) * POINTS_TO_MINOR;
|
||||||
const taxesMinor = Math.round(baseFareMinor * 0.05);
|
const taxesMinor = Math.round(baseFareMinor * 0.05);
|
||||||
return { tripId: dto.tripId, serviceClass: dto.serviceClass, passengerCount: count, baseFareMinor, discountMinor, loyaltyRedemptionMinor: loyaltyMinor, taxesFeesMinor: taxesMinor, totalMinor: Math.max(0, baseFareMinor - discountMinor - loyaltyMinor + taxesMinor), currency: 'ETB' };
|
return { tripId: dto.tripId, seatClassId: dto.seatClassId, seatClassName: seatClass.name, passengerCount: count, baseFareMinor, discountMinor, loyaltyRedemptionMinor: loyaltyMinor, taxesFeesMinor: taxesMinor, totalMinor: Math.max(0, baseFareMinor - discountMinor - loyaltyMinor + taxesMinor), currency: 'ETB' };
|
||||||
}
|
}
|
||||||
|
|
||||||
private defaultFare(serviceClass: string): number {
|
|
||||||
return ({ ECONOMY: 45000, BUSINESS: 90000, FIRST: 135000 } as any)[serviceClass] ?? 45000;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Body, Controller, Get, Param, Patch, Post, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiResponse, ApiBody } from '@nestjs/swagger';
|
||||||
|
import { SeatClassesService } from './seat-classes.service';
|
||||||
|
import { CreateSeatClassDto, UpdateSeatClassDto } from './seat-classes.dto';
|
||||||
|
import { JwtGuard } from '../../common/jwt.guard';
|
||||||
|
|
||||||
|
@ApiTags('Seat Classes')
|
||||||
|
@Controller('seat-classes')
|
||||||
|
export class SeatClassesController {
|
||||||
|
constructor(private service: SeatClassesService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'List all seat classes' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Returns all seat classes with their coaches' })
|
||||||
|
listSeatClasses() { return this.service.listSeatClasses(); }
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get a seat class by ID' })
|
||||||
|
@ApiParam({ name: 'id', description: 'Seat class UUID' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Returns seat class with its coaches' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Seat class not found' })
|
||||||
|
getSeatClass(@Param('id') id: string) { return this.service.getSeatClass(id); }
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
|
||||||
|
@ApiOperation({ summary: 'Create a seat class' })
|
||||||
|
@ApiBody({ type: CreateSeatClassDto })
|
||||||
|
@ApiResponse({ status: 201, description: 'Seat class created' })
|
||||||
|
@ApiResponse({ status: 409, description: 'Seat class name already exists' })
|
||||||
|
createSeatClass(@Body() dto: CreateSeatClassDto) { return this.service.createSeatClass(dto); }
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
|
||||||
|
@ApiOperation({ summary: 'Update a seat class' })
|
||||||
|
@ApiParam({ name: 'id', description: 'Seat class UUID' })
|
||||||
|
@ApiBody({ type: UpdateSeatClassDto })
|
||||||
|
@ApiResponse({ status: 200, description: 'Seat class updated' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Seat class not found' })
|
||||||
|
updateSeatClass(@Param('id') id: string, @Body() dto: UpdateSeatClassDto) { return this.service.updateSeatClass(id, dto); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { IsString, IsInt, IsBoolean, IsOptional } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional, PartialType } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class CreateSeatClassDto {
|
||||||
|
@ApiProperty({ example: 'Economy Seat' })
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'Standard economy seating' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 45000, description: 'Base price in minor currency units' })
|
||||||
|
@IsInt()
|
||||||
|
basePrice: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateSeatClassDto extends PartialType(CreateSeatClassDto) {}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SeatClassesController } from './seat-classes.controller';
|
||||||
|
import { SeatClassesService } from './seat-classes.service';
|
||||||
|
|
||||||
|
@Module({ controllers: [SeatClassesController], providers: [SeatClassesService], exports: [SeatClassesService] })
|
||||||
|
export class SeatClassesModule {}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../../common/prisma.service';
|
||||||
|
import { CreateSeatClassDto, UpdateSeatClassDto } from './seat-classes.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SeatClassesService {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
private readonly coachInclude = {
|
||||||
|
coaches: {
|
||||||
|
select: { id: true, label: true, tripId: true, _count: { select: { seats: true } } },
|
||||||
|
orderBy: { label: 'asc' as const },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
listSeatClasses() {
|
||||||
|
return this.prisma.seatClass.findMany({ orderBy: { createdAt: 'asc' }, include: this.coachInclude });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSeatClass(id: string) {
|
||||||
|
const sc = await this.prisma.seatClass.findUnique({ where: { id }, include: this.coachInclude });
|
||||||
|
if (!sc) throw new NotFoundException('SeatClass not found');
|
||||||
|
return sc;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSeatClass(dto: CreateSeatClassDto) {
|
||||||
|
try {
|
||||||
|
return await this.prisma.seatClass.create({ data: dto });
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.code === 'P2002') throw new ConflictException(`Seat class "${dto.name}" already exists`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSeatClass(id: string, dto: UpdateSeatClassDto) {
|
||||||
|
const sc = await this.prisma.seatClass.findUnique({ where: { id } });
|
||||||
|
if (!sc) throw new NotFoundException('SeatClass not found');
|
||||||
|
return this.prisma.seatClass.update({ where: { id }, data: dto, include: this.coachInclude });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger';
|
||||||
import { SeatsService } from './seats.service';
|
import { SeatsService } from './seats.service';
|
||||||
import { HoldSeatsDto } from './seats.dto';
|
import { HoldSeatsDto } from './seats.dto';
|
||||||
import { JwtGuard } from '../../common/jwt.guard';
|
import { JwtGuard } from '../../common/jwt.guard';
|
||||||
@@ -8,10 +8,28 @@ import { JwtGuard } from '../../common/jwt.guard';
|
|||||||
@Controller('seats')
|
@Controller('seats')
|
||||||
export class SeatsController {
|
export class SeatsController {
|
||||||
constructor(private service: SeatsService) {}
|
constructor(private service: SeatsService) {}
|
||||||
@Get('seatmap/:tripId') @ApiOperation({ summary: 'Get seat map for a trip' })
|
|
||||||
|
// ── Seat Map ──────────────────────────────────────────────────────────────
|
||||||
|
@Get('seatmap/:tripId')
|
||||||
|
@ApiOperation({ summary: 'Get seat map for a trip' })
|
||||||
|
@ApiParam({ name: 'tripId', description: 'Trip UUID' })
|
||||||
|
@ApiQuery({ name: 'coachId', required: false, description: 'Filter by coach UUID' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Returns coaches with seats and seat class info' })
|
||||||
getSeatMap(@Param('tripId') tripId: string, @Query('coachId') coachId?: string) { return this.service.getSeatMap(tripId, coachId); }
|
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' })
|
|
||||||
|
// ── Hold / Release ────────────────────────────────────────────────────────
|
||||||
|
@Post('hold')
|
||||||
|
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
|
||||||
|
@ApiOperation({ summary: 'Hold seats for 15 minutes' })
|
||||||
|
@ApiResponse({ status: 201, description: 'Seats held successfully' })
|
||||||
|
@ApiResponse({ status: 409, description: 'One or more seats unavailable' })
|
||||||
holdSeats(@Body() dto: HoldSeatsDto) { return this.service.holdSeats(dto); }
|
holdSeats(@Body() dto: HoldSeatsDto) { return this.service.holdSeats(dto); }
|
||||||
@Delete('hold/:holdId') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Release a seat hold' })
|
|
||||||
|
@Delete('hold/:holdId')
|
||||||
|
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
|
||||||
|
@ApiOperation({ summary: 'Release a seat hold' })
|
||||||
|
@ApiParam({ name: 'holdId', description: 'Hold UUID' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Hold released' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Hold not found' })
|
||||||
releaseHold(@Param('holdId') holdId: string) { return this.service.releaseHold(holdId); }
|
releaseHold(@Param('holdId') holdId: string) { return this.service.releaseHold(holdId); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IsString, IsArray } from 'class-validator';
|
import { IsString, IsArray, IsOptional } from 'class-validator';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class HoldSeatsDto {
|
export class HoldSeatsDto {
|
||||||
@ApiProperty() @IsString() tripId: string;
|
@ApiProperty({ example: 'trip-uuid' }) @IsString() tripId: string;
|
||||||
@ApiProperty() @IsString() passengerId: string;
|
@ApiProperty({ example: 'passenger-uuid' }) @IsString() passengerId: string;
|
||||||
@ApiProperty({ type: [String] }) @IsArray() seatIds: string[];
|
@ApiProperty({ type: [String], example: ['seat-uuid-1', 'seat-uuid-2'] }) @IsArray() seatIds: string[];
|
||||||
@ApiProperty({ required: false }) fareQuoteId?: string;
|
@ApiPropertyOptional({ example: 'fare-quote-uuid' }) @IsOptional() @IsString() fareQuoteId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +7,20 @@ import { Cron, CronExpression } from '@nestjs/schedule';
|
|||||||
export class SeatsService {
|
export class SeatsService {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
// ── Seat Map ──────────────────────────────────────────────────────────────
|
||||||
async getSeatMap(tripId: string, coachId?: string) {
|
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' }] } } });
|
const coaches = await this.prisma.coach.findMany({ where: { tripId, ...(coachId ? { id: coachId } : {}) }, include: { seatClass: true, seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] } } });
|
||||||
return {
|
return {
|
||||||
coaches: coaches.map((coach) => ({
|
coaches: coaches.map((coach) => ({
|
||||||
id: coach.id,
|
id: coach.id,
|
||||||
name: `Coach ${coach.label}`,
|
name: `Coach ${coach.label}`,
|
||||||
type: coach.serviceClass,
|
seatClass: { id: coach.seatClass.id, name: coach.seatClass.name, basePrice: coach.seatClass.basePrice },
|
||||||
seats: coach.seats.map((s) => ({ id: s.id, number: s.label, status: s.status, kind: s.kind })),
|
seats: coach.seats.map((s) => ({ id: s.id, number: s.label, status: s.status, kind: s.kind })),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Hold / Release ────────────────────────────────────────────────────────
|
||||||
async holdSeats(dto: HoldSeatsDto) {
|
async holdSeats(dto: HoldSeatsDto) {
|
||||||
const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
|
const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
|
||||||
const hold = await this.prisma.$transaction(async (tx) => {
|
const hold = await this.prisma.$transaction(async (tx) => {
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ export class EnhancedSeatsService {
|
|||||||
id: seat.id,
|
id: seat.id,
|
||||||
label: seat.label,
|
label: seat.label,
|
||||||
coach: coach.label,
|
coach: coach.label,
|
||||||
serviceClass: coach.serviceClass,
|
serviceClass: coach.seatClassId,
|
||||||
row: seat.row,
|
row: seat.row,
|
||||||
col: seat.col
|
col: seat.col
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user