Skip to content

Commit fed4c2f

Browse files
committed
HP-2769: implement progressive price rendering and unit tests (#575)
* HP-2769: implement progressive price rendering and unit tests - Added `RepresentablePrice` interface for price representation. - Updated `ProgressivePricePresenter` to handle progressive pricing logic. - Added unit tests for `ProgressivePricePresenter`. - Improved `Threshold` and `Price` models to support new pricing structure. * HP-2769: update `ProgressivePricePresenter` to include 'Custom' label in price rendering and enhance unit tests * HP-2769: enhance progressive price handling framework - Introduced `ProgressivePresenter` widget for price progressive rendering. - Updated `ProgressivePricePresenter` and `PricePresenter` to utilize user permissions. - Improved `Threshold` and `Price` models for detailed progressive pricing. - Added `getSubtype`, `isOveruse`, and `isQuantityPredefined` methods to `RepresentablePrice`. - Refined `DynamicFormWidget` integration to better handle pricing thresholds. - Adjusted controllers to support new progressive price logic. - Revised unit tests for `ProgressivePricePresenter` to validate progressive thresholds and permissions. * HP-2769: refine DynamicFormWidget and ProgressivePresenter integration - Updated `ProgressivePresenter` to utilize `DynamicFormWidget` instance instead of widget container class for better encapsulation. - Adjusted JS logic to handle dynamic form widget updates more effectively. - Improved thresholds rendering in `priceFields.php` for progressive pricing. * HP-2769: remove unused `actionShowProgressive` and fix progressive price data mapping - Deleted unused `actionShowProgressive` method in `PlanController`. - Improved data mapping in `actionGetProgressiveInfo` to filter price keys dynamically and handle nested arrays correctly. * HP-2769: add tests and refine prepaid quantity handling in ProgressivePricePresenter - Added unit tests to validate rendering logic for prepaid quantities in `ProgressivePricePresenter`. - Enhanced `renderPrice` method to properly skip thresholds within prepaid quantity range and adjust "Next" tiers. - Updated `createPrice` method in tests to support `quantity` for prepaid handling. - Improved logic to differentiate between "Included" and priced thresholds. * HP-2769: add discount calculation and tests to ProgressivePricePresenter - Introduced discount calculation logic to `ProgressivePricePresenter` for accurate progressive pricing. - Updated `renderPrice` method to display discounts for each threshold tier, except the first one. - Added extensive unit tests to validate discount rendering, calculation rounding, and prepaid handling. - Refined CSS style in `ProgressivePresenter` for better visual display of progressive pricing. * HP-2769: update tests and refine logic in ProgressivePricePresenter - Adjusted price assertions in unit tests to reflect updated price values. - Implemented `getSubtype`, `isOveruse`, and `isQuantityPredefined` methods in `RepresentablePrice` to return default values. - Declared strict types in `Threshold` model for better type safety. - Added `getThresholds` method to `RepresentablePrice`. - Updated `ProgressivePresenter` to simplify `ProgressivePricePresenter` initialization. - Enhanced `PriceController` to skip processing thresholds when prices are not defined. * HP-2769: refactor unit tests in ProgressivePricePresenterTest for cleaner assertions and enhanced readability - Added helper methods: `assertLinesContain` and `getCleanLines` for streamlined validation. - Replaced repetitive assertions with ordered line checks to simplify test structure and improve maintainability. - Updated all relevant test cases to utilize the new helper methods. * HP-2769: validate objects in PriceController before processing - Added validation checks for `Price` and `Threshold` objects to ensure data consistency. - Refined `array_map` logic in `PriceController` to skip invalid price rows. - Updated threshold handling to validate and process valid threshold objects only. * HP-2769: refine doc comments and improve `Threshold` property handling - Corrected and clarified doc comments for `ProgressivePricePresenterTest`. - Updated `Threshold` model to ensure proper null checks and type consistency for `unitLabel` and `currencyLabel`. * minor
1 parent 0a7d316 commit fed4c2f

File tree

15 files changed

+726
-89
lines changed

15 files changed

+726
-89
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@ ocular.phar
4141
php-cs-fixer.phar
4242
phpunit-skelgen.phar
4343
phpunit.phar
44+
45+
# PHPUnit
46+
.phpunit.result.cache

src/controllers/PlanController.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,7 @@
4747

4848
class PlanController extends CrudController
4949
{
50-
/**
51-
* @var PriceModelFactory
52-
*/
53-
public $priceModelFactory;
50+
private PriceModelFactory $priceModelFactory;
5451

5552
/**
5653
* PlanController constructor.
@@ -440,7 +437,6 @@ private function populateWithPrices(Plan $plan, $pricesData): void
440437
$priceData['class'] = 'CertificatePrice';
441438
}
442439

443-
/** @var Price $price */
444440
$price = Price::instantiate($priceData);
445441
$price->setScenario('create');
446442
$price->setAttributes($priceData);

src/controllers/PriceController.php

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
use hipanel\helpers\ArrayHelper;
2323
use hipanel\modules\finance\actions\PriceUpdateAction;
2424
use hipanel\modules\finance\collections\PricesCollection;
25+
use hipanel\modules\finance\grid\presenters\price\ProgressivePricePresenter;
2526
use hipanel\modules\finance\helpers\PriceSort;
2627
use hipanel\modules\finance\models\Plan;
2728
use hipanel\modules\finance\models\Price;
2829
use hipanel\modules\finance\models\query\PriceQuery;
2930
use hipanel\modules\finance\models\TargetObject;
31+
use hipanel\modules\finance\models\Threshold;
3032
use Yii;
3133
use yii\base\DynamicModel;
3234
use yii\base\Event;
@@ -225,7 +227,10 @@ public function actionAddExtraPrices(int $plan_id, string $type)
225227
}
226228
$prices = $this->getSuggested($plan_id, $plan_id, null, $type);
227229
$existingObjects = array_keys(ArrayHelper::map($prices, 'object_id', 'id'));
228-
$uniqSuggestions = array_filter($suggestions, static fn($suggestion) => !in_array($suggestion->object_id, $existingObjects, true));
230+
$uniqSuggestions = array_filter(
231+
$suggestions,
232+
static fn($suggestion) => !in_array($suggestion->object_id, $existingObjects, true)
233+
);
229234
$prices = array_merge($prices, $uniqSuggestions);
230235
$prices = PriceSort::anyPrices()->values($prices, true);
231236

@@ -262,6 +267,46 @@ public function actionSuggest($plan_id, $object_id = null, $template_plan_id = n
262267
]);
263268
}
264269

270+
public function actionGetProgressiveInfo(): string
271+
{
272+
$presenter = Yii::$container->get(ProgressivePricePresenter::class);
273+
$dataPrice = array_filter($this->request->post(), fn($key) => str_ends_with($key, 'Price'), ARRAY_FILTER_USE_KEY);
274+
$dataThreshold = $this->request->post('Threshold', []);
275+
276+
if (!$dataPrice) {
277+
return '';
278+
}
279+
280+
$prices = array_filter(array_map(function (array $priceRow): ?Price {
281+
$price = new Price($priceRow);
282+
if ($price->validate()) {
283+
return $price;
284+
}
285+
286+
return null;
287+
}, reset($dataPrice)));
288+
if (empty($prices)) {
289+
return '';
290+
}
291+
foreach ($dataThreshold as $j => $thresholds) {
292+
if (!isset($prices[$j])) {
293+
continue;
294+
}
295+
$thresholdRows = [];
296+
$price = $prices[$j];
297+
foreach ($thresholds as $threshold) {
298+
$threshold['parent'] = $price;
299+
$thresholdObj = new Threshold($threshold);
300+
if ($thresholdObj->validate()) { // todo: validation needed?
301+
$thresholdRows[] = $thresholdObj;
302+
}
303+
}
304+
$price->setProgressivePricingThresholds($thresholdRows);
305+
}
306+
307+
return $presenter->renderPrice(reset($prices));
308+
}
309+
265310
private function getSuggested($plan_id, $object_id = null, $template_plan_id = null, string $type = 'default'): array
266311
{
267312
$suggestions = (new Price())->batchQuery('suggest', [

src/grid/PriceGridView.php

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,12 @@
2929
*/
3030
class PriceGridView extends BoxedGridView
3131
{
32-
/**
33-
* @var PricePresenterFactory
34-
*/
35-
private $presenterFactory;
36-
37-
public function __construct(PricePresenterFactory $presenterFactory, array $config = [])
32+
public function __construct(
33+
readonly private PricePresenterFactory $presenterFactory,
34+
array $config = []
35+
)
3836
{
3937
parent::__construct($config);
40-
$this->presenterFactory = $presenterFactory;
4138
}
4239

4340
public function columns()
@@ -60,7 +57,6 @@ public function columns()
6057
'label' => Yii::t('hipanel.finance.price', 'Old price'),
6158
'format' => 'raw',
6259
'value' => function (Price $model): string {
63-
/** @var PricePresenter $presenter */
6460
$presenter = $this->presenterFactory->build($model::class);
6561

6662
return $presenter

src/grid/presenters/price/PricePresenter.php

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010

1111
namespace hipanel\modules\finance\grid\presenters\price;
1212

13-
use hipanel\modules\finance\models\Price;
13+
use hipanel\modules\finance\models\RepresentablePrice;
1414
use hipanel\widgets\ArraySpoiler;
1515
use NumberFormatter;
16+
use Throwable;
1617
use Yii;
1718
use yii\base\InvalidConfigException;
1819
use yii\bootstrap\Html;
20+
use yii\di\NotInstantiableException;
1921
use yii\i18n\Formatter;
2022
use yii\web\User;
2123

@@ -27,16 +29,13 @@
2729
*/
2830
class PricePresenter
2931
{
30-
protected Formatter $formatter;
31-
32-
protected User $user;
33-
3432
protected string $priceAttribute = 'price';
3533

36-
public function __construct(Formatter $formatter, User $user)
34+
public function __construct(
35+
readonly protected Formatter $formatter,
36+
readonly protected User $user,
37+
)
3738
{
38-
$this->formatter = $formatter;
39-
$this->user = $user;
4039
}
4140

4241
/**
@@ -51,25 +50,25 @@ public function setPriceAttribute(string $priceAttribute): self
5150
}
5251

5352
/**
54-
* @param Price $price
55-
* @throws InvalidConfigException
53+
* @param RepresentablePrice $price
5654
* @return string
55+
* @throws InvalidConfigException
56+
* @throws Throwable
57+
* @throws NotInstantiableException
5758
*/
58-
public function renderPrice(Price $price): string
59+
public function renderPrice(RepresentablePrice $price): string
5960
{
6061
$unit = $formula = '';
6162
if ($price->getUnitLabel()) {
6263
$unit = ' ' . Yii::t('hipanel:finance', 'per {unit}', ['unit' => Html::encode($price->getUnitLabel())]);
6364
}
6465

65-
$activeFormulas = array_filter($price->getFormulaLines(), fn ($el) => $el['is_actual']);
66+
$activeFormulas = array_filter($price->getFormulaLines(), fn($el) => $el['is_actual']);
6667
if (!empty($activeFormulas)) {
6768
$formula = ArraySpoiler::widget([
68-
'id' => mt_rand(),
69+
'id' => uniqid('f_'),
6970
'data' => $activeFormulas,
70-
'formatter' => function ($v) {
71-
return Html::tag('kbd', Html::encode($v['formula']), ['class' => 'javascript']);
72-
},
71+
'formatter' => fn($v) => Html::tag('kbd', Html::encode($v['formula']), ['class' => 'javascript']),
7372
'visibleCount' => 0,
7473
'delimiter' => '<br />',
7574
'button' => [
@@ -81,17 +80,21 @@ public function renderPrice(Price $price): string
8180
],
8281
]);
8382
}
84-
$sum = $this->formatter->asCurrency($price->{$this->priceAttribute}, $price->currency, [NumberFormatter::MAX_FRACTION_DIGITS => 20]);
83+
$sum = $this->formatter->asCurrency(
84+
$price->{$this->priceAttribute},
85+
$price->currency,
86+
[NumberFormatter::MAX_FRACTION_DIGITS => 20]
87+
);
8588

8689
return Html::tag('strong', $sum) . $unit . $formula;
8790
}
8891

8992
/**
90-
* @param Price $price
93+
* @param RepresentablePrice $price
9194
* @param string $attribute
9295
* @return string
9396
*/
94-
public function renderInfo(Price $price, string $attribute = 'quantity'): string
97+
public function renderInfo(RepresentablePrice $price, string $attribute = 'quantity'): string
9598
{
9699
if (!$price->isQuantityPredefined()) {
97100
return Yii::t('hipanel:finance', '{icon} Quantity: {quantity}', [

src/grid/presenters/price/PricePresenterFactory.php

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?php
1+
<?php declare(strict_types=1);
22
/**
33
* Finance module for HiPanel
44
*
@@ -14,8 +14,10 @@
1414
use hipanel\modules\finance\models\Price;
1515
use hipanel\modules\finance\models\ProgressivePrice;
1616
use hipanel\modules\finance\models\TemplatePrice;
17+
use Psr\Container\ContainerExceptionInterface;
1718
use Psr\Container\ContainerInterface;
18-
use yii\base\InvalidConfigException;
19+
use Psr\Container\NotFoundExceptionInterface;
20+
use yii\web\User;
1921

2022
/**
2123
* Class PricePresenterFactory.
@@ -27,34 +29,28 @@ class PricePresenterFactory
2729
/**
2830
* @var array map of model class to its presenter
2931
*/
30-
protected $map = [
32+
protected array $map = [
3133
Price::class => PricePresenter::class,
3234
TemplatePrice::class => TemplatePricePresenter::class,
3335
CertificatePrice::class => CertificatePricePresenter::class,
3436
ProgressivePrice::class => ProgressivePricePresenter::class,
3537
'*' => PricePresenter::class,
3638
];
39+
protected array $cache = [];
3740

38-
/**
39-
* @var array
40-
*/
41-
protected $cache = [];
42-
/**
43-
* @var ContainerInterface
44-
*/
45-
private $container;
46-
47-
public function __construct(ContainerInterface $container)
41+
public function __construct(
42+
private readonly ContainerInterface $container,
43+
)
4844
{
49-
$this->container = $container;
5045
}
5146

5247
/**
5348
* @param string $name
5449
* @return PricePresenter
55-
* @throws InvalidConfigException
50+
* @throws ContainerExceptionInterface
51+
* @throws NotFoundExceptionInterface
5652
*/
57-
public function build($name)
53+
public function build(string $name): PricePresenter
5854
{
5955
$className = $this->map[$name] ?? $this->map['*'];
6056

@@ -65,9 +61,6 @@ public function build($name)
6561
return $this->cache[$name];
6662
}
6763

68-
/**
69-
* @return array
70-
*/
7164
public function getMap(): array
7265
{
7366
return $this->map;

0 commit comments

Comments
 (0)