generator client { provider = "prisma-client-js" previewFeatures = ["multiSchema"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") schemas = ["passenger"] } enum UserRole { PASSENGER AGENT SUPERVISOR ADMIN STAFF @@schema("passenger") } enum TripStatus { SCHEDULED BOARDING EN_ROUTE ARRIVED CANCELLED DELAYED @@schema("passenger") } enum SeatKind { STANDARD PREMIUM ACCESSIBLE @@schema("passenger") } enum SeatStatus { AVAILABLE HELD BOOKED BLOCKED @@schema("passenger") } enum PassengerCategory { ADULT CHILD @@schema("passenger") } enum IdDocumentType { NATIONAL_ID PASSPORT DRIVING_LICENSE OTHER @@schema("passenger") } enum Currency { ETB DJF USD @@schema("passenger") } model CoachType { id String @id @default(uuid()) code String name String type String @default("passenger") // 'passenger', 'sleeper', 'dining', 'baggage' createdAt DateTime @default(now()) updatedAt DateTime @updatedAt coaches Coach[] seatClasses SeatClass[] @@schema("passenger") } model SeatClass { id String @id @default(uuid()) coachTypeId String name String description String? nationalityType String? // 'LOCAL' | 'INTERNATIONAL' bedPosition String? // 'UPPER' | 'MIDDLE' | 'LOWER' | null for regular seat baseFareMinor Int @default(0) // per-km rate (tariff decimal × 100000) premiumMinor Int @default(0) // flat fee per passenger insuranceFeeMinor Int @default(0) // flat fee per passenger isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt coachType CoachType @relation(fields: [coachTypeId], references: [id]) fareRules FareRule[] routeFareRules RouteFareRule[] segmentFares SegmentFareRule[] packagePriceTiers PackagePriceTier[] @@unique([coachTypeId, name]) @@index([coachTypeId]) @@index([coachTypeId, nationalityType, bedPosition]) @@schema("passenger") } enum BookingStatus { DRAFT PENDING_PAYMENT CONFIRMED CANCELLED BOARDED NO_SHOW REFUNDED @@schema("passenger") } enum ReturnLegStatus { NOT_APPLICABLE BOTH_USED OUTBOUND_ONLY INBOUND_ONLY NEITHER_USED @@schema("passenger") } enum PaymentRegion { ETHIOPIA DJIBOUTI INTERNATIONAL GLOBAL @@schema("passenger") } enum PaymentMethodType { TELEBIRR CBE_BIRR EBIRR CARD WALLET WAAFI DMONEY CAC_BANK CBE_BILL @@schema("passenger") } enum PaymentIntentStatus { REQUIRES_ACTION PROCESSING SUCCEEDED FAILED CANCELLED REFUNDED @@schema("passenger") } enum WalletLedgerType { CREDIT DEBIT @@schema("passenger") } enum NotificationCategory { BOOKING PAYMENT DISRUPTION PROMOTION SYSTEM @@schema("passenger") } enum StopStatus { OPEN CHECKIN_CLOSED BOARDED COMPLETED @@schema("passenger") } enum SupportConversationStatus { OPEN RESOLVED CLOSED @@schema("passenger") } enum SupportSender { USER BOT AGENT @@schema("passenger") } enum LoyaltyTier { BRONZE SILVER GOLD PLATINUM @@schema("passenger") } enum LoyaltyLedgerReason { TRIP_COMPLETED REWARD_REDEEMED PROMO_BONUS MANUAL_ADJUSTMENT EXPIRY @@schema("passenger") } enum FoodOrderStatus { PENDING PREPARING READY DELIVERED CANCELLED @@schema("passenger") } enum DevicePlatform { IOS ANDROID WEB @@schema("passenger") } 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? gender String? // Male, Female, Other dateOfBirth DateTime? passportNumber String? nationalId String? failedLoginAttempts Int @default(0) lockedUntil DateTime? blockedUntil DateTime? lastLoginAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt faydaVerified Boolean @default(false) faydaVerifiedAt DateTime? faydaSub String? @unique sessions Session[] passenger Passenger? @@schema("passenger") } 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) @@schema("passenger") } model Passenger { id String @id @default(uuid()) userId String? @unique iamUserId String? @unique defaultTravelerProfileId String? preferredLanguage String? blockedUntil DateTime? createdAt DateTime @default(now()) user User? @relation(fields: [userId], references: [id]) bookings Booking[] loyalty LoyaltyAccount? wallet WalletAccount? notifications Notification[] travelerProfiles TravelerProfile[] savedRoutes SavedRoute[] packageBookings PackageBooking[] supportConversations SupportConversation[] @@index([userId]) @@index([iamUserId]) @@schema("passenger") } model TravelerProfile { id String @id @default(uuid()) passengerId String fullName String gender String? relationship String dateOfBirth DateTime? nationalId String? notes String? createdAt DateTime @default(now()) passenger Passenger @relation(fields: [passengerId], references: [id]) @@schema("passenger") } model Station { id String @id @default(uuid()) code String @unique name String city String countryCode String? sequence Int @default(0) isOperational Boolean @default(true) lat Decimal? @db.Decimal(9, 6) lng Decimal? @db.Decimal(9, 6) originSchedules TrainSchedule[] @relation("OriginTrips") destinationSchedules TrainSchedule[] @relation("DestinationTrips") stopTimes TripStopTime[] crowdSignals StationCrowdSignal[] bookingDepartures Booking[] @relation("BookingPackageDepartureStation") packageBookingDepartures PackageBooking[] @relation("PackageBookingDepartureStation") @@index([city, countryCode]) @@index([sequence]) @@schema("passenger") } model Train { id String @id @default(uuid()) number String @unique name String operatorId String @default("op_edr") operatorName String? description String? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt schedules TrainSchedule[] @@schema("passenger") } model TrainSchedule { id String @id @default(uuid()) trainId String routeId String? originStationId String destinationStationId String departureAt DateTime arrivalAt DateTime durationMinutes Int status TripStatus @default(SCHEDULED) stopsCount Int @default(0) reservedCount Int @default(0) onTimePercent Int @default(100) carbonRating String @default("A") notes String? isPackageOnly Boolean @default(false) train Train @relation(fields: [trainId], references: [id]) route Route? @relation(fields: [routeId], references: [id]) originStation Station @relation("OriginTrips", fields: [originStationId], references: [id]) destinationStation Station @relation("DestinationTrips", fields: [destinationStationId], references: [id]) coachAssignments CoachAssignment[] bookings Booking[] @relation("OutboundSchedule") returnBookings Booking[] @relation("ReturnSchedule") stopTimes TripStopTime[] liveStatus TripLiveStatus? menuItems MenuItem[] journeySegments JourneySegment[] outboundPackages TravelPackage[] @relation("PackageOutbound") returnPackages TravelPackage[] @relation("PackageReturn") @@index([departureAt, originStationId]) @@schema("passenger") } model TripStopTime { id String @id @default(uuid()) scheduleId String stationId String sequence Int plannedArrivalAt DateTime? plannedDepartureAt DateTime? actualArrivalAt DateTime? status StopStatus @default(OPEN) schedule TrainSchedule @relation(fields: [scheduleId], references: [id]) station Station @relation(fields: [stationId], references: [id]) @@unique([scheduleId, sequence]) @@schema("passenger") } model TripLiveStatus { id String @id @default(uuid()) scheduleId String @unique state String currentLocationLabel String? progressPercent Int @default(0) delayMinutes Int @default(0) currentSpeedKph Int? platformLabel String? updatedAt DateTime @updatedAt schedule TrainSchedule @relation(fields: [scheduleId], references: [id]) @@schema("passenger") } model Coach { id String @id @default(uuid()) coachTypeId String number String @unique arrangement String @default("2+2") // e.g., '2+2', '3+2', '2+2+2' capacity Int @default(0) // Total seats/beds sequence Int @default(0) status String @default("ACTIVE") // 'ACTIVE', 'MAINTENANCE', 'INACTIVE' createdAt DateTime @default(now()) updatedAt DateTime @updatedAt coachType CoachType @relation(fields: [coachTypeId], references: [id]) seats Seat[] assignments CoachAssignment[] routeTemplates RouteCoachTemplate[] @@index([coachTypeId]) @@index([sequence]) @@schema("passenger") } model CoachAssignment { id String @id @default(uuid()) scheduleId String coachId String positionNumber Int isOperational Boolean @default(true) createdAt DateTime @default(now()) schedule TrainSchedule @relation(fields: [scheduleId], references: [id]) coach Coach @relation(fields: [coachId], references: [id]) @@unique([scheduleId, positionNumber]) @@index([scheduleId]) @@schema("passenger") } model Seat { id String @id @default(uuid()) coachId String seatNumber String // Auto-generated: e.g., '1', '2', '3' (unique per coach) row Int col String kind SeatKind @default(STANDARD) status SeatStatus @default(AVAILABLE) heldUntil DateTime? isWindow Boolean @default(false) isAisle Boolean @default(false) bedPosition String? // 'lower', 'middle', 'upper' premiumFeeMinor Int @default(0) coach Coach @relation(fields: [coachId], references: [id]) bookingSeats BookingSeat[] blocks SeatBlock[] tickets Ticket[] @@unique([coachId, seatNumber]) @@unique([coachId, row, col]) @@index([coachId]) @@schema("passenger") } model SeatHold { id String @id @default(uuid()) scheduleId String seatIds String[] fareQuoteId String? passengerId String createdBy String? expiresAt DateTime createdAt DateTime @default(now()) @@index([expiresAt]) @@schema("passenger") } model FareRule { id String @id @default(uuid()) tripId String? route String? nationality String? // Ethiopian, Djiboutian, Other seatClassId String baseFareMinor Int seatClass SeatClass @relation(fields: [seatClassId], references: [id]) currency String @default("ETB") refundable Boolean @default(true) validFrom DateTime validUntil DateTime? createdAt DateTime @default(now()) @@schema("passenger") } model Booking { id String @id @default(uuid()) bookingRef String @unique passengerId String scheduleId String packageId String? priceTierId String? bookingType String @default("ONE_WAY") status BookingStatus @default(DRAFT) currency String @default("ETB") totalMinor Int adultCount Int @default(1) childCount Int @default(0) displayCurrency Currency? displayTotalMinor Int? returnScheduleId String? returnOriginStationId String? returnDestinationStationId String? returnHoldId String? returnSeatClassId String? returnLegStatus ReturnLegStatus @default(NOT_APPLICABLE) // Transit leg-2 fields (single-booking transit) leg2ScheduleId String? leg2OriginStationId String? leg2DestinationStationId String? leg2SeatClassId String? // Round-trip transit: return journey transit fields returnLeg2ScheduleId String? returnLeg2OriginStationId String? returnLeg2DestStationId String? returnLeg2SeatClassId String? originStationId String? destinationStationId String? outboundBoardedAt DateTime? returnBoardedAt DateTime? contactEmail String? contactPhone String? userAgent String? source String @default("WEB") promoCode String? paidAt DateTime? paymentReminderSentAt DateTime? packageDepartureStationId String? payToken String? @unique payTokenExpiresAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt passenger Passenger @relation(fields: [passengerId], references: [id]) schedule TrainSchedule @relation("OutboundSchedule", fields: [scheduleId], references: [id]) returnSchedule TrainSchedule? @relation("ReturnSchedule", fields: [returnScheduleId], references: [id]) package TravelPackage? @relation(fields: [packageId], references: [id]) priceTier PackagePriceTier? @relation(fields: [priceTierId], references: [id]) departureStation Station? @relation("BookingPackageDepartureStation", fields: [packageDepartureStationId], references: [id]) seats BookingSeat[] paymentIntent PaymentIntent? tickets Ticket[] foodOrders FoodOrder[] agentBooking AgentBooking? modifications BookingModification[] cancellation BookingCancellation? baggage BaggageBooking[] excessBaggageCharges ExcessBaggageCharge[] supplementaryCharges SupplementaryCharge[] journey Journey? @@index([passengerId, status]) @@index([bookingType]) @@schema("passenger") } model BookingSeat { id String @id @default(uuid()) bookingId String seatId String leg Int @default(1) // 1=outbound/leg-1, 2=return/leg-2 scheduleId String? // which schedule this seat belongs to passengerName String dateOfBirth DateTime? passengerCategory PassengerCategory @default(ADULT) idDocumentType IdDocumentType? idDocumentNumber String? passportNumber String? passportCountry String? verifaydaVerified Boolean @default(false) verifaydaData Json? faydaVerifiedAt DateTime? faydaSub String? faydaVerifiedName String? seatLabelSnapshot String? fareMinor Int? displayCurrency Currency? displayFareMinor Int? booking Booking @relation(fields: [bookingId], references: [id]) seat Seat @relation(fields: [seatId], references: [id]) @@unique([scheduleId, seatId]) @@schema("passenger") } model PaymentMethod { id String @id @default(uuid()) type PaymentMethodType @unique displayName String region PaymentRegion @default(GLOBAL) currency String @default("ETB") providerId String? isDefault Boolean @default(false) enabled Boolean @default(true) sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@schema("passenger") } model PaymentIntent { id String @id @default(uuid()) bookingId String @unique amountMinor Float 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]) @@schema("passenger") } 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]) @@schema("passenger") } 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]) @@schema("passenger") } model Ticket { id String @id @default(uuid()) bookingId String bookingRef String passengerName String seatId String leg Int @default(1) scheduleId String? status String @default("ACTIVE") qrPayload String barcodePayload String? pdfUrl String? deliveryChannel String @default("EMAIL") issuedAt DateTime @default(now()) validatedAt DateTime? validatorId String? boardedAt DateTime? booking Booking @relation(fields: [bookingId], references: [id]) seat Seat @relation(fields: [seatId], references: [id]) validationLogs GateValidationLog[] @@index([bookingId]) @@index([seatId]) @@schema("passenger") } 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[] @@schema("passenger") } 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]) @@schema("passenger") } 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]) @@schema("passenger") } 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]) @@schema("passenger") } 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]) @@schema("passenger") } 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]) @@schema("passenger") } 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()) @@schema("passenger") } 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]) @@schema("passenger") } model WeatherAlert { id String @id @default(uuid()) region String severity String title String message String validUntil DateTime createdAt DateTime @default(now()) @@schema("passenger") } model MenuCategory { id String @id @default(uuid()) name String items MenuItem[] @@schema("passenger") } model MenuItem { id String @id @default(uuid()) scheduleId String categoryId String name String priceMinor Int currency String @default("ETB") available Boolean @default(true) availableUntil DateTime? schedule TrainSchedule @relation(fields: [scheduleId], references: [id]) category MenuCategory @relation(fields: [categoryId], references: [id]) @@schema("passenger") } 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[] @@schema("passenger") } 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]) @@schema("passenger") } model FaqCategory { id String @id @default(uuid()) title String iconKey String? articles FaqArticle[] @@schema("passenger") } model FaqArticle { id String @id @default(uuid()) categoryId String question String answerMarkdown String rank Int @default(0) category FaqCategory @relation(fields: [categoryId], references: [id]) @@schema("passenger") } model SupportConversation { id String @id @default(uuid()) userId String? guestId String? guestName String? guestEmail String? guestPhone String? passengerId String? passengerName String? subject String? assignedAgentId String? status SupportConversationStatus @default(OPEN) lastMessageAt DateTime? lastMessagePreview String? lastMessageSender SupportSender? userLastReadAt DateTime? agentLastReadAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @default(now()) messages SupportMessage[] passenger Passenger? @relation(fields: [passengerId], references: [id]) @@index([userId]) @@index([guestId]) @@index([status, lastMessageAt]) @@schema("passenger") } model SupportMessage { id String @id @default(uuid()) conversationId String sender SupportSender /// NULL for an attachment-only message — absence of text is representable /// rather than smuggled through "". The DTO maps NULL -> "". text String? createdAt DateTime @default(now()) conversation SupportConversation @relation(fields: [conversationId], references: [id]) attachments SupportAttachment[] /// Backs keyset pagination of a thread (newest-first over (createdAt, id)). /// Without it, paging a long thread degrades to a scan per page. @@index([conversationId, createdAt]) @@schema("passenger") } /// A file posted on a support message. /// /// The freight side stores the equivalent in its polymorphic `freight.files` /// table; this app has no such table (and no TypeORM), so chat attachments get a /// purpose-built model rather than a shared one. Bytes live in MinIO — `url` is /// the unsigned object path, signed on read for preview. model SupportAttachment { id String @id @default(uuid()) messageId String name String mimeType String /// Bytes. size Int /// Unsigned MinIO object URL. Not directly fetchable by a browser — the API /// mints a short-lived signed URL per response. url String createdAt DateTime @default(now()) message SupportMessage @relation(fields: [messageId], references: [id], onDelete: Cascade) @@index([messageId]) @@schema("passenger") } model UserPreferences { id String @id @default(uuid()) iamUserId 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") @@schema("passenger") } model Device { id String @id @default(uuid()) iamUserId String platform DevicePlatform name String pushToken String? trusted Boolean @default(false) lastSeenAt DateTime @default(now()) @@schema("passenger") } 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]) @@schema("passenger") } model Journey { id String @id @default(uuid()) passengerId String bookingId String? @unique status String totalMinor Int currency String @default("ETB") createdAt DateTime @default(now()) booking Booking? @relation(fields: [bookingId], references: [id]) journeySegments JourneySegment[] @@schema("passenger") } model JourneySegment { id String @id @default(uuid()) journeyId String scheduleId String segmentOrder Int seatId String? coachId String? departureStationId String arrivalStationId String journey Journey @relation(fields: [journeyId], references: [id]) schedule TrainSchedule @relation(fields: [scheduleId], references: [id]) // Prevents two confirmed bookings from occupying the same seat on the same // schedule hop — the hard DB backstop against application-level race conditions. @@unique([scheduleId, seatId, departureStationId]) @@schema("passenger") } 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]) @@schema("passenger") } model PasswordResetToken { id String @id @default(uuid()) userId String token String @unique expiresAt DateTime used Boolean @default(false) createdAt DateTime @default(now()) @@index([userId]) @@schema("passenger") } model Route { id String @id @default(uuid()) code String @unique name String description String? effectiveFrom DateTime effectiveUntil DateTime? active Boolean @default(true) checkinMinutesBefore Int @default(30) createdAt DateTime @default(now()) stops RouteStop[] fareRules RouteFareRule[] segmentFares SegmentFareRule[] schedules TrainSchedule[] coachTemplates RouteCoachTemplate[] @@schema("passenger") } model RouteStop { id String @id @default(uuid()) routeId String stationId String sequence Int distanceKm Float? checkinMinutesBefore Int? travelMinutesToStop Int? plannedArrivalTime DateTime? plannedDepartureTime DateTime? createdAt DateTime @default(now()) route Route @relation(fields: [routeId], references: [id], onDelete: Cascade) @@unique([routeId, sequence]) @@index([routeId, stationId]) @@schema("passenger") } model RouteCoachTemplate { id String @id @default(uuid()) routeId String coachId String positionNumber Int createdAt DateTime @default(now()) route Route @relation(fields: [routeId], references: [id], onDelete: Cascade) coach Coach @relation(fields: [coachId], references: [id]) @@unique([routeId, positionNumber]) @@index([routeId]) @@schema("passenger") } model RouteFareRule { id String @id @default(uuid()) routeId String seatClassId String 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) seatClass SeatClass @relation(fields: [seatClassId], references: [id]) @@index([routeId, seatClassId]) @@schema("passenger") } model SegmentFareRule { id String @id @default(uuid()) routeId String originStopSequence Int destinationStopSequence Int seatClassId String baseFareMinor Int nationality String? // Optional: Ethiopian, Djiboutian, Other currency String @default("ETB") validFrom DateTime validUntil DateTime? createdAt DateTime @default(now()) route Route @relation(fields: [routeId], references: [id], onDelete: Cascade) seatClass SeatClass @relation(fields: [seatClassId], references: [id]) @@unique([routeId, originStopSequence, destinationStopSequence, seatClassId, nationality]) @@index([routeId, seatClassId]) @@schema("passenger") } model Agent { id String @id @default(uuid()) iamUserId String? @unique agentCode String @unique stationId String? commissionRate Int @default(5) active Boolean @default(true) createdAt DateTime @default(now()) bookings AgentBooking[] shifts AgentShift[] commissions AgentCommission[] @@index([iamUserId]) @@schema("passenger") } 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]) @@schema("passenger") } 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]) @@schema("passenger") } 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]) @@schema("passenger") } 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]) @@schema("passenger") } 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]) @@schema("passenger") } model GateValidationLog { id String @id @default(uuid()) ticketId String validatorId String gateId String? leg String? // 'OUTBOUND' | 'RETURN' — for round-trip tickets status String reason String? validatedAt DateTime @default(now()) ticket Ticket @relation(fields: [ticketId], references: [id]) @@index([ticketId]) @@index([validatorId]) @@schema("passenger") } model BaggageAllowance { id String @id @default(uuid()) seatClassId String maxWeightKg Int maxPiecesCount Int excessFeePerKg Int currency String @default("ETB") createdAt DateTime @default(now()) @@schema("passenger") } 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]) @@schema("passenger") } model SupplementaryCharge { id String @id @default(uuid()) bookingId String reason String // e.g. "UNDERPAYMENT", "FARE_CORRECTION" amountMinor Int currency String @default("ETB") status String @default("PENDING") // PENDING | PAID | WAIVED | EXPIRED paymentToken String @unique @default(uuid()) providerTxnId String? notes String? createdBy String paidAt DateTime? expiresAt DateTime? createdAt DateTime @default(now()) booking Booking @relation(fields: [bookingId], references: [id]) @@index([bookingId]) @@index([paymentToken]) @@index([status]) @@schema("passenger") } model ExcessBaggageCharge { id String @id @default(uuid()) bookingId String agentId String excessWeightKg Int feePerKgMinor Int totalMinor Int currency String @default("ETB") status String @default("PENDING") // PENDING | PAID | EXPIRED | WAIVED | CASH_COLLECTED paymentToken String @unique @default(uuid()) expiresAt DateTime paidAt DateTime? waivedBy String? waivedReason String? contactPhone String? contactEmail String? createdAt DateTime @default(now()) booking Booking @relation(fields: [bookingId], references: [id]) @@index([bookingId]) @@index([paymentToken]) @@index([status]) @@schema("passenger") } model AuditLog { id String @id @default(uuid()) iamUserId String? /// Actor's display name, denormalized at write time so a reader needs no cross-schema /// lookup into iam.users and the trail survives the IAM user being deleted. userName String? /// Actor's contact number, denormalized for the same reason. userPhone String? action String entityType String entityId String? oldData Json? newData Json? ipAddress String? userAgent String? createdAt DateTime @default(now()) @@index([iamUserId, createdAt]) @@index([entityType, entityId]) @@index([createdAt]) @@schema("passenger") } model NotificationTemplate { id String @id @default(uuid()) code String @unique channel String subject String? bodyTemplate String active Boolean @default(true) createdAt DateTime @default(now()) @@schema("passenger") } /// Why a seat was pulled out of sale. Coarse bucket for reporting; the free-text /// `reason` stays as the operator's detail. Nullable — rows written before this /// column existed have no category and report as "Uncategorized". enum SeatBlockReasonCategory { MAINTENANCE VIP_RESERVED SAFETY OPERATIONAL OTHER @@schema("passenger") } model SeatBlock { id String @id @default(uuid()) seatId String scheduleId String? reason String reasonCategory SeatBlockReasonCategory? blockedBy String /// Display name of the blocking staff member, denormalized at write time so the /// revenue-loss report needs no cross-service IAM lookup. Null on legacy rows. blockedByName String? approvedBy String? blockedAt DateTime @default(now()) unblockAt DateTime? seat Seat @relation(fields: [seatId], references: [id]) @@index([seatId]) @@index([scheduleId]) @@index([blockedAt]) @@index([scheduleId, seatId]) @@schema("passenger") } model OperationalReport { id String @id @default(uuid()) reportType String dateFrom DateTime dateTo DateTime data Json generatedBy String? createdAt DateTime @default(now()) @@index([reportType, dateFrom]) @@schema("passenger") } 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 @@schema("passenger") } model FraudAlert { id String @id @default(uuid()) iamUserId String eventType String triggeredRules String[] context Json severity String @default("MEDIUM") acknowledged Boolean @default(false) acknowledgedAt DateTime? createdAt DateTime @default(now()) @@index([iamUserId, createdAt]) @@index([acknowledged]) @@schema("passenger") } 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]) @@schema("passenger") } 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]) @@schema("passenger") } model SavedPassengerProfile { id String @id @default(uuid()) userId String? deviceId String? passengerName String dateOfBirth DateTime idDocumentType IdDocumentType passportNumber String? passportCountry String? nationality String? phone String? email String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId]) @@index([deviceId]) @@schema("passenger") } model FaydaVerificationSession { id String @id @default(uuid()) state String @unique codeVerifier String purpose String @default("VERIFY") // VERIFY | LOGIN platform String @default("WEB") // WEB | MOBILE — recorded for audit redirectUri String? // exact OAuth redirect_uri sent to eSignet at start; reused verbatim at token exchange (domain-aware for WEB) saveToAccount Boolean @default(false) status String @default("PENDING") errorCode String? errorDescription String? authCode String? createdAt DateTime @default(now()) expiresAt DateTime completedAt DateTime? iamUserId String? bookingId String? @@index([iamUserId]) @@index([bookingId]) @@index([state]) @@index([expiresAt]) @@schema("passenger") } model SystemConfig { id String @id @default(uuid()) key String @unique value String updatedAt DateTime @updatedAt @@schema("passenger") } enum PackageStatus { DRAFT ACTIVE SOLD_OUT EXPIRED CANCELLED @@schema("passenger") } model TravelPackage { id String @id @default(uuid()) code String @unique name String description String? status PackageStatus @default(DRAFT) outboundScheduleId String returnScheduleId String originStationId String destinationStationId String boardingTime DateTime departureTime DateTime arrivalTime DateTime totalCapacity Int bookedCount Int @default(0) includedServices Json coachConfiguration String? busTransferIncluded Boolean @default(false) busTransferRoute String? validFrom DateTime validUntil DateTime createdAt DateTime @default(now()) updatedAt DateTime @updatedAt outboundSchedule TrainSchedule @relation("PackageOutbound", fields: [outboundScheduleId], references: [id]) returnSchedule TrainSchedule @relation("PackageReturn", fields: [returnScheduleId], references: [id]) priceTiers PackagePriceTier[] bookings Booking[] packageBookings PackageBooking[] inquiries PackageInquiry[] @@index([status, validFrom]) @@schema("passenger") } model PackagePriceTier { id String @id @default(uuid()) packageId String seatClassId String? seatType String label String priceMinor Int currency String @default("ETB") availableSeats Int @default(0) bookedSeats Int @default(0) package TravelPackage @relation(fields: [packageId], references: [id]) seatClass SeatClass? @relation(fields: [seatClassId], references: [id]) bookings Booking[] packageBookings PackageBooking[] inquiries PackageInquiry[] @@unique([packageId, seatType]) @@schema("passenger") } model PackageBooking { id String @id @default(uuid()) bookingRef String @unique packageId String priceTierId String passengerId String? contactEmail String? contactPhone String? status BookingStatus @default(PENDING_PAYMENT) passengerCount Int @default(1) adultCount Int @default(1) childCount Int @default(0) totalMinor Int currency String @default("ETB") displayCurrency Currency? displayTotalMinor Int? promoCode String? source String @default("WEB") paidAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt packageDepartureStationId String? package TravelPackage @relation(fields: [packageId], references: [id]) priceTier PackagePriceTier @relation(fields: [priceTierId], references: [id]) passenger Passenger? @relation(fields: [passengerId], references: [id]) departureStation Station? @relation("PackageBookingDepartureStation", fields: [packageDepartureStationId], references: [id]) passengers PackageBookingPassenger[] paymentIntent PackagePaymentIntent? @@index([packageId, status]) @@schema("passenger") } model PackageBookingPassenger { id String @id @default(uuid()) bookingId String passengerName String dateOfBirth DateTime? idDocumentType IdDocumentType? idDocumentNumber String? passportNumber String? passportCountry String? seatLabel String? booking PackageBooking @relation(fields: [bookingId], references: [id]) @@schema("passenger") } model PackagePaymentIntent { id String @id @default(uuid()) packageBookingId String @unique amountMinor Int currency String @default("ETB") method PaymentMethodType status PaymentIntentStatus @default(REQUIRES_ACTION) providerRef String? paidAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt packageBooking PackageBooking @relation(fields: [packageBookingId], references: [id]) @@schema("passenger") } model PackageInquiry { id String @id @default(uuid()) packageId String priceTierId String? travelerCount Int contactName String contactEmail String? contactPhone String? notes String? status String @default("NEW") enquiredAt DateTime @default(now()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt package TravelPackage @relation(fields: [packageId], references: [id]) priceTier PackagePriceTier? @relation(fields: [priceTierId], references: [id]) @@index([packageId]) @@schema("passenger") } model AppRelease { id String @id @default(uuid()) os String // "android" | "ios" version String forceUpdate Boolean @default(false) storeLink String? notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([os, version]) @@schema("passenger") }