Files
edr-platform/apps/edr-passenger-api/prisma/seed.ts

185 lines
14 KiB
TypeScript

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());