Skip to content

Complex Scenarios

Braden Keith edited this page Sep 20, 2025 · 1 revision

Complex Scenarios

Real-world implementation patterns for sophisticated availability requirements.

Multi-Resource Booking System

Scenario

Conference room booking with equipment, catering, and staff requirements.

namespace App\Services;

use RomegaSoftware\Availability\Support\AvailabilityEngine;
use RomegaSoftware\Availability\Support\Effect;

class ConferenceBookingService
{
    private AvailabilityEngine $engine;
    
    public function __construct(AvailabilityEngine $engine)
    {
        $this->engine = $engine;
    }
    
    public function checkAvailability($roomId, $startTime, $duration, array $requirements = [])
    {
        $room = ConferenceRoom::find($roomId);
        $endTime = $startTime->copy()->addMinutes($duration);
        
        // Check room availability
        if (!$this->isTimeSlotAvailable($room, $startTime, $endTime)) {
            return ['available' => false, 'reason' => 'Room not available'];
        }
        
        // Check equipment
        if (!empty($requirements['equipment'])) {
            foreach ($requirements['equipment'] as $equipmentId) {
                $equipment = Equipment::find($equipmentId);
                if (!$this->isTimeSlotAvailable($equipment, $startTime, $endTime)) {
                    return [
                        'available' => false,
                        'reason' => "Equipment {$equipment->name} not available"
                    ];
                }
            }
        }
        
        // Check catering availability
        if ($requirements['catering'] ?? false) {
            $catering = CateringService::first();
            if (!$this->engine->isAvailable($catering, $startTime)) {
                return ['available' => false, 'reason' => 'Catering not available'];
            }
        }
        
        // Check staff requirements
        if ($requirements['staff_support'] ?? false) {
            $availableStaff = Staff::whereHas('availabilityRules')
                ->get()
                ->filter(fn($staff) => $this->engine->isAvailable($staff, $startTime))
                ->count();
            
            if ($availableStaff < 1) {
                return ['available' => false, 'reason' => 'No staff available'];
            }
        }
        
        return ['available' => true, 'resources' => $this->gatherResources($requirements)];
    }
    
    private function isTimeSlotAvailable($resource, $start, $end)
    {
        $current = $start->copy();
        while ($current < $end) {
            if (!$this->engine->isAvailable($resource, $current)) {
                return false;
            }
            $current->addMinutes(15); // Check every 15 minutes
        }
        return true;
    }
    
    public function setupRoomRules(ConferenceRoom $room)
    {
        // Basic business hours
        $room->availabilityRules()->createMany([
            [
                'type' => 'weekdays',
                'config' => ['days' => [1, 2, 3, 4, 5]],
                'effect' => Effect::Allow,
                'priority' => 10,
            ],
            [
                'type' => 'time_of_day',
                'config' => ['from' => '08:00', 'to' => '18:00'],
                'effect' => Effect::Allow,
                'priority' => 20,
            ],
        ]);
        
        // Maintenance window
        $room->availabilityRules()->create([
            'type' => 'rrule',
            'config' => [
                'rrule' => 'FREQ=WEEKLY;BYDAY=FR;BYHOUR=16,17',
            ],
            'effect' => Effect::Deny,
            'priority' => 30,
        ]);
        
        // Capacity check
        $room->availabilityRules()->create([
            'type' => 'inventory_gate',
            'config' => ['min' => 1],
            'effect' => Effect::Allow,
            'priority' => 40,
        ]);
    }
}

Appointment Scheduling with Buffer Times

Scenario

Medical appointments requiring prep and cleanup time.

namespace App\Models;

use RomegaSoftware\Availability\Traits\HasAvailability;

class Practitioner extends Model
{
    use HasAvailability;
    
    public function createAppointmentSlot($startTime, $duration, $type)
    {
        $bufferBefore = $this->getBufferBefore($type);
        $bufferAfter = $this->getBufferAfter($type);
        
        $totalDuration = $bufferBefore + $duration + $bufferAfter;
        $actualStart = $startTime->copy()->subMinutes($bufferBefore);
        $actualEnd = $startTime->copy()->addMinutes($duration + $bufferAfter);
        
        // Create blackout for the entire period including buffers
        $this->availabilityRules()->create([
            'type' => 'blackout_date',
            'config' => [
                'dates' => [$actualStart->format('Y-m-d')],
                '_times' => [
                    'start' => $actualStart->format('H:i'),
                    'end' => $actualEnd->format('H:i'),
                ],
            ],
            'effect' => Effect::Deny,
            'priority' => 100, // High priority to override all other rules
        ]);
        
        return Appointment::create([
            'practitioner_id' => $this->id,
            'start_time' => $startTime,
            'duration' => $duration,
            'type' => $type,
            'buffer_before' => $bufferBefore,
            'buffer_after' => $bufferAfter,
        ]);
    }
    
    private function getBufferBefore($type)
    {
        return match($type) {
            'surgery' => 30,
            'consultation' => 10,
            'checkup' => 5,
            default => 15,
        };
    }
    
    private function getBufferAfter($type)
    {
        return match($type) {
            'surgery' => 45,
            'consultation' => 5,
            'checkup' => 5,
            default => 10,
        };
    }
}

// Custom evaluator for time-specific blackouts
class TimeSpecificBlackoutEvaluator implements RuleEvaluator
{
    public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
    {
        $dates = $config['dates'] ?? [];
        $times = $config['_times'] ?? null;
        
        if (!in_array($moment->format('Y-m-d'), $dates)) {
            return false;
        }
        
        if ($times) {
            $start = Carbon::parse($moment->format('Y-m-d') . ' ' . $times['start']);
            $end = Carbon::parse($moment->format('Y-m-d') . ' ' . $times['end']);
            return $moment->between($start, $end);
        }
        
        return true;
    }
}

Subscription-Based Access Control

Scenario

Content availability based on subscription tiers and limits.

namespace App\Services;

class SubscriptionAvailabilityService
{
    public function setupSubscriptionRules(User $user)
    {
        $subscription = $user->currentSubscription();
        
        switch ($subscription->tier) {
            case 'basic':
                $this->setupBasicTier($user);
                break;
            case 'professional':
                $this->setupProfessionalTier($user);
                break;
            case 'enterprise':
                $this->setupEnterpriseTier($user);
                break;
        }
    }
    
    private function setupBasicTier($user)
    {
        $user->availabilityRules()->createMany([
            // Business hours only
            [
                'type' => 'time_of_day',
                'config' => ['from' => '09:00', 'to' => '17:00'],
                'effect' => Effect::Allow,
                'priority' => 10,
            ],
            // Weekdays only
            [
                'type' => 'weekdays',
                'config' => ['days' => [1, 2, 3, 4, 5]],
                'effect' => Effect::Allow,
                'priority' => 20,
            ],
            // Usage quota
            [
                'type' => 'quota',
                'config' => ['type' => 'daily', 'limit' => 100],
                'effect' => Effect::Allow,
                'priority' => 30,
            ],
        ]);
    }
    
    private function setupProfessionalTier($user)
    {
        $user->availabilityRules()->createMany([
            // Extended hours
            [
                'type' => 'time_of_day',
                'config' => ['from' => '06:00', 'to' => '22:00'],
                'effect' => Effect::Allow,
                'priority' => 10,
            ],
            // All week
            [
                'type' => 'weekdays',
                'config' => ['days' => [1, 2, 3, 4, 5, 6, 7]],
                'effect' => Effect::Allow,
                'priority' => 20,
            ],
            // Higher quota
            [
                'type' => 'quota',
                'config' => ['type' => 'daily', 'limit' => 1000],
                'effect' => Effect::Allow,
                'priority' => 30,
            ],
        ]);
    }
    
    private function setupEnterpriseTier($user)
    {
        // Enterprise has no restrictions
        $user->availability_default = Effect::Allow;
        $user->save();
        
        // Only apply system maintenance windows
        $user->availabilityRules()->create([
            'type' => 'rrule',
            'config' => [
                'rrule' => 'FREQ=MONTHLY;BYMONTHDAY=1;BYHOUR=2,3',
            ],
            'effect' => Effect::Deny,
            'priority' => 100,
        ]);
    }
}

Dynamic Pricing with Availability

Scenario

Adjust availability based on demand and pricing tiers.

namespace App\Models;

class PricingAwareResource extends Model
{
    use HasAvailability;
    
    public function checkAvailabilityWithPricing($moment, $maxPrice = null)
    {
        $engine = app(AvailabilityEngine::class);
        
        // Base availability check
        if (!$engine->isAvailable($this, $moment)) {
            return ['available' => false, 'reason' => 'Not available at this time'];
        }
        
        // Calculate dynamic price
        $price = $this->calculatePrice($moment);
        
        // Check if price is within budget
        if ($maxPrice && $price > $maxPrice) {
            return [
                'available' => false,
                'reason' => 'Price exceeds budget',
                'current_price' => $price,
            ];
        }
        
        return [
            'available' => true,
            'price' => $price,
            'tier' => $this->getPriceTier($moment),
        ];
    }
    
    private function calculatePrice($moment)
    {
        $basePrice = $this->base_price;
        $multiplier = 1.0;
        
        // Peak hours (20% increase)
        if ($moment->hour >= 17 && $moment->hour <= 20) {
            $multiplier *= 1.2;
        }
        
        // Weekend (15% increase)
        if ($moment->isWeekend()) {
            $multiplier *= 1.15;
        }
        
        // High demand dates (30% increase)
        if ($this->isHighDemand($moment)) {
            $multiplier *= 1.3;
        }
        
        // Low demand (20% discount)
        if ($this->isLowDemand($moment)) {
            $multiplier *= 0.8;
        }
        
        return round($basePrice * $multiplier, 2);
    }
    
    private function isHighDemand($moment)
    {
        $bookingRate = $this->bookings()
            ->whereDate('date', $moment->format('Y-m-d'))
            ->count();
        
        return $bookingRate > $this->capacity * 0.8;
    }
    
    private function isLowDemand($moment)
    {
        $daysAhead = now()->diffInDays($moment);
        
        // Last minute and low bookings
        return $daysAhead <= 2 && 
               $this->bookings()->whereDate('date', $moment->format('Y-m-d'))->count() < 2;
    }
}

// Custom evaluator for price-based availability
class PriceThresholdEvaluator implements RuleEvaluator
{
    public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
    {
        $maxPrice = $config['max_price'] ?? PHP_INT_MAX;
        $minPrice = $config['min_price'] ?? 0;
        
        if (method_exists($subject, 'calculatePrice')) {
            $price = $subject->calculatePrice($moment);
            return $price >= $minPrice && $price <= $maxPrice;
        }
        
        return true;
    }
}

Cascading Availability Rules

Scenario

Hotel with rooms, amenities, and services with interdependent availability.

namespace App\Services;

class HotelAvailabilityService
{
    public function setupCascadingRules(Hotel $hotel)
    {
        // Hotel-level rules (affects all resources)
        $hotel->availabilityRules()->createMany([
            // Open year-round except Christmas
            [
                'type' => 'blackout_date',
                'config' => ['dates' => ['2025-12-25']],
                'effect' => Effect::Deny,
                'priority' => 100,
            ],
        ]);
        
        // Room-specific rules
        foreach ($hotel->rooms as $room) {
            $this->setupRoomRules($room, $hotel);
        }
        
        // Amenity rules dependent on hotel
        foreach ($hotel->amenities as $amenity) {
            $this->setupAmenityRules($amenity, $hotel);
        }
    }
    
    private function setupRoomRules($room, $hotel)
    {
        // Inherit hotel availability
        $room->availabilityRules()->create([
            'type' => 'parent_availability',
            'config' => ['parent_id' => $hotel->id, 'parent_type' => Hotel::class],
            'effect' => Effect::Allow,
            'priority' => 5,
        ]);
        
        // Room-specific maintenance
        if ($room->next_maintenance_date) {
            $room->availabilityRules()->create([
                'type' => 'date_range',
                'config' => [
                    'from' => $room->next_maintenance_date,
                    'to' => $room->next_maintenance_date->addDays(3)->format('Y-m-d'),
                    'kind' => 'absolute',
                ],
                'effect' => Effect::Deny,
                'priority' => 50,
            ]);
        }
    }
    
    private function setupAmenityRules($amenity, $hotel)
    {
        switch ($amenity->type) {
            case 'pool':
                $amenity->availabilityRules()->createMany([
                    // Seasonal
                    [
                        'type' => 'months_of_year',
                        'config' => ['months' => [5, 6, 7, 8, 9]],
                        'effect' => Effect::Allow,
                        'priority' => 10,
                    ],
                    // Daily hours
                    [
                        'type' => 'time_of_day',
                        'config' => ['from' => '06:00', 'to' => '22:00'],
                        'effect' => Effect::Allow,
                        'priority' => 20,
                    ],
                ]);
                break;
                
            case 'restaurant':
                $amenity->availabilityRules()->createMany([
                    // Breakfast
                    [
                        'type' => 'time_of_day',
                        'config' => ['from' => '06:30', 'to' => '10:30'],
                        'effect' => Effect::Allow,
                        'priority' => 10,
                    ],
                    // Dinner
                    [
                        'type' => 'time_of_day',
                        'config' => ['from' => '18:00', 'to' => '22:00'],
                        'effect' => Effect::Allow,
                        'priority' => 15,
                    ],
                ]);
                break;
        }
    }
}

// Custom evaluator for parent availability
class ParentAvailabilityEvaluator implements RuleEvaluator
{
    private AvailabilityEngine $engine;
    
    public function __construct(AvailabilityEngine $engine)
    {
        $this->engine = $engine;
    }
    
    public function matches(array $config, CarbonInterface $moment, AvailabilitySubject $subject): bool
    {
        $parentType = $config['parent_type'] ?? null;
        $parentId = $config['parent_id'] ?? null;
        
        if (!$parentType || !$parentId) {
            return true;
        }
        
        $parent = $parentType::find($parentId);
        if (!$parent) {
            return true;
        }
        
        return $this->engine->isAvailable($parent, $moment);
    }
}

Performance Optimization Example

Scenario

High-traffic availability checking with caching.

namespace App\Services;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;

class OptimizedAvailabilityService
{
    private AvailabilityEngine $engine;
    
    public function checkAvailabilityOptimized($resource, $moment)
    {
        // Level 1: In-memory cache
        $memoryKey = "availability.{$resource->id}.{$moment->timestamp}";
        if (isset($this->memoryCache[$memoryKey])) {
            return $this->memoryCache[$memoryKey];
        }
        
        // Level 2: Redis cache
        $cacheKey = $this->getCacheKey($resource, $moment);
        $cached = Cache::get($cacheKey);
        if ($cached !== null) {
            $this->memoryCache[$memoryKey] = $cached;
            return $cached;
        }
        
        // Level 3: Check if rules have changed
        $rulesHash = $this->getRulesHash($resource);
        $lastHash = Cache::get("rules_hash.{$resource->id}");
        
        if ($rulesHash !== $lastHash) {
            // Rules changed, invalidate cache
            $this->invalidateResourceCache($resource);
            Cache::put("rules_hash.{$resource->id}", $rulesHash, 3600);
        }
        
        // Actual availability check
        $result = $this->engine->isAvailable($resource, $moment);
        
        // Cache result
        $this->cacheResult($resource, $moment, $result);
        
        return $result;
    }
    
    private function getCacheKey($resource, $moment)
    {
        // Round to nearest 5 minutes for better cache hit rate
        $rounded = $moment->copy()->minute(floor($moment->minute / 5) * 5)->second(0);
        return "avail:{$resource->getMorphClass()}:{$resource->id}:{$rounded->timestamp}";
    }
    
    private function getRulesHash($resource)
    {
        $rules = $resource->availabilityRules()
            ->where('enabled', true)
            ->orderBy('priority')
            ->get(['type', 'config', 'effect', 'priority']);
        
        return md5(json_encode($rules->toArray()));
    }
    
    private function cacheResult($resource, $moment, $result)
    {
        $key = $this->getCacheKey($resource, $moment);
        
        // Different TTL based on how far in future
        $daysAhead = now()->diffInDays($moment);
        $ttl = match(true) {
            $daysAhead <= 1 => 300,    // 5 minutes for today/tomorrow
            $daysAhead <= 7 => 1800,   // 30 minutes for this week
            $daysAhead <= 30 => 3600,  // 1 hour for this month
            default => 7200,            // 2 hours for far future
        };
        
        Cache::put($key, $result, $ttl);
    }
    
    public function batchCheckAvailability(array $resources, $moment)
    {
        // Use pipeline for Redis operations
        $results = [];
        $uncached = [];
        
        Redis::pipeline(function ($pipe) use ($resources, $moment, &$results, &$uncached) {
            foreach ($resources as $resource) {
                $key = $this->getCacheKey($resource, $moment);
                $pipe->get($key);
            }
        });
        
        // Process uncached resources in parallel
        $chunks = array_chunk($uncached, 10);
        $promises = [];
        
        foreach ($chunks as $chunk) {
            $promises[] = async(fn() => $this->processChunk($chunk, $moment));
        }
        
        $chunkResults = await($promises);
        
        return array_merge($results, ...$chunkResults);
    }
}

Next Steps

Getting Started

Installation
Set up the package in your Laravel app

Quick Start
Get running in 5 minutes

Basic Usage
Common patterns and examples


Core Concepts

How It Works
Understanding the evaluation engine

Rule Types
Available rule types and configurations

Priority System
How rule priority affects evaluation


Advanced Topics

Inventory Gates
Dynamic availability based on stock

Custom Evaluators
Build your own rule types

Complex Scenarios
Real-world implementation patterns

Performance Tips
Optimization strategies


API Reference

Configuration
Package configuration options

Models & Traits
Available models and traits

Testing
Testing your availability rules

Clone this wiki locally