Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 131 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
ApiTaxController | |
0.00% |
0 / 131 |
|
0.00% |
0 / 12 |
1560 | |
0.00% |
0 / 1 |
getTaxCodeFromClientItem | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
12 | |||
apiTaxCombinationCreate | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
createTaxCombinationFromRequest | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
validateTaxCombinationCreate | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
getClientTaxCode | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
42 | |||
apiTaxCombinationUpdate | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
apiChangeDefaultTaxCombinations | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
42 | |||
validateDefaultTaxCombinationChange | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
updateTaxCombinationFromRequest | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
validateTaxCombinationUpdate | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
apiTaxCombinationDelete | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
validateTaxCombinationDelete | |
0.00% |
0 / 4 |
|
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 | */ |
13 | declare(strict_types=1); |
14 | |
15 | namespace Modules\Billing\Controller; |
16 | |
17 | use Modules\Admin\Models\Address; |
18 | use Modules\Attribute\Models\AttributeValue; |
19 | use Modules\Attribute\Models\NullAttributeValue; |
20 | use Modules\Billing\Models\Tax\TaxCombination; |
21 | use Modules\Billing\Models\Tax\TaxCombinationMapper; |
22 | use Modules\ClientManagement\Models\Attribute\ClientAttributeTypeMapper; |
23 | use Modules\ClientManagement\Models\Client; |
24 | use Modules\Finance\Models\TaxCode; |
25 | use Modules\Finance\Models\TaxCodeMapper; |
26 | use Modules\ItemManagement\Models\Item; |
27 | use Modules\Organization\Models\UnitMapper; |
28 | use phpOMS\Localization\ISO3166CharEnum; |
29 | use phpOMS\Message\Http\RequestStatusCode; |
30 | use phpOMS\Message\RequestAbstract; |
31 | use phpOMS\Message\ResponseAbstract; |
32 | use 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 | */ |
42 | final 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 | } |