Files
edr-platform/apps/edr-passenger-api/prisma/seed.ts
Abubeker Yasin 556c2dc1f8 Update seed.ts
2026-06-11 14:24:56 +03:00

616 lines
22 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
import { randomUUID as uuidv4 } from 'crypto';
const prisma = new PrismaClient();
const EDR_ROUTE_ID = uuidv4();
const TRAIN_ID = uuidv4();
async function seedSystemUsers() {
console.log('👥 Seeding system users...');
const adminHash = await bcrypt.hash('admin123', 10);
const passengerHash = await bcrypt.hash('password123', 10);
const agentHash = await bcrypt.hash('agent123', 10);
const supervisorHash = await bcrypt.hash('supervisor123', 10);
const staffHash = await bcrypt.hash('staff123', 10);
const admin = await prisma.user.upsert({
where: { email: 'admin@edr-platform.com' },
update: { passwordHash: adminHash, role: 'ADMIN' },
create: {
fullName: 'System Admin',
email: 'admin@edr-platform.com',
phone: '+251900000000',
passwordHash: adminHash,
role: 'ADMIN',
},
});
console.log(' ✅ Admin: admin@edr-platform.com / admin123');
const passenger = await prisma.user.upsert({
where: { email: 'kelemu@email.com' },
update: { passwordHash: passengerHash },
create: {
fullName: 'Kelemu Kebede',
email: 'kelemu@email.com',
phone: '+251911234567',
passwordHash: passengerHash,
role: 'PASSENGER',
nationality: 'Ethiopian',
faydaVerified: true,
},
});
let passengerRecord = await prisma.passenger.findUnique({ where: { userId: passenger.id } });
if (!passengerRecord) {
passengerRecord = await prisma.passenger.create({ data: { userId: passenger.id } });
await prisma.loyaltyAccount.create({
data: { passengerId: passengerRecord.id, pointsBalance: 1500, lifetimePoints: 3000, tier: 'SILVER' },
});
await prisma.walletAccount.create({
data: { passengerId: passengerRecord.id, balanceMinor: 50000 },
});
}
await prisma.userPreferences.upsert({
where: { userId: passenger.id },
update: {},
create: { userId: passenger.id, language: 'en' },
});
console.log(' ✅ Passenger: kelemu@email.com / password123');
const agent = await prisma.user.upsert({
where: { email: 'agent@edr-platform.com' },
update: { passwordHash: agentHash, role: 'AGENT' },
create: {
fullName: 'Booking Agent',
email: 'agent@edr-platform.com',
phone: '+251911111111',
passwordHash: agentHash,
role: 'AGENT',
},
});
await prisma.agent.upsert({
where: { userId: agent.id },
update: {},
create: { userId: agent.id, agentCode: 'AG0001', commissionRate: 5 },
});
console.log(' ✅ Agent: agent@edr-platform.com / agent123');
const supervisor = await prisma.user.upsert({
where: { email: 'supervisor@edr-platform.com' },
update: { passwordHash: supervisorHash, role: 'SUPERVISOR' },
create: {
fullName: 'System Supervisor',
email: 'supervisor@edr-platform.com',
phone: '+251922222222',
passwordHash: supervisorHash,
role: 'SUPERVISOR',
},
});
console.log(' ✅ Supervisor: supervisor@edr-platform.com / supervisor123');
const staff = await prisma.user.upsert({
where: { email: 'staff@edr-platform.com' },
update: { passwordHash: staffHash, role: 'STAFF' },
create: {
fullName: 'Support Staff',
email: 'staff@edr-platform.com',
phone: '+251933333333',
passwordHash: staffHash,
role: 'STAFF',
},
});
console.log(' ✅ Staff: staff@edr-platform.com / staff123');
}
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 },
];
for (const station of stations) {
await prisma.station.upsert({
where: { code: station.code },
update: {},
create: {
id: uuidv4(),
...station,
},
});
}
console.log(`${stations.length} stations created`);
return stations;
}
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' },
];
for (const ct of coachTypes) {
await prisma.coachType.upsert({
where: { id: ct.code },
update: {},
create: { ...ct, id: ct.code },
});
}
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 },
];
for (const sc of seatClasses) {
const ct = await prisma.coachType.findUnique({ where: { id: sc.coachCode } });
await prisma.seatClass.upsert({
where: { coachTypeId_name: { coachTypeId: ct!.id, name: sc.name } },
update: {},
create: { coachTypeId: ct!.id, name: sc.name, baseFareMinor: sc.baseFareMinor },
});
}
console.log(`${coachTypes.length} coach types, ${seatClasses.length} seat classes created`);
}
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' },
update: {},
create: {
code: 'EDR-101',
name: 'Sebeta - Dire Dawa',
description: 'Outbound local route from Sebeta to Dire Dawa',
effectiveFrom: new Date('2026-01-01'),
effectiveUntil: new Date('2034-12-31'),
active: true,
},
});
const stationCodes = ['SBT', 'LEB', 'BSH', 'MOJ', 'ADM', 'MTE', 'MIS', 'BIK', 'DRE'];
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 },
});
}
console.log(` ✅ Route with ${stationCodes.length} stops created`);
}
async function seedCoaches() {
console.log('\n🚃 Seeding coaches and seats...');
const ecoCoachType = await prisma.coachType.findUnique({ where: { id: 'HSC' } });
const ecoBedCoachType = await prisma.coachType.findUnique({ where: { id: 'HBC' } });
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 },
];
let totalSeats = 0;
for (const coach of coaches) {
const c = await prisma.coach.upsert({
where: { number: coach.number },
update: {},
create: coach,
});
// Idempotently reconcile the coach's seats. Upsert keyed on the
// @@unique([coachId, row, col]) constraint so a re-seed updates existing
// rows in place instead of deleting them. Deleting Seats fails with a P2003
// 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']) {
if (seatIndex > coach.capacity) break;
let bedPosition: string | null = null;
if (c.coachTypeId === ecoBedCoachType!.id || c.coachTypeId === vipBedCoachType!.id) {
if (row % 3 === 1) bedPosition = 'upper';
else if (row % 3 === 2) bedPosition = 'middle';
else bedPosition = 'lower';
}
const seatData = {
seatNumber: seatIndex.toString(),
isWindow: col === 'A' || col === 'D',
isAisle: col === 'B' || col === 'C',
bedPosition,
};
await prisma.seat.upsert({
where: { coachId_row_col: { coachId: c.id, row, col } },
update: seatData,
create: { coachId: c.id, row, col, ...seatData },
});
seatIndex++;
}
}
totalSeats += coach.capacity;
}
console.log(`${coaches.length} coaches with ${totalSeats} seats created`);
}
async function seedTrips() {
console.log('\n🚆 Seeding train service, schedule, and trips...');
const train = await prisma.train.upsert({
where: { number: 'EDR-001' },
update: {},
create: { id: TRAIN_ID, number: 'EDR-001', name: 'Djibouti Express' },
});
const route = await prisma.route.findUnique({ where: { code: 'EDR-101' } });
const firstStation = await prisma.station.findUnique({ where: { code: 'SBT' } });
const lastStation = await prisma.station.findUnique({ where: { code: 'DRE' } });
const coaches = await prisma.coach.findMany();
const now = new Date();
const schedules = [];
for (let d = 0; d < 30; 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);
schedules.push({
trainId: train.id,
routeId: route!.id,
originStationId: firstStation!.id,
destinationStationId: lastStation!.id,
departureAt,
arrivalAt,
durationMinutes: 4 * 24 * 60,
stopsCount: 15,
});
}
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 (const schedule of createdSchedules) {
const stopTimes = [];
for (const routeStop of routeStops) {
const minutesFromStart = (routeStop.sequence - 1) * 480; // 8 hours per stop
const plannedDepartureAt = new Date(schedule.departureAt.getTime() + minutesFromStart * 60_000);
const plannedArrivalAt = new Date(plannedDepartureAt.getTime() + 30 * 60_000); // 30 min stop
stopTimes.push({
scheduleId: schedule.id,
stationId: routeStop.stationId,
sequence: routeStop.sequence,
plannedArrivalAt,
plannedDepartureAt,
});
}
await Promise.all(
stopTimes.map(st => prisma.tripStopTime.create({ data: st }))
);
}
const coachAssignments = [];
const liveStatuses = [];
for (const schedule of createdSchedules) {
for (let p = 0; p < coaches.length; p++) {
coachAssignments.push({
scheduleId: schedule.id,
coachId: coaches[p].id,
positionNumber: p + 1,
});
}
liveStatuses.push({
scheduleId: schedule.id,
state: 'scheduled',
progressPercent: 0,
});
}
await Promise.all([
...coachAssignments.map(ca => prisma.coachAssignment.create({ data: ca })),
...liveStatuses.map(ls => prisma.tripLiveStatus.create({ data: ls })),
]);
console.log(` ✅ Train with ${createdSchedules.length} upcoming trips created`);
}
async function seedFareRules() {
console.log('\n💰 Seeding fare rules...');
const route = await prisma.route.findUnique({ where: { code: 'EDR-101' } });
const seatClasses = await prisma.seatClass.findMany();
const validFrom = new Date('2024-01-01');
const fareRules = [];
for (const sc of seatClasses) {
fareRules.push({
routeId: route!.id,
seatClassId: sc.id,
passengerCategory: 'ADULT' as const,
baseFareMinor: sc.baseFareMinor,
currency: 'ETB',
validFrom,
});
fareRules.push({
routeId: route!.id,
seatClassId: sc.id,
passengerCategory: 'CHILD' as const,
baseFareMinor: Math.floor(sc.baseFareMinor * 0.5),
discountPercent: 50,
currency: 'ETB',
validFrom,
});
}
await Promise.all(
fareRules.map(fr => prisma.routeFareRule.create({ data: fr }))
);
console.log(`${fareRules.length} fare rules for ADULT/CHILD categories created`);
}
async function seedCurrency() {
console.log('\n💱 Seeding currency exchange rates...');
const rates = [
{ from: 'ETB', to: 'DJF', rate: 3.25 },
{ from: 'ETB', to: 'USD', rate: 0.018 },
{ from: 'DJF', to: 'ETB', rate: 0.3077 },
{ from: 'USD', to: 'ETB', rate: 55.56 },
];
for (const r of rates) {
await prisma.currencyExchangeRate.upsert({
where: {
fromCurrency_toCurrency_effectiveDate: {
fromCurrency: r.from as any,
toCurrency: r.to as any,
effectiveDate: new Date('2024-01-01'),
},
},
update: { rate: r.rate },
create: {
fromCurrency: r.from as any,
toCurrency: r.to as any,
rate: r.rate,
effectiveDate: new Date('2024-01-01'),
},
});
}
console.log(` ✅ 4 currency exchange rates created`);
}
async function seedPaymentMethods() {
console.log('\n💳 Seeding payment methods...');
const methods = [
{ type: 'TELEBIRR', displayName: 'Telebirr', region: 'ETHIOPIA' },
{ type: 'CBE_BIRR', displayName: 'CBE Birr', region: 'ETHIOPIA' },
{ type: 'EBIRR', displayName: 'eBirr', region: 'ETHIOPIA' },
{ type: 'CARD', displayName: 'Credit/Debit Card', region: 'GLOBAL' },
{ type: 'WALLET', displayName: 'Wallet', region: 'GLOBAL' },
];
for (const m of methods) {
await prisma.paymentMethod.upsert({
where: { type: m.type as any },
update: {},
create: { ...m, type: m.type as any, region: m.region as any },
});
}
console.log(`${methods.length} payment methods 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: 'PROMOTION', channel: 'PUSH', bodyTemplate: 'Get {{percentOff}}% off on {{route}}' },
];
for (const t of templates) {
await prisma.notificationTemplate.upsert({
where: { code: t.code },
update: {},
create: t,
});
}
console.log(`${templates.length} notification templates created`);
}
async function seedMenuAndFood() {
console.log('\n🍽 Seeding menu categories and items...');
const beverages = await prisma.menuCategory.upsert({
where: { id: uuidv4() },
update: {},
create: { id: uuidv4(), name: 'Beverages' },
});
const snacks = await prisma.menuCategory.upsert({
where: { id: uuidv4() },
update: {},
create: { id: uuidv4(), name: 'Snacks' },
});
const schedule = await prisma.trainSchedule.findFirst();
if (schedule) {
const coffeeId = uuidv4();
const juiceId = uuidv4();
const sandwichId = uuidv4();
await prisma.menuItem.create({
data: { id: coffeeId, scheduleId: schedule.id, categoryId: beverages.id, name: 'Ethiopian Coffee', priceMinor: 5000 },
}).catch(() => {}); // ignore if exists
await prisma.menuItem.create({
data: { id: juiceId, scheduleId: schedule.id, categoryId: beverages.id, name: 'Fresh Juice', priceMinor: 3500 },
}).catch(() => {}); // ignore if exists
await prisma.menuItem.create({
data: { id: sandwichId, scheduleId: schedule.id, categoryId: snacks.id, name: 'Sandwich', priceMinor: 8000 },
}).catch(() => {}); // ignore if exists
}
console.log(` ✅ Menu categories and items created`);
}
async function seedPromotions() {
console.log('\n🎉 Seeding promotions...');
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) },
];
for (const p of promos) {
await prisma.promotion.upsert({
where: { code: p.code },
update: {},
create: p,
});
}
console.log(`${promos.length} promotions created`);
}
async function seedFAQ() {
console.log('\n❓ Seeding FAQ...');
const generalId = uuidv4();
const bookingId = uuidv4();
const general = await prisma.faqCategory.upsert({
where: { id: generalId },
update: {},
create: { id: generalId, title: 'General', iconKey: 'help' },
});
const booking = await prisma.faqCategory.upsert({
where: { id: bookingId },
update: {},
create: { id: bookingId, title: 'Booking', iconKey: 'book' },
});
await prisma.faqArticle.upsert({
where: { id: uuidv4() },
update: {},
create: { id: uuidv4(), categoryId: general.id, question: 'What is EDR?', answerMarkdown: 'Ethio-Djibouti Railway' },
});
await prisma.faqArticle.upsert({
where: { id: uuidv4() },
update: {},
create: { id: uuidv4(), categoryId: booking.id, question: 'How to book?', answerMarkdown: 'Use the booking system' },
});
console.log(` ✅ FAQ categories and articles created`);
}
async function seedFraudRules() {
console.log('\n🔐 Seeding fraud detection rules...');
const rules = [
{ type: 'RAPID_BOOKINGS', threshold: 10, enabled: true },
{ type: 'HIGH_VALUE_BOOKING', threshold: 500000, enabled: true },
{ type: 'UNUSUAL_DEVICE', threshold: 0.8, enabled: true },
];
for (const r of rules) {
await prisma.fraudRule.upsert({
where: { type: r.type },
update: {},
create: r,
});
}
console.log(`${rules.length} fraud detection rules created`);
}
// Run a seed step in isolation: if it throws (FK conflict, duplicate row,
// missing record, etc.) log the error and keep going so the rest of the seed —
// and the API startup that follows it — are never blocked by one bad step.
async function runStep(name: string, step: () => Promise<unknown>): Promise<boolean> {
try {
await step();
return true;
} catch (e) {
console.error(`⚠️ Seed step "${name}" failed — skipping and continuing:`, e);
return false;
}
}
async function main() {
console.log('🌱 Comprehensive EDR Seed Starting...\n');
const steps: Array<[string, () => Promise<unknown>]> = [
['system users', seedSystemUsers],
['stations', seedStations],
['coach types & classes', seedCoachTypesAndClasses],
['route', seedRoute],
['coaches', seedCoaches],
['trips', seedTrips],
['fare rules', seedFareRules],
['currency', seedCurrency],
['payment methods', seedPaymentMethods],
['notification templates', seedNotificationTemplates],
['menu & food', seedMenuAndFood],
['promotions', seedPromotions],
['FAQ', seedFAQ],
['fraud rules', seedFraudRules],
];
let failed = 0;
for (const [name, step] of steps) {
if (!(await runStep(name, step))) failed++;
}
if (failed > 0) {
console.warn(`\n⚠ Seed finished with ${failed}/${steps.length} step(s) failed (see logs above).\n`);
} else {
console.log('\n✅ Seed complete!\n');
}
console.log('🔑 System Users:');
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(' Supervisor: supervisor@edr-platform.com / supervisor123');
console.log(' Staff: staff@edr-platform.com / staff123');
}
main()
.catch((e) => {
// Intentionally do NOT process.exit(1): the docker entrypoint runs under
// `set -e`, so a non-zero exit here would abort container startup and the
// API would never boot. Log and exit cleanly instead.
console.error('❌ Seed crashed unexpectedly — continuing so the API can start:', e);
})
.finally(async () => {
await prisma.$disconnect();
});