Shortage Resolver Registry
Contract for registering shortage resolver definitions and resolving applicable resolver implementations.
Overview
App\Services\Shortages\ShortageResolverRegistry is the composition root for the shortage-resolution extension seam.
Core binds the registry as a singleton in AppServiceProvider and registers the built-in resolver definitions during boot. The registry stores ShortageResolverDefinition value objects, resolves resolver implementations through Laravel's container, and presents them in priority order for both single-shortage and batch-shortage flows.
Public surface
| Method | Purpose |
|---|---|
register() |
Store one ShortageResolverDefinition under its key |
get() |
Return one definition by key or throw |
definitions() |
Return the complete keyed definition map |
has() |
Check whether a resolver key is registered |
resolve() |
Resolve one ShortageResolverContract implementation from the flightcase |
all() |
Resolve every registered resolver and sort them by ascending priority() |
applicableTo() |
Return resolvers whose canResolve() matches one Shortage |
batchApplicableTo() |
Return batch-capable resolvers applicable to one ShortageGroup |
Accepted definition and resolver contracts
The accepted registration value is App\Services\Shortages\ShortageResolverDefinition:
new ShortageResolverDefinition(
key: 'subhire',
resolverClass: SubHireResolver::class,
);
key is the stable registry identity. resolverClass is expected to be a container-resolvable class-string for an App\Contracts\ShortageResolverContract implementation.
That resolver contract requires:
key(): stringname(): stringpriority(): intisAutoExecutable(): boolcanResolve(Shortage $shortage): boolgetOptions(Shortage $shortage): arrayapply(Shortage $shortage, ResolutionOption $option): ResolutionResult
Resolvers that can consolidate multiple shortages also implement App\Contracts\BatchShortageResolverContract, adding supportsBatch(), canResolveBatch(), getBatchOptions(), and applyBatch().
Identity, collisions, and construction
register() stores definitions in an associative map keyed by the definition's key. Registering another definition with the same key replaces the earlier definition; duplicate keys are not rejected.
resolve() and all() do not store resolver instances. They use Laravel's container each time they need an implementation, so resolver dependencies stay injectable and test doubles can replace concrete classes through the container.
The registry does not validate that a definition key matches the resolved class's own key() return value, and it does not preflight the class at registration time. A registration is therefore responsible for supplying a container-resolvable class that really implements ShortageResolverContract.
Failure behaviour
get($key) throws InvalidArgumentException with Unknown shortage resolver: {key} when the key is missing. resolve($key) delegates to get(), so it shares the same unknown-key failure.
If a definition exists but its class cannot be built by the container, that failure bubbles from Laravel's container rather than from custom registry logic.
Write-path actions add their own validation around the registry:
ApplyShortageResolutionrejects unknown keys, non-applicable resolvers, and out-of-range option indicesApplyBatchShortageResolutionrejects unknown keys, resolvers that do not implementBatchShortageResolverContract, non-applicable batch resolvers, and out-of-range batch option indices
Current core registrations
The built-in definitions are:
| Key | Name | Priority | Batch-capable |
|---|---|---|---|
reallocate |
Reallocate from quote | 10 |
No |
substitute |
Substitute catalogue item | 20 |
No |
transfer |
Warehouse transfer | 30 |
No |
date_shift |
Shift dates | 40 |
No |
partial |
Partial fulfilment | 50 |
No |
subhire |
Sub-hire | 60 |
Yes |
waitlist |
Waitlist | 90 |
No |
Today subhire is the only built-in resolver that implements BatchShortageResolverContract. The others participate only in single-item shortage flows.
Priority ordering and single-item applicability
all() resolves every registered implementation and sorts the resulting list by ascending priority(). Equal-priority resolvers retain registration order. That sorted list is the source catalogue the shortage UI and API consume.
applicableTo() then filters that ordered list by canResolve($shortage). A resolver stays in priority order only if it declares itself applicable to the specific Shortage being evaluated.
That means the current contract is:
- lower
priority()values run first - registration order breaks equal-priority ties
- applicability is determined by each resolver's
canResolve()logic - the registry does not special-case any built-in key
Batch applicability
batchApplicableTo() is stricter than applicableTo(). It only returns resolvers that:
- appear in the priority-sorted
all()list - implement
BatchShortageResolverContract - return
truefromsupportsBatch() - return
truefromcanResolveBatch($group)
The current batch API and action layers use that contract to expose only legitimate batch options for a ShortageGroup. A resolver that can resolve single shortages but does not implement the batch contract is invisible to batch flows.
Automatic resolution
ShortageAutoResolver consumes the registry during the write-time rental.convert_to_order guard. ShortageConfirmationRule::evaluate() runs it before ShortageConfirmationGate, so the gate evaluates only the residual shortage. The dry-run precheck() path does not auto-resolve anything.
The warehouse's shortage_preferred_resolvers setting controls resolver selection:
- a non-empty configured list is resolved in its configured order, skipping unknown keys
- an empty list falls back to all registered resolvers in priority order
- a non-empty list containing only unknown keys produces no resolver attempts; it does not trigger the empty-list fallback
Auto-resolution is a no-op unless the warehouse enables it. For each unresolved item shortage, the loop requires both isAutoExecutable() and canResolve() to return true, selects the first option whose autoExecutable flag is true, and calls apply(). It then re-detects the item's shortage after each execution so the next resolver sees the remaining shortfall, and stops once the item is fully covered. The current built-ins are not auto-executable under their default configuration.
Runtime consumers
Current core consumers use the registry in four main ways:
ShortageController::resolvers()callsapplicableTo()and thengetOptions()for the current line-item shortageShortageController::batchResolvers()callsbatchApplicableTo()and thengetBatchOptions()for each eligible groupApplyShortageResolutionandApplyBatchShortageResolutionvalidate the requested key, resolve the implementation through the registry, and then execute the chosen optionShortageAutoResolverfollows the warehouse's configured order or the priority fallback before automatically applying eligible options
Because applicableTo() and batchApplicableTo() resolve implementations from the container, those consumers always work with the current concrete resolver objects rather than with stored metadata alone.
Worked example
This is the single-shortage selection path:
$resolvers = array_map(
static fn ($resolver): array => [
'resolver_key' => $resolver->key(),
'name' => $resolver->name(),
'priority' => $resolver->priority(),
'options' => array_map(
static fn ($option): array => $option->toArray(),
$resolver->getOptions($shortage),
),
],
$registry->applicableTo($shortage),
);
The registry first resolves and sorts every registered resolver, then keeps only the ones whose canResolve() matches the current shortage. The API response therefore reflects both the registry catalogue and the runtime applicability logic of each resolver implementation.