import { PrismaClient, SeatKind } from '@prisma/client'; import * as bcrypt from 'bcrypt'; const prisma = new PrismaClient(); async function main() { console.log('🌱 Starting comprehensive seed...'); // 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 } }); // 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 } }); // 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' } }); // 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 } }); // Create seats for each physical coach for (const coach of [coachA1, coachB1, coachC1, coachA2, coachB2, coachC2]) { const existingSeats = await prisma.seat.count({ where: { coachId: coach.id } }); if (existingSeats === 0) { const seats = []; const rows = Math.ceil(coach.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 }); } } await prisma.seat.createMany({ data: seats }); } } // Train Schedules — delete dependents first to avoid FK violations const existingScheduleIds = (await prisma.trainSchedule.findMany({ where: { trainId: { in: [train301.id, train302.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, }); // 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, }); } // 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 } }); } await prisma.userPreferences.upsert({ where: { userId: passengerUser.id }, update: {}, create: { userId: passengerUser.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 } }); // 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 }, ], }); // 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 } }); // 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' } } }); // 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() }] }); 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'); } main().catch(console.error).finally(() => prisma.$disconnect());