Files
edr-platform/apps/edr-passenger-api/src/modules/bookings/bookings.dto.ts
2026-07-08 18:16:57 +03:00

219 lines
12 KiB
TypeScript

import { IsString, IsArray, ValidateNested, IsOptional, IsInt, IsEnum, IsDate, MaxDate } from 'class-validator';
import { Type, Transform } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Currency, IdDocumentType } from '@prisma/client';
export class PassengerInputDto {
@ApiPropertyOptional({ example: 'seat-uuid', description: 'Outbound / leg-1 seat ID (all booking types). Omit for free children (package bookings) — backend auto-assigns.' }) @IsOptional() @IsString() seatId?: string;
@ApiPropertyOptional({ example: 'leg2-seat-uuid', description: '**TRANSIT / ROUND_TRIP_TRANSIT:** Outbound leg-2 seat ID' }) @IsOptional() @IsString() leg2SeatId?: string;
@ApiPropertyOptional({ example: 'return-seat-uuid', description: '**ROUND_TRIP / ROUND_TRIP_TRANSIT:** Return leg-1 seat ID' }) @IsOptional() @IsString() returnSeatId?: string;
@ApiPropertyOptional({ example: 'ret-leg2-seat-uuid', description: '**ROUND_TRIP_TRANSIT:** Return leg-2 seat ID' }) @IsOptional() @IsString() returnLeg2SeatId?: string;
@ApiProperty({ example: 'Abebe Kebede' }) @IsString() passengerName: string;
@ApiProperty({ example: '1990-05-15', description: 'Date of birth (YYYY-MM-DD). Must not be a future date.' })
// Incoming value is an ISO date string (YYYY-MM-DD); transform to a Date so
// @MaxDate (which requires an actual Date instance) evaluates correctly.
@Transform(({ value }) => (value ? new Date(value) : value))
@IsDate()
@MaxDate(() => new Date(), { message: 'Date of birth cannot be in the future' })
dateOfBirth: Date;
@ApiProperty({ example: 'NATIONAL_ID', enum: IdDocumentType, description: 'NATIONAL_ID for Ethiopians (Verifayda verified), PASSPORT for others' }) @IsEnum(IdDocumentType) idDocumentType: IdDocumentType;
@ApiPropertyOptional({ example: 'ET123456789', description: 'Ethiopian national ID - verified via Verifayda 2.0 (NOT stored in database)' }) @IsOptional() @IsString() idDocumentNumber?: string;
@ApiPropertyOptional({ example: 'P1234567', description: 'Passport number for non-Ethiopian passengers (no verification)' }) @IsOptional() @IsString() passportNumber?: string;
@ApiPropertyOptional({ example: 'Djibouti', description: 'Passport issuing country for non-Ethiopians' }) @IsOptional() @IsString() passportCountry?: string;
@ApiPropertyOptional({ example: 'Ethiopian', description: 'Ethiopian (Verifayda + Telebirr/CBE/eBirr), Djiboutian (Passport + Waafi), Other (Passport + Card)' }) @IsOptional() @IsString() nationality?: string;
@ApiPropertyOptional({ example: 35000, description: 'Actual fare for this passenger in minor units (ETB). When provided, overrides the fare engine calculation — use for berth-specific pricing (Upper/Middle/Lower).' }) @IsOptional() @IsInt() seatFareMinor?: number;
@ApiPropertyOptional({ example: 35000, description: 'Return leg fare for this passenger in minor units (ETB). Used for ROUND_TRIP berth-specific pricing.' }) @IsOptional() @IsInt() returnSeatFareMinor?: number;
}
export class RoundTripPassengerDto {
@ApiPropertyOptional({ description: 'Outbound journey seat ID', example: 'seat-uuid-outbound' })
@IsOptional() @IsString() outboundSeatId?: string;
@ApiPropertyOptional({ description: 'Return journey seat ID', example: 'seat-uuid-return' })
@IsOptional() @IsString() returnSeatId?: string;
@ApiPropertyOptional({ description: '**ROUND_TRIP_TRANSIT:** Outbound leg-2 seat ID' })
@IsOptional() @IsString() outboundLeg2SeatId?: string;
@ApiPropertyOptional({ description: '**ROUND_TRIP_TRANSIT:** Return leg-2 seat ID' })
@IsOptional() @IsString() returnLeg2SeatId?: string;
@ApiProperty({
example: 'Abebe Kebede',
description: 'Full passenger name (will be verified via Verifayda for Ethiopian nationals)'
})
@IsString() passengerName: string;
@ApiProperty({
example: '1990-05-15',
description: 'Date of birth (YYYY-MM-DD). Must not be a future date.'
})
// Incoming value is an ISO date string (YYYY-MM-DD); transform to a Date so
// @MaxDate (which requires an actual Date instance) evaluates correctly.
@Transform(({ value }) => (value ? new Date(value) : value))
@IsDate()
@MaxDate(() => new Date(), { message: 'Date of birth cannot be in the future' })
dateOfBirth: Date;
@ApiProperty({
example: 'NATIONAL_ID',
enum: IdDocumentType,
description: 'NATIONAL_ID for Ethiopians (Verifayda verified), PASSPORT for others'
})
@IsEnum(IdDocumentType) idDocumentType: IdDocumentType;
@ApiPropertyOptional({
example: 'ET123456789',
description: 'Ethiopian national ID - verified via Verifayda 2.0 (NOT stored in database)'
})
@IsOptional() @IsString() idDocumentNumber?: string;
@ApiPropertyOptional({
example: 'P1234567',
description: 'Passport number for non-Ethiopian passengers (no verification)'
})
@IsOptional() @IsString() passportNumber?: string;
@ApiPropertyOptional({
example: 'Djibouti',
description: 'Passport issuing country for non-Ethiopians'
})
@IsOptional() @IsString() passportCountry?: string;
@ApiPropertyOptional({
example: 'Ethiopian',
description: 'Ethiopian (Verifayda + Telebirr/CBE/eBirr), Djiboutian (Passport + Waafi), Other (Passport + Card)'
})
@IsOptional() @IsString() nationality?: string;
}
export class CreateBookingDto {
@ApiProperty({ description: 'Passenger ID' })
@IsString() passengerId: string;
@ApiProperty({ description: 'Outbound / leg-1 schedule ID' })
@IsString() scheduleId: string;
@ApiProperty({ description: 'Outbound / leg-1 seat hold ID' })
@IsString() holdId: string;
@ApiProperty({ example: 'station-uuid', description: 'Outbound origin station UUID' })
@IsString() originStationId: string;
@ApiProperty({ example: 'station-uuid', description: 'Outbound destination station UUID' })
@IsString() destinationStationId: string;
@ApiProperty({ example: 'seat-class-uuid', description: 'Outbound seat class UUID (Economy Regular, Economy Bed, VIP Bed)' })
@IsString() seatClassId: string;
@ApiProperty({
example: 'ONE_WAY',
enum: ['ONE_WAY', 'ROUND_TRIP', 'TRANSIT', 'ROUND_TRIP_TRANSIT'],
description: `Booking type:
**ONE_WAY:** Single direct journey — needs: scheduleId, holdId. Passenger: seatId.
**ROUND_TRIP:** Outbound + return, single PNR — needs above + returnScheduleId, returnHoldId, returnOriginStationId, returnDestinationStationId. Passenger: seatId + returnSeatId.
**TRANSIT:** Single journey via connecting train, single PNR — needs above + leg2ScheduleId, leg2HoldId, transitStationId, leg2DestinationStationId. Passenger: seatId + leg2SeatId.
**ROUND_TRIP_TRANSIT:** Round trip via connecting trains — needs all 4 hold sets + all station fields. Passenger: seatId + leg2SeatId + returnSeatId + returnLeg2SeatId.`,
default: 'ONE_WAY'
})
@IsOptional() @IsString() bookingType?: string;
@ApiProperty({
type: [PassengerInputDto],
description: `Passenger array — required seat fields vary by bookingType:
**ONE_WAY:** { seatId, passengerName, dateOfBirth, idDocumentType, … }
**ROUND_TRIP:** { seatId (outbound leg-1), returnSeatId (return leg-1), passengerName, … }
**TRANSIT:** { seatId (leg-1), leg2SeatId (leg-2), passengerName, … }
**ROUND_TRIP_TRANSIT:** { seatId, leg2SeatId, returnSeatId, returnLeg2SeatId, passengerName, … }
**Age-based pricing:** First child (<5 years) travels FREE, subsequent children pay full fare.`
})
@IsArray() @ValidateNested({ each: true }) @Type(() => PassengerInputDto)
passengers: PassengerInputDto[];
@ApiPropertyOptional({ description: 'Package ID — when set, fare is taken from the package price tier instead of the fare engine' })
@IsOptional() @IsString() packageId?: string;
@ApiPropertyOptional({ description: 'Package price tier ID — required when packageId is provided' })
@IsOptional() @IsString() priceTierId?: string;
@ApiPropertyOptional({ description: 'Total amount in minor units (ETB) as computed and displayed on the review page. When provided, this overrides the fare engine total — use to pass the exact berth-specific amount the user saw.' })
@IsOptional() @IsInt() reviewedTotalMinor?: number;
@ApiPropertyOptional({ description: 'Promo code for discount (applies to combined fare for round-trip)' })
@IsOptional() @IsString() promoCode?: string;
@ApiPropertyOptional({ description: 'Loyalty points to redeem (applies to combined fare for round-trip)' })
@IsOptional() @IsInt() loyaltyRedemptionPoints?: number;
@ApiPropertyOptional({ example: 'DJF', enum: Currency, description: 'Display currency for fare breakdown (ETB, DJF, USD). Transaction always in ETB.' })
@IsOptional() @IsEnum(Currency) displayCurrency?: Currency;
// Transit-specific fields
@ApiPropertyOptional({ description: '**TRANSIT / ROUND_TRIP_TRANSIT:** Leg-2 schedule ID' })
@IsOptional() @IsString() leg2ScheduleId?: string;
@ApiPropertyOptional({ description: '**TRANSIT / ROUND_TRIP_TRANSIT:** Leg-2 seat hold ID' })
@IsOptional() @IsString() leg2HoldId?: string;
@ApiPropertyOptional({ description: '**TRANSIT / ROUND_TRIP_TRANSIT:** Transit (connecting) station UUID' })
@IsOptional() @IsString() transitStationId?: string;
@ApiPropertyOptional({ description: '**TRANSIT / ROUND_TRIP_TRANSIT:** Leg-2 destination station UUID' })
@IsOptional() @IsString() leg2DestinationStationId?: string;
@ApiPropertyOptional({ description: '**TRANSIT / ROUND_TRIP_TRANSIT:** Leg-2 seat class ID (defaults to outbound seatClassId)' })
@IsOptional() @IsString() leg2SeatClassId?: string;
// Round-trip transit: return direction transit fields
@ApiPropertyOptional({ description: '**ROUND_TRIP_TRANSIT:** Return leg-1 schedule ID' })
@IsOptional() @IsString() returnScheduleId?: string;
@ApiPropertyOptional({ description: '**ROUND_TRIP / ROUND_TRIP_TRANSIT:** Return origin station ID' })
@IsOptional() @IsString() returnOriginStationId?: string;
@ApiPropertyOptional({ description: '**ROUND_TRIP / ROUND_TRIP_TRANSIT:** Return destination station ID' })
@IsOptional() @IsString() returnDestinationStationId?: string;
@ApiPropertyOptional({ description: '**ROUND_TRIP / ROUND_TRIP_TRANSIT:** Return seat hold ID' })
@IsOptional() @IsString() returnHoldId?: string;
@ApiPropertyOptional({ description: '**ROUND_TRIP / ROUND_TRIP_TRANSIT:** Return seat class ID' })
@IsOptional() @IsString() returnSeatClassId?: string;
@ApiPropertyOptional({ description: '**ROUND_TRIP_TRANSIT:** Return leg-2 schedule ID' })
@IsOptional() @IsString() returnLeg2ScheduleId?: string;
@ApiPropertyOptional({ description: '**ROUND_TRIP_TRANSIT:** Return leg-2 seat hold ID' })
@IsOptional() @IsString() returnLeg2HoldId?: string;
@ApiPropertyOptional({ description: '**ROUND_TRIP_TRANSIT:** Return transit (connecting) station UUID' })
@IsOptional() @IsString() returnTransitStationId?: string;
@ApiPropertyOptional({ description: '**ROUND_TRIP_TRANSIT:** Return leg-2 destination station UUID' })
@IsOptional() @IsString() returnLeg2DestinationStationId?: string;
@ApiPropertyOptional({ description: '**ROUND_TRIP_TRANSIT:** Return leg-2 seat class ID' })
@IsOptional() @IsString() returnLeg2SeatClassId?: string;
}
export class ModifyBookingDto {
@ApiProperty() @IsString() bookingRef: string;
@ApiProperty({ example: 'schedule-uuid' }) @IsString() newScheduleId: string;
@ApiProperty({ type: [String] }) @IsArray() newSeatIds: string[];
@ApiPropertyOptional() @IsOptional() @IsString() reason?: string;
}
export class CancelBookingDto {
@ApiProperty() @IsString() bookingRef: string;
@ApiPropertyOptional() @IsOptional() @IsString() reason?: string;
}