Refactored the whole app based on the requirements shared

This commit is contained in:
Stephanos A
2026-05-21 08:48:28 +03:00
parent 2dc3da9e74
commit 51bc906792
84 changed files with 6880 additions and 12659 deletions

View File

@@ -1,31 +0,0 @@
-- CreateTable
CREATE TABLE "Journey" (
"id" TEXT NOT NULL,
"passengerId" TEXT NOT NULL,
"status" TEXT NOT NULL,
"totalMinor" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'ETB',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Journey_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "JourneySegment" (
"id" TEXT NOT NULL,
"journeyId" TEXT NOT NULL,
"tripId" TEXT NOT NULL,
"segmentOrder" INTEGER NOT NULL,
"seatId" TEXT,
"coachId" TEXT,
"departureStationId" TEXT NOT NULL,
"arrivalStationId" TEXT NOT NULL,
CONSTRAINT "JourneySegment_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "JourneySegment" ADD CONSTRAINT "JourneySegment_journeyId_fkey" FOREIGN KEY ("journeyId") REFERENCES "Journey"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "JourneySegment" ADD CONSTRAINT "JourneySegment_tripId_fkey" FOREIGN KEY ("tripId") REFERENCES "Trip"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,63 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[merchantOrderId]` on the table `PaymentIntent` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "PaymentIntent" ADD COLUMN "expiresAt" TIMESTAMP(3),
ADD COLUMN "failureCode" TEXT,
ADD COLUMN "failureMessage" TEXT,
ADD COLUMN "merchantOrderId" TEXT,
ADD COLUMN "paidAt" TIMESTAMP(3),
ADD COLUMN "providerOrderId" TEXT,
ADD COLUMN "providerTxnId" TEXT,
ADD COLUMN "rawInitiation" JSONB;
-- CreateTable
CREATE TABLE "PaymentWebhookEvent" (
"id" TEXT NOT NULL,
"provider" "PaymentMethodType" NOT NULL,
"externalEventId" TEXT NOT NULL,
"merchantOrderId" TEXT,
"providerTxnId" TEXT,
"signatureValid" BOOLEAN NOT NULL,
"status" TEXT NOT NULL,
"payload" JSONB NOT NULL,
"receivedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"processedAt" TIMESTAMP(3),
"processingError" TEXT,
CONSTRAINT "PaymentWebhookEvent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PaymentRefund" (
"id" TEXT NOT NULL,
"paymentIntentId" TEXT NOT NULL,
"amountMinor" INTEGER NOT NULL,
"reason" TEXT,
"providerRefundId" TEXT,
"status" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PaymentRefund_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "PaymentWebhookEvent_merchantOrderId_idx" ON "PaymentWebhookEvent"("merchantOrderId");
-- CreateIndex
CREATE UNIQUE INDEX "PaymentWebhookEvent_provider_externalEventId_key" ON "PaymentWebhookEvent"("provider", "externalEventId");
-- CreateIndex
CREATE UNIQUE INDEX "PaymentIntent_merchantOrderId_key" ON "PaymentIntent"("merchantOrderId");
-- CreateIndex
CREATE INDEX "PaymentIntent_providerOrderId_idx" ON "PaymentIntent"("providerOrderId");
-- CreateIndex
CREATE INDEX "PaymentIntent_providerTxnId_idx" ON "PaymentIntent"("providerTxnId");
-- AddForeignKey
ALTER TABLE "PaymentRefund" ADD CONSTRAINT "PaymentRefund_paymentIntentId_fkey" FOREIGN KEY ("paymentIntentId") REFERENCES "PaymentIntent"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,5 +1,5 @@
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('PASSENGER', 'ADMIN', 'STAFF');
CREATE TYPE "UserRole" AS ENUM ('PASSENGER', 'AGENT', 'SUPERVISOR', 'ADMIN', 'STAFF');
-- CreateEnum
CREATE TYPE "TripStatus" AS ENUM ('SCHEDULED', 'BOARDING', 'EN_ROUTE', 'ARRIVED', 'CANCELLED', 'DELAYED');
@@ -11,16 +11,16 @@ CREATE TYPE "SeatKind" AS ENUM ('STANDARD', 'PREMIUM', 'ACCESSIBLE');
CREATE TYPE "SeatStatus" AS ENUM ('AVAILABLE', 'HELD', 'BOOKED', 'BLOCKED');
-- CreateEnum
CREATE TYPE "ServiceClass" AS ENUM ('ECONOMY', 'BUSINESS', 'FIRST');
CREATE TYPE "ServiceClass" AS ENUM ('ECONOMY_REGULAR', 'ECONOMY_BED_LOWER', 'ECONOMY_BED_MIDDLE', 'ECONOMY_BED_UPPER', 'VIP_BED_LOWER', 'VIP_BED_UPPER');
-- CreateEnum
CREATE TYPE "BookingStatus" AS ENUM ('DRAFT', 'PENDING_PAYMENT', 'CONFIRMED', 'CANCELLED', 'COMPLETED', 'NO_SHOW');
CREATE TYPE "BookingStatus" AS ENUM ('DRAFT', 'PENDING_PAYMENT', 'CONFIRMED', 'CANCELLED', 'COMPLETED', 'NO_SHOW', 'REFUNDED');
-- CreateEnum
CREATE TYPE "PaymentMethodType" AS ENUM ('TELEBIRR', 'CBE_BIRR', 'EBIRR', 'CARD', 'WALLET');
-- CreateEnum
CREATE TYPE "PaymentIntentStatus" AS ENUM ('REQUIRES_ACTION', 'PROCESSING', 'SUCCEEDED', 'FAILED', 'CANCELLED');
CREATE TYPE "PaymentIntentStatus" AS ENUM ('REQUIRES_ACTION', 'PROCESSING', 'SUCCEEDED', 'FAILED', 'CANCELLED', 'REFUNDED');
-- CreateEnum
CREATE TYPE "WalletLedgerType" AS ENUM ('CREDIT', 'DEBIT');
@@ -57,6 +57,11 @@ CREATE TABLE "User" (
"fullName" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"role" "UserRole" NOT NULL DEFAULT 'PASSENGER',
"nationality" TEXT,
"passportNumber" TEXT,
"nationalId" TEXT,
"failedLoginAttempts" INTEGER NOT NULL DEFAULT 0,
"lockedUntil" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
@@ -69,6 +74,9 @@ CREATE TABLE "Session" (
"userId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"ipAddress" TEXT,
"userAgent" TEXT,
"lastActivityAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
@@ -186,6 +194,8 @@ CREATE TABLE "Seat" (
"kind" "SeatKind" NOT NULL DEFAULT 'STANDARD',
"status" "SeatStatus" NOT NULL DEFAULT 'AVAILABLE',
"heldUntil" TIMESTAMP(3),
"premiumFeeMinor" INTEGER NOT NULL DEFAULT 0,
"eligibility" TEXT,
CONSTRAINT "Seat_pkey" PRIMARY KEY ("id")
);
@@ -228,6 +238,7 @@ CREATE TABLE "Booking" (
"status" "BookingStatus" NOT NULL DEFAULT 'DRAFT',
"currency" TEXT NOT NULL DEFAULT 'ETB',
"totalMinor" INTEGER NOT NULL,
"bookingType" TEXT NOT NULL DEFAULT 'ONE_WAY',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
@@ -269,12 +280,50 @@ CREATE TABLE "PaymentIntent" (
"status" "PaymentIntentStatus" NOT NULL DEFAULT 'REQUIRES_ACTION',
"providerRef" TEXT,
"clientAction" JSONB,
"merchantOrderId" TEXT,
"providerOrderId" TEXT,
"providerTxnId" TEXT,
"rawInitiation" JSONB,
"paidAt" TIMESTAMP(3),
"failureCode" TEXT,
"failureMessage" TEXT,
"expiresAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PaymentIntent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PaymentWebhookEvent" (
"id" TEXT NOT NULL,
"provider" "PaymentMethodType" NOT NULL,
"externalEventId" TEXT NOT NULL,
"merchantOrderId" TEXT,
"providerTxnId" TEXT,
"signatureValid" BOOLEAN NOT NULL,
"status" TEXT NOT NULL,
"payload" JSONB NOT NULL,
"receivedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"processedAt" TIMESTAMP(3),
"processingError" TEXT,
CONSTRAINT "PaymentWebhookEvent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PaymentRefund" (
"id" TEXT NOT NULL,
"paymentIntentId" TEXT NOT NULL,
"amountMinor" INTEGER NOT NULL,
"reason" TEXT,
"providerRefundId" TEXT,
"status" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PaymentRefund_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Ticket" (
"id" TEXT NOT NULL,
@@ -282,6 +331,9 @@ CREATE TABLE "Ticket" (
"bookingRef" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'CONFIRMED',
"qrPayload" TEXT NOT NULL,
"barcodePayload" TEXT,
"pdfUrl" TEXT,
"deliveryChannel" TEXT NOT NULL DEFAULT 'EMAIL',
"issuedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"validatedAt" TIMESTAMP(3),
"validatorId" TEXT,
@@ -508,6 +560,7 @@ CREATE TABLE "UserPreferences" (
"dataSharing" BOOLEAN NOT NULL DEFAULT false,
"locale" TEXT NOT NULL DEFAULT 'en',
"darkMode" BOOLEAN NOT NULL DEFAULT false,
"language" TEXT NOT NULL DEFAULT 'en',
CONSTRAINT "UserPreferences_pkey" PRIMARY KEY ("id")
);
@@ -539,6 +592,279 @@ CREATE TABLE "SavedRoute" (
CONSTRAINT "SavedRoute_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Journey" (
"id" TEXT NOT NULL,
"passengerId" TEXT NOT NULL,
"status" TEXT NOT NULL,
"totalMinor" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'ETB',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Journey_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "JourneySegment" (
"id" TEXT NOT NULL,
"journeyId" TEXT NOT NULL,
"tripId" TEXT NOT NULL,
"segmentOrder" INTEGER NOT NULL,
"seatId" TEXT,
"coachId" TEXT,
"departureStationId" TEXT NOT NULL,
"arrivalStationId" TEXT NOT NULL,
CONSTRAINT "JourneySegment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OtpCode" (
"id" TEXT NOT NULL,
"userId" TEXT,
"email" TEXT,
"phone" TEXT,
"code" TEXT NOT NULL,
"purpose" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"verified" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "OtpCode_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PasswordResetToken" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"used" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Route" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"effectiveFrom" TIMESTAMP(3) NOT NULL,
"effectiveUntil" TIMESTAMP(3),
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Route_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RouteStop" (
"id" TEXT NOT NULL,
"routeId" TEXT NOT NULL,
"stationId" TEXT NOT NULL,
"sequence" INTEGER NOT NULL,
"distanceKm" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "RouteStop_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RouteFareRule" (
"id" TEXT NOT NULL,
"routeId" TEXT NOT NULL,
"serviceClass" "ServiceClass" NOT NULL,
"passengerCategory" TEXT NOT NULL DEFAULT 'ADULT',
"baseFareMinor" INTEGER NOT NULL,
"discountPercent" INTEGER,
"currency" TEXT NOT NULL DEFAULT 'ETB',
"validFrom" TIMESTAMP(3) NOT NULL,
"validUntil" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "RouteFareRule_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Agent" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"agentCode" TEXT NOT NULL,
"stationId" TEXT,
"commissionRate" INTEGER NOT NULL DEFAULT 5,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Agent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AgentBooking" (
"id" TEXT NOT NULL,
"agentId" TEXT NOT NULL,
"bookingId" TEXT NOT NULL,
"paymentMethod" TEXT NOT NULL,
"cashReceived" INTEGER,
"changeGiven" INTEGER,
"paperTicket" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AgentBooking_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AgentShift" (
"id" TEXT NOT NULL,
"agentId" TEXT NOT NULL,
"openedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"closedAt" TIMESTAMP(3),
"openingBalance" INTEGER NOT NULL DEFAULT 0,
"closingBalance" INTEGER,
"reconciled" BOOLEAN NOT NULL DEFAULT false,
"notes" TEXT,
CONSTRAINT "AgentShift_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AgentCommission" (
"id" TEXT NOT NULL,
"agentId" TEXT NOT NULL,
"bookingId" TEXT NOT NULL,
"amountMinor" INTEGER NOT NULL,
"rate" INTEGER NOT NULL,
"paidAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AgentCommission_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BookingModification" (
"id" TEXT NOT NULL,
"bookingId" TEXT NOT NULL,
"modifiedBy" TEXT NOT NULL,
"modificationType" TEXT NOT NULL,
"oldData" JSONB NOT NULL,
"newData" JSONB NOT NULL,
"fareAdjustment" INTEGER NOT NULL DEFAULT 0,
"reason" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "BookingModification_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BookingCancellation" (
"id" TEXT NOT NULL,
"bookingId" TEXT NOT NULL,
"cancelledBy" TEXT NOT NULL,
"reason" TEXT,
"refundAmount" INTEGER NOT NULL,
"refundMethod" TEXT NOT NULL,
"refundStatus" TEXT NOT NULL,
"processedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "BookingCancellation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GateValidationLog" (
"id" TEXT NOT NULL,
"ticketId" TEXT NOT NULL,
"validatorId" TEXT NOT NULL,
"gateId" TEXT,
"status" TEXT NOT NULL,
"reason" TEXT,
"validatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "GateValidationLog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BaggageAllowance" (
"id" TEXT NOT NULL,
"serviceClass" "ServiceClass" NOT NULL,
"maxWeightKg" INTEGER NOT NULL,
"maxPiecesCount" INTEGER NOT NULL,
"excessFeePerKg" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'ETB',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "BaggageAllowance_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BaggageBooking" (
"id" TEXT NOT NULL,
"bookingId" TEXT NOT NULL,
"weightKg" INTEGER NOT NULL,
"piecesCount" INTEGER NOT NULL,
"excessFeeMinor" INTEGER NOT NULL DEFAULT 0,
"paid" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "BaggageBooking_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AuditLog" (
"id" TEXT NOT NULL,
"userId" TEXT,
"action" TEXT NOT NULL,
"entityType" TEXT NOT NULL,
"entityId" TEXT,
"oldData" JSONB,
"newData" JSONB,
"ipAddress" TEXT,
"userAgent" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "NotificationTemplate" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"channel" TEXT NOT NULL,
"subject" TEXT,
"bodyTemplate" TEXT NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "NotificationTemplate_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SeatBlock" (
"id" TEXT NOT NULL,
"seatId" TEXT NOT NULL,
"reason" TEXT NOT NULL,
"blockedBy" TEXT NOT NULL,
"approvedBy" TEXT,
"blockedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"unblockAt" TIMESTAMP(3),
CONSTRAINT "SeatBlock_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OperationalReport" (
"id" TEXT NOT NULL,
"reportType" TEXT NOT NULL,
"dateFrom" TIMESTAMP(3) NOT NULL,
"dateTo" TIMESTAMP(3) NOT NULL,
"data" JSONB NOT NULL,
"generatedBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "OperationalReport_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
@@ -575,6 +901,21 @@ CREATE UNIQUE INDEX "Booking_bookingRef_key" ON "Booking"("bookingRef");
-- CreateIndex
CREATE UNIQUE INDEX "PaymentIntent_bookingId_key" ON "PaymentIntent"("bookingId");
-- CreateIndex
CREATE UNIQUE INDEX "PaymentIntent_merchantOrderId_key" ON "PaymentIntent"("merchantOrderId");
-- CreateIndex
CREATE INDEX "PaymentIntent_providerOrderId_idx" ON "PaymentIntent"("providerOrderId");
-- CreateIndex
CREATE INDEX "PaymentIntent_providerTxnId_idx" ON "PaymentIntent"("providerTxnId");
-- CreateIndex
CREATE INDEX "PaymentWebhookEvent_merchantOrderId_idx" ON "PaymentWebhookEvent"("merchantOrderId");
-- CreateIndex
CREATE UNIQUE INDEX "PaymentWebhookEvent_provider_externalEventId_key" ON "PaymentWebhookEvent"("provider", "externalEventId");
-- CreateIndex
CREATE UNIQUE INDEX "Ticket_bookingId_key" ON "Ticket"("bookingId");
@@ -590,6 +931,72 @@ CREATE UNIQUE INDEX "Promotion_code_key" ON "Promotion"("code");
-- CreateIndex
CREATE UNIQUE INDEX "UserPreferences_userId_key" ON "UserPreferences"("userId");
-- CreateIndex
CREATE INDEX "OtpCode_email_phone_idx" ON "OtpCode"("email", "phone");
-- CreateIndex
CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token");
-- CreateIndex
CREATE INDEX "PasswordResetToken_userId_idx" ON "PasswordResetToken"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "Route_code_key" ON "Route"("code");
-- CreateIndex
CREATE INDEX "RouteStop_routeId_stationId_idx" ON "RouteStop"("routeId", "stationId");
-- CreateIndex
CREATE UNIQUE INDEX "RouteStop_routeId_sequence_key" ON "RouteStop"("routeId", "sequence");
-- CreateIndex
CREATE INDEX "RouteFareRule_routeId_serviceClass_idx" ON "RouteFareRule"("routeId", "serviceClass");
-- CreateIndex
CREATE UNIQUE INDEX "Agent_userId_key" ON "Agent"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "Agent_agentCode_key" ON "Agent"("agentCode");
-- CreateIndex
CREATE UNIQUE INDEX "AgentBooking_bookingId_key" ON "AgentBooking"("bookingId");
-- CreateIndex
CREATE INDEX "AgentShift_agentId_openedAt_idx" ON "AgentShift"("agentId", "openedAt");
-- CreateIndex
CREATE INDEX "AgentCommission_agentId_paidAt_idx" ON "AgentCommission"("agentId", "paidAt");
-- CreateIndex
CREATE INDEX "BookingModification_bookingId_idx" ON "BookingModification"("bookingId");
-- CreateIndex
CREATE UNIQUE INDEX "BookingCancellation_bookingId_key" ON "BookingCancellation"("bookingId");
-- CreateIndex
CREATE INDEX "GateValidationLog_ticketId_idx" ON "GateValidationLog"("ticketId");
-- CreateIndex
CREATE INDEX "GateValidationLog_validatorId_idx" ON "GateValidationLog"("validatorId");
-- CreateIndex
CREATE INDEX "BaggageBooking_bookingId_idx" ON "BaggageBooking"("bookingId");
-- CreateIndex
CREATE INDEX "AuditLog_userId_createdAt_idx" ON "AuditLog"("userId", "createdAt");
-- CreateIndex
CREATE INDEX "AuditLog_entityType_entityId_idx" ON "AuditLog"("entityType", "entityId");
-- CreateIndex
CREATE UNIQUE INDEX "NotificationTemplate_code_key" ON "NotificationTemplate"("code");
-- CreateIndex
CREATE INDEX "SeatBlock_seatId_idx" ON "SeatBlock"("seatId");
-- CreateIndex
CREATE INDEX "OperationalReport_reportType_dateFrom_idx" ON "OperationalReport"("reportType", "dateFrom");
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -638,6 +1045,9 @@ ALTER TABLE "BookingSeat" ADD CONSTRAINT "BookingSeat_seatId_fkey" FOREIGN KEY (
-- AddForeignKey
ALTER TABLE "PaymentIntent" ADD CONSTRAINT "PaymentIntent_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PaymentRefund" ADD CONSTRAINT "PaymentRefund_paymentIntentId_fkey" FOREIGN KEY ("paymentIntentId") REFERENCES "PaymentIntent"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Ticket" ADD CONSTRAINT "Ticket_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -688,3 +1098,48 @@ ALTER TABLE "Device" ADD CONSTRAINT "Device_userId_fkey" FOREIGN KEY ("userId")
-- AddForeignKey
ALTER TABLE "SavedRoute" ADD CONSTRAINT "SavedRoute_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "Passenger"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "JourneySegment" ADD CONSTRAINT "JourneySegment_journeyId_fkey" FOREIGN KEY ("journeyId") REFERENCES "Journey"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "JourneySegment" ADD CONSTRAINT "JourneySegment_tripId_fkey" FOREIGN KEY ("tripId") REFERENCES "Trip"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RouteStop" ADD CONSTRAINT "RouteStop_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "Route"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RouteFareRule" ADD CONSTRAINT "RouteFareRule_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "Route"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Agent" ADD CONSTRAINT "Agent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AgentBooking" ADD CONSTRAINT "AgentBooking_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AgentBooking" ADD CONSTRAINT "AgentBooking_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AgentShift" ADD CONSTRAINT "AgentShift_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AgentCommission" ADD CONSTRAINT "AgentCommission_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BookingModification" ADD CONSTRAINT "BookingModification_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BookingCancellation" ADD CONSTRAINT "BookingCancellation_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GateValidationLog" ADD CONSTRAINT "GateValidationLog_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "Ticket"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BaggageBooking" ADD CONSTRAINT "BaggageBooking_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SeatBlock" ADD CONSTRAINT "SeatBlock_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "Seat"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,2 @@
-- Language field already exists in UserPreferences table
-- No migration needed

View File

@@ -0,0 +1,74 @@
-- Migration: Add Fraud Detection Tables and User.blockedUntil field
-- Date: 2026-05-21
-- Description: Adds FraudRule and FraudAlert tables, and blockedUntil field to User table
-- Add blockedUntil field to User table if not exists
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'User' AND column_name = 'blockedUntil'
) THEN
ALTER TABLE "User" ADD COLUMN "blockedUntil" TIMESTAMP(3);
END IF;
END $$;
-- CreateTable FraudRule
CREATE TABLE IF NOT EXISTS "FraudRule" (
"id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"threshold" DOUBLE PRECISION NOT NULL,
"config" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "FraudRule_pkey" PRIMARY KEY ("id")
);
-- CreateTable FraudAlert
CREATE TABLE IF NOT EXISTS "FraudAlert" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"eventType" TEXT NOT NULL,
"triggeredRules" TEXT[],
"context" JSONB NOT NULL,
"severity" TEXT NOT NULL DEFAULT 'MEDIUM',
"acknowledged" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "FraudAlert_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX IF NOT EXISTS "FraudRule_type_key" ON "FraudRule"("type");
-- CreateIndex
CREATE INDEX IF NOT EXISTS "FraudAlert_userId_createdAt_idx" ON "FraudAlert"("userId", "createdAt");
-- CreateIndex
CREATE INDEX IF NOT EXISTS "FraudAlert_acknowledged_idx" ON "FraudAlert"("acknowledged");
-- AddForeignKey
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'FraudAlert_userId_fkey'
) THEN
ALTER TABLE "FraudAlert" ADD CONSTRAINT "FraudAlert_userId_fkey"
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
-- Insert default fraud rules
INSERT INTO "FraudRule" ("id", "type", "enabled", "threshold", "config", "createdAt", "updatedAt")
VALUES
(gen_random_uuid(), 'VELOCITY', true, 5, '{"timeWindowMinutes": 30, "blockDurationMinutes": 30}'::jsonb, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(gen_random_uuid(), 'HIGH_VALUE', true, 10000, '{"blockDurationMinutes": 60}'::jsonb, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(gen_random_uuid(), 'FAILED_PAYMENTS', true, 3, '{"timeWindowMinutes": 60, "blockDurationMinutes": 30}'::jsonb, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT (type) DO NOTHING;
-- Add comments
COMMENT ON TABLE "FraudRule" IS 'Fraud detection rules for monitoring suspicious activities';
COMMENT ON TABLE "FraudAlert" IS 'Fraud alerts triggered by rule violations';
COMMENT ON COLUMN "User"."blockedUntil" IS 'Timestamp until which the user is blocked due to fraud or security reasons';

View File

@@ -1,2 +0,0 @@
export {};
//# sourceMappingURL=reset-admin.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"reset-admin.d.ts","sourceRoot":"","sources":["reset-admin.ts"],"names":[],"mappings":""}

View File

@@ -1,49 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const client_1 = require("@prisma/client");
const bcrypt = __importStar(require("bcrypt"));
const prisma = new client_1.PrismaClient();
async function main() {
const passwordHash = await bcrypt.hash('admin123', 10);
const user = await prisma.user.upsert({
where: { email: 'admin@edr-platform.com' },
update: { passwordHash, role: 'ADMIN' },
create: { fullName: 'EDR Admin', email: 'admin@edr-platform.com', phone: '+251900000000', passwordHash, role: 'ADMIN' },
});
console.log('✅ Admin ready:', user.email);
}
main().catch(console.error).finally(() => prisma.$disconnect());
//# sourceMappingURL=reset-admin.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"reset-admin.js","sourceRoot":"","sources":["reset-admin.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAA8C;AAC9C,+CAAiC;AAEjC,MAAM,MAAM,GAAG,IAAI,qBAAY,EAAE,CAAC;AAElC,KAAK,UAAU,IAAI;IACjB,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IACvD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;QACpC,KAAK,EAAE,EAAE,KAAK,EAAE,wBAAwB,EAAE;QAC1C,MAAM,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE;QACvC,MAAM,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,wBAAwB,EAAE,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE;KACxH,CAAC,CAAC;IACH,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;AAC5C,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC"}

View File

@@ -1,16 +0,0 @@
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
const prisma = new PrismaClient();
async function main() {
const passwordHash = await bcrypt.hash('admin123', 10);
const user = await prisma.user.upsert({
where: { email: 'admin@edr-platform.com' },
update: { passwordHash, role: 'ADMIN' },
create: { fullName: 'EDR Admin', email: 'admin@edr-platform.com', phone: '+251900000000', passwordHash, role: 'ADMIN' },
});
console.log('✅ Admin ready:', user.email);
}
main().catch(console.error).finally(() => prisma.$disconnect());

View File

@@ -9,6 +9,8 @@ datasource db {
enum UserRole {
PASSENGER
AGENT
SUPERVISOR
ADMIN
STAFF
}
@@ -36,9 +38,12 @@ enum SeatStatus {
}
enum ServiceClass {
ECONOMY
BUSINESS
FIRST
ECONOMY_REGULAR
ECONOMY_BED_LOWER
ECONOMY_BED_MIDDLE
ECONOMY_BED_UPPER
VIP_BED_LOWER
VIP_BED_UPPER
}
enum BookingStatus {
@@ -48,6 +53,7 @@ enum BookingStatus {
CANCELLED
COMPLETED
NO_SHOW
REFUNDED
}
enum PaymentMethodType {
@@ -64,6 +70,7 @@ enum PaymentIntentStatus {
SUCCEEDED
FAILED
CANCELLED
REFUNDED
}
enum WalletLedgerType {
@@ -134,12 +141,21 @@ model User {
fullName String
passwordHash String
role UserRole @default(PASSENGER)
nationality String?
passportNumber String?
nationalId String?
failedLoginAttempts Int @default(0)
lockedUntil DateTime?
blockedUntil DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
passenger Passenger?
agent Agent?
sessions Session[]
devices Device[]
preferences UserPreferences?
auditLogs AuditLog[]
fraudAlerts FraudAlert[]
}
model Session {
@@ -147,6 +163,9 @@ model Session {
userId String
token String @unique
expiresAt DateTime
ipAddress String?
userAgent String?
lastActivityAt DateTime @default(now())
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
@@ -267,8 +286,11 @@ model Seat {
kind SeatKind @default(STANDARD)
status SeatStatus @default(AVAILABLE)
heldUntil DateTime?
premiumFeeMinor Int @default(0)
eligibility String?
coach Coach @relation(fields: [coachId], references: [id])
bookingSeats BookingSeat[]
blocks SeatBlock[]
@@unique([coachId, row, col])
}
@@ -303,6 +325,7 @@ model Booking {
status BookingStatus @default(DRAFT)
currency String @default("ETB")
totalMinor Int
bookingType String @default("ONE_WAY")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
passenger Passenger @relation(fields: [passengerId], references: [id])
@@ -311,6 +334,10 @@ model Booking {
paymentIntent PaymentIntent?
ticket Ticket?
foodOrders FoodOrder[]
agentBooking AgentBooking?
modifications BookingModification[]
cancellation BookingCancellation?
baggage BaggageBooking[]
}
model BookingSeat {
@@ -392,10 +419,14 @@ model Ticket {
bookingRef String
status String @default("CONFIRMED")
qrPayload String
barcodePayload String?
pdfUrl String?
deliveryChannel String @default("EMAIL")
issuedAt DateTime @default(now())
validatedAt DateTime?
validatorId String?
booking Booking @relation(fields: [bookingId], references: [id])
validationLogs GateValidationLog[]
}
model LoyaltyAccount {
@@ -585,6 +616,7 @@ model UserPreferences {
dataSharing Boolean @default(false)
locale String @default("en")
darkMode Boolean @default(false)
language String @default("en")
user User @relation(fields: [userId], references: [id])
}
@@ -633,3 +665,253 @@ model JourneySegment {
journey Journey @relation(fields: [journeyId], references: [id])
trip Trip @relation(fields: [tripId], references: [id])
}
model OtpCode {
id String @id @default(uuid())
userId String?
email String?
phone String?
code String
purpose String
expiresAt DateTime
verified Boolean @default(false)
createdAt DateTime @default(now())
@@index([email, phone])
}
model PasswordResetToken {
id String @id @default(uuid())
userId String
token String @unique
expiresAt DateTime
used Boolean @default(false)
createdAt DateTime @default(now())
@@index([userId])
}
model Route {
id String @id @default(uuid())
code String @unique
name String
description String?
effectiveFrom DateTime
effectiveUntil DateTime?
active Boolean @default(true)
createdAt DateTime @default(now())
stops RouteStop[]
fareRules RouteFareRule[]
}
model RouteStop {
id String @id @default(uuid())
routeId String
stationId String
sequence Int
distanceKm Int?
createdAt DateTime @default(now())
route Route @relation(fields: [routeId], references: [id], onDelete: Cascade)
@@unique([routeId, sequence])
@@index([routeId, stationId])
}
model RouteFareRule {
id String @id @default(uuid())
routeId String
serviceClass ServiceClass
passengerCategory String @default("ADULT")
baseFareMinor Int
discountPercent Int?
currency String @default("ETB")
validFrom DateTime
validUntil DateTime?
createdAt DateTime @default(now())
route Route @relation(fields: [routeId], references: [id], onDelete: Cascade)
@@index([routeId, serviceClass])
}
model Agent {
id String @id @default(uuid())
userId String @unique
agentCode String @unique
stationId String?
commissionRate Int @default(5)
active Boolean @default(true)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
bookings AgentBooking[]
shifts AgentShift[]
commissions AgentCommission[]
}
model AgentBooking {
id String @id @default(uuid())
agentId String
bookingId String @unique
paymentMethod String
cashReceived Int?
changeGiven Int?
paperTicket Boolean @default(false)
createdAt DateTime @default(now())
agent Agent @relation(fields: [agentId], references: [id])
booking Booking @relation(fields: [bookingId], references: [id])
}
model AgentShift {
id String @id @default(uuid())
agentId String
openedAt DateTime @default(now())
closedAt DateTime?
openingBalance Int @default(0)
closingBalance Int?
reconciled Boolean @default(false)
notes String?
agent Agent @relation(fields: [agentId], references: [id])
@@index([agentId, openedAt])
}
model AgentCommission {
id String @id @default(uuid())
agentId String
bookingId String
amountMinor Int
rate Int
paidAt DateTime?
createdAt DateTime @default(now())
agent Agent @relation(fields: [agentId], references: [id])
@@index([agentId, paidAt])
}
model BookingModification {
id String @id @default(uuid())
bookingId String
modifiedBy String
modificationType String
oldData Json
newData Json
fareAdjustment Int @default(0)
reason String?
createdAt DateTime @default(now())
booking Booking @relation(fields: [bookingId], references: [id])
@@index([bookingId])
}
model BookingCancellation {
id String @id @default(uuid())
bookingId String @unique
cancelledBy String
reason String?
refundAmount Int
refundMethod String
refundStatus String
processedAt DateTime?
createdAt DateTime @default(now())
booking Booking @relation(fields: [bookingId], references: [id])
}
model GateValidationLog {
id String @id @default(uuid())
ticketId String
validatorId String
gateId String?
status String
reason String?
validatedAt DateTime @default(now())
ticket Ticket @relation(fields: [ticketId], references: [id])
@@index([ticketId])
@@index([validatorId])
}
model BaggageAllowance {
id String @id @default(uuid())
serviceClass ServiceClass
maxWeightKg Int
maxPiecesCount Int
excessFeePerKg Int
currency String @default("ETB")
createdAt DateTime @default(now())
}
model BaggageBooking {
id String @id @default(uuid())
bookingId String
weightKg Int
piecesCount Int
excessFeeMinor Int @default(0)
paid Boolean @default(false)
createdAt DateTime @default(now())
booking Booking @relation(fields: [bookingId], references: [id])
@@index([bookingId])
}
model AuditLog {
id String @id @default(uuid())
userId String?
action String
entityType String
entityId String?
oldData Json?
newData Json?
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
user User? @relation(fields: [userId], references: [id])
@@index([userId, createdAt])
@@index([entityType, entityId])
}
model NotificationTemplate {
id String @id @default(uuid())
code String @unique
channel String
subject String?
bodyTemplate String
active Boolean @default(true)
createdAt DateTime @default(now())
}
model SeatBlock {
id String @id @default(uuid())
seatId String
reason String
blockedBy String
approvedBy String?
blockedAt DateTime @default(now())
unblockAt DateTime?
seat Seat @relation(fields: [seatId], references: [id])
@@index([seatId])
}
model OperationalReport {
id String @id @default(uuid())
reportType String
dateFrom DateTime
dateTo DateTime
data Json
generatedBy String?
createdAt DateTime @default(now())
@@index([reportType, dateFrom])
}
model FraudRule {
id String @id @default(uuid())
type String @unique
enabled Boolean @default(true)
threshold Float
config Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model FraudAlert {
id String @id @default(uuid())
userId String
eventType String
triggeredRules String[]
context Json
severity String @default("MEDIUM")
acknowledged Boolean @default(false)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, createdAt])
@@index([acknowledged])
}

View File

@@ -1,2 +0,0 @@
export {};
//# sourceMappingURL=seed.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["seed.ts"],"names":[],"mappings":""}

View File

@@ -1,73 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const client_1 = require("@prisma/client");
const bcrypt = __importStar(require("bcrypt"));
const prisma = new client_1.PrismaClient();
async function main() {
const addis = await prisma.station.upsert({ where: { code: 'ADD' }, update: {}, create: { code: 'ADD', name: 'Addis Ababa', city: 'Addis Ababa', lat: 9.0054, lng: 38.7636 } });
const direDawa = await prisma.station.upsert({ where: { code: 'DDW' }, update: {}, create: { code: 'DDW', name: 'Dire Dawa', city: 'Dire Dawa', lat: 9.5931, lng: 41.8661 } });
const djibouti = await prisma.station.upsert({ where: { code: 'DJI' }, update: {}, create: { code: 'DJI', name: 'Djibouti', city: 'Djibouti', timezone: 'Africa/Djibouti', lat: 11.5720, lng: 43.1456 } });
const service = await prisma.trainService.upsert({ where: { number: '301' }, update: {}, create: { number: '301', name: 'Express 301' } });
const trip = await prisma.trip.create({
data: { serviceId: service.id, originStationId: addis.id, destinationStationId: djibouti.id, departureAt: new Date('2026-05-11T08:30:00Z'), arrivalAt: new Date('2026-05-11T20:00:00Z'), durationMinutes: 690, stopsCount: 1 },
});
for (const [label, cls] of [['A', 'ECONOMY'], ['B', 'BUSINESS']]) {
const coach = await prisma.coach.create({ data: { tripId: trip.id, label, serviceClass: cls } });
const seats = [];
for (let row = 1; row <= 10; row++) {
for (const col of ['A', 'B', 'C', 'D'])
seats.push({ coachId: coach.id, row, col, label: `${row}${col}` });
}
await prisma.seat.createMany({ data: seats });
}
await prisma.fareRule.create({ data: { tripId: trip.id, serviceClass: 'ECONOMY', baseFareMinor: 45000, validFrom: new Date('2026-01-01') } });
const hash = await bcrypt.hash('password123', 10);
const user = await prisma.user.upsert({ where: { email: 'kelemu@email.com' }, update: {}, create: { fullName: 'Kelemu Ketsela', email: 'kelemu@email.com', phone: '+251912345678', passwordHash: hash } });
let passenger = await prisma.passenger.findUnique({ where: { userId: user.id } });
if (!passenger) {
passenger = await prisma.passenger.create({ data: { userId: user.id } });
await prisma.loyaltyAccount.create({ data: { passengerId: passenger.id, pointsBalance: 2450, tier: 'SILVER' } });
await prisma.walletAccount.create({ data: { passengerId: passenger.id, balanceMinor: 125000 } });
}
await prisma.userPreferences.upsert({ where: { userId: user.id }, update: {}, create: { userId: user.id } });
await prisma.user.upsert({ where: { email: 'admin@edr-platform.com' }, update: { passwordHash: await bcrypt.hash('admin123', 10), role: 'ADMIN' }, create: { fullName: 'EDR Admin', email: 'admin@edr-platform.com', phone: '+251900000000', passwordHash: await bcrypt.hash('admin123', 10), role: 'ADMIN' } });
await prisma.promotion.upsert({ where: { code: 'WEEKEND15' }, update: {}, create: { title: 'Weekend Sale', code: 'WEEKEND15', percentOff: 15, validUntil: new Date('2026-12-31') } });
const faqCat = await prisma.faqCategory.create({ data: { title: 'Booking & Tickets', iconKey: 'description_outlined' } });
await prisma.faqArticle.create({ data: { categoryId: faqCat.id, question: 'How do I book a train ticket?', answerMarkdown: 'Open Search, pick stations and date, select seats, and proceed to payment.', rank: 1 } });
console.log('✅ Seed complete');
}
main().catch(console.error).finally(() => prisma.$disconnect());
//# sourceMappingURL=seed.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"seed.js","sourceRoot":"","sources":["seed.ts"],"names":[],"mappings":"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"}

View File

@@ -1,48 +1,237 @@
import { PrismaClient } from '@prisma/client';
import { PrismaClient, ServiceClass, UserRole, LoyaltyTier, SeatKind } from '@prisma/client';
import * as bcrypt from 'bcrypt';
const prisma = new PrismaClient();
async function main() {
const addis = await prisma.station.upsert({ where: { code: 'ADD' }, update: {}, create: { code: 'ADD', name: 'Addis Ababa', city: 'Addis Ababa', lat: 9.0054, lng: 38.7636 } });
console.log('🌱 Starting comprehensive seed...');
// All 18 Stations (Ethiopian-Djibouti Railway)
const addis = await prisma.station.upsert({ where: { code: 'ADD' }, update: {}, create: { code: 'ADD', name: 'Addis Ababa Central', city: 'Addis Ababa', lat: 9.0054, lng: 38.7636 } });
const sebeta = await prisma.station.upsert({ where: { code: 'SBT' }, update: {}, create: { code: 'SBT', name: 'Sebeta', city: 'Sebeta', lat: 8.9167, lng: 38.6167 } });
const labu = await prisma.station.upsert({ where: { code: 'LBU' }, update: {}, create: { code: 'LBU', name: 'Labu', city: 'Labu', lat: 8.8500, lng: 38.8500 } });
const indode = await prisma.station.upsert({ where: { code: 'IND' }, update: {}, create: { code: 'IND', name: 'Indode', city: 'Indode', lat: 8.7833, lng: 39.0167 } });
const bishoftu = await prisma.station.upsert({ where: { code: 'BSH' }, update: {}, create: { code: 'BSH', name: 'Bishoftu', city: 'Bishoftu', lat: 8.7500, lng: 38.9833 } });
const mojo = await prisma.station.upsert({ where: { code: 'MJO' }, update: {}, create: { code: 'MJO', name: 'Mojo', city: 'Mojo', lat: 8.5833, lng: 39.1167 } });
const adama = await prisma.station.upsert({ where: { code: 'ADM' }, update: {}, create: { code: 'ADM', name: 'Adama', city: 'Adama', lat: 8.5400, lng: 39.2675 } });
const feto = await prisma.station.upsert({ where: { code: 'FTO' }, update: {}, create: { code: 'FTO', name: 'Feto', city: 'Feto', lat: 8.7167, lng: 39.5833 } });
const metahara = await prisma.station.upsert({ where: { code: 'MTH' }, update: {}, create: { code: 'MTH', name: 'Metahara', city: 'Metahara', lat: 8.9000, lng: 39.9167 } });
const awash = await prisma.station.upsert({ where: { code: 'AWS' }, update: {}, create: { code: 'AWS', name: 'Awash', city: 'Awash', lat: 8.9833, lng: 40.1667 } });
const mieso = await prisma.station.upsert({ where: { code: 'MSO' }, update: {}, create: { code: 'MSO', name: 'Mieso', city: 'Mieso', lat: 9.2333, lng: 40.7500 } });
const bike = await prisma.station.upsert({ where: { code: 'BKE' }, update: {}, create: { code: 'BKE', name: 'Bike', city: 'Bike', 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', lat: 9.5931, lng: 41.8661 } });
const arawa = await prisma.station.upsert({ where: { code: 'ARW' }, update: {}, create: { code: 'ARW', name: 'Arawa', city: 'Arawa', lat: 10.0833, lng: 42.2500 } });
const adigala = await prisma.station.upsert({ where: { code: 'ADG' }, update: {}, create: { code: 'ADG', name: 'Adigala', city: 'Adigala', lat: 10.5000, lng: 42.5833 } });
const aysha = await prisma.station.upsert({ where: { code: 'AYS' }, update: {}, create: { code: 'AYS', name: 'Aysha', city: 'Aysha', lat: 11.5500, lng: 42.7167 } });
const dawanle = await prisma.station.upsert({ where: { code: 'DWN' }, update: {}, create: { code: 'DWN', name: 'Dawanle', city: 'Dawanle', 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', 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', 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', 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', timezone: 'Africa/Djibouti', lat: 11.5720, lng: 43.1456 } });
const service = await prisma.trainService.upsert({ where: { number: '301' }, update: {}, create: { number: '301', name: 'Express 301' } });
// 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 },
]});
const trip = await prisma.trip.create({
data: { serviceId: service.id, originStationId: addis.id, destinationStationId: djibouti.id, departureAt: new Date('2026-05-11T08:30:00Z'), arrivalAt: new Date('2026-05-11T20:00:00Z'), durationMinutes: 690, stopsCount: 1 },
// Route Fare Rules
await prisma.routeFareRule.deleteMany({ where: { routeId: route1.id } });
await prisma.routeFareRule.createMany({ data: [
{ routeId: route1.id, serviceClass: 'ECONOMY_REGULAR', baseFareMinor: 45000, validFrom: new Date('2026-01-01') },
{ routeId: route1.id, serviceClass: 'ECONOMY_BED_LOWER', baseFareMinor: 65000, validFrom: new Date('2026-01-01') },
{ routeId: route1.id, serviceClass: 'VIP_BED_LOWER', baseFareMinor: 95000, validFrom: new Date('2026-01-01') },
]});
// 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' } });
// 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 },
});
for (const [label, cls] of [['A', 'ECONOMY'], ['B', 'BUSINESS']] as const) {
const coach = await prisma.coach.create({ data: { tripId: trip.id, label, serviceClass: cls } });
const seats = [];
for (let row = 1; row <= 10; row++) {
for (const col of ['A', 'B', 'C', 'D']) seats.push({ coachId: coach.id, row, col, label: `${row}${col}` });
// 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 } });
const seats = [];
const rows = Math.ceil(seatCount / 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 });
}
}
await prisma.seat.createMany({ data: seats });
}
await prisma.seat.createMany({ data: seats });
}
await prisma.fareRule.create({ data: { tripId: trip.id, serviceClass: 'ECONOMY', baseFareMinor: 45000, validFrom: new Date('2026-01-01') } });
// 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 },
]});
}
// Users
const hash = await bcrypt.hash('password123', 10);
const user = await prisma.user.upsert({ where: { email: 'kelemu@email.com' }, update: {}, create: { fullName: 'Kelemu Ketsela', email: 'kelemu@email.com', phone: '+251912345678', passwordHash: hash } });
let passenger = await prisma.passenger.findUnique({ where: { userId: user.id } });
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' } });
const passengerUser = await prisma.user.upsert({ where: { email: 'kelemu@email.com' }, update: {}, create: { fullName: 'Kelemu Ketsela', email: 'kelemu@email.com', phone: '+251912345678', passwordHash: hash, nationality: 'Ethiopian', nationalId: 'ET123456789' } });
let passenger = await prisma.passenger.findUnique({ where: { userId: passengerUser.id } });
if (!passenger) {
passenger = await prisma.passenger.create({ data: { userId: user.id } });
passenger = await prisma.passenger.create({ data: { userId: passengerUser.id } });
await prisma.loyaltyAccount.create({ data: { passengerId: passenger.id, pointsBalance: 2450, tier: 'SILVER' } });
await prisma.walletAccount.create({ data: { passengerId: passenger.id, balanceMinor: 125000 } });
}
await prisma.userPreferences.upsert({ where: { userId: user.id }, update: {}, create: { userId: user.id } });
await prisma.userPreferences.upsert({ where: { userId: passengerUser.id }, update: {}, create: { userId: passengerUser.id, language: 'en' } });
await prisma.user.upsert({ where: { email: 'admin@edr-platform.com' }, update: { passwordHash: await bcrypt.hash('admin123', 10), role: 'ADMIN' }, create: { fullName: 'EDR Admin', email: 'admin@edr-platform.com', phone: '+251900000000', passwordHash: await bcrypt.hash('admin123', 10), role: 'ADMIN' } });
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 } });
await prisma.promotion.upsert({ where: { code: 'WEEKEND15' }, update: {}, create: { title: 'Weekend Sale', code: 'WEEKEND15', percentOff: 15, validUntil: new Date('2026-12-31') } });
// Baggage Allowance - Delete and recreate
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 },
]});
const faqCat = await prisma.faqCategory.create({ data: { title: 'Booking & Tickets', iconKey: 'description_outlined' } });
await prisma.faqArticle.create({ data: { categoryId: faqCat.id, question: 'How do I book a train ticket?', answerMarkdown: 'Open Search, pick stations and date, select seats, and proceed to payment.', rank: 1 } });
// 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 } });
console.log('✅ Seed complete');
// 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
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 },
]});
// 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
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' },
]});
// 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' },
]});
}
}
console.log('✅ Comprehensive seed complete');
console.log('\n📋 Seed Summary:');
console.log(' - 18 Stations (Complete Ethiopian-Djibouti Railway)');
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(' - 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');
}
main().catch(console.error).finally(() => prisma.$disconnect());