mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 12:41:04 +00:00
638 lines
31 KiB
TypeScript
638 lines
31 KiB
TypeScript
import { PrismaClient } 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',
|
|
});
|
|
}
|
|
}
|
|
await prisma.seat.createMany({ data: seats as any });
|
|
}
|
|
}
|
|
|
|
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, train303.id] } },
|
|
select: { id: true },
|
|
})).map((s: { id: string }) => 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 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,
|
|
},
|
|
];
|
|
|
|
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: 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: '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 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();
|
|
});
|