Completed
Push — master ( 19a019...bc1809 )
by Klochok
11:54
created

PlanController::actionCreatePrices()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 0
cts 18
cp 0
rs 9.552
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 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\VariantsAction;
22
use hipanel\actions\ViewAction;
23
use hipanel\base\CrudController;
24
use hipanel\filters\EasyAccessControl;
25
use hipanel\helpers\ArrayHelper;
26
use hipanel\modules\finance\collections\PricesCollection;
27
use hipanel\modules\finance\grid\PriceGridView;
28
use hipanel\modules\finance\helpers\PlanInternalsGrouper;
29
use hipanel\modules\finance\helpers\PriceChargesEstimator;
30
use hipanel\modules\finance\helpers\PriceSort;
31
use hipanel\modules\finance\models\factories\PriceModelFactory;
32
use hipanel\modules\finance\models\Plan;
33
use hipanel\modules\finance\models\PlanAttribute;
34
use hipanel\modules\finance\models\Price;
35
use hipanel\modules\finance\models\PriceSuggestionRequestForm;
36
use hipanel\modules\finance\models\query\PlanQuery;
37
use hipanel\modules\finance\models\TargetObject;
38
use hiqdev\hiart\ResponseErrorException;
39
use Yii;
40
use yii\base\Event;
41
use yii\base\Module;
42
use yii\data\ArrayDataProvider;
43
use yii\web\NotFoundHttpException;
44
use yii\web\Response;
45
use yii\web\UnprocessableEntityHttpException;
46
47
class PlanController extends CrudController
48
{
49
    /**
50
     * @var PriceModelFactory
51
     */
52
    public $priceModelFactory;
53
54
    /**
55
     * PlanController constructor.
56
     * @param string $id
57
     * @param Module $module
58
     * @param PriceModelFactory $priceModelFactory
59
     * @param array $config
60
     */
61
    public function __construct(string $id, Module $module, PriceModelFactory $priceModelFactory, array $config = [])
62
    {
63
        parent::__construct($id, $module, $config);
64
65
        $this->priceModelFactory = $priceModelFactory;
66
    }
67
68
    public function behaviors()
69
    {
70
        return array_merge(parent::behaviors(), [
71
            [
72
                'class' => EasyAccessControl::class,
73
                'actions' => [
74
                    'create' => 'plan.create',
75
                    'update' => 'plan.update',
76
                    'update-prices' => 'plan.update',
77
                    'templates' => 'plan.create',
78
                    'create-prices' => 'plan.create',
79
                    'delete' => 'plan.delete',
80
                    '*' => 'plan.read',
81
                ],
82
            ],
83
        ]);
84
    }
85
86
    public function actions()
87
    {
88
        return array_merge(parent::actions(), [
89
            'create' => [
90
                'class' => SmartCreateAction::class,
91
                'success' => Yii::t('hipanel.finance.plan', 'Plan was successfully created'),
92
                'on beforeSave' => $this->saveWithPlanAttributes(),
93
            ],
94
            'update' => [
95
                'class' => SmartUpdateAction::class,
96
                'success' => Yii::t('hipanel.finance.plan', 'Plan was successfully updated'),
97
                'on beforeSave' => $this->saveWithPlanAttributes(),
98
            ],
99
            'index' => [
100
                'responseVariants' => [
101
                    'get-total-count' => fn(VariantsAction $action): int => Plan::find()->count(),
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_VARIABLE, expecting ',' or ')'
Loading history...
102
                ],
103
                'class' => IndexAction::class,
104
            ],
105
            'view' => [
106
                'class' => ViewAction::class,
107
                'on beforePerform' => function (Event $event) {
108
                    /** @var PlanQuery $query */
109
                    $query = $event->sender->getDataProvider()->query;
110
                    $query
111
                        ->withSales()
112
                        ->withPrices()
113
                        ->withPriceHistory();
114
                },
115
                'data' => function (Action $action, array $data) {
116
                    return array_merge($data, array_filter([
117
                        'grouper' => new PlanInternalsGrouper($data['model']),
118
                        'parentPrices' => Yii::$app->user->can('plan.update') ? $this->getParentPrices($data['model']['id']) : null,
119
                    ]));
120
                },
121
            ],
122
            'set-note' => [
123
                'class' => SmartUpdateAction::class,
124
                'success' => Yii::t('hipanel', 'Note changed'),
125
            ],
126
            'validate-form' => [
127
                'class' => ValidateFormAction::class,
128
            ],
129
            'validate-single-form' => [
130
                'class' => ValidateFormAction::class,
131
                'validatedInputId' => false,
132
            ],
133
            'delete' => [
134
                'class' => SmartDeleteAction::class,
135
                'success' => Yii::t('hipanel.finance.plan', 'Plan was successfully deleted'),
136
            ],
137
            'restore' => [
138
                'class' => SmartPerformAction::class,
139
                'success' => Yii::t('hipanel.finance.plan', 'Plan was successfully restored'),
140
            ],
141
            'copy' => [
142
                'class' => SmartUpdateAction::class,
143
                'view' => 'modals/copy',
144
                'queryOptions' => ['batch' => false],
145
            ],
146
        ]);
147
    }
148
149
    public function actionCreatePrices(int $plan_id, int $template_plan_id)
150
    {
151
        $plan = $this->findTemplatePlan($plan_id, $plan_id, $template_plan_id);
152
153
        $suggestions = (new Price())->batchQuery('suggest', [
154
            'object_id' => $plan_id,
155
            'plan_id' => $plan_id,
156
            'template_plan_id' => $template_plan_id,
157
            'type' => $plan->type,
158
        ]);
159
        $this->populateWithPrices($plan, $suggestions);
160
161
        $parentPrices = $this->getParentPrices($plan_id);
162
163
        $targetPlan = Plan::findOne(['id' => $plan_id]);
164
165
        $grouper = new PlanInternalsGrouper($plan);
166
        [$plan->name, $plan->id] = [$targetPlan->name, $targetPlan->id];
167
        $action = ['@plan/update-prices', 'id' => $plan->id, 'scenario' => 'create'];
168
169
        return $this->render($plan->type . '/' . 'createPrices',
170
            compact('plan', 'grouper', 'parentPrices', 'action', 'plan_id'));
171
    }
172
173
    public function actionGetPlanHistory(int $plan_id, string $date)
174
    {
175
        $plan = Plan::find()
176
            ->where(['id' => $plan_id])
177
            ->andWhere(['history_time' => $date])
178
            ->withSales()
179
            ->withPriceHistory()
180
            ->one();
181
182
        return PriceGridView::widget([
183
            'boxed' => false,
184
            'showHeader' => true,
185
            'showFooter' => false,
186
            'summaryRenderer' => function (): string {
187
                return '';
188
            },
189
            'emptyText' => Yii::t('hipanel.finance.price', 'No prices found'),
190
            'dataProvider' => new ArrayDataProvider([
191
                'allModels' => $plan->priceHistory,
192
                'pagination' => false,
193
            ]),
194
            'columns' => [
195
                'object->name',
196
                'type',
197
                'info',
198
                'old_quantity',
199
                'old_price',
200
                'note',
201
            ],
202
        ]);
203
    }
204
205
    public function actionSuggestPricesModal($id)
206
    {
207
        /** @var Plan $plan */
208
        $plan = $this->findPlan($id);
209
        $model = new PriceSuggestionRequestForm([
210
            'plan_id' => $plan->id,
211
            'plan_type' => $plan->type,
212
        ]);
213
214
        return $this->renderAjax('modals/suggestPrices', compact('plan', 'model'));
215
    }
216
217
    public function actionSuggestGroupingPricesModal($id)
218
    {
219
        /** @var Plan $plan */
220
        $plan = $this->findPlan($id);
221
        $model = new PriceSuggestionRequestForm([
222
            'plan_id' => $plan->id,
223
            'plan_type' => $plan->type,
224
            'object_id' => $plan->id,
225
            'scenario' => PriceSuggestionRequestForm::SCENARIO_PREDEFINED_OBJECT,
226
        ]);
227
228
        return $this->renderAjax('modals/suggestPrices', compact('plan', 'model'));
229
    }
230
231
    public function actionSuggestSharedPricesModal($id)
232
    {
233
        /** @var Plan $plan */
234
        $plan = $this->findPlan($id);
235
        $model = new PriceSuggestionRequestForm([
236
            'plan_id' => $plan->id,
237
            'plan_type' => $plan->type,
238
            'scenario' => PriceSuggestionRequestForm::SCENARIO_PREDEFINED_OBJECT,
239
        ]);
240
241
        return $this->renderAjax('modals/suggestPrices', compact('plan', 'model'));
242
    }
243
244
    private function findTemplatePlan(int $targetPlan, int $object_id, int $expectedTemplateId): Plan
245
    {
246
        $result = Plan::perform('search-templates', [
247
            'id' => $targetPlan,
248
            'object_id' => $object_id,
249
        ]);
250
        $plans = ArrayHelper::index($result, 'id');
251
252
        if (!isset($plans[$expectedTemplateId])) {
253
            throw new NotFoundHttpException('Requested template plan not found');
254
        }
255
256
        $plan = Plan::instantiate($plans[$expectedTemplateId]);
257
        Plan::populateRecord($plan, $plans[$expectedTemplateId]);
258
259
        return $plan;
260
    }
261
262
    /**
263
     * @param $id integer
264
     * @return Plan|null
265
     * @throws NotFoundHttpException
266
     */
267
    private function findPlan(int $id): ?Plan
268
    {
269
        $plan = Plan::findOne(['id' => $id]);
270
        if ($plan === null) {
271
            throw new NotFoundHttpException('Not found');
272
        }
273
274
        return $plan;
275
    }
276
277
    /**
278
     * @param string $plan_id
279
     * @param string|null $object_id Object ID or `null`
280
     * when the desired templates are not related to a specific object
281
     * @param string $name_ilike
282
     * @return array
283
     */
284
    public function actionTemplates($plan_id, $object_id = null, string $name_ilike = null)
285
    {
286
        $templates = (new Plan())->query('search-templates', [
287
            'id' => $plan_id,
288
            'object_id' => $object_id ?? $plan_id,
289
            'name_ilike' => $name_ilike,
290
        ]);
291
292
        Yii::$app->response->format = Response::FORMAT_JSON;
293
294
        return $templates;
295
    }
296
297
    public function actionCalculateCharges()
298
    {
299
        Yii::$app->response->format = Response::FORMAT_JSON;
300
        $request = Yii::$app->request;
301
302
        $periods = ['now', 'first day of +1 month', 'first day of +1 year'];
303
        $calculations = Plan::perform('calculate-charges', [
304
            'actions' => $request->post('actions'),
305
            'prices' => $request->post('prices'),
306
            'times' => $periods,
307
        ]);
308
        /** @var PriceChargesEstimator $calculator */
309
        $calculator = Yii::$container->get(PriceChargesEstimator::class, [$calculations]);
310
311
        try {
312
            return $calculator->calculateForPeriods($periods);
313
        } catch (ResponseErrorException $exception) {
314
            Yii::$app->response->setStatusCode(412, $exception->getMessage());
315
316
            return [
317
                'formula' => $exception->getResponse()->getData()['_error_ops']['formula'] ?? null,
318
            ];
319
        }
320
    }
321
322
    public function actionCalculateValues($planId)
323
    {
324
        Yii::$app->response->format = Response::FORMAT_JSON;
325
        $periods = ['now', 'first day of +1 month', 'first day of +1 year'];
326
        try {
327
            $calculations = Plan::perform('calculate-values', ['id' => $planId, 'times' => $periods]);
328
            $calculator = Yii::$container->get(PriceChargesEstimator::class, [$calculations]);
329
330
            return $calculator->calculateForPeriods($periods);
331
        } catch (ResponseErrorException $exception) {
332
            Yii::$app->response->setStatusCode(412, $exception->getMessage());
333
334
            return [
335
                'formula' => $exception->getResponse()->getData()['_error_ops']['formula'] ?? null,
336
            ];
337
        }
338
    }
339
340
    public function actionUpdatePrices(int $id, string $scenario = 'update')
341
    {
342
        $plan = Plan::find()
343
            ->byId($id)
344
            ->withPrices()
345
            ->one();
346
347
        $request = Yii::$app->request;
348
        if ($request->isPost) {
349
            try {
350
                $collection = new PricesCollection($this->priceModelFactory, ['scenario' => $scenario]);
351
                $collection->load();
352
                if ($collection->save() === false) {
353
                    if ($scenario === 'create') {
354
                        Yii::$app->session->addFlash('error', Yii::t('hipanel.finance.price', 'Error occurred during creation of prices'));
355
                    } elseif ($scenario === 'update') {
356
                        Yii::$app->session->addFlash('error', Yii::t('hipanel.finance.price', 'Error occurred during prices update'));
357
                    }
358
                } else {
359
                    if ($scenario === 'create') {
360
                        Yii::$app->session->addFlash('success', Yii::t('hipanel.finance.price', 'Prices were successfully created'));
361
                    } elseif ($scenario === 'update') {
362
                        Yii::$app->session->addFlash('success', Yii::t('hipanel.finance.price', 'Prices were successfully updated'));
363
                    }
364
                }
365
366
                return $this->redirect(['@plan/view', 'id' => $id]);
367
            } catch (\Exception $e) {
368
                throw new UnprocessableEntityHttpException($e->getMessage(), 0, $e);
369
            }
370
        }
371
372
        $grouper = new PlanInternalsGrouper($plan);
373
        $parentPrices = $this->getParentPrices($id);
374
375
        return $this->render($plan->type . '/' . 'updatePrices',
376
            compact('plan', 'grouper', 'parentPrices'));
377
    }
378
379
    /**
380
     * @param int $plan_id
381
     * @return Price[]|null Array of parent plan prices or `null`, when parent plan was not found
382
     */
383
    private function getParentPrices(int $plan_id)
384
    {
385
        $plan = Plan::find()
386
            ->addAction('get-parent')
387
            ->where(['id' => $plan_id])
388
            ->joinWithPrices()
389
            ->one();
390
391
        if ($plan === null || $plan->id === null) {
392
            return null;
393
        }
394
395
        return (new PlanInternalsGrouper($plan))->group();
396
    }
397
398
    /**
399
     * @param Plan $plan
400
     * @param array $pricesData
401
     */
402
    private function populateWithPrices(Plan $plan, $pricesData): void
403
    {
404
        $prices = [];
405
        foreach ($pricesData as $priceData) {
406
            $object = ArrayHelper::remove($priceData, 'object');
407
            if (isset($priceData['plan_type']) &&
408
                $priceData['plan_type'] === 'certificate') {
409
                $priceData['class'] = 'CertificatePrice';
410
            }
411
412
            /** @var Price $price */
413
            $price = Price::instantiate($priceData);
414
            $price->setScenario('create');
415
            $price->setAttributes($priceData);
416
            $price->populateRelation('object', new TargetObject($object));
417
            $price->trigger(Price::EVENT_AFTER_FIND);
418
            $prices[] = $price;
419
        }
420
        $prices = PriceSort::anyPrices()->values($prices, true);
421
422
        $plan->populateRelation('prices', $prices);
423
    }
424
425
    private function saveWithPlanAttributes(): Closure
426
    {
427
        return static function (Event $event): void {
428
            $action = $event->sender;
429
            $request = $action->controller->request;
430
            $attributeModel = new PlanAttribute();
431
            $planAttributeData = $request->post($attributeModel->formName(), []);
432
            foreach ($action->collection->models as $model) {
433
                $customData['attributes'] = [];
434
                foreach ($planAttributeData as $planAttribute) {
435
                    $attributeModel->load($planAttribute, '');
436
                    if ($attributeModel->validate()) {
437
                        $customData['attributes'][$attributeModel->name] = $attributeModel->value;
438
                    }
439
                }
440
                $model->custom_data = $customData;
441
            }
442
        };
443
    }
444
}
445