Loading content...
Hotel pricing is one of the most sophisticated revenue optimization problems in hospitality—and one of the most complex aspects of booking system design. Unlike retail where a product has a fixed price, hotel room rates fluctuate constantly based on demand, competition, seasonality, events, and guest segmentation.
This page explores how to design a pricing engine that supports dynamic rates, multiple rate plans, promotions, taxes, and the complex repricing scenarios that occur when reservations are modified.
By the end of this page, you will understand how to design a flexible pricing architecture that supports dynamic rates per night, multiple rate plans with restrictions, layered promotions, tax calculations, and the repricing logic needed for reservation modifications. This knowledge is critical for any production-grade booking system.
Before diving into implementation, we must understand the hierarchical nature of hotel rates. Prices are not simply "$200 per night"—they're the result of multiple layers of configuration and calculation.
Rate Determination Hierarchy:
1234567891011121314151617181920212223242526272829303132333435363738394041424344
/** * RateConfiguration - defines pricing for a room type * This is the starting point for all rate calculations */interface RateConfiguration { roomTypeId: RoomTypeId; rackRate: Money; // Maximum base rate seasonalRates: SeasonalRate[]; dayOfWeekModifiers: DayOfWeekModifier[]; lengthOfStayDiscounts: LOSDiscount[];} interface SeasonalRate { seasonCode: string; // 'PEAK', 'SHOULDER', 'LOW' dateRanges: DateRange[]; // When this season applies modifier: RateModifier; // Percentage or fixed amount} interface DayOfWeekModifier { daysOfWeek: DayOfWeek[]; // FRIDAY, SATURDAY, etc. modifier: RateModifier; // Typically premium for weekends} interface LOSDiscount { minimumNights: number; // 3, 7, 14 nights, etc. discount: RateModifier; // Usually percentage discount applicableTo: 'ALL_NIGHTS' | 'EXTRA_NIGHTS_ONLY';} /** * Rate modifier - can be percentage or fixed amount */interface RateModifier { type: 'PERCENTAGE' | 'FIXED_AMOUNT' | 'FIXED_RATE'; value: number; // Percentage: 1.2 = 20% increase // Fixed rate: absolute value direction: 'INCREASE' | 'DECREASE'; // For clarity, though sign in value works} // Example: Peak weekend rate with length-of-stay discount// Rack rate: $200// Peak season: +20% → $240// Weekend: +10% → $264// 7+ night stay: -15% → $224.40 per nightThe order of rate modifications affects the final price significantly. Applying a 20% peak modifier then 10% weekend gives a different result than applying them in reverse if using multiplicative application. Define a consistent order and test edge cases where multiple modifiers stack.
Rate Plans are named pricing configurations that bundle a rate calculation with specific terms and conditions. They're how hotels segment their pricing strategy for different guest types and booking behaviors.
Common Rate Plan Types:
| Rate Plan | Discount | Restrictions | Target Segment |
|---|---|---|---|
| Best Available Rate (BAR) | None (market rate) | Flexible cancellation | General consumers |
| Advance Purchase | 15-30% | Non-refundable, prepaid | Price-sensitive travelers |
| AAA/AARP | 10-15% | Membership verification | Association members |
| Corporate Rate | Negotiated | Company code required | Business travelers |
| Government/Military | Varies | ID verification at check-in | Government employees |
| Package Rate | Included extras | Minimum stay, non-refundable | Leisure travelers |
| Last Minute | Variable | Very short booking window | Spontaneous travelers |
| Long Stay | 20-40% | 7/14/30+ night minimum | Extended stay guests |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
/** * RatePlan entity - named pricing configuration with terms */class RatePlan { private readonly id: RatePlanId; private readonly hotelId: HotelId; private code: string; // e.g., "BAR", "ADVANCE21", "AAA" private name: string; // Display name private description: string; // Pricing private baseRateModifier: RateModifier; // Applied to calculated base rate private includedMeals: MealPlan; // BB, HB, FB, RO (room only) private includedExtras: IncludedExtra[]; // Parking, WiFi, etc. // Availability restrictions private bookableChannels: BookingChannel[]; // Where this rate is visible private validDateRanges: DateRange[]; // When rate plan is active private blackoutDates: LocalDate[]; // Dates where not available // Booking restrictions private advanceBookingRequirement: AdvanceBookingRule | null; private minimumStay: number; // Minimum nights private maximumStay: number | null; // Maximum nights private closedToArrival: DayOfWeek[]; // Cannot arrive on these days private closedToDeparture: DayOfWeek[]; // Cannot depart on these days // Guest restrictions private requiredMembership: MembershipType | null; private requiredLoyaltyTier: LoyaltyTier | null; private guestVerificationRequired: boolean; // Check ID at arrival // Terms private cancellationPolicyId: CancellationPolicyId; private prepaymentRequired: boolean; private prepaymentPercentage: number; // 0-100 private guaranteeRequired: boolean; // Status private isActive: boolean; private displayPriority: number; // Order in search results constructor(params: RatePlanCreationParams) { this.validate(params); // ... initialization } // ============ Eligibility Methods ============ /** * Check if this rate plan is available for a booking */ isAvailableFor(context: BookingContext): boolean { return ( this.isActive && this.isWithinValidDates(context.stayDates) && this.meetsAdvanceBookingRequirement(context.bookingDate) && this.meetsStayLengthRequirement(context.nights) && this.isAvailableOnChannel(context.channel) && this.meetsGuestRequirements(context.guest) && !this.hasBlackoutConflict(context.stayDates) ); } private meetsAdvanceBookingRequirement(bookingDate: LocalDate): boolean { if (!this.advanceBookingRequirement) return true; const daysInAdvance = bookingDate.until( this.advanceBookingRequirement.checkInDate, ChronoUnit.DAYS ); return daysInAdvance >= this.advanceBookingRequirement.minimumDays; } private meetsStayLengthRequirement(nights: number): boolean { if (nights < this.minimumStay) return false; if (this.maximumStay && nights > this.maximumStay) return false; return true; } private hasBlackoutConflict(stayDates: DateRange): boolean { return this.blackoutDates.some(blackout => stayDates.contains(blackout) ); } // ============ Pricing Methods ============ /** * Apply this rate plan's modifier to base rate */ applyToBaseRate(baseRate: Money): Money { return this.baseRateModifier.apply(baseRate); } /** * Get effective cancellation policy for this rate plan */ getCancellationPolicy(): CancellationPolicy { return CancellationPolicyRepository.findById( this.cancellationPolicyId ); } // ============ Query Methods ============ isPrepaid(): boolean { return this.prepaymentRequired && this.prepaymentPercentage === 100; } isRefundable(): boolean { const policy = this.getCancellationPolicy(); return policy.allowsAnyCancellation(); }} interface AdvanceBookingRule { minimumDays: number; // Book at least N days before check-in maximumDays: number; // Book no more than N days before checkInDate: LocalDate; // Reference date for calculation} interface IncludedExtra { type: ExtraType; // PARKING, WIFI, BREAKFAST, GYM description: string; monetaryValue: Money; // For display purposes}Rate plans and promotions serve different purposes. Rate plans are ongoing pricing structures for market segments (AAA members always get 10% off). Promotions are temporary offers (Flash sale: 25% off this weekend only). Design them as separate concepts that can stack—a AAA member might also use a promotional code.
Dynamic pricing adjusts rates in real-time based on demand, competition, and current inventory. This is where revenue management science meets software engineering.
Factors Influencing Dynamic Rates:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
/** * PricingEngine - calculates final rate for a stay * Orchestrates all pricing components and modifiers */class PricingEngine { private readonly rateRepository: RateRepository; private readonly promotionEngine: PromotionEngine; private readonly taxCalculator: TaxCalculator; private readonly demandAnalyzer: DemandAnalyzer; constructor(dependencies: PricingEngineDependencies) { this.rateRepository = dependencies.rateRepository; this.promotionEngine = dependencies.promotionEngine; this.taxCalculator = dependencies.taxCalculator; this.demandAnalyzer = dependencies.demandAnalyzer; } /** * Calculate complete pricing for a potential booking */ calculatePricing(request: PricingRequest): PricingResult { const nightlyBreakdown: NightlyRate[] = []; let subtotal = Money.zero(); // Calculate rate for each night for (const night of request.stayDates.eachNight()) { const nightRate = this.calculateNightRate(request, night); nightlyBreakdown.push(nightRate); subtotal = subtotal.add(nightRate.totalBeforeTax); } // Apply promotions to the total const promotionResult = this.promotionEngine.apply( subtotal, request.promoCode, request.context ); // Calculate taxes on post-promotion amount const taxableAmount = promotionResult.finalAmount; const taxes = this.taxCalculator.calculate( taxableAmount, request.hotel.address, request.stayDates ); // Add fixed fees const fees = this.calculateFees(request); return new PricingResult({ nightlyBreakdown, roomSubtotal: subtotal, promotionDiscount: promotionResult.discountAmount, promotionsApplied: promotionResult.appliedPromotions, subtotalAfterPromotion: promotionResult.finalAmount, taxes, fees, grandTotal: promotionResult.finalAmount .add(taxes.totalTax) .add(fees.totalFees), currency: request.currency, }); } /** * Calculate rate for a single night */ private calculateNightRate( request: PricingRequest, night: LocalDate ): NightlyRate { // 1. Get base rate for room type const rackRate = request.roomType.rackRate; // 2. Apply seasonal modifier const seasonModifier = this.rateRepository.getSeasonalModifier( request.roomType.id, night ); let rate = seasonModifier.apply(rackRate); // 3. Apply day-of-week modifier const dowModifier = this.rateRepository.getDayOfWeekModifier( request.roomType.id, night.dayOfWeek ); rate = dowModifier.apply(rate); // 4. Check for specific date override (events, holidays) const dateOverride = this.rateRepository.getDateOverride( request.roomType.id, night ); if (dateOverride) { rate = dateOverride.rate; } // 5. Apply dynamic pricing adjustment based on demand const demandMultiplier = this.demandAnalyzer.getMultiplier( request.roomType.id, night, request.hotel.id ); rate = rate.multiply(demandMultiplier); // 6. Apply rate plan modifier rate = request.ratePlan.applyToBaseRate(rate); // 7. Apply length-of-stay discount const losDiscount = this.getLengthOfStayDiscount( request.roomType.id, request.nights ); rate = losDiscount.apply(rate); // 8. Calculate extra person charges const extraPersonCharge = request.roomType.calculateExtraPersonCharge( request.adults, request.children, this.getExtraPersonRates(request.roomType.id) ); return new NightlyRate({ date: night, baseRate: rackRate, adjustedRate: rate, extraPersonCharge, totalBeforeTax: rate.add(extraPersonCharge), modifiersApplied: [ seasonModifier, dowModifier, demandMultiplier, losDiscount, ].filter(m => !m.isIdentity()), }); } /** * Calculate fixed fees (resort fee, parking, etc.) */ private calculateFees(request: PricingRequest): FeeBreakdown { const fees: Fee[] = []; // Resort/Amenity fee (per night) const resortFee = this.rateRepository.getResortFee(request.hotel.id); if (resortFee) { fees.push(new Fee({ type: FeeType.RESORT_FEE, amount: resortFee.multiply(request.nights), description: 'Resort amenity fee', taxable: true, })); } // Parking (if selected) if (request.parkingRequested) { const parkingRate = this.rateRepository.getParkingRate( request.hotel.id, request.parkingType ); fees.push(new Fee({ type: FeeType.PARKING, amount: parkingRate.multiply(request.nights), description: `${request.parkingType} parking`, taxable: true, })); } // Pet fee (if applicable) if (request.hasPets) { const petFee = this.rateRepository.getPetFee(request.hotel.id); fees.push(new Fee({ type: FeeType.PET_FEE, amount: petFee, // Usually one-time fee description: 'Pet accommodation fee', taxable: false, })); } return new FeeBreakdown(fees); }} interface PricingRequest { hotel: Hotel; roomType: RoomType; ratePlan: RatePlan; stayDates: DateRange; nights: number; adults: number; children: number; promoCode: string | null; currency: Currency; parkingRequested: boolean; parkingType: 'SELF' | 'VALET'; hasPets: boolean; context: BookingContext;}In high-demand scenarios, prices can change between search and booking. Design strategies include: rate locks (hold price for 15 minutes), optimistic booking (book at displayed price, adjust if significantly changed), or real-time re-validation. The choice depends on business priority between price accuracy and conversion rate.
Promotions add another layer to pricing—temporary offers that stack on top of rate plan pricing. A well-designed promotion system supports complex business requirements while remaining maintainable.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
/** * Promotion entity - temporary pricing offer */class Promotion { private readonly id: PromotionId; private code: string; // User-facing code: "SUMMER25" private name: string; private description: string; // Discount definition private discountType: DiscountType; private discountValue: number; // Amount or percentage private maximumDiscount: Money | null; // Cap for percentage discounts // Validity private bookingValidFrom: Instant; // When promo can be used private bookingValidUntil: Instant; private stayValidFrom: LocalDate; // When stay must occur private stayValidUntil: LocalDate; // Restrictions private applicableRoomTypes: RoomTypeId[] | 'ALL'; private applicableRatePlans: RatePlanId[] | 'ALL'; private minimumStay: number; private minimumSpend: Money | null; private applicableChannels: BookingChannel[]; // Usage limits private totalUsageLimit: number | null; // Max total uses private currentUsageCount: number; private perGuestLimit: number | null; // Max uses per guest // Stacking rules private stackable: boolean; // Can combine with other promos private stackPriority: number; // Order when stacking // Target audience private loyaltyTierRequired: LoyaltyTier | null; private newGuestOnly: boolean; private returningGuestOnly: boolean; constructor(params: PromotionCreationParams) { this.validate(params); // ... initialization } // ============ Eligibility Methods ============ isApplicable(context: PromotionContext): PromotionEligibility { const checks: EligibilityCheck[] = []; // Time-based checks checks.push(this.checkBookingWindow(context.bookingTime)); checks.push(this.checkStayWindow(context.stayDates)); // Room/rate checks checks.push(this.checkRoomTypeApplicability(context.roomTypeId)); checks.push(this.checkRatePlanApplicability(context.ratePlanId)); // Stay requirement checks checks.push(this.checkMinimumStay(context.nights)); checks.push(this.checkMinimumSpend(context.subtotal)); // Guest checks checks.push(this.checkLoyaltyRequirement(context.guest)); checks.push(this.checkNewGuestRequirement(context.guest)); checks.push(this.checkPerGuestLimit(context.guest)); // Usage checks checks.push(this.checkTotalUsageLimit()); const failedCheck = checks.find(c => !c.passed); if (failedCheck) { return PromotionEligibility.ineligible(failedCheck.reason); } return PromotionEligibility.eligible(); } private checkTotalUsageLimit(): EligibilityCheck { if (!this.totalUsageLimit) { return { passed: true }; } if (this.currentUsageCount >= this.totalUsageLimit) { return { passed: false, reason: 'Promotion has reached its usage limit', }; } return { passed: true }; } // ============ Discount Calculation ============ calculateDiscount(subtotal: Money): DiscountResult { let discount: Money; switch (this.discountType) { case DiscountType.PERCENTAGE: discount = subtotal.multiply(this.discountValue / 100); break; case DiscountType.FIXED_AMOUNT: discount = Money.of(this.discountValue, subtotal.currency); break; case DiscountType.FIXED_PRICE_PER_NIGHT: // Handled differently - replaces rate throw new Error('Fixed price promos handled at night level'); default: discount = Money.zero(); } // Apply maximum cap if (this.maximumDiscount && discount.isGreaterThan(this.maximumDiscount)) { discount = this.maximumDiscount; } // Cannot discount more than subtotal if (discount.isGreaterThan(subtotal)) { discount = subtotal; } return new DiscountResult({ promotionId: this.id, promotionCode: this.code, originalAmount: subtotal, discountAmount: discount, finalAmount: subtotal.subtract(discount), }); } // ============ Usage Tracking ============ recordUsage(reservationId: ReservationId): void { this.currentUsageCount += 1; // Would also record in usage history for per-guest tracking }} enum DiscountType { PERCENTAGE = 'PERCENTAGE', FIXED_AMOUNT = 'FIXED_AMOUNT', FIXED_PRICE_PER_NIGHT = 'FIXED_PRICE_PER_NIGHT', FREE_NIGHT = 'FREE_NIGHT', // Stay 4 nights, pay for 3 FREE_UPGRADE = 'FREE_UPGRADE',} /** * PromotionEngine - orchestrates promotion evaluation and stacking */class PromotionEngine { private readonly promotionRepository: PromotionRepository; /** * Apply promotion(s) to a booking subtotal */ apply( subtotal: Money, promoCode: string | null, context: PromotionContext ): PromotionApplicationResult { const appliedPromotions: AppliedPromotion[] = []; let currentAmount = subtotal; // 1. Find explicitly requested promotion if (promoCode) { const promotion = this.promotionRepository.findByCode(promoCode); if (!promotion) { throw new InvalidPromotionCodeError(promoCode); } const eligibility = promotion.isApplicable(context); if (!eligibility.eligible) { throw new PromotionNotApplicableError( promoCode, eligibility.reason ); } const result = promotion.calculateDiscount(currentAmount); currentAmount = result.finalAmount; appliedPromotions.push(result.toAppliedPromotion()); } // 2. Find and apply automatic promotions (member benefits, etc.) const autoPromotions = this.promotionRepository .findAutoApplyPromotions() .filter(p => p.isApplicable(context).eligible) .sort((a, b) => b.stackPriority - a.stackPriority); for (const promo of autoPromotions) { // Check stacking rules if (!promo.stackable && appliedPromotions.length > 0) { continue; } const result = promo.calculateDiscount(currentAmount); currentAmount = result.finalAmount; appliedPromotions.push(result.toAppliedPromotion()); } return new PromotionApplicationResult({ originalAmount: subtotal, finalAmount: currentAmount, discountAmount: subtotal.subtract(currentAmount), appliedPromotions, }); }}Hotels typically allow specific stacking combinations: loyalty discount + seasonal promo (yes), two promo codes (no). Implement this through stackable flags and exclusion rules rather than hard-coding. This allows revenue managers to adjust stacking policies without code changes.
Tax calculation in hospitality is notoriously complex. Hotels face multiple tax jurisdictions, tax types, and exemptions that vary by location, guest type, and stay length.
| Tax Type | Typical Rate | Applied To | Notes |
|---|---|---|---|
| Sales Tax | 4-10% | Room + fees | State/local, varies widely |
| Occupancy Tax | 2-15% | Room only | Also called lodging/hotel tax |
| Tourism Tax | 1-5% | Room only | Funds local tourism promotion |
| Convention Tax | 0.5-2% | Room only | Specific to convention cities |
| City Tax | 1-5% | Room only | Municipal levy |
| VAT (Europe) | 7-20% | Room + eligible fees | Value-added tax |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
/** * TaxCalculator - handles complex multi-jurisdiction tax calculations */class TaxCalculator { private readonly taxRateRepository: TaxRateRepository; /** * Calculate all applicable taxes for a hotel stay */ calculate( taxableAmount: Money, hotelAddress: Address, stayDates: DateRange ): TaxBreakdown { const applicableTaxes: TaxLineItem[] = []; // Get tax rates for this jurisdiction and date range // Tax rates can change! Must use rates valid during the stay const taxRates = this.taxRateRepository.getRatesFor( hotelAddress.country, hotelAddress.state, hotelAddress.city, stayDates ); // Apply each tax type for (const rate of taxRates) { // Check if tax applies to this amount type if (!rate.appliesTo.includes(TaxableType.ROOM_CHARGE)) { continue; } // Check for exemptions (long stays often exempt from tourism tax) if (this.isExempt(rate, stayDates.nights)) { continue; } const taxAmount = this.calculateTaxAmount( taxableAmount, rate, stayDates.nights ); applicableTaxes.push(new TaxLineItem({ taxType: rate.taxType, taxName: rate.displayName, rate: rate.rate, rateType: rate.rateType, // PERCENTAGE or PER_NIGHT taxableAmount: taxableAmount, calculatedTax: taxAmount, jurisdictionCode: rate.jurisdictionCode, })); } const totalTax = applicableTaxes.reduce( (sum, item) => sum.add(item.calculatedTax), Money.zero() ); return new TaxBreakdown({ lineItems: applicableTaxes, totalTax, }); } private calculateTaxAmount( taxableAmount: Money, rate: TaxRate, nights: number ): Money { switch (rate.rateType) { case TaxRateType.PERCENTAGE: return taxableAmount.multiply(rate.rate / 100); case TaxRateType.PER_NIGHT: return Money.of(rate.rate, taxableAmount.currency) .multiply(nights); case TaxRateType.PER_ROOM_PER_NIGHT: // Would need room count from context return Money.of(rate.rate, taxableAmount.currency) .multiply(nights); default: throw new UnknownTaxRateTypeError(rate.rateType); } } private isExempt(rate: TaxRate, nights: number): boolean { // Common exemption: extended stays (30+ nights) exempt from tourism tax if (rate.exemptAfterNights && nights >= rate.exemptAfterNights) { return true; } return false; }} interface TaxRate { taxType: TaxType; displayName: string; rate: number; rateType: TaxRateType; appliesTo: TaxableType[]; jurisdictionCode: string; effectiveFrom: LocalDate; effectiveUntil: LocalDate | null; exemptAfterNights: number | null;} enum TaxType { SALES = 'SALES', OCCUPANCY = 'OCCUPANCY', TOURISM = 'TOURISM', CONVENTION = 'CONVENTION', CITY = 'CITY', VAT = 'VAT',} enum TaxableType { ROOM_CHARGE = 'ROOM_CHARGE', RESORT_FEE = 'RESORT_FEE', PARKING = 'PARKING', F_AND_B = 'F_AND_B', // Food and beverage}Tax rates change! A booking made in December for a February stay must use the tax rates effective in February, not December. Always look up rates based on the stay date, not the booking date. Additionally, stays spanning a rate change (rare but possible) may need split calculations.
When a reservation is modified, repricing logic determines whether rates change and how. This is one of the most complex areas of booking system design, with significant business and technical implications.
Key Questions:
| Modification | Original Nights | New Nights | Repricing Rule |
|---|---|---|---|
| Extend Stay | Keep original rates | Current rates for new nights | Rate lock for original, market rate for extension |
| Shorten Stay | Recalculate at original rates | N/A - nights removed | May need refund calculation at original rates |
| Date Shift (same length) | May reprice entirely | Uses new dates' rates | Policy-dependent: honor original or reprice |
| Room Upgrade | Keep original nights | Price difference per night | Add upgrade charge, don't reprice base |
| Room Downgrade | Keep original nights | Price difference per night | Policy: refund difference or no change |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
/** * RepricingService - handles pricing recalculation for modifications */class RepricingService { private readonly pricingEngine: PricingEngine; private readonly repricePolicy: RepricingPolicy; /** * Calculate pricing impact of a date modification */ calculateDateModificationPricing( reservation: Reservation, newCheckIn: LocalDate, newCheckOut: LocalDate ): DateModificationPricing { const originalDates = new DateRange( reservation.checkInDate, reservation.checkOutDate ); const newDates = new DateRange(newCheckIn, newCheckOut); // Identify which nights are unchanged, added, or removed const nightAnalysis = this.analyzeNightChanges(originalDates, newDates); // Calculate pricing for each category const unchanged = this.priceUnchangedNights( reservation, nightAnalysis.unchangedNights ); const added = this.priceAddedNights( reservation, nightAnalysis.addedNights ); const removed = this.calculateRemovalCredit( reservation, nightAnalysis.removedNights ); // Recalculate taxes on new total const newSubtotal = unchanged.total .add(added.total) .subtract(removed.creditAmount); const newTaxes = this.pricingEngine.calculateTaxes( newSubtotal, reservation.hotel, newDates ); // Determine net change const originalTotal = reservation.totalAmount; const newTotal = newSubtotal.add(newTaxes.totalTax); const difference = newTotal.subtract(originalTotal); return new DateModificationPricing({ originalTotal, newTotal, difference, isAdditionalPaymentRequired: difference.isPositive(), isRefundDue: difference.isNegative(), breakdown: { unchangedNights: unchanged, addedNights: added, removedNights: removed, newTaxBreakdown: newTaxes, }, }); } private analyzeNightChanges( original: DateRange, newRange: DateRange ): NightChangeAnalysis { const originalNights = new Set(original.eachNight().map(d => d.toString())); const newNights = new Set(newRange.eachNight().map(d => d.toString())); const unchanged: LocalDate[] = []; const added: LocalDate[] = []; const removed: LocalDate[] = []; // Find unchanged (intersection) for (const night of originalNights) { if (newNights.has(night)) { unchanged.push(LocalDate.parse(night)); } else { removed.push(LocalDate.parse(night)); } } // Find added (in new but not original) for (const night of newNights) { if (!originalNights.has(night)) { added.push(LocalDate.parse(night)); } } return { unchangedNights: unchanged, addedNights: added, removedNights: removed }; } private priceUnchangedNights( reservation: Reservation, nights: LocalDate[] ): NightsPricing { // Apply rate lock policy - usually keep original rates if (this.repricePolicy.honorOriginalRateForUnchangedNights) { return this.extractOriginalPricing(reservation, nights); } // Or reprice at current rates (unusual but some policies require) return this.priceAtCurrentRates(reservation, nights); } private priceAddedNights( reservation: Reservation, nights: LocalDate[] ): NightsPricing { if (nights.length === 0) { return NightsPricing.empty(); } // Strategy depends on policy if (this.repricePolicy.useOriginalRateForExtensions) { // Honor original rate plan and rate for extensions // This is guest-friendly but revenue-suboptimal return this.priceAtOriginalRates(reservation, nights); } // Standard: price new nights at current market rates const pricingRequest = this.buildPricingRequest(reservation, nights); return this.pricingEngine.priceNights(pricingRequest, nights); } private calculateRemovalCredit( reservation: Reservation, nights: LocalDate[] ): RemovalCredit { if (nights.length === 0) { return RemovalCredit.none(); } // Always credit at original rates (what was charged) const originalPricing = this.extractOriginalPricing(reservation, nights); // Apply cancellation policy if applicable if (this.repricePolicy.applyPolicyToRemovedNights) { const policy = reservation.cancellationPolicy; const penalty = policy.calculatePenaltyForNights(nights.length); return new RemovalCredit({ nights, grossCredit: originalPricing.total, penaltyAmount: penalty, netCredit: originalPricing.total.subtract(penalty), }); } return new RemovalCredit({ nights, grossCredit: originalPricing.total, penaltyAmount: Money.zero(), netCredit: originalPricing.total, }); }} /** * Policy configuration for repricing behavior */interface RepricingPolicy { honorOriginalRateForUnchangedNights: boolean; useOriginalRateForExtensions: boolean; useOriginalPromotionsForExtensions: boolean; applyPolicyToRemovedNights: boolean; allowDowngradeRefund: boolean; repriceOnRoomTypeChange: 'DIFFERENTIAL' | 'COMPLETE';}Notice how RepricingPolicy configuration drives behavior. Different hotels have different philosophies—luxury properties often honor original rates for modifications (guest satisfaction priority), while budget properties might reprice (revenue priority). Make policy configurable, not hard-coded.
We've explored the complex world of hotel booking pricing—from rate hierarchies through dynamic pricing to modification repricing. This is where business complexity meets technical design.
What's Next:
With pricing design complete, we'll explore Availability Management—how to track room inventory across dates, handle overbooking thresholds, and maintain consistency under concurrent access. This is where the temporal complexity of hotel systems becomes most apparent.
You now understand the architecture of a hotel pricing engine—from rate structures and rate plans through dynamic pricing and modification repricing. This knowledge enables you to design pricing systems that are both flexible for business needs and maintainable for engineering teams.