Discount Source Registry
Contract for registering sources of candidate item discount percentages.
Overview
App\Services\Pricing\DiscountSourceRegistry stores providers of candidate discount percentages for rental items.
Core binds the registry as a singleton in AppServiceProvider, but currently ships with no discount sources. RentalTotalsCalculator consumes every registered source when it resolves an item's effective discount.
Public surface
| Method | Purpose |
|---|---|
register() |
Store one source under its key() |
get() |
Return a source for a key or throw |
has() |
Check whether a source key is registered |
all() |
Return the complete keyed source map |
Accepted contract and identity
Every registration implements App\Contracts\Pricing\DiscountSource:
public function key(): string;
public function discountPercentForItem(
RentalItem $item,
Rental $rental,
CarbonInterface $at,
): ?string;
The source's key() is its registry identity. discountPercentForItem() returns a decimal percentage string when the source supplies a candidate, or null when it does not apply to that item at the supplied time.
The contract does not prescribe a percentage range, declare sources pure, or provide a priority. Registrations are responsible for returning values suitable for the calculator's decimal comparison.
Collisions and lookup failures
register() assigns directly into an associative map. A later source with the same key replaces the earlier object; duplicate registration is not rejected.
get($key) throws InvalidArgumentException with Unknown discount source: {key} for an unregistered key. Use has() before an optional direct lookup.
The normal pricing consumer does not call get(): it evaluates every source returned by all().
Core registrations and ordering
The framework currently ships with no discount sources. The core registry is therefore empty until an extension registers one.
all() is not sorted and the registry has no priority API. RentalTotalsCalculator considers the item's explicit percentage first, then iterates the warehoused sources in registry order.
The calculator replaces the current winner only when bccomp($candidate, $best, 2) returns a value greater than zero. The highest percentage therefore wins at scale two, but candidates that compare equal at scale two do not replace one another: the first candidate wins. Selection is not completely order-independent. The explicit item percentage wins a scale-two tie with any source, and equal source candidates are decided by registry order. Call order can also matter to implementations with side effects; the contract makes no side-effect guarantee.
Runtime consumption
For each rental item, RentalTotalsCalculator builds candidate percentages from:
- the item's explicit
discount_percent, when present - every non-null value returned by registered
DiscountSourceimplementations
The calculator compares candidates with bccomp(..., 2) and keeps the first greatest value at that scale. It then passes that winning value through RentalItemDiscountPercentFilterRegistry before applying the percentage to the line net.
The timestamp supplied to each source is the item's start, falling back to the rental start and then the current time. A source can use the item, owning rental, and timestamp to determine applicability.
Worked example
This is the current registry consumption shape inside RentalTotalsCalculator:
foreach (app(DiscountSourceRegistry::class)->all() as $source) {
$percent = $source->discountPercentForItem($item, $rental, $at);
if ($percent !== null) {
$candidates[] = $percent;
}
}
With the core empty registry, only the item's explicit percentage can enter from this stage. Once a source is registered, its non-null value competes with every other candidate and the highest percentage wins.