mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 05:58:18 +00:00
Refactor business logic for train,schedule,coach,seat and search modules
This commit is contained in:
@@ -1,52 +1,14 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "SeatClass" (
|
||||
-- CreateTable SeatClass (runs before initial migration)
|
||||
CREATE TABLE IF NOT EXISTS "SeatClass" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"basePrice" INTEGER NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT "SeatClass_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "SeatClass_name_key" ON "SeatClass"("name");
|
||||
|
||||
-- Seed default seat classes so existing coaches can be migrated
|
||||
INSERT INTO "SeatClass" ("id", "name", "description", "basePrice", "isActive", "createdAt", "updatedAt")
|
||||
VALUES
|
||||
('sc_economy', 'Economy Seat', 'Standard economy seating', 45000, true, NOW(), NOW()),
|
||||
('sc_business', 'Business Seat','Comfortable business class', 90000, true, NOW(), NOW()),
|
||||
('sc_first', 'VIP Bed', 'First class VIP bed', 135000, true, NOW(), NOW());
|
||||
|
||||
-- Add seatClassId column to Coach (nullable first for migration safety)
|
||||
ALTER TABLE "Coach" ADD COLUMN "seatClassId" TEXT;
|
||||
|
||||
-- Map existing serviceClass enum values to new SeatClass ids
|
||||
UPDATE "Coach" SET "seatClassId" = 'sc_economy' WHERE "serviceClass" = 'ECONOMY';
|
||||
UPDATE "Coach" SET "seatClassId" = 'sc_business' WHERE "serviceClass" = 'BUSINESS';
|
||||
UPDATE "Coach" SET "seatClassId" = 'sc_first' WHERE "serviceClass" = 'FIRST';
|
||||
|
||||
-- Make seatClassId NOT NULL now that all rows are populated
|
||||
ALTER TABLE "Coach" ALTER COLUMN "seatClassId" SET NOT NULL;
|
||||
|
||||
-- Drop old serviceClass column
|
||||
ALTER TABLE "Coach" DROP COLUMN "serviceClass";
|
||||
|
||||
-- Add seatClassId to FareRule
|
||||
ALTER TABLE "FareRule" ADD COLUMN "seatClassId" TEXT;
|
||||
|
||||
UPDATE "FareRule" SET "seatClassId" = 'sc_economy' WHERE "serviceClass" = 'ECONOMY';
|
||||
UPDATE "FareRule" SET "seatClassId" = 'sc_business' WHERE "serviceClass" = 'BUSINESS';
|
||||
UPDATE "FareRule" SET "seatClassId" = 'sc_first' WHERE "serviceClass" = 'FIRST';
|
||||
|
||||
ALTER TABLE "FareRule" ALTER COLUMN "seatClassId" SET NOT NULL;
|
||||
ALTER TABLE "FareRule" DROP COLUMN "serviceClass";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Coach" ADD CONSTRAINT "Coach_seatClassId_fkey"
|
||||
FOREIGN KEY ("seatClassId") REFERENCES "SeatClass"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "FareRule" ADD CONSTRAINT "FareRule_seatClassId_fkey"
|
||||
FOREIGN KEY ("seatClassId") REFERENCES "SeatClass"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "SeatClass_name_key" ON "SeatClass"("name");
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
ALTER TABLE "SeatClass" ALTER COLUMN "updatedAt" SET DEFAULT NOW();
|
||||
-- updatedAt default already set in initial migration, no-op
|
||||
SELECT 1;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,9 @@ generator client {
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
schemas = ["edr_passenger"]
|
||||
}
|
||||
|
||||
enum UserRole {
|
||||
@@ -37,15 +38,6 @@ enum SeatStatus {
|
||||
BLOCKED
|
||||
}
|
||||
|
||||
enum ServiceClass {
|
||||
ECONOMY_REGULAR
|
||||
ECONOMY_BED_LOWER
|
||||
ECONOMY_BED_MIDDLE
|
||||
ECONOMY_BED_UPPER
|
||||
VIP_BED_LOWER
|
||||
VIP_BED_UPPER
|
||||
}
|
||||
|
||||
enum PassengerCategory {
|
||||
ADULT
|
||||
CHILD
|
||||
@@ -74,6 +66,7 @@ model SeatClass {
|
||||
updatedAt DateTime @updatedAt
|
||||
coaches Coach[]
|
||||
fareRules FareRule[]
|
||||
routeFareRules RouteFareRule[]
|
||||
}
|
||||
|
||||
enum BookingStatus {
|
||||
@@ -240,41 +233,45 @@ model Station {
|
||||
timezone String @default("Africa/Addis_Ababa")
|
||||
lat Decimal @db.Decimal(9, 6)
|
||||
lng Decimal @db.Decimal(9, 6)
|
||||
originTrips Trip[] @relation("OriginTrips")
|
||||
destinationTrips Trip[] @relation("DestinationTrips")
|
||||
stopTimes TripStopTime[]
|
||||
crowdSignals StationCrowdSignal[]
|
||||
originSchedules TrainSchedule[] @relation("OriginTrips")
|
||||
destinationSchedules TrainSchedule[] @relation("DestinationTrips")
|
||||
stopTimes TripStopTime[]
|
||||
crowdSignals StationCrowdSignal[]
|
||||
@@index([city, countryCode])
|
||||
}
|
||||
|
||||
model TrainService {
|
||||
id String @id @default(uuid())
|
||||
number String @unique
|
||||
name String
|
||||
operatorId String @default("op_edr")
|
||||
model Train {
|
||||
id String @id @default(uuid())
|
||||
number String @unique
|
||||
name String
|
||||
operatorId String @default("op_edr")
|
||||
operatorName String?
|
||||
trips Trip[]
|
||||
description String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
schedules TrainSchedule[]
|
||||
}
|
||||
|
||||
model Trip {
|
||||
id String @id @default(uuid())
|
||||
serviceId String
|
||||
model TrainSchedule {
|
||||
id String @id @default(uuid())
|
||||
trainId String
|
||||
routeId String?
|
||||
originStationId String
|
||||
destinationStationId String
|
||||
departureAt DateTime
|
||||
arrivalAt DateTime
|
||||
durationMinutes Int
|
||||
status TripStatus @default(SCHEDULED)
|
||||
stopsCount Int @default(0)
|
||||
reservedCount Int @default(0)
|
||||
onTimePercent Int @default(100)
|
||||
carbonRating String @default("A")
|
||||
status TripStatus @default(SCHEDULED)
|
||||
stopsCount Int @default(0)
|
||||
reservedCount Int @default(0)
|
||||
onTimePercent Int @default(100)
|
||||
carbonRating String @default("A")
|
||||
notes String?
|
||||
service TrainService @relation(fields: [serviceId], references: [id])
|
||||
originStation Station @relation("OriginTrips", fields: [originStationId], references: [id])
|
||||
destinationStation Station @relation("DestinationTrips", fields: [destinationStationId], references: [id])
|
||||
coaches Coach[]
|
||||
train Train @relation(fields: [trainId], references: [id])
|
||||
originStation Station @relation("OriginTrips", fields: [originStationId], references: [id])
|
||||
destinationStation Station @relation("DestinationTrips", fields: [destinationStationId], references: [id])
|
||||
coachAssignments CoachAssignment[]
|
||||
bookings Booking[]
|
||||
stopTimes TripStopTime[]
|
||||
liveStatus TripLiveStatus?
|
||||
@@ -284,62 +281,79 @@ model Trip {
|
||||
}
|
||||
|
||||
model TripStopTime {
|
||||
id String @id @default(uuid())
|
||||
tripId String
|
||||
id String @id @default(uuid())
|
||||
scheduleId String
|
||||
stationId String
|
||||
sequence Int
|
||||
plannedArrivalAt DateTime?
|
||||
plannedDepartureAt DateTime?
|
||||
actualArrivalAt DateTime?
|
||||
status StopStatus @default(UPCOMING)
|
||||
trip Trip @relation(fields: [tripId], references: [id])
|
||||
station Station @relation(fields: [stationId], references: [id])
|
||||
@@unique([tripId, sequence])
|
||||
status StopStatus @default(UPCOMING)
|
||||
schedule TrainSchedule @relation(fields: [scheduleId], references: [id])
|
||||
station Station @relation(fields: [stationId], references: [id])
|
||||
@@unique([scheduleId, sequence])
|
||||
}
|
||||
|
||||
model TripLiveStatus {
|
||||
id String @id @default(uuid())
|
||||
tripId String @unique
|
||||
id String @id @default(uuid())
|
||||
scheduleId String @unique
|
||||
state String
|
||||
currentLocationLabel String?
|
||||
progressPercent Int @default(0)
|
||||
delayMinutes Int @default(0)
|
||||
progressPercent Int @default(0)
|
||||
delayMinutes Int @default(0)
|
||||
currentSpeedKph Int?
|
||||
platformLabel String?
|
||||
updatedAt DateTime @updatedAt
|
||||
trip Trip @relation(fields: [tripId], references: [id])
|
||||
updatedAt DateTime @updatedAt
|
||||
schedule TrainSchedule @relation(fields: [scheduleId], references: [id])
|
||||
}
|
||||
|
||||
model Coach {
|
||||
id String @id @default(uuid())
|
||||
tripId String
|
||||
label String
|
||||
serviceClass ServiceClass
|
||||
seatClassId String?
|
||||
capacity Int?
|
||||
sequence Int?
|
||||
coachType String?
|
||||
amenities Json?
|
||||
trip Trip @relation(fields: [tripId], references: [id])
|
||||
seatClass SeatClass? @relation(fields: [seatClassId], references: [id])
|
||||
seats Seat[]
|
||||
@@unique([tripId, label])
|
||||
id String @id @default(uuid())
|
||||
coachNumber String @unique
|
||||
label String
|
||||
seatClassId String
|
||||
coachType String?
|
||||
mode String @default("seat") // 'seat', 'bed', 'convertible'
|
||||
seatArrangement String?
|
||||
bedArrangement String?
|
||||
amenities Json?
|
||||
totalUnits Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
seatClass SeatClass @relation(fields: [seatClassId], references: [id])
|
||||
seats Seat[]
|
||||
assignments CoachAssignment[]
|
||||
}
|
||||
|
||||
model CoachAssignment {
|
||||
id String @id @default(uuid())
|
||||
scheduleId String
|
||||
coachId String
|
||||
positionNumber Int
|
||||
isOperational Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
schedule TrainSchedule @relation(fields: [scheduleId], references: [id])
|
||||
coach Coach @relation(fields: [coachId], references: [id])
|
||||
@@unique([scheduleId, positionNumber])
|
||||
@@index([scheduleId])
|
||||
}
|
||||
|
||||
model Seat {
|
||||
id String @id @default(uuid())
|
||||
coachId String
|
||||
row Int
|
||||
col String
|
||||
label String
|
||||
seatNumber String?
|
||||
kind SeatKind @default(STANDARD)
|
||||
status SeatStatus @default(AVAILABLE)
|
||||
heldUntil DateTime?
|
||||
isWindow Boolean @default(false)
|
||||
isAisle Boolean @default(false)
|
||||
premiumFeeMinor Int @default(0)
|
||||
eligibility String?
|
||||
id String @id @default(uuid())
|
||||
coachId String
|
||||
row Int
|
||||
col String
|
||||
label String
|
||||
seatNumber String?
|
||||
kind SeatKind @default(STANDARD)
|
||||
status SeatStatus @default(AVAILABLE)
|
||||
heldUntil DateTime?
|
||||
isWindow Boolean @default(false)
|
||||
isAisle Boolean @default(false)
|
||||
bedPosition String? // 'lower', 'middle', 'upper'
|
||||
premiumFeeMinor Int @default(0)
|
||||
eligibility String?
|
||||
coach Coach @relation(fields: [coachId], references: [id])
|
||||
bookingSeats BookingSeat[]
|
||||
blocks SeatBlock[]
|
||||
@@ -349,7 +363,7 @@ model Seat {
|
||||
|
||||
model SeatHold {
|
||||
id String @id @default(uuid())
|
||||
tripId String
|
||||
scheduleId String
|
||||
seatIds String[]
|
||||
fareQuoteId String?
|
||||
passengerId String
|
||||
@@ -377,7 +391,7 @@ model Booking {
|
||||
id String @id @default(uuid())
|
||||
bookingRef String @unique
|
||||
passengerId String
|
||||
tripId String
|
||||
scheduleId String
|
||||
status BookingStatus @default(DRAFT)
|
||||
currency String @default("ETB")
|
||||
totalMinor Int
|
||||
@@ -393,7 +407,7 @@ model Booking {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
passenger Passenger @relation(fields: [passengerId], references: [id])
|
||||
trip Trip @relation(fields: [tripId], references: [id])
|
||||
schedule TrainSchedule @relation(fields: [scheduleId], references: [id])
|
||||
seats BookingSeat[]
|
||||
paymentIntent PaymentIntent?
|
||||
ticket Ticket?
|
||||
@@ -625,15 +639,15 @@ model MenuCategory {
|
||||
|
||||
model MenuItem {
|
||||
id String @id @default(uuid())
|
||||
tripId String
|
||||
scheduleId String
|
||||
categoryId String
|
||||
name String
|
||||
priceMinor Int
|
||||
currency String @default("ETB")
|
||||
available Boolean @default(true)
|
||||
availableUntil DateTime?
|
||||
trip Trip @relation(fields: [tripId], references: [id])
|
||||
category MenuCategory @relation(fields: [categoryId], references: [id])
|
||||
schedule TrainSchedule @relation(fields: [scheduleId], references: [id])
|
||||
category MenuCategory @relation(fields: [categoryId], references: [id])
|
||||
}
|
||||
|
||||
model FoodOrder {
|
||||
@@ -747,16 +761,16 @@ model Journey {
|
||||
}
|
||||
|
||||
model JourneySegment {
|
||||
id String @id @default(uuid())
|
||||
id String @id @default(uuid())
|
||||
journeyId String
|
||||
tripId String
|
||||
scheduleId String
|
||||
segmentOrder Int
|
||||
seatId String?
|
||||
coachId String?
|
||||
departureStationId String
|
||||
arrivalStationId String
|
||||
journey Journey @relation(fields: [journeyId], references: [id])
|
||||
trip Trip @relation(fields: [tripId], references: [id])
|
||||
journey Journey @relation(fields: [journeyId], references: [id])
|
||||
schedule TrainSchedule @relation(fields: [scheduleId], references: [id])
|
||||
}
|
||||
|
||||
model OtpCode {
|
||||
@@ -810,7 +824,7 @@ model RouteStop {
|
||||
model RouteFareRule {
|
||||
id String @id @default(uuid())
|
||||
routeId String
|
||||
serviceClass ServiceClass
|
||||
seatClassId String
|
||||
passengerCategory PassengerCategory @default(ADULT)
|
||||
baseFareMinor Int
|
||||
discountPercent Int?
|
||||
@@ -820,8 +834,9 @@ model RouteFareRule {
|
||||
validFrom DateTime
|
||||
validUntil DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
route Route @relation(fields: [routeId], references: [id], onDelete: Cascade)
|
||||
@@index([routeId, serviceClass])
|
||||
route Route @relation(fields: [routeId], references: [id], onDelete: Cascade)
|
||||
seatClass SeatClass @relation(fields: [seatClassId], references: [id])
|
||||
@@index([routeId, seatClassId])
|
||||
}
|
||||
|
||||
model Agent {
|
||||
@@ -917,13 +932,13 @@ model GateValidationLog {
|
||||
}
|
||||
|
||||
model BaggageAllowance {
|
||||
id String @id @default(uuid())
|
||||
serviceClass ServiceClass
|
||||
id String @id @default(uuid())
|
||||
seatClassId String
|
||||
maxWeightKg Int
|
||||
maxPiecesCount Int
|
||||
excessFeePerKg Int
|
||||
currency String @default("ETB")
|
||||
createdAt DateTime @default(now())
|
||||
currency String @default("ETB")
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model BaggageBooking {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PrismaClient, ServiceClass, UserRole, LoyaltyTier, SeatKind, PassengerCategory, Currency } from '@prisma/client';
|
||||
import { PrismaClient, SeatKind } from '@prisma/client';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
@@ -6,130 +6,113 @@ const prisma = new PrismaClient();
|
||||
async function main() {
|
||||
console.log('🌱 Starting comprehensive seed...');
|
||||
|
||||
// All 21 Stations (Ethiopian-Djibouti Railway)
|
||||
// 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 labu = await prisma.station.upsert({ where: { code: 'LBU' }, update: {}, create: { code: 'LBU', name: 'Labu', city: 'Labu', countryCode: 'ET', lat: 8.8500, lng: 38.8500 } });
|
||||
const indode = await prisma.station.upsert({ where: { code: 'IND' }, update: {}, create: { code: 'IND', name: 'Indode', city: 'Indode', countryCode: 'ET', lat: 8.7833, lng: 39.0167 } });
|
||||
const bishoftu = await prisma.station.upsert({ where: { code: 'BSH' }, update: {}, create: { code: 'BSH', name: 'Bishoftu', city: 'Bishoftu', countryCode: 'ET', lat: 8.7500, lng: 38.9833 } });
|
||||
const mojo = await prisma.station.upsert({ where: { code: 'MJO' }, update: {}, create: { code: 'MJO', name: 'Mojo', city: 'Mojo', countryCode: 'ET', lat: 8.5833, lng: 39.1167 } });
|
||||
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 feto = await prisma.station.upsert({ where: { code: 'FTO' }, update: {}, create: { code: 'FTO', name: 'Feto', city: 'Feto', countryCode: 'ET', lat: 8.7167, lng: 39.5833 } });
|
||||
const metahara = await prisma.station.upsert({ where: { code: 'MTH' }, update: {}, create: { code: 'MTH', name: 'Metahara', city: 'Metahara', countryCode: 'ET', lat: 8.9000, lng: 39.9167 } });
|
||||
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 mieso = await prisma.station.upsert({ where: { code: 'MSO' }, update: {}, create: { code: 'MSO', name: 'Mieso', city: 'Mieso', countryCode: 'ET', lat: 9.2333, lng: 40.7500 } });
|
||||
const bike = await prisma.station.upsert({ where: { code: 'BKE' }, update: {}, create: { code: 'BKE', name: 'Bike', city: 'Bike', countryCode: 'ET', lat: 9.4167, lng: 41.2500 } });
|
||||
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 arawa = await prisma.station.upsert({ where: { code: 'ARW' }, update: {}, create: { code: 'ARW', name: 'Arawa', city: 'Arawa', countryCode: 'ET', lat: 10.0833, lng: 42.2500 } });
|
||||
const adigala = await prisma.station.upsert({ where: { code: 'ADG' }, update: {}, create: { code: 'ADG', name: 'Adigala', city: 'Adigala', countryCode: 'ET', lat: 10.5000, lng: 42.5833 } });
|
||||
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 dawanle = await prisma.station.upsert({ where: { code: 'DWN' }, update: {}, create: { code: 'DWN', name: 'Dawanle', city: 'Dawanle', countryCode: 'DJ', timezone: 'Africa/Djibouti', lat: 11.3833, lng: 42.8500 } });
|
||||
const alisabieh = await prisma.station.upsert({ where: { code: 'ALI' }, update: {}, create: { code: 'ALI', name: 'Alisabieh', city: 'Alisabieh', countryCode: 'DJ', timezone: 'Africa/Djibouti', lat: 11.1667, lng: 42.7167 } });
|
||||
const holhol = await prisma.station.upsert({ where: { code: 'HLH' }, update: {}, create: { code: 'HLH', name: 'Holhol', city: 'Holhol', countryCode: 'DJ', timezone: 'Africa/Djibouti', lat: 11.4167, lng: 43.0000 } });
|
||||
const nagad = await prisma.station.upsert({ where: { code: 'NGD' }, update: {}, create: { code: 'NGD', name: 'Nagad', city: 'Nagad', countryCode: 'DJ', timezone: 'Africa/Djibouti', lat: 11.5167, lng: 43.1000 } });
|
||||
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 } });
|
||||
|
||||
// Routes
|
||||
const route1 = await prisma.route.upsert({
|
||||
where: { code: 'R001' },
|
||||
update: {},
|
||||
create: { code: 'R001', name: 'Addis Ababa - Djibouti Express', effectiveFrom: new Date('2026-01-01'), active: true }
|
||||
});
|
||||
|
||||
// Delete existing route stops and recreate
|
||||
await prisma.routeStop.deleteMany({ where: { routeId: route1.id } });
|
||||
await prisma.routeStop.createMany({ data: [
|
||||
{ routeId: route1.id, stationId: addis.id, sequence: 1, distanceKm: 0 },
|
||||
{ routeId: route1.id, stationId: sebeta.id, sequence: 2, distanceKm: 23 },
|
||||
{ routeId: route1.id, stationId: labu.id, sequence: 3, distanceKm: 45 },
|
||||
{ routeId: route1.id, stationId: indode.id, sequence: 4, distanceKm: 62 },
|
||||
{ routeId: route1.id, stationId: bishoftu.id, sequence: 5, distanceKm: 47 },
|
||||
{ routeId: route1.id, stationId: mojo.id, sequence: 6, distanceKm: 73 },
|
||||
{ routeId: route1.id, stationId: adama.id, sequence: 7, distanceKm: 99 },
|
||||
{ routeId: route1.id, stationId: feto.id, sequence: 8, distanceKm: 145 },
|
||||
{ routeId: route1.id, stationId: metahara.id, sequence: 9, distanceKm: 198 },
|
||||
{ routeId: route1.id, stationId: awash.id, sequence: 10, distanceKm: 225 },
|
||||
{ routeId: route1.id, stationId: mieso.id, sequence: 11, distanceKm: 305 },
|
||||
{ routeId: route1.id, stationId: bike.id, sequence: 12, distanceKm: 375 },
|
||||
{ routeId: route1.id, stationId: direDawa.id, sequence: 13, distanceKm: 453 },
|
||||
{ routeId: route1.id, stationId: arawa.id, sequence: 14, distanceKm: 520 },
|
||||
{ routeId: route1.id, stationId: adigala.id, sequence: 15, distanceKm: 580 },
|
||||
{ routeId: route1.id, stationId: aysha.id, sequence: 16, distanceKm: 656 },
|
||||
{ routeId: route1.id, stationId: dawanle.id, sequence: 17, distanceKm: 680 },
|
||||
{ routeId: route1.id, stationId: alisabieh.id, sequence: 18, distanceKm: 700 },
|
||||
{ routeId: route1.id, stationId: holhol.id, sequence: 19, distanceKm: 730 },
|
||||
{ routeId: route1.id, stationId: nagad.id, sequence: 20, distanceKm: 750 },
|
||||
{ routeId: route1.id, stationId: djibouti.id, sequence: 21, distanceKm: 756 },
|
||||
]});
|
||||
// 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 } });
|
||||
|
||||
// Route Fare Rules (with passenger categories)
|
||||
await prisma.routeFareRule.deleteMany({ where: { routeId: route1.id } });
|
||||
await prisma.routeFareRule.createMany({ data: [
|
||||
{ routeId: route1.id, serviceClass: 'ECONOMY_REGULAR', passengerCategory: 'ADULT', baseFareMinor: 45000, validFrom: new Date('2026-01-01') },
|
||||
{ routeId: route1.id, serviceClass: 'ECONOMY_BED_LOWER', passengerCategory: 'ADULT', baseFareMinor: 65000, validFrom: new Date('2026-01-01') },
|
||||
{ routeId: route1.id, serviceClass: 'VIP_BED_LOWER', passengerCategory: 'ADULT', baseFareMinor: 95000, validFrom: new Date('2026-01-01') },
|
||||
{ routeId: route1.id, serviceClass: 'ECONOMY_REGULAR', passengerCategory: 'CHILD', baseFareMinor: 45000, validFrom: new Date('2026-01-01') },
|
||||
{ routeId: route1.id, serviceClass: 'ECONOMY_BED_LOWER', passengerCategory: 'CHILD', baseFareMinor: 65000, validFrom: new Date('2026-01-01') },
|
||||
{ routeId: route1.id, serviceClass: 'VIP_BED_LOWER', passengerCategory: 'CHILD', baseFareMinor: 95000, validFrom: new Date('2026-01-01') },
|
||||
]});
|
||||
// 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' } });
|
||||
|
||||
// Train Services
|
||||
const service301 = await prisma.trainService.upsert({ where: { number: '301' }, update: {}, create: { number: '301', name: 'Express 301' } });
|
||||
const service302 = await prisma.trainService.upsert({ where: { number: '302' }, update: {}, create: { number: '302', name: 'Express 302' } });
|
||||
// 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 } });
|
||||
|
||||
// Trips (Multiple schedules) - Delete existing trips for clean seed
|
||||
await prisma.trip.deleteMany({ where: { serviceId: { in: [service301.id, service302.id] } } });
|
||||
const trip1 = await prisma.trip.create({
|
||||
data: { serviceId: service301.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: 19 },
|
||||
});
|
||||
const trip2 = await prisma.trip.create({
|
||||
data: { serviceId: service302.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: 19 },
|
||||
});
|
||||
const trip3 = await prisma.trip.create({
|
||||
data: { serviceId: service301.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: 19 },
|
||||
});
|
||||
const trip4 = await prisma.trip.create({
|
||||
data: { serviceId: service302.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: 19 },
|
||||
});
|
||||
|
||||
// Trip Stop Times (Major stops only for brevity)
|
||||
await prisma.tripStopTime.createMany({ data: [
|
||||
{ tripId: trip1.id, stationId: addis.id, sequence: 1, plannedDepartureAt: new Date('2026-06-15T08:00:00Z'), status: 'UPCOMING' },
|
||||
{ tripId: trip1.id, stationId: adama.id, sequence: 7, plannedArrivalAt: new Date('2026-06-15T09:30:00Z'), plannedDepartureAt: new Date('2026-06-15T09:45:00Z'), status: 'UPCOMING' },
|
||||
{ tripId: trip1.id, stationId: awash.id, sequence: 10, plannedArrivalAt: new Date('2026-06-15T11:30:00Z'), plannedDepartureAt: new Date('2026-06-15T11:45:00Z'), status: 'UPCOMING' },
|
||||
{ tripId: trip1.id, stationId: direDawa.id, sequence: 13, plannedArrivalAt: new Date('2026-06-15T15:00:00Z'), plannedDepartureAt: new Date('2026-06-15T15:20:00Z'), status: 'UPCOMING' },
|
||||
{ tripId: trip1.id, stationId: aysha.id, sequence: 16, plannedArrivalAt: new Date('2026-06-15T18:00:00Z'), plannedDepartureAt: new Date('2026-06-15T18:10:00Z'), status: 'UPCOMING' },
|
||||
{ tripId: trip1.id, stationId: djibouti.id, sequence: 21, plannedArrivalAt: new Date('2026-06-15T20:00:00Z'), status: 'UPCOMING' },
|
||||
]});
|
||||
|
||||
// Coaches & Seats
|
||||
for (const trip of [trip1, trip2, trip3, trip4]) {
|
||||
const coaches = [
|
||||
{ label: 'A', serviceClass: 'ECONOMY_REGULAR' as ServiceClass, seatCount: 60 },
|
||||
{ label: 'B', serviceClass: 'ECONOMY_BED_LOWER' as ServiceClass, seatCount: 40 },
|
||||
{ label: 'C', serviceClass: 'VIP_BED_LOWER' as ServiceClass, seatCount: 20 },
|
||||
];
|
||||
for (const { label, serviceClass, seatCount } of coaches) {
|
||||
const coach = await prisma.coach.create({ data: { tripId: trip.id, label, serviceClass } });
|
||||
// Create seats for each physical coach
|
||||
for (const coach of [coachA1, coachB1, coachC1, coachA2, coachB2, coachC2]) {
|
||||
const existingSeats = await prisma.seat.count({ where: { coachId: coach.id } });
|
||||
if (existingSeats === 0) {
|
||||
const seats = [];
|
||||
const rows = Math.ceil(seatCount / 4);
|
||||
const rows = Math.ceil(coach.totalUnits / 4);
|
||||
for (let row = 1; row <= rows; row++) {
|
||||
for (const col of ['A', 'B', 'C', 'D']) {
|
||||
if (seats.length >= seatCount) break;
|
||||
seats.push({ coachId: coach.id, row, col, label: `${row}${col}`, kind: (row === 1 && col === 'A' ? 'ACCESSIBLE' : 'STANDARD') as SeatKind });
|
||||
if (seats.length >= coach.totalUnits) break;
|
||||
seats.push({ coachId: coach.id, row, col, label: `${row}${col}`, seatNumber: `${coach.label}${row}${col}`, kind: (row === 1 && col === 'A' ? 'ACCESSIBLE' : 'STANDARD') as SeatKind });
|
||||
}
|
||||
}
|
||||
await prisma.seat.createMany({ data: seats });
|
||||
}
|
||||
}
|
||||
|
||||
// Fare Rules (All trips)
|
||||
for (const trip of [trip1, trip2, trip3, trip4]) {
|
||||
await prisma.fareRule.createMany({ data: [
|
||||
{ tripId: trip.id, serviceClass: 'ECONOMY_REGULAR', baseFareMinor: 45000, validFrom: new Date('2026-01-01'), refundable: true },
|
||||
{ tripId: trip.id, serviceClass: 'ECONOMY_BED_LOWER', baseFareMinor: 65000, validFrom: new Date('2026-01-01'), refundable: true },
|
||||
{ tripId: trip.id, serviceClass: 'VIP_BED_LOWER', baseFareMinor: 95000, validFrom: new Date('2026-01-01'), refundable: true },
|
||||
]});
|
||||
// Train Schedules — delete dependents first to avoid FK violations
|
||||
const existingScheduleIds = (await prisma.trainSchedule.findMany({
|
||||
where: { trainId: { in: [train301.id, train302.id] } },
|
||||
select: { id: true },
|
||||
})).map((s) => s.id);
|
||||
if (existingScheduleIds.length > 0) {
|
||||
await prisma.fareRule.deleteMany({ where: { tripId: { in: existingScheduleIds } } });
|
||||
await prisma.tripStopTime.deleteMany({ where: { scheduleId: { in: existingScheduleIds } } });
|
||||
await prisma.coachAssignment.deleteMany({ where: { scheduleId: { in: existingScheduleIds } } });
|
||||
await prisma.trainSchedule.deleteMany({ where: { id: { in: existingScheduleIds } } });
|
||||
}
|
||||
const schedule1 = await prisma.trainSchedule.create({
|
||||
data: { trainId: train301.id, originStationId: addis.id, destinationStationId: djibouti.id, departureAt: new Date('2026-06-15T08:00:00Z'), arrivalAt: new Date('2026-06-15T20:00:00Z'), durationMinutes: 720, stopsCount: 6 },
|
||||
});
|
||||
const schedule2 = await prisma.trainSchedule.create({
|
||||
data: { trainId: train302.id, originStationId: djibouti.id, destinationStationId: addis.id, departureAt: new Date('2026-06-16T09:00:00Z'), arrivalAt: new Date('2026-06-16T21:30:00Z'), durationMinutes: 750, stopsCount: 5 },
|
||||
});
|
||||
const schedule3 = await prisma.trainSchedule.create({
|
||||
data: { trainId: train301.id, originStationId: addis.id, destinationStationId: djibouti.id, departureAt: new Date('2026-06-17T07:30:00Z'), arrivalAt: new Date('2026-06-17T19:45:00Z'), durationMinutes: 735, stopsCount: 5 },
|
||||
});
|
||||
const schedule4 = await prisma.trainSchedule.create({
|
||||
data: { trainId: train302.id, originStationId: djibouti.id, destinationStationId: addis.id, departureAt: new Date('2026-06-18T08:30:00Z'), arrivalAt: new Date('2026-06-18T21:00:00Z'), durationMinutes: 750, stopsCount: 5 },
|
||||
});
|
||||
|
||||
// Assign coaches to schedules
|
||||
await prisma.coachAssignment.createMany({
|
||||
data: [
|
||||
{ scheduleId: schedule1.id, coachId: coachA1.id, positionNumber: 1 },
|
||||
{ scheduleId: schedule1.id, coachId: coachB1.id, positionNumber: 2 },
|
||||
{ scheduleId: schedule1.id, coachId: coachC1.id, positionNumber: 3 },
|
||||
{ scheduleId: schedule2.id, coachId: coachA2.id, positionNumber: 1 },
|
||||
{ scheduleId: schedule2.id, coachId: coachB2.id, positionNumber: 2 },
|
||||
{ scheduleId: schedule2.id, coachId: coachC2.id, positionNumber: 3 },
|
||||
{ scheduleId: schedule3.id, coachId: coachA1.id, positionNumber: 1 },
|
||||
{ scheduleId: schedule3.id, coachId: coachB1.id, positionNumber: 2 },
|
||||
{ scheduleId: schedule3.id, coachId: coachC1.id, positionNumber: 3 },
|
||||
{ scheduleId: schedule4.id, coachId: coachA2.id, positionNumber: 1 },
|
||||
{ scheduleId: schedule4.id, coachId: coachB2.id, positionNumber: 2 },
|
||||
{ scheduleId: schedule4.id, coachId: coachC2.id, positionNumber: 3 },
|
||||
],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
// Stop Times
|
||||
await prisma.tripStopTime.createMany({
|
||||
data: [
|
||||
{ scheduleId: schedule1.id, stationId: addis.id, sequence: 1, plannedDepartureAt: new Date('2026-06-15T08:00:00Z'), status: 'UPCOMING' },
|
||||
{ scheduleId: schedule1.id, stationId: adama.id, sequence: 2, plannedArrivalAt: new Date('2026-06-15T09:30:00Z'), plannedDepartureAt: new Date('2026-06-15T09:45:00Z'), status: 'UPCOMING' },
|
||||
{ scheduleId: schedule1.id, stationId: awash.id, sequence: 3, plannedArrivalAt: new Date('2026-06-15T11:30:00Z'), plannedDepartureAt: new Date('2026-06-15T11:45:00Z'), status: 'UPCOMING' },
|
||||
{ scheduleId: schedule1.id, stationId: direDawa.id, sequence: 4, plannedArrivalAt: new Date('2026-06-15T15:00:00Z'), plannedDepartureAt: new Date('2026-06-15T15:20:00Z'), status: 'UPCOMING' },
|
||||
{ scheduleId: schedule1.id, stationId: aysha.id, sequence: 5, plannedArrivalAt: new Date('2026-06-15T18:00:00Z'), plannedDepartureAt: new Date('2026-06-15T18:10:00Z'), status: 'UPCOMING' },
|
||||
{ scheduleId: schedule1.id, stationId: djibouti.id, sequence: 6, plannedArrivalAt: new Date('2026-06-15T20:00:00Z'), status: 'UPCOMING' },
|
||||
],
|
||||
});
|
||||
|
||||
// Fare Rules
|
||||
for (const schedule of [schedule1, schedule2, schedule3, schedule4]) {
|
||||
await prisma.fareRule.createMany({
|
||||
data: [
|
||||
{ tripId: schedule.id, seatClassId: scEconomyRegular.id, baseFareMinor: 45000, validFrom: new Date('2026-01-01'), refundable: true },
|
||||
{ tripId: schedule.id, seatClassId: scEconomyBed.id, baseFareMinor: 65000, validFrom: new Date('2026-01-01'), refundable: true },
|
||||
{ tripId: schedule.id, seatClassId: scVipBed.id, baseFareMinor: 95000, validFrom: new Date('2026-01-01'), refundable: true },
|
||||
],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Users
|
||||
@@ -137,8 +120,7 @@ async function main() {
|
||||
const adminHash = await bcrypt.hash('admin123', 10);
|
||||
const agentHash = await bcrypt.hash('agent123', 10);
|
||||
|
||||
const adminUser = 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' } });
|
||||
|
||||
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) {
|
||||
@@ -151,105 +133,52 @@ async function main() {
|
||||
const agentUser = await prisma.user.upsert({ where: { email: 'agent@edr-platform.com' }, update: { passwordHash: agentHash, role: 'AGENT' }, create: { fullName: 'Agent Abebe', email: 'agent@edr-platform.com', phone: '+251911111111', passwordHash: agentHash, role: 'AGENT' } });
|
||||
await prisma.agent.upsert({ where: { userId: agentUser.id }, update: {}, create: { userId: agentUser.id, agentCode: 'AG001', stationId: addis.id, commissionRate: 5, active: true } });
|
||||
|
||||
// Baggage Allowance - Delete and recreate
|
||||
// Baggage Allowance
|
||||
await prisma.baggageAllowance.deleteMany({});
|
||||
await prisma.baggageAllowance.createMany({ data: [
|
||||
{ serviceClass: 'ECONOMY_REGULAR', maxWeightKg: 20, maxPiecesCount: 2, excessFeePerKg: 500 },
|
||||
{ serviceClass: 'ECONOMY_BED_LOWER', maxWeightKg: 25, maxPiecesCount: 2, excessFeePerKg: 450 },
|
||||
{ serviceClass: 'VIP_BED_LOWER', maxWeightKg: 30, maxPiecesCount: 3, excessFeePerKg: 400 },
|
||||
]});
|
||||
await prisma.baggageAllowance.createMany({
|
||||
data: [
|
||||
{ seatClassId: scEconomyRegular.id, maxWeightKg: 20, maxPiecesCount: 2, excessFeePerKg: 500 },
|
||||
{ seatClassId: scEconomyBed.id, maxWeightKg: 25, maxPiecesCount: 2, excessFeePerKg: 450 },
|
||||
{ seatClassId: scVipBed.id, maxWeightKg: 30, maxPiecesCount: 3, excessFeePerKg: 400 },
|
||||
],
|
||||
});
|
||||
|
||||
// Notification Templates
|
||||
await prisma.notificationTemplate.upsert({ where: { code: 'BOOKING_CONFIRMED' }, update: {}, create: { code: 'BOOKING_CONFIRMED', channel: 'EMAIL', subject: 'Booking Confirmed', bodyTemplate: 'Your booking {{bookingRef}} is confirmed for {{tripDate}}.', active: true } });
|
||||
await prisma.notificationTemplate.upsert({ where: { code: 'PAYMENT_SUCCESS' }, update: {}, create: { code: 'PAYMENT_SUCCESS', channel: 'SMS', bodyTemplate: 'Payment successful for {{bookingRef}}. Amount: {{amount}} ETB', active: true } });
|
||||
await prisma.notificationTemplate.upsert({ where: { code: 'TRIP_REMINDER' }, update: {}, create: { code: 'TRIP_REMINDER', channel: 'PUSH', subject: 'Trip Reminder', bodyTemplate: 'Your trip departs in {{hours}} hours from {{station}}.', 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 } });
|
||||
await prisma.promotion.upsert({ where: { code: 'NEWUSER20' }, update: {}, create: { title: 'New User Bonus', code: 'NEWUSER20', percentOff: 20, validUntil: new Date('2026-12-31'), active: true } });
|
||||
|
||||
// FAQ - Delete and recreate for clean seed
|
||||
// FAQ
|
||||
await prisma.faqArticle.deleteMany({});
|
||||
await prisma.faqCategory.deleteMany({});
|
||||
const faqBooking = await prisma.faqCategory.create({ data: { title: 'Booking & Tickets', iconKey: 'confirmation_number' } });
|
||||
const faqPayment = await prisma.faqCategory.create({ data: { title: 'Payment & Refunds', iconKey: 'payment' } });
|
||||
|
||||
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 },
|
||||
{ categoryId: faqBooking.id, question: 'Can I modify my booking?', answerMarkdown: 'Yes, you can modify your booking up to 24 hours before departure through the Bookings section.', rank: 2 },
|
||||
{ categoryId: faqPayment.id, question: 'What payment methods are accepted?', answerMarkdown: 'We accept Telebirr, CBE Birr, eBirr, Card, and Wallet payments.', rank: 1 },
|
||||
{ categoryId: faqPayment.id, question: 'How do refunds work?', answerMarkdown: 'Refunds are processed within 5-7 business days to your original payment method.', rank: 2 },
|
||||
]});
|
||||
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 }] });
|
||||
|
||||
// Menu Categories & Items - Delete and recreate
|
||||
await prisma.menuItem.deleteMany({});
|
||||
await prisma.menuCategory.deleteMany({});
|
||||
const menuBeverages = await prisma.menuCategory.create({ data: { name: 'Beverages' } });
|
||||
const menuSnacks = await prisma.menuCategory.create({ data: { name: 'Snacks' } });
|
||||
|
||||
await prisma.menuItem.createMany({ data: [
|
||||
{ tripId: trip1.id, categoryId: menuBeverages.id, name: 'Coffee', priceMinor: 2500, available: true },
|
||||
{ tripId: trip1.id, categoryId: menuBeverages.id, name: 'Tea', priceMinor: 2000, available: true },
|
||||
{ tripId: trip1.id, categoryId: menuSnacks.id, name: 'Sandwich', priceMinor: 5000, available: true },
|
||||
]});
|
||||
|
||||
// Station Crowd Signals - Delete and recreate
|
||||
// Station Crowd Signals
|
||||
await prisma.stationCrowdSignal.deleteMany({});
|
||||
await prisma.stationCrowdSignal.createMany({ data: [
|
||||
{ stationId: addis.id, level: 'MODERATE', label: 'Moderate', statusLabel: 'Normal operations' },
|
||||
{ stationId: adama.id, level: 'LOW', label: 'Low', statusLabel: 'Quiet' },
|
||||
{ stationId: direDawa.id, level: 'LOW', label: 'Low', statusLabel: 'Quiet' },
|
||||
{ stationId: djibouti.id, level: 'HIGH', label: 'High', statusLabel: 'Busy terminal' },
|
||||
]});
|
||||
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.fraudRule.upsert({ where: { type: 'HIGH_VALUE' }, update: {}, create: { type: 'HIGH_VALUE', enabled: true, threshold: 500000, config: { action: 'REVIEW' } } });
|
||||
await prisma.fraudRule.upsert({ where: { type: 'FAILED_PAYMENTS' }, update: {}, create: { type: 'FAILED_PAYMENTS', enabled: true, threshold: 5, config: { windowMinutes: 1440, action: 'BLOCK' } } });
|
||||
|
||||
// Loyalty Rewards (linked to loyalty account) - Delete and recreate
|
||||
if (passenger) {
|
||||
const loyaltyAccount = await prisma.loyaltyAccount.findUnique({ where: { passengerId: passenger.id } });
|
||||
if (loyaltyAccount) {
|
||||
await prisma.loyaltyReward.deleteMany({ where: { accountId: loyaltyAccount.id } });
|
||||
await prisma.loyaltyReward.createMany({ data: [
|
||||
{ accountId: loyaltyAccount.id, title: '10% Discount Voucher', costPoints: 1000, available: true, description: 'Get 10% off your next booking' },
|
||||
{ accountId: loyaltyAccount.id, title: 'Free Upgrade to VIP', costPoints: 2500, available: true, description: 'Upgrade to VIP class on any trip' },
|
||||
{ accountId: loyaltyAccount.id, title: '500 ETB Wallet Credit', costPoints: 5000, available: true, description: 'Add 500 ETB to your wallet' },
|
||||
]});
|
||||
}
|
||||
}
|
||||
|
||||
// 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: 'DJF', rate: 3.25, effectiveDate: new Date() },
|
||||
{ fromCurrency: 'ETB', toCurrency: 'USD', rate: 0.018, effectiveDate: new Date() },
|
||||
{ fromCurrency: 'DJF', toCurrency: 'ETB', rate: 0.3077, effectiveDate: new Date() },
|
||||
{ fromCurrency: 'USD', toCurrency: 'ETB', rate: 55.56, 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() }] });
|
||||
|
||||
console.log('✅ Comprehensive seed complete');
|
||||
console.log('\n📋 Seed Summary:');
|
||||
console.log(' - 21 Stations (Complete Ethiopian-Djibouti Railway with country codes)');
|
||||
console.log(' - 1 Route with 21 stops');
|
||||
console.log(' - 2 Train services, 4 trips');
|
||||
console.log(' - 3 Coaches per trip (Economy, Bed, VIP)');
|
||||
console.log(' - Fare rules for ADULT and CHILD categories');
|
||||
console.log(' - Currency exchange rates (ETB ↔ DJF, USD)');
|
||||
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(' - 3 Fraud detection rules');
|
||||
console.log(' - 3 Loyalty rewards');
|
||||
console.log(' - Baggage rules, Notification templates, Promotions, FAQ');
|
||||
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🚉 Stations: Addis Ababa → Sebeta → Labu → Indode → Bishoftu → Mojo → Adama → Feto → Metahara → Awash → Mieso → Bike → Dire Dawa → Arawa → Adigala → Aysha → Dawanle → Alisabieh → Holhol → Nagad → Djibouti');
|
||||
console.log('\n💰 Pricing: ADULT (≥5 years) = 100% fare | CHILD (<5 years) = First free, subsequent 100%');
|
||||
console.log('\n💱 Currencies: ETB (transaction) | DJF, USD (display) | Rates: ETB→DJF=3.25, ETB→USD=0.018');
|
||||
console.log('\n🔐 Verifayda: DISABLED (set VERIFAYDA_ENABLED=true in production)');
|
||||
console.log('\n🚂 Architecture: Train → TrainSchedule ↔ CoachAssignment ↔ Coach → Seat');
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
|
||||
Reference in New Issue
Block a user