SIGNALS Documentation
API Reference

Cost Apportionment Registry

Contract for registering strategies that allocate virtual-stock costs across rentals.

Overview

App\Services\Shortages\CostApportionmentRegistry stores the strategies used to allocate a virtual-stock intake's cost across participating rentals.

Core binds the registry as a singleton in AppServiceProvider. ApportionVirtualStockCost selects one registered strategy, calculates the desired allocation, and synchronises the resulting rental costs.

Public surface

Method Purpose
register() Store one strategy under its key()
get() Return the strategy for a key or throw
has() Check whether a strategy key is registered
all() Return the complete keyed strategy map

Accepted contract and identity

Every registration implements App\Contracts\CostApportionmentStrategyContract:

  • key(): string supplies the registry key
  • name(): string supplies a human-readable label
  • requiresManualInput(): bool identifies strategies that need explicit allocations
  • calculate(mixed $intake): array returns an rental_id => cost in minor units map

The five allocating strategies currently require an App\Services\Shortages\CostApportionmentContext. Passing a different value to them throws InvalidArgumentException. The none strategy is the no-op exception: it accepts the loose contract input and returns [].

Collisions, lookup, and ordering

The strategy's key() is its identity. register() assigns directly into an associative map, so registering the same key again replaces the previous strategy; duplicate registration is not rejected.

get($key) throws InvalidArgumentException with Unknown cost-apportionment strategy: {key} when the key is absent. Use has() before an optional lookup.

The registry does not define priorities or sort strategies. all() returns the keyed map in its stored order, but runtime calculation selects one strategy by key, so catalogue order does not control allocation.

Current core registrations

The complete built-in set is:

Key Label Current behavior
none No apportionment Returns no allocations
primary_job Primary job Assigns the full cost to the primary rental
even_split Even split Splits cost equally between participating rentals
proportional_quantity Proportional by quantity Weights allocation by each rental's quantity
proportional_quantity_duration Proportional by quantity × duration Weights allocation by quantity and overlap days
manual Manual Uses stored per-rental unit-cost overrides

The non-manual proportional strategies use deterministic minor-unit apportionment. Rental IDs provide stable tie-breaking for remainder pennies. manual is the only built-in for which requiresManualInput() returns true; it reads VirtualStockCostOverride records and missing overrides allocate zero.

The interface itself does not prohibit database access or other side effects, so purity must not be inferred as a registry-wide guarantee.

Runtime selection and failure behavior

ApportionVirtualStockCost resolves the strategy key in this order:

  1. a non-empty explicit key, when that key is registered
  2. the intake's cost_apportionment_strategy value
  3. the shortages.cost_apportionment_strategy setting, falling back to configuration and then primary_job

Only the explicit override is guarded with has(). When the action proceeds to calculation, an unknown persisted, setting, or configuration key reaches get() and throws the registry's unknown-strategy exception. An ineligible intake returns before strategy lookup, so that early-return path does not throw for an unknown key.

The action calculates allocations only for confirmed or received intakes with a positive quantity and a non-negative unit cost. It records a warning when a manual allocation does not equal the intake total; that mismatch does not block synchronisation.

VirtualStockIntakeProfitAndLossBuilder also uses the registry to turn a known strategy key into its display name.

Worked example

This is the current core consumption shape after ApportionVirtualStockCost has built its context:

$strategy = app(CostApportionmentRegistry::class)->get('primary_job');
$allocations = $strategy->calculate($context);

The result is a map of rental IDs to integer minor-unit costs. The action then diffs that map against existing intake-linked costs so repeating the operation converges on the same persisted allocations.