OOD - Car Rental System
Problem
Design an object-oriented system for a modern vehicle rental company that operates across multiple locations. The system must handle vehicle inventory, customer reservations, rental transactions, damage tracking, and dynamic pricing. Your design should demonstrate SOLID principles and incorporate appropriate design patterns to ensure extensibility for new vehicle types and pricing strategies.
Solution
1. Requirements Analysis
Functional Requirements:
- Manage diverse vehicle inventory (sedans, SUVs, trucks, motorcycles, electric vehicles) across multiple rental locations
- Enable customers to search and filter available vehicles by location, dates, type, and features
- Handle complete reservation lifecycle: creation, modification, confirmation, and cancellation with refund logic
- Implement flexible pricing system that can apply different strategies (base rate, seasonal adjustments, demand-based)
- Process vehicle checkout with license verification, initial damage inspection, and mileage recording
- Handle vehicle returns with damage comparison, late fee calculation, fuel charge assessment
- Track all damage reports with photos and repair cost estimation
- Support add-ons (GPS, child seats, insurance) with separate pricing
- Maintain customer profiles with rental history and loyalty status
- Send automated notifications for confirmations, reminders, and overdue alerts
Non-Functional Requirements:
- Extensibility: Adding new vehicle types or pricing rules should not require modifying existing classes (Open/Closed Principle)
- Maintainability: Each class should have a single, well-defined responsibility (Single Responsibility Principle)
- Testability: Dependencies should be based on interfaces to allow easy mocking
- Performance: Vehicle availability queries must handle concurrent reservation attempts
2. Use Case Diagram
Actors:
- Customer: Searches vehicles, makes/modifies/cancels bookings
- Agent: Processes check-outs and returns, assists customers
- Manager: Manages fleet, configures pricing, views reports
- System: Auto-sends notifications, enforces business rules
Primary Use Cases:
- Search Vehicles - Find available vehicles matching criteria
- Create Reservation - Book a vehicle for specific dates
- Modify/Cancel Reservation - Change booking or get refund
- Check Out Vehicle - Verify customer, inspect vehicle, hand over keys
- Return Vehicle - Accept vehicle, assess condition, calculate charges
- Configure Pricing - Set up pricing rules and strategies
- Manage Vehicle Fleet - Add/update/deactivate vehicles
graph TD subgraph RentalSystem["Car Rental System"] UC1[Search Vehicles] UC2[Create Reservation] UC3[Modify Reservation] UC4[Cancel Reservation] UC5[Check Out Vehicle] UC6[Return Vehicle] UC7[Configure Pricing] UC8[Manage Fleet] UC9[Send Notifications] end Customer[๐ค Customer] Agent[๐ Agent] Manager[๐จโ๐ผ Manager] System[๐ฅ๏ธ System] Customer --> UC1 Customer --> UC2 Customer --> UC3 Customer --> UC4 Agent --> UC5 Agent --> UC6 Manager --> UC7 Manager --> UC8 System --> UC9 UC2 -.includes.-> UC7 UC5 -.includes.-> UC9 style Customer fill:#E3F2FD,stroke:#1976D2,stroke-width:2px style Agent fill:#FFF3E0,stroke:#F57C00,stroke-width:2px style Manager fill:#F3E5F5,stroke:#7B1FA2,stroke-width:2px style System fill:#E8F5E9,stroke:#388E3C,stroke-width:2px style UC1 fill:#B3E5FC,stroke:#0277BD,stroke-width:2px style UC2 fill:#B3E5FC,stroke:#0277BD,stroke-width:2px style UC3 fill:#B3E5FC,stroke:#0277BD,stroke-width:2px style UC4 fill:#B3E5FC,stroke:#0277BD,stroke-width:2px style UC5 fill:#FFE0B2,stroke:#E65100,stroke-width:2px style UC6 fill:#FFE0B2,stroke:#E65100,stroke-width:2px style UC7 fill:#E1BEE7,stroke:#6A1B9A,stroke-width:2px style UC8 fill:#E1BEE7,stroke:#6A1B9A,stroke-width:2px style UC9 fill:#C8E6C9,stroke:#2E7D32,stroke-width:2px
3. Class Diagram
Core Classes and Their Responsibilities:
Vehicle Hierarchy (Demonstrates Liskov Substitution Principle):
Vehicle(abstract) - Base class with common attributes and behaviorsSedan,SUV,Truck,Motorcycle- Specific vehicle implementations
Reservation Management:
Reservation- Manages booking lifecycle and state transitionsRentalTransaction- Records actual rental with pickup/return details
Customer Management:
Customer- Stores customer info and rental historyDriverLicense- Encapsulates license validation logic
Pricing (Strategy Pattern - demonstrates Open/Closed Principle):
PricingCalculator(interface) - Defines pricing contractBaseRatePricing- Simple per-day pricingSeasonalPricing- Adjusts rates by seasonDemandBasedPricing- Dynamic pricing based on availability
Damage Tracking:
DamageReport- Documents vehicle conditionDamageItem- Individual damage with severity and cost
Infrastructure (Repository Pattern):
VehicleRepository- Abstracts vehicle data accessReservationRepository- Handles reservation persistence
Notifications (Observer Pattern):
NotificationService- Sends alerts for various events
classDiagram %% Vehicle hierarchy class Vehicle { <<abstract>> #String vin #String make #String model #int manufactureYear #int currentMileage #VehicleStatus status #Location assignedLocation +getDetails() VehicleInfo +isAvailableFor(startDate, endDate) boolean +reserveForPeriod(startDate, endDate) +markAsRented() +markAsReturned() +getDailyRate() Money* } class Sedan { -boolean hasSunroof -int trunkCapacityLiters +getDailyRate() Money } class SUV { -String driveType -int passengerCapacity +getDailyRate() Money } class Truck { -int payloadKg -boolean hasTowHitch +getDailyRate() Money } class Motorcycle { -int engineCC -String bikeStyle +getDailyRate() Money } Vehicle <|-- Sedan Vehicle <|-- SUV Vehicle <|-- Truck Vehicle <|-- Motorcycle %% Reservation system class Reservation { -String reservationId -Customer customer -Vehicle vehicle -Date startDate -Date endDate -ReservationStatus status -Money totalCost -List~RentalAddOn~ addOns +confirm() +cancel() Money +calculateRefund() Money } class RentalTransaction { -String transactionId -Reservation reservation -DateTime checkOutTime -DateTime expectedReturnTime -int initialMileage -FuelLevel initialFuel -DamageReport initialDamage -DateTime actualReturnTime -int finalMileage -FuelLevel finalFuel -DamageReport finalDamage +calculateCharges() Money +isOverdue() boolean } %% Customer class Customer { -String customerId -String name -String email -DriverLicense license -MembershipTier tier -List~Reservation~ history +isEligibleToRent() boolean +getDiscountRate() decimal } class DriverLicense { -String number -String state -Date expiryDate +isValid() boolean +isExpired() boolean } %% Pricing Strategy Pattern class PricingCalculator { <<interface>> +calculatePrice(Reservation) Money } class BaseRatePricing { -Map~VehicleType Money~ dailyRates +calculatePrice(Reservation) Money } class SeasonalPricing { -PricingCalculator basePricing -Map~Season decimal~ seasonMultipliers +calculatePrice(Reservation) Money } class DemandBasedPricing { -PricingCalculator basePricing -VehicleRepository vehicleRepo +calculatePrice(Reservation) Money -getUtilizationRate(vehicle, dates) decimal } PricingCalculator <|.. BaseRatePricing PricingCalculator <|.. SeasonalPricing PricingCalculator <|.. DemandBasedPricing SeasonalPricing --> PricingCalculator : wraps DemandBasedPricing --> PricingCalculator : wraps %% Damage tracking class DamageReport { -String reportId -DateTime inspectionTime -String inspectorName -List~DamageItem~ items -List~String~ photoUrls +addDamageItem(DamageItem) +getTotalRepairCost() Money +compareTo(DamageReport) List~DamageItem~ } class DamageItem { -String description -DamageSeverity severity -String location -Money estimatedCost } %% Location class Location { -String locationId -String name -String address -List~Vehicle~ inventory +getAvailableVehicles(startDate, endDate) List~Vehicle~ } %% Repositories class VehicleRepository { <<interface>> +findById(vin) Vehicle +findAvailableByType(type, location, dates) List~Vehicle~ +save(Vehicle) +getUtilization(location, vehicleType, dates) decimal } class ReservationRepository { <<interface>> +findById(reservationId) Reservation +findByCustomer(customerId) List~Reservation~ +findOverlapping(vehicle, dates) List~Reservation~ +save(Reservation) } %% Notification Service class NotificationService { +sendReservationConfirmation(Reservation) +sendCheckOutConfirmation(RentalTransaction) +sendOverdueAlert(RentalTransaction) +sendReturnReceipt(RentalTransaction, Money) } %% Relationships Reservation --> Customer Reservation --> Vehicle RentalTransaction --> Reservation RentalTransaction --> DamageReport Vehicle --> Location DamageReport --> DamageItem SeasonalPricing ..> VehicleRepository DemandBasedPricing ..> VehicleRepository
Design Patterns Used:
-
Strategy Pattern -
PricingCalculatorinterface allows swapping pricing algorithms:- Open/Closed: New pricing strategies can be added without modifying existing code
- Each strategy encapsulates a different algorithm
- Pricing can be changed at runtime
-
Repository Pattern -
VehicleRepositoryandReservationRepository:- Separates data access logic from business logic
- Allows switching between different data sources
- Facilitates testing with mock repositories
-
Template Method (implied in Vehicle) -
Vehicle.getDailyRate()is abstract:- Base class defines structure, subclasses provide specifics
- Ensures all vehicles implement rate calculation
-
Observer Pattern -
NotificationService:- System sends notifications when reservation states change
- Decouples notification logic from core business logic
SOLID Principles Demonstrated:
-
Single Responsibility: Each class has one reason to change
Reservationhandles booking lifecycleDamageReporthandles damage trackingPricingCalculatoronly calculates prices
-
Open/Closed: Classes open for extension, closed for modification
- New vehicle types can be added by extending
Vehicle - New pricing strategies without changing
Reservation
- New vehicle types can be added by extending
-
Liskov Substitution: Subtypes can replace base types
- Any
Vehiclesubclass works whereVehicleis expected - Any
PricingCalculatorimplementation is interchangeable
- Any
-
Interface Segregation: Clients depend only on methods they use
- Repository interfaces are focused and minimal
-
Dependency Inversion: Depend on abstractions, not concretions
- Services depend on
PricingCalculatorinterface, not concrete implementations - Business logic depends on
Repositoryinterfaces, not databases
- Services depend on
4. Activity Diagrams
Activity: Creating a Reservation
flowchart TD Start([๐ Customer wants to rent]) --> Search[๐ Search available vehicles] Search --> Select[๐ Select vehicle and dates] Select --> Check{โ Vehicle available?} Check -->|No| Alternatives[๐ก Show alternatives] Alternatives --> Select Check -->|Yes| CalcPrice[๐ฐ Calculate total price] CalcPrice --> SelectAddOns[๐ ๏ธ Select add-ons] SelectAddOns --> Review[๐ Review booking summary] Review --> Confirm{๐ค Customer confirms?} Confirm -->|No| Cancel([โ End - Cancelled]) Confirm -->|Yes| Payment[๐ณ Process payment] Payment --> PaySuccess{โ Payment OK?} PaySuccess -->|No| PaymentFail[โ ๏ธ Show error] PaymentFail --> Payment PaySuccess -->|Yes| CreateRes[๐ Create reservation] CreateRes --> SendEmail[๐ง Send confirmation email] SendEmail --> End([โ Reservation confirmed]) style Start fill:#E1F5FE,stroke:#01579B,stroke-width:3px style End fill:#C8E6C9,stroke:#2E7D32,stroke-width:3px style Cancel fill:#FFCDD2,stroke:#C62828,stroke-width:3px style Search fill:#B3E5FC,stroke:#0277BD,stroke-width:2px style Select fill:#B3E5FC,stroke:#0277BD,stroke-width:2px style CalcPrice fill:#B3E5FC,stroke:#0277BD,stroke-width:2px style SelectAddOns fill:#B3E5FC,stroke:#0277BD,stroke-width:2px style Review fill:#B3E5FC,stroke:#0277BD,stroke-width:2px style CreateRes fill:#A5D6A7,stroke:#388E3C,stroke-width:2px style SendEmail fill:#A5D6A7,stroke:#388E3C,stroke-width:2px style Payment fill:#FFE082,stroke:#F57F17,stroke-width:2px style PaymentFail fill:#FFAB91,stroke:#D84315,stroke-width:2px style Alternatives fill:#CE93D8,stroke:#6A1B9A,stroke-width:2px style Check fill:#FFF9C4,stroke:#F9A825,stroke-width:2px style Confirm fill:#FFF9C4,stroke:#F9A825,stroke-width:2px style PaySuccess fill:#FFF9C4,stroke:#F9A825,stroke-width:2px
Activity: Vehicle Check-Out
flowchart TD Start([๐ค Customer arrives]) --> GetRes[๐ Retrieve reservation] GetRes --> VerifyRes{โ Valid reservation?} VerifyRes -->|No| Error([โ End - Error]) VerifyRes -->|Yes| CheckLicense[๐ชช Verify driver license] CheckLicense --> LicenseOK{โ License valid?} LicenseOK -->|No| Error LicenseOK -->|Yes| Inspect[๐ Inspect vehicle exterior] Inspect --> TakePhotos[๐ธ Take damage photos] TakePhotos --> CreateDamageReport[๐ Create damage report] CreateDamageReport --> RecordMileage[โฝ Record mileage & fuel] RecordMileage --> SignContract[โ๏ธ Customer signs agreement] SignContract --> CollectDeposit[๐ณ Collect security deposit] CollectDeposit --> DepositOK{โ Deposit authorized?} DepositOK -->|No| Error DepositOK -->|Yes| ActivateRental[๐ Create rental transaction] ActivateRental --> UpdateStatus[๐ Update reservation to ACTIVE] UpdateStatus --> HandKeys[๐ Hand over keys] HandKeys --> SendNotif[๐ง Send checkout confirmation] SendNotif --> End([โ Vehicle checked out]) style Start fill:#FFF3E0,stroke:#E65100,stroke-width:3px style End fill:#C8E6C9,stroke:#2E7D32,stroke-width:3px style Error fill:#FFCDD2,stroke:#C62828,stroke-width:3px style GetRes fill:#FFE0B2,stroke:#EF6C00,stroke-width:2px style CheckLicense fill:#FFE0B2,stroke:#EF6C00,stroke-width:2px style Inspect fill:#FFCC80,stroke:#F57C00,stroke-width:2px style TakePhotos fill:#FFCC80,stroke:#F57C00,stroke-width:2px style CreateDamageReport fill:#FFCC80,stroke:#F57C00,stroke-width:2px style RecordMileage fill:#FFB74D,stroke:#FB8C00,stroke-width:2px style SignContract fill:#FFB74D,stroke:#FB8C00,stroke-width:2px style CollectDeposit fill:#FFE082,stroke:#F9A825,stroke-width:2px style ActivateRental fill:#AED581,stroke:#689F38,stroke-width:2px style UpdateStatus fill:#AED581,stroke:#689F38,stroke-width:2px style HandKeys fill:#81C784,stroke:#388E3C,stroke-width:2px style SendNotif fill:#81C784,stroke:#388E3C,stroke-width:2px style VerifyRes fill:#FFF59D,stroke:#FBC02D,stroke-width:2px style LicenseOK fill:#FFF59D,stroke:#FBC02D,stroke-width:2px style DepositOK fill:#FFF59D,stroke:#FBC02D,stroke-width:2px
Activity: Vehicle Return
flowchart TD Start([๐ Customer returns]) --> GetTrans[๐ Retrieve active transaction] GetTrans --> InspectReturn[๐ Inspect vehicle] InspectReturn --> PhotosReturn[๐ธ Take photos] PhotosReturn --> CreateReturnReport[๐ Create return damage report] CreateReturnReport --> CompareDamage{๐ง New damage?} CompareDamage -->|Yes| AssessDamage[๐ฐ Assess damage cost] CompareDamage -->|No| CheckMileage[๐ Record final mileage] AssessDamage --> CheckMileage CheckMileage --> CheckFuel[โฝ Check fuel level] CheckFuel --> CalcBase[๐ต Calculate base rental charge] CalcBase --> CheckLate{โฐ Late return?} CheckLate -->|Yes| AddLateFee[๐ธ Add late fees] CheckLate -->|No| CheckFuelCharge{โฝ Fuel low?} AddLateFee --> CheckFuelCharge CheckFuelCharge -->|Yes| AddFuelFee[๐ฐ Add fuel charge] CheckFuelCharge -->|No| AddDamageCharge{๐ง Damage charges?} AddFuelFee --> AddDamageCharge AddDamageCharge -->|Yes| AddDamageFee[๐ธ Add damage fees] AddDamageCharge -->|No| GenInvoice[๐งพ Generate final invoice] AddDamageFee --> GenInvoice GenInvoice --> ProcessPay[๐ณ Process final payment] ProcessPay --> PayOK{โ Payment successful?} PayOK -->|No| PayError([โ Payment failed]) PayOK -->|Yes| ReleaseDeposit[๐ฐ Release security deposit] ReleaseDeposit --> UpdateResStatus[๐ Update reservation to COMPLETED] UpdateResStatus --> MarkVehicle[โ Mark vehicle AVAILABLE] MarkVehicle --> SendReceipt[๐ง Send receipt email] SendReceipt --> End([โ Return completed]) style Start fill:#F3E5F5,stroke:#7B1FA2,stroke-width:3px style End fill:#C8E6C9,stroke:#2E7D32,stroke-width:3px style PayError fill:#FFCDD2,stroke:#C62828,stroke-width:3px style GetTrans fill:#E1BEE7,stroke:#8E24AA,stroke-width:2px style InspectReturn fill:#E1BEE7,stroke:#8E24AA,stroke-width:2px style PhotosReturn fill:#CE93D8,stroke:#7B1FA2,stroke-width:2px style CreateReturnReport fill:#CE93D8,stroke:#7B1FA2,stroke-width:2px style AssessDamage fill:#FFAB91,stroke:#D84315,stroke-width:2px style AddDamageFee fill:#FFAB91,stroke:#D84315,stroke-width:2px style AddLateFee fill:#FFAB91,stroke:#D84315,stroke-width:2px style AddFuelFee fill:#FFAB91,stroke:#D84315,stroke-width:2px style CheckMileage fill:#BA68C8,stroke:#6A1B9A,stroke-width:2px style CheckFuel fill:#BA68C8,stroke:#6A1B9A,stroke-width:2px style CalcBase fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff style GenInvoice fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff style ProcessPay fill:#FFE082,stroke:#F9A825,stroke-width:2px style ReleaseDeposit fill:#AED581,stroke:#689F38,stroke-width:2px style UpdateResStatus fill:#81C784,stroke:#388E3C,stroke-width:2px style MarkVehicle fill:#66BB6A,stroke:#2E7D32,stroke-width:2px style SendReceipt fill:#66BB6A,stroke:#2E7D32,stroke-width:2px style CompareDamage fill:#FFF59D,stroke:#FBC02D,stroke-width:2px style CheckLate fill:#FFF59D,stroke:#FBC02D,stroke-width:2px style CheckFuelCharge fill:#FFF59D,stroke:#FBC02D,stroke-width:2px style AddDamageCharge fill:#FFF59D,stroke:#FBC02D,stroke-width:2px style PayOK fill:#FFF59D,stroke:#FBC02D,stroke-width:2px
5. High-Level Code Implementation
Java
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.math.BigDecimal;
import java.util.*;
// ============================================================================
// Enums
// ============================================================================
enum VehicleStatus {
AVAILABLE, RESERVED, RENTED, IN_MAINTENANCE, RETIRED
}
enum ReservationStatus {
PENDING_PAYMENT, CONFIRMED, ACTIVE, COMPLETED, CANCELLED
}
enum DamageSeverity {
MINOR, MODERATE, SEVERE, TOTALED
}
enum FuelLevel {
EMPTY, QUARTER, HALF, THREE_QUARTER, FULL
}
enum MembershipTier {
BASIC(0.0),
SILVER(0.05),
GOLD(0.10),
PLATINUM(0.15);
private final double discountRate;
MembershipTier(double rate) {
this.discountRate = rate;
}
public double getDiscountRate() {
return discountRate;
}
}
// ============================================================================
// Value Objects / Helper Classes
// ============================================================================
class Money {
private final BigDecimal amount;
private final String currency;
public Money(double amount) {
this(BigDecimal.valueOf(amount), "USD");
}
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public Money add(Money other) {
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(double factor) {
return new Money(this.amount.multiply(BigDecimal.valueOf(factor)), this.currency);
}
public boolean isZero() {
return amount.compareTo(BigDecimal.ZERO) == 0;
}
}
class DateRange {
private final LocalDate startDate;
private final LocalDate endDate;
public DateRange(LocalDate start, LocalDate end) {
if (start.isAfter(end)) {
throw new IllegalArgumentException("Start date must be before end date");
}
this.startDate = start;
this.endDate = end;
}
public long getDays() {
return ChronoUnit.DAYS.between(startDate, endDate);
}
public boolean overlaps(DateRange other) {
return !this.endDate.isBefore(other.startDate) &&
!this.startDate.isAfter(other.endDate);
}
public LocalDate getStartDate() { return startDate; }
public LocalDate getEndDate() { return endDate; }
}
// ============================================================================
// Domain Classes - Vehicle Hierarchy
// ============================================================================
abstract class Vehicle {
protected String vin;
protected String make;
protected String model;
protected int manufactureYear;
protected int currentMileage;
protected VehicleStatus status;
protected Location assignedLocation;
public Vehicle(String vin, String make, String model, int year) {
this.vin = vin;
this.make = make;
this.model = model;
this.manufactureYear = year;
this.status = VehicleStatus.AVAILABLE;
}
// Template method - each subclass must implement
public abstract Money getDailyRate();
public boolean isAvailableFor(DateRange period) {
return status == VehicleStatus.AVAILABLE;
}
public void markAsRented() {
if (status != VehicleStatus.AVAILABLE) {
throw new IllegalStateException("Vehicle not available for rent");
}
this.status = VehicleStatus.RENTED;
}
public void markAsReturned() {
this.status = VehicleStatus.AVAILABLE;
}
public void updateMileage(int mileage) {
if (mileage < this.currentMileage) {
throw new IllegalArgumentException("Cannot decrease mileage");
}
this.currentMileage = mileage;
}
// Getters
public String getVin() { return vin; }
public VehicleStatus getStatus() { return status; }
}
class Sedan extends Vehicle {
private boolean hasSunroof;
private int trunkCapacityLiters;
public Sedan(String vin, String make, String model, int year,
boolean hasSunroof, int trunkCapacity) {
super(vin, make, model, year);
this.hasSunroof = hasSunroof;
this.trunkCapacityLiters = trunkCapacity;
}
@Override
public Money getDailyRate() {
return new Money(45.00);
}
}
class SUV extends Vehicle {
private String driveType; // "AWD", "FWD", "4WD"
private int passengerCapacity;
public SUV(String vin, String make, String model, int year,
String driveType, int capacity) {
super(vin, make, model, year);
this.driveType = driveType;
this.passengerCapacity = capacity;
}
@Override
public Money getDailyRate() {
return new Money(75.00);
}
public int getPassengerCapacity() { return passengerCapacity; }
}
class Truck extends Vehicle {
private int payloadKg;
private boolean hasTowHitch;
public Truck(String vin, String make, String model, int year,
int payload, boolean towHitch) {
super(vin, make, model, year);
this.payloadKg = payload;
this.hasTowHitch = towHitch;
}
@Override
public Money getDailyRate() {
return new Money(95.00);
}
}
class Motorcycle extends Vehicle {
private int engineCC;
private String bikeStyle; // "Sport", "Cruiser", "Touring"
public Motorcycle(String vin, String make, String model, int year,
int cc, String style) {
super(vin, make, model, year);
this.engineCC = cc;
this.bikeStyle = style;
}
@Override
public Money getDailyRate() {
return new Money(35.00);
}
}
// ============================================================================
// Customer & License
// ============================================================================
class DriverLicense {
private String number;
private String issuingState;
private LocalDate expiryDate;
public DriverLicense(String number, String state, LocalDate expiry) {
this.number = number;
this.issuingState = state;
this.expiryDate = expiry;
}
public boolean isValid() {
return !isExpired();
}
public boolean isExpired() {
return LocalDate.now().isAfter(expiryDate);
}
}
class Customer {
private String customerId;
private String name;
private String email;
private String phone;
private DriverLicense license;
private MembershipTier tier;
private List<Reservation> rentalHistory;
public Customer(String id, String name, String email, DriverLicense license) {
this.customerId = id;
this.name = name;
this.email = email;
this.license = license;
this.tier = MembershipTier.BASIC;
this.rentalHistory = new ArrayList<>();
}
public boolean isEligibleToRent() {
return license != null && license.isValid() && !hasOverdueRentals();
}
private boolean hasOverdueRentals() {
return rentalHistory.stream()
.anyMatch(r -> r.getStatus() == ReservationStatus.ACTIVE && r.isOverdue());
}
public double getDiscountRate() {
return tier.getDiscountRate();
}
// Getters
public String getCustomerId() { return customerId; }
public String getName() { return name; }
public String getEmail() { return email; }
public MembershipTier getTier() { return tier; }
}
// ============================================================================
// Reservation System
// ============================================================================
class Reservation {
private String reservationId;
private Customer customer;
private Vehicle vehicle;
private DateRange rentalPeriod;
private ReservationStatus status;
private Money totalCost;
private List<RentalAddOn> addOns;
private LocalDateTime createdAt;
public Reservation(String id, Customer customer, Vehicle vehicle, DateRange period) {
this.reservationId = id;
this.customer = customer;
this.vehicle = vehicle;
this.rentalPeriod = period;
this.status = ReservationStatus.PENDING_PAYMENT;
this.addOns = new ArrayList<>();
this.createdAt = LocalDateTime.now();
}
public void confirm() {
if (status != ReservationStatus.PENDING_PAYMENT) {
throw new IllegalStateException("Can only confirm pending reservations");
}
this.status = ReservationStatus.CONFIRMED;
}
public Money cancel() {
if (status == ReservationStatus.COMPLETED || status == ReservationStatus.CANCELLED) {
throw new IllegalStateException("Cannot cancel this reservation");
}
this.status = ReservationStatus.CANCELLED;
return calculateRefund();
}
public Money calculateRefund() {
long hoursUntilStart = ChronoUnit.HOURS.between(
LocalDateTime.now(),
rentalPeriod.getStartDate().atStartOfDay()
);
if (hoursUntilStart > 48) {
return totalCost; // Full refund
} else if (hoursUntilStart > 24) {
return totalCost.multiply(0.5); // 50% refund
} else {
return new Money(0); // No refund
}
}
public boolean isOverdue() {
return status == ReservationStatus.ACTIVE &&
LocalDate.now().isAfter(rentalPeriod.getEndDate());
}
public void setTotalCost(Money cost) { this.totalCost = cost; }
public void setStatus(ReservationStatus status) { this.status = status; }
// Getters
public String getReservationId() { return reservationId; }
public Customer getCustomer() { return customer; }
public Vehicle getVehicle() { return vehicle; }
public DateRange getRentalPeriod() { return rentalPeriod; }
public ReservationStatus getStatus() { return status; }
public Money getTotalCost() { return totalCost; }
public List<RentalAddOn> getAddOns() { return addOns; }
}
class RentalTransaction {
private String transactionId;
private Reservation reservation;
private LocalDateTime checkOutTime;
private LocalDateTime expectedReturnTime;
private int initialMileage;
private FuelLevel initialFuel;
private DamageReport initialDamage;
private LocalDateTime actualReturnTime;
private int finalMileage;
private FuelLevel finalFuel;
private DamageReport finalDamage;
public RentalTransaction(String id, Reservation reservation,
int mileage, FuelLevel fuel, DamageReport damage) {
this.transactionId = id;
this.reservation = reservation;
this.checkOutTime = LocalDateTime.now();
this.expectedReturnTime = reservation.getRentalPeriod()
.getEndDate().atStartOfDay();
this.initialMileage = mileage;
this.initialFuel = fuel;
this.initialDamage = damage;
}
public Money calculateCharges() {
Money baseCharge = reservation.getTotalCost();
Money lateCharges = calculateLateFees();
Money fuelCharges = calculateFuelCharges();
Money damageCharges = finalDamage != null ?
finalDamage.getTotalRepairCost() : new Money(0);
return baseCharge.add(lateCharges).add(fuelCharges).add(damageCharges);
}
private Money calculateLateFees() {
if (!isOverdue()) return new Money(0);
long overdueDays = ChronoUnit.DAYS.between(
expectedReturnTime.toLocalDate(),
actualReturnTime.toLocalDate()
);
Money dailyRate = reservation.getVehicle().getDailyRate();
return dailyRate.multiply(1.5 * overdueDays);
}
private Money calculateFuelCharges() {
if (finalFuel.ordinal() >= initialFuel.ordinal()) {
return new Money(0);
}
int levelDiff = initialFuel.ordinal() - finalFuel.ordinal();
return new Money(levelDiff * 20.0);
}
public boolean isOverdue() {
return actualReturnTime != null &&
actualReturnTime.isAfter(expectedReturnTime);
}
public void recordReturn(int mileage, FuelLevel fuel, DamageReport damage) {
this.actualReturnTime = LocalDateTime.now();
this.finalMileage = mileage;
this.finalFuel = fuel;
this.finalDamage = damage;
}
}
class RentalAddOn {
private String name;
private String description;
private Money dailyRate;
public RentalAddOn(String name, String desc, Money rate) {
this.name = name;
this.description = desc;
this.dailyRate = rate;
}
public Money getDailyRate() { return dailyRate; }
}
// ============================================================================
// Damage Tracking
// ============================================================================
class DamageReport {
private String reportId;
private LocalDateTime inspectionTime;
private String inspectorName;
private List<DamageItem> items;
private List<String> photoUrls;
public DamageReport(String id, String inspector) {
this.reportId = id;
this.inspectionTime = LocalDateTime.now();
this.inspectorName = inspector;
this.items = new ArrayList<>();
this.photoUrls = new ArrayList<>();
}
public void addDamageItem(DamageItem item) {
items.add(item);
}
public Money getTotalRepairCost() {
return items.stream()
.map(DamageItem::getEstimatedCost)
.reduce(new Money(0), Money::add);
}
public List<DamageItem> compareTo(DamageReport other) {
// Find new damage items that weren't in the other report
List<DamageItem> newDamage = new ArrayList<>();
for (DamageItem item : this.items) {
boolean foundInOther = other.items.stream()
.anyMatch(otherItem -> otherItem.isSimilarTo(item));
if (!foundInOther) {
newDamage.add(item);
}
}
return newDamage;
}
}
class DamageItem {
private String description;
private DamageSeverity severity;
private String location;
private Money estimatedCost;
public DamageItem(String desc, DamageSeverity severity,
String location, Money cost) {
this.description = desc;
this.severity = severity;
this.location = location;
this.estimatedCost = cost;
}
public boolean isSimilarTo(DamageItem other) {
return this.location.equals(other.location) &&
this.description.contains(other.description);
}
public Money getEstimatedCost() { return estimatedCost; }
}
// ============================================================================
// Location
// ============================================================================
class Location {
private String locationId;
private String name;
private String address;
private List<Vehicle> inventory;
public Location(String id, String name, String address) {
this.locationId = id;
this.name = name;
this.address = address;
this.inventory = new ArrayList<>();
}
public List<Vehicle> getAvailableVehicles(DateRange period) {
return inventory.stream()
.filter(v -> v.isAvailableFor(period))
.toList();
}
}
// ============================================================================
// Pricing Strategy Pattern (Open/Closed Principle)
// ============================================================================
interface PricingCalculator {
Money calculatePrice(Reservation reservation);
}
class BaseRatePricing implements PricingCalculator {
@Override
public Money calculatePrice(Reservation reservation) {
Vehicle vehicle = reservation.getVehicle();
long days = reservation.getRentalPeriod().getDays();
Money basePrice = vehicle.getDailyRate().multiply(days);
// Add add-ons
Money addOnCost = reservation.getAddOns().stream()
.map(addon -> addon.getDailyRate().multiply(days))
.reduce(new Money(0), Money::add);
Money total = basePrice.add(addOnCost);
// Apply customer discount
double discount = reservation.getCustomer().getDiscountRate();
return total.multiply(1.0 - discount);
}
}
class SeasonalPricing implements PricingCalculator {
private PricingCalculator basePricing;
private Map<Integer, Double> seasonalMultipliers;
public SeasonalPricing(PricingCalculator base) {
this.basePricing = base;
this.seasonalMultipliers = new HashMap<>();
// Summer (Jun-Aug): +30%
seasonalMultipliers.put(6, 1.3);
seasonalMultipliers.put(7, 1.3);
seasonalMultipliers.put(8, 1.3);
// Winter holidays (Dec-Jan): +20%
seasonalMultipliers.put(12, 1.2);
seasonalMultipliers.put(1, 1.2);
}
@Override
public Money calculatePrice(Reservation reservation) {
Money basePrice = basePricing.calculatePrice(reservation);
int month = reservation.getRentalPeriod().getStartDate().getMonthValue();
double multiplier = seasonalMultipliers.getOrDefault(month, 1.0);
return basePrice.multiply(multiplier);
}
}
class DemandBasedPricing implements PricingCalculator {
private PricingCalculator basePricing;
private VehicleRepository vehicleRepo;
public DemandBasedPricing(PricingCalculator base, VehicleRepository repo) {
this.basePricing = base;
this.vehicleRepo = repo;
}
@Override
public Money calculatePrice(Reservation reservation) {
Money basePrice = basePricing.calculatePrice(reservation);
// Get utilization rate for this vehicle type
double utilization = vehicleRepo.getUtilization(
reservation.getVehicle().assignedLocation,
reservation.getVehicle().getClass().getSimpleName(),
reservation.getRentalPeriod()
);
// Apply demand multiplier
double demandMultiplier = 1.0;
if (utilization > 0.90) {
demandMultiplier = 1.5; // 50% surge
} else if (utilization > 0.75) {
demandMultiplier = 1.25; // 25% surge
} else if (utilization > 0.60) {
demandMultiplier = 1.1; // 10% surge
}
return basePrice.multiply(demandMultiplier);
}
}
// ============================================================================
// Repository Interfaces (Dependency Inversion Principle)
// ============================================================================
interface VehicleRepository {
Vehicle findById(String vin);
List<Vehicle> findAvailableByType(String vehicleType, Location location,
DateRange dates);
void save(Vehicle vehicle);
double getUtilization(Location location, String vehicleType, DateRange dates);
}
interface ReservationRepository {
Reservation findById(String reservationId);
List<Reservation> findByCustomer(String customerId);
List<Reservation> findOverlapping(Vehicle vehicle, DateRange dates);
void save(Reservation reservation);
}
// ============================================================================
// Notification Service (Observer Pattern)
// ============================================================================
class NotificationService {
public void sendReservationConfirmation(Reservation reservation) {
String message = String.format(
"Reservation %s confirmed for %s from %s to %s",
reservation.getReservationId(),
reservation.getCustomer().getName(),
reservation.getRentalPeriod().getStartDate(),
reservation.getRentalPeriod().getEndDate()
);
sendEmail(reservation.getCustomer().getEmail(), message);
}
public void sendCheckOutConfirmation(RentalTransaction transaction) {
String message = "Vehicle checked out successfully. Enjoy your trip!";
sendEmail(transaction.reservation.getCustomer().getEmail(), message);
}
public void sendOverdueAlert(RentalTransaction transaction) {
String message = "Your rental is overdue. Please return the vehicle.";
sendEmail(transaction.reservation.getCustomer().getEmail(), message);
}
public void sendReturnReceipt(RentalTransaction transaction, Money finalCharge) {
String message = String.format(
"Vehicle returned. Total charges: %s",
finalCharge.toString()
);
sendEmail(transaction.reservation.getCustomer().getEmail(), message);
}
private void sendEmail(String email, String message) {
// Email sending implementation
System.out.println("Email to " + email + ": " + message);
}
}
Python
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from decimal import Decimal
from enum import Enum
from typing import List, Optional, Dict
from uuid import uuid4
# ============================================================================
# Enums
# ============================================================================
class VehicleStatus(Enum):
AVAILABLE = "available"
RESERVED = "reserved"
RENTED = "rented"
IN_MAINTENANCE = "in_maintenance"
RETIRED = "retired"
class ReservationStatus(Enum):
PENDING_PAYMENT = "pending_payment"
CONFIRMED = "confirmed"
ACTIVE = "active"
COMPLETED = "completed"
CANCELLED = "cancelled"
class DamageSeverity(Enum):
MINOR = "minor"
MODERATE = "moderate"
SEVERE = "severe"
TOTALED = "totaled"
class FuelLevel(Enum):
EMPTY = 0
QUARTER = 1
HALF = 2
THREE_QUARTER = 3
FULL = 4
class MembershipTier(Enum):
BASIC = 0.0
SILVER = 0.05
GOLD = 0.10
PLATINUM = 0.15
@property
def discount_rate(self) -> float:
return self.value
# ============================================================================
# Value Objects
# ============================================================================
@dataclass(frozen=True)
class Money:
"""Immutable money representation"""
amount: Decimal
currency: str = "USD"
def add(self, other: 'Money') -> 'Money':
if self.currency != other.currency:
raise ValueError("Currency mismatch")
return Money(self.amount + other.amount, self.currency)
def multiply(self, factor: float) -> 'Money':
return Money(self.amount * Decimal(str(factor)), self.currency)
def is_zero(self) -> bool:
return self.amount == Decimal('0')
@dataclass(frozen=True)
class DateRange:
"""Immutable date range"""
start_date: date
end_date: date
def __post_init__(self) -> None:
if self.start_date > self.end_date:
raise ValueError("Start must be before end")
def get_days(self) -> int:
return (self.end_date - self.start_date).days
def overlaps(self, other: 'DateRange') -> bool:
return not (self.end_date < other.start_date or
self.start_date > other.end_date)
# ============================================================================
# Vehicle Hierarchy (Liskov Substitution Principle)
# ============================================================================
class Vehicle(ABC):
"""Abstract base class for all vehicles"""
def __init__(self, vin: str, make: str, model: str, year: int) -> None:
self.vin = vin
self.make = make
self.model = model
self.manufacture_year = year
self.current_mileage = 0
self.status = VehicleStatus.AVAILABLE
self.assigned_location: Optional['Location'] = None
@abstractmethod
def get_daily_rate(self) -> Money:
"""Each vehicle type must define its daily rate"""
pass
def is_available_for(self, period: DateRange) -> bool:
return self.status == VehicleStatus.AVAILABLE
def mark_as_rented(self) -> None:
if self.status != VehicleStatus.AVAILABLE:
raise ValueError("Vehicle not available")
self.status = VehicleStatus.RENTED
def mark_as_returned(self) -> None:
self.status = VehicleStatus.AVAILABLE
def update_mileage(self, mileage: int) -> None:
if mileage < self.current_mileage:
raise ValueError("Cannot decrease mileage")
self.current_mileage = mileage
class Sedan(Vehicle):
def __init__(self, vin: str, make: str, model: str, year: int,
has_sunroof: bool, trunk_capacity: int) -> None:
super().__init__(vin, make, model, year)
self.has_sunroof = has_sunroof
self.trunk_capacity_liters = trunk_capacity
def get_daily_rate(self) -> Money:
return Money(Decimal('45.00'))
class SUV(Vehicle):
def __init__(self, vin: str, make: str, model: str, year: int,
drive_type: str, capacity: int) -> None:
super().__init__(vin, make, model, year)
self.drive_type = drive_type
self.passenger_capacity = capacity
def get_daily_rate(self) -> Money:
return Money(Decimal('75.00'))
class Truck(Vehicle):
def __init__(self, vin: str, make: str, model: str, year: int,
payload: int, has_tow: bool) -> None:
super().__init__(vin, make, model, year)
self.payload_kg = payload
self.has_tow_hitch = has_tow
def get_daily_rate(self) -> Money:
return Money(Decimal('95.00'))
class Motorcycle(Vehicle):
def __init__(self, vin: str, make: str, model: str, year: int,
cc: int, style: str) -> None:
super().__init__(vin, make, model, year)
self.engine_cc = cc
self.bike_style = style
def get_daily_rate(self) -> Money:
return Money(Decimal('35.00'))
# ============================================================================
# Customer & License (Single Responsibility Principle)
# ============================================================================
@dataclass
class DriverLicense:
number: str
issuing_state: str
expiry_date: date
def is_valid(self) -> bool:
return not self.is_expired()
def is_expired(self) -> bool:
return date.today() > self.expiry_date
class Customer:
def __init__(self, customer_id: str, name: str, email: str,
license: DriverLicense) -> None:
self.customer_id = customer_id
self.name = name
self.email = email
self.phone: Optional[str] = None
self.license = license
self.tier = MembershipTier.BASIC
self.rental_history: List['Reservation'] = []
def is_eligible_to_rent(self) -> bool:
return (self.license is not None and
self.license.is_valid() and
not self._has_overdue_rentals())
def _has_overdue_rentals(self) -> bool:
return any(r.status == ReservationStatus.ACTIVE and r.is_overdue()
for r in self.rental_history)
def get_discount_rate(self) -> float:
return self.tier.discount_rate
# ============================================================================
# Reservation System
# ============================================================================
class Reservation:
def __init__(self, reservation_id: str, customer: Customer,
vehicle: Vehicle, period: DateRange) -> None:
self.reservation_id = reservation_id
self.customer = customer
self.vehicle = vehicle
self.rental_period = period
self.status = ReservationStatus.PENDING_PAYMENT
self.total_cost: Optional[Money] = None
self.add_ons: List['RentalAddOn'] = []
self.created_at = datetime.now()
def confirm(self) -> None:
if self.status != ReservationStatus.PENDING_PAYMENT:
raise ValueError("Can only confirm pending reservations")
self.status = ReservationStatus.CONFIRMED
def cancel(self) -> Money:
if self.status in [ReservationStatus.COMPLETED, ReservationStatus.CANCELLED]:
raise ValueError("Cannot cancel this reservation")
self.status = ReservationStatus.CANCELLED
return self.calculate_refund()
def calculate_refund(self) -> Money:
hours_until_start = (
datetime.combine(self.rental_period.start_date, datetime.min.time()) -
datetime.now()
).total_seconds() / 3600
if hours_until_start > 48:
return self.total_cost
elif hours_until_start > 24:
return self.total_cost.multiply(0.5)
else:
return Money(Decimal('0'))
def is_overdue(self) -> bool:
return (self.status == ReservationStatus.ACTIVE and
date.today() > self.rental_period.end_date)
class RentalTransaction:
def __init__(self, transaction_id: str, reservation: Reservation,
mileage: int, fuel: FuelLevel, damage: 'DamageReport') -> None:
self.transaction_id = transaction_id
self.reservation = reservation
self.check_out_time = datetime.now()
self.expected_return = datetime.combine(
reservation.rental_period.end_date,
datetime.min.time()
)
self.initial_mileage = mileage
self.initial_fuel = fuel
self.initial_damage = damage
self.actual_return_time: Optional[datetime] = None
self.final_mileage: Optional[int] = None
self.final_fuel: Optional[FuelLevel] = None
self.final_damage: Optional['DamageReport'] = None
def calculate_charges(self) -> Money:
base = self.reservation.total_cost
late = self._calculate_late_fees()
fuel = self._calculate_fuel_charges()
damage = (self.final_damage.get_total_repair_cost()
if self.final_damage else Money(Decimal('0')))
return base.add(late).add(fuel).add(damage)
def _calculate_late_fees(self) -> Money:
if not self.is_overdue():
return Money(Decimal('0'))
days = (self.actual_return_time.date() -
self.expected_return.date()).days
rate = self.reservation.vehicle.get_daily_rate()
return rate.multiply(1.5 * days)
def _calculate_fuel_charges(self) -> Money:
if self.final_fuel.value >= self.initial_fuel.value:
return Money(Decimal('0'))
diff = self.initial_fuel.value - self.final_fuel.value
return Money(Decimal(str(diff * 20)))
def is_overdue(self) -> bool:
return (self.actual_return_time is not None and
self.actual_return_time > self.expected_return)
def record_return(self, mileage: int, fuel: FuelLevel,
damage: 'DamageReport') -> None:
self.actual_return_time = datetime.now()
self.final_mileage = mileage
self.final_fuel = fuel
self.final_damage = damage
@dataclass
class RentalAddOn:
name: str
description: str
daily_rate: Money
# ============================================================================
# Damage Tracking
# ============================================================================
@dataclass
class DamageItem:
description: str
severity: DamageSeverity
location: str
estimated_cost: Money
def is_similar_to(self, other: 'DamageItem') -> bool:
return (self.location == other.location and
other.description in self.description)
class DamageReport:
def __init__(self, report_id: str, inspector: str) -> None:
self.report_id = report_id
self.inspection_time = datetime.now()
self.inspector_name = inspector
self.items: List[DamageItem] = []
self.photo_urls: List[str] = []
def add_damage_item(self, item: DamageItem) -> None:
self.items.append(item)
def get_total_repair_cost(self) -> Money:
if not self.items:
return Money(Decimal('0'))
total = self.items[0].estimated_cost
for item in self.items[1:]:
total = total.add(item.estimated_cost)
return total
def compare_to(self, other: 'DamageReport') -> List[DamageItem]:
"""Find new damage not in the other report"""
new_damage = []
for item in self.items:
if not any(other_item.is_similar_to(item) for other_item in other.items):
new_damage.append(item)
return new_damage
# ============================================================================
# Location
# ============================================================================
class Location:
def __init__(self, location_id: str, name: str, address: str) -> None:
self.location_id = location_id
self.name = name
self.address = address
self.inventory: List[Vehicle] = []
def get_available_vehicles(self, period: DateRange) -> List[Vehicle]:
return [v for v in self.inventory if v.is_available_for(period)]
# ============================================================================
# Pricing Strategy Pattern (Open/Closed Principle)
# ============================================================================
class PricingCalculator(ABC):
"""Strategy interface for pricing"""
@abstractmethod
def calculate_price(self, reservation: Reservation) -> Money:
pass
class BaseRatePricing(PricingCalculator):
def calculate_price(self, reservation: Reservation) -> Money:
vehicle = reservation.vehicle
days = reservation.rental_period.get_days()
base = vehicle.get_daily_rate().multiply(days)
add_on_total = Money(Decimal('0'))
for addon in reservation.add_ons:
add_on_total = add_on_total.add(addon.daily_rate.multiply(days))
total = base.add(add_on_total)
discount = reservation.customer.get_discount_rate()
return total.multiply(1.0 - discount)
class SeasonalPricing(PricingCalculator):
def __init__(self, base_pricing: PricingCalculator) -> None:
self.base_pricing = base_pricing
self.seasonal_multipliers = {
6: 1.3, 7: 1.3, 8: 1.3, # Summer
12: 1.2, 1: 1.2 # Winter holidays
}
def calculate_price(self, reservation: Reservation) -> Money:
base_price = self.base_pricing.calculate_price(reservation)
month = reservation.rental_period.start_date.month
multiplier = self.seasonal_multipliers.get(month, 1.0)
return base_price.multiply(multiplier)
class DemandBasedPricing(PricingCalculator):
def __init__(self, base_pricing: PricingCalculator,
vehicle_repo: 'VehicleRepository') -> None:
self.base_pricing = base_pricing
self.vehicle_repo = vehicle_repo
def calculate_price(self, reservation: Reservation) -> Money:
base_price = self.base_pricing.calculate_price(reservation)
utilization = self.vehicle_repo.get_utilization(
reservation.vehicle.assigned_location,
type(reservation.vehicle).__name__,
reservation.rental_period
)
multiplier = 1.0
if utilization > 0.90:
multiplier = 1.5
elif utilization > 0.75:
multiplier = 1.25
elif utilization > 0.60:
multiplier = 1.1
return base_price.multiply(multiplier)
# ============================================================================
# Repository Interfaces (Dependency Inversion)
# ============================================================================
class VehicleRepository(ABC):
@abstractmethod
def find_by_id(self, vin: str) -> Vehicle:
pass
@abstractmethod
def find_available_by_type(self, vehicle_type: str, location: Location,
dates: DateRange) -> List[Vehicle]:
pass
@abstractmethod
def save(self, vehicle: Vehicle) -> None:
pass
@abstractmethod
def get_utilization(self, location: Location, vehicle_type: str,
dates: DateRange) -> float:
pass
class ReservationRepository(ABC):
@abstractmethod
def find_by_id(self, reservation_id: str) -> Reservation:
pass
@abstractmethod
def find_by_customer(self, customer_id: str) -> List[Reservation]:
pass
@abstractmethod
def find_overlapping(self, vehicle: Vehicle, dates: DateRange) -> List[Reservation]:
pass
@abstractmethod
def save(self, reservation: Reservation) -> None:
pass
# ============================================================================
# Notification Service (Observer Pattern)
# ============================================================================
class NotificationService:
def send_reservation_confirmation(self, reservation: Reservation) -> None:
msg = (f"Reservation {reservation.reservation_id} confirmed for "
f"{reservation.customer.name}")
self._send_email(reservation.customer.email, msg)
def send_check_out_confirmation(self, transaction: RentalTransaction) -> None:
msg = "Vehicle checked out. Enjoy your trip!"
self._send_email(transaction.reservation.customer.email, msg)
def send_overdue_alert(self, transaction: RentalTransaction) -> None:
msg = "Your rental is overdue. Please return the vehicle."
self._send_email(transaction.reservation.customer.email, msg)
def send_return_receipt(self, transaction: RentalTransaction,
final_charge: Money) -> None:
msg = f"Vehicle returned. Total: {final_charge.amount}"
self._send_email(transaction.reservation.customer.email, msg)
def _send_email(self, email: str, message: str) -> None:
print(f"Email to {email}: {message}")
This implementation showcases:
- SOLID Principles clearly demonstrated in each component
- Strategy Pattern for extensible pricing without modifying existing code
- Repository Pattern for clean data access abstraction
- Observer Pattern for decoupled notifications
- Template Method in Vehicle hierarchy
- Clean, readable code with proper type hints and documentation