mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 06:28:12 +00:00
Tour package booking, app release, new endpoints, more updates and fixes
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { Body, Controller, Get, Param, Post, Patch, Delete, UseGuards, Request, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
||||
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
|
||||
import { PackagesService } from './packages.service';
|
||||
import { CreatePackageDto, BookPackageDto, CreatePriceTierDto, UpdatePriceTierDto, CreateInquiryDto, UpdateInquiryStatusDto } from './packages.dto';
|
||||
import { CreatePackageDto, BookPackageDto, CreatePriceTierDto, UpdatePriceTierDto, CreateInquiryDto, UpdateInquiryStatusDto, PackageBookingContextDto } from './packages.dto';
|
||||
import { IamGuard } from '../../common/iam-adapter';
|
||||
import { JwtGuard } from '../../common/jwt.guard';
|
||||
import { OptionalJwtGuard } from '../verifayda/optional-jwt.guard';
|
||||
@@ -63,6 +63,19 @@ export class PackagesController {
|
||||
return this.service.listAll(page ? +page : 1, pageSize ? +pageSize : 20);
|
||||
}
|
||||
|
||||
@Get('bookings')
|
||||
@UseGuards(IamGuard)
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
@ApiOperation({ summary: 'List all package bookings (backoffice)' })
|
||||
listBookings(
|
||||
@Query('packageId') packageId?: string,
|
||||
@Query('status') status?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
return this.service.listBookings({ packageId, status, page: page ? +page : 1, pageSize: pageSize ? +pageSize : 20 });
|
||||
}
|
||||
|
||||
@Get('my-bookings')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@@ -78,6 +91,21 @@ export class PackagesController {
|
||||
return this.service.getBookingByRef(ref);
|
||||
}
|
||||
|
||||
@Get(':id/booking-context')
|
||||
@IsPublic()
|
||||
@ApiOperation({ summary: 'Get booking context for self-service package booking' })
|
||||
@ApiQuery({ name: 'tierId', required: true })
|
||||
@ApiQuery({ name: 'adultCount', required: true })
|
||||
@ApiQuery({ name: 'childCount', required: false })
|
||||
getBookingContext(
|
||||
@Param('id') id: string,
|
||||
@Query('tierId') tierId: string,
|
||||
@Query('adultCount') adultCount: string,
|
||||
@Query('childCount') childCount?: string,
|
||||
) {
|
||||
return this.service.getBookingContext(id, tierId, parseInt(adultCount), childCount ? parseInt(childCount) : 0);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@IsPublic()
|
||||
@ApiOperation({ summary: 'Get package details' })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsString, IsOptional, IsInt, IsBoolean, IsArray, IsDateString, Min, ValidateNested, IsUUID } from 'class-validator';
|
||||
import { IsString, IsOptional, IsInt, IsBoolean, IsArray, IsDateString, Min, ValidateNested, IsUUID, IsPositive } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
@@ -93,6 +93,12 @@ export class BookPackagePassengerDto {
|
||||
@ApiPropertyOptional() @IsOptional() @IsString() passportCountry?: string;
|
||||
}
|
||||
|
||||
export class PackageBookingContextDto {
|
||||
@ApiProperty() @IsUUID() tierId: string;
|
||||
@ApiProperty({ example: 1 }) @IsInt() @IsPositive() adultCount: number;
|
||||
@ApiPropertyOptional({ example: 0 }) @IsOptional() @IsInt() @Min(0) childCount?: number;
|
||||
}
|
||||
|
||||
export class BookPackageDto {
|
||||
@ApiProperty() @IsUUID() packageId: string;
|
||||
@ApiProperty() @IsUUID() priceTierId: string;
|
||||
|
||||
@@ -3,9 +3,10 @@ import { PrismaModule } from '../../common/prisma.module';
|
||||
import { PackagesController } from './packages.controller';
|
||||
import { PackagesService } from './packages.service';
|
||||
import { CurrencyModule } from '../currency/currency.module';
|
||||
import { BookingsModule } from '../bookings/bookings.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, CurrencyModule],
|
||||
imports: [PrismaModule, CurrencyModule, BookingsModule],
|
||||
controllers: [PackagesController],
|
||||
providers: [PackagesService],
|
||||
exports: [PackagesService],
|
||||
|
||||
@@ -3,6 +3,8 @@ import { PrismaService } from '../../common/prisma.service';
|
||||
import { CurrencyService } from '../currency/currency.service';
|
||||
import { CreatePackageDto, BookPackageDto, UpdatePriceTierDto, CreatePriceTierDto, CreateInquiryDto } from './packages.dto';
|
||||
import { Currency } from '@prisma/client';
|
||||
import { BookingsService } from '../bookings/bookings.service';
|
||||
import { GuestBookingService } from '../bookings/guest-booking.service';
|
||||
|
||||
function generateRef(): string {
|
||||
return 'PKG-' + Array.from({ length: 6 }, () =>
|
||||
@@ -15,8 +17,94 @@ export class PackagesService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly currencyService: CurrencyService,
|
||||
private readonly bookingsService: BookingsService,
|
||||
private readonly guestBookingService: GuestBookingService,
|
||||
) {}
|
||||
|
||||
async getBookingContext(packageId: string, tierId: string, adultCount: number, childCount = 0) {
|
||||
const pkg = await this.prisma.travelPackage.findUnique({
|
||||
where: { id: packageId },
|
||||
include: {
|
||||
priceTiers: true,
|
||||
outboundSchedule: {
|
||||
include: {
|
||||
originStation: true,
|
||||
destinationStation: true,
|
||||
coachAssignments: { include: { coach: { include: { coachType: { include: { seatClasses: true } } } } } },
|
||||
},
|
||||
},
|
||||
returnSchedule: { include: { originStation: true, destinationStation: true } },
|
||||
},
|
||||
});
|
||||
if (!pkg || pkg.status !== 'ACTIVE') throw new NotFoundException('Package not available');
|
||||
|
||||
const tier = pkg.priceTiers.find(t => t.id === tierId);
|
||||
if (!tier) throw new NotFoundException('Price tier not found');
|
||||
|
||||
const passengerCount = adultCount + childCount;
|
||||
if (passengerCount < 1) throw new BadRequestException('At least one passenger required');
|
||||
|
||||
const remaining = tier.availableSeats - tier.bookedSeats;
|
||||
if (passengerCount > remaining)
|
||||
throw new BadRequestException(`Only ${remaining} seat(s) remaining in the ${tier.label} tier`);
|
||||
|
||||
const totalMinor = tier.priceMinor * passengerCount;
|
||||
|
||||
// Resolve the seatClassId and coachTypeId that matches this tier's seatType from the outbound schedule coaches
|
||||
let seatClassId: string | null = null;
|
||||
let coachTypeId: string | null = null;
|
||||
for (const a of pkg.outboundSchedule.coachAssignments) {
|
||||
const sc = a.coach.coachType?.seatClasses?.find(
|
||||
(s: any) => s.name.toLowerCase().includes(tier.seatType.toLowerCase()) ||
|
||||
tier.seatType.toLowerCase().includes(s.name.toLowerCase()),
|
||||
);
|
||||
if (sc) { seatClassId = sc.id; coachTypeId = a.coach.coachTypeId ?? a.coach.coachType?.id ?? null; break; }
|
||||
}
|
||||
// Fallback: use the first coach assignment's coachTypeId if no match found
|
||||
if (!coachTypeId && pkg.outboundSchedule.coachAssignments.length > 0) {
|
||||
const first = pkg.outboundSchedule.coachAssignments[0];
|
||||
coachTypeId = first.coach.coachTypeId ?? first.coach.coachType?.id ?? null;
|
||||
}
|
||||
|
||||
return {
|
||||
packageId: pkg.id,
|
||||
packageName: pkg.name,
|
||||
priceTierId: tier.id,
|
||||
tierLabel: tier.label,
|
||||
seatType: tier.seatType,
|
||||
seatClassId,
|
||||
coachTypeId,
|
||||
adultCount,
|
||||
childCount,
|
||||
passengerCount,
|
||||
pricePerPassengerMinor: tier.priceMinor,
|
||||
totalMinor,
|
||||
currency: tier.currency,
|
||||
remainingSeats: remaining,
|
||||
outboundSchedule: {
|
||||
scheduleId: pkg.outboundScheduleId,
|
||||
originStationId: pkg.originStationId,
|
||||
destinationStationId: pkg.destinationStationId,
|
||||
departureAt: pkg.outboundSchedule.departureAt,
|
||||
arrivalAt: pkg.outboundSchedule.arrivalAt,
|
||||
originStation: pkg.outboundSchedule.originStation,
|
||||
destinationStation: pkg.outboundSchedule.destinationStation,
|
||||
},
|
||||
returnSchedule: pkg.returnSchedule ? {
|
||||
scheduleId: pkg.returnScheduleId,
|
||||
originStationId: pkg.destinationStationId,
|
||||
destinationStationId: pkg.originStationId,
|
||||
departureAt: pkg.returnSchedule.departureAt,
|
||||
arrivalAt: pkg.returnSchedule.arrivalAt,
|
||||
originStation: pkg.returnSchedule.destinationStation,
|
||||
destinationStation: pkg.returnSchedule.originStation,
|
||||
} : null,
|
||||
includedServices: pkg.includedServices,
|
||||
busTransferIncluded: pkg.busTransferIncluded,
|
||||
busTransferRoute: pkg.busTransferRoute,
|
||||
};
|
||||
}
|
||||
|
||||
async createInquiry(dto: CreateInquiryDto) {
|
||||
return this.prisma.packageInquiry.create({
|
||||
data: {
|
||||
@@ -74,7 +162,7 @@ export class PackagesService {
|
||||
returnSchedule: { include: { originStation: true, destinationStation: true } },
|
||||
},
|
||||
orderBy: { validFrom: 'asc' },
|
||||
});
|
||||
}).then(pkgs => pkgs.map(p => ({ ...p, journeyType: p.returnScheduleId ? 'ROUND_TRIP' : 'ONE_WAY' })));
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
@@ -87,7 +175,7 @@ export class PackagesService {
|
||||
},
|
||||
});
|
||||
if (!pkg) throw new NotFoundException('Package not found');
|
||||
return pkg;
|
||||
return { ...pkg, journeyType: pkg.returnScheduleId ? 'ROUND_TRIP' : 'ONE_WAY' };
|
||||
}
|
||||
|
||||
create(dto: CreatePackageDto) {
|
||||
@@ -296,6 +384,29 @@ export class PackagesService {
|
||||
return booking;
|
||||
}
|
||||
|
||||
async listBookings({ packageId, status, page = 1, pageSize = 20 }: { packageId?: string; status?: string; page?: number; pageSize?: number }) {
|
||||
const where: any = {};
|
||||
if (packageId) where.packageId = packageId;
|
||||
if (status) where.status = status;
|
||||
const skip = (page - 1) * pageSize;
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.packageBooking.findMany({
|
||||
where,
|
||||
include: {
|
||||
package: { select: { id: true, name: true, code: true } },
|
||||
priceTier: { select: { id: true, label: true, seatType: true } },
|
||||
passengers: true,
|
||||
paymentIntent: true,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.packageBooking.count({ where }),
|
||||
]);
|
||||
return { items, total, page, pageSize, totalPages: Math.ceil(total / pageSize) };
|
||||
}
|
||||
|
||||
async listAll(page = 1, pageSize = 20) {
|
||||
const skip = (page - 1) * pageSize;
|
||||
const [items, total] = await Promise.all([
|
||||
|
||||
Reference in New Issue
Block a user