generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } enum UserRole { PASSENGER AGENT SUPERVISOR ADMIN STAFF } enum TripStatus { SCHEDULED BOARDING EN_ROUTE ARRIVED CANCELLED DELAYED } enum SeatKind { STANDARD PREMIUM ACCESSIBLE } enum SeatStatus { AVAILABLE HELD BOOKED BLOCKED } enum ServiceClass { ECONOMY_REGULAR ECONOMY_BED_LOWER ECONOMY_BED_MIDDLE ECONOMY_BED_UPPER VIP_BED_LOWER VIP_BED_UPPER } enum PassengerCategory { ADULT CHILD } enum IdDocumentType { NATIONAL_ID PASSPORT DRIVING_LICENSE OTHER } enum Currency { ETB DJF USD } enum BookingStatus { DRAFT PENDING_PAYMENT CONFIRMED CANCELLED COMPLETED NO_SHOW REFUNDED } enum PaymentMethodType { TELEBIRR CBE_BIRR EBIRR CARD WALLET } enum PaymentIntentStatus { REQUIRES_ACTION PROCESSING SUCCEEDED FAILED CANCELLED REFUNDED } enum WalletLedgerType { CREDIT DEBIT } enum NotificationCategory { BOOKING PAYMENT DISRUPTION PROMOTION SYSTEM } enum StopStatus { COMPLETED APPROACHING CURRENT UPCOMING } enum SupportConversationStatus { OPEN RESOLVED CLOSED } enum SupportSender { USER BOT AGENT } enum LoyaltyTier { BRONZE SILVER GOLD PLATINUM } enum LoyaltyLedgerReason { TRIP_COMPLETED REWARD_REDEEMED PROMO_BONUS MANUAL_ADJUSTMENT EXPIRY } enum FoodOrderStatus { PENDING PREPARING READY DELIVERED CANCELLED } enum DevicePlatform { IOS ANDROID WEB } model User { id String @id @default(uuid()) email String @unique phone String @unique fullName String passwordHash String role UserRole @default(PASSENGER) nationality String? nationalityCode String? passportNumber String? nationalId String? failedLoginAttempts Int @default(0) lockedUntil DateTime? blockedUntil DateTime? lastLoginAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt passenger Passenger? agent Agent? sessions Session[] devices Device[] preferences UserPreferences? auditLogs AuditLog[] fraudAlerts FraudAlert[] } model Session { id String @id @default(uuid()) 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) } model Passenger { id String @id @default(uuid()) userId String @unique defaultTravelerProfileId String? preferredLanguage String? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) bookings Booking[] loyalty LoyaltyAccount? wallet WalletAccount? notifications Notification[] travelerProfiles TravelerProfile[] savedRoutes SavedRoute[] @@index([userId]) } model TravelerProfile { id String @id @default(uuid()) passengerId String fullName String relationship String dateOfBirth DateTime? nationalId String? notes String? createdAt DateTime @default(now()) passenger Passenger @relation(fields: [passengerId], references: [id]) } model Station { id String @id @default(uuid()) code String @unique name String city String countryCode String? isOperational Boolean @default(true) timezone String @default("Africa/Addis_Ababa") lat Decimal @db.Decimal(9, 6) lng Decimal @db.Decimal(9, 6) originTrips Trip[] @relation("OriginTrips") destinationTrips Trip[] @relation("DestinationTrips") stopTimes TripStopTime[] crowdSignals StationCrowdSignal[] @@index([city, countryCode]) } model TrainService { id String @id @default(uuid()) number String @unique name String operatorId String @default("op_edr") operatorName String? trips Trip[] } model Trip { id String @id @default(uuid()) serviceId String routeId String? originStationId String destinationStationId String departureAt DateTime arrivalAt DateTime durationMinutes Int status TripStatus @default(SCHEDULED) stopsCount Int @default(0) reservedCount Int @default(0) onTimePercent Int @default(100) carbonRating String @default("A") notes String? service TrainService @relation(fields: [serviceId], references: [id]) originStation Station @relation("OriginTrips", fields: [originStationId], references: [id]) destinationStation Station @relation("DestinationTrips", fields: [destinationStationId], references: [id]) coaches Coach[] bookings Booking[] stopTimes TripStopTime[] liveStatus TripLiveStatus? menuItems MenuItem[] journeySegments JourneySegment[] @@index([departureAt, originStationId]) } model TripStopTime { id String @id @default(uuid()) tripId String stationId String sequence Int plannedArrivalAt DateTime? plannedDepartureAt DateTime? actualArrivalAt DateTime? status StopStatus @default(UPCOMING) trip Trip @relation(fields: [tripId], references: [id]) station Station @relation(fields: [stationId], references: [id]) @@unique([tripId, sequence]) } model TripLiveStatus { id String @id @default(uuid()) tripId String @unique state String currentLocationLabel String? progressPercent Int @default(0) delayMinutes Int @default(0) currentSpeedKph Int? platformLabel String? updatedAt DateTime @updatedAt trip Trip @relation(fields: [tripId], references: [id]) } model Coach { id String @id @default(uuid()) tripId String label String serviceClass ServiceClass capacity Int? sequence Int? coachType String? amenities Json? trip Trip @relation(fields: [tripId], references: [id]) seats Seat[] @@unique([tripId, label]) } model Seat { id String @id @default(uuid()) coachId String row Int col String label String seatNumber String? kind SeatKind @default(STANDARD) status SeatStatus @default(AVAILABLE) heldUntil DateTime? isWindow Boolean @default(false) isAisle Boolean @default(false) premiumFeeMinor Int @default(0) eligibility String? coach Coach @relation(fields: [coachId], references: [id]) bookingSeats BookingSeat[] blocks SeatBlock[] @@unique([coachId, row, col]) @@unique([coachId, seatNumber]) } model SeatHold { id String @id @default(uuid()) tripId String seatIds String[] fareQuoteId String? passengerId String createdBy String? expiresAt DateTime createdAt DateTime @default(now()) @@index([expiresAt]) } model FareRule { id String @id @default(uuid()) tripId String? route String? serviceClass ServiceClass baseFareMinor Int currency String @default("ETB") refundable Boolean @default(true) validFrom DateTime validUntil DateTime? createdAt DateTime @default(now()) } model Booking { id String @id @default(uuid()) bookingRef String @unique passengerId String tripId String status BookingStatus @default(DRAFT) currency String @default("ETB") totalMinor Int adultCount Int @default(1) childCount Int @default(0) displayCurrency Currency? displayTotalMinor Int? bookingType String @default("ONE_WAY") userAgent String? source String @default("WEB") promoCode String? paidAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt passenger Passenger @relation(fields: [passengerId], references: [id]) trip Trip @relation(fields: [tripId], references: [id]) seats BookingSeat[] paymentIntent PaymentIntent? ticket Ticket? foodOrders FoodOrder[] agentBooking AgentBooking? modifications BookingModification[] cancellation BookingCancellation? baggage BaggageBooking[] @@index([passengerId, status]) } model BookingSeat { id String @id @default(uuid()) bookingId String seatId String passengerName String dateOfBirth DateTime? passengerCategory PassengerCategory @default(ADULT) idDocumentType IdDocumentType? idDocumentNumber String? passportNumber String? passportCountry String? verifaydaVerified Boolean @default(false) verifaydaData Json? seatLabelSnapshot String? fareMinor Int? displayCurrency Currency? displayFareMinor Int? booking Booking @relation(fields: [bookingId], references: [id]) seat Seat @relation(fields: [seatId], references: [id]) } model PaymentMethod { id String @id @default(uuid()) userId String type PaymentMethodType displayName String maskedHint String? providerId String? isDefault Boolean @default(false) createdAt DateTime @default(now()) @@index([userId, isDefault]) } model PaymentIntent { id String @id @default(uuid()) bookingId String @unique amountMinor Int currency String @default("ETB") method PaymentMethodType provider String? status PaymentIntentStatus @default(REQUIRES_ACTION) providerRef String? clientAction Json? merchantOrderId String? @unique providerOrderId String? providerTxnId String? rawInitiation Json? paidAt DateTime? refundedAt DateTime? captureMethod String? failureCode String? failureMessage String? expiresAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt booking Booking @relation(fields: [bookingId], references: [id]) refunds PaymentRefund[] @@index([providerOrderId]) @@index([providerTxnId]) } model PaymentWebhookEvent { id String @id @default(uuid()) provider PaymentMethodType externalEventId String merchantOrderId String? providerTxnId String? signatureValid Boolean status String payload Json receivedAt DateTime @default(now()) processedAt DateTime? processingError String? @@unique([provider, externalEventId]) @@index([merchantOrderId]) } model PaymentRefund { id String @id @default(uuid()) paymentIntentId String amountMinor Int reason String? providerRefundId String? status String createdAt DateTime @default(now()) paymentIntent PaymentIntent @relation(fields: [paymentIntentId], references: [id]) } model Ticket { id String @id @default(uuid()) bookingId String @unique 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 { id String @id @default(uuid()) passengerId String @unique pointsBalance Int @default(0) lifetimePoints Int @default(0) tier LoyaltyTier @default(BRONZE) tierUpdatedAt DateTime? updatedAt DateTime @updatedAt passenger Passenger @relation(fields: [passengerId], references: [id]) ledger LoyaltyLedgerEntry[] rewards LoyaltyReward[] } model LoyaltyLedgerEntry { id String @id @default(uuid()) accountId String delta Int reason LoyaltyLedgerReason bookingId String? balanceAfter Int createdAt DateTime @default(now()) account LoyaltyAccount @relation(fields: [accountId], references: [id]) } model LoyaltyReward { id String @id @default(uuid()) accountId String title String costPoints Int available Boolean @default(true) description String? account LoyaltyAccount @relation(fields: [accountId], references: [id]) } model WalletAccount { id String @id @default(uuid()) passengerId String @unique balanceMinor Int @default(0) status String @default("ACTIVE") holdMinor Int @default(0) currency String @default("ETB") updatedAt DateTime @updatedAt passenger Passenger @relation(fields: [passengerId], references: [id]) ledger WalletLedgerEntry[] @@index([passengerId]) } model WalletLedgerEntry { id String @id @default(uuid()) walletId String type WalletLedgerType amountMinor Int balanceAfterMinor Int description String relatedBookingId String? createdAt DateTime @default(now()) wallet WalletAccount @relation(fields: [walletId], references: [id]) } model Notification { id String @id @default(uuid()) passengerId String title String body String category NotificationCategory read Boolean @default(false) deepLink String? metadata Json? createdAt DateTime @default(now()) passenger Passenger @relation(fields: [passengerId], references: [id]) } model Promotion { id String @id @default(uuid()) title String subtitle String? code String @unique percentOff Int? amountOffMinor Int? validUntil DateTime ctaLabel String? deepLink String? active Boolean @default(true) createdAt DateTime @default(now()) } model StationCrowdSignal { id String @id @default(uuid()) stationId String level String label String statusLabel String confidence Int? observedAt DateTime? updatedAt DateTime @updatedAt station Station @relation(fields: [stationId], references: [id]) } model WeatherAlert { id String @id @default(uuid()) region String severity String title String message String validUntil DateTime createdAt DateTime @default(now()) } model MenuCategory { id String @id @default(uuid()) name String items MenuItem[] } model MenuItem { id String @id @default(uuid()) tripId String categoryId String name String priceMinor Int currency String @default("ETB") available Boolean @default(true) availableUntil DateTime? trip Trip @relation(fields: [tripId], references: [id]) category MenuCategory @relation(fields: [categoryId], references: [id]) } model FoodOrder { id String @id @default(uuid()) bookingId String status FoodOrderStatus @default(PENDING) totalMinor Int currency String @default("ETB") specialInstructions String? estimatedReadyAt DateTime? createdAt DateTime @default(now()) booking Booking @relation(fields: [bookingId], references: [id]) items FoodOrderItem[] } model FoodOrderItem { id String @id @default(uuid()) orderId String menuItemId String name String quantity Int unitPriceMinor Int? lineTotalMinor Int order FoodOrder @relation(fields: [orderId], references: [id]) } model FaqCategory { id String @id @default(uuid()) title String iconKey String? articles FaqArticle[] } model FaqArticle { id String @id @default(uuid()) categoryId String question String answerMarkdown String rank Int @default(0) category FaqCategory @relation(fields: [categoryId], references: [id]) } model SupportConversation { id String @id @default(uuid()) userId String assignedAgentId String? status SupportConversationStatus @default(OPEN) createdAt DateTime @default(now()) messages SupportMessage[] } model SupportMessage { id String @id @default(uuid()) conversationId String sender SupportSender text String attachments Json? createdAt DateTime @default(now()) conversation SupportConversation @relation(fields: [conversationId], references: [id]) } model UserPreferences { id String @id @default(uuid()) userId String @unique pushEnabled Boolean @default(true) emailEnabled Boolean @default(true) smsEnabled Boolean @default(false) promosEnabled Boolean @default(true) biometricEnabled Boolean @default(false) twoFactorEnabled Boolean @default(false) defaultPaymentMethodId String? autoDownloadTickets Boolean @default(true) dataSharing Boolean @default(false) locale String @default("en") darkMode Boolean @default(false) language String @default("en") user User @relation(fields: [userId], references: [id]) } model Device { id String @id @default(uuid()) userId String platform DevicePlatform name String pushToken String? trusted Boolean @default(false) lastSeenAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) } model SavedRoute { id String @id @default(uuid()) passengerId String fromStationId String toStationId String fromName String toName String tripCount Int @default(0) createdAt DateTime @default(now()) passenger Passenger @relation(fields: [passengerId], references: [id]) } model Journey { id String @id @default(uuid()) passengerId String status String totalMinor Int currency String @default("ETB") createdAt DateTime @default(now()) journeySegments JourneySegment[] } model JourneySegment { id String @id @default(uuid()) journeyId String tripId String segmentOrder Int seatId String? coachId String? departureStationId String arrivalStationId String journey Journey @relation(fields: [journeyId], references: [id]) trip Trip @relation(fields: [tripId], references: [id]) } 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 PassengerCategory @default(ADULT) baseFareMinor Int discountPercent Int? taxPercent Int? surchargeMinor 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]) } model CurrencyExchangeRate { id String @id @default(uuid()) fromCurrency Currency toCurrency Currency rate Decimal @db.Decimal(18, 6) effectiveDate DateTime @default(now()) source String @default("MANUAL") createdAt DateTime @default(now()) @@unique([fromCurrency, toCurrency, effectiveDate]) @@index([fromCurrency, toCurrency]) } model VerifaydaVerification { id String @id @default(uuid()) bookingId String? nationalId String requestPayload Json responsePayload Json? verified Boolean @default(false) failureReason String? verifiedAt DateTime? createdAt DateTime @default(now()) @@index([nationalId]) @@index([bookingId]) }