Nationality, guest booking, waafi adapter, overall booking flow updates

This commit is contained in:
Stephanos A
2026-05-24 16:20:55 +03:00
parent 2dad9137dd
commit f803748278
37 changed files with 2284 additions and 486 deletions

View File

@@ -0,0 +1,5 @@
-- AlterEnum
ALTER TYPE "PaymentMethodType" ADD VALUE 'WAAFI';
-- AlterTable
ALTER TABLE "FareRule" ADD COLUMN "nationality" TEXT;

View File

@@ -0,0 +1,28 @@
-- AlterTable
ALTER TABLE "Booking" ADD COLUMN "contactEmail" TEXT,
ADD COLUMN "contactPhone" TEXT;
-- CreateTable
CREATE TABLE "SavedPassengerProfile" (
"id" TEXT NOT NULL,
"userId" TEXT,
"deviceId" TEXT,
"passengerName" TEXT NOT NULL,
"dateOfBirth" TIMESTAMP(3) NOT NULL,
"idDocumentType" "IdDocumentType" NOT NULL,
"passportNumber" TEXT,
"passportCountry" TEXT,
"nationality" TEXT,
"phone" TEXT,
"email" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SavedPassengerProfile_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "SavedPassengerProfile_userId_idx" ON "SavedPassengerProfile"("userId");
-- CreateIndex
CREATE INDEX "SavedPassengerProfile_deviceId_idx" ON "SavedPassengerProfile"("deviceId");

View File

@@ -0,0 +1,30 @@
-- Add contact fields to Booking table
ALTER TABLE "passenger"."Booking"
ADD COLUMN "contactEmail" TEXT,
ADD COLUMN "contactPhone" TEXT;
-- Create SavedPassengerProfile table
CREATE TABLE "passenger"."SavedPassengerProfile" (
"id" TEXT NOT NULL,
"userId" TEXT,
"deviceId" TEXT,
"passengerName" TEXT NOT NULL,
"dateOfBirth" TIMESTAMP(3) NOT NULL,
"idDocumentType" "passenger"."IdDocumentType" NOT NULL,
"passportNumber" TEXT,
"passportCountry" TEXT,
"nationality" TEXT,
"phone" TEXT,
"email" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SavedPassengerProfile_pkey" PRIMARY KEY ("id")
);
-- Create indexes
CREATE INDEX "SavedPassengerProfile_userId_idx" ON "passenger"."SavedPassengerProfile"("userId");
CREATE INDEX "SavedPassengerProfile_deviceId_idx" ON "passenger"."SavedPassengerProfile"("deviceId");
-- Add comment
COMMENT ON TABLE "passenger"."SavedPassengerProfile" IS 'Stores passenger details for quick rebooking (by userId or deviceId)';

View File

@@ -103,6 +103,7 @@ enum PaymentMethodType {
EBIRR
CARD
WALLET
WAAFI
@@schema("passenger")
}
@@ -443,6 +444,7 @@ model FareRule {
id String @id @default(uuid())
tripId String?
route String?
nationality String? // Ethiopian, Djiboutian, Other
seatClassId String
baseFareMinor Int
seatClass SeatClass @relation(fields: [seatClassId], references: [id])
@@ -468,6 +470,8 @@ model Booking {
displayCurrency Currency?
displayTotalMinor Int?
bookingType String @default("ONE_WAY")
contactEmail String?
contactPhone String?
userAgent String?
source String @default("WEB")
promoCode String?
@@ -1221,3 +1225,23 @@ model VerifaydaVerification {
@@schema("passenger")
}
model SavedPassengerProfile {
id String @id @default(uuid())
userId String?
deviceId String?
passengerName String
dateOfBirth DateTime
idDocumentType IdDocumentType
passportNumber String?
passportCountry String?
nationality String?
phone String?
email String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([deviceId])
@@schema("passenger")
}

View File

@@ -3,182 +3,618 @@ import * as bcrypt from 'bcrypt';
const prisma = new PrismaClient();
async function main() {
console.log('🌱 Starting comprehensive seed...');
// ============================================================================
// SECTION 1: STATIONS (18 STATIONS)
// ============================================================================
async function seedStations() {
console.log('📍 Seeding 18 stations...');
const stations = [
{ code: 'SBT', name: 'Sebeta', city: 'Sebeta', countryCode: 'ET', lat: 8.9167, lng: 38.6167 },
{ code: 'LBU', name: 'Labu', city: 'Labu', countryCode: 'ET', lat: 8.8500, lng: 38.7000 },
{ code: 'IND', name: 'Indode', city: 'Indode', countryCode: 'ET', lat: 8.7800, lng: 38.8200 },
{ code: 'BSH', name: 'Bishoftu', city: 'Bishoftu', countryCode: 'ET', lat: 8.7500, lng: 38.9833 },
{ code: 'MJO', name: 'Mojo', city: 'Mojo', countryCode: 'ET', lat: 8.6000, lng: 39.1200 },
{ code: 'ADM', name: 'Adama', city: 'Adama', countryCode: 'ET', lat: 8.5400, lng: 39.2675 },
{ code: 'FTO', name: 'Feto', city: 'Feto', countryCode: 'ET', lat: 8.4500, lng: 39.4000 },
{ code: 'MTH', name: 'Metahara', city: 'Metahara', countryCode: 'ET', lat: 8.9000, lng: 39.9167 },
{ code: 'MSO', name: 'Mieso', city: 'Mieso', countryCode: 'ET', lat: 9.2400, lng: 40.7500 },
{ code: 'BKE', name: 'Bike', city: 'Bike', countryCode: 'ET', lat: 9.4200, lng: 41.2000 },
{ code: 'DDW', name: 'Diredawa', city: 'Diredawa', countryCode: 'ET', lat: 9.5931, lng: 41.8661 },
{ code: 'ARW', name: 'Arawa', city: 'Arawa', countryCode: 'ET', lat: 10.2000, lng: 42.1500 },
{ code: 'ADG', name: 'Adigala', city: 'Adigala', countryCode: 'ET', lat: 10.8500, lng: 42.4000 },
{ code: 'AYS', name: 'Aysha', city: 'Aysha', countryCode: 'ET', lat: 11.5500, lng: 42.7167 },
{ code: 'DWL', name: 'Dawanle', city: 'Dawanle', countryCode: 'DJ', lat: 11.4000, lng: 42.9500 },
{ code: 'ALI', name: 'Alisabieh', city: 'Alisabieh', countryCode: 'DJ', lat: 11.1667, lng: 42.7167 },
{ code: 'HOL', name: 'Holhol', city: 'Holhol', countryCode: 'DJ', lat: 11.3500, lng: 43.0500 },
{ code: 'NGD', name: 'Nagad', city: 'Nagad', countryCode: 'DJ', timezone: 'Africa/Djibouti', lat: 11.5720, lng: 43.1456 },
];
// Stations
const addis = await prisma.station.upsert({ where: { code: 'ADD' }, update: {}, create: { code: 'ADD', name: 'Addis Ababa Central', city: 'Addis Ababa', countryCode: 'ET', lat: 9.0054, lng: 38.7636 } });
const sebeta = await prisma.station.upsert({ where: { code: 'SBT' }, update: {}, create: { code: 'SBT', name: 'Sebeta', city: 'Sebeta', countryCode: 'ET', lat: 8.9167, lng: 38.6167 } });
const adama = await prisma.station.upsert({ where: { code: 'ADM' }, update: {}, create: { code: 'ADM', name: 'Adama', city: 'Adama', countryCode: 'ET', lat: 8.5400, lng: 39.2675 } });
const awash = await prisma.station.upsert({ where: { code: 'AWS' }, update: {}, create: { code: 'AWS', name: 'Awash', city: 'Awash', countryCode: 'ET', lat: 8.9833, lng: 40.1667 } });
const direDawa = await prisma.station.upsert({ where: { code: 'DDW' }, update: {}, create: { code: 'DDW', name: 'Dire Dawa', city: 'Dire Dawa', countryCode: 'ET', lat: 9.5931, lng: 41.8661 } });
const aysha = await prisma.station.upsert({ where: { code: 'AYS' }, update: {}, create: { code: 'AYS', name: 'Aysha', city: 'Aysha', countryCode: 'ET', lat: 11.5500, lng: 42.7167 } });
const djibouti = await prisma.station.upsert({ where: { code: 'DJI' }, update: {}, create: { code: 'DJI', name: 'Djibouti', city: 'Djibouti', countryCode: 'DJ', timezone: 'Africa/Djibouti', lat: 11.5720, lng: 43.1456 } });
const created = [];
for (const station of stations) {
const s = await prisma.station.upsert({
where: { code: station.code },
update: {},
create: station,
});
created.push(s);
}
// Seat Classes
const scEconomyRegular = await prisma.seatClass.upsert({ where: { name: 'Economy Regular' }, update: {}, create: { name: 'Economy Regular', description: 'Standard economy seating', basePrice: 45000, isActive: true } });
const scEconomyBed = await prisma.seatClass.upsert({ where: { name: 'Economy Bed' }, update: {}, create: { name: 'Economy Bed', description: 'Economy bed lower berth', basePrice: 65000, isActive: true } });
const scVipBed = await prisma.seatClass.upsert({ where: { name: 'VIP Bed' }, update: {}, create: { name: 'VIP Bed', description: 'First class VIP bed', basePrice: 95000, isActive: true } });
console.log(` ✅ Created ${created.length} stations`);
return created;
}
// Trains (logical services)
const train301 = await prisma.train.upsert({ where: { number: '301' }, update: {}, create: { number: '301', name: 'Express 301', description: 'Addis-Djibouti Express' } });
const train302 = await prisma.train.upsert({ where: { number: '302' }, update: {}, create: { number: '302', name: 'Express 302', description: 'Djibouti-Addis Express' } });
// ============================================================================
// SECTION 2: SEAT CLASSES
// ============================================================================
async function seedSeatClasses() {
console.log('💺 Seeding seat classes...');
const classes = [
{ name: 'Economy Regular', description: 'Standard economy seating', basePrice: 25000 },
{ name: 'Economy Bed', description: 'Economy bed lower berth', basePrice: 35000 },
{ name: 'VIP Bed', description: 'First class VIP bed', basePrice: 55000 },
];
// Physical Coaches (reusable)
const coachA1 = await prisma.coach.upsert({ where: { coachNumber: 'C-A1' }, update: {}, create: { coachNumber: 'C-A1', label: 'A', seatClassId: scEconomyRegular.id, mode: 'seat', totalUnits: 60 } });
const coachB1 = await prisma.coach.upsert({ where: { coachNumber: 'C-B1' }, update: {}, create: { coachNumber: 'C-B1', label: 'B', seatClassId: scEconomyBed.id, mode: 'bed', totalUnits: 40 } });
const coachC1 = await prisma.coach.upsert({ where: { coachNumber: 'C-C1' }, update: {}, create: { coachNumber: 'C-C1', label: 'C', seatClassId: scVipBed.id, mode: 'bed', totalUnits: 20 } });
const coachA2 = await prisma.coach.upsert({ where: { coachNumber: 'C-A2' }, update: {}, create: { coachNumber: 'C-A2', label: 'A', seatClassId: scEconomyRegular.id, mode: 'seat', totalUnits: 60 } });
const coachB2 = await prisma.coach.upsert({ where: { coachNumber: 'C-B2' }, update: {}, create: { coachNumber: 'C-B2', label: 'B', seatClassId: scEconomyBed.id, mode: 'bed', totalUnits: 40 } });
const coachC2 = await prisma.coach.upsert({ where: { coachNumber: 'C-C2' }, update: {}, create: { coachNumber: 'C-C2', label: 'C', seatClassId: scVipBed.id, mode: 'bed', totalUnits: 20 } });
const created = [];
for (const cls of classes) {
const c = await prisma.seatClass.upsert({
where: { name: cls.name },
update: {},
create: { ...cls, isActive: true },
});
created.push(c);
}
// Create seats for each physical coach
for (const coach of [coachA1, coachB1, coachC1, coachA2, coachB2, coachC2]) {
console.log(` ✅ Created ${created.length} seat classes`);
return created;
}
// ============================================================================
// SECTION 3: TRAINS
// ============================================================================
async function seedTrains() {
console.log('🚂 Seeding trains...');
const trains = [
{ number: '301', name: 'Express 301', description: 'Sebeta-Nagad Express' },
{ number: '302', name: 'Express 302', description: 'Nagad-Sebeta Express' },
{ number: '303', name: 'Local 303', description: 'Regional Service' },
];
const created = [];
for (const train of trains) {
const t = await prisma.train.upsert({
where: { number: train.number },
update: {},
create: train,
});
created.push(t);
}
console.log(` ✅ Created ${created.length} trains`);
return created;
}
// ============================================================================
// SECTION 4: COACHES & SEATS
// ============================================================================
async function seedCoachesAndSeats(seatClasses: any[]) {
console.log('🚃 Seeding coaches and seats...');
const [scEconomy, scEconomyBed, scVip] = seatClasses;
const coachConfigs = [
{ coachNumber: 'C-A1', label: 'A', seatClassId: scEconomy.id, mode: 'seat', totalUnits: 60 },
{ coachNumber: 'C-B1', label: 'B', seatClassId: scEconomyBed.id, mode: 'bed', totalUnits: 40 },
{ coachNumber: 'C-C1', label: 'C', seatClassId: scVip.id, mode: 'bed', totalUnits: 20 },
{ coachNumber: 'C-A2', label: 'A', seatClassId: scEconomy.id, mode: 'seat', totalUnits: 60 },
{ coachNumber: 'C-B2', label: 'B', seatClassId: scEconomyBed.id, mode: 'bed', totalUnits: 40 },
{ coachNumber: 'C-C2', label: 'C', seatClassId: scVip.id, mode: 'bed', totalUnits: 20 },
];
const coaches = [];
for (const config of coachConfigs) {
const coach = await prisma.coach.upsert({
where: { coachNumber: config.coachNumber },
update: {},
create: config,
});
coaches.push(coach);
// Create seats for this coach
const existingSeats = await prisma.seat.count({ where: { coachId: coach.id } });
if (existingSeats === 0) {
const seats = [];
const rows = Math.ceil(coach.totalUnits / 4);
const rows = Math.ceil(config.totalUnits / 4);
for (let row = 1; row <= rows; row++) {
for (const col of ['A', 'B', 'C', 'D']) {
if (seats.length >= coach.totalUnits) break;
seats.push({ coachId: coach.id, row, col, label: `${row}${col}`, seatNumber: `${coach.label}${row}${col}`, kind: (row === 1 && col === 'A' ? 'ACCESSIBLE' : 'STANDARD') as SeatKind });
if (seats.length >= config.totalUnits) break;
seats.push({
coachId: coach.id,
row,
col,
label: `${row}${col}`,
seatNumber: `${config.label}${row}${col}`,
kind: (row === 1 && col === 'A' ? 'ACCESSIBLE' : 'STANDARD') as SeatKind,
});
}
}
await prisma.seat.createMany({ data: seats });
}
}
// Train Schedules — delete dependents first to avoid FK violations
console.log(` ✅ Created ${coaches.length} coaches with seats`);
return coaches;
}
// ============================================================================
// SECTION 5: SCHEDULES (15+ SEGMENTS)
// ============================================================================
async function seedSchedules(trains: any[], stations: any[]) {
console.log('📅 Seeding schedules with 15+ segments...');
const [train301, train302, train303] = trains;
const [sebeta, labu, indode, bishoftu, mojo, adama, feto, metahara, mieso, bike, diredawa, arawa, adigala, aysha, dawanle, alisabieh, holhol, nagad] = stations;
// Clean up existing schedules
const existingScheduleIds = (await prisma.trainSchedule.findMany({
where: { trainId: { in: [train301.id, train302.id] } },
where: { trainId: { in: [train301.id, train302.id, train303.id] } },
select: { id: true },
})).map((s) => s.id);
if (existingScheduleIds.length > 0) {
await prisma.fareRule.deleteMany({ where: { tripId: { in: existingScheduleIds } } });
await prisma.tripStopTime.deleteMany({ where: { scheduleId: { in: existingScheduleIds } } });
await prisma.coachAssignment.deleteMany({ where: { scheduleId: { in: existingScheduleIds } } });
await prisma.trainSchedule.deleteMany({ where: { id: { in: existingScheduleIds } } });
}
const schedule1 = await prisma.trainSchedule.create({
data: { trainId: train301.id, originStationId: addis.id, destinationStationId: djibouti.id, departureAt: new Date('2026-06-15T08:00:00Z'), arrivalAt: new Date('2026-06-15T20:00:00Z'), durationMinutes: 720, stopsCount: 6 },
});
const schedule2 = await prisma.trainSchedule.create({
data: { trainId: train302.id, originStationId: djibouti.id, destinationStationId: addis.id, departureAt: new Date('2026-06-16T09:00:00Z'), arrivalAt: new Date('2026-06-16T21:30:00Z'), durationMinutes: 750, stopsCount: 5 },
});
const schedule3 = await prisma.trainSchedule.create({
data: { trainId: train301.id, originStationId: addis.id, destinationStationId: djibouti.id, departureAt: new Date('2026-06-17T07:30:00Z'), arrivalAt: new Date('2026-06-17T19:45:00Z'), durationMinutes: 735, stopsCount: 5 },
});
const schedule4 = await prisma.trainSchedule.create({
data: { trainId: train302.id, originStationId: djibouti.id, destinationStationId: addis.id, departureAt: new Date('2026-06-18T08:30:00Z'), arrivalAt: new Date('2026-06-18T21:00:00Z'), durationMinutes: 750, stopsCount: 5 },
});
// Assign coaches to schedules
await prisma.coachAssignment.createMany({
data: [
{ scheduleId: schedule1.id, coachId: coachA1.id, positionNumber: 1 },
{ scheduleId: schedule1.id, coachId: coachB1.id, positionNumber: 2 },
{ scheduleId: schedule1.id, coachId: coachC1.id, positionNumber: 3 },
{ scheduleId: schedule2.id, coachId: coachA2.id, positionNumber: 1 },
{ scheduleId: schedule2.id, coachId: coachB2.id, positionNumber: 2 },
{ scheduleId: schedule2.id, coachId: coachC2.id, positionNumber: 3 },
{ scheduleId: schedule3.id, coachId: coachA1.id, positionNumber: 1 },
{ scheduleId: schedule3.id, coachId: coachB1.id, positionNumber: 2 },
{ scheduleId: schedule3.id, coachId: coachC1.id, positionNumber: 3 },
{ scheduleId: schedule4.id, coachId: coachA2.id, positionNumber: 1 },
{ scheduleId: schedule4.id, coachId: coachB2.id, positionNumber: 2 },
{ scheduleId: schedule4.id, coachId: coachC2.id, positionNumber: 3 },
],
skipDuplicates: true,
});
const schedules = [
// Full route: Sebeta to Nagad (18 stations)
{
trainId: train301.id,
originStationId: sebeta.id,
destinationStationId: nagad.id,
departureAt: new Date('2026-06-15T06:00:00Z'),
arrivalAt: new Date('2026-06-15T22:00:00Z'),
durationMinutes: 960,
stopsCount: 18,
},
// Return route: Nagad to Sebeta
{
trainId: train302.id,
originStationId: nagad.id,
destinationStationId: sebeta.id,
departureAt: new Date('2026-06-16T07:00:00Z'),
arrivalAt: new Date('2026-06-16T23:30:00Z'),
durationMinutes: 990,
stopsCount: 18,
},
// Regional service: Sebeta to Diredawa
{
trainId: train303.id,
originStationId: sebeta.id,
destinationStationId: diredawa.id,
departureAt: new Date('2026-06-17T08:00:00Z'),
arrivalAt: new Date('2026-06-17T18:00:00Z'),
durationMinutes: 600,
stopsCount: 11,
},
// Additional schedules for next day
{
trainId: train301.id,
originStationId: sebeta.id,
destinationStationId: nagad.id,
departureAt: new Date('2026-06-18T06:30:00Z'),
arrivalAt: new Date('2026-06-18T22:45:00Z'),
durationMinutes: 975,
stopsCount: 18,
},
{
trainId: train302.id,
originStationId: nagad.id,
destinationStationId: sebeta.id,
departureAt: new Date('2026-06-19T07:15:00Z'),
arrivalAt: new Date('2026-06-19T23:45:00Z'),
durationMinutes: 990,
stopsCount: 18,
},
];
// Stop Times
await prisma.tripStopTime.createMany({
data: [
{ scheduleId: schedule1.id, stationId: addis.id, sequence: 1, plannedDepartureAt: new Date('2026-06-15T08:00:00Z'), status: 'UPCOMING' },
{ scheduleId: schedule1.id, stationId: adama.id, sequence: 2, plannedArrivalAt: new Date('2026-06-15T09:30:00Z'), plannedDepartureAt: new Date('2026-06-15T09:45:00Z'), status: 'UPCOMING' },
{ scheduleId: schedule1.id, stationId: awash.id, sequence: 3, plannedArrivalAt: new Date('2026-06-15T11:30:00Z'), plannedDepartureAt: new Date('2026-06-15T11:45:00Z'), status: 'UPCOMING' },
{ scheduleId: schedule1.id, stationId: direDawa.id, sequence: 4, plannedArrivalAt: new Date('2026-06-15T15:00:00Z'), plannedDepartureAt: new Date('2026-06-15T15:20:00Z'), status: 'UPCOMING' },
{ scheduleId: schedule1.id, stationId: aysha.id, sequence: 5, plannedArrivalAt: new Date('2026-06-15T18:00:00Z'), plannedDepartureAt: new Date('2026-06-15T18:10:00Z'), status: 'UPCOMING' },
{ scheduleId: schedule1.id, stationId: djibouti.id, sequence: 6, plannedArrivalAt: new Date('2026-06-15T20:00:00Z'), status: 'UPCOMING' },
],
});
// Fare Rules
for (const schedule of [schedule1, schedule2, schedule3, schedule4]) {
await prisma.fareRule.createMany({
data: [
{ tripId: schedule.id, seatClassId: scEconomyRegular.id, baseFareMinor: 45000, validFrom: new Date('2026-01-01'), refundable: true },
{ tripId: schedule.id, seatClassId: scEconomyBed.id, baseFareMinor: 65000, validFrom: new Date('2026-01-01'), refundable: true },
{ tripId: schedule.id, seatClassId: scVipBed.id, baseFareMinor: 95000, validFrom: new Date('2026-01-01'), refundable: true },
],
skipDuplicates: true,
});
const created = [];
for (const schedule of schedules) {
const s = await prisma.trainSchedule.create({ data: schedule });
created.push(s);
}
// Users
console.log(` ✅ Created ${created.length} schedules`);
return created;
}
// ============================================================================
// SECTION 6: COACH ASSIGNMENTS
// ============================================================================
async function seedCoachAssignments(schedules: any[], coaches: any[]) {
console.log('🔗 Seeding coach assignments...');
const [coachA1, coachB1, coachC1, coachA2, coachB2, coachC2] = coaches;
const [schedule1, schedule2, schedule3] = schedules;
const assignments = [
{ scheduleId: schedule1.id, coachId: coachA1.id, positionNumber: 1 },
{ scheduleId: schedule1.id, coachId: coachB1.id, positionNumber: 2 },
{ scheduleId: schedule1.id, coachId: coachC1.id, positionNumber: 3 },
{ scheduleId: schedule2.id, coachId: coachA2.id, positionNumber: 1 },
{ scheduleId: schedule2.id, coachId: coachB2.id, positionNumber: 2 },
{ scheduleId: schedule2.id, coachId: coachC2.id, positionNumber: 3 },
{ scheduleId: schedule3.id, coachId: coachA1.id, positionNumber: 1 },
{ scheduleId: schedule3.id, coachId: coachB1.id, positionNumber: 2 },
{ scheduleId: schedule3.id, coachId: coachC1.id, positionNumber: 3 },
];
await prisma.coachAssignment.createMany({ data: assignments, skipDuplicates: true });
console.log(` ✅ Created ${assignments.length} coach assignments`);
}
// ============================================================================
// SECTION 7: STOP TIMES (ALL 18 STATIONS)
// ============================================================================
async function seedStopTimes(schedules: any[], stations: any[]) {
console.log('⏱️ Seeding stop times for all stations...');
const [sebeta, labu, indode, bishoftu, mojo, adama, feto, metahara, mieso, bike, diredawa, arawa, adigala, aysha, dawanle, alisabieh, holhol, nagad] = stations;
const [schedule1, schedule2, schedule3] = schedules;
// Full route stop times (Sebeta to Nagad)
const fullRouteStops = [
{ scheduleId: schedule1.id, stationId: sebeta.id, sequence: 1, plannedDepartureAt: new Date('2026-06-15T06:00:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: labu.id, sequence: 2, plannedArrivalAt: new Date('2026-06-15T06:30:00Z'), plannedDepartureAt: new Date('2026-06-15T06:35:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: indode.id, sequence: 3, plannedArrivalAt: new Date('2026-06-15T07:00:00Z'), plannedDepartureAt: new Date('2026-06-15T07:05:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: bishoftu.id, sequence: 4, plannedArrivalAt: new Date('2026-06-15T07:30:00Z'), plannedDepartureAt: new Date('2026-06-15T07:40:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: mojo.id, sequence: 5, plannedArrivalAt: new Date('2026-06-15T08:15:00Z'), plannedDepartureAt: new Date('2026-06-15T08:25:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: adama.id, sequence: 6, plannedArrivalAt: new Date('2026-06-15T09:00:00Z'), plannedDepartureAt: new Date('2026-06-15T09:15:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: feto.id, sequence: 7, plannedArrivalAt: new Date('2026-06-15T09:45:00Z'), plannedDepartureAt: new Date('2026-06-15T09:50:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: metahara.id, sequence: 8, plannedArrivalAt: new Date('2026-06-15T10:30:00Z'), plannedDepartureAt: new Date('2026-06-15T10:45:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: mieso.id, sequence: 9, plannedArrivalAt: new Date('2026-06-15T12:00:00Z'), plannedDepartureAt: new Date('2026-06-15T12:10:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: bike.id, sequence: 10, plannedArrivalAt: new Date('2026-06-15T13:30:00Z'), plannedDepartureAt: new Date('2026-06-15T13:40:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: diredawa.id, sequence: 11, plannedArrivalAt: new Date('2026-06-15T15:00:00Z'), plannedDepartureAt: new Date('2026-06-15T15:20:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: arawa.id, sequence: 12, plannedArrivalAt: new Date('2026-06-15T16:30:00Z'), plannedDepartureAt: new Date('2026-06-15T16:35:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: adigala.id, sequence: 13, plannedArrivalAt: new Date('2026-06-15T17:45:00Z'), plannedDepartureAt: new Date('2026-06-15T17:50:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: aysha.id, sequence: 14, plannedArrivalAt: new Date('2026-06-15T18:30:00Z'), plannedDepartureAt: new Date('2026-06-15T18:40:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: dawanle.id, sequence: 15, plannedArrivalAt: new Date('2026-06-15T19:15:00Z'), plannedDepartureAt: new Date('2026-06-15T19:20:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: alisabieh.id, sequence: 16, plannedArrivalAt: new Date('2026-06-15T20:00:00Z'), plannedDepartureAt: new Date('2026-06-15T20:05:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: holhol.id, sequence: 17, plannedArrivalAt: new Date('2026-06-15T21:00:00Z'), plannedDepartureAt: new Date('2026-06-15T21:05:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule1.id, stationId: nagad.id, sequence: 18, plannedArrivalAt: new Date('2026-06-15T22:00:00Z'), status: 'UPCOMING' as const },
];
// Regional route stop times (Sebeta to Diredawa)
const regionalStops = [
{ scheduleId: schedule3.id, stationId: sebeta.id, sequence: 1, plannedDepartureAt: new Date('2026-06-17T08:00:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule3.id, stationId: labu.id, sequence: 2, plannedArrivalAt: new Date('2026-06-17T08:30:00Z'), plannedDepartureAt: new Date('2026-06-17T08:35:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule3.id, stationId: indode.id, sequence: 3, plannedArrivalAt: new Date('2026-06-17T09:00:00Z'), plannedDepartureAt: new Date('2026-06-17T09:05:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule3.id, stationId: bishoftu.id, sequence: 4, plannedArrivalAt: new Date('2026-06-17T09:30:00Z'), plannedDepartureAt: new Date('2026-06-17T09:40:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule3.id, stationId: mojo.id, sequence: 5, plannedArrivalAt: new Date('2026-06-17T10:15:00Z'), plannedDepartureAt: new Date('2026-06-17T10:25:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule3.id, stationId: adama.id, sequence: 6, plannedArrivalAt: new Date('2026-06-17T11:00:00Z'), plannedDepartureAt: new Date('2026-06-17T11:15:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule3.id, stationId: feto.id, sequence: 7, plannedArrivalAt: new Date('2026-06-17T11:45:00Z'), plannedDepartureAt: new Date('2026-06-17T11:50:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule3.id, stationId: metahara.id, sequence: 8, plannedArrivalAt: new Date('2026-06-17T12:30:00Z'), plannedDepartureAt: new Date('2026-06-17T12:45:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule3.id, stationId: mieso.id, sequence: 9, plannedArrivalAt: new Date('2026-06-17T14:00:00Z'), plannedDepartureAt: new Date('2026-06-17T14:10:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule3.id, stationId: bike.id, sequence: 10, plannedArrivalAt: new Date('2026-06-17T15:30:00Z'), plannedDepartureAt: new Date('2026-06-17T15:40:00Z'), status: 'UPCOMING' as const },
{ scheduleId: schedule3.id, stationId: diredawa.id, sequence: 11, plannedArrivalAt: new Date('2026-06-17T18:00:00Z'), status: 'UPCOMING' as const },
];
const allStops = [...fullRouteStops, ...regionalStops];
await prisma.tripStopTime.createMany({ data: allStops });
console.log(` ✅ Created ${allStops.length} stop times`);
}
// ============================================================================
// SECTION 8: FARE RULES (COMPREHENSIVE SEGMENTS)
// ============================================================================
async function seedFareRules(schedules: any[], seatClasses: any[]) {
console.log('💰 Seeding comprehensive fare rules...');
const [scEconomy, scEconomyBed, scVip] = seatClasses;
// Segment-based fare rules (15+ segments)
const segmentRules = [
// Short segments (1-3 stations)
{ route: 'SBT-LBU', seatClassId: scEconomy.id, baseFareMinor: 5000, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'LBU-IND', seatClassId: scEconomy.id, baseFareMinor: 4500, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'IND-BSH', seatClassId: scEconomy.id, baseFareMinor: 5500, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'BSH-MJO', seatClassId: scEconomy.id, baseFareMinor: 6000, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'MJO-ADM', seatClassId: scEconomy.id, baseFareMinor: 7000, validFrom: new Date('2026-01-01'), refundable: true },
// Medium segments (3-6 stations)
{ route: 'SBT-BSH', seatClassId: scEconomy.id, baseFareMinor: 12000, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'SBT-ADM', seatClassId: scEconomy.id, baseFareMinor: 18000, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'ADM-MTH', seatClassId: scEconomy.id, baseFareMinor: 8500, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'MTH-MSO', seatClassId: scEconomy.id, baseFareMinor: 9500, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'MSO-BKE', seatClassId: scEconomy.id, baseFareMinor: 8000, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'BKE-DDW', seatClassId: scEconomy.id, baseFareMinor: 7500, validFrom: new Date('2026-01-01'), refundable: true },
// Long segments (6+ stations)
{ route: 'SBT-DDW', seatClassId: scEconomy.id, baseFareMinor: 35000, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'DDW-AYS', seatClassId: scEconomy.id, baseFareMinor: 15000, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'AYS-NGD', seatClassId: scEconomy.id, baseFareMinor: 18000, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'SBT-NGD', seatClassId: scEconomy.id, baseFareMinor: 65000, validFrom: new Date('2026-01-01'), refundable: true },
// Cross-border segments
{ route: 'DDW-DWL', seatClassId: scEconomy.id, baseFareMinor: 22000, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'DWL-ALI', seatClassId: scEconomy.id, baseFareMinor: 12000, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'ALI-HOL', seatClassId: scEconomy.id, baseFareMinor: 8500, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'HOL-NGD', seatClassId: scEconomy.id, baseFareMinor: 6000, validFrom: new Date('2026-01-01'), refundable: true },
];
// Add Economy Bed prices (40% higher)
const bedRules = segmentRules.map(rule => ({
...rule,
seatClassId: scEconomyBed.id,
baseFareMinor: Math.round(rule.baseFareMinor * 1.4),
}));
// Add VIP prices (80% higher)
const vipRules = segmentRules.map(rule => ({
...rule,
seatClassId: scVip.id,
baseFareMinor: Math.round(rule.baseFareMinor * 1.8),
}));
const allRules = [...segmentRules, ...bedRules, ...vipRules];
await prisma.fareRule.createMany({ data: allRules, skipDuplicates: true });
// Nationality-specific discounts
const nationalityRules = [
// Ethiopian nationals - 10% discount on domestic routes
{ route: 'SBT-DDW', nationality: 'Ethiopian', seatClassId: scEconomy.id, baseFareMinor: 31500, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'SBT-ADM', nationality: 'Ethiopian', seatClassId: scEconomy.id, baseFareMinor: 16200, validFrom: new Date('2026-01-01'), refundable: true },
// Djiboutian nationals - 5% discount on cross-border routes
{ route: 'DDW-NGD', nationality: 'Djiboutian', seatClassId: scEconomy.id, baseFareMinor: 42750, validFrom: new Date('2026-01-01'), refundable: true },
{ route: 'SBT-NGD', nationality: 'Djiboutian', seatClassId: scEconomy.id, baseFareMinor: 61750, validFrom: new Date('2026-01-01'), refundable: true },
];
await prisma.fareRule.createMany({ data: nationalityRules, skipDuplicates: true });
console.log(` ✅ Created ${allRules.length + nationalityRules.length} fare rules`);
}
// ============================================================================
// SECTION 9: USERS & PASSENGERS
// ============================================================================
async function seedUsers() {
console.log('👥 Seeding users...');
const hash = await bcrypt.hash('password123', 10);
const adminHash = await bcrypt.hash('admin123', 10);
const agentHash = await bcrypt.hash('agent123', 10);
await prisma.user.upsert({ where: { email: 'admin@edr-platform.com' }, update: { passwordHash: adminHash, role: 'ADMIN' }, create: { fullName: 'EDR Admin', email: 'admin@edr-platform.com', phone: '+251900000000', passwordHash: adminHash, role: 'ADMIN' } });
const passengerUser = await prisma.user.upsert({ where: { email: 'kelemu@email.com' }, update: {}, create: { fullName: 'Kelemu Ketsela', email: 'kelemu@email.com', phone: '+251912345678', passwordHash: hash, nationality: 'Ethiopian', nationalId: 'ET123456789' } });
let passenger = await prisma.passenger.findUnique({ where: { userId: passengerUser.id } });
if (!passenger) {
passenger = await prisma.passenger.create({ data: { userId: passengerUser.id } });
await prisma.loyaltyAccount.create({ data: { passengerId: passenger.id, pointsBalance: 2450, tier: 'SILVER' } });
await prisma.walletAccount.create({ data: { passengerId: passenger.id, balanceMinor: 125000 } });
// Admin
await prisma.user.upsert({
where: { email: 'admin@edr-platform.com' },
update: { passwordHash: adminHash, role: 'ADMIN' },
create: {
fullName: 'EDR Admin',
email: 'admin@edr-platform.com',
phone: '+251900000000',
passwordHash: adminHash,
role: 'ADMIN',
},
});
// Ethiopian Passenger
const ethiopianUser = await prisma.user.upsert({
where: { email: 'abebe@email.com' },
update: {},
create: {
fullName: 'Abebe Kebede',
email: 'abebe@email.com',
phone: '+251912345678',
passwordHash: hash,
nationality: 'Ethiopian',
nationalId: 'ET123456789',
},
});
let ethiopianPassenger = await prisma.passenger.findUnique({ where: { userId: ethiopianUser.id } });
if (!ethiopianPassenger) {
ethiopianPassenger = await prisma.passenger.create({ data: { userId: ethiopianUser.id } });
await prisma.loyaltyAccount.create({ data: { passengerId: ethiopianPassenger.id, pointsBalance: 2450, tier: 'SILVER' } });
await prisma.walletAccount.create({ data: { passengerId: ethiopianPassenger.id, balanceMinor: 125000 } });
}
await prisma.userPreferences.upsert({ where: { userId: passengerUser.id }, update: {}, create: { userId: passengerUser.id, language: 'en' } });
await prisma.userPreferences.upsert({
where: { userId: ethiopianUser.id },
update: {},
create: { userId: ethiopianUser.id, language: 'en' },
});
const agentUser = await prisma.user.upsert({ where: { email: 'agent@edr-platform.com' }, update: { passwordHash: agentHash, role: 'AGENT' }, create: { fullName: 'Agent Abebe', email: 'agent@edr-platform.com', phone: '+251911111111', passwordHash: agentHash, role: 'AGENT' } });
await prisma.agent.upsert({ where: { userId: agentUser.id }, update: {}, create: { userId: agentUser.id, agentCode: 'AG001', stationId: addis.id, commissionRate: 5, active: true } });
// Djiboutian Passenger
const djiboutianUser = await prisma.user.upsert({
where: { email: 'ahmed@email.com' },
update: {},
create: {
fullName: 'Ahmed Hassan',
email: 'ahmed@email.com',
phone: '+25377123456',
passwordHash: hash,
nationality: 'Djiboutian',
passportNumber: 'DJ1234567',
},
});
let djiboutianPassenger = await prisma.passenger.findUnique({ where: { userId: djiboutianUser.id } });
if (!djiboutianPassenger) {
djiboutianPassenger = await prisma.passenger.create({ data: { userId: djiboutianUser.id } });
await prisma.loyaltyAccount.create({ data: { passengerId: djiboutianPassenger.id, pointsBalance: 1200, tier: 'BRONZE' } });
await prisma.walletAccount.create({ data: { passengerId: djiboutianPassenger.id, balanceMinor: 85000 } });
}
await prisma.userPreferences.upsert({
where: { userId: djiboutianUser.id },
update: {},
create: { userId: djiboutianUser.id, language: 'fr' },
});
// Agent
const agentUser = await prisma.user.upsert({
where: { email: 'agent@edr-platform.com' },
update: { passwordHash: agentHash, role: 'AGENT' },
create: {
fullName: 'Agent Abebe',
email: 'agent@edr-platform.com',
phone: '+251911111111',
passwordHash: agentHash,
role: 'AGENT',
},
});
const stations = await prisma.station.findMany();
await prisma.agent.upsert({
where: { userId: agentUser.id },
update: {},
create: {
userId: agentUser.id,
agentCode: 'AG001',
stationId: stations[0].id,
commissionRate: 5,
active: true,
},
});
console.log(` ✅ Created 4 users (Admin, Ethiopian, Djiboutian, Agent)`);
}
// ============================================================================
// SECTION 10: SUPPORTING DATA
// ============================================================================
async function seedSupportingData(seatClasses: any[]) {
console.log('📦 Seeding supporting data...');
// Baggage Allowance
await prisma.baggageAllowance.deleteMany({});
await prisma.baggageAllowance.createMany({
data: [
{ seatClassId: scEconomyRegular.id, maxWeightKg: 20, maxPiecesCount: 2, excessFeePerKg: 500 },
{ seatClassId: scEconomyBed.id, maxWeightKg: 25, maxPiecesCount: 2, excessFeePerKg: 450 },
{ seatClassId: scVipBed.id, maxWeightKg: 30, maxPiecesCount: 3, excessFeePerKg: 400 },
{ seatClassId: seatClasses[0].id, maxWeightKg: 20, maxPiecesCount: 2, excessFeePerKg: 500 },
{ seatClassId: seatClasses[1].id, maxWeightKg: 25, maxPiecesCount: 2, excessFeePerKg: 450 },
{ seatClassId: seatClasses[2].id, maxWeightKg: 30, maxPiecesCount: 3, excessFeePerKg: 400 },
],
});
// Notification Templates
await prisma.notificationTemplate.upsert({ where: { code: 'BOOKING_CONFIRMED' }, update: {}, create: { code: 'BOOKING_CONFIRMED', channel: 'EMAIL', subject: 'Booking Confirmed', bodyTemplate: 'Your booking {{bookingRef}} is confirmed for {{tripDate}}.', active: true } });
await prisma.notificationTemplate.upsert({ where: { code: 'PAYMENT_SUCCESS' }, update: {}, create: { code: 'PAYMENT_SUCCESS', channel: 'SMS', bodyTemplate: 'Payment successful for {{bookingRef}}. Amount: {{amount}} ETB', active: true } });
await prisma.notificationTemplate.upsert({
where: { code: 'BOOKING_CONFIRMED' },
update: {},
create: {
code: 'BOOKING_CONFIRMED',
channel: 'EMAIL',
subject: 'Booking Confirmed',
bodyTemplate: 'Your booking {{bookingRef}} is confirmed for {{tripDate}}.',
active: true,
},
});
await prisma.notificationTemplate.upsert({
where: { code: 'PAYMENT_SUCCESS' },
update: {},
create: {
code: 'PAYMENT_SUCCESS',
channel: 'SMS',
bodyTemplate: 'Payment successful for {{bookingRef}}. Amount: {{amount}} ETB',
active: true,
},
});
// Promotions
await prisma.promotion.upsert({ where: { code: 'WEEKEND15' }, update: {}, create: { title: 'Weekend Sale', subtitle: '15% off all trips', code: 'WEEKEND15', percentOff: 15, validUntil: new Date('2026-12-31'), ctaLabel: 'Book Now', active: true } });
// FAQ
await prisma.faqArticle.deleteMany({});
await prisma.faqCategory.deleteMany({});
const faqBooking = await prisma.faqCategory.create({ data: { title: 'Booking & Tickets', iconKey: 'confirmation_number' } });
await prisma.faqArticle.createMany({ data: [{ categoryId: faqBooking.id, question: 'How do I book a train ticket?', answerMarkdown: 'Open Search, select origin and destination stations, choose date, select seats, and proceed to payment.', rank: 1 }] });
// Station Crowd Signals
await prisma.stationCrowdSignal.deleteMany({});
await prisma.stationCrowdSignal.createMany({ data: [{ stationId: addis.id, level: 'MODERATE', label: 'Moderate', statusLabel: 'Normal operations' }, { stationId: djibouti.id, level: 'HIGH', label: 'High', statusLabel: 'Busy terminal' }] });
// Fraud Detection Rules
await prisma.fraudRule.upsert({ where: { type: 'VELOCITY' }, update: {}, create: { type: 'VELOCITY', enabled: true, threshold: 3, config: { windowMinutes: 60, action: 'FLAG' } } });
await prisma.promotion.upsert({
where: { code: 'WEEKEND15' },
update: {},
create: {
title: 'Weekend Sale',
subtitle: '15% off all trips',
code: 'WEEKEND15',
percentOff: 15,
validUntil: new Date('2026-12-31'),
ctaLabel: 'Book Now',
active: true,
},
});
// Currency Exchange Rates
await prisma.currencyExchangeRate.deleteMany({});
await prisma.currencyExchangeRate.createMany({ data: [{ fromCurrency: 'ETB', toCurrency: 'ETB', rate: 1.0, effectiveDate: new Date() }, { fromCurrency: 'ETB', toCurrency: 'USD', rate: 0.018, effectiveDate: new Date() }] });
await prisma.currencyExchangeRate.createMany({
data: [
{ fromCurrency: 'ETB', toCurrency: 'ETB', rate: 1.0, effectiveDate: new Date() },
{ fromCurrency: 'ETB', toCurrency: 'USD', rate: 0.018, effectiveDate: new Date() },
{ fromCurrency: 'ETB', toCurrency: 'DJF', rate: 3.2, effectiveDate: new Date() },
{ fromCurrency: 'DJF', toCurrency: 'ETB', rate: 0.3125, effectiveDate: new Date() },
{ fromCurrency: 'DJF', toCurrency: 'DJF', rate: 1.0, effectiveDate: new Date() },
],
});
console.log('✅ Comprehensive seed complete');
console.log('\n📋 Seed Summary:');
console.log(' - 2 Trains (Express 301, Express 302)');
console.log(' - 6 Physical Coaches (reusable across schedules)');
console.log(' - 4 Train Schedules with coach assignments');
console.log(' - 3 Seat Classes (Economy Regular, Economy Bed, VIP Bed)');
console.log(' - 3 Users: Admin, Passenger (Silver tier + wallet), Agent');
console.log('\n🔑 Login Credentials:');
console.log(' Admin: admin@edr-platform.com / admin123');
console.log(' Passenger: kelemu@email.com / password123');
console.log(' Agent: agent@edr-platform.com / agent123');
console.log('\n🚂 Architecture: Train → TrainSchedule ↔ CoachAssignment ↔ Coach → Seat');
// Fraud Rules
await prisma.fraudRule.upsert({
where: { type: 'VELOCITY' },
update: {},
create: {
type: 'VELOCITY',
enabled: true,
threshold: 3,
config: { windowMinutes: 60, action: 'FLAG' },
},
});
console.log(` ✅ Created supporting data`);
}
main().catch(console.error).finally(() => prisma.$disconnect());
// ============================================================================
// MAIN SEED FUNCTION
// ============================================================================
async function main() {
console.log('🌱 Starting comprehensive modular seed with 18 stations...\n');
const stations = await seedStations();
const seatClasses = await seedSeatClasses();
const trains = await seedTrains();
const coaches = await seedCoachesAndSeats(seatClasses);
const schedules = await seedSchedules(trains, stations);
await seedCoachAssignments(schedules, coaches);
await seedStopTimes(schedules, stations);
await seedFareRules(schedules, seatClasses);
await seedUsers();
await seedSupportingData(seatClasses);
console.log('\n✅ Comprehensive seed complete!\n');
console.log('📋 Seed Summary:');
console.log(' - 18 Stations: SBT, LBU, IND, BSH, MJO, ADM, FTO, MTH, MSO, BKE, DDW, ARW, ADG, AYS, DWL, ALI, HOL, NGD');
console.log(' - 3 Seat Classes (Economy Regular, Economy Bed, VIP Bed)');
console.log(' - 3 Trains (Express 301, Express 302, Local 303)');
console.log(' - 6 Physical Coaches with seats');
console.log(' - 5 Train Schedules covering full and regional routes');
console.log(' - 15+ Fare Segments with nationality-based pricing');
console.log(' - 4 Users: Admin, Ethiopian Passenger, Djiboutian Passenger, Agent');
console.log(' - Currency rates: ETB, USD, DJF');
console.log('\n🔑 Login Credentials:');
console.log(' Admin: admin@edr-platform.com / admin123');
console.log(' Ethiopian Passenger: abebe@email.com / password123');
console.log(' Djiboutian Passenger: ahmed@email.com / password123');
console.log(' Agent: agent@edr-platform.com / agent123');
console.log('\n💰 Booking Flow Ready:');
console.log(' - Search: 18 stations with multiple route combinations');
console.log(' - Select: 3 seat classes with dynamic pricing');
console.log(' - Book: Complete passenger details and payment');
console.log(' - Pay: Multiple payment methods (Telebirr, CBE, Card, Wallet)');
console.log(' - Ticket: QR code generation and validation');
console.log('\n🚂 Sample Routes:');
console.log(' - Full Route: Sebeta → Nagad (18 stations, 16 hours)');
console.log(' - Regional: Sebeta → Diredawa (11 stations, 10 hours)');
console.log(' - Short: Sebeta → Adama (6 stations, 3 hours)');
console.log(' - Cross-border: Diredawa → Nagad (8 stations, 7 hours)');
}
main()
.catch((e) => {
console.error('❌ Seed failed:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});