SIGNALS Documentation
API Reference

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(): string
  • name(): string
  • priority(): int
  • isAutoExecutable(): bool
  • canResolve(Shortage $shortage): bool
  • getOptions(Shortage $shortage): array
  • apply(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:

  • ApplyShortageResolution rejects unknown keys, non-applicable resolvers, and out-of-range option indices
  • ApplyBatchShortageResolution rejects unknown keys, resolvers that do not implement BatchShortageResolverContract, 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:

  1. lower priority() values run first
  2. registration order breaks equal-priority ties
  3. applicability is determined by each resolver's canResolve() logic
  4. the registry does not special-case any built-in key

Batch applicability

batchApplicableTo() is stricter than applicableTo(). It only returns resolvers that:

  1. appear in the priority-sorted all() list
  2. implement BatchShortageResolverContract
  3. return true from supportsBatch()
  4. return true from canResolveBatch($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:

  1. a non-empty configured list is resolved in its configured order, skipping unknown keys
  2. an empty list falls back to all registered resolvers in priority order
  3. 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:

  1. ShortageController::resolvers() calls applicableTo() and then getOptions() for the current line-item shortage
  2. ShortageController::batchResolvers() calls batchApplicableTo() and then getBatchOptions() for each eligible group
  3. ApplyShortageResolution and ApplyBatchShortageResolution validate the requested key, resolve the implementation through the registry, and then execute the chosen option
  4. ShortageAutoResolver follows 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.