Completed
Push — master ( 691d80...8f3098 )
by Dmitry
13:51
created

PlanController::actionGetPlanHistory()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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