-
Notifications
You must be signed in to change notification settings - Fork 0
Inventory Gates
Dynamic availability based on inventory, capacity, or other real-time conditions.
Inventory gates allow availability to depend on dynamic factors like:
- Stock levels
- Booking capacity
- Resource allocation
- API responses
- Database queries
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);
}]);
}$product->availabilityRules()->create([
'type' => 'inventory_gate',
'config' => ['min' => 1], // Minimum stock required
'effect' => Effect::Allow,
'priority' => 30,
]);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
}]);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'];
}]);// 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,
]);// 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,
]);// 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,
],
]);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;
},
]]);
}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);
});
}]);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;
}
}]);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);
}]);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);
}]);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()));
}// 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
}]);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);
});
}]);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'];
}]);- Custom Evaluators - Build completely custom rule types
- Complex Scenarios - Real-world implementations
- Performance Tips - Optimization strategies
Romega Software is software development agency specializing in helping customers integrate AI and custom software into their business, helping companies in growth mode better acquire, visualize, and utilize their data, and helping entrepreneurs bring their ideas to life.
Installation
Set up the package in your Laravel app
Quick Start
Get running in 5 minutes
Basic Usage
Common patterns and examples
How It Works
Understanding the evaluation engine
Rule Types
Available rule types and configurations
Priority System
How rule priority affects evaluation
Inventory Gates
Dynamic availability based on stock
Custom Evaluators
Build your own rule types
Complex Scenarios
Real-world implementation patterns
Performance Tips
Optimization strategies
Configuration
Package configuration options
Models & Traits
Available models and traits
Testing
Testing your availability rules