SIGNALS Documentation
API Reference

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:

  1. the item's explicit discount_percent, when present
  2. every non-null value returned by registered DiscountSource implementations

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.