Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 186
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiPriceController
0.00% covered (danger)
0.00%
0 / 186
0.00% covered (danger)
0.00%
0 / 9
2862
0.00% covered (danger)
0.00%
0 / 1
 apiPricingFind
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 1
650
 apiPriceCreate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 createPriceFromRequest
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
2
 validatePriceCreate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiPriceUpdate
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 updatePriceFromRequest
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
210
 validatePriceUpdate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiPriceDelete
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 validatePriceDelete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3/**
4 * Jingga
5 *
6 * PHP Version 8.1
7 *
8 * @package   Modules\Billing
9 * @copyright Dennis Eichhorn
10 * @license   OMS License 2.0
11 * @version   1.0.0
12 * @link      https://jingga.app
13 */
14declare(strict_types=1);
15
16namespace Modules\Billing\Controller;
17
18use Modules\Attribute\Models\NullAttributeValue;
19use Modules\Billing\Models\Price\Price;
20use Modules\Billing\Models\Price\PriceMapper;
21use Modules\Billing\Models\Price\PriceType;
22use Modules\Billing\Models\Tax\TaxCombinationMapper;
23use Modules\ClientManagement\Models\ClientMapper;
24use Modules\ClientManagement\Models\NullClient;
25use Modules\ItemManagement\Models\ItemMapper;
26use Modules\ItemManagement\Models\NullItem;
27use Modules\SupplierManagement\Models\NullSupplier;
28use Modules\SupplierManagement\Models\SupplierMapper;
29use phpOMS\Localization\ISO4217CharEnum;
30use phpOMS\Message\Http\RequestStatusCode;
31use phpOMS\Message\RequestAbstract;
32use phpOMS\Message\ResponseAbstract;
33use phpOMS\Stdlib\Base\FloatInt;
34use phpOMS\System\MimeType;
35
36/**
37 * Billing class.
38 *
39 * @package Modules\Billing
40 * @license OMS License 2.0
41 * @link    https://jingga.app
42 * @since   1.0.0
43 */
44final class ApiPriceController extends Controller
45{
46    /**
47     * Api method to find items
48     *
49     * @param RequestAbstract  $request  Request
50     * @param ResponseAbstract $response Response
51     * @param array            $data     Generic data
52     *
53     * @return void
54     *
55     * @api
56     *
57     * @since 1.0.0
58     */
59    public function apiPricingFind(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
60    {
61        // Get item
62        /** @var null|\Modules\ItemManagement\Models\Item $item */
63        $item = null;
64        if ($request->hasData('price_item')) {
65            /** @var null|\Modules\ItemManagement\Models\Item $item */
66            $item = ItemMapper::get()
67                ->with('attributes')
68                ->with('attributes/type')
69                ->with('attributes/value')
70                ->where('id', (int) $request->getData('price_item'))
71                ->execute();
72        }
73
74        // Get account
75        /** @var null|\Modules\ClientManagement\Models\Client|\Modules\SupplierManagement\Models\Supplier $account */
76        $account = null;
77
78        /** @var null|\Modules\ClientManagement\Models\Client $client */
79        $client = null;
80
81        /** @var null|\Modules\SupplierManagement\Models\Supplier $supplier */
82        $supplier = null;
83
84        if ($request->hasData('price_client')) {
85            /** @var \Modules\ClientManagement\Models\Client $client */
86            $client = ClientMapper::get()
87                ->with('attributes')
88                ->with('attributes/type')
89                ->with('attributes/value')
90                ->where('id', (int) $request->getData('price_client'))
91                ->execute();
92
93            /** @var \Modules\ClientManagement\Models\Client */
94            $account = $client;
95        } else {
96            /** @var \Modules\SupplierManagement\Models\Supplier $supplier */
97            $supplier = SupplierMapper::get()
98                ->with('attributes')
99                ->with('attributes/type')
100                ->with('attributes/value')
101                ->where('id', (int) $request->getData('price_supplier'))
102                ->execute();
103
104            /** @var \Modules\SupplierManagement\Models\Supplier $account */
105            $account = $supplier;
106        }
107
108        // Get all relevant prices
109        // @todo: allow to define NOT IN somehow (e.g. not in France -> simple solution to define export prices etc.)
110        $queryMapper = PriceMapper::getAll();
111
112        if ($request->hasData('price_name')) {
113            $queryMapper->where('name', $request->getData('price_name'));
114        }
115
116        $queryMapper->where('promocode', \array_unique([$request->getData('price_promocode'), null]), 'IN');
117
118        $queryMapper->where('item', \array_unique([$request->getData('price_item', 'int'), null]), 'IN');
119        $queryMapper->where('itemgroup', \array_unique([$request->getData('price_itemgroup', 'int'), $item?->getAttribute('itemgroup')->id, null]), 'IN');
120        $queryMapper->where('itemsegment', \array_unique([$request->getData('price_itemsegment', 'int'), $item?->getAttribute('itemsegment')->id, null]), 'IN');
121        $queryMapper->where('itemsection', \array_unique([$request->getData('price_itemsection', 'int'), $item?->getAttribute('itemsection')->id, null]), 'IN');
122        $queryMapper->where('itemtype', \array_unique([$request->getData('price_itemtype', 'int'), $item?->getAttribute('itemtype')->id, null]), 'IN');
123
124        $queryMapper->where('client', \array_unique([$request->getData('price_client', 'int'), null]), 'IN');
125        $queryMapper->where('clientgroup', \array_unique([$request->getData('price_clientgroup', 'int'), $client?->getAttribute('clientgroup')->id, null]), 'IN');
126        $queryMapper->where('clientsegment', \array_unique([$request->getData('price_clientsegment', 'int'), $client?->getAttribute('clientsegment')->id, null]), 'IN');
127        $queryMapper->where('clientsection', \array_unique([$request->getData('price_clientsection', 'int'), $client?->getAttribute('clientsection')->id, null]), 'IN');
128        $queryMapper->where('clienttype', \array_unique([$request->getData('price_clienttype', 'int'), $client?->getAttribute('clienttype')->id, null]), 'IN');
129        $queryMapper->where('clientcountry', \array_unique([$request->getData('price_clientcountry'), $client?->mainAddress->getCountry(), null]), 'IN');
130
131        $queryMapper->where('supplier', \array_unique([$request->getData('price_supplier', 'int'), null]), 'IN');
132        $queryMapper->where('unit', \array_unique([$request->getData('price_unit', 'int'), null]), 'IN');
133        $queryMapper->where('type', $request->getData('price_type', 'int') ?? PriceType::SALES);
134        $queryMapper->where('currency', \array_unique([$request->getData('price_currency', 'int'), null]), 'IN');
135
136        // @todo: implement start and end
137
138        /*
139        @todo: implement quantity
140        if ($request->hasData('price_quantity')) {
141            $whereQuery = new Where();
142            $whereQuery->where('quantity', (int) $request->getData('price_quantity'), '<=')
143                ->where('quantity', null, '=', 'OR')
144
145            $queryMapper->where('quantity', $whereQuery);
146        }
147        */
148
149        /** @var \Modules\Billing\Models\Price\Price[] $prices */
150        $prices = $queryMapper->execute();
151
152        // Find base price (@todo: probably not a good solution)
153        $bestBasePrice = null;
154        foreach ($prices as $price) {
155            if ($price->price->value !== 0 && $price->priceNew === 0
156                && $price->item->id !== 0
157                && $price->itemgroup->id === 0
158                && $price->itemsegment->id === 0
159                && $price->itemsection->id === 0
160                && $price->itemtype->id === 0
161                && $price->client->id === 0
162                && $price->clientgroup->id === 0
163                && $price->clientsegment->id === 0
164                && $price->clientsection->id === 0
165                && $price->clienttype->id === 0
166                && $price->promocode === ''
167                && $price->price->value < ($bestBasePrice?->price->value ?? \PHP_INT_MAX)
168            ) {
169                $bestBasePrice = $price;
170            }
171        }
172
173        // @todo: implement prices which cannot be improved even if there are better prices available (i.e. some customer groups may not get better prices, Dentagen Beispiel)
174        // alternatively set prices as 'improvable' => which whitelists a price as can be improved or 'alwaysimproces' which always overwrites other prices
175        // Find best price
176        $bestPrice      = null;
177        $bestPriceValue = \PHP_INT_MAX;
178
179        foreach ($prices as $price) {
180            $newPrice = $bestBasePrice?->price->value ?? \PHP_INT_MAX;
181
182            if ($price->price->value < $newPrice) {
183                $newPrice = $price->price->value;
184            }
185
186            if ($price->priceNew < $newPrice) {
187                $newPrice = $price->priceNew;
188            }
189
190            $newPrice -= $price->discount;
191            $newPrice  = (int) ((10000 / $price->discountPercentage) * $newPrice);
192            $newPrice  = (int) (($price->quantity === 0 ? 10000 : $price->quantity) / (10000 + $price->bonus) * $newPrice);
193
194            // @todo: the calculation above regarding discount and bonus don't consider the purchased quantity.
195            // If a customer receives 1+1 but purchases 2, then he gets 2+2 (if multiply === true) which is better than 1+1 with multiply false.
196
197            if ($newPrice < $bestPriceValue) {
198                $bestPriceValue = $newPrice;
199                $bestPrice      = $price;
200            }
201        }
202
203        // Get tax definition
204        /** @var \Modules\Billing\Models\Tax\TaxCombination $tax */
205        $tax = ($request->getDataInt('price_type') ?? PriceType::SALES) === PriceType::SALES
206            ? TaxCombinationMapper::get()
207                ->where('itemCode', $request->getDataInt('price_item'))
208                ->where('clientCode', $account->getAttribute('client_code')->value->id)
209                ->execute()
210            : TaxCombinationMapper::get()
211                ->where('itemCode', $request->getDataInt('price_item'))
212                ->where('supplierCode', $account->getAttribute('supplier_code')->value->id)
213                ->execute();
214
215        $response->header->set('Content-Type', MimeType::M_JSON, true);
216        $response->set(
217            $request->uri->__toString(),
218            \array_values($prices)
219        );
220    }
221
222    /**
223     * Api method to create item bill type
224     *
225     * @param RequestAbstract  $request  Request
226     * @param ResponseAbstract $response Response
227     * @param array            $data     Generic data
228     *
229     * @return void
230     *
231     * @api
232     *
233     * @since 1.0.0
234     */
235    public function apiPriceCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
236    {
237        if (!empty($val = $this->validatePriceCreate($request))) {
238            $response->header->status = RequestStatusCode::R_400;
239            $this->createInvalidCreateResponse($request, $response, $val);
240
241            return;
242        }
243
244        $tax = $this->createPriceFromRequest($request);
245        $this->createModel($request->header->account, $tax, PriceMapper::class, 'price', $request->getOrigin());
246        $this->createStandardCreateResponse($request, $response, $tax);
247    }
248
249    /**
250     * Method to create item attribute from request.
251     *
252     * @param RequestAbstract $request Request
253     *
254     * @return Price
255     *
256     * @since 1.0.0
257     */
258    private function createPriceFromRequest(RequestAbstract $request) : Price
259    {
260        $price            = new Price();
261        $price->name      = $request->getDataString('name') ?? '';
262        $price->promocode = $request->getDataString('promocode') ?? '';
263
264        $price->item        = new NullItem((int) $request->getData('item'));
265        $price->itemgroup   = new NullAttributeValue((int) $request->getData('itemgroup'));
266        $price->itemsegment = new NullAttributeValue((int) $request->getData('itemsegment'));
267        $price->itemsection = new NullAttributeValue((int) $request->getData('itemsection'));
268        $price->itemtype    = new NullAttributeValue((int) $request->getData('itemtype'));
269
270        $price->client        = new NullClient((int) $request->getData('client'));
271        $price->clientgroup   = new NullAttributeValue((int) $request->getData('clientgroup'));
272        $price->clientsegment = new NullAttributeValue((int) $request->getData('clientsegment'));
273        $price->clientsection = new NullAttributeValue((int) $request->getData('clientsection'));
274        $price->clienttype    = new NullAttributeValue((int) $request->getData('clienttype'));
275
276        $price->supplier           = new NullSupplier((int) $request->getData('supplier'));
277        $price->unit               = (int) $request->getData('unit');
278        $price->type               = $request->getDataInt('type') ?? PriceType::SALES;
279        $price->quantity           = (int) $request->getData('quantity');
280        $price->price              = new FloatInt((int) $request->getData('price'));
281        $price->priceNew           = (int) $request->getData('price_new');
282        $price->discount           = (int) $request->getData('discount');
283        $price->discountPercentage = (int) $request->getData('discountPercentage');
284        $price->bonus              = (int) $request->getData('bonus');
285        $price->multiply           = $request->getDataBool('multiply') ?? false;
286        $price->currency           = $request->getDataString('currency') ?? ISO4217CharEnum::_EUR;
287        $price->start              = $request->getDataDateTime('start') ?? null;
288        $price->end                = $request->getDataDateTime('end') ?? null;
289
290        return $price;
291    }
292
293    /**
294     * Validate item attribute create request
295     *
296     * @param RequestAbstract $request Request
297     *
298     * @return array<string, bool>
299     *
300     * @todo: consider to prevent name 'base'?
301     * Might not be possible because it is used internally as well (see apiItemCreate in ItemManagement)
302     *
303     * @since 1.0.0
304     */
305    private function validatePriceCreate(RequestAbstract $request) : array
306    {
307        $val = [];
308        if (($val['name'] = !$request->hasData('name'))) {
309            return $val;
310        }
311
312        return [];
313    }
314
315    /**
316     * Api method to update Price
317     *
318     * @param RequestAbstract  $request  Request
319     * @param ResponseAbstract $response Response
320     * @param array            $data     Generic data
321     *
322     * @return void
323     *
324     * @api
325     *
326     * @since 1.0.0
327     */
328    public function apiPriceUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
329    {
330        if (!empty($val = $this->validatePriceUpdate($request))) {
331            $response->header->status = RequestStatusCode::R_400;
332            $this->createInvalidUpdateResponse($request, $response, $val);
333
334            return;
335        }
336
337        /** @var \Modules\Billing\Models\Price\Price $old */
338        $old = PriceMapper::get()->where('id', (int) $request->getData('id'))->execute();
339        $new = $this->updatePriceFromRequest($request, clone $old);
340
341        $this->updateModel($request->header->account, $old, $new, PriceMapper::class, 'price', $request->getOrigin());
342        $this->createStandardUpdateResponse($request, $response, $new);
343    }
344
345    /**
346     * Method to update Price from request.
347     *
348     * @param RequestAbstract $request Request
349     * @param Price           $new     Model to modify
350     *
351     * @return Price
352     *
353     * @since 1.0.0
354     */
355    public function updatePriceFromRequest(RequestAbstract $request, Price $new) : Price
356    {
357        $new->name = $new->name !== 'base'
358            ? ($request->getDataString('name') ?? $new->name)
359            : $new->name;
360
361        $new->promocode = $request->getDataString('promocode') ?? $new->promocode;
362
363        $new->item        = $request->hasData('item') ? new NullItem((int) $request->getData('item')) : $new->item;
364        $new->itemgroup   = $request->hasData('itemgroup') ? new NullAttributeValue((int) $request->getData('itemgroup')) : $new->itemgroup;
365        $new->itemsegment = $request->hasData('itemsegment') ? new NullAttributeValue((int) $request->getData('itemsegment')) : $new->itemsegment;
366        $new->itemsection = $request->hasData('itemsection') ? new NullAttributeValue((int) $request->getData('itemsection')) : $new->itemsection;
367        $new->itemtype    = $request->hasData('itemtype') ? new NullAttributeValue((int) $request->getData('itemtype')) : $new->itemtype;
368
369        $new->client        = $request->hasData('client') ? new NullClient((int) $request->getData('client')) : $new->client;
370        $new->clientgroup   = $request->hasData('clientgroup') ? new NullAttributeValue((int) $request->getData('clientgroup')) : $new->clientgroup;
371        $new->clientsegment = $request->hasData('clientsegment') ? new NullAttributeValue((int) $request->getData('clientsegment')) : $new->clientsegment;
372        $new->clientsection = $request->hasData('clientsection') ? new NullAttributeValue((int) $request->getData('clientsection')) : $new->clientsection;
373        $new->clienttype    = $request->hasData('clienttype') ? new NullAttributeValue((int) $request->getData('clienttype')) : $new->clienttype;
374
375        $new->supplier           = $request->hasData('supplier') ? new NullSupplier((int) $request->getData('supplier')) : $new->supplier;
376        $new->unit               = $request->getDataInt('unit') ?? $new->unit;
377        $new->type               = $request->getDataInt('type') ?? $new->type;
378        $new->quantity           = $request->getDataInt('quantity') ?? $new->quantity;
379        $new->price              = $request->hasData('price') ? new FloatInt((int) $request->getData('price')) : $new->price;
380        $new->priceNew           = $request->getDataInt('price_new') ?? $new->priceNew;
381        $new->discount           = $request->getDataInt('discount') ?? $new->discount;
382        $new->discountPercentage = $request->getDataInt('discountPercentage') ?? $new->discountPercentage;
383        $new->bonus              = $request->getDataInt('bonus') ?? $new->bonus;
384        $new->multiply           = $request->getDataBool('multiply') ?? $new->multiply;
385        $new->currency           = $request->getDataString('currency') ?? $new->currency;
386        $new->start              = $request->getDataDateTime('start') ?? $new->start;
387        $new->end                = $request->getDataDateTime('end') ?? $new->end;
388
389        return $new;
390    }
391
392    /**
393     * Validate Price update request
394     *
395     * @param RequestAbstract $request Request
396     *
397     * @return array<string, bool>
398     *
399     * @todo: implement
400     * @todo: consider to block 'base' name
401     *
402     * @since 1.0.0
403     */
404    private function validatePriceUpdate(RequestAbstract $request) : array
405    {
406        $val = [];
407        if (($val['id'] = !$request->hasData('id'))) {
408            return $val;
409        }
410
411        return [];
412    }
413
414    /**
415     * Api method to delete Price
416     *
417     * @param RequestAbstract  $request  Request
418     * @param ResponseAbstract $response Response
419     * @param array            $data     Generic data
420     *
421     * @return void
422     *
423     * @api
424     *
425     * @since 1.0.0
426     */
427    public function apiPriceDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
428    {
429        if (!empty($val = $this->validatePriceDelete($request))) {
430            $response->header->status = RequestStatusCode::R_400;
431            $this->createInvalidDeleteResponse($request, $response, $val);
432
433            return;
434        }
435
436        /** @var \Modules\Billing\Models\Price\Price $price */
437        $price = PriceMapper::get()->where('id', (int) $request->getData('id'))->execute();
438
439        if ($price->name === 'base') {
440            // default price cannot be deleted
441            $this->createInvalidDeleteResponse($request, $response, []);
442
443            return;
444        }
445
446        $this->deleteModel($request->header->account, $price, PriceMapper::class, 'price', $request->getOrigin());
447        $this->createStandardDeleteResponse($request, $response, $price);
448    }
449
450    /**
451     * Validate Price delete request
452     *
453     * @param RequestAbstract $request Request
454     *
455     * @return array<string, bool>
456     *
457     * @since 1.0.0
458     */
459    private function validatePriceDelete(RequestAbstract $request) : array
460    {
461        $val = [];
462        if (($val['id'] = !$request->hasData('id'))) {
463            return $val;
464        }
465
466        return [];
467    }
468}