Round trip journeys, feedback items, backoffice documentation

This commit is contained in:
Stephanos A
2026-06-16 13:33:03 +03:00
parent 28e183b086
commit 75c8423f50
46 changed files with 7222 additions and 570 deletions

View File

@@ -4,7 +4,6 @@ import { randomUUID as uuidv4 } from 'crypto';
const prisma = new PrismaClient();
const EDR_ROUTE_ID = uuidv4();
const TRAIN_ID = uuidv4();
async function seedSystemUsers() {
@@ -24,6 +23,10 @@ async function seedSystemUsers() {
phone: '+251900000000',
passwordHash: adminHash,
role: 'ADMIN',
gender: 'Male',
dateOfBirth: new Date('1980-05-20'),
nationality: 'Ethiopian',
nationalId: 'ET123456789',
},
});
console.log(' ✅ Admin: admin@edr-platform.com / admin123');
@@ -39,6 +42,9 @@ async function seedSystemUsers() {
role: 'PASSENGER',
nationality: 'Ethiopian',
faydaVerified: true,
gender: 'Male',
dateOfBirth: new Date('1990-03-15'),
nationalId: 'ET987654321',
},
});
@@ -49,7 +55,7 @@ async function seedSystemUsers() {
data: { passengerId: passengerRecord.id, pointsBalance: 1500, lifetimePoints: 3000, tier: 'SILVER' },
});
await prisma.walletAccount.create({
data: { passengerId: passengerRecord.id, balanceMinor: 50000 },
data: { passengerId: passengerRecord.id, balanceMinor: 500 },
});
}
await prisma.userPreferences.upsert({
@@ -68,6 +74,8 @@ async function seedSystemUsers() {
phone: '+251911111111',
passwordHash: agentHash,
role: 'AGENT',
gender: 'Female',
dateOfBirth: new Date('1992-07-22'),
},
});
await prisma.agent.upsert({
@@ -86,6 +94,8 @@ async function seedSystemUsers() {
phone: '+251922222222',
passwordHash: supervisorHash,
role: 'SUPERVISOR',
gender: 'Male',
dateOfBirth: new Date('1985-11-10'),
},
});
console.log(' ✅ Supervisor: supervisor@edr-platform.com / supervisor123');
@@ -99,6 +109,8 @@ async function seedSystemUsers() {
phone: '+251933333333',
passwordHash: staffHash,
role: 'STAFF',
gender: 'Female',
dateOfBirth: new Date('1995-09-08'),
},
});
console.log(' ✅ Staff: staff@edr-platform.com / staff123');
@@ -107,21 +119,21 @@ async function seedSystemUsers() {
async function seedStations() {
console.log('\n📍 Seeding 15 stations (Ethio-Djibouti Railway)...');
const stations = [
{ code: 'SBT', name: 'Sebeta', city: 'Sebeta', countryCode: 'ET', lat: 8.9520, lng: 38.6150 },
{ code: 'LEB', name: 'Lebu', city: 'Lebu', countryCode: 'ET', lat: 8.8890, lng: 38.5320 },
{ code: 'BSH', name: 'Bishoftu', city: 'Bishoftu', countryCode: 'ET', lat: 8.7650, lng: 39.0240 },
{ code: 'MOJ', name: 'Mojo', city: 'Mojo', countryCode: 'ET', lat: 8.6780, lng: 39.2130 },
{ code: 'ADM', name: 'Adama', city: 'Adama', countryCode: 'ET', lat: 8.5420, lng: 39.2780 },
{ code: 'MTE', name: 'Metehara', city: 'Metehara', countryCode: 'ET', lat: 8.7890, lng: 39.8920 },
{ code: 'MIS', name: 'Mieso', city: 'Mieso', countryCode: 'ET', lat: 8.9120, lng: 40.3450 },
{ code: 'BIK', name: 'Bike', city: 'Bike', countryCode: 'ET', lat: 9.1230, lng: 40.8670 },
{ code: 'DRE', name: 'Dire Dawa', city: 'Dire Dawa', countryCode: 'ET', lat: 9.5915, lng: 41.8578 },
{ code: 'ADG', name: 'Adigala', city: 'Adigala', countryCode: 'ET', lat: 9.7340, lng: 42.2150 },
{ code: 'AYS', name: 'Aysha', city: 'Aysha', countryCode: 'ET', lat: 10.0120, lng: 42.5670 },
{ code: 'DAW', name: 'Dawanle', city: 'Dawanle', countryCode: 'ET', lat: 10.2340, lng: 42.8340 },
{ code: 'ALS', name: 'Alisabieh', city: 'Alisabieh', countryCode: 'DJ', lat: 10.8950, lng: 42.9560 },
{ code: 'HOL', name: 'Holhol', city: 'Holhol', countryCode: 'DJ', lat: 11.1230, lng: 43.0450 },
{ code: 'NAG', name: 'Nagad', city: 'Nagad', countryCode: 'DJ', lat: 11.3780, lng: 43.1200 },
{ code: 'SBT', name: 'Sebeta', city: 'Sebeta', countryCode: 'ET', lat: 8.9520, lng: 38.6150, sequence: 1 },
{ code: 'LEB', name: 'Lebu', city: 'Lebu', countryCode: 'ET', lat: 8.8890, lng: 38.5320, sequence: 2 },
{ code: 'BSH', name: 'Bishoftu', city: 'Bishoftu', countryCode: 'ET', lat: 8.7650, lng: 39.0240, sequence: 3 },
{ code: 'MOJ', name: 'Mojo', city: 'Mojo', countryCode: 'ET', lat: 8.6780, lng: 39.2130, sequence: 4 },
{ code: 'ADM', name: 'Adama', city: 'Adama', countryCode: 'ET', lat: 8.5420, lng: 39.2780, sequence: 5 },
{ code: 'MTE', name: 'Metehara', city: 'Metehara', countryCode: 'ET', lat: 8.7890, lng: 39.8920, sequence: 6 },
{ code: 'MIS', name: 'Mieso', city: 'Mieso', countryCode: 'ET', lat: 8.9120, lng: 40.3450, sequence: 7 },
{ code: 'BIK', name: 'Bike', city: 'Bike', countryCode: 'ET', lat: 9.1230, lng: 40.8670, sequence: 8 },
{ code: 'DRE', name: 'Dire Dawa', city: 'Dire Dawa', countryCode: 'ET', lat: 9.5915, lng: 41.8578, sequence: 9 },
{ code: 'ADG', name: 'Adigala', city: 'Adigala', countryCode: 'ET', lat: 9.7340, lng: 42.2150, sequence: 10 },
{ code: 'AYS', name: 'Aysha', city: 'Aysha', countryCode: 'ET', lat: 10.0120, lng: 42.5670, sequence: 11 },
{ code: 'DAW', name: 'Dawanle', city: 'Dawanle', countryCode: 'ET', lat: 10.2340, lng: 42.8340, sequence: 12 },
{ code: 'ALS', name: 'Alisabieh', city: 'Alisabieh', countryCode: 'DJ', lat: 10.8950, lng: 42.9560, sequence: 13 },
{ code: 'HOL', name: 'Holhol', city: 'Holhol', countryCode: 'DJ', lat: 11.1230, lng: 43.0450, sequence: 14 },
{ code: 'NAG', name: 'Nagad', city: 'Nagad', countryCode: 'DJ', lat: 11.3780, lng: 43.1200, sequence: 15 },
];
for (const station of stations) {
@@ -142,8 +154,8 @@ async function seedCoachTypesAndClasses() {
console.log('\n🚂 Seeding coach types and seat classes...');
const coachTypes = [
{ code: 'HSC', name: 'Hard Seat Coach', type: 'Economy Regular' },
{ code: 'HBC', name: 'Hard Bed Coach', type: 'Economy Bed' },
{ code: 'SBC', name: 'Soft Bed Coach', type: 'VIP Bed' },
{ code: 'HBC', name: 'Hard Berth Coach', type: 'Economy Bed' },
{ code: 'SBC', name: 'Soft Berth Coach', type: 'VIP Bed' },
];
for (const ct of coachTypes) {
@@ -155,12 +167,12 @@ async function seedCoachTypesAndClasses() {
}
const seatClasses = [
{ name: 'VIP Bed Lower', coachCode: 'SBC', baseFareMinor: 900 },
{ name: 'VIP Bed Upper', coachCode: 'SBC', baseFareMinor: 800 },
{ name: 'Economy Bed Upper', coachCode: 'HBC', baseFareMinor: 600 },
{ name: 'Economy Bed Middle', coachCode: 'HBC', baseFareMinor: 550 },
{ name: 'Economy Bed Lower', coachCode: 'HBC', baseFareMinor: 500 },
{ name: 'Economy Regular', coachCode: 'HSC', baseFareMinor: 250 },
{ name: 'VIP Bed Lower', coachCode: 'SBC', baseFareMinor: 900, premiumMinor: 50, insuranceFeeMinor: 25 },
{ name: 'VIP Bed Upper', coachCode: 'SBC', baseFareMinor: 800, premiumMinor: 45, insuranceFeeMinor: 20 },
{ name: 'Economy Bed Upper', coachCode: 'HBC', baseFareMinor: 600, premiumMinor: 30, insuranceFeeMinor: 15 },
{ name: 'Economy Bed Middle', coachCode: 'HBC', baseFareMinor: 550, premiumMinor: 28, insuranceFeeMinor: 14 },
{ name: 'Economy Bed Lower', coachCode: 'HBC', baseFareMinor: 500, premiumMinor: 25, insuranceFeeMinor: 12 },
{ name: 'Economy Regular', coachCode: 'HSC', baseFareMinor: 250, premiumMinor: 12, insuranceFeeMinor: 6 },
];
for (const sc of seatClasses) {
@@ -168,7 +180,7 @@ async function seedCoachTypesAndClasses() {
await prisma.seatClass.upsert({
where: { coachTypeId_name: { coachTypeId: ct!.id, name: sc.name } },
update: {},
create: { coachTypeId: ct!.id, name: sc.name, baseFareMinor: sc.baseFareMinor },
create: { coachTypeId: ct!.id, name: sc.name, baseFareMinor: sc.baseFareMinor, premiumMinor: sc.premiumMinor, insuranceFeeMinor: sc.insuranceFeeMinor },
});
}
console.log(`${coachTypes.length} coach types, ${seatClasses.length} seat classes created`);
@@ -176,14 +188,12 @@ async function seedCoachTypesAndClasses() {
async function seedRoute() {
console.log('\n🛣 Seeding route and stops...');
const firstStation = await prisma.station.findUnique({ where: { code: 'SBT' } });
const lastStation = await prisma.station.findUnique({ where: { code: 'NAG' } });
const route = await prisma.route.upsert({
where: { code: 'EDR-101' },
where: { code: 'Route-101' },
update: {},
create: {
code: 'EDR-101',
code: 'Route-101',
name: 'Sebeta - Dire Dawa',
description: 'Outbound local route from Sebeta to Dire Dawa',
effectiveFrom: new Date('2026-01-01'),
@@ -193,15 +203,40 @@ async function seedRoute() {
});
const stationCodes = ['SBT', 'LEB', 'BSH', 'MOJ', 'ADM', 'MTE', 'MIS', 'BIK', 'DRE'];
const routeDistancesKm = [0, 11.5, 67.2, 89.9, 106.7, 180.2, 231.6, 293.6, 413.0];
for (let i = 0; i < stationCodes.length; i++) {
const station = await prisma.station.findUnique({ where: { code: stationCodes[i] } });
await prisma.routeStop.upsert({
where: { routeId_sequence: { routeId: route.id, sequence: i + 1 } },
update: {},
create: { routeId: route.id, stationId: station!.id, sequence: i + 1, distanceKm: i * 85 },
create: { routeId: route.id, stationId: station!.id, sequence: i + 1, distanceKm: routeDistancesKm[i] },
});
}
console.log(` ✅ Route with ${stationCodes.length} stops created`);
const returnRoute = await prisma.route.upsert({
where: { code: 'Route-102' },
update: {},
create: {
code: 'Route-102',
name: 'Dire Dawa - Sebeta',
description: 'Inbound local route from Dire Dawa to Sebeta',
effectiveFrom: new Date('2026-01-01'),
effectiveUntil: new Date('2034-12-31'),
active: true,
},
});
const returnStationCodes = ['DRE', 'BIK', 'MIS', 'MTE', 'ADM', 'MOJ', 'BSH', 'LEB', 'SBT'];
const returnRouteDistancesKm = [0, 119.4, 181.4, 232.8, 306.3, 323.1, 345.8, 401.5, 413.0];
for (let i = 0; i < returnStationCodes.length; i++) {
const station = await prisma.station.findUnique({ where: { code: returnStationCodes[i] } });
await prisma.routeStop.upsert({
where: { routeId_sequence: { routeId: returnRoute!.id, sequence: i + 1 } },
update: {},
create: { routeId: returnRoute!.id, stationId: station!.id, sequence: i + 1, distanceKm: returnRouteDistancesKm[i] },
});
}
console.log(` ✅ Route with ${returnStationCodes.length} stops created`);
}
async function seedCoaches() {
@@ -211,9 +246,9 @@ async function seedCoaches() {
const vipBedCoachType = await prisma.coachType.findUnique({ where: { id: 'SBC' } });
const coaches = [
{ number: 'HSC-0001', coachTypeId: ecoCoachType!.id, arrangement: '3+2', capacity: 40 },
{ number: 'HBC-0001', coachTypeId: ecoBedCoachType!.id, arrangement: '3+0', capacity: 66 },
{ number: 'SBC-0001', coachTypeId: vipBedCoachType!.id, arrangement: '2+0', capacity: 120 },
{ number: 'HSC-0001', coachTypeId: ecoCoachType!.id, arrangement: '3+2', capacity: 128, sequence: 1 },
{ number: 'HBC-0001', coachTypeId: ecoBedCoachType!.id, arrangement: '3+0', capacity: 66, sequence: 2 },
{ number: 'SBC-0001', coachTypeId: vipBedCoachType!.id, arrangement: '2+0', capacity: 40, sequence: 3 },
];
let totalSeats = 0;
@@ -230,19 +265,23 @@ async function seedCoaches() {
// FK violation once BookingSeat/SeatBlock/TicketSeat rows reference them.
let seatIndex = 1;
for (let row = 1; row <= Math.ceil(coach.capacity / 2); row++) {
for (const col of ['A', 'B', 'C', 'D']) {
for (const col of ['A', 'B', 'C', 'D', 'E']) {
if (seatIndex > coach.capacity) break;
let bedPosition: string | null = null;
if (c.coachTypeId === ecoBedCoachType!.id || c.coachTypeId === vipBedCoachType!.id) {
if (c.coachTypeId === ecoBedCoachType!.id) {
// Economy Bed: 3-row cycle (upper, middle, lower)
if (row % 3 === 1) bedPosition = 'upper';
else if (row % 3 === 2) bedPosition = 'middle';
else bedPosition = 'lower';
} else if (c.coachTypeId === vipBedCoachType!.id) {
// VIP Bed: 2-row cycle (upper, lower)
bedPosition = row % 2 === 1 ? 'upper' : 'lower';
}
const seatData = {
seatNumber: seatIndex.toString(),
isWindow: col === 'A' || col === 'D',
isAisle: col === 'B' || col === 'C',
isWindow: col === 'A' || col === 'E',
isAisle: col === 'B' || col === 'C' || col === 'D',
bedPosition,
};
@@ -264,67 +303,102 @@ async function seedTrips() {
const train = await prisma.train.upsert({
where: { number: 'EDR-001' },
update: {},
create: { id: TRAIN_ID, number: 'EDR-001', name: 'Djibouti Express' },
create: { id: TRAIN_ID, number: 'EDR-001', name: 'Express Service' },
});
const route = await prisma.route.findUnique({ where: { code: 'EDR-101' } });
const route = await prisma.route.findUnique({ where: { code: 'Route-101' } });
const returnRoute = await prisma.route.findUnique({ where: { code: 'Route-102' } });
const firstStation = await prisma.station.findUnique({ where: { code: 'SBT' } });
const lastStation = await prisma.station.findUnique({ where: { code: 'DRE' } });
const firstReturnStation = await prisma.station.findUnique({ where: { code: 'DRE' } });
const lastReturnStation = await prisma.station.findUnique({ where: { code: 'SBT' } });
const coaches = await prisma.coach.findMany();
const now = new Date();
const schedules = [];
const tomorrow = new Date(now);
tomorrow.setDate(now.getDate() + 1);
for (let d = 0; d < 30; d++) {
const schedules = [];
for (let d = 0; d < 5; d++) {
const tripDate = new Date(now);
tripDate.setDate(tripDate.getDate() + d);
tripDate.setHours(8, 0, 0, 0);
const departureAt = new Date(tripDate);
const arrivalAt = new Date(departureAt.getTime() + 4 * 24 * 60 * 60 * 1000);
tripDate.setHours(20, 30, 0, 0);
schedules.push({
trainId: train.id,
routeId: route!.id,
originStationId: firstStation!.id,
destinationStationId: lastStation!.id,
departureAt,
arrivalAt,
durationMinutes: 4 * 24 * 60,
stopsCount: 15,
departureAt: new Date(tripDate),
arrivalAt: new Date(tripDate), // patched below
durationMinutes: 0, // patched below
stopsCount: 9,
});
}
const createdSchedules = await Promise.all(
schedules.map(s => prisma.trainSchedule.create({ data: s }))
);
// Create TripStopTimes for each schedule
const routeStops = await prisma.routeStop.findMany({
where: { routeId: route!.id },
orderBy: { sequence: 'asc' },
include: { route: true },
for (let d = 0; d < 5; d++) {
const returnTripDate = new Date(tomorrow);
returnTripDate.setDate(returnTripDate.getDate() + d);
returnTripDate.setHours(20, 0, 0, 0);
schedules.push({
trainId: train.id,
routeId: returnRoute!.id,
originStationId: firstReturnStation!.id,
destinationStationId: lastReturnStation!.id,
departureAt: new Date(returnTripDate),
arrivalAt: new Date(returnTripDate), // patched below
durationMinutes: 0, // patched below
stopsCount: 9,
});
}
// Load route stops for both routes upfront
const routeStopsMap = new Map<string, { stationId: string; sequence: number; distanceKm: number }[]>();
for (const r of [route!, returnRoute!]) {
const stops = await prisma.routeStop.findMany({
where: { routeId: r.id },
orderBy: { sequence: 'asc' },
});
routeStopsMap.set(r.id, stops.map(s => ({ stationId: s.stationId, sequence: s.sequence, distanceKm: s.distanceKm! })));
}
// Compute duration from total route distance at 60 km/h
function routeDuration(stops: { distanceKm: number }[]): number {
const totalKm = stops[stops.length - 1].distanceKm - stops[0].distanceKm;
return Math.ceil(totalKm / 60 * 60);
}
// Patch arrivalAt and durationMinutes using distance-based timing
const patchedSchedules = schedules.map(s => {
const stops = routeStopsMap.get(s.routeId!)!;
const durationMinutes = routeDuration(stops);
return { ...s, durationMinutes, arrivalAt: new Date(s.departureAt.getTime() + durationMinutes * 60_000) };
});
const createdSchedules = await Promise.all(
patchedSchedules.map(s => prisma.trainSchedule.create({ data: s }))
);
// Create TripStopTimes using cumulative distanceKm at 60 km/h
for (const schedule of createdSchedules) {
const stopTimes = [];
for (const routeStop of routeStops) {
const minutesFromStart = (routeStop.sequence - 1) * 480; // 8 hours per stop
const stops = routeStopsMap.get(schedule.routeId!)!;
const originKm = stops[0].distanceKm;
const stopTimes = stops.map(stop => {
const minutesFromStart = Math.ceil((stop.distanceKm - originKm) / 60 * 60);
const plannedDepartureAt = new Date(schedule.departureAt.getTime() + minutesFromStart * 60_000);
const plannedArrivalAt = new Date(plannedDepartureAt.getTime() + 30 * 60_000); // 30 min stop
stopTimes.push({
const plannedArrivalAt = new Date(plannedDepartureAt.getTime() - 5 * 60_000); // 5 min dwell
return {
scheduleId: schedule.id,
stationId: routeStop.stationId,
sequence: routeStop.sequence,
stationId: stop.stationId,
sequence: stop.sequence,
plannedArrivalAt,
plannedDepartureAt,
});
}
await Promise.all(
stopTimes.map(st => prisma.tripStopTime.create({ data: st }))
);
};
});
// First stop: arrival = departure (no dwell at origin)
stopTimes[0].plannedArrivalAt = stopTimes[0].plannedDepartureAt;
await Promise.all(stopTimes.map(st => prisma.tripStopTime.create({ data: st })));
}
const coachAssignments = [];
@@ -355,7 +429,7 @@ async function seedTrips() {
async function seedFareRules() {
console.log('\n💰 Seeding fare rules...');
const route = await prisma.route.findUnique({ where: { code: 'EDR-101' } });
const route = await prisma.route.findUnique({ where: { code: 'Route-101' } });
const seatClasses = await prisma.seatClass.findMany();
const validFrom = new Date('2024-01-01');
@@ -374,7 +448,7 @@ async function seedFareRules() {
seatClassId: sc.id,
passengerCategory: 'CHILD' as const,
baseFareMinor: Math.floor(sc.baseFareMinor * 0.5),
discountPercent: 50,
discountPercent: 10,
currency: 'ETB',
validFrom,
});
@@ -436,13 +510,50 @@ async function seedPaymentMethods() {
console.log(`${methods.length} payment methods created`);
}
async function seedSegmentFares() {
console.log('\n📍 Seeding segment fare rules...');
const route = await prisma.route.findUnique({
where: { code: 'Route-101' },
include: { stops: { orderBy: { sequence: 'asc' } } },
});
const seatClasses = await prisma.seatClass.findMany();
const validFrom = new Date('2024-01-01');
if (route && route.stops.length > 2) {
for (const sc of seatClasses) {
await prisma.segmentFareRule.create({
data: {
routeId: route.id,
seatClassId: sc.id,
originStopSequence: 1,
destinationStopSequence: 3,
baseFareMinor: Math.floor(sc.baseFareMinor * 0.4),
validFrom,
},
}).catch(() => {});
await prisma.segmentFareRule.create({
data: {
routeId: route.id,
seatClassId: sc.id,
originStopSequence: 5,
destinationStopSequence: 9,
baseFareMinor: Math.floor(sc.baseFareMinor * 0.6),
validFrom,
},
}).catch(() => {});
}
console.log(`${seatClasses.length * 2} segment fare rules created`);
}
}
async function seedNotificationTemplates() {
console.log('\n🔔 Seeding notification templates...');
const templates = [
{ id: uuidv4(), code: 'BOOKING_CONFIRMED', channel: 'EMAIL', subject: 'Booking Confirmed', bodyTemplate: 'Your booking {{bookingRef}} is confirmed' },
{ id: uuidv4(), code: 'PAYMENT_RECEIVED', channel: 'SMS', bodyTemplate: 'Payment received for {{bookingRef}}' },
{ id: uuidv4(), code: 'TRIP_DEPARTURE', channel: 'PUSH', bodyTemplate: 'Your trip departs in {{minutes}} minutes' },
{ id: uuidv4(), code: 'TRIP_DELAY', channel: 'EMAIL', subject: 'Trip Delayed', bodyTemplate: 'Your trip is delayed by {{delayMinutes}} minutes' },
{ id: uuidv4(), code: 'BOOKING_CONFIRMED', channel: 'EMAIL', subject: 'Booking Confirmed', bodyTemplate: 'Your booking {{bookingRef}} is confirmed for {{date}}' },
{ id: uuidv4(), code: 'PAYMENT_RECEIVED', channel: 'SMS', bodyTemplate: 'Payment ETB {{amount}} received for {{bookingRef}}' },
{ id: uuidv4(), code: 'TRIP_DEPARTURE', channel: 'PUSH', bodyTemplate: 'Your trip {{route}} departs in {{minutes}} minutes' },
{ id: uuidv4(), code: 'TRIP_DELAY', channel: 'EMAIL', subject: 'Trip Delayed', bodyTemplate: 'Your trip {{route}} is delayed by {{delayMinutes}} minutes' },
{ id: uuidv4(), code: 'PROMOTION', channel: 'PUSH', bodyTemplate: 'Get {{percentOff}}% off on {{route}}' },
];
@@ -476,13 +587,13 @@ async function seedMenuAndFood() {
const sandwichId = uuidv4();
await prisma.menuItem.create({
data: { id: coffeeId, scheduleId: schedule.id, categoryId: beverages.id, name: 'Ethiopian Coffee', priceMinor: 5000 },
data: { id: coffeeId, scheduleId: schedule.id, categoryId: beverages.id, name: 'Ethiopian Coffee', priceMinor: 50 },
}).catch(() => {}); // ignore if exists
await prisma.menuItem.create({
data: { id: juiceId, scheduleId: schedule.id, categoryId: beverages.id, name: 'Fresh Juice', priceMinor: 3500 },
data: { id: juiceId, scheduleId: schedule.id, categoryId: beverages.id, name: 'Fresh Juice', priceMinor: 35 },
}).catch(() => {}); // ignore if exists
await prisma.menuItem.create({
data: { id: sandwichId, scheduleId: schedule.id, categoryId: snacks.id, name: 'Sandwich', priceMinor: 8000 },
data: { id: sandwichId, scheduleId: schedule.id, categoryId: snacks.id, name: 'Sandwich', priceMinor: 80 },
}).catch(() => {}); // ignore if exists
}
console.log(` ✅ Menu categories and items created`);
@@ -493,7 +604,7 @@ async function seedPromotions() {
const promos = [
{ id: uuidv4(), title: 'Early Bird Discount', code: 'EARLY20', percentOff: 20, validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) },
{ id: uuidv4(), title: 'Student Discount', code: 'STUDENT15', percentOff: 15, validUntil: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000) },
{ id: uuidv4(), title: 'Group Booking', code: 'GROUP10', amountOffMinor: 10000, validUntil: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) },
{ id: uuidv4(), title: 'Group Booking', code: 'GROUP10', amountOffMinor: 100, validUntil: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) },
];
for (const p of promos) {
@@ -583,6 +694,7 @@ async function main() {
['promotions', seedPromotions],
['FAQ', seedFAQ],
['fraud rules', seedFraudRules],
['segment fares', seedSegmentFares],
];
let failed = 0;