Completed
Push — master ( 8d2b6e...df3805 )
by Andrii
13:34
created

PlanController::saveWithPlanAttributes()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 0
cts 0
cp 0
rs 9.6333
c 0
b 0
f 0
cc 4
nc 1
nop 0
crap 20
1
<?php
2
/**
3
 * Finance module for HiPanel
4
 *
5
 * @link      https://github.com/hiqdev/hipanel-module-finance
6
 * @package   hipanel-module-finance
7
 * @license   BSD-3-Clause
8
 * @copyright Copyright (c) 2015-2019, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hipanel\modules\finance\controllers;
12
13
use Closure;
14
use hipanel\actions\Action;
15
use hipanel\actions\IndexAction;
16
use hipanel\actions\SmartCreateAction;
17
use hipanel\actions\SmartDeleteAction;
18
use hipanel\actions\SmartPerformAction;
19
use hipanel\actions\SmartUpdateAction;
20
use hipanel\actions\ValidateFormAction;
21
use hipanel\actions\ViewAction;
22
use hipanel\base\CrudController;
23
use hipanel\filters\EasyAccessControl;
24
use hipanel\helpers\ArrayHelper;
25
use hipanel\modules\finance\collections\PricesCollection;
26
use hipanel\modules\finance\grid\PriceGridView;
27
use hipanel\modules\finance\helpers\PlanInternalsGrouper;
28
use hipanel\modules\finance\helpers\PriceChargesEstimator;
29
use hipanel\modules\finance\helpers\PriceSort;
30
use hipanel\modules\finance\models\factories\PriceModelFactory;
31
use hipanel\modules\finance\models\Plan;
32
use hipanel\modules\finance\models\PlanAttribute;
33
use hipanel\modules\finance\models\Price;
34
use hipanel\modules\finance\models\PriceSuggestionRequestForm;
35
use hipanel\modules\finance\models\query\PlanQuery;
36
use hipanel\modules\finance\models\TargetObject;
37
use hiqdev\hiart\ResponseErrorException;
38
use Yii;
39
use yii\base\Event;
40
use yii\base\Module;
41
use yii\data\ArrayDataProvider;
42
use yii\web\NotFoundHttpException;
43
use yii\web\Response;
44
use yii\web\UnprocessableEntityHttpException;
45
46
class PlanController extends CrudController
47
{
48
    /**
49
     * @var PriceModelFactory
50
     */
51
    public $priceModelFactory;
52
53
    /**
54
     * PlanController constructor.
55
     * @param string $id
56
     * @param Module $module
57
     * @param PriceModelFactory $priceModelFactory
58
     * @param array $config
59
     */
60
    public function __construct(string $id, Module $module, PriceModelFactory $priceModelFactory, array $config = [])
61
    {
62
        parent::__construct($id, $module, $config);
63
64
        $this->priceModelFactory = $priceModelFactory;
65
    }
66
67
    public function behaviors()
68
    {
69
        return array_merge(parent::behaviors(), [
70
            [
71
                'class' => EasyAccessControl::class,
72
                'actions' => [
73
                    'create' => 'plan.create',
74
                    'update' => 'plan.update',
75
                    'update-prices' => 'plan.update',
76
                    'templates' => 'plan.create',
77
                    'create-prices' => 'plan.create',
78
                    'delete' => 'plan.delete',
79
                    '*' => 'plan.read',
80
                ],
81
            ],
82
        ]);
83
    }
84
85
    public function actions()
86
    {
87
        return array_merge(parent::actions(), [
88
            'create' => [
89
                'class' => SmartCreateAction::class,
90
                'success' => Yii::t('hipanel.finance.plan', 'Plan was successfully created'),
91
                'on beforeSave' => $this->saveWithPlanAttributes(),
92
            ],
93
            'update' => [
94
                'class' => SmartUpdateAction::class,
95
                'success' => Yii::t('hipanel.finance.plan', 'Plan was successfully updated'),
96
                'on beforeSave' => $this->saveWithPlanAttributes(),
97
            ],
98
            'index' => [
99
                'class' => IndexAction::class,
100
            ],
101
            'view' => [
102
                'class' => ViewAction::class,
103
                'on beforePerform' => function (Event $event) {
104
                    /** @var PlanQuery $query */
105
                    $query = $event->sender->getDataProvider()->query;
106
                    $query
107
                        ->withSales()
108
                        ->withPrices()
109
                        ->withPriceHistory();
110
                },
111
                'data' => function (Action $action, array $data) {
112
                    return array_merge($data, array_filter([
113
                        'grouper' => new PlanInternalsGrouper($data['model']),
114
                        'parentPrices' => Yii::$app->user->can('plan.update') ? $this->getParentPrices($data['model']['id']) : null,
115
                    ]));
116
                },
117
            ],
118
            'set-note' => [
119
                'class' => SmartUpdateAction::class,
120
                'success' => Yii::t('hipanel', 'Note changed'),
121
            ],
122
            'validate-form' => [
123
                'class' => ValidateFormAction::class,
124
            ],
125
            'validate-single-form' => [
126
                'class' => ValidateFormAction::class,
127
                'validatedInputId' => false,
128
            ],
129
            'delete' => [
130
                'class' => SmartDeleteAction::class,
131
                'success' => Yii::t('hipanel.finance.plan', 'Plan was successfully deleted'),
132
            ],
133
            'restore' => [
134
                'class' => SmartPerformAction::class,
135
                'success' => Yii::t('hipanel.finance.plan', 'Plan was successfully restored'),
136
            ],
137
            'copy' => [
138
                'class' => SmartUpdateAction::class,
139
                'view' => 'modals/copy',
140
                'queryOptions' => ['batch' => false],
141
            ],
142
        ]);
143
    }
144
145
    public function actionCreatePrices(int $plan_id, int $template_plan_id)
146
    {
147
        $plan = $this->findTemplatePlan($plan_id, $plan_id, $template_plan_id);
148
149
        $suggestions = (new Price())->batchQuery('suggest', [
150
            'object_id' => $plan_id,
151
            'plan_id' => $plan_id,
152
            'template_plan_id' => $template_plan_id,
153
            'type' => $plan->type,
154
        ]);
155
        $this->populateWithPrices($plan, $suggestions);
156
157
        $parentPrices = $this->getParentPrices($plan_id);
158
159
        $targetPlan = Plan::findOne(['id' => $plan_id]);
160
161
        $grouper = new PlanInternalsGrouper($plan);
162
        [$plan->name, $plan->id] = [$targetPlan->name, $targetPlan->id];
163
        $action = ['@plan/update-prices', 'id' => $plan->id, 'scenario' => 'create'];
164
165
        return $this->render($plan->type . '/' . 'createPrices',
166
            compact('plan', 'grouper', 'parentPrices', 'action', 'plan_id'));
167
    }
168
169
    public function actionGetPlanHistory(int $plan_id, string $date)
170
    {
171
        $plan = Plan::find()
172
            ->where(['id' => $plan_id])
173
            ->andWhere(['history_time' => $date])
174
            ->withSales()
175
            ->withPriceHistory()
176
            ->one();
177
178
        return PriceGridView::widget([
179
            'boxed' => false,
180
            'showHeader' => true,
181
            'showFooter' => false,
182
            'summaryRenderer' => function (): string {
183
                return '';
184
            },
185
            'emptyText' => Yii::t('hipanel.finance.price', 'No prices found'),
186
            'dataProvider' => new ArrayDataProvider([
187
                'allModels' => $plan->priceHistory,
188
                'pagination' => false,
189
            ]),
190
            'columns' => [
191
                'object->name',
192
                'type',
193
                'info',
194
                'old_quantity',
195
                'old_price',
196
                'note',
197
            ],
198
        ]);
199
    }
200
201
    public function actionSuggestPricesModal($id)
202
    {
203
        /** @var Plan $plan */
204
        $plan = $this->findPlan($id);
205
        $model = new PriceSuggestionRequestForm([
206
            'plan_id' => $plan->id,
207
            'plan_type' => $plan->type,
208
        ]);
209
210
        return $this->renderAjax('modals/suggestPrices', compact('plan', 'model'));
211
    }
212
213
    public function actionSuggestGroupingPricesModal($id)
214
    {
215
        /** @var Plan $plan */
216
        $plan = $this->findPlan($id);
217
        $model = new PriceSuggestionRequestForm([
218
            'plan_id' => $plan->id,
219
            'plan_type' => $plan->type,
220
            'object_id' => $plan->id,
221
            'scenario' => PriceSuggestionRequestForm::SCENARIO_PREDEFINED_OBJECT,
222
        ]);
223
224
        return $this->renderAjax('modals/suggestPrices', compact('plan', 'model'));
225
    }
226
227
    public function actionSuggestSharedPricesModal($id)
228
    {
229
        /** @var Plan $plan */
230
        $plan = $this->findPlan($id);
231
        $model = new PriceSuggestionRequestForm([
232
            'plan_id' => $plan->id,
233
            'plan_type' => $plan->type,
234
            'scenario' => PriceSuggestionRequestForm::SCENARIO_PREDEFINED_OBJECT,
235
        ]);
236
237
        return $this->renderAjax('modals/suggestPrices', compact('plan', 'model'));
238
    }
239
240
    private function findTemplatePlan(int $targetPlan, int $object_id, int $expectedTemplateId): Plan
241
    {
242
        $result = Plan::perform('search-templates', [
243
            'id' => $targetPlan,
244
            'object_id' => $object_id,
245
        ]);
246
        $plans = ArrayHelper::index($result, 'id');
247
248
        if (!isset($plans[$expectedTemplateId])) {
249
            throw new NotFoundHttpException('Requested template plan not found');
250
        }
251
252
        $plan = Plan::instantiate($plans[$expectedTemplateId]);
253
        Plan::populateRecord($plan, $plans[$expectedTemplateId]);
254
255
        return $plan;
256
    }
257
258
    /**
259
     * @param $id integer
260
     * @return Plan|null
261
     * @throws NotFoundHttpException
262
     */
263
    private function findPlan(int $id): ?Plan
264
    {
265
        $plan = Plan::findOne(['id' => $id]);
266
        if ($plan === null) {
267
            throw new NotFoundHttpException('Not found');
268
        }
269
270
        return $plan;
271
    }
272
273
    /**
274
     * @param string $plan_id
275
     * @param string|null $object_id Object ID or `null`
276
     * when the desired templates are not related to a specific object
277
     * @param string $name_ilike
278
     * @return array
279
     */
280
    public function actionTemplates($plan_id, $object_id = null, string $name_ilike = null)
281
    {
282
        $templates = (new Plan())->query('search-templates', [
283
            'id' => $plan_id,
284
            'object_id' => $object_id ?? $plan_id,
285
            'name_ilike' => $name_ilike,
286
        ]);
287
288
        Yii::$app->response->format = Response::FORMAT_JSON;
289
290
        return $templates;
291
    }
292
293
    public function actionCalculateCharges()
294
    {
295
        Yii::$app->response->format = Response::FORMAT_JSON;
296
        $request = Yii::$app->request;
297
298
        $periods = ['now', 'first day of +1 month', 'first day of +1 year'];
299
        $calculations = Plan::perform('calculate-charges', [
300
            'actions' => $request->post('actions'),
301
            'prices' => $request->post('prices'),
302
            'times' => $periods,
303
        ]);
304
        /** @var PriceChargesEstimator $calculator */
305
        $calculator = Yii::$container->get(PriceChargesEstimator::class, [$calculations]);
306
307
        try {
308
            return $calculator->calculateForPeriods($periods);
309
        } catch (ResponseErrorException $exception) {
310
            Yii::$app->response->setStatusCode(412, $exception->getMessage());
311
312
            return [
313
                'formula' => $exception->getResponse()->getData()['_error_ops']['formula'] ?? null,
314
            ];
315
        }
316
    }
317
318
    public function actionCalculateValues($planId)
319
    {
320
        Yii::$app->response->format = Response::FORMAT_JSON;
321
        $periods = ['now', 'first day of +1 month', 'first day of +1 year'];
322
        try {
323
            $calculations = Plan::perform('calculate-values', ['id' => $planId, 'times' => $periods]);
324
            $calculator = Yii::$container->get(PriceChargesEstimator::class, [$calculations]);
325
326
            return $calculator->calculateForPeriods($periods);
327
        } catch (ResponseErrorException $exception) {
328
            Yii::$app->response->setStatusCode(412, $exception->getMessage());
329
330
            return [
331
                'formula' => $exception->getResponse()->getData()['_error_ops']['formula'] ?? null,
332
            ];
333
        }
334
    }
335
336
    public function actionUpdatePrices(int $id, string $scenario = 'update')
337
    {
338
        $plan = Plan::find()
339
            ->byId($id)
340
            ->withPrices()
341
            ->one();
342
343
        $request = Yii::$app->request;
344
        if ($request->isPost) {
345
            try {
346
                $collection = new PricesCollection($this->priceModelFactory, ['scenario' => $scenario]);
347
                $collection->load();
348
                if ($collection->save() === false) {
349
                    if ($scenario === 'create') {
350
                        Yii::$app->session->addFlash('error', Yii::t('hipanel.finance.price', 'Error occurred during creation of prices'));
351
                    } elseif ($scenario === 'update') {
352
                        Yii::$app->session->addFlash('error', Yii::t('hipanel.finance.price', 'Error occurred during prices update'));
353
                    }
354
                } else {
355
                    if ($scenario === 'create') {
356
                        Yii::$app->session->addFlash('success', Yii::t('hipanel.finance.price', 'Prices were successfully created'));
357
                    } elseif ($scenario === 'update') {
358
                        Yii::$app->session->addFlash('success', Yii::t('hipanel.finance.price', 'Prices were successfully updated'));
359
                    }
360
                }
361
362
                return $this->redirect(['@plan/view', 'id' => $id]);
363
            } catch (\Exception $e) {
364
                throw new UnprocessableEntityHttpException($e->getMessage(), 0, $e);
365
            }
366
        }
367
368
        $grouper = new PlanInternalsGrouper($plan);
0 ignored issues
show
Documentation introduced by
$plan is of type object<hiqdev\hiart\ActiveRecord>|array|null, but the function expects a object<hipanel\modules\finance\models\Plan>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
369
        $parentPrices = $this->getParentPrices($id);
370
371
        return $this->render($plan->type . '/' . 'updatePrices',
372
            compact('plan', 'grouper', 'parentPrices'));
373
    }
374
375
    /**
376
     * @param int $plan_id
377
     * @return Price[]|null Array of parent plan prices or `null`, when parent plan was not found
378
     */
379
    private function getParentPrices(int $plan_id)
380
    {
381
        $plan = Plan::find()
382
            ->addAction('get-parent')
383
            ->where(['id' => $plan_id])
384
            ->joinWithPrices()
385
            ->one();
386
387
        if ($plan === null || $plan->id === null) {
388
            return null;
389
        }
390
391
        return (new PlanInternalsGrouper($plan))->group();
0 ignored issues
show
Documentation introduced by
$plan is of type object<hiqdev\hiart\ActiveRecord>|array, but the function expects a object<hipanel\modules\finance\models\Plan>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
392
    }
393
394
    /**
395
     * @param Plan $plan
396
     * @param array $pricesData
397
     */
398
    private function populateWithPrices(Plan $plan, $pricesData): void
399
    {
400
        $prices = [];
401
        foreach ($pricesData as $priceData) {
402
            $object = ArrayHelper::remove($priceData, 'object');
403
            if (isset($priceData['plan_type']) &&
404
                $priceData['plan_type'] === 'certificate') {
405
                $priceData['class'] = 'CertificatePrice';
406
            }
407
408
            /** @var Price $price */
409
            $price = Price::instantiate($priceData);
410
            $price->setScenario('create');
411
            $price->setAttributes($priceData);
412
            $price->populateRelation('object', new TargetObject($object));
413
            $price->trigger(Price::EVENT_AFTER_FIND);
414
            $prices[] = $price;
415
        }
416
        $prices = PriceSort::anyPrices()->values($prices, true);
417
418
        $plan->populateRelation('prices', $prices);
419
    }
420
421
    private function saveWithPlanAttributes(): Closure
422
    {
423
        return static function (Event $event): void {
424
            $action = $event->sender;
425
            $request = $action->controller->request;
426
            $attributeModel = new PlanAttribute();
427
            $planAttributeData = $request->post($attributeModel->formName(), []);
428
            foreach ($action->collection->models as $model) {
429
                $customData['attributes'] = [];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$customData was never initialized. Although not strictly required by PHP, it is generally a good practice to add $customData = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
430
                foreach ($planAttributeData as $planAttribute) {
431
                    $attributeModel->load($planAttribute, '');
432
                    if ($attributeModel->validate()) {
433
                        $customData['attributes'][$attributeModel->name] = $attributeModel->value;
434
                    }
435
                }
436
                $model->custom_data = $customData;
437
            }
438
        };
439
    }
440
}
441