|
|
|
|
@@ -3,182 +3,635 @@ 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 },
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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: '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();
|
|
|
|
|
});
|
|
|
|
|
|