Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
127 / 127
100.00% covered (success)
100.00%
15 / 15
CRAP
100.00% covered (success)
100.00%
1 / 1
TaxHtmxController
100.00% covered (success)
100.00%
126 / 126
100.00% covered (success)
100.00%
15 / 15
25
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tabRates
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 tabGroups
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 tabRules
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 tabSettings
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 tabSimulator
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 rateModal
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 rateSave
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 rateToggle
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 rateDelete
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 ruleModal
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 ruleSave
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 ruleDelete
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 simulate
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
1 / 1
4
 htmlResponse
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Modules\Tax\Presentation\Htmx;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Tax\Application\DTO\CalculateDocumentRequest;
12use App\Modules\Tax\Application\DTO\TaxGroupDto;
13use App\Modules\Tax\Application\DTO\TaxRateDto;
14use App\Modules\Tax\Application\DTO\TaxRuleDto;
15use App\Modules\Tax\Application\Service\TaxApplicationService;
16use Nyholm\Psr7\Factory\Psr17Factory;
17use Psr\Http\Message\ResponseInterface;
18use Psr\Http\Message\ServerRequestInterface;
19use Throwable;
20use Twig\Environment as TwigEnvironment;
21
22/**
23 * HTMX Controller for Tax management interface partials and modal interactions.
24 */
25final readonly class TaxHtmxController
26{
27    private const string HTML_CONTENT_TYPE = 'text/html; charset=utf-8';
28
29    /**
30     * TaxHtmxController constructor.
31     */
32    public function __construct(
33        private TaxApplicationService $taxService,
34        private TwigEnvironment $twig,
35        private Psr17Factory $psr17Factory,
36    ) {
37    }
38
39    /**
40     * Render Rates tab content (GET /htmx/tax/tab-rates).
41     */
42    public function tabRates(): ResponseInterface
43    {
44        $rates = $this->taxService->getTaxRates();
45        $html = $this->twig->render('tax/partials/rates_tab.twig', [
46            'rates' => $rates,
47        ]);
48
49        return $this->htmlResponse($html);
50    }
51
52    /**
53     * Render Groups tab content (GET /htmx/tax/tab-groups).
54     */
55    public function tabGroups(): ResponseInterface
56    {
57        $groups = $this->taxService->getTaxGroups();
58        $rates = $this->taxService->getTaxRates(['is_active' => 1]);
59        $html = $this->twig->render('tax/partials/groups_tab.twig', [
60            'groups' => $groups,
61            'rates' => $rates,
62        ]);
63
64        return $this->htmlResponse($html);
65    }
66
67    /**
68     * Render Rules tab content (GET /htmx/tax/tab-rules).
69     */
70    public function tabRules(): ResponseInterface
71    {
72        $rules = $this->taxService->getTaxRules();
73        $rates = $this->taxService->getTaxRates(['is_active' => 1]);
74        $html = $this->twig->render('tax/partials/rules_tab.twig', [
75            'rules' => $rules,
76            'rates' => $rates,
77        ]);
78
79        return $this->htmlResponse($html);
80    }
81
82    /**
83     * Render Engine Settings tab (GET /htmx/tax/tab-settings).
84     */
85    public function tabSettings(): ResponseInterface
86    {
87        $html = $this->twig->render('tax/partials/settings_tab.twig');
88
89        return $this->htmlResponse($html);
90    }
91
92    /**
93     * Render Live Simulator tab (GET /htmx/tax/tab-simulator).
94     */
95    public function tabSimulator(): ResponseInterface
96    {
97        $rates = $this->taxService->getTaxRates(['is_active' => 1]);
98        $html = $this->twig->render('tax/partials/simulator_tab.twig', [
99            'rates' => $rates,
100        ]);
101
102        return $this->htmlResponse($html);
103    }
104
105    /**
106     * Render Rate Modal for Create or Edit (GET /htmx/tax/rate-modal/{id:\d+}?).
107     */
108    public function rateModal(ServerRequestInterface $request): ResponseInterface
109    {
110        $id = (int) ($request->getAttribute('id') ?? 0);
111        $rate = $id > 0 ? $this->taxService->getTaxRate($id) : null;
112
113        $html = $this->twig->render('tax/partials/rate_modal.twig', [
114            'rate' => $rate,
115        ]);
116
117        return $this->htmlResponse($html);
118    }
119
120    /**
121     * Save Rate via HTMX Form (POST /htmx/tax/rate-save).
122     */
123    public function rateSave(ServerRequestInterface $request): ResponseInterface
124    {
125        $data = $request->getParsedBody();
126        $body = is_array($data) ? $data : [];
127
128        try {
129            $dto = TaxRateDto::fromArray($body);
130            $this->taxService->saveTaxRate($dto);
131
132            return $this->tabRates($request);
133        } catch (Throwable $e) {
134            return $this->htmlResponse(
135                "<div class='alert alert-danger mb-3'>" . $e->getMessage() . "</div>",
136                400
137            );
138        }
139    }
140
141    /**
142     * Toggle Rate Active Status (POST /htmx/tax/rate-toggle/{id:\d+}).
143     */
144    public function rateToggle(ServerRequestInterface $request): ResponseInterface
145    {
146        $id = (int) ($request->getAttribute('id') ?? 0);
147        $rate = $this->taxService->getTaxRate($id);
148
149        if ($rate !== null) {
150            $rate->isActive = !$rate->isActive;
151            $dto = new TaxRateDto(
152                id: $rate->id,
153                taxCode: $rate->taxCode,
154                taxName: $rate->taxName,
155                ratePercent: $rate->ratePercent,
156                calculationMode: $rate->calculationMode->value,
157                taxStatus: $rate->taxStatus->value,
158                legalExemptionNote: $rate->legalExemptionNote,
159                fiscalCode: $rate->fiscalCode,
160                isDefault: $rate->isDefault,
161                isActive: $rate->isActive,
162            );
163            $this->taxService->saveTaxRate($dto);
164        }
165
166        return $this->tabRates($request);
167    }
168
169    /**
170     * Delete Rate (DELETE /htmx/tax/rate-delete/{id:\d+}).
171     */
172    public function rateDelete(ServerRequestInterface $request): ResponseInterface
173    {
174        $id = (int) ($request->getAttribute('id') ?? 0);
175        $this->taxService->deleteTaxRate($id);
176
177        return $this->tabRates($request);
178    }
179
180    /**
181     * Render Rule Modal (GET /htmx/tax/rule-modal/{id:\d+}?).
182     */
183    public function ruleModal(ServerRequestInterface $request): ResponseInterface
184    {
185        $id = (int) ($request->getAttribute('id') ?? 0);
186        $rule = $id > 0 ? $this->taxService->getTaxRule($id) : null;
187        $rates = $this->taxService->getTaxRates(['is_active' => 1]);
188
189        $html = $this->twig->render('tax/partials/rule_modal.twig', [
190            'rule' => $rule,
191            'rates' => $rates,
192        ]);
193
194        return $this->htmlResponse($html);
195    }
196
197    /**
198     * Save Rule (POST /htmx/tax/rule-save).
199     */
200    public function ruleSave(ServerRequestInterface $request): ResponseInterface
201    {
202        $data = $request->getParsedBody();
203        $body = is_array($data) ? $data : [];
204
205        try {
206            $dto = TaxRuleDto::fromArray($body);
207            $this->taxService->saveTaxRule($dto);
208
209            return $this->tabRules($request);
210        } catch (Throwable $e) {
211            return $this->htmlResponse("<div class='alert alert-danger mb-3'>" . $e->getMessage() . "</div>", 400);
212        }
213    }
214
215    /**
216     * Delete Rule (DELETE /htmx/tax/rule-delete/{id:\d+}).
217     */
218    public function ruleDelete(ServerRequestInterface $request): ResponseInterface
219    {
220        $id = (int) ($request->getAttribute('id') ?? 0);
221        $this->taxService->deleteTaxRule($id);
222
223        return $this->tabRules($request);
224    }
225
226    /**
227     * Execute live simulation via HTMX (POST /htmx/tax/simulate).
228     */
229    public function simulate(ServerRequestInterface $request): ResponseInterface
230    {
231        $data = $request->getParsedBody();
232        $body = is_array($data) ? $data : [];
233
234        $price = (float) ($body['unit_price'] ?? 100.0);
235        $qty = (float) ($body['quantity'] ?? 1.0);
236        $discount = (float) ($body['discount'] ?? 0.0);
237        $costPrice = !empty($body['cost_price']) ? (float) $body['cost_price'] : null;
238        $taxRateId = !empty($body['tax_rate_id']) ? (int) $body['tax_rate_id'] : null;
239        $customerType = (string) ($body['customer_type'] ?? 'all');
240        $geoZone = (string) ($body['geo_zone'] ?? 'domestic');
241        $itemType = (string) ($body['item_type'] ?? 'product');
242        $shipping = (float) ($body['shipping_net'] ?? 0.0);
243        $globalDiscount = (float) ($body['global_discount'] ?? 0.0);
244        $deposits = (float) ($body['returnable_deposits'] ?? 0.0);
245        $strategy = (string) ($body['rounding_strategy'] ?? 'line_level');
246        $method = (string) ($body['rounding_method'] ?? 'half_up');
247
248        $req = new CalculateDocumentRequest(
249            items: [
250                [
251                    'unit_price' => $price,
252                    'quantity' => $qty,
253                    'discount' => $discount,
254                    'tax_rate_id' => $taxRateId,
255                    'cost_price' => $costPrice,
256                    'item_type' => $itemType,
257                ]
258            ],
259            customerType: $customerType,
260            geoZone: $geoZone,
261            globalDiscount: $globalDiscount,
262            shippingNet: $shipping,
263            returnableDeposits: $deposits,
264            roundingStrategy: $strategy,
265            roundingMethod: $method
266        );
267
268        $summary = $this->taxService->calculateDocument($req);
269
270        $html = $this->twig->render('tax/partials/simulator_result.twig', [
271            'summary' => $summary,
272            'json' => json_encode($summary->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
273        ]);
274
275        return $this->htmlResponse($html);
276    }
277
278    /**
279     * Helper to create PSR-7 HTML response.
280     */
281    private function htmlResponse(string $html, int $status = 200): ResponseInterface
282    {
283        $response = $this->psr17Factory->createResponse($status)
284            ->withHeader('Content-Type', self::HTML_CONTENT_TYPE);
285        $response->getBody()->write($html);
286
287        return $response;
288    }
289}