Round trip journeys, feedback items, backoffice documentation

This commit is contained in:
Stephanos A
2026-06-16 13:33:03 +03:00
parent 28e183b086
commit 75c8423f50
46 changed files with 7222 additions and 570 deletions

View File

@@ -142,6 +142,14 @@ export class SchedulesController {
@Body() dto: UpdateStopTimeDto,
) { return this.service.updateStop(id, sequence, dto); }
@Get(':scheduleId/fares/stored')
@ApiOperation({ summary: 'Get stored fare rules for a schedule' })
@ApiParam({ name: 'scheduleId', description: 'TrainSchedule UUID' })
@ApiResponse({ status: 200, description: 'List of stored fare rules with seat class info' })
getStoredFares(@Param('scheduleId') scheduleId: string) {
return this.service.getFareRules(scheduleId);
}
@Get(':scheduleId/fares')
@ApiOperation({ summary: 'Get fare for a schedule and seat class from the fare engine' })
@ApiParam({ name: 'scheduleId', description: 'TrainSchedule UUID' })

View File

@@ -1,7 +1,7 @@
import { IsString, IsDateString, IsInt, IsOptional, IsEnum, IsArray, ValidateNested, IsObject, Min } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { TripStatus, StopStatus } from '@prisma/client';
import { TripStatus, StopStatus, PassengerCategory } from '@prisma/client';
export class PlannedStopTimeDto {
@ApiProperty({ example: 1, description: 'Route stop sequence number this timing applies to' }) @IsInt() @Min(1) sequence: number;
@@ -51,6 +51,7 @@ export class CreateFareRuleDto {
@ApiPropertyOptional({ example: 'schedule-uuid', description: 'Scope fare rule to a specific schedule' }) @IsOptional() @IsString() scheduleId?: string;
@ApiPropertyOptional({ example: 'ADD-DJI', description: 'Scope fare rule to a route code (e.g. ADD-DJI for full route or ADD-ADM for segment)' }) @IsOptional() @IsString() route?: string;
@ApiPropertyOptional({ example: 'Ethiopian', description: 'Scope fare rule to nationality: Ethiopian, Djiboutian, Other' }) @IsOptional() @IsString() nationality?: string;
@ApiPropertyOptional({ enum: PassengerCategory, example: 'ADULT', description: 'Passenger category: ADULT (5+ yrs) or CHILD (<5 yrs)' }) @IsOptional() @IsEnum(PassengerCategory) passengerCategory?: PassengerCategory;
@ApiProperty({ example: 'seat-class-uuid', description: 'Seat class UUID' }) @IsString() seatClassId: string;
@ApiProperty({ example: 45000, description: 'Base fare in minor currency units (ETB cents)' }) @IsInt() baseFareMinor: number;
@ApiProperty({ example: '2026-01-01T00:00:00Z' }) @IsDateString() validFrom: string;
@@ -64,6 +65,7 @@ export class CreateSegmentFareRuleDto {
@ApiProperty({ example: 'seat-class-uuid', description: 'Seat class UUID' }) @IsString() seatClassId: string;
@ApiProperty({ example: 45000, description: 'Base fare in minor currency units (ETB cents)' }) @IsInt() baseFareMinor: number;
@ApiPropertyOptional({ example: 'Ethiopian', description: 'Nationality scope (Ethiopian, Djiboutian, Other)' }) @IsOptional() @IsString() nationality?: string;
@ApiPropertyOptional({ enum: PassengerCategory, example: 'ADULT', description: 'Passenger category: ADULT (5+ yrs) or CHILD (<5 yrs)' }) @IsOptional() @IsEnum(PassengerCategory) passengerCategory?: PassengerCategory;
@ApiProperty({ example: '2026-01-01T00:00:00Z' }) @IsDateString() validFrom: string;
@ApiPropertyOptional({ example: '2026-12-31T23:59:59Z' }) @IsOptional() @IsDateString() validUntil?: string;
}

View File

@@ -329,50 +329,6 @@ export class SchedulesService {
async deleteSchedule(id: string) {
const schedule = await this.prisma.trainSchedule.findUnique({ where: { id } });
if (!schedule) throw new NotFoundException('Schedule not found');
await this.prisma.journeySegment.deleteMany({ where: { scheduleId: id } });
await this.prisma.seatHold.deleteMany({ where: { scheduleId: id } });
const bookings = await this.prisma.booking.findMany({
where: { scheduleId: id },
select: { id: true },
});
const bookingIds = bookings.map(b => b.id);
if (bookingIds.length > 0) {
const paymentIntents = await this.prisma.paymentIntent.findMany({
where: { bookingId: { in: bookingIds } },
select: { id: true },
});
const paymentIntentIds = paymentIntents.map(pi => pi.id);
if (paymentIntentIds.length > 0) {
await this.prisma.paymentRefund.deleteMany({
where: { paymentIntentId: { in: paymentIntentIds } },
});
}
await this.prisma.ticket.deleteMany({
where: { bookingId: { in: bookingIds } },
});
await this.prisma.bookingSeat.deleteMany({
where: { bookingId: { in: bookingIds } },
});
await this.prisma.bookingModification.deleteMany({
where: { bookingId: { in: bookingIds } },
});
await this.prisma.bookingCancellation.deleteMany({
where: { bookingId: { in: bookingIds } },
});
await this.prisma.paymentIntent.deleteMany({
where: { bookingId: { in: bookingIds } },
});
}
await this.prisma.booking.deleteMany({ where: { scheduleId: id } });
await this.prisma.tripStopTime.deleteMany({ where: { scheduleId: id } });
await this.prisma.coachAssignment.deleteMany({ where: { scheduleId: id } });
return this.prisma.trainSchedule.delete({ where: { id } });
}
@@ -402,7 +358,7 @@ export class SchedulesService {
}
createFareRule(dto: CreateFareRuleDto) {
const { validFrom, validUntil, scheduleId, nationality, ...rest } = dto;
const { validFrom, validUntil, scheduleId, nationality, passengerCategory, ...rest } = dto;
return this.prisma.fareRule.create({
data: {
...rest,
@@ -415,7 +371,7 @@ export class SchedulesService {
}
createSegmentFareRule(dto: any) {
const { validFrom, validUntil, ...rest } = dto;
const { validFrom, validUntil, passengerCategory, ...rest } = dto;
return this.prisma.segmentFareRule.create({
data: {
...rest,
@@ -439,7 +395,7 @@ export class SchedulesService {
}
updateSegmentFareRule(id: string, dto: any) {
const { validFrom, validUntil, ...rest } = dto;
const { validFrom, validUntil, passengerCategory, ...rest } = dto;
return this.prisma.segmentFareRule.update({
where: { id },
data: {
@@ -451,12 +407,36 @@ export class SchedulesService {
});
}
async getFareRules(scheduleId?: string) {
const where: any = {};
if (scheduleId) where.tripId = scheduleId;
return this.prisma.fareRule.findMany({
where,
include: { seatClass: true },
orderBy: { createdAt: 'desc' },
});
}
getFareFromEngine(scheduleId: string, seatClassId: string, nationality?: string) {
return this.fareEngine.calculateForSchedule(scheduleId, seatClassId, nationality);
}
getAllFaresFromEngine(scheduleId: string, nationality?: string) {
return this.fareEngine.calculateAllForSchedule(scheduleId, nationality);
async getAllFaresFromEngine(scheduleId: string, nationality?: string) {
try {
const schedule = await this.prisma.trainSchedule.findUnique({
where: { id: scheduleId },
select: { routeId: true, originStationId: true, destinationStationId: true },
});
if (!schedule) throw new NotFoundException('Schedule not found');
if (!schedule.routeId) throw new BadRequestException('Schedule has no associated route');
return await this.fareEngine.calculateAllForSchedule(scheduleId, nationality);
} catch (error) {
throw new BadRequestException(
error instanceof Error ? error.message : 'Failed to calculate fares for schedule'
);
}
}
async syncFaresFromEngine(scheduleId: string): Promise<{ synced: number; errors: string[] }> {