import { PrismaClient, SeatKind } from '@prisma/client'; import * as bcrypt from 'bcrypt'; const prisma = new PrismaClient(); // ============================================================================ // 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 }, ]; const created = []; for (const station of stations) { const s = await prisma.station.upsert({ where: { code: station.code }, update: {}, create: station, }); created.push(s); } console.log(` āœ… Created ${created.length} stations`); return created; } // ============================================================================ // 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 }, ]; 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); } 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(config.totalUnits / 4); for (let row = 1; row <= rows; row++) { for (const col of ['A', 'B', 'C', 'D']) { 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 }); } } console.log(` āœ… Created ${coaches.length} coaches with seats`); return coaches; } // ============================================================================ // SECTION 5: SCHEDULES (15+ SEGMENTS) // ============================================================================ async function seedSchedules(trains: any[], stations: any[], routes: 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; const [fullRoute, regionalRoute] = routes; // Clean up existing schedules const existingScheduleIds = (await prisma.trainSchedule.findMany({ where: { trainId: { in: [train301.id, train302.id, train303.id] } }, select: { id: true }, })).map((s) => s.id); if (existingScheduleIds.length > 0) { // Delete in correct order to avoid foreign key constraints await prisma.bookingSeat.deleteMany({ where: { booking: { scheduleId: { in: existingScheduleIds } } } }); await prisma.booking.deleteMany({ where: { scheduleId: { in: existingScheduleIds } } }); 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 schedules = [ // Full route: Sebeta to Nagad (18 stations) { trainId: train301.id, routeId: fullRoute.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, routeId: fullRoute.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, routeId: regionalRoute.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, routeId: fullRoute.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, routeId: fullRoute.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, }, ]; const created = []; for (const schedule of schedules) { const s = await prisma.trainSchedule.create({ data: schedule }); created.push(s); } 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); // 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: ethiopianUser.id }, update: {}, create: { userId: ethiopianUser.id, language: 'en' }, }); // 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: ROUTES // ============================================================================ async function seedRoutes(stations: any[], seatClasses: any[]) { console.log('šŸ›¤ļø Seeding routes...'); const [sebeta, labu, indode, bishoftu, mojo, adama, feto, metahara, mieso, bike, diredawa, arawa, adigala, aysha, dawanle, alisabieh, holhol, nagad] = stations; const [scEconomy, scEconomyBed, scVip] = seatClasses; // Route 1: Full Line (Sebeta to Nagad) const fullRoute = await prisma.route.upsert({ where: { code: 'SBT-NGD-FULL' }, update: {}, create: { code: 'SBT-NGD-FULL', name: 'Sebeta - Nagad Express', description: 'Complete Ethio-Djibouti Railway route from Sebeta to Nagad', effectiveFrom: new Date('2026-01-01'), active: true, }, }); // Create stops for full route const fullRouteStops = [ { routeId: fullRoute.id, stationId: sebeta.id, sequence: 1, distanceKm: 0 }, { routeId: fullRoute.id, stationId: labu.id, sequence: 2, distanceKm: 15 }, { routeId: fullRoute.id, stationId: indode.id, sequence: 3, distanceKm: 28 }, { routeId: fullRoute.id, stationId: bishoftu.id, sequence: 4, distanceKm: 45 }, { routeId: fullRoute.id, stationId: mojo.id, sequence: 5, distanceKm: 73 }, { routeId: fullRoute.id, stationId: adama.id, sequence: 6, distanceKm: 99 }, { routeId: fullRoute.id, stationId: feto.id, sequence: 7, distanceKm: 125 }, { routeId: fullRoute.id, stationId: metahara.id, sequence: 8, distanceKm: 168 }, { routeId: fullRoute.id, stationId: mieso.id, sequence: 9, distanceKm: 245 }, { routeId: fullRoute.id, stationId: bike.id, sequence: 10, distanceKm: 312 }, { routeId: fullRoute.id, stationId: diredawa.id, sequence: 11, distanceKm: 378 }, { routeId: fullRoute.id, stationId: arawa.id, sequence: 12, distanceKm: 445 }, { routeId: fullRoute.id, stationId: adigala.id, sequence: 13, distanceKm: 512 }, { routeId: fullRoute.id, stationId: aysha.id, sequence: 14, distanceKm: 578 }, { routeId: fullRoute.id, stationId: dawanle.id, sequence: 15, distanceKm: 625 }, { routeId: fullRoute.id, stationId: alisabieh.id, sequence: 16, distanceKm: 672 }, { routeId: fullRoute.id, stationId: holhol.id, sequence: 17, distanceKm: 718 }, { routeId: fullRoute.id, stationId: nagad.id, sequence: 18, distanceKm: 756 }, ]; await prisma.routeStop.createMany({ data: fullRouteStops, skipDuplicates: true }); // Fare rules for full route const fullRouteFares = [ { routeId: fullRoute.id, seatClassId: scEconomy.id, passengerCategory: 'ADULT' as const, baseFareMinor: 65000, validFrom: new Date('2026-01-01') }, { routeId: fullRoute.id, seatClassId: scEconomy.id, passengerCategory: 'CHILD' as const, baseFareMinor: 65000, validFrom: new Date('2026-01-01') }, { routeId: fullRoute.id, seatClassId: scEconomyBed.id, passengerCategory: 'ADULT' as const, baseFareMinor: 91000, validFrom: new Date('2026-01-01') }, { routeId: fullRoute.id, seatClassId: scEconomyBed.id, passengerCategory: 'CHILD' as const, baseFareMinor: 91000, validFrom: new Date('2026-01-01') }, { routeId: fullRoute.id, seatClassId: scVip.id, passengerCategory: 'ADULT' as const, baseFareMinor: 117000, validFrom: new Date('2026-01-01') }, { routeId: fullRoute.id, seatClassId: scVip.id, passengerCategory: 'CHILD' as const, baseFareMinor: 117000, validFrom: new Date('2026-01-01') }, ]; await prisma.routeFareRule.createMany({ data: fullRouteFares, skipDuplicates: true }); // Route 2: Regional (Sebeta to Diredawa) const regionalRoute = await prisma.route.upsert({ where: { code: 'SBT-DDW-REG' }, update: {}, create: { code: 'SBT-DDW-REG', name: 'Sebeta - Diredawa Regional', description: 'Regional service from Sebeta to Diredawa', effectiveFrom: new Date('2026-01-01'), active: true, }, }); const regionalStops = [ { routeId: regionalRoute.id, stationId: sebeta.id, sequence: 1, distanceKm: 0 }, { routeId: regionalRoute.id, stationId: labu.id, sequence: 2, distanceKm: 15 }, { routeId: regionalRoute.id, stationId: indode.id, sequence: 3, distanceKm: 28 }, { routeId: regionalRoute.id, stationId: bishoftu.id, sequence: 4, distanceKm: 45 }, { routeId: regionalRoute.id, stationId: mojo.id, sequence: 5, distanceKm: 73 }, { routeId: regionalRoute.id, stationId: adama.id, sequence: 6, distanceKm: 99 }, { routeId: regionalRoute.id, stationId: feto.id, sequence: 7, distanceKm: 125 }, { routeId: regionalRoute.id, stationId: metahara.id, sequence: 8, distanceKm: 168 }, { routeId: regionalRoute.id, stationId: mieso.id, sequence: 9, distanceKm: 245 }, { routeId: regionalRoute.id, stationId: bike.id, sequence: 10, distanceKm: 312 }, { routeId: regionalRoute.id, stationId: diredawa.id, sequence: 11, distanceKm: 378 }, ]; await prisma.routeStop.createMany({ data: regionalStops, skipDuplicates: true }); const regionalFares = [ { routeId: regionalRoute.id, seatClassId: scEconomy.id, passengerCategory: 'ADULT' as const, baseFareMinor: 35000, validFrom: new Date('2026-01-01') }, { routeId: regionalRoute.id, seatClassId: scEconomy.id, passengerCategory: 'CHILD' as const, baseFareMinor: 35000, validFrom: new Date('2026-01-01') }, { routeId: regionalRoute.id, seatClassId: scEconomyBed.id, passengerCategory: 'ADULT' as const, baseFareMinor: 49000, validFrom: new Date('2026-01-01') }, { routeId: regionalRoute.id, seatClassId: scEconomyBed.id, passengerCategory: 'CHILD' as const, baseFareMinor: 49000, validFrom: new Date('2026-01-01') }, { routeId: regionalRoute.id, seatClassId: scVip.id, passengerCategory: 'ADULT' as const, baseFareMinor: 63000, validFrom: new Date('2026-01-01') }, { routeId: regionalRoute.id, seatClassId: scVip.id, passengerCategory: 'CHILD' as const, baseFareMinor: 63000, validFrom: new Date('2026-01-01') }, ]; await prisma.routeFareRule.createMany({ data: regionalFares, skipDuplicates: true }); // Route 3: Short Distance (Sebeta to Adama) const shortRoute = await prisma.route.upsert({ where: { code: 'SBT-ADM-SHORT' }, update: {}, create: { code: 'SBT-ADM-SHORT', name: 'Sebeta - Adama Commuter', description: 'Short distance commuter service', effectiveFrom: new Date('2026-01-01'), active: true, }, }); const shortStops = [ { routeId: shortRoute.id, stationId: sebeta.id, sequence: 1, distanceKm: 0 }, { routeId: shortRoute.id, stationId: labu.id, sequence: 2, distanceKm: 15 }, { routeId: shortRoute.id, stationId: indode.id, sequence: 3, distanceKm: 28 }, { routeId: shortRoute.id, stationId: bishoftu.id, sequence: 4, distanceKm: 45 }, { routeId: shortRoute.id, stationId: mojo.id, sequence: 5, distanceKm: 73 }, { routeId: shortRoute.id, stationId: adama.id, sequence: 6, distanceKm: 99 }, ]; await prisma.routeStop.createMany({ data: shortStops, skipDuplicates: true }); const shortFares = [ { routeId: shortRoute.id, seatClassId: scEconomy.id, passengerCategory: 'ADULT' as const, baseFareMinor: 18000, validFrom: new Date('2026-01-01') }, { routeId: shortRoute.id, seatClassId: scEconomy.id, passengerCategory: 'CHILD' as const, baseFareMinor: 18000, validFrom: new Date('2026-01-01') }, { routeId: shortRoute.id, seatClassId: scEconomyBed.id, passengerCategory: 'ADULT' as const, baseFareMinor: 25200, validFrom: new Date('2026-01-01') }, { routeId: shortRoute.id, seatClassId: scEconomyBed.id, passengerCategory: 'CHILD' as const, baseFareMinor: 25200, validFrom: new Date('2026-01-01') }, { routeId: shortRoute.id, seatClassId: scVip.id, passengerCategory: 'ADULT' as const, baseFareMinor: 32400, validFrom: new Date('2026-01-01') }, { routeId: shortRoute.id, seatClassId: scVip.id, passengerCategory: 'CHILD' as const, baseFareMinor: 32400, validFrom: new Date('2026-01-01') }, ]; await prisma.routeFareRule.createMany({ data: shortFares, skipDuplicates: true }); // Route 4: Cross-Border (Diredawa to Nagad) const crossBorderRoute = await prisma.route.upsert({ where: { code: 'DDW-NGD-INTL' }, update: {}, create: { code: 'DDW-NGD-INTL', name: 'Diredawa - Nagad International', description: 'Cross-border service from Ethiopia to Djibouti', effectiveFrom: new Date('2026-01-01'), active: true, }, }); const crossBorderStops = [ { routeId: crossBorderRoute.id, stationId: diredawa.id, sequence: 1, distanceKm: 0 }, { routeId: crossBorderRoute.id, stationId: arawa.id, sequence: 2, distanceKm: 67 }, { routeId: crossBorderRoute.id, stationId: adigala.id, sequence: 3, distanceKm: 134 }, { routeId: crossBorderRoute.id, stationId: aysha.id, sequence: 4, distanceKm: 200 }, { routeId: crossBorderRoute.id, stationId: dawanle.id, sequence: 5, distanceKm: 247 }, { routeId: crossBorderRoute.id, stationId: alisabieh.id, sequence: 6, distanceKm: 294 }, { routeId: crossBorderRoute.id, stationId: holhol.id, sequence: 7, distanceKm: 340 }, { routeId: crossBorderRoute.id, stationId: nagad.id, sequence: 8, distanceKm: 378 }, ]; await prisma.routeStop.createMany({ data: crossBorderStops, skipDuplicates: true }); const crossBorderFares = [ { routeId: crossBorderRoute.id, seatClassId: scEconomy.id, passengerCategory: 'ADULT' as const, baseFareMinor: 45000, validFrom: new Date('2026-01-01') }, { routeId: crossBorderRoute.id, seatClassId: scEconomy.id, passengerCategory: 'CHILD' as const, baseFareMinor: 45000, validFrom: new Date('2026-01-01') }, { routeId: crossBorderRoute.id, seatClassId: scEconomyBed.id, passengerCategory: 'ADULT' as const, baseFareMinor: 63000, validFrom: new Date('2026-01-01') }, { routeId: crossBorderRoute.id, seatClassId: scEconomyBed.id, passengerCategory: 'CHILD' as const, baseFareMinor: 63000, validFrom: new Date('2026-01-01') }, { routeId: crossBorderRoute.id, seatClassId: scVip.id, passengerCategory: 'ADULT' as const, baseFareMinor: 81000, validFrom: new Date('2026-01-01') }, { routeId: crossBorderRoute.id, seatClassId: scVip.id, passengerCategory: 'CHILD' as const, baseFareMinor: 81000, validFrom: new Date('2026-01-01') }, ]; await prisma.routeFareRule.createMany({ data: crossBorderFares, skipDuplicates: true }); console.log(` āœ… Created 4 routes with stops and fare rules`); return [fullRoute, regionalRoute, shortRoute, crossBorderRoute]; } // ============================================================================ // SECTION 11: SUPPORTING DATA // ============================================================================ async function seedSupportingData(seatClasses: any[]) { console.log('šŸ“¦ Seeding supporting data...'); // Baggage Allowance await prisma.baggageAllowance.deleteMany({}); await prisma.baggageAllowance.createMany({ data: [ { 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 }, ], }); // Supported Payment Methods (platform-wide catalog) const paymentMethods = [ { type: 'TELEBIRR', displayName: 'Telebirr', region: 'ETHIOPIA', currency: 'ETB', sortOrder: 1, isDefault: true }, { type: 'CBE_BIRR', displayName: 'CBE Birr', region: 'ETHIOPIA', currency: 'ETB', sortOrder: 2 }, { type: 'EBIRR', displayName: 'E-Birr', region: 'ETHIOPIA', currency: 'ETB', sortOrder: 3 }, { type: 'WAAFI', displayName: 'Waafi', region: 'DJIBOUTI', currency: 'DJF', sortOrder: 4 }, { type: 'CARD', displayName: 'Credit / Debit Card', region: 'INTERNATIONAL', currency: 'USD', sortOrder: 5 }, { type: 'WALLET', displayName: 'EDR Wallet', region: 'GLOBAL', currency: 'ETB', sortOrder: 6 }, ] as const; for (const pm of paymentMethods) { await prisma.paymentMethod.upsert({ where: { type: pm.type as any }, update: { displayName: pm.displayName, region: pm.region as any, currency: pm.currency, sortOrder: pm.sortOrder, enabled: true }, create: { ...pm, region: pm.region as any, type: pm.type as any }, }); } // 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: 'booking.created' }, update: {}, create: { code: 'booking.created', channel: 'EMAIL', subject: 'Booking Created', bodyTemplate: 'Your booking {{bookingRef}} has been created successfully.', 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, }, }); // 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() }, { 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() }, ], }); // 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 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 routes = await seedRoutes(stations, seatClasses); const schedules = await seedSchedules(trains, stations, routes); 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(' - 4 Routes with stops and fare rules'); 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, 756 km)'); console.log(' - Regional: Sebeta → Diredawa (11 stations, 378 km)'); console.log(' - Short: Sebeta → Adama (6 stations, 99 km)'); console.log(' - Cross-border: Diredawa → Nagad (8 stations, 378 km)'); } main() .catch((e) => { console.error('āŒ Seed failed:', e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });