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(): stringsupplies the registry keyname(): stringsupplies a human-readable labelrequiresManualInput(): boolidentifies strategies that need explicit allocationscalculate(mixed $intake): arrayreturns anrental_id => cost in minor unitsmap
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:
- a non-empty explicit key, when that key is registered
- the intake's
cost_apportionment_strategyvalue - the
shortages.cost_apportionment_strategysetting, falling back to configuration and thenprimary_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.