diff --git a/docs/formula-and-modifiers.md b/docs/formula-and-modifiers.md index 05aa1eae..fd6c7970 100644 --- a/docs/formula-and-modifiers.md +++ b/docs/formula-and-modifiers.md @@ -71,9 +71,10 @@ discount.since('08.2018').grows('1%').every('month').min('10 USD') discount.since('08.2000').grows('30%').every('year').max('100%') discount.since('08.2018').grows('20 USD').every('2 months').min('15 USD').max('80 USD') discount.since('08.2018').till('12.2018').grows('10pp').every('month') +discount.since('08.2018').grows('10 USD').every('month').stopsGrowing('10.2018') ``` -Supports absolute (`USD`), relative (`%`), and percentage point (`pp`) steps. `min`/`max` cap the accumulated discount. +Supports absolute (`USD`), relative (`%`), and percentage point (`pp`) steps. `min`/`max` cap the accumulated discount. `stopsGrowing()` caps the growth calculation at the given month while the discount continues to apply. ### Increase @@ -159,6 +160,7 @@ Modifiers use a composable addon system for configuration: | `MonthPeriod` / `YearPeriod` / `DayPeriod` | Time periods for `every()` and `lasts()` | | `Discount` | Discount value (absolute, relative %, or percentage point) | | `Step` | Growth step for GrowingDiscount | +| `StopsGrowing` | Last month when GrowingDiscount grows | | `Reason` | Human-readable comment attached to modifier charges | | `Minimum` / `Maximum` | Bounds for accumulated discount values | diff --git a/src/charge/modifiers/GrowingDiscount.php b/src/charge/modifiers/GrowingDiscount.php index 6607e897..3ee71824 100644 --- a/src/charge/modifiers/GrowingDiscount.php +++ b/src/charge/modifiers/GrowingDiscount.php @@ -19,6 +19,7 @@ use hiqdev\php\billing\charge\modifiers\addons\Period; use hiqdev\php\billing\charge\modifiers\addons\Since; use hiqdev\php\billing\charge\modifiers\addons\Step; +use hiqdev\php\billing\charge\modifiers\addons\StopsGrowing; use Money\Money; /** @@ -32,6 +33,7 @@ class GrowingDiscount extends FixedDiscount public const STEP = 'step'; public const MIN = 'min'; public const MAX = 'max'; + public const STOPS_GROWING = 'stopsGrowing'; public function __construct($step, $min = null, array $addons = []) { @@ -90,6 +92,16 @@ public function every($string = 1) return $this->addAddon(self::PERIOD, Period::fromString($string)); } + public function stopsGrowing($month) + { + return $this->addAddon(self::STOPS_GROWING, new StopsGrowing($month)); + } + + public function getStopsGrowing(): ?StopsGrowing + { + return $this->getAddon(self::STOPS_GROWING); + } + #[\Override] public function calculateSum(?ChargeInterface $charge = null): Money { @@ -126,6 +138,15 @@ protected function countPeriodsPassed(DateTimeImmutable $time) throw new \Exception('no period given for growing discount'); } - return $period->countPeriodsPassed($since->getValue(), $time); + $sinceTime = $since->getValue(); + $stopsGrowing = $this->getStopsGrowing(); + if ($stopsGrowing instanceof StopsGrowing && $stopsGrowing->getValue() < $time) { + $time = $stopsGrowing->getValue(); + } + if ($time < $sinceTime) { + $time = $sinceTime; + } + + return $period->countPeriodsPassed($sinceTime, $time); } } diff --git a/src/charge/modifiers/addons/StopsGrowing.php b/src/charge/modifiers/addons/StopsGrowing.php new file mode 100644 index 00000000..edeeb0e7 --- /dev/null +++ b/src/charge/modifiers/addons/StopsGrowing.php @@ -0,0 +1,25 @@ + + */ +class StopsGrowing extends Date implements AddonInterface +{ +} diff --git a/tests/behat/GrowingDiscount.feature b/tests/behat/GrowingDiscount.feature index 90593bab..2eca23a2 100644 --- a/tests/behat/GrowingDiscount.feature +++ b/tests/behat/GrowingDiscount.feature @@ -73,6 +73,40 @@ Feature: Growing discount | 2019-01-01 | monthly 100 USD | discount -80 USD | | 2028-11-01 | monthly 100 USD | discount -80 USD | + Scenario Outline: absolute discount stops growing but keeps applying + Given formula is discount.since('08.2018').grows('20 USD').every('2 months').min('15 USD').max('80 USD').stopsGrowing('10.2018') + When action date is + Then first charge is + And second charge is + Examples: + | date | first | second | + | 2018-07-31 | monthly 100 USD | | + | 2018-08-01 | monthly 100 USD | discount -30 USD | + | 2018-09-01 | monthly 100 USD | discount -30 USD | + | 2018-10-01 | monthly 100 USD | discount -70 USD | + | 2018-11-01 | monthly 100 USD | discount -70 USD | + | 2018-12-01 | monthly 100 USD | discount -70 USD | + | 2019-01-01 | monthly 100 USD | discount -70 USD | + | 2028-11-01 | monthly 100 USD | discount -70 USD | + + Scenario Outline: relative discount stops growing but keeps applying + Given formula is discount.since('08.2000').grows('30%').every('year').max('100%').stopsGrowing('08.2003') + When action date is + Then first charge is + And second charge is + Examples: + | date | first | second | + | 2000-07-31 | monthly 100 USD | | + | 2000-08-01 | monthly 100 USD | discount -30.00 USD | + | 2001-08-01 | monthly 100 USD | discount -51.00 USD | + | 2002-08-01 | monthly 100 USD | discount -65.70 USD | + | 2003-01-01 | monthly 100 USD | discount -65.70 USD | + | 2003-08-01 | monthly 100 USD | discount -75.99 USD | + | 2004-08-01 | monthly 100 USD | discount -75.99 USD | + | 2005-08-01 | monthly 100 USD | discount -75.99 USD | + | 2011-08-01 | monthly 100 USD | discount -75.99 USD | + | 2024-08-01 | monthly 100 USD | discount -75.99 USD | + Scenario Outline: discount maximum may not match step: relative step but absolute max Given formula is discount.since('08.2018').grows('20pp').every('month').min('30%').max('77 USD') When action date is