Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 131
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiTaxController
0.00% covered (danger)
0.00%
0 / 131
0.00% covered (danger)
0.00%
0 / 12
1560
0.00% covered (danger)
0.00%
0 / 1
 getTaxCodeFromClientItem
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
12
 apiTaxCombinationCreate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 createTaxCombinationFromRequest
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 validateTaxCombinationCreate
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 getClientTaxCode
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 apiTaxCombinationUpdate
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 apiChangeDefaultTaxCombinations
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
42
 validateDefaultTaxCombinationChange
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 updateTaxCombinationFromRequest
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 validateTaxCombinationUpdate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiTaxCombinationDelete
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 validateTaxCombinationDelete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   Modules\Billing
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace Modules\Billing\Controller;
16
17use Modules\Admin\Models\Address;
18use Modules\Attribute\Models\AttributeValue;
19use Modules\Attribute\Models\NullAttributeValue;
20use Modules\Billing\Models\Tax\TaxCombination;
21use Modules\Billing\Models\Tax\TaxCombinationMapper;
22use Modules\ClientManagement\Models\Attribute\ClientAttributeTypeMapper;
23use Modules\ClientManagement\Models\Client;
24use Modules\Finance\Models\TaxCode;
25use Modules\Finance\Models\TaxCodeMapper;
26use Modules\ItemManagement\Models\Item;
27use Modules\Organization\Models\UnitMapper;
28use phpOMS\Localization\ISO3166CharEnum;
29use phpOMS\Message\Http\RequestStatusCode;
30use phpOMS\Message\RequestAbstract;
31use phpOMS\Message\ResponseAbstract;
32use phpOMS\Security\Guard;
33
34/**
35 * Billing class.
36 *
37 * @package Modules\Billing
38 * @license OMS License 2.0
39 * @link    https://jingga.app
40 * @since   1.0.0
41 */
42final class ApiTaxController extends Controller
43{
44    /**
45     * Get tax code from client and item.
46     *
47     * @param Client $client         Client to get tax code from
48     * @param Item   $item           Item toget tax code from
49     * @param string $defaultCountry default country to use if no valid tax code could be found and if the unit country code shouldn't be used
50     *
51     * @return TaxCode
52     *
53     * @since 1.0.0
54     */
55    public function getTaxCodeFromClientItem(Client $client, Item $item, string $defaultCountry = '') : TaxCode
56    {
57        // @todo: define default sales tax code if none available?!
58        // @todo: consider to actually use a ownsOne reference instead of only a string, this way the next line with the TaxCodeMapper can be removed
59
60        /** @var \Modules\Billing\Models\Tax\TaxCombination $taxCombination */
61        $taxCombination = TaxCombinationMapper::get()
62            ->where('itemCode', $item->getAttribute('sales_tax_code')->value->id)
63            ->where('clientCode', $client->getAttribute('sales_tax_code')->value->id)
64            ->execute();
65
66        /** @var \Modules\Finance\Models\TaxCode $taxCode */
67        $taxCode = TaxCodeMapper::get()
68            ->where('abbr', $taxCombination->taxCode)
69            ->execute();
70
71        if ($taxCode->id !== 0) {
72            return $taxCode;
73        }
74
75        /** @var \Modules\Organization\Models\Unit $unit */
76        $unit = UnitMapper::get()
77            ->with('mainAddress')
78            ->where('id', $this->app->unitId)
79            ->execute();
80
81        // Create dummy client
82        $client              = new Client();
83        $client->mainAddress =  $unit->mainAddress;
84
85        if (!empty($defaultCountry)) {
86            $client->mainAddress->setCountry($defaultCountry);
87        }
88
89        $taxCodeAttribute = $this->getClientTaxCode($client,  $unit->mainAddress);
90
91        /** @var \Modules\Billing\Models\Tax\TaxCombination $taxCombination */
92        $taxCombination = TaxCombinationMapper::get()
93            ->where('itemCode', $item->getAttribute('sales_tax_code')->value->id)
94            ->where('clientCode', $taxCodeAttribute->id)
95            ->execute();
96
97        /** @var \Modules\Finance\Models\TaxCode $taxCode */
98        $taxCode = TaxCodeMapper::get()
99            ->where('abbr', $taxCombination->taxCode)
100            ->execute();
101
102        return $taxCode;
103    }
104
105    /**
106     * Create a tax combination for a client and item
107     *
108     * @param RequestAbstract  $request  Request
109     * @param ResponseAbstract $response Response
110     * @param array            $data     Data
111     *
112     * @return void
113     *
114     * @since 1.0.0
115     */
116    public function apiTaxCombinationCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
117    {
118        if (!empty($val = $this->validateTaxCombinationCreate($request))) {
119            $response->header->status = RequestStatusCode::R_400;
120            $this->createInvalidCreateResponse($request, $response, $val);
121
122            return;
123        }
124
125        $tax = $this->createTaxCombinationFromRequest($request);
126        $this->createModel($request->header->account, $tax, TaxCombinationMapper::class, 'tax_combination', $request->getOrigin());
127        $this->createStandardCreateResponse($request, $response, $tax);
128    }
129
130    /**
131     * Method to create item attribute from request.
132     *
133     * @param RequestAbstract $request Request
134     *
135     * @return TaxCombination
136     *
137     * @since 1.0.0
138     */
139    private function createTaxCombinationFromRequest(RequestAbstract $request) : TaxCombination
140    {
141        $tax           = new TaxCombination();
142        $tax->taxType  = $request->getDataInt('tax_type') ?? 1;
143        $tax->taxCode  = (string) $request->getData('tax_code');
144        $tax->itemCode = new NullAttributeValue((int) $request->getData('item_code'));
145
146        if ($tax->taxType === 1) {
147            $tax->clientCode = new NullAttributeValue((int) $request->getData('account_code'));
148        } else {
149            $tax->supplierCode = new NullAttributeValue((int) $request->getData('account_code'));
150        }
151
152        return $tax;
153    }
154
155    /**
156     * Validate item attribute create request
157     *
158     * @param RequestAbstract $request Request
159     *
160     * @return array<string, bool>
161     *
162     * @since 1.0.0
163     */
164    private function validateTaxCombinationCreate(RequestAbstract $request) : array
165    {
166        $val = [];
167        if (($val['tax_type'] = !$request->hasData('tax_type'))
168            || ($val['tax_code'] = !$request->hasData('tax_code'))
169            || ($val['item_code'] = !$request->hasData('item_code'))
170            || ($val['account_code'] = !$request->hasData('account_code'))
171        ) {
172            return $val;
173        }
174
175        return [];
176    }
177
178    /**
179     * Get the client's tax code based on their country and tax office address
180     *
181     * @param Client  $client           The client to get the tax code for
182     * @param Address $taxOfficeAddress The tax office address used to determine the tax code
183     *
184     * @return AttributeValue The client's tax code
185     *
186     * @since 1.0.0
187     */
188    public function getClientTaxCode(Client $client, Address $taxOfficeAddress) : AttributeValue
189    {
190        /** @var \Modules\Attribute\Models\AttributeType $codes */
191        $codes = ClientAttributeTypeMapper::get()
192            ->with('defaults')
193            ->where('name', 'sales_tax_code')
194            ->execute();
195
196        $taxCode = new NullAttributeValue();
197
198        // @todo: need to consider own tax id as well
199        if ($taxOfficeAddress->getCountry() === $client->mainAddress->getCountry()) {
200            $taxCode = $codes->getDefaultByValue($client->mainAddress->getCountry());
201        } elseif (\in_array($taxOfficeAddress->getCountry(), ISO3166CharEnum::getRegion('eu'))
202            && \in_array($client->mainAddress->getCountry(), ISO3166CharEnum::getRegion('eu'))
203        ) {
204            if (!empty($client->getAttribute('vat_id')->value->getValue())) {
205                // Is EU company
206                $taxCode = $codes->getDefaultByValue('EU');
207            } else {
208                // Is EU private customer
209                $taxCode = $codes->getDefaultByValue($client->mainAddress->getCountry());
210            }
211        } elseif (\in_array($taxOfficeAddress->getCountry(), ISO3166CharEnum::getRegion('eu'))) {
212            // None EU company but we are EU company
213            $taxCode = $codes->getDefaultByValue('INT');
214        } else {
215            // None EU company and we are also none EU company
216            $taxCode = $codes->getDefaultByValue('INT');
217        }
218
219        return $taxCode;
220    }
221
222    /**
223     * Api method to update TaxCombination
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 apiTaxCombinationUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
236    {
237        if (!empty($val = $this->validateTaxCombinationUpdate($request))) {
238            $response->header->status = RequestStatusCode::R_400;
239            $this->createInvalidUpdateResponse($request, $response, $val);
240
241            return;
242        }
243
244        /** @var \Modules\Billing\Models\Tax\TaxCombination $old */
245        $old = TaxCombinationMapper::get()->where('id', (int) $request->getData('id'))->execute();
246        $new = $this->updateTaxCombinationFromRequest($request, clone $old);
247
248        $this->updateModel($request->header->account, $old, $new, TaxCombinationMapper::class, 'tax_combination', $request->getOrigin());
249        $this->createStandardUpdateResponse($request, $response, $new);
250    }
251
252    /**
253     * Api method to update TaxCombination
254     *
255     * @param RequestAbstract  $request  Request
256     * @param ResponseAbstract $response Response
257     * @param array            $data     Generic data
258     *
259     * @return void
260     *
261     * @api
262     *
263     * @since 1.0.0
264     */
265    public function apiChangeDefaultTaxCombinations(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
266    {
267        if (!empty($val = $this->validateDefaultTaxCombinationChange($request))) {
268            $response->header->status = RequestStatusCode::R_400;
269            $this->createInvalidUpdateResponse($request, $response, $val);
270
271            return;
272        }
273
274        if (!Guard::isSafePath(
275                $path = __DIR__ . '/../Admin/Install/Taxes/' . $request->getDataString('type') . '.json',
276                __DIR__ . '/../Admin/Install/Taxes'
277            )
278        ) {
279            $this->createInvalidUpdateResponse($request, $response, []);
280
281            return;
282        }
283
284        $content = \file_get_contents($path);
285        if ($content === false) {
286            $this->createInvalidUpdateResponse($request, $response, []);
287
288            return;
289        }
290
291        /** @var array $combinations */
292        $combinations = \json_decode($content, true);
293
294        foreach ($combinations as $combination) {
295            /** @var TaxCombination[] $old */
296            $old = TaxCombinationMapper::getAll()
297                ->with('clientCode')
298                ->with('itemCode')
299                ->where('clientCode/valueStr', $combination['account_code'] ?? '')
300                ->where('itemCode/valueStr', $combination['item_code'] ?? '')
301                ->execute();
302
303            if (\count($old) !== 1) {
304                continue;
305            }
306
307            $old = \reset($old);
308
309            $new          = clone $old;
310            $new->taxCode = $combination['tax_code'] ?? '';
311
312            $this->updateModel($request->header->account, $old, $new, TaxCombinationMapper::class, 'tax_combination', $request->getOrigin());
313        }
314
315        $this->createStandardUpdateResponse($request, $response, []);
316    }
317
318    /**
319     * Validate TaxCombination update request
320     *
321     * @param RequestAbstract $request Request
322     *
323     * @return array<string, bool>
324     *
325     * @since 1.0.0
326     */
327    private function validateDefaultTaxCombinationChange(RequestAbstract $request) : array
328    {
329        $val = [];
330        if (($val['type'] = !$request->hasData('type'))) {
331            return $val;
332        }
333
334        return [];
335    }
336
337    /**
338     * Method to update TaxCombination from request.
339     *
340     * @param RequestAbstract $request Request
341     * @param TaxCombination  $new     Model to modify
342     *
343     * @return TaxCombination
344     *
345     * @since 1.0.0
346     */
347    public function updateTaxCombinationFromRequest(RequestAbstract $request, TaxCombination $new) : TaxCombination
348    {
349        $new->taxType  = $request->getDataInt('tax_type') ?? $new->taxType;
350        $new->taxCode  = $request->getDataString('tax_code') ?? $new->taxCode;
351        $new->itemCode = $request->hasData('item_code') ? new NullAttributeValue((int) $request->getData('item_code')) : $new->itemCode;
352
353        if ($new->taxType === 1) {
354            $new->clientCode = $request->hasData('account_code') ? new NullAttributeValue((int) $request->getData('account_code')) : $new->clientCode;
355        } else {
356            $new->supplierCode = $request->hasData('account_code') ? new NullAttributeValue((int) $request->getData('account_code')) : $new->supplierCode;
357        }
358
359        return $new;
360    }
361
362    /**
363     * Validate TaxCombination update request
364     *
365     * @param RequestAbstract $request Request
366     *
367     * @return array<string, bool>
368     *
369     * @since 1.0.0
370     */
371    private function validateTaxCombinationUpdate(RequestAbstract $request) : array
372    {
373        $val = [];
374        if (($val['id'] = !$request->hasData('id'))) {
375            return $val;
376        }
377
378        return [];
379    }
380
381    /**
382     * Api method to delete TaxCombination
383     *
384     * @param RequestAbstract  $request  Request
385     * @param ResponseAbstract $response Response
386     * @param array            $data     Generic data
387     *
388     * @return void
389     *
390     * @api
391     *
392     * @since 1.0.0
393     */
394    public function apiTaxCombinationDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
395    {
396        if (!empty($val = $this->validateTaxCombinationDelete($request))) {
397            $response->header->status = RequestStatusCode::R_400;
398            $this->createInvalidDeleteResponse($request, $response, $val);
399
400            return;
401        }
402
403        /** @var \Modules\Billing\Models\Tax\TaxCombination $taxCombination */
404        $taxCombination = TaxCombinationMapper::get()->where('id', (int) $request->getData('id'))->execute();
405        $this->deleteModel($request->header->account, $taxCombination, TaxCombinationMapper::class, 'tax_combination', $request->getOrigin());
406        $this->createStandardDeleteResponse($request, $response, $taxCombination);
407    }
408
409    /**
410     * Validate TaxCombination delete request
411     *
412     * @param RequestAbstract $request Request
413     *
414     * @return array<string, bool>
415     *
416     * @todo: implement
417     *
418     * @since 1.0.0
419     */
420    private function validateTaxCombinationDelete(RequestAbstract $request) : array
421    {
422        $val = [];
423        if (($val['id'] = !$request->hasData('id'))) {
424            return $val;
425        }
426
427        return [];
428    }
429}