mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 18:18:18 +00:00
Refactored the whole app based on the requirements shared
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Language field already exists in UserPreferences table
|
||||
-- No migration needed
|
||||
@@ -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';
|
||||
@@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=reset-admin.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"reset-admin.d.ts","sourceRoot":"","sources":["reset-admin.ts"],"names":[],"mappings":""}
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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());
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
2
apps/edr-passenger-api/prisma/seed.d.ts
vendored
2
apps/edr-passenger-api/prisma/seed.d.ts
vendored
@@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=seed.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["seed.ts"],"names":[],"mappings":""}
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"seed.js","sourceRoot":"","sources":["seed.ts"],"names":[],"mappings":"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"}
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user