Skip to content

Inventory Gates

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

Inventory Gates

Dynamic availability based on inventory, capacity, or other real-time conditions.

Overview

Inventory gates allow availability to depend on dynamic factors like:

  • Stock levels
  • Booking capacity
  • Resource allocation
  • API responses
  • Database queries

Basic Setup

1. Configure a Resolver

The resolver is a function that determines availability based on current conditions:

// In AppServiceProvider or a dedicated provider
use RomegaSoftware\Availability\Contracts\AvailabilitySubject;
use Carbon\CarbonInterface;

public function boot()
{
    config(['availability.inventory_gate.resolver' => function (
        AvailabilitySubject $subject,
        CarbonInterface $moment,
        array $ruleConfig
    ) {
        // Return a number (compared against min)
        return $subject->getStockLevel();
        
        // Or return a boolean directly
        // return $subject->hasCapacity($moment);
    }]);
}

2. Create Inventory Rules

$product->availabilityRules()->create([
    'type' => 'inventory_gate',
    'config' => ['min' => 1], // Minimum stock required
    'effect' => Effect::Allow,
    'priority' => 30,
]);

Resolver Return Values

Numeric Returns

When returning a number, it's compared against the min config value:

config(['availability.inventory_gate.resolver' => function ($subject, $moment, $config) {
    $stock = $subject->stock_quantity;
    return $stock; // If >= $config['min'], rule matches
}]);

Boolean Returns

Return true or false directly for custom logic:

config(['availability.inventory_gate.resolver' => function ($subject, $moment, $config) {
    // Complex logic
    if ($subject->isUnderMaintenance($moment)) {
        return false;
    }
    
    if ($subject->hasSpecialReservation($moment)) {
        return false;
    }
    
    return $subject->getCapacity() >= $config['min'];
}]);

Real-World Examples

E-Commerce Product

// Resolver
config(['availability.inventory_gate.resolver' => function ($product, $moment, $config) {
    // Check real-time stock
    $stock = $product->inventory()
        ->where('warehouse_id', $config['warehouse_id'] ?? null)
        ->sum('quantity');
    
    // Account for pending orders
    $pending = $product->orderItems()
        ->whereIn('status', ['pending', 'processing'])
        ->sum('quantity');
    
    return $stock - $pending;
}]);

// Rule
$product->availabilityRules()->create([
    'type' => 'inventory_gate',
    'config' => [
        'min' => 1,
        'warehouse_id' => 5, // Optional warehouse filter
    ],
    'effect' => Effect::Allow,
    'priority' => 50,
]);

Hotel Room Booking

// Resolver
config(['availability.inventory_gate.resolver' => function ($room, $moment, $config) {
    // Check if room is booked for this date
    $isBooked = $room->bookings()
        ->where('check_in', '<=', $moment)
        ->where('check_out', '>', $moment)
        ->where('status', '!=', 'cancelled')
        ->exists();
    
    return !$isBooked;
}]);

// Rule
$room->availabilityRules()->create([
    'type' => 'inventory_gate',
    'config' => ['min' => 1], // Just needs to return true
    'effect' => Effect::Allow,
    'priority' => 40,
]);

Event Venue Capacity

// Resolver
config(['availability.inventory_gate.resolver' => function ($venue, $moment, $config) {
    $date = $moment->format('Y-m-d');
    
    // Get total capacity
    $totalCapacity = $venue->capacity;
    
    // Get current bookings for this date
    $bookedCapacity = $venue->bookings()
        ->whereDate('event_date', $date)
        ->sum('attendees');
    
    // Return available capacity
    return $totalCapacity - $bookedCapacity;
}]);

// Rules for different event types
$venue->availabilityRules()->createMany([
    [
        'type' => 'inventory_gate',
        'config' => [
            'min' => 50, // Small events need 50+ capacity
            '_label' => 'small_event',
        ],
        'effect' => Effect::Allow,
        'priority' => 30,
    ],
    [
        'type' => 'inventory_gate',
        'config' => [
            'min' => 200, // Large events need 200+ capacity
            '_label' => 'large_event',
        ],
        'effect' => Effect::Allow,
        'priority' => 35,
    ],
]);

Per-Model Resolvers

Different models can have different resolver logic:

// In AppServiceProvider
public function boot()
{
    config(['availability.inventory_gate.resolvers' => [
        // Products check stock
        Product::class => function($product, $moment, $config) {
            return $product->stock_on_hand;
        },
        
        // Rooms check bookings
        Room::class => function($room, $moment, $config) {
            return !$room->isBookedAt($moment);
        },
        
        // Services check capacity
        Service::class => function($service, $moment, $config) {
            $maxCapacity = $service->max_concurrent_users;
            $currentUsers = $service->getActiveUsersAt($moment);
            return $maxCapacity - $currentUsers;
        },
    ]]);
}

Advanced Patterns

Caching Resolver Results

config(['availability.inventory_gate.resolver' => function ($subject, $moment, $config) {
    $cacheKey = "inventory.{$subject->id}.{$moment->timestamp}";
    
    return Cache::remember($cacheKey, 60, function () use ($subject, $moment) {
        // Expensive calculation
        return $subject->calculateAvailableInventory($moment);
    });
}]);

External API Integration

config(['availability.inventory_gate.resolver' => function ($subject, $moment, $config) {
    try {
        $response = Http::timeout(5)->get('https://api.inventory.com/check', [
            'sku' => $subject->sku,
            'date' => $moment->format('Y-m-d'),
        ]);
        
        return $response->json('available_quantity', 0);
    } catch (\Exception $e) {
        // Fallback to local data
        Log::error('Inventory API failed', ['error' => $e->getMessage()]);
        return $subject->local_stock ?? 0;
    }
}]);

Multi-Factor Availability

config(['availability.inventory_gate.resolver' => function ($resource, $moment, $config) {
    $checks = [
        'has_stock' => $resource->stock >= ($config['min'] ?? 1),
        'is_not_maintenance' => !$resource->isUnderMaintenance($moment),
        'has_staff' => $resource->hasAvailableStaff($moment),
        'within_budget' => $resource->price <= ($config['max_price'] ?? PHP_INT_MAX),
    ];
    
    // All checks must pass
    return !in_array(false, $checks, true);
}]);

Predictive Inventory

config(['availability.inventory_gate.resolver' => function ($product, $moment, $config) {
    $currentStock = $product->current_stock;
    
    // Calculate days until the moment
    $daysUntil = now()->diffInDays($moment);
    
    // Predict stock based on average daily sales
    $avgDailySales = $product->sales()
        ->where('created_at', '>=', now()->subDays(30))
        ->sum('quantity') / 30;
    
    $predictedStock = $currentStock - ($avgDailySales * $daysUntil);
    
    return max(0, $predictedStock);
}]);

Testing Inventory Gates

use Mockery;

public function test_inventory_gate_with_sufficient_stock()
{
    config(['availability.inventory_gate.resolver' => function ($subject) {
        return 10; // Mock inventory of 10
    }]);
    
    $product = Product::factory()->create([
        'availability_default' => Effect::Deny,
    ]);
    
    $product->availabilityRules()->create([
        'type' => 'inventory_gate',
        'config' => ['min' => 5],
        'effect' => Effect::Allow,
        'priority' => 10,
    ]);
    
    $engine = app(AvailabilityEngine::class);
    
    // Should be available (10 >= 5)
    $this->assertTrue($engine->isAvailable($product, now()));
}

public function test_inventory_gate_with_insufficient_stock()
{
    config(['availability.inventory_gate.resolver' => function ($subject) {
        return 3; // Mock inventory of 3
    }]);
    
    $product = Product::factory()->create([
        'availability_default' => Effect::Deny,
    ]);
    
    $product->availabilityRules()->create([
        'type' => 'inventory_gate',
        'config' => ['min' => 5],
        'effect' => Effect::Allow,
        'priority' => 10,
    ]);
    
    $engine = app(AvailabilityEngine::class);
    
    // Should not be available (3 < 5)
    $this->assertFalse($engine->isAvailable($product, now()));
}

Performance Considerations

1. Optimize Queries

// Bad: N+1 query problem
config(['availability.inventory_gate.resolver' => function ($product) {
    return $product->warehouses->sum('stock'); // Loads all warehouses
}]);

// Good: Single query
config(['availability.inventory_gate.resolver' => function ($product) {
    return $product->warehouses()->sum('stock'); // Query builder
}]);

2. Cache Expensive Operations

config(['availability.inventory_gate.resolver' => function ($subject, $moment) {
    return Cache::tags(['inventory', "subject-{$subject->id}"])
        ->remember("availability-{$moment->timestamp}", 300, function () use ($subject, $moment) {
            // Expensive calculation here
            return $subject->calculateComplexAvailability($moment);
        });
}]);

3. Fail Fast

config(['availability.inventory_gate.resolver' => function ($subject, $moment, $config) {
    // Quick checks first
    if ($subject->is_discontinued) {
        return false;
    }
    
    if ($subject->stock === 0) {
        return false;
    }
    
    // Then expensive checks
    return $subject->calculateNetAvailability($moment) >= $config['min'];
}]);

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