Merge branch 'alpha' into passenger/feat/iam

This commit is contained in:
Abubeker Yasin
2026-06-23 14:47:24 +03:00
37 changed files with 1271 additions and 1048 deletions

View File

@@ -20,7 +20,7 @@ CREATE TYPE "IdDocumentType" AS ENUM ('NATIONAL_ID', 'PASSPORT', 'DRIVING_LICENS
CREATE TYPE "Currency" AS ENUM ('ETB', 'DJF', 'USD');
-- CreateEnum
CREATE TYPE "BookingStatus" AS ENUM ('DRAFT', 'PENDING_PAYMENT', 'CONFIRMED', 'CANCELLED', 'COMPLETED', 'NO_SHOW', 'REFUNDED');
CREATE TYPE "BookingStatus" AS ENUM ('DRAFT', 'PENDING_PAYMENT', 'CONFIRMED', 'CANCELLED', 'BOARDED', 'NO_SHOW', 'REFUNDED');
-- CreateEnum
CREATE TYPE "PaymentRegion" AS ENUM ('ETHIOPIA', 'DJIBOUTI', 'INTERNATIONAL', 'GLOBAL');
@@ -76,7 +76,9 @@ CREATE TABLE "SeatClass" (
"coachTypeId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"baseFareMinor" INTEGER NOT NULL,
"baseFareMinor" INTEGER NOT NULL DEFAULT 0,
"premiumMinor" INTEGER NOT NULL DEFAULT 0,
"insuranceFeeMinor" INTEGER NOT NULL DEFAULT 0,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
@@ -94,6 +96,8 @@ CREATE TABLE "User" (
"role" "UserRole" NOT NULL DEFAULT 'PASSENGER',
"nationality" TEXT,
"nationalityCode" TEXT,
"gender" TEXT,
"dateOfBirth" TIMESTAMP(3),
"passportNumber" TEXT,
"nationalId" TEXT,
"failedLoginAttempts" INTEGER NOT NULL DEFAULT 0,
@@ -155,10 +159,11 @@ CREATE TABLE "Station" (
"name" TEXT NOT NULL,
"city" TEXT NOT NULL,
"countryCode" TEXT,
"sequence" INTEGER NOT NULL DEFAULT 0,
"isOperational" BOOLEAN NOT NULL DEFAULT true,
"timezone" TEXT NOT NULL DEFAULT 'Africa/Addis_Ababa',
"lat" DECIMAL(9,6) NOT NULL,
"lng" DECIMAL(9,6) NOT NULL,
"lat" DECIMAL(9,6),
"lng" DECIMAL(9,6),
CONSTRAINT "Station_pkey" PRIMARY KEY ("id")
);
@@ -234,6 +239,7 @@ CREATE TABLE "Coach" (
"number" TEXT NOT NULL,
"arrangement" TEXT NOT NULL DEFAULT '2+2',
"capacity" INTEGER NOT NULL DEFAULT 0,
"sequence" INTEGER NOT NULL DEFAULT 0,
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

View File

@@ -140,11 +140,7 @@ ALTER TABLE "SeatClass" ALTER COLUMN "baseFareMinor" SET DEFAULT 0;
-- AlterTable
ALTER TABLE "Ticket" ALTER COLUMN "status" SET DEFAULT 'ACTIVE';
-- AlterTable
-- gender is created here on a clean migration history (no prior migration adds it);
-- on an already-drifted DB where it exists as varchar, normalize it to TEXT.
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "gender" TEXT;
ALTER TABLE "User" ALTER COLUMN "gender" SET DATA TYPE TEXT;
-- gender column already TEXT from init migration
-- CreateIndex
CREATE INDEX "Booking_bookingType_idx" ON "Booking"("bookingType");

View File

@@ -0,0 +1,2 @@
-- Empty placeholder migration
SELECT 1;

View File

@@ -1,36 +1,9 @@
-- Add sequence column to Station table if it doesn't exist
ALTER TABLE "passenger"."Station" ADD COLUMN IF NOT EXISTS "sequence" INTEGER NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS "Station_sequence_idx" ON "Station"("sequence");
-- Add index on sequence for Station
CREATE INDEX IF NOT EXISTS "Station_sequence_idx" ON "passenger"."Station"("sequence");
-- Add sequence column to Coach table if it doesn't exist
ALTER TABLE "passenger"."Coach" ADD COLUMN IF NOT EXISTS "sequence" INTEGER NOT NULL DEFAULT 0;
-- Add index on sequence for Coach
CREATE INDEX IF NOT EXISTS "Coach_sequence_idx" ON "passenger"."Coach"("sequence");
-- Add missing columns to SeatClass if they don't exist
ALTER TABLE "passenger"."SeatClass" ADD COLUMN IF NOT EXISTS "premiumMinor" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "passenger"."SeatClass" ADD COLUMN IF NOT EXISTS "insuranceFeeMinor" INTEGER NOT NULL DEFAULT 0;
-- Add missing columns to User if they don't exist
ALTER TABLE "passenger"."User" ADD COLUMN IF NOT EXISTS "gender" VARCHAR(255);
ALTER TABLE "passenger"."User" ADD COLUMN IF NOT EXISTS "dateOfBirth" TIMESTAMP(3);
ALTER TABLE "passenger"."User" ADD COLUMN IF NOT EXISTS "passportNumber" VARCHAR(255);
ALTER TABLE "passenger"."User" ADD COLUMN IF NOT EXISTS "nationalId" VARCHAR(255);
-- Ensure Ticket has all required columns
ALTER TABLE "passenger"."Ticket" ADD COLUMN IF NOT EXISTS "validatedAt" TIMESTAMP(3);
ALTER TABLE "passenger"."Ticket" ADD COLUMN IF NOT EXISTS "boardedAt" TIMESTAMP(3);
-- Add missing columns to Booking if they don't exist
ALTER TABLE "passenger"."Booking" ADD COLUMN IF NOT EXISTS "bookingType" VARCHAR(255) NOT NULL DEFAULT 'ONE_WAY';
ALTER TABLE "passenger"."Booking" ADD COLUMN IF NOT EXISTS "displayCurrency" VARCHAR(255);
ALTER TABLE "passenger"."Booking" ADD COLUMN IF NOT EXISTS "displayTotalMinor" INTEGER;
CREATE INDEX IF NOT EXISTS "Coach_sequence_idx" ON "Coach"("sequence");
-- Ensure all indexes exist
CREATE INDEX IF NOT EXISTS "Station_city_countryCode_idx" ON "passenger"."Station"("city", "countryCode");
CREATE INDEX IF NOT EXISTS "Coach_coachTypeId_idx" ON "passenger"."Coach"("coachTypeId");
CREATE INDEX IF NOT EXISTS "TrainSchedule_departureAt_originStationId_idx" ON "passenger"."TrainSchedule"("departureAt", "originStationId");
CREATE INDEX IF NOT EXISTS "Booking_passengerId_status_idx" ON "passenger"."Booking"("passengerId", "status");
CREATE INDEX IF NOT EXISTS "Station_city_countryCode_idx" ON "Station"("city", "countryCode");
CREATE INDEX IF NOT EXISTS "Coach_coachTypeId_idx" ON "Coach"("coachTypeId");
CREATE INDEX IF NOT EXISTS "TrainSchedule_departureAt_originStationId_idx" ON "TrainSchedule"("departureAt", "originStationId");
CREATE INDEX IF NOT EXISTS "Booking_passengerId_status_idx" ON "Booking"("passengerId", "status");

View File

@@ -1,164 +1,164 @@
-- Add CASCADE delete to all foreign key constraints that are missing it
-- TrainSchedule relations
ALTER TABLE "passenger"."TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_trainId_fkey";
ALTER TABLE "passenger"."TrainSchedule" ADD CONSTRAINT "TrainSchedule_trainId_fkey" FOREIGN KEY ("trainId") REFERENCES "passenger"."Train"("id") ON DELETE CASCADE;
ALTER TABLE "TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_trainId_fkey";
ALTER TABLE "TrainSchedule" ADD CONSTRAINT "TrainSchedule_trainId_fkey" FOREIGN KEY ("trainId") REFERENCES "Train"("id") ON DELETE CASCADE;
ALTER TABLE "passenger"."TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_routeId_fkey";
ALTER TABLE "passenger"."TrainSchedule" ADD CONSTRAINT "TrainSchedule_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "passenger"."Route"("id") ON DELETE CASCADE;
ALTER TABLE "TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_routeId_fkey";
ALTER TABLE "TrainSchedule" ADD CONSTRAINT "TrainSchedule_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "Route"("id") ON DELETE CASCADE;
ALTER TABLE "passenger"."TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_originStationId_fkey";
ALTER TABLE "passenger"."TrainSchedule" ADD CONSTRAINT "TrainSchedule_originStationId_fkey" FOREIGN KEY ("originStationId") REFERENCES "passenger"."Station"("id") ON DELETE CASCADE;
ALTER TABLE "TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_originStationId_fkey";
ALTER TABLE "TrainSchedule" ADD CONSTRAINT "TrainSchedule_originStationId_fkey" FOREIGN KEY ("originStationId") REFERENCES "Station"("id") ON DELETE CASCADE;
ALTER TABLE "passenger"."TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_destinationStationId_fkey";
ALTER TABLE "passenger"."TrainSchedule" ADD CONSTRAINT "TrainSchedule_destinationStationId_fkey" FOREIGN KEY ("destinationStationId") REFERENCES "passenger"."Station"("id") ON DELETE CASCADE;
ALTER TABLE "TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_destinationStationId_fkey";
ALTER TABLE "TrainSchedule" ADD CONSTRAINT "TrainSchedule_destinationStationId_fkey" FOREIGN KEY ("destinationStationId") REFERENCES "Station"("id") ON DELETE CASCADE;
-- Coach relation
ALTER TABLE "passenger"."Coach" DROP CONSTRAINT IF EXISTS "Coach_coachTypeId_fkey";
ALTER TABLE "passenger"."Coach" ADD CONSTRAINT "Coach_coachTypeId_fkey" FOREIGN KEY ("coachTypeId") REFERENCES "passenger"."CoachType"("id") ON DELETE CASCADE;
ALTER TABLE "Coach" DROP CONSTRAINT IF EXISTS "Coach_coachTypeId_fkey";
ALTER TABLE "Coach" ADD CONSTRAINT "Coach_coachTypeId_fkey" FOREIGN KEY ("coachTypeId") REFERENCES "CoachType"("id") ON DELETE CASCADE;
-- CoachAssignment relations
ALTER TABLE "passenger"."CoachAssignment" DROP CONSTRAINT IF EXISTS "CoachAssignment_scheduleId_fkey";
ALTER TABLE "passenger"."CoachAssignment" ADD CONSTRAINT "CoachAssignment_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE;
ALTER TABLE "CoachAssignment" DROP CONSTRAINT IF EXISTS "CoachAssignment_scheduleId_fkey";
ALTER TABLE "CoachAssignment" ADD CONSTRAINT "CoachAssignment_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE;
ALTER TABLE "passenger"."CoachAssignment" DROP CONSTRAINT IF EXISTS "CoachAssignment_coachId_fkey";
ALTER TABLE "passenger"."CoachAssignment" ADD CONSTRAINT "CoachAssignment_coachId_fkey" FOREIGN KEY ("coachId") REFERENCES "passenger"."Coach"("id") ON DELETE CASCADE;
ALTER TABLE "CoachAssignment" DROP CONSTRAINT IF EXISTS "CoachAssignment_coachId_fkey";
ALTER TABLE "CoachAssignment" ADD CONSTRAINT "CoachAssignment_coachId_fkey" FOREIGN KEY ("coachId") REFERENCES "Coach"("id") ON DELETE CASCADE;
-- Booking relations
ALTER TABLE "passenger"."Booking" DROP CONSTRAINT IF EXISTS "Booking_passengerId_fkey";
ALTER TABLE "passenger"."Booking" ADD CONSTRAINT "Booking_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "passenger"."Passenger"("id") ON DELETE CASCADE;
ALTER TABLE "Booking" DROP CONSTRAINT IF EXISTS "Booking_passengerId_fkey";
ALTER TABLE "Booking" ADD CONSTRAINT "Booking_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "Passenger"("id") ON DELETE CASCADE;
ALTER TABLE "passenger"."Booking" DROP CONSTRAINT IF EXISTS "Booking_scheduleId_fkey";
ALTER TABLE "passenger"."Booking" ADD CONSTRAINT "Booking_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE;
ALTER TABLE "Booking" DROP CONSTRAINT IF EXISTS "Booking_scheduleId_fkey";
ALTER TABLE "Booking" ADD CONSTRAINT "Booking_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE;
-- BookingSeat relations
ALTER TABLE "passenger"."BookingSeat" DROP CONSTRAINT IF EXISTS "BookingSeat_bookingId_fkey";
ALTER TABLE "passenger"."BookingSeat" ADD CONSTRAINT "BookingSeat_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE;
ALTER TABLE "BookingSeat" DROP CONSTRAINT IF EXISTS "BookingSeat_bookingId_fkey";
ALTER TABLE "BookingSeat" ADD CONSTRAINT "BookingSeat_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE;
ALTER TABLE "passenger"."BookingSeat" DROP CONSTRAINT IF EXISTS "BookingSeat_seatId_fkey";
ALTER TABLE "passenger"."BookingSeat" ADD CONSTRAINT "BookingSeat_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "passenger"."Seat"("id") ON DELETE CASCADE;
ALTER TABLE "BookingSeat" DROP CONSTRAINT IF EXISTS "BookingSeat_seatId_fkey";
ALTER TABLE "BookingSeat" ADD CONSTRAINT "BookingSeat_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "Seat"("id") ON DELETE CASCADE;
-- PaymentIntent
ALTER TABLE "passenger"."PaymentIntent" DROP CONSTRAINT IF EXISTS "PaymentIntent_bookingId_fkey";
ALTER TABLE "passenger"."PaymentIntent" ADD CONSTRAINT "PaymentIntent_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE;
ALTER TABLE "PaymentIntent" DROP CONSTRAINT IF EXISTS "PaymentIntent_bookingId_fkey";
ALTER TABLE "PaymentIntent" ADD CONSTRAINT "PaymentIntent_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE;
-- PaymentRefund
ALTER TABLE "passenger"."PaymentRefund" DROP CONSTRAINT IF EXISTS "PaymentRefund_paymentIntentId_fkey";
ALTER TABLE "passenger"."PaymentRefund" ADD CONSTRAINT "PaymentRefund_paymentIntentId_fkey" FOREIGN KEY ("paymentIntentId") REFERENCES "passenger"."PaymentIntent"("id") ON DELETE CASCADE;
ALTER TABLE "PaymentRefund" DROP CONSTRAINT IF EXISTS "PaymentRefund_paymentIntentId_fkey";
ALTER TABLE "PaymentRefund" ADD CONSTRAINT "PaymentRefund_paymentIntentId_fkey" FOREIGN KEY ("paymentIntentId") REFERENCES "PaymentIntent"("id") ON DELETE CASCADE;
-- Ticket
ALTER TABLE "passenger"."Ticket" DROP CONSTRAINT IF EXISTS "Ticket_bookingId_fkey";
ALTER TABLE "passenger"."Ticket" ADD CONSTRAINT "Ticket_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE;
ALTER TABLE "Ticket" DROP CONSTRAINT IF EXISTS "Ticket_bookingId_fkey";
ALTER TABLE "Ticket" ADD CONSTRAINT "Ticket_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE;
-- TicketSeat
ALTER TABLE "passenger"."TicketSeat" DROP CONSTRAINT IF EXISTS "TicketSeat_seatId_fkey";
ALTER TABLE "passenger"."TicketSeat" ADD CONSTRAINT "TicketSeat_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "passenger"."Seat"("id") ON DELETE CASCADE;
ALTER TABLE "TicketSeat" DROP CONSTRAINT IF EXISTS "TicketSeat_seatId_fkey";
ALTER TABLE "TicketSeat" ADD CONSTRAINT "TicketSeat_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "Seat"("id") ON DELETE CASCADE;
-- WalletLedgerEntry
ALTER TABLE "passenger"."WalletLedgerEntry" DROP CONSTRAINT IF EXISTS "WalletLedgerEntry_walletId_fkey";
ALTER TABLE "passenger"."WalletLedgerEntry" ADD CONSTRAINT "WalletLedgerEntry_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "passenger"."WalletAccount"("id") ON DELETE CASCADE;
ALTER TABLE "WalletLedgerEntry" DROP CONSTRAINT IF EXISTS "WalletLedgerEntry_walletId_fkey";
ALTER TABLE "WalletLedgerEntry" ADD CONSTRAINT "WalletLedgerEntry_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "WalletAccount"("id") ON DELETE CASCADE;
-- Notification
ALTER TABLE "passenger"."Notification" DROP CONSTRAINT IF EXISTS "Notification_passengerId_fkey";
ALTER TABLE "passenger"."Notification" ADD CONSTRAINT "Notification_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "passenger"."Passenger"("id") ON DELETE CASCADE;
ALTER TABLE "Notification" DROP CONSTRAINT IF EXISTS "Notification_passengerId_fkey";
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "Passenger"("id") ON DELETE CASCADE;
-- MenuItem
ALTER TABLE "passenger"."MenuItem" DROP CONSTRAINT IF EXISTS "MenuItem_scheduleId_fkey";
ALTER TABLE "passenger"."MenuItem" ADD CONSTRAINT "MenuItem_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE;
ALTER TABLE "MenuItem" DROP CONSTRAINT IF EXISTS "MenuItem_scheduleId_fkey";
ALTER TABLE "MenuItem" ADD CONSTRAINT "MenuItem_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE;
ALTER TABLE "passenger"."MenuItem" DROP CONSTRAINT IF EXISTS "MenuItem_categoryId_fkey";
ALTER TABLE "passenger"."MenuItem" ADD CONSTRAINT "MenuItem_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "passenger"."MenuCategory"("id") ON DELETE CASCADE;
ALTER TABLE "MenuItem" DROP CONSTRAINT IF EXISTS "MenuItem_categoryId_fkey";
ALTER TABLE "MenuItem" ADD CONSTRAINT "MenuItem_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "MenuCategory"("id") ON DELETE CASCADE;
-- FoodOrder
ALTER TABLE "passenger"."FoodOrder" DROP CONSTRAINT IF EXISTS "FoodOrder_bookingId_fkey";
ALTER TABLE "passenger"."FoodOrder" ADD CONSTRAINT "FoodOrder_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE;
ALTER TABLE "FoodOrder" DROP CONSTRAINT IF EXISTS "FoodOrder_bookingId_fkey";
ALTER TABLE "FoodOrder" ADD CONSTRAINT "FoodOrder_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE;
-- FoodOrderItem
ALTER TABLE "passenger"."FoodOrderItem" DROP CONSTRAINT IF EXISTS "FoodOrderItem_orderId_fkey";
ALTER TABLE "passenger"."FoodOrderItem" ADD CONSTRAINT "FoodOrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "passenger"."FoodOrder"("id") ON DELETE CASCADE;
ALTER TABLE "FoodOrderItem" DROP CONSTRAINT IF EXISTS "FoodOrderItem_orderId_fkey";
ALTER TABLE "FoodOrderItem" ADD CONSTRAINT "FoodOrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "FoodOrder"("id") ON DELETE CASCADE;
-- FaqArticle
ALTER TABLE "passenger"."FaqArticle" DROP CONSTRAINT IF EXISTS "FaqArticle_categoryId_fkey";
ALTER TABLE "passenger"."FaqArticle" ADD CONSTRAINT "FaqArticle_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "passenger"."FaqCategory"("id") ON DELETE CASCADE;
ALTER TABLE "FaqArticle" DROP CONSTRAINT IF EXISTS "FaqArticle_categoryId_fkey";
ALTER TABLE "FaqArticle" ADD CONSTRAINT "FaqArticle_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "FaqCategory"("id") ON DELETE CASCADE;
-- SupportMessage
ALTER TABLE "passenger"."SupportMessage" DROP CONSTRAINT IF EXISTS "SupportMessage_conversationId_fkey";
ALTER TABLE "passenger"."SupportMessage" ADD CONSTRAINT "SupportMessage_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "passenger"."SupportConversation"("id") ON DELETE CASCADE;
ALTER TABLE "SupportMessage" DROP CONSTRAINT IF EXISTS "SupportMessage_conversationId_fkey";
ALTER TABLE "SupportMessage" ADD CONSTRAINT "SupportMessage_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "SupportConversation"("id") ON DELETE CASCADE;
-- TripStopTime
ALTER TABLE "passenger"."TripStopTime" DROP CONSTRAINT IF EXISTS "TripStopTime_scheduleId_fkey";
ALTER TABLE "passenger"."TripStopTime" ADD CONSTRAINT "TripStopTime_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE;
ALTER TABLE "TripStopTime" DROP CONSTRAINT IF EXISTS "TripStopTime_scheduleId_fkey";
ALTER TABLE "TripStopTime" ADD CONSTRAINT "TripStopTime_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE;
-- TripLiveStatus
ALTER TABLE "passenger"."TripLiveStatus" DROP CONSTRAINT IF EXISTS "TripLiveStatus_scheduleId_fkey";
ALTER TABLE "passenger"."TripLiveStatus" ADD CONSTRAINT "TripLiveStatus_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE;
ALTER TABLE "TripLiveStatus" DROP CONSTRAINT IF EXISTS "TripLiveStatus_scheduleId_fkey";
ALTER TABLE "TripLiveStatus" ADD CONSTRAINT "TripLiveStatus_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE;
-- JourneySegment
ALTER TABLE "passenger"."JourneySegment" DROP CONSTRAINT IF EXISTS "JourneySegment_journeyId_fkey";
ALTER TABLE "passenger"."JourneySegment" ADD CONSTRAINT "JourneySegment_journeyId_fkey" FOREIGN KEY ("journeyId") REFERENCES "passenger"."Journey"("id") ON DELETE CASCADE;
ALTER TABLE "JourneySegment" DROP CONSTRAINT IF EXISTS "JourneySegment_journeyId_fkey";
ALTER TABLE "JourneySegment" ADD CONSTRAINT "JourneySegment_journeyId_fkey" FOREIGN KEY ("journeyId") REFERENCES "Journey"("id") ON DELETE CASCADE;
ALTER TABLE "passenger"."JourneySegment" DROP CONSTRAINT IF EXISTS "JourneySegment_scheduleId_fkey";
ALTER TABLE "passenger"."JourneySegment" ADD CONSTRAINT "JourneySegment_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE;
ALTER TABLE "JourneySegment" DROP CONSTRAINT IF EXISTS "JourneySegment_scheduleId_fkey";
ALTER TABLE "JourneySegment" ADD CONSTRAINT "JourneySegment_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE;
-- AgentBooking
ALTER TABLE "passenger"."AgentBooking" DROP CONSTRAINT IF EXISTS "AgentBooking_agentId_fkey";
ALTER TABLE "passenger"."AgentBooking" ADD CONSTRAINT "AgentBooking_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "passenger"."Agent"("id") ON DELETE CASCADE;
ALTER TABLE "AgentBooking" DROP CONSTRAINT IF EXISTS "AgentBooking_agentId_fkey";
ALTER TABLE "AgentBooking" ADD CONSTRAINT "AgentBooking_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE CASCADE;
ALTER TABLE "passenger"."AgentBooking" DROP CONSTRAINT IF EXISTS "AgentBooking_bookingId_fkey";
ALTER TABLE "passenger"."AgentBooking" ADD CONSTRAINT "AgentBooking_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE;
ALTER TABLE "AgentBooking" DROP CONSTRAINT IF EXISTS "AgentBooking_bookingId_fkey";
ALTER TABLE "AgentBooking" ADD CONSTRAINT "AgentBooking_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE;
-- AgentShift
ALTER TABLE "passenger"."AgentShift" DROP CONSTRAINT IF EXISTS "AgentShift_agentId_fkey";
ALTER TABLE "passenger"."AgentShift" ADD CONSTRAINT "AgentShift_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "passenger"."Agent"("id") ON DELETE CASCADE;
ALTER TABLE "AgentShift" DROP CONSTRAINT IF EXISTS "AgentShift_agentId_fkey";
ALTER TABLE "AgentShift" ADD CONSTRAINT "AgentShift_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE CASCADE;
-- AgentCommission
ALTER TABLE "passenger"."AgentCommission" DROP CONSTRAINT IF EXISTS "AgentCommission_agentId_fkey";
ALTER TABLE "passenger"."AgentCommission" ADD CONSTRAINT "AgentCommission_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "passenger"."Agent"("id") ON DELETE CASCADE;
ALTER TABLE "AgentCommission" DROP CONSTRAINT IF EXISTS "AgentCommission_agentId_fkey";
ALTER TABLE "AgentCommission" ADD CONSTRAINT "AgentCommission_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE CASCADE;
-- BookingModification
ALTER TABLE "passenger"."BookingModification" DROP CONSTRAINT IF EXISTS "BookingModification_bookingId_fkey";
ALTER TABLE "passenger"."BookingModification" ADD CONSTRAINT "BookingModification_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE;
ALTER TABLE "BookingModification" DROP CONSTRAINT IF EXISTS "BookingModification_bookingId_fkey";
ALTER TABLE "BookingModification" ADD CONSTRAINT "BookingModification_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE;
-- BookingCancellation
ALTER TABLE "passenger"."BookingCancellation" DROP CONSTRAINT IF EXISTS "BookingCancellation_bookingId_fkey";
ALTER TABLE "passenger"."BookingCancellation" ADD CONSTRAINT "BookingCancellation_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE;
ALTER TABLE "BookingCancellation" DROP CONSTRAINT IF EXISTS "BookingCancellation_bookingId_fkey";
ALTER TABLE "BookingCancellation" ADD CONSTRAINT "BookingCancellation_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE;
-- GateValidationLog
ALTER TABLE "passenger"."GateValidationLog" DROP CONSTRAINT IF EXISTS "GateValidationLog_ticketId_fkey";
ALTER TABLE "passenger"."GateValidationLog" ADD CONSTRAINT "GateValidationLog_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "passenger"."Ticket"("id") ON DELETE CASCADE;
ALTER TABLE "GateValidationLog" DROP CONSTRAINT IF EXISTS "GateValidationLog_ticketId_fkey";
ALTER TABLE "GateValidationLog" ADD CONSTRAINT "GateValidationLog_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "Ticket"("id") ON DELETE CASCADE;
-- BaggageBooking
ALTER TABLE "passenger"."BaggageBooking" DROP CONSTRAINT IF EXISTS "BaggageBooking_bookingId_fkey";
ALTER TABLE "passenger"."BaggageBooking" ADD CONSTRAINT "BaggageBooking_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE;
ALTER TABLE "BaggageBooking" DROP CONSTRAINT IF EXISTS "BaggageBooking_bookingId_fkey";
ALTER TABLE "BaggageBooking" ADD CONSTRAINT "BaggageBooking_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE;
-- RouteFareRule
ALTER TABLE "passenger"."RouteFareRule" DROP CONSTRAINT IF EXISTS "RouteFareRule_seatClassId_fkey";
ALTER TABLE "passenger"."RouteFareRule" ADD CONSTRAINT "RouteFareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "passenger"."SeatClass"("id") ON DELETE CASCADE;
ALTER TABLE "RouteFareRule" DROP CONSTRAINT IF EXISTS "RouteFareRule_seatClassId_fkey";
ALTER TABLE "RouteFareRule" ADD CONSTRAINT "RouteFareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "SeatClass"("id") ON DELETE CASCADE;
-- SegmentFareRule
ALTER TABLE "passenger"."SegmentFareRule" DROP CONSTRAINT IF EXISTS "SegmentFareRule_seatClassId_fkey";
ALTER TABLE "passenger"."SegmentFareRule" ADD CONSTRAINT "SegmentFareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "passenger"."SeatClass"("id") ON DELETE CASCADE;
ALTER TABLE "SegmentFareRule" DROP CONSTRAINT IF EXISTS "SegmentFareRule_seatClassId_fkey";
ALTER TABLE "SegmentFareRule" ADD CONSTRAINT "SegmentFareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "SeatClass"("id") ON DELETE CASCADE;
-- StationCrowdSignal
ALTER TABLE "passenger"."StationCrowdSignal" DROP CONSTRAINT IF EXISTS "StationCrowdSignal_stationId_fkey";
ALTER TABLE "passenger"."StationCrowdSignal" ADD CONSTRAINT "StationCrowdSignal_stationId_fkey" FOREIGN KEY ("stationId") REFERENCES "passenger"."Station"("id") ON DELETE CASCADE;
ALTER TABLE "StationCrowdSignal" DROP CONSTRAINT IF EXISTS "StationCrowdSignal_stationId_fkey";
ALTER TABLE "StationCrowdSignal" ADD CONSTRAINT "StationCrowdSignal_stationId_fkey" FOREIGN KEY ("stationId") REFERENCES "Station"("id") ON DELETE CASCADE;
-- SeatBlock
ALTER TABLE "passenger"."SeatBlock" DROP CONSTRAINT IF EXISTS "SeatBlock_seatId_fkey";
ALTER TABLE "passenger"."SeatBlock" ADD CONSTRAINT "SeatBlock_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "passenger"."Seat"("id") ON DELETE CASCADE;
ALTER TABLE "SeatBlock" DROP CONSTRAINT IF EXISTS "SeatBlock_seatId_fkey";
ALTER TABLE "SeatBlock" ADD CONSTRAINT "SeatBlock_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "Seat"("id") ON DELETE CASCADE;
-- SavedRoute
ALTER TABLE "passenger"."SavedRoute" DROP CONSTRAINT IF EXISTS "SavedRoute_passengerId_fkey";
ALTER TABLE "passenger"."SavedRoute" ADD CONSTRAINT "SavedRoute_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "passenger"."Passenger"("id") ON DELETE CASCADE;
ALTER TABLE "SavedRoute" DROP CONSTRAINT IF EXISTS "SavedRoute_passengerId_fkey";
ALTER TABLE "SavedRoute" ADD CONSTRAINT "SavedRoute_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "Passenger"("id") ON DELETE CASCADE;
-- LoyaltyLedgerEntry
ALTER TABLE "passenger"."LoyaltyLedgerEntry" DROP CONSTRAINT IF EXISTS "LoyaltyLedgerEntry_accountId_fkey";
ALTER TABLE "passenger"."LoyaltyLedgerEntry" ADD CONSTRAINT "LoyaltyLedgerEntry_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "passenger"."LoyaltyAccount"("id") ON DELETE CASCADE;
ALTER TABLE "LoyaltyLedgerEntry" DROP CONSTRAINT IF EXISTS "LoyaltyLedgerEntry_accountId_fkey";
ALTER TABLE "LoyaltyLedgerEntry" ADD CONSTRAINT "LoyaltyLedgerEntry_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "LoyaltyAccount"("id") ON DELETE CASCADE;
-- LoyaltyReward
ALTER TABLE "passenger"."LoyaltyReward" DROP CONSTRAINT IF EXISTS "LoyaltyReward_accountId_fkey";
ALTER TABLE "passenger"."LoyaltyReward" ADD CONSTRAINT "LoyaltyReward_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "passenger"."LoyaltyAccount"("id") ON DELETE CASCADE;
ALTER TABLE "LoyaltyReward" DROP CONSTRAINT IF EXISTS "LoyaltyReward_accountId_fkey";
ALTER TABLE "LoyaltyReward" ADD CONSTRAINT "LoyaltyReward_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "LoyaltyAccount"("id") ON DELETE CASCADE;
-- FareRule
ALTER TABLE "passenger"."FareRule" DROP CONSTRAINT IF EXISTS "FareRule_seatClassId_fkey";
ALTER TABLE "passenger"."FareRule" ADD CONSTRAINT "FareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "passenger"."SeatClass"("id") ON DELETE CASCADE;
ALTER TABLE "FareRule" DROP CONSTRAINT IF EXISTS "FareRule_seatClassId_fkey";
ALTER TABLE "FareRule" ADD CONSTRAINT "FareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "SeatClass"("id") ON DELETE CASCADE;

View File

@@ -1,18 +1,18 @@
-- CreateEnum
CREATE TYPE "passenger"."ReturnLegStatus" AS ENUM ('NOT_APPLICABLE', 'BOTH_USED', 'OUTBOUND_ONLY', 'INBOUND_ONLY', 'NEITHER_USED');
CREATE TYPE "ReturnLegStatus" AS ENUM ('NOT_APPLICABLE', 'BOTH_USED', 'OUTBOUND_ONLY', 'INBOUND_ONLY', 'NEITHER_USED');
-- AlterTable: add return leg tracking columns to Booking
ALTER TABLE "passenger"."Booking"
ADD COLUMN "returnLegStatus" "passenger"."ReturnLegStatus" NOT NULL DEFAULT 'NOT_APPLICABLE',
ALTER TABLE "Booking"
ADD COLUMN "returnLegStatus" "ReturnLegStatus" NOT NULL DEFAULT 'NOT_APPLICABLE',
ADD COLUMN "outboundBoardedAt" TIMESTAMP(3),
ADD COLUMN "returnBoardedAt" TIMESTAMP(3);
-- Set NEITHER_USED for existing confirmed round-trip bookings
UPDATE "passenger"."Booking"
UPDATE "Booking"
SET "returnLegStatus" = 'NEITHER_USED'
WHERE "bookingType" = 'ROUND_TRIP'
AND "status" IN ('CONFIRMED', 'COMPLETED');
AND "status" IN ('CONFIRMED', 'BOARDED');
-- AlterTable: add leg column to GateValidationLog
ALTER TABLE "passenger"."GateValidationLog"
ALTER TABLE "GateValidationLog"
ADD COLUMN "leg" TEXT;

View File

@@ -1,7 +1,7 @@
-- Create passenger schema if it doesn't exist
CREATE SCHEMA IF NOT EXISTS passenger;
-- Move all enums from public to passenger schema
-- Move enums from public to passenger schema (only if they exist in public)
DO $$
DECLARE
e text;
@@ -13,9 +13,10 @@ BEGIN
LOOP
EXECUTE format('ALTER TYPE public.%I SET SCHEMA passenger', e);
END LOOP;
EXCEPTION WHEN others THEN NULL;
END $$;
-- Move all tables from public to passenger schema
-- Move tables from public to passenger schema (only if they exist in public)
DO $$
DECLARE
t text;
@@ -26,6 +27,7 @@ BEGIN
LOOP
EXECUTE format('ALTER TABLE public.%I SET SCHEMA passenger', t);
END LOOP;
EXCEPTION WHEN others THEN NULL;
END $$;
-- Add missing columns to Booking

View File

@@ -0,0 +1,14 @@
-- Add bookingId to Journey for per-booking segment release
ALTER TABLE "passenger"."Journey"
ADD COLUMN IF NOT EXISTS "bookingId" TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS "Journey_bookingId_key" ON "passenger"."Journey"("bookingId");
CREATE INDEX IF NOT EXISTS "Journey_bookingId_idx" ON "passenger"."Journey"("bookingId");
-- Ensure JourneySegment cascades on Journey delete
ALTER TABLE "passenger"."JourneySegment"
DROP CONSTRAINT IF EXISTS "JourneySegment_journeyId_fkey";
ALTER TABLE "passenger"."JourneySegment"
ADD CONSTRAINT "JourneySegment_journeyId_fkey"
FOREIGN KEY ("journeyId") REFERENCES "passenger"."Journey"("id") ON DELETE CASCADE;

View File

@@ -108,7 +108,7 @@ enum BookingStatus {
PENDING_PAYMENT
CONFIRMED
CANCELLED
COMPLETED
BOARDED
NO_SHOW
REFUNDED
@@ -319,8 +319,8 @@ model Station {
sequence Int @default(0)
isOperational Boolean @default(true)
timezone String @default("Africa/Addis_Ababa")
lat Decimal @db.Decimal(9, 6)
lng Decimal @db.Decimal(9, 6)
lat Decimal? @db.Decimal(9, 6)
lng Decimal? @db.Decimal(9, 6)
originSchedules TrainSchedule[] @relation("OriginTrips")
destinationSchedules TrainSchedule[] @relation("DestinationTrips")
stopTimes TripStopTime[]
@@ -542,6 +542,7 @@ model Booking {
modifications BookingModification[]
cancellation BookingCancellation?
baggage BaggageBooking[]
journey Journey?
@@index([passengerId, status])
@@index([bookingType])
@@ -933,10 +934,12 @@ model SavedRoute {
model Journey {
id String @id @default(uuid())
passengerId String
bookingId String? @unique
status String
totalMinor Int
currency String @default("ETB")
createdAt DateTime @default(now())
booking Booking? @relation(fields: [bookingId], references: [id])
journeySegments JourneySegment[]
@@schema("passenger")
}

View File

@@ -216,6 +216,7 @@ export class BookingsService {
...(matchedPassengers.length > 0
? [{ passengerId: { in: matchedPassengers.map(p => p.id) } }]
: []),
{ seats: { some: { passengerName: { contains: search, mode: 'insensitive' } } } },
];
}
@@ -268,6 +269,7 @@ export class BookingsService {
passenger: iam
? { fullName: iam.name?.en ?? iam.name?.am ?? null, email: iam.email, phone: iam.phone_number }
: null,
passengerNames: [...new Set(booking.seats.map((s: any) => s.passengerName))],
schedule: {
train: booking.schedule.train,
originStation: booking.schedule.originStation,
@@ -294,10 +296,23 @@ export class BookingsService {
return this.createOneWayBooking(dto);
}
private validateSeatIdsAgainstHold(holdId: string, holdSeatIds: string[], requestedSeatIds: string[]) {
for (const seatId of requestedSeatIds) {
if (!holdSeatIds.includes(seatId)) {
throw new BadRequestException(
`Seat ${seatId} is not part of hold ${holdId}. Use seat IDs returned from POST /seats/hold.`,
);
}
}
}
private async createOneWayBooking(dto: CreateBookingDto) {
const hold = await this.prisma.seatHold.findUnique({ where: { id: dto.holdId } });
if (!hold || hold.expiresAt < new Date()) throw new BadRequestException('Seat hold expired');
const requestedSeatIds = (dto.passengers as any[]).map(p => p.seatId);
this.validateSeatIdsAgainstHold(dto.holdId, hold.seatIds, requestedSeatIds);
const schedule = await this.prisma.trainSchedule.findUnique({
where: { id: dto.scheduleId },
include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } }
@@ -367,6 +382,11 @@ export class BookingsService {
if (!outboundHold || outboundHold.expiresAt < new Date()) throw new BadRequestException('Outbound seat hold expired');
if (!returnHold || returnHold.expiresAt < new Date()) throw new BadRequestException('Return seat hold expired');
const holdObSeatIds = (dto.passengers as any[]).map((p: any) => p.seatId ?? p.outboundSeatId).filter(Boolean);
const holdRetSeatIds = (dto.passengers as any[]).map((p: any) => p.returnSeatId).filter(Boolean);
if (holdObSeatIds.length) this.validateSeatIdsAgainstHold(dto.holdId, outboundHold.seatIds, holdObSeatIds);
if (holdRetSeatIds.length) this.validateSeatIdsAgainstHold(dto.returnHoldId!, returnHold.seatIds, holdRetSeatIds);
const [outboundSchedule, returnSchedule] = await Promise.all([
this.prisma.trainSchedule.findUnique({
where: { id: dto.scheduleId },
@@ -510,6 +530,11 @@ export class BookingsService {
if (!leg1Hold || leg1Hold.expiresAt < new Date()) throw new BadRequestException('Leg-1 seat hold expired');
if (!leg2Hold || leg2Hold.expiresAt < new Date()) throw new BadRequestException('Leg-2 seat hold expired');
const leg1SeatIds = (dto.passengers as any[]).map(p => p.seatId);
const leg2SeatIds = (dto.passengers as any[]).map(p => p.leg2SeatId ?? p.seatId);
this.validateSeatIdsAgainstHold(dto.holdId, leg1Hold.seatIds, leg1SeatIds);
this.validateSeatIdsAgainstHold(dto.leg2HoldId!, leg2Hold.seatIds, leg2SeatIds);
const [leg1Schedule, leg2Schedule] = await Promise.all([
this.prisma.trainSchedule.findUnique({
where: { id: dto.scheduleId },
@@ -656,6 +681,11 @@ export class BookingsService {
if (!retL1Hold || retL1Hold.expiresAt < now) throw new BadRequestException('Return leg-1 seat hold expired');
if (!retL2Hold || retL2Hold.expiresAt < now) throw new BadRequestException('Return leg-2 seat hold expired');
this.validateSeatIdsAgainstHold(dto.holdId, obL1Hold.seatIds, (dto.passengers as any[]).map(p => p.seatId));
this.validateSeatIdsAgainstHold(dto.leg2HoldId!, obL2Hold.seatIds, (dto.passengers as any[]).map(p => p.leg2SeatId ?? p.seatId));
this.validateSeatIdsAgainstHold(dto.returnHoldId!, retL1Hold.seatIds, (dto.passengers as any[]).map(p => p.returnSeatId));
this.validateSeatIdsAgainstHold(dto.returnLeg2HoldId!, retL2Hold.seatIds, (dto.passengers as any[]).map(p => p.returnLeg2SeatId ?? p.returnSeatId));
// Load all 4 schedules
const [obL1Sched, obL2Sched, retL1Sched, retL2Sched] = await Promise.all([
this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } }),
@@ -847,7 +877,21 @@ export class BookingsService {
nationality = nationality || (passenger.passportCountry === 'Djibouti' ? 'Djiboutian' : 'Other');
}
processedPassengers.push({ ...passenger, passengerName, dateOfBirth, category, verifaydaVerified, verifaydaData, nationality });
processedPassengers.push({
...passenger,
passengerName,
dateOfBirth,
category,
verifaydaVerified,
verifaydaData,
nationality,
// Normalise: PassengerInputDto uses seatId/returnSeatId; RoundTripPassengerDto uses
// outboundSeatId/returnSeatId. Accept either form so both DTOs work.
outboundSeatId: passenger.outboundSeatId ?? passenger.seatId,
outboundLeg2SeatId: passenger.outboundLeg2SeatId ?? passenger.leg2SeatId,
returnSeatId: passenger.returnSeatId,
returnLeg2SeatId: passenger.returnLeg2SeatId,
});
}
return processedPassengers;
}
@@ -1042,7 +1086,7 @@ export class BookingsService {
await this.prisma.bookingModification.create({
data: { bookingId: booking.id, modifiedBy: booking.passengerId, modificationType: 'SEAT_CHANGE', oldData: { scheduleId: booking.scheduleId, seatIds: oldSeats }, newData: { scheduleId: dto.newScheduleId, seatIds: dto.newSeatIds }, fareAdjustment: 0, reason: dto.reason },
});
await this.seatsService.releaseSeats(oldSeats);
await this.seatsService.releaseSeats(booking.id);
await this.seatsService.confirmSeats(dto.newSeatIds);
return { modified: true, bookingRef: dto.bookingRef };
}
@@ -1053,7 +1097,7 @@ export class BookingsService {
if (booking.status === 'CANCELLED') throw new BadRequestException('Booking already cancelled');
const refundAmount = booking.status === 'CONFIRMED' ? Math.floor(booking.totalMinor * 0.8) : 0;
await this.prisma.bookingCancellation.create({ data: { bookingId: booking.id, cancelledBy: booking.passengerId, reason, refundAmount, refundMethod: booking.paymentIntent?.method ?? 'ORIGINAL', refundStatus: 'PENDING' } });
await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId));
await this.seatsService.releaseSeats(booking.id);
await this.prisma.booking.update({ where: { bookingRef }, data: { status: 'CANCELLED' } });
this.eventEmitter.emit('booking.cancelled', { booking, refundAmount });
return { cancelled: true, refundAmount: refundAmount / 100, currency: 'ETB' };
@@ -1082,7 +1126,7 @@ export class BookingsService {
const booking = await this.prisma.booking.findUnique({ where: { id }, include: { seats: true } });
if (!booking) throw new NotFoundException('Booking not found');
await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId));
await this.seatsService.releaseSeats(booking.id);
await this.prisma.bookingSeat.deleteMany({ where: { bookingId: id } });
await this.prisma.booking.delete({ where: { id } });
@@ -1118,7 +1162,7 @@ export class BookingsService {
const cutoff = new Date(Date.now() - 20 * 60 * 1000);
const expired = await this.prisma.booking.findMany({ where: { status: 'PENDING_PAYMENT', createdAt: { lt: cutoff } }, include: { seats: true } });
for (const b of expired) {
await this.seatsService.releaseSeats(b.seats.map((s) => s.seatId));
await this.seatsService.releaseSeats(b.id);
await this.prisma.booking.update({ where: { id: b.id }, data: { status: 'CANCELLED' } });
}
}

View File

@@ -14,6 +14,21 @@ function generateRef(): string {
return 'EDR-' + Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
}
// Ethiopian mobile prefixes: Ethio Telecom (09xx) and Safaricom ET (07xx)
const ETH_MOBILE_PREFIXES = ['911','912','913','914','915','916','917','921','922','923','924','930','931','932','933','934','935','936','937','938','939','961','962','963','964'];
function generateEthiopianPhone(): string {
const prefix = ETH_MOBILE_PREFIXES[Math.floor(Math.random() * ETH_MOBILE_PREFIXES.length)];
const suffix = String(Math.floor(Math.random() * 1_000_000)).padStart(6, '0');
return `+251${prefix}${suffix}`;
}
function generateGuestEmail(uniqueId: string): string {
const domains = ['gmail.com', 'yahoo.com', 'ethionet.et', 'telecom.et'];
const domain = domains[Math.floor(Math.random() * domains.length)];
return `guest.edr.${uniqueId}@${domain}`;
}
function calculateAge(dateOfBirth: Date): number {
const today = new Date();
let age = today.getFullYear() - dateOfBirth.getFullYear();

View File

@@ -189,11 +189,11 @@ export class PassengersService {
async getStats(passengerId: string) {
const [totalTrips, totalSpendResult, loyalty] = await Promise.all([
this.prisma.booking.count({ where: { passengerId, status: 'COMPLETED' } }),
this.prisma.booking.aggregate({ where: { passengerId, status: 'COMPLETED' }, _sum: { totalMinor: true } }),
this.prisma.booking.count({ where: { passengerId, status: 'BOARDED' as any } }),
this.prisma.booking.aggregate({ where: { passengerId, status: 'BOARDED' as any }, _sum: { totalMinor: true } }),
this.prisma.loyaltyAccount.findUnique({ where: { passengerId } }),
]);
const totalSpend = (totalSpendResult._sum.totalMinor ?? 0) / 100;
const totalSpend = ((totalSpendResult._sum?.totalMinor ?? 0) as number) / 100;
return { totalTrips, totalSpend, loyaltyPoints: loyalty?.pointsBalance ?? 0, co2Saved: totalTrips * 6 };
}

View File

@@ -447,7 +447,7 @@ export class PaymentsService {
include: { seats: true },
});
if (booking) {
await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId));
await this.seatsService.releaseSeats(booking.id);
await this.prisma.booking.update({
where: { id: dto.bookingId },
data: { status: "CANCELLED" },
@@ -746,51 +746,125 @@ export class PaymentsService {
private async createJourneySegments(
booking: Prisma.BookingGetPayload<{ include: { seats: true } }>,
) {
const schedule = await this.prisma.trainSchedule.findUnique({
where: { id: booking.scheduleId },
include: {
stopTimes: { include: { station: true }, orderBy: { sequence: "asc" } },
},
});
if (!schedule) return;
const b = booking as any;
const stopTimes = schedule.stopTimes;
if (stopTimes.length < 2) return;
// Build per-leg definitions: { scheduleId, originStationId, destinationStationId, seatIds[] }
// BookingSeat.leg: 1=outbound/leg-1, 2=return/leg-2, 3=return leg-1 (transit), 4=return leg-2
type LegDef = { scheduleId: string; originStationId: string; destinationStationId: string; seatIds: string[] };
const legDefs: LegDef[] = [];
const originSequence = stopTimes.findIndex(
(st) => st.stationId === schedule.originStationId,
);
const destSequence = stopTimes.findIndex(
(st) => st.stationId === schedule.destinationStationId,
);
const seatsForLeg = (legNum: number) =>
booking.seats.filter((s: any) => s.leg === legNum).map((s: any) => s.seatId);
if (
originSequence < 0 ||
destSequence < 0 ||
originSequence >= destSequence
)
return;
if (booking.bookingType === 'ONE_WAY') {
legDefs.push({
scheduleId: booking.scheduleId,
originStationId: b.originStationId,
destinationStationId: b.destinationStationId,
seatIds: booking.seats.map((s: any) => s.seatId),
});
} else if (booking.bookingType === 'ROUND_TRIP') {
legDefs.push({
scheduleId: booking.scheduleId,
originStationId: b.originStationId,
destinationStationId: b.destinationStationId,
seatIds: seatsForLeg(1),
});
if (b.returnScheduleId && b.returnOriginStationId && b.returnDestinationStationId) {
legDefs.push({
scheduleId: b.returnScheduleId,
originStationId: b.returnOriginStationId,
destinationStationId: b.returnDestinationStationId,
seatIds: seatsForLeg(2),
});
}
} else if (booking.bookingType === 'TRANSIT') {
legDefs.push({
scheduleId: booking.scheduleId,
originStationId: b.originStationId,
destinationStationId: b.leg2OriginStationId, // transit station
seatIds: seatsForLeg(1),
});
if (b.leg2ScheduleId && b.leg2OriginStationId && b.leg2DestinationStationId) {
legDefs.push({
scheduleId: b.leg2ScheduleId,
originStationId: b.leg2OriginStationId,
destinationStationId: b.leg2DestinationStationId,
seatIds: seatsForLeg(2),
});
}
} else if (booking.bookingType === 'ROUND_TRIP_TRANSIT') {
legDefs.push({
scheduleId: booking.scheduleId,
originStationId: b.originStationId,
destinationStationId: b.leg2OriginStationId,
seatIds: seatsForLeg(1),
});
if (b.leg2ScheduleId && b.leg2OriginStationId && b.leg2DestinationStationId) {
legDefs.push({
scheduleId: b.leg2ScheduleId,
originStationId: b.leg2OriginStationId,
destinationStationId: b.leg2DestinationStationId,
seatIds: seatsForLeg(2),
});
}
if (b.returnScheduleId && b.returnOriginStationId && b.returnDestinationStationId) {
legDefs.push({
scheduleId: b.returnScheduleId,
originStationId: b.returnOriginStationId,
destinationStationId: b.returnLeg2OriginStationId ?? b.returnDestinationStationId,
seatIds: seatsForLeg(3),
});
}
if (b.returnLeg2ScheduleId && b.returnLeg2OriginStationId && b.returnLeg2DestStationId) {
legDefs.push({
scheduleId: b.returnLeg2ScheduleId,
originStationId: b.returnLeg2OriginStationId,
destinationStationId: b.returnLeg2DestStationId,
seatIds: seatsForLeg(4),
});
}
}
if (legDefs.length === 0) return;
const journey = await this.prisma.journey.create({
data: {
passengerId: booking.passengerId,
status: "CONFIRMED",
totalMinor: booking.totalMinor,
currency: booking.currency,
bookingId: booking.id,
status: 'CONFIRMED',
totalMinor: booking.totalMinor,
currency: booking.currency,
},
});
const journeySegments = [];
for (const bookingSeat of booking.seats) {
for (let i = originSequence; i < destSequence; i++) {
journeySegments.push({
journeyId: journey.id,
scheduleId: booking.scheduleId,
segmentOrder: i,
seatId: bookingSeat.seatId,
departureStationId: stopTimes[i].stationId,
arrivalStationId: stopTimes[i + 1].stationId,
});
const journeySegments: any[] = [];
let segmentOrder = 0;
for (const leg of legDefs) {
if (leg.seatIds.length === 0) continue;
const stopTimes = await this.prisma.tripStopTime.findMany({
where: { scheduleId: leg.scheduleId },
orderBy: { sequence: 'asc' },
select: { stationId: true, sequence: true },
});
const originIdx = stopTimes.findIndex(st => st.stationId === leg.originStationId);
const destIdx = stopTimes.findIndex(st => st.stationId === leg.destinationStationId);
if (originIdx < 0 || destIdx < 0 || originIdx >= destIdx) continue;
for (const seatId of leg.seatIds) {
for (let i = originIdx; i < destIdx; i++) {
journeySegments.push({
journeyId: journey.id,
scheduleId: leg.scheduleId,
segmentOrder: segmentOrder++,
seatId,
departureStationId: stopTimes[i].stationId,
arrivalStationId: stopTimes[i + 1].stationId,
});
}
}
}

View File

@@ -39,6 +39,23 @@ export class SearchService {
const outbound = [...direct, ...transit];
if (outbound.length === 0) {
const alternativesOutbound = await this.searchAlternatives(
dto.originStationId,
dto.destinationStationId,
dto.date,
dto.adultCount,
dto.childCount,
dto.nationality,
);
return {
journeyType: dto.journeyType === 'ROUND_TRIP' ? 'ROUND_TRIP' : 'ONE_WAY',
outbound: [],
alternativeOutbound: alternativesOutbound,
requestedDate: dto.date,
};
}
if (dto.journeyType === 'ROUND_TRIP') {
const [returnDirect, returnTransit] = await Promise.all([
this.searchSchedules(
@@ -68,12 +85,85 @@ export class SearchService {
new Date(s.departureAt ?? s.leg1?.departureAt).getTime() > latestOutboundArrival
);
if (inbound.length === 0) {
const alternativeInbound = await this.searchAlternatives(
dto.destinationStationId,
dto.originStationId,
dto.returnDate ?? dto.date,
dto.adultCount,
dto.childCount,
dto.nationality,
);
return { journeyType: 'ROUND_TRIP', outbound, inbound: [], alternativeInbound };
}
return { journeyType: 'ROUND_TRIP', outbound, inbound };
}
return { journeyType: 'ONE_WAY', outbound };
}
private async searchAlternatives(
originStationId: string,
destinationStationId: string,
dateStr: string,
adultCount: number,
childCount?: number,
nationality?: string,
) {
const [y, m, d] = dateStr.split('-').map(Number);
const requestedDate = new Date(y, m - 1, d, 0, 0, 0, 0);
const now = new Date();
const daysBefore = Math.min(7, Math.floor(requestedDate.getTime() / 86_400_000));
const daysAfter = 14 - daysBefore;
const windowStart = new Date(requestedDate);
windowStart.setDate(windowStart.getDate() - daysBefore);
if (windowStart < now) windowStart.setTime(now.getTime());
const windowEnd = new Date(requestedDate);
windowEnd.setDate(windowEnd.getDate() + daysAfter + 1); // exclusive upper bound
const totalPassengers = adultCount + (childCount ?? 0);
const requestedNextDay = new Date(y, m - 1, d + 1, 0, 0, 0, 0);
const schedules = await this.prisma.trainSchedule.findMany({
where: {
status: { in: ['SCHEDULED', 'BOARDING'] },
OR: [
{ departureAt: { gte: windowStart, lt: requestedDate } },
{ departureAt: { gte: requestedNextDay < now ? now : requestedNextDay, lt: windowEnd } },
],
stopTimes: { some: { stationId: originStationId } },
},
include: {
train: true,
originStation: true,
destinationStation: true,
stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
coachAssignments: {
include: { coach: { include: { seats: true, coachType: { include: { seatClasses: true } } } } },
},
},
orderBy: { departureAt: 'asc' },
});
const results: any[] = [];
for (const schedule of schedules) {
const result = await this.buildScheduleResult(
schedule,
originStationId,
destinationStationId,
totalPassengers,
nationality,
);
if (result) results.push(result);
}
return results;
}
private async searchSchedules(
originStationId: string,
destinationStationId: string,
@@ -85,12 +175,13 @@ export class SearchService {
const [y, m, d] = dateStr.split('-').map(Number);
const date = new Date(y, m - 1, d, 0, 0, 0, 0);
const nextDay = new Date(y, m - 1, d + 1, 0, 0, 0, 0);
const now = new Date();
const totalPassengers = adultCount + (childCount ?? 0);
const schedules = await this.prisma.trainSchedule.findMany({
where: {
status: { in: ['SCHEDULED', 'BOARDING'] },
departureAt: { gte: date, lt: nextDay },
departureAt: { gte: date < now ? now : date, lt: nextDay },
stopTimes: { some: { stationId: originStationId } },
},
include: {

View File

@@ -11,7 +11,7 @@ export class SeatsService {
private segmentsService: SegmentsService,
) {}
async getSeatMap(scheduleId: string, coachId?: string) {
async getSeatMap(scheduleId: string, coachId?: string, originStationId?: string, destinationStationId?: string) {
const assignments = await this.prisma.coachAssignment.findMany({
where: { scheduleId, ...(coachId ? { coachId } : {}) },
include: {
@@ -25,16 +25,14 @@ export class SeatsService {
orderBy: { positionNumber: 'asc' },
});
console.log(`[getSeatMap] scheduleId=${scheduleId}, coachId=${coachId}, found ${assignments.length} coach assignments`);
const allSeatIds = assignments.flatMap((a: any) => a.coach.seats.map((s: any) => s.id));
const effectiveStatuses = await this.resolveEffectiveStatuses(scheduleId, allSeatIds);
const effectiveStatuses = await this.resolveEffectiveStatuses(scheduleId, allSeatIds, originStationId, destinationStationId);
const response = {
return {
coaches: assignments.map((a) => {
const allSeats = a.coach.seats;
const seatClassNames = a.coach.coachType.seatClasses.map((sc: any) => sc.name);
return {
id: a.coach.id,
assignmentId: a.id,
@@ -68,43 +66,95 @@ export class SeatsService {
};
}),
};
console.log(`[getSeatMap] returning ${response.coaches.length} coaches with seats`);
return response;
}
async resolveEffectiveStatuses(
scheduleId: string,
seatIds: string[],
originStationId?: string,
destinationStationId?: string,
): Promise<Map<string, string>> {
const statusMap = new Map<string, string>();
if (seatIds.length === 0) return statusMap;
// Resolve the requested leg's sequence range once
let reqFrom: number | undefined;
let reqTo: number | undefined;
let allStopTimes: { stationId: string; sequence: number }[] | null = null;
const getStopTimes = async () => {
if (!allStopTimes) {
allStopTimes = await this.prisma.tripStopTime.findMany({
where: { scheduleId },
select: { stationId: true, sequence: true },
});
}
return allStopTimes;
};
if (originStationId && destinationStationId) {
const stops = await getStopTimes();
const seqOf = (id: string) => stops.find(s => s.stationId === id)?.sequence;
reqFrom = seqOf(originStationId);
reqTo = seqOf(destinationStationId);
}
// ── Active holds ──────────────────────────────────────────────────────────
const activeHolds = await this.prisma.seatHold.findMany({
where: {
scheduleId,
expiresAt: { gt: new Date() },
seatIds: { hasSome: seatIds },
},
select: { seatIds: true },
where: { scheduleId, expiresAt: { gt: new Date() }, seatIds: { hasSome: seatIds } },
select: { seatIds: true, createdBy: true },
});
for (const hold of activeHolds) {
let holdFrom: number | undefined;
let holdTo: number | undefined;
try {
if (hold.createdBy?.trimStart().startsWith('{')) {
const meta = JSON.parse(hold.createdBy);
const stops = await getStopTimes();
const seqOf = (id: string) => stops.find(s => s.stationId === id)?.sequence;
holdFrom = seqOf(meta.originStationId);
holdTo = seqOf(meta.destinationStationId);
}
} catch { /* ignore */ }
for (const seatId of hold.seatIds) {
if (seatIds.includes(seatId)) statusMap.set(seatId, 'HELD');
if (!seatIds.includes(seatId)) continue;
if (reqFrom !== undefined && reqTo !== undefined && holdFrom !== undefined && holdTo !== undefined) {
if (holdFrom < reqTo && reqFrom < holdTo) statusMap.set(seatId, 'HELD');
} else {
statusMap.set(seatId, 'HELD');
}
}
}
// ── Confirmed bookings via JourneySegment ─────────────────────────────────
const bookedSegments = await this.prisma.journeySegment.findMany({
where: {
scheduleId,
seatId: { in: seatIds },
journey: { status: { in: ['CONFIRMED', 'PENDING_PAYMENT'] } },
},
select: { seatId: true },
select: { seatId: true, departureStationId: true, arrivalStationId: true },
});
for (const seg of bookedSegments) {
if (seg.seatId) statusMap.set(seg.seatId, 'BOOKED');
if (reqFrom !== undefined && reqTo !== undefined) {
const stops = await getStopTimes();
const seqOf = (id: string) => stops.find(s => s.stationId === id)?.sequence;
for (const seg of bookedSegments) {
if (!seg.seatId) continue;
const segFrom = seqOf(seg.departureStationId);
const segTo = seqOf(seg.arrivalStationId);
if (segFrom !== undefined && segTo !== undefined) {
if (segFrom < reqTo && reqFrom < segTo) statusMap.set(seg.seatId, 'BOOKED');
} else {
statusMap.set(seg.seatId, 'BOOKED');
}
}
} else {
for (const seg of bookedSegments) {
if (seg.seatId) statusMap.set(seg.seatId, 'BOOKED');
}
}
return statusMap;
@@ -154,6 +204,7 @@ export class SeatsService {
if (reqFrom >= reqTo)
throw new BadRequestException('Origin must come before destination');
// ── Check existing holds for overlap ────────────────────────────────────
const activeHolds = await tx.seatHold.findMany({
where: { scheduleId: dto.scheduleId, expiresAt: { gt: new Date() } },
select: { seatIds: true, createdBy: true },
@@ -197,6 +248,29 @@ export class SeatsService {
}
}
// ── Check confirmed JourneySegments for overlap ──────────────────────────
const bookedSegments = await tx.journeySegment.findMany({
where: {
scheduleId: dto.scheduleId,
seatId: { in: seatIds },
journey: { status: { in: ['CONFIRMED', 'PENDING_PAYMENT'] } },
},
select: { seatId: true, departureStationId: true, arrivalStationId: true },
});
for (const seg of bookedSegments) {
if (!seg.seatId) continue;
const segFrom = seqOf(seg.departureStationId);
const segTo = seqOf(seg.arrivalStationId);
if (segFrom !== undefined && segTo !== undefined) {
if (segFrom < reqTo && reqFrom < segTo) {
throw new ConflictException(
`Seat ${seatLabelById[seg.seatId]} is already booked for this leg`,
);
}
}
}
const holdMeta = {
originStationId: dto.originStationId,
destinationStationId: dto.destinationStationId,
@@ -347,16 +421,12 @@ export class SeatsService {
return { released: true, holdId };
}
async confirmSeats(seatIds: string[]) {
// No-op
}
// Physical seat.status stays AVAILABLE — segment rows are the source of truth for occupancy.
async confirmSeats(_seatIds: string[]) {}
async releaseSeats(seatIds: string[]) {
if (seatIds.length > 0) {
await this.prisma.journeySegment.deleteMany({
where: { seatId: { in: seatIds } },
});
}
// Delete the Journey (and its JourneySegments) scoped to this booking.
async releaseSeats(bookingId: string) {
await this.prisma.journey.deleteMany({ where: { bookingId } });
}
async autoAssignSeats(scheduleId: string, count: number, seatClassName: string): Promise<string[]> {
@@ -424,7 +494,7 @@ export class SeatsService {
invalid++;
continue;
}
const [coachId, coachLabel, row, col, seatNumber, kind, status, premiumFeeMinor] = parts;
const [coachId, , row, col, seatNumber] = parts;
if (!coachId || !row || !col || !seatNumber) {
errors.push(`Line ${i + 2}: Missing required fields`);
invalid++;
@@ -448,7 +518,7 @@ export class SeatsService {
for (let i = 0; i < lines.length; i++) {
try {
const parts = lines[i].split(',');
const [coachId, coachLabel, row, col, seatNumber, kind, status, premiumFeeMinor] = parts;
const [coachId, , row, col, seatNumber, kind, status, premiumFeeMinor] = parts;
await this.prisma.seat.upsert({
where: { coachId_row_col: { coachId, row: parseInt(row), col } },
@@ -481,18 +551,8 @@ export class SeatsService {
const seat = await this.prisma.seat.findUnique({ where: { id: seatId } });
if (!seat) throw new NotFoundException('Seat not found');
await this.prisma.seat.update({
where: { id: seatId },
data: { status: 'BLOCKED' },
});
await this.prisma.seatBlock.create({
data: {
seatId,
reason,
blockedBy: 'system',
},
});
await this.prisma.seat.update({ where: { id: seatId }, data: { status: 'BLOCKED' } });
await this.prisma.seatBlock.create({ data: { seatId, reason, blockedBy: 'system' } });
return { blocked: true, seatId, reason };
}
@@ -501,14 +561,8 @@ export class SeatsService {
const seat = await this.prisma.seat.findUnique({ where: { id: seatId } });
if (!seat) throw new NotFoundException('Seat not found');
await this.prisma.seat.update({
where: { id: seatId },
data: { status: 'AVAILABLE' },
});
await this.prisma.seatBlock.deleteMany({
where: { seatId },
});
await this.prisma.seat.update({ where: { id: seatId }, data: { status: 'AVAILABLE' } });
await this.prisma.seatBlock.deleteMany({ where: { seatId } });
return { unblocked: true, seatId };
}
@@ -518,11 +572,9 @@ export class SeatsService {
if (!seat) throw new NotFoundException('Seat not found');
if (!seat.seatNumber) throw new BadRequestException('Seat already removed');
// Mark removed seat with negative seatNumber (e.g., '1' → '-1') to show empty space
const negatedNumber = `-${seat.seatNumber}`;
await this.prisma.seat.update({
where: { id: seatId },
data: { seatNumber: negatedNumber },
data: { seatNumber: `-${seat.seatNumber}` },
});
return { removed: true, seatId, originalSeatNumber: seat.seatNumber };
@@ -535,26 +587,15 @@ export class SeatsService {
throw new BadRequestException('Seat is not removed');
}
// Restore original seatNumber by removing the negative sign
const originalNumber = seat.seatNumber.slice(1);
await this.prisma.seat.update({
where: { id: seatId },
data: { seatNumber: originalNumber },
});
await this.prisma.seat.update({ where: { id: seatId }, data: { seatNumber: originalNumber } });
return { restored: true, seatId, seatNumber: originalNumber };
}
@Cron(CronExpression.EVERY_MINUTE)
async expireHolds() {
const now = new Date();
const expired = await this.prisma.seatHold.findMany({ where: { expiresAt: { lt: now } } });
if (expired.length === 0) return;
const expiredIds = expired.map(h => h.id);
for (const hold of expired) {
await this.releaseSeats(hold.seatIds);
}
await this.prisma.seatHold.deleteMany({ where: { id: { in: expiredIds } } });
// Holds are temporary and don't create Journey rows — just delete expired ones.
await this.prisma.seatHold.deleteMany({ where: { expiresAt: { lt: new Date() } } });
}
}

View File

@@ -7,8 +7,8 @@ export class CreateStationDto {
@ApiProperty({ example: 'Addis Ababa' }) @IsString() city: string;
@ApiPropertyOptional() @IsOptional() @IsString() timezone?: string;
@ApiPropertyOptional() @IsOptional() @IsString() countryCode?: string;
@ApiProperty({ example: 9.0054 }) @IsNumber() lat: number;
@ApiProperty({ example: 38.7636 }) @IsNumber() lng: number;
@ApiPropertyOptional({ example: 9.0054 }) @IsOptional() @IsNumber() lat?: number;
@ApiPropertyOptional({ example: 38.7636 }) @IsOptional() @IsNumber() lng?: number;
@ApiPropertyOptional({ example: 1 }) @IsOptional() @IsInt() sequence?: number;
@ApiPropertyOptional({ example: true }) @IsOptional() @IsBoolean() isOperational?: boolean;
}

View File

@@ -50,7 +50,10 @@ export class StationsService {
}
async create(dto: CreateStationDto) {
const station = await this.prisma.station.create({ data: dto });
const { lat, lng, ...rest } = dto;
const station = await this.prisma.station.create({
data: { ...rest, ...(lat !== undefined && { lat }), ...(lng !== undefined && { lng }) } as any,
});
await this.auditService.log({
userId: this.request?.user?.id,

View File

@@ -50,7 +50,8 @@ export class TicketsService {
booking: {
include: {
schedule: { include: { originStation: true, destinationStation: true, train: true } },
seats: { include: { seat: { include: { coach: true } } } },
returnSchedule: { select: { departureAt: true, arrivalAt: true, originStation: true, destinationStation: true } },
seats: { include: { seat: { include: { coach: { include: { coachType: true } } } } } },
passenger: { select: { id: true, iamUserId: true } },
},
},
@@ -90,6 +91,16 @@ export class TicketsService {
schedule: t.booking.schedule,
seat: t.booking.seats[0]?.seat,
status: t.booking.status,
bookingType: t.booking.bookingType,
returnLegStatus: (t.booking as any).returnLegStatus ?? null,
outboundBoardedAt: (t.booking as any).outboundBoardedAt ?? null,
returnBoardedAt: (t.booking as any).returnBoardedAt ?? null,
totalMinor: t.booking.totalMinor,
displayCurrency: t.booking.displayCurrency,
displayTotalMinor: t.booking.displayTotalMinor,
contactEmail: t.booking.contactEmail,
contactPhone: t.booking.contactPhone,
returnSchedule: (t.booking as any).returnSchedule ?? null,
validatedAt: t.validatedAt,
createdAt: t.issuedAt,
};
@@ -130,7 +141,7 @@ export class TicketsService {
legs: legSummary,
});
const qrPayload = await QRCode.toDataURL(qrData);
const barcodePayload = `EDR${booking.bookingRef}${booking.id.substring(0, 8).toUpperCase()}`;
const barcodePayload = `${booking.bookingRef}${booking.id.substring(0, 8).toUpperCase()}`;
const ticket = await this.prisma.ticket.upsert({
where: { bookingId },