Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Implementation Plan: Advance & Last-Minute Booking Pricing Rules

Status: Planning
Created: December 17, 2025
Type: New Feature - Pricing Rule Type


Overview

Implement a new pricing rule type that allows restaurants to configure price adjustments based on booking lead time. This enables:

  • Advance Booking Discounts: Reward customers who book early (e.g., 15 days advance = 5% off)
  • Last-Minute Surge Pricing: Charge premium for urgent bookings (e.g., 30 minutes before = 10% increase)

This is a master data configuration feature - rules are stored and managed, not actively calculating prices in real-time.


UI Analysis (Based on Screenshots)

1. Condition Type

  • Options: “Advance Booking” | “Last-Minute Booking”
  • Maps to: timing_type column

2. Period Configuration

  • Number Input: Value (e.g., 15, 30, 7)
  • Dropdown: Unit (Minutes | Hours | Days | Weeks | Months)
  • Maps to: period_value (integer) + period_unit (string)

3. Action (Price Adjustment)

  • Options: “Increase Package Price” | “Decrease Package Price”
  • Maps to: action_type column

4. Price Type

  • Options: “By Percentage (%)” | “By Fixed Amount (THB)” *adult price only
  • Maps to: value_type column

5. Value Input

  • Number Field: Adjustment amount
  • Maps to: value column (stored in cents for fixed, basis points for percentage)

6. Multiple Conditions

  • UI allows adding multiple “When/Do this” pairs
  • Multiple BookingTimingCondition records per PricingRule
  • System applies highest priority rule only when multiple conditions match

Technical Specifications

Time Unit Standardization

All periods normalized to MINUTES for comparison:

PERIOD_MULTIPLIERS = {
  'minutes' => 1,
  'hours' => 60,
  'days' => 1440,
  'weeks' => 10080,
  'months' => 43200  # Approximate: 30 days
}.freeze

# Example: 15 days = 15 * 1440 = 21,600 minutes

Overlapping Condition Logic

Highest Priority Rule Wins:

  • When booking falls into multiple timing windows, apply only the rule with highest priority value
  • If same priority, first matching condition (by ID order) is applied
  • Uses existing Pricing::PriorityResolver service

Example Scenarios

Scenario 1: Advance Booking Discount

Condition: Advance Booking, 15 days, Decrease, Percentage, 5%
Booking Time: Dec 1, 2025 10:00 AM
Service Time: Dec 20, 2025 7:00 PM
Lead Time: 19 days 9 hours = 27,540 minutes
Result: ✅ Matched (≥ 21,600 minutes)
Adjustment: -5% from package price

Scenario 2: Last-Minute Surge

Condition: Last-Minute Booking, 30 minutes, Increase, Fixed, 500 THB
Booking Time: Dec 17, 2025 6:45 PM
Service Time: Dec 17, 2025 7:00 PM
Lead Time: 15 minutes
Result: ✅ Matched (≤ 30 minutes)
Adjustment: +500 THB to package price

Scenario 3: Not Matched

Condition: Advance Booking, 7 days, Decrease, Percentage, 3%
Booking Time: Dec 15, 2025
Service Time: Dec 18, 2025
Lead Time: 3 days = 4,320 minutes
Result: ❌ Not Matched (< 10,080 minutes required)

Database Design

Migration: Create booking_timing_conditions Table

File: db/migrate/YYYYMMDDHHMMSS_create_booking_timing_conditions.rb

class CreateBookingTimingConditions < ActiveRecord::Migration[7.0]
  def change
    create_table :booking_timing_conditions do |t|
      t.references :pricing_rule, null: false, foreign_key: true, index: true
      
      # Condition Configuration
      t.string :timing_type, null: false  # 'advance_booking' | 'last_minute_booking'
      t.integer :period_value, null: false  # e.g., 15, 30, 7
      t.string :period_unit, null: false  # 'minutes' | 'hours' | 'days' | 'weeks' | 'months'
      
      # Action Configuration
      t.string :action_type, null: false  # 'increase' | 'decrease'
      t.string :value_type, null: false  # 'percentage' | 'fixed'
      t.integer :value, null: false  # cents for fixed, basis points (1/100 of %) for percentage
      
      t.timestamps
    end
    
    # Composite index for query performance
    add_index :booking_timing_conditions, [:pricing_rule_id, :timing_type], 
              name: 'index_booking_timing_on_rule_and_type'
  end
end

Schema Update

Add to PricingRule:

# No changes needed to pricing_rules table
# Just update RULE_TYPES constant in model
RULE_TYPES = %w[
  specific_date_time
  advance_last_minute  # NEW
].freeze

Implementation Tasks

Phase 1: Database & Models (Day 1)

Task 1.1: Create Migration

  • Create db/migrate/YYYYMMDDHHMMSS_create_booking_timing_conditions.rb
  • Run migration: bundle exec rails db:migrate
  • Verify schema: bundle exec rails db:migrate:status

Task 1.2: Create BookingTimingCondition Model

  • Create app/models/booking_timing_condition.rb
  • Follow File Structure from Development Standards (includes, constants, validations, methods)
  • Add associations: belongs_to :pricing_rule
  • Add RDoc class documentation with description, @example, @see tags
  • Add validations following Model Validations standards:
    • Presence: timing_type, period_value, period_unit, action_type, value_type, value
    • Inclusion: timing_type in [‘advance_booking’, ‘last_minute_booking’]
    • Inclusion: period_unit in [‘minutes’, ‘hours’, ‘days’, ‘weeks’, ‘months’]
    • Inclusion: action_type in [‘increase’, ‘decrease’]
    • Inclusion: value_type in [‘percentage’, ‘fixed’]
    • Numericality: period_value > 0, value > 0
    • Custom validation: validate :period_makes_sense
  • Add cache invalidation: after_commit :invalidate_caches
  • Add constants with documentation:
    # Valid timing types for booking conditions
    TIMING_TYPES = %w[advance_booking last_minute_booking].freeze
    
    # Valid period units for time calculations
    PERIOD_UNITS = %w[minutes hours days weeks months].freeze
    
    # Multipliers to convert periods to minutes
    PERIOD_MULTIPLIERS = {
      'minutes' => 1,
      'hours' => 60,
      'days' => 1440,
      'weeks' => 10080,
      'months' => 43200
    }.freeze
    
  • Add public helper method with RDoc documentation:
    # Converts the period to minutes for standardized comparison
    #
    # @return [Integer] the period converted to minutes
    # @example
    #   condition.period_value = 15
    #   condition.period_unit = 'days'
    #   condition.period_in_minutes # => 21600
    def period_in_minutes
      period_value * PERIOD_MULTIPLIERS.fetch(period_unit, 1)
    end
    
  • Use meaningful variable names (no abbreviations)
  • Mark private methods with private keyword

Task 1.3: Update PricingRule Model

  • Update app/models/pricing_rule.rb
  • Add association: has_many :booking_timing_conditions, dependent: :destroy
  • Add nested attributes: accepts_nested_attributes_for :booking_timing_conditions, allow_destroy: true
  • Update RULE_TYPES constant to include 'advance_last_minute'
  • Update .with_conditions scope to include :booking_timing_conditions
  • Add validation: ensure conditions only exist when rule_type == 'advance_last_minute'

Phase 2: API Layer (Day 2)

Task 2.1: Create Resource Serializer

  • Create app/resources/booking_timing_condition_resource.rb
  • Follow Serializer Pattern (Alba Resources) from Development Standards
  • Include Alba::Resource
  • Add RDoc class documentation with description and @example
  • Define attributes: id, timing_type, period_value, period_unit, action_type, value_type, value, created_at, updated_at
  • Format timestamps using .iso8601 following existing patterns
  • Example implementation:
    # Serializer for BookingTimingCondition API responses
    #
    # Provides JSON serialization for booking timing condition data
    # following the Alba resource pattern.
    #
    # @example Basic serialization
    #   condition = BookingTimingCondition.find(1)
    #   BookingTimingConditionResource.new(condition).to_h
    #
    # @see PricingRuleResource for parent rule serialization
    class BookingTimingConditionResource
      include Alba::Resource
      
      attributes :id, :timing_type, :period_value, :period_unit,
                 :action_type, :value_type, :value
      
      attribute :created_at do |condition|
        condition.created_at.iso8601
      end
      
      attribute :updated_at do |condition|
        condition.updated_at.iso8601
      end
    end
    

Task 2.2: Update PricingRuleResource

  • Modify app/resources/pricing_rule_resource.rb
  • Add conditional serialization:
    many :booking_timing_conditions, 
         resource: BookingTimingConditionResource,
         if: proc { |rule| rule.rule_type == 'advance_last_minute' }
    

Task 2.3: Update Controller Strong Params

  • Modify app/controllers/api/partner/v1/pricing_rules_controller.rb
  • Add to pricing_rule_params:
    booking_timing_conditions_attributes: [
      :id,
      :timing_type,
      :period_value,
      :period_unit,
      :action_type,
      :value_type,
      :value,
      :_destroy
    ]
    
  • Update RDoc comments for controller actions

Task 2.4: Update Validation Service

  • Modify app/services/pricing/rule_validation_service.rb
  • Follow Service Object Pattern and Error Handling standards
  • Add case for 'advance_last_minute' rule type in validation logic
  • Use guard clauses to reduce nesting
  • Implement validations:
    • Validate at least one booking_timing_condition exists
    • Validate period_value and period_unit combinations make sense
    • Validate against GlobalPricingSetting limits for increase/decrease caps
    • Ensure no date_time_conditions exist for this rule type (mutual exclusion)
  • Return ServiceResult with success_result or failure_result
  • Add RDoc method documentation with @param, @return, @example
  • Use meaningful variable names throughout
  • Example validation logic:
    # Validates advance/last-minute booking timing rules
    #
    # @param pricing_rule [PricingRule] the rule to validate
    # @return [ServiceResult] success or failure with validation errors
    def validate_advance_last_minute_rule(pricing_rule)
      errors = []
      
      # Guard clause: must have conditions
      if pricing_rule.booking_timing_conditions.empty?
        return failure_result('Must have at least one booking timing condition')
      end
      
      # Guard clause: cannot have date_time_conditions
      if pricing_rule.date_time_conditions.any?
        errors << 'Cannot have date/time conditions with advance_last_minute rule type'
      end
      
      # Validate each condition
      pricing_rule.booking_timing_conditions.each do |condition|
        errors.concat(validate_timing_condition(condition))
      end
      
      errors.any? ? failure_result(errors) : success_result
    end
    
    private
    
    def validate_timing_condition(condition)
      errors = []
      
      # Warn about suspicious period configurations
      if condition.timing_type == 'last_minute_booking' && condition.period_in_minutes > 1440
        errors << "Period #{condition.period_value} #{condition.period_unit} seems too long for last-minute booking"
      end
      
      if condition.timing_type == 'advance_booking' && condition.period_in_minutes < 60
        errors << "Period #{condition.period_value} #{condition.period_unit} seems too short for advance booking"
      end
      
      # Validate against global limits
      errors.concat(validate_against_global_limits(condition))
      
      errors
    end
    

Phase 3: Business Logic Services (Day 3)

Task 3.1: Create BookingTimingEvaluator Service

  • Create app/services/pricing/booking_timing_evaluator.rb
  • Follow Service Object Pattern - inherit from BaseService
  • Include ApmInstrumented for APM Monitoring
  • Add comprehensive RDoc documentation with class description, @example, @see tags
  • Implement #initialize with RDoc @param tags
  • Implement #call method following Service Result Pattern
  • Use guard clauses to handle edge cases first
  • Use meaningful variable names (e.g., lead_time_minutes not ltm)
  • Extract complex logic to private methods with RDoc documentation
  • Return success_result with matched conditions or failure_result with errors
  • Handle errors with handle_error and proper logging
  • Example implementation:
    module Pricing
      # Evaluates booking timing conditions against booking and service times
      #
      # This service determines which booking timing conditions match based on
      # the lead time between when a booking is made and when service occurs.
      # Supports both advance booking discounts and last-minute surge pricing.
      #
      # @example Advance booking evaluation
      #   evaluator = Pricing::BookingTimingEvaluator.new(
      #     conditions: rule.booking_timing_conditions,
      #     booking_time: Time.parse('2025-01-01 10:00'),
      #     service_time: Time.parse('2025-01-15 19:00')
      #   )
      #   result = evaluator.call
      #   result.result # => [matched conditions]
      #
      # @example Last-minute booking evaluation
      #   evaluator = Pricing::BookingTimingEvaluator.new(
      #     conditions: rule.booking_timing_conditions,
      #     booking_time: Time.parse('2025-01-01 18:30'),
      #     service_time: Time.parse('2025-01-01 19:00')
      #   )
      #   result = evaluator.call
      #
      # @see BookingTimingCondition for condition model
      # @see Pricing::Engine for pricing engine integration
      class BookingTimingEvaluator < BaseService
        include ApmInstrumented
        
        # Initialize the evaluator with conditions and times
        #
        # @param conditions [Array<BookingTimingCondition>] conditions to evaluate
        # @param booking_time [Time] when the booking is made
        # @param service_time [Time] when the service will occur
        # @raise [ArgumentError] if times are invalid or booking_time > service_time
        def initialize(conditions:, booking_time:, service_time:)
          @conditions = conditions
          @booking_time = booking_time
          @service_time = service_time
        end
        
        # Evaluates conditions and returns matching ones
        #
        # @return [ServiceResult] success with matched conditions array or failure with errors
        def call
          # Guard clauses for invalid inputs
          return failure_result('Booking time cannot be after service time') if booking_time_after_service?
          return failure_result('No conditions to evaluate') if @conditions.empty?
          
          lead_time_minutes = calculate_lead_time_minutes
          matched_conditions = find_matching_conditions(lead_time_minutes)
          
          success_result(matched_conditions)
        rescue StandardError => e
          handle_error(e, 'Failed to evaluate booking timing conditions')
          failure_result(e.message)
        end
        
        private
        
        attr_reader :conditions, :booking_time, :service_time
        
        # Checks if booking time is after service time (invalid)
        #
        # @return [Boolean] true if invalid time ordering
        def booking_time_after_service?
          @booking_time > @service_time
        end
        
        # Calculates lead time between booking and service in minutes
        #
        # @return [Integer] lead time in minutes
        def calculate_lead_time_minutes
          ((@service_time - @booking_time) / 60).to_i
        end
        
        # Finds conditions that match the given lead time
        #
        # @param lead_time_minutes [Integer] the calculated lead time
        # @return [Array<BookingTimingCondition>] matching conditions
        def find_matching_conditions(lead_time_minutes)
          @conditions.select do |condition|
            matches_timing_criteria?(condition, lead_time_minutes)
          end
        end
        
        # Checks if a condition matches the lead time criteria
        #
        # @param condition [BookingTimingCondition] the condition to check
        # @param lead_time_minutes [Integer] the lead time to compare
        # @return [Boolean] true if condition matches
        def matches_timing_criteria?(condition, lead_time_minutes)
          condition_minutes = condition.period_in_minutes
          
          case condition.timing_type
          when 'advance_booking'
            lead_time_minutes >= condition_minutes
          when 'last_minute_booking'
            lead_time_minutes <= condition_minutes
          else
            false
          end
        end
      end
    end
    
  • Run Rubocop and fix any violations: bundle exec rubocop app/services/pricing/booking_timing_evaluator.rb -a

Task 3.2: Update Pricing::Engine

  • Modify app/services/pricing/engine.rb
  • Add optional parameter: booking_time: nil, service_time: nil
  • Add logic to detect advance_last_minute rule type
  • Call Pricing::BookingTimingEvaluator for matching
  • Pass matched conditions to Pricing::PriceCalculator
  • Maintain backward compatibility (skip if booking_time not provided)

Task 3.3: Update PriceCalculator

  • Modify app/services/pricing/price_calculator.rb
  • Follow DRY Principle - extract common logic between DateTimeCondition and BookingTimingCondition
  • Add support for BookingTimingCondition objects
  • Ensure percentage/fixed calculations work identically to DateTimeCondition
  • Apply Pricing::PriorityResolver with :highest_priority strategy
  • Use guard clauses to handle nil/empty conditions
  • Add RDoc documentation for new/modified methods
  • Extract calculation logic to private reusable methods:
    # Calculates the price adjustment for a condition
    #
    # @param base_price [Float] the base price to adjust
    # @param condition [DateTimeCondition, BookingTimingCondition] the condition
    # @return [Float] the adjusted price
    def calculate_adjustment(base_price, condition)
      adjustment_amount = case condition.value_type
                          when 'percentage'
                            calculate_percentage_adjustment(base_price, condition.value)
                          when 'fixed'
                            calculate_fixed_adjustment(condition.value)
                          else
                            0
                          end
      
      apply_action_type(base_price, adjustment_amount, condition.action_type)
    end
    
    private
    
    # Calculates percentage-based adjustment
    #
    # @param base_price [Float] the base price
    # @param basis_points [Integer] the percentage in basis points (1/100 of %)
    # @return [Float] the adjustment amount
    def calculate_percentage_adjustment(base_price, basis_points)
      base_price * (basis_points / 10000.0)
    end
    
    # Calculates fixed amount adjustment
    #
    # @param cents [Integer] the fixed amount in cents
    # @return [Float] the adjustment amount in currency units
    def calculate_fixed_adjustment(cents)
      cents / 100.0
    end
    
    # Applies increase or decrease action to price
    #
    # @param base_price [Float] the base price
    # @param adjustment [Float] the adjustment amount
    # @param action_type [String] 'increase' or 'decrease'
    # @return [Float] the final adjusted price
    def apply_action_type(base_price, adjustment, action_type)
      case action_type
      when 'increase'
        base_price + adjustment
      when 'decrease'
        base_price - adjustment
      else
        base_price
      end
    end
    
  • Ensure no magic numbers - use constants or extract to configuration

Phase 4: Repository & Caching (Day 3)

Task 4.1: Update PricingRuleRepository

  • Modify app/repositories/pricing_rule_repository.rb
  • Follow Repository Pattern and Caching Strategy standards
  • Include :booking_timing_conditions in .includes() for query optimization
  • Update methods to prevent N+1 queries:
    • find_active_rules
    • find_with_conditions
    • find_by_id_with_details
  • Update cache keys to include advance_last_minute rules
  • Add RDoc documentation for new/modified query methods
  • Use meaningful method names and meaningful variable names
  • Example implementation:
    # Fetches a pricing rule with all its conditions eagerly loaded
    #
    # Uses Redis caching with 5-minute TTL to reduce database load.
    # Includes date_time_conditions and booking_timing_conditions to prevent N+1 queries.
    #
    # @param rule_id [Integer] the pricing rule ID
    # @return [PricingRule, nil] the rule with conditions or nil if not found
    #
    # @example
    #   repository = PricingRuleRepository.new
    #   rule = repository.find_with_conditions(123)
    def find_with_conditions(rule_id)
      cache_key = "pricing_rule:#{rule_id}:with_all_conditions"
      
      cache_fetch(cache_key, ttl: 5.minutes) do
        PricingRule
          .includes(:date_time_conditions, :booking_timing_conditions, :pricing_rules_packages)
          .find_by(id: rule_id)
      end
    end
    
    # Fetches active rules for a restaurant with optional rule type filtering
    #
    # @param restaurant_id [String] the restaurant identifier
    # @param rule_type [String, nil] optional filter by rule type
    # @return [ActiveRecord::Relation] active pricing rules with conditions
    #
    # @example Get all active rules
    #   repository.find_active_rules(restaurant_id: 'rest_123')
    #
    # @example Get only advance/last-minute rules
    #   repository.find_active_rules(restaurant_id: 'rest_123', rule_type: 'advance_last_minute')
    def find_active_rules(restaurant_id:, rule_type: nil)
      query = PricingRule
        .active
        .where(restaurant_id: restaurant_id)
        .includes(
          :date_time_conditions,
          :booking_timing_conditions,
          :pricing_rules_packages
        )
      
      query = query.where(rule_type: rule_type) if rule_type.present?
      query.order(priority: :desc, created_at: :desc)
    end
    
  • Verify cache invalidation callbacks are working correctly
  • Test cache hit rates with Redis monitoring

Phase 5: Testing (Day 4-5)

Task 5.1: Model Specs

  • Create spec/models/booking_timing_condition_spec.rb
  • Follow Testing Standards - aim for 95%+ coverage
  • Test all validations following Unit Tests pattern:
    • Presence validations for all required fields
    • Inclusion validations for enum fields
    • Numericality validations for period_value and value
    • Custom validation: period_makes_sense
  • Test associations with PricingRule (belongs_to)
  • Test helper method #period_in_minutes with all units
  • Test cache invalidation callbacks (verify cache is cleared after save)
  • Use FactoryBot factories for test data
  • Follow RSpec best practices:
    • Use descriptive describe and context blocks
    • One expectation per test (when possible)
    • Use let for test data setup
    • Use shared examples for common behavior
  • Example test structure:
    require 'rails_helper'
    
    RSpec.describe BookingTimingCondition, type: :model do
      describe 'associations' do
        it { should belong_to(:pricing_rule) }
      end
      
      describe 'validations' do
        subject { build(:booking_timing_condition) }
        
        describe 'timing_type' do
          it { should validate_presence_of(:timing_type) }
          it { should validate_inclusion_of(:timing_type).in_array(%w[advance_booking last_minute_booking]) }
        end
        
        describe 'period_value' do
          it { should validate_presence_of(:period_value) }
          it { should validate_numericality_of(:period_value).is_greater_than(0) }
        end
        
        # ... more validations
        
        describe 'period_makes_sense' do
          context 'when last_minute_booking with long period' do
            it 'adds error for period > 1 day' do
              condition = build(:booking_timing_condition, 
                              timing_type: 'last_minute_booking',
                              period_value: 2,
                              period_unit: 'days')
              expect(condition).not_to be_valid
              expect(condition.errors[:period_value]).to include(/too long for last-minute/)
            end
          end
          
          context 'when advance_booking with short period' do
            it 'adds error for period < 1 hour' do
              condition = build(:booking_timing_condition,
                              timing_type: 'advance_booking',
                              period_value: 30,
                              period_unit: 'minutes')
              expect(condition).not_to be_valid
              expect(condition.errors[:period_value]).to include(/too short for advance/)
            end
          end
        end
      end
      
      describe '#period_in_minutes' do
        context 'with minutes' do
          it 'returns the value unchanged' do
            condition = build(:booking_timing_condition, period_value: 30, period_unit: 'minutes')
            expect(condition.period_in_minutes).to eq(30)
          end
        end
        
        context 'with hours' do
          it 'converts to minutes' do
            condition = build(:booking_timing_condition, period_value: 2, period_unit: 'hours')
            expect(condition.period_in_minutes).to eq(120)
          end
        end
        
        context 'with days' do
          it 'converts to minutes' do
            condition = build(:booking_timing_condition, period_value: 15, period_unit: 'days')
            expect(condition.period_in_minutes).to eq(21600)
          end
        end
        
        context 'with weeks' do
          it 'converts to minutes' do
            condition = build(:booking_timing_condition, period_value: 1, period_unit: 'weeks')
            expect(condition.period_in_minutes).to eq(10080)
          end
        end
        
        context 'with months' do
          it 'converts to minutes' do
            condition = build(:booking_timing_condition, period_value: 1, period_unit: 'months')
            expect(condition.period_in_minutes).to eq(43200)
          end
        end
      end
      
      describe 'cache invalidation' do
        it 'invalidates cache after create' do
          expect(PricingRuleRepository).to receive(:invalidate_cache)
          create(:booking_timing_condition)
        end
        
        it 'invalidates cache after update' do
          condition = create(:booking_timing_condition)
          expect(PricingRuleRepository).to receive(:invalidate_cache)
          condition.update(period_value: 20)
        end
      end
    end
    
  • Run tests: bundle exec rspec spec/models/booking_timing_condition_spec.rb
  • Verify coverage: COVERAGE=true bundle exec rspec spec/models/booking_timing_condition_spec.rb

Task 5.2: Service Specs

  • Create spec/services/pricing/booking_timing_evaluator_spec.rb
  • Follow Service Object Tests standards
  • Test success cases with success? and result
  • Test failure cases with failure? and errors
  • Test APM instrumentation (verify service is tracked)
  • Use FactoryBot factories for test data
  • Test advance booking scenarios:
    • 15 days advance matches 7-day rule ✅
    • 3 days advance doesn’t match 7-day rule ❌
    • Exactly 7 days matches 7-day rule ✅ (boundary test)
    • 6.99 days doesn’t match 7-day rule ❌ (boundary test)
  • Test last-minute scenarios:
    • 10 minutes before matches 30-minute rule ✅
    • 45 minutes before doesn’t match 30-minute rule ❌
    • Exactly 30 minutes matches 30-minute rule ✅ (boundary test)
    • 30.01 minutes doesn’t match 30-minute rule ❌ (boundary test)
  • Test edge cases following Additional Best Practices:
    • Same booking and service time (0 lead time)
    • Service time in the past (negative lead time)
    • Multiple conditions matching (verify all returned)
    • Empty conditions array
    • Nil booking_time or service_time
  • Test error handling and logging
  • Example test structure:
    require 'rails_helper'
    
    RSpec.describe Pricing::BookingTimingEvaluator, type: :service do
      let(:pricing_rule) { create(:pricing_rule, rule_type: 'advance_last_minute') }
      
      describe '#call' do
        context 'with advance booking conditions' do
          let!(:seven_day_condition) do
            create(:booking_timing_condition, :advance_booking_discount,
                   pricing_rule: pricing_rule,
                   period_value: 7,
                   period_unit: 'days')
          end
          
          let(:service_time) { Time.parse('2025-01-15 19:00') }
          
          context 'when booking 15 days in advance' do
            let(:booking_time) { Time.parse('2024-12-31 10:00') }
            
            it 'returns success result' do
              evaluator = described_class.new(
                conditions: [seven_day_condition],
                booking_time: booking_time,
                service_time: service_time
              )
              result = evaluator.call
              
              expect(result).to be_success
            end
            
            it 'includes the matched condition' do
              evaluator = described_class.new(
                conditions: [seven_day_condition],
                booking_time: booking_time,
                service_time: service_time
              )
              result = evaluator.call
              
              expect(result.result).to include(seven_day_condition)
            end
          end
          
          context 'when booking 3 days in advance' do
            let(:booking_time) { Time.parse('2025-01-12 10:00') }
            
            it 'returns success with empty matches' do
              evaluator = described_class.new(
                conditions: [seven_day_condition],
                booking_time: booking_time,
                service_time: service_time
              )
              result = evaluator.call
              
              expect(result).to be_success
              expect(result.result).to be_empty
            end
          end
          
          context 'when booking exactly 7 days in advance (boundary)' do
            let(:booking_time) { service_time - 7.days }
            
            it 'matches the condition' do
              evaluator = described_class.new(
                conditions: [seven_day_condition],
                booking_time: booking_time,
                service_time: service_time
              )
              result = evaluator.call
              
              expect(result).to be_success
              expect(result.result).to include(seven_day_condition)
            end
          end
        end
        
        context 'with last-minute booking conditions' do
          let!(:thirty_min_condition) do
            create(:booking_timing_condition, :last_minute_surge,
                   pricing_rule: pricing_rule,
                   period_value: 30,
                   period_unit: 'minutes')
          end
          
          let(:service_time) { Time.parse('2025-01-01 19:00') }
          
          context 'when booking 10 minutes before' do
            let(:booking_time) { service_time - 10.minutes }
            
            it 'matches the condition' do
              evaluator = described_class.new(
                conditions: [thirty_min_condition],
                booking_time: booking_time,
                service_time: service_time
              )
              result = evaluator.call
              
              expect(result).to be_success
              expect(result.result).to include(thirty_min_condition)
            end
          end
          
          context 'when booking 45 minutes before' do
            let(:booking_time) { service_time - 45.minutes }
            
            it 'does not match the condition' do
              evaluator = described_class.new(
                conditions: [thirty_min_condition],
                booking_time: booking_time,
                service_time: service_time
              )
              result = evaluator.call
              
              expect(result).to be_success
              expect(result.result).to be_empty
            end
          end
        end
        
        context 'edge cases' do
          let(:service_time) { Time.parse('2025-01-01 19:00') }
          let(:condition) { create(:booking_timing_condition, pricing_rule: pricing_rule) }
          
          context 'when booking_time equals service_time' do
            let(:booking_time) { service_time }
            
            it 'handles zero lead time gracefully' do
              evaluator = described_class.new(
                conditions: [condition],
                booking_time: booking_time,
                service_time: service_time
              )
              result = evaluator.call
              
              expect(result).to be_success
            end
          end
          
          context 'when booking_time is after service_time' do
            let(:booking_time) { service_time + 1.hour }
            
            it 'returns failure result' do
              evaluator = described_class.new(
                conditions: [condition],
                booking_time: booking_time,
                service_time: service_time
              )
              result = evaluator.call
              
              expect(result).to be_failure
              expect(result.errors).to include(/cannot be after service time/)
            end
          end
          
          context 'when conditions array is empty' do
            it 'returns failure result' do
              evaluator = described_class.new(
                conditions: [],
                booking_time: booking_time,
                service_time: service_time
              )
              result = evaluator.call
              
              expect(result).to be_failure
            end
          end
        end
        
        context 'error handling' do
          it 'handles exceptions gracefully' do
            evaluator = described_class.new(
              conditions: [double('condition', period_in_minutes: nil)],
              booking_time: Time.current,
              service_time: Time.current + 1.hour
            )
            
            expect { evaluator.call }.not_to raise_error
            result = evaluator.call
            expect(result).to be_failure
          end
        end
      end
    end
    
  • Run tests: bundle exec rspec spec/services/pricing/booking_timing_evaluator_spec.rb --format documentation
  • Verify 95%+ coverage

Task 5.3: Integration Specs

  • Update spec/requests/api/partner/v1/pricing_rules_spec.rb
  • Follow Integration Tests and Swagger Documentation standards
  • Add Swagger-documented tests covering:
    • Happy path scenarios (201, 200 responses)
    • Error cases (400, 401, 403, 404, 422, 500)
    • Edge cases
    • Authentication and authorization
  • Test nested attribute creation with booking_timing_conditions_attributes
  • Test nested attribute updates with existing condition IDs
  • Test nested attribute destruction with _destroy: true
  • Test validation error responses (422 with error details)
  • Follow Swagger/RSpec pattern with run_test!
  • Example test structure:
    require 'swagger_helper'
    
    RSpec.describe 'API::Partner::V1::PricingRules - Booking Timing', type: :request do
      let(:restaurant_id) { 'rest_123' }
      let(:access_token) { create(:access_token, restaurant_id: restaurant_id) }
      let(:authorization) { "Bearer #{access_token.token}" }
      
      path '/api/partner/v1/pricing_rules' do
        post 'Creates a pricing rule with booking timing conditions' do
          tags 'Pricing Rules'
          consumes 'application/json'
          produces 'application/json'
          security [{ partner_auth: [] }]
          
          parameter name: 'X-Restaurant-Id', in: :header, type: :string, required: true
          parameter name: :pricing_rule, in: :body, schema: {
            type: :object,
            properties: {
              pricing_rule: {
                type: :object,
                properties: {
                  name: { type: :string },
                  rule_type: { type: :string, enum: ['advance_last_minute'] },
                  priority: { type: :integer },
                  active: { type: :boolean },
                  apply_to_all_packages: { type: :boolean },
                  booking_timing_conditions_attributes: {
                    type: :array,
                    items: {
                      type: :object,
                      properties: {
                        timing_type: { type: :string, enum: ['advance_booking', 'last_minute_booking'] },
                        period_value: { type: :integer },
                        period_unit: { type: :string, enum: ['minutes', 'hours', 'days', 'weeks', 'months'] },
                        action_type: { type: :string, enum: ['increase', 'decrease'] },
                        value_type: { type: :string, enum: ['percentage', 'fixed'] },
                        value: { type: :integer }
                      },
                      required: ['timing_type', 'period_value', 'period_unit', 'action_type', 'value_type', 'value']
                    }
                  }
                },
                required: ['name', 'rule_type']
              }
            }
          }
          
          response '201', 'pricing rule created with booking timing conditions' do
            let(:'X-Restaurant-Id') { restaurant_id }
            let(:pricing_rule) do
              {
                pricing_rule: {
                  name: 'Early Bird Discount',
                  rule_type: 'advance_last_minute',
                  priority: 80,
                  active: true,
                  apply_to_all_packages: true,
                  booking_timing_conditions_attributes: [
                    {
                      timing_type: 'advance_booking',
                      period_value: 15,
                      period_unit: 'days',
                      action_type: 'decrease',
                      value_type: 'percentage',
                      value: 500
                    }
                  ]
                }
              }
            end
            
            run_test! do |response|
              data = JSON.parse(response.body)
              
              expect(data['name']).to eq('Early Bird Discount')
              expect(data['rule_type']).to eq('advance_last_minute')
              expect(data['booking_timing_conditions']).to be_an(Array)
              expect(data['booking_timing_conditions'].length).to eq(1)
              
              condition = data['booking_timing_conditions'].first
              expect(condition['timing_type']).to eq('advance_booking')
              expect(condition['period_value']).to eq(15)
              expect(condition['period_unit']).to eq('days')
              expect(condition['action_type']).to eq('decrease')
              expect(condition['value_type']).to eq('percentage')
              expect(condition['value']).to eq(500)
            end
          end
          
          response '422', 'invalid request - validation errors' do
            let(:'X-Restaurant-Id') { restaurant_id }
            let(:pricing_rule) do
              {
                pricing_rule: {
                  name: '',
                  rule_type: 'advance_last_minute',
                  booking_timing_conditions_attributes: []
                }
              }
            end
            
            run_test! do |response|
              data = JSON.parse(response.body)
              expect(data['errors']).to be_present
            end
          end
          
          response '401', 'unauthorized - invalid token' do
            let(:authorization) { 'Bearer invalid_token' }
            let(:'X-Restaurant-Id') { restaurant_id }
            let(:pricing_rule) { { pricing_rule: { name: 'Test' } } }
            
            run_test!
          end
        end
      end
      
      path '/api/partner/v1/pricing_rules/{id}' do
        patch 'Updates a pricing rule with booking timing conditions' do
          tags 'Pricing Rules'
          consumes 'application/json'
          produces 'application/json'
          security [{ partner_auth: [] }]
          
          parameter name: 'X-Restaurant-Id', in: :header, type: :string, required: true
          parameter name: :id, in: :path, type: :integer, required: true
          parameter name: :pricing_rule, in: :body, schema: {
            type: :object,
            properties: {
              pricing_rule: {
                type: :object,
                properties: {
                  name: { type: :string },
                  booking_timing_conditions_attributes: {
                    type: :array,
                    items: {
                      type: :object,
                      properties: {
                        id: { type: :integer },
                        timing_type: { type: :string },
                        period_value: { type: :integer },
                        period_unit: { type: :string },
                        action_type: { type: :string },
                        value_type: { type: :string },
                        value: { type: :integer },
                        _destroy: { type: :boolean }
                      }
                    }
                  }
                }
              }
            }
          }
          
          let(:existing_rule) do
            create(:pricing_rule, 
                   rule_type: 'advance_last_minute',
                   restaurant_id: restaurant_id)
          end
          
          let(:existing_condition) do
            create(:booking_timing_condition, pricing_rule: existing_rule)
          end
          
          response '200', 'pricing rule updated' do
            let(:'X-Restaurant-Id') { restaurant_id }
            let(:id) { existing_rule.id }
            let(:pricing_rule) do
              {
                pricing_rule: {
                  name: 'Updated Name',
                  booking_timing_conditions_attributes: [
                    {
                      id: existing_condition.id,
                      value: 700
                    },
                    {
                      timing_type: 'last_minute_booking',
                      period_value: 30,
                      period_unit: 'minutes',
                      action_type: 'increase',
                      value_type: 'fixed',
                      value: 50000
                    }
                  ]
                }
              }
            end
            
            run_test! do |response|
              data = JSON.parse(response.body)
              expect(data['name']).to eq('Updated Name')
              expect(data['booking_timing_conditions'].length).to eq(2)
            end
          end
          
          response '404', 'pricing rule not found' do
            let(:'X-Restaurant-Id') { restaurant_id }
            let(:id) { 99999 }
            let(:pricing_rule) { { pricing_rule: { name: 'Test' } } }
            
            run_test!
          end
        end
        
        get 'Retrieves a pricing rule with booking timing conditions' do
          tags 'Pricing Rules'
          produces 'application/json'
          security [{ partner_auth: [] }]
          
          parameter name: 'X-Restaurant-Id', in: :header, type: :string, required: true
          parameter name: :id, in: :path, type: :integer, required: true
          
          let(:existing_rule) do
            rule = create(:pricing_rule,
                         rule_type: 'advance_last_minute',
                         restaurant_id: restaurant_id)
            create(:booking_timing_condition, :advance_booking_discount, pricing_rule: rule)
            rule
          end
          
          response '200', 'pricing rule found' do
            let(:'X-Restaurant-Id') { restaurant_id }
            let(:id) { existing_rule.id }
            
            run_test! do |response|
              data = JSON.parse(response.body)
              expect(data['rule_type']).to eq('advance_last_minute')
              expect(data['booking_timing_conditions']).to be_present
              expect(data['booking_timing_conditions']).to be_an(Array)
            end
          end
        end
      end
    end
    
  • Run integration tests: bundle exec rspec spec/requests/api/partner/v1/pricing_rules_spec.rb
  • Generate Swagger docs: bundle exec rake rswag:specs:swaggerize
  • Verify Swagger UI renders correctly

Task 5.4: Engine Integration Tests

  • Update spec/services/pricing/engine_spec.rb
  • Test end-to-end pricing calculation with booking timing rules
  • Test priority resolution when multiple rules match
  • Test that missing booking_time parameter doesn’t break existing flows

Phase 6: Factories & Seeds (Day 5)

Task 6.1: Create Factory

  • Create spec/factories/booking_timing_conditions.rb
  • Follow Factory Definitions standards
  • Use Faker for dynamic test data where appropriate
  • Create base factory with all required attributes
  • Add traits for common scenarios
  • Use meaningful trait names that clearly describe the scenario
  • Example implementation:
    # spec/factories/booking_timing_conditions.rb
    FactoryBot.define do
      factory :booking_timing_condition do
        association :pricing_rule
        
        timing_type { 'advance_booking' }
        period_value { 7 }
        period_unit { 'days' }
        action_type { 'decrease' }
        value_type { 'percentage' }
        value { 500 }  # 5.00% in basis points
        
        # Trait: 15-day advance booking with 5% discount
        trait :advance_booking_discount do
          timing_type { 'advance_booking' }
          period_value { 15 }
          period_unit { 'days' }
          action_type { 'decrease' }
          value_type { 'percentage' }
          value { 500 }  # 5.00%
        end
        
        # Trait: 30-minute last-minute booking with 10% surge
        trait :last_minute_surge do
          timing_type { 'last_minute_booking' }
          period_value { 30 }
          period_unit { 'minutes' }
          action_type { 'increase' }
          value_type { 'percentage' }
          value { 1000 }  # 10.00%
        end
        
        # Trait: 3-month early bird with fixed 500 THB discount
        trait :early_bird_fixed do
          timing_type { 'advance_booking' }
          period_value { 3 }
          period_unit { 'months' }
          action_type { 'decrease' }
          value_type { 'fixed' }
          value { 50000 }  # 500.00 THB (in cents)
        end
        
        # Trait: 1-hour last-minute with fixed 100 THB surcharge
        trait :last_hour_surcharge do
          timing_type { 'last_minute_booking' }
          period_value { 1 }
          period_unit { 'hours' }
          action_type { 'increase' }
          value_type { 'fixed' }
          value { 10000 }  # 100.00 THB (in cents)
        end
        
        # Trait: 7-day advance booking with 3% discount
        trait :week_advance_discount do
          timing_type { 'advance_booking' }
          period_value { 7 }
          period_unit { 'days' }
          action_type { 'decrease' }
          value_type { 'percentage' }
          value { 300 }  # 3.00%
        end
        
        # Trait: 2-week advance with 8% discount
        trait :two_week_advance do
          timing_type { 'advance_booking' }
          period_value { 2 }
          period_unit { 'weeks' }
          action_type { 'decrease' }
          value_type { 'percentage' }
          value { 800 }  # 8.00%
        end
        
        # Trait: 15-minute last-minute with 15% surge
        trait :extreme_last_minute do
          timing_type { 'last_minute_booking' }
          period_value { 15 }
          period_unit { 'minutes' }
          action_type { 'increase' }
          value_type { 'percentage' }
          value { 1500 }  # 15.00%
        end
      end
    end
    
  • Verify factories are valid: bundle exec rake factory_bot:lint

Task 6.2: Update Seeds

  • Modify db/seeds.rb
  • Add example advance/last-minute pricing rules for demo restaurants

Phase 7: Documentation (Day 6)

Task 7.1: Update API Documentation

  • Modify docs/004-API-ENDPOINTS.md
  • Document advance_last_minute rule_type
  • Add request/response examples with booking_timing_conditions_attributes
  • Document validation rules and error responses

Task 7.2: Generate Swagger Docs

  • Run bundle exec rake rswag:specs:swaggerize
  • Verify swagger/v1/swagger.yaml updated correctly
  • Test Swagger UI renders new endpoints

Task 7.3: Update Postman Collection

  • Open docs/postman/Partner_API_v1.postman_collection.json
  • Add folder: “Booking Timing Rules”
  • Add requests:
    • Create Advance Booking Discount Rule (15 days, -5%)
    • Create Last-Minute Surge Rule (30 minutes, +10%)
    • Update Rule with Multiple Conditions
    • Delete Booking Timing Condition

Task 7.4: Create Feature Documentation

  • Create docs/013-BOOKING-TIMING-RULES.md
  • Explain feature purpose and use cases
  • Provide configuration examples
  • Document timing_type logic and period calculations
  • Add troubleshooting section

Phase 8: Code Review & Cleanup (Day 6)

Task 8.1: Run Code Quality Checks

  • Run all tests: bundle exec rspec spec/
  • Verify 90%+ coverage: COVERAGE=true bundle exec rspec spec/
  • Check coverage report: open coverage/index.html
  • Run Rubocop: bundle exec rubocop
  • Fix all violations: bundle exec rubocop -a
  • Verify no offenses remain: bundle exec rubocop --format offenses

Task 8.2: Review Against Development Standards

  • RDoc Documentation Standards:
    • All classes have RDoc comments with description and @example
    • All public methods have @param, @return, @raise tags
    • Constants have documentation comments
    • Generate RDoc: bundle exec rails rdoc
    • Review docs: open doc/index.html
  • Architecture Patterns:
    • Service objects inherit from BaseService
    • Services include ApmInstrumented
    • Services use success_result/failure_result pattern
    • Repository queries use caching with cache_fetch
    • Serializers use Alba pattern correctly
    • Controllers use strong parameters
  • Code Reusability & DRY:
    • No duplicate validation logic
    • No duplicate query patterns
    • No duplicate calculation logic
    • Common behavior extracted to private methods
    • Helper methods properly documented
  • Error Handling & Logging:
    • All exceptions handled with handle_error
    • Appropriate log levels used (debug, info, warn, error)
    • ServiceResult pattern used consistently
    • Guard clauses reduce nesting
  • Additional Best Practices:
    • No magic numbers (all extracted to constants)
    • Meaningful variable names (no abbreviations)
    • Methods have single responsibility
    • Private methods marked with private keyword
    • Comments explain WHY, not WHAT
    • Consistent method return types
    • Guard clauses used appropriately
  • Testing Standards:
    • All models have unit tests (90%+ coverage)
    • All services have service object tests
    • All endpoints have integration tests with Swagger docs
    • Factories use traits for common scenarios
    • Tests use descriptive describe/context blocks

Task 8.3: Security Review

  • Authentication:
    • All endpoints verify access tokens
    • Restaurant context validated
    • Authentication attempts logged
  • Input Validation:
    • Strong parameters properly configured
    • All user inputs validated
    • Database constraints in place
    • Length and format checks present
  • SQL Injection Prevention:
    • All queries use parameterized queries or ActiveRecord methods
    • No string interpolation in SQL queries
    • Repository pattern followed consistently
  • Mass Assignment Protection:
    • Strong parameters configured for nested attributes
    • Only permitted attributes accepted
    • _destroy flag properly handled
  • Performance:
    • Check for N+1 queries (use .includes())
    • Verify cache invalidation works
    • Test query performance with APM
    • Monitor Redis cache hit rates

Testing Strategy

Unit Test Coverage (Target: 95%+)

Models:

  • Validations (all presence, inclusion, numericality rules)
  • Associations (belongs_to, has_many through)
  • Helper methods (#period_in_minutes)
  • Cache invalidation callbacks

Services:

  • Success paths with valid data
  • Failure paths with invalid data
  • Edge cases (zero values, boundary conditions)
  • Error handling and logging

Resources:

  • Attribute serialization
  • Conditional rendering
  • Nested associations

Integration Test Coverage

API Endpoints:

  • CRUD operations with nested attributes
  • Authentication and authorization
  • Validation error responses
  • Swagger documentation compliance

End-to-End:

  • Full pricing calculation flow
  • Priority resolution with multiple rules
  • Backward compatibility with existing rule types

Rollout Plan

Phase 1: Development & Testing (Days 1-6)

  • Complete all implementation tasks
  • Achieve 90%+ test coverage
  • Pass all Rubocop checks

Phase 2: Code Review (Day 7)

  • Submit PR with comprehensive description
  • Address review feedback
  • Update documentation based on feedback

Phase 3: Staging Deployment (Day 8)

  • Deploy to staging environment
  • Run smoke tests
  • Verify API documentation renders correctly

Phase 4: QA Testing (Days 9-10)

  • QA team tests UI integration
  • Verify booking timing calculations
  • Test edge cases and error scenarios

Phase 5: Production Deployment (Day 11)

  • Deploy to production during low-traffic window
  • Monitor APM for errors
  • Verify cache warming
  • Check logs for validation errors

Phase 6: Monitoring (Week 2)

  • Monitor rule creation rate
  • Track validation errors
  • Collect user feedback
  • Plan iteration improvements

Validation Rules Summary

BookingTimingCondition Validations

validates :timing_type, presence: true, 
          inclusion: { in: %w[advance_booking last_minute_booking] }

validates :period_value, presence: true, 
          numericality: { greater_than: 0, only_integer: true }

validates :period_unit, presence: true,
          inclusion: { in: %w[minutes hours days weeks months] }

validates :action_type, presence: true,
          inclusion: { in: %w[increase decrease] }

validates :value_type, presence: true,
          inclusion: { in: %w[percentage fixed] }

validates :value, presence: true,
          numericality: { greater_than: 0, only_integer: true }

# Business Rule Validation
validate :period_makes_sense

private

def period_makes_sense
  # Warn if using 'last_minute_booking' with very long periods
  if timing_type == 'last_minute_booking' && period_in_minutes > 1440 # > 1 day
    errors.add(:period_value, 'seems too long for last-minute booking')
  end
  
  # Warn if using 'advance_booking' with very short periods
  if timing_type == 'advance_booking' && period_in_minutes < 60 # < 1 hour
    errors.add(:period_value, 'seems too short for advance booking')
  end
end

API Request/Response Examples

Create Rule with Advance Booking Discount

Request:

POST /api/partner/v1/pricing_rules
Content-Type: application/json
X-Restaurant-Id: rest_123
Authorization: Bearer <token>

{
  "pricing_rule": {
    "name": "Early Bird Discount",
    "rule_type": "advance_last_minute",
    "priority": 80,
    "active": true,
    "apply_to_all_packages": true,
    "booking_timing_conditions_attributes": [
      {
        "timing_type": "advance_booking",
        "period_value": 15,
        "period_unit": "days",
        "action_type": "decrease",
        "value_type": "percentage",
        "value": 500
      }
    ]
  }
}

Response:

{
  "id": 789,
  "restaurant_id": "rest_123",
  "name": "Early Bird Discount",
  "rule_type": "advance_last_minute",
  "priority": 80,
  "active": true,
  "requires_approval": false,
  "apply_to_all_packages": true,
  "booking_timing_conditions": [
    {
      "id": 1001,
      "timing_type": "advance_booking",
      "period_value": 15,
      "period_unit": "days",
      "action_type": "decrease",
      "value_type": "percentage",
      "value": 500,
      "created_at": "2025-12-17T10:00:00Z",
      "updated_at": "2025-12-17T10:00:00Z"
    }
  ],
  "created_at": "2025-12-17T10:00:00Z",
  "updated_at": "2025-12-17T10:00:00Z"
}

Create Rule with Last-Minute Surge

Request:

POST /api/partner/v1/pricing_rules
Content-Type: application/json
X-Restaurant-Id: rest_123
Authorization: Bearer <token>

{
  "pricing_rule": {
    "name": "Last-Minute Premium",
    "rule_type": "advance_last_minute",
    "priority": 90,
    "active": true,
    "package_ids": ["pkg_456"],
    "booking_timing_conditions_attributes": [
      {
        "timing_type": "last_minute_booking",
        "period_value": 30,
        "period_unit": "minutes",
        "action_type": "increase",
        "value_type": "fixed",
        "value": 50000
      }
    ]
  }
}

Update Rule with Multiple Conditions

Request:

PATCH /api/partner/v1/pricing_rules/789
Content-Type: application/json
X-Restaurant-Id: rest_123
Authorization: Bearer <token>

{
  "pricing_rule": {
    "name": "Tiered Advance Discounts",
    "booking_timing_conditions_attributes": [
      {
        "id": 1001,
        "value": 700
      },
      {
        "timing_type": "advance_booking",
        "period_value": 7,
        "period_unit": "days",
        "action_type": "decrease",
        "value_type": "percentage",
        "value": 300
      }
    ]
  }
}

File Checklist

New Files to Create

  • db/migrate/YYYYMMDDHHMMSS_create_booking_timing_conditions.rb
  • app/models/booking_timing_condition.rb
  • app/resources/booking_timing_condition_resource.rb
  • app/services/pricing/booking_timing_evaluator.rb
  • spec/models/booking_timing_condition_spec.rb
  • spec/services/pricing/booking_timing_evaluator_spec.rb
  • spec/factories/booking_timing_conditions.rb
  • docs/013-BOOKING-TIMING-RULES.md

Files to Modify

  • app/models/pricing_rule.rb
  • app/resources/pricing_rule_resource.rb
  • app/controllers/api/partner/v1/pricing_rules_controller.rb
  • app/services/pricing/rule_validation_service.rb
  • app/services/pricing/engine.rb
  • app/services/pricing/price_calculator.rb
  • app/repositories/pricing_rule_repository.rb
  • spec/requests/api/partner/v1/pricing_rules_spec.rb
  • spec/services/pricing/engine_spec.rb
  • db/seeds.rb
  • docs/004-API-ENDPOINTS.md
  • docs/postman/Partner_API_v1.postman_collection.json

Success Criteria

All criteria must be met before merging to main:

  • Testing: All tests pass with 90%+ coverage
  • Code Quality: Rubocop violations resolved (zero offenses)
  • Documentation:
    • API documentation complete and accurate
    • RDoc generated and reviewed
    • Postman collection updated and tested
    • Feature documentation (013-BOOKING-TIMING-RULES.md) complete
  • Code Review: Approved by 2+ developers
  • Development Standards Compliance:
    • RDoc documentation on all classes/methods
    • Service Object Pattern followed
    • Repository Pattern with caching
    • Alba serializers for API responses
    • Error Handling & Logging standards met
    • DRY principle applied
    • Guard clauses used appropriately
    • Meaningful variable names throughout
    • No magic numbers
    • Private methods properly marked
  • QA Sign-off: Functionality tested and approved
  • Performance: Benchmarks met (no degradation)
  • APM Monitoring: Shows no errors or warnings
  • Database:
    • Migrations tested (up and down)
    • Indexes properly created
    • Cache invalidation working
  • Security:
    • Authentication verified
    • Input validation complete
    • No SQL injection vulnerabilities
    • Mass assignment protection verified

Risk Mitigation

Risk 1: Period Conversion Edge Cases

Mitigation: Extensive unit tests covering all period_unit conversions, boundary values, and edge cases like leap years.

Risk 2: Timezone Handling

Mitigation: All times stored in UTC, conversions handled at application boundary. Document timezone expectations clearly.

Risk 3: Performance Impact on Repository Queries

Mitigation: Ensure .includes(:booking_timing_conditions) prevents N+1 queries. Monitor query performance with APM.

Risk 4: Cache Invalidation Issues

Mitigation: Use after_commit callbacks consistently, test cache invalidation in specs, monitor Redis cache hit rates.

Risk 5: Validation Complexity

Mitigation: Extract validation to dedicated service, provide clear error messages, add comprehensive integration tests.


Next Steps

  1. Review this plan with tech lead and product owner
  2. Create JIRA tickets for each phase
  3. Set up feature branch: feature/booking-timing-rules
  4. Begin Phase 1: Database & Models
  5. Daily standups to track progress and blockers

Document Version: 1.0
Last Updated: December 17, 2025
Author: AI Development Assistant
Reviewers: [Pending]