Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.69% covered (success)
94.69%
232 / 245
68.75% covered (warning)
68.75%
11 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommerceRouteDispatcher
94.67% covered (success)
94.67%
231 / 244
68.75% covered (warning)
68.75%
11 / 16
110.80
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canDispatch
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
14
 dispatch
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
18
 dispatchTaxWebRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 dispatchDiscountWebRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 dispatchDataMappingWebRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 dispatchInventoryFieldsWebRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 dispatchQuoteConversionRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 dispatchHtmxTaxRoutes
89.19% covered (warning)
89.19%
33 / 37
0.00% covered (danger)
0.00%
0 / 1
22.61
 dispatchHtmxDiscountRoutes
88.89% covered (warning)
88.89%
24 / 27
0.00% covered (danger)
0.00%
0 / 1
13.23
 dispatchHtmxInventoryFieldRoutes
88.46% covered (warning)
88.46%
23 / 26
0.00% covered (danger)
0.00%
0 / 1
11.19
 dispatchHtmxInventoryItemRoutes
92.59% covered (success)
92.59%
25 / 27
0.00% covered (danger)
0.00%
0 / 1
8.03
 dispatchHtmxDataMappingRoutes
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
6.01
 dispatchCurrenciesWebRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 dispatchCurrenciesAddRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 dispatchCurrenciesDeleteRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
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\Core\Bootstrap\Dispatcher;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\User\Presentation\Middleware\AuthenticatedOnlyMiddleware;
12use App\Modules\DataMapping\Presentation\Htmx\DataMappingHtmxController;
13use App\Modules\DataMapping\Presentation\Web\DataMappingWebController;
14use App\Modules\Discount\Presentation\Htmx\DiscountHtmxController;
15use App\Modules\Discount\Presentation\Web\DiscountSettingsWebController;
16use App\Modules\Inventory\Presentation\Htmx\InventoryItemsHtmxController;
17use App\Modules\Inventory\Presentation\Htmx\InventorySettingsHtmxController;
18use App\Modules\Inventory\Presentation\Web\InventorySettingsWebController;
19use App\Modules\Currencies\Presentation\Web\CurrenciesWebController;
20use App\Modules\Quotes\Presentation\Web\QuoteConversionWebController;
21use App\Modules\Tax\Presentation\Htmx\TaxHtmxController;
22use App\Modules\Tax\Presentation\Web\TaxSettingsWebController;
23use Nyholm\Psr7\Factory\Psr17Factory;
24use Psr\Http\Message\ResponseInterface;
25use Psr\Http\Message\ServerRequestInterface;
26use Psr\Http\Server\RequestHandlerInterface;
27
28/**
29 * Modular route dispatcher for Commerce subsystem:
30 * Taxes, Discounts, Inventory Fields & Items, Data Mapping, Quote conversion, and Currencies.
31 */
32final readonly class CommerceRouteDispatcher implements ModuleRouteDispatcherInterface
33{
34    public function __construct(
35        private ?TaxSettingsWebController $taxWebController,
36        private ?TaxHtmxController $taxHtmxController,
37        private ?DiscountSettingsWebController $discountWebController,
38        private ?DiscountHtmxController $discountHtmxController,
39        private ?InventorySettingsWebController $inventoryWebController,
40        private ?InventorySettingsHtmxController $inventoryFieldsHtmxController,
41        private ?InventoryItemsHtmxController $inventoryItemsHtmxController,
42        private ?DataMappingWebController $dataMappingWebController,
43        private ?DataMappingHtmxController $dataMappingHtmxController,
44        private ?QuoteConversionWebController $quoteConversionWebController,
45        private AuthenticatedOnlyMiddleware $authMiddleware,
46        private Psr17Factory $psr17Factory,
47        private ?CurrenciesWebController $currenciesWebController = null
48    ) {
49    }
50
51    public function canDispatch(string $path): bool
52    {
53        return $path === '/settings/tax'
54            || $path === '/settings/discount'
55            || $path === '/settings/data-mapping'
56            || $path === '/settings/currencies'
57            || $path === '/settings/currencies/add'
58            || $path === '/settings/currencies/delete'
59            || $path === '/tools/currencies'
60            || str_starts_with($path, '/htmx/tax/')
61            || str_starts_with($path, '/htmx/discount/')
62            || str_starts_with($path, '/htmx/inventory-fields/')
63            || str_starts_with($path, '/htmx/inventory/')
64            || str_starts_with($path, '/htmx/data-mapping/')
65            || (bool) preg_match('#^/settings/modules/(\d+)/inventory-fields$#', $path)
66            || (bool) preg_match('#^/quotes/(\d+)/convert-to-order$#', $path);
67    }
68
69    public function dispatch(string $path, ServerRequestInterface $request): ?ResponseInterface
70    {
71        return match (true) {
72            $path === '/settings/tax' => $this->dispatchTaxWebRoute($request),
73            $path === '/settings/discount' => $this->dispatchDiscountWebRoute($request),
74            $path === '/settings/data-mapping' => $this->dispatchDataMappingWebRoute($request),
75            $path === '/settings/currencies' || $path === '/tools/currencies' =>
76                $this->dispatchCurrenciesWebRoute($request),
77            $path === '/settings/currencies/add' && $request->getMethod() === 'POST' =>
78                $this->dispatchCurrenciesAddRoute($request),
79            $path === '/settings/currencies/delete' && $request->getMethod() === 'POST' =>
80                $this->dispatchCurrenciesDeleteRoute($request),
81            str_starts_with($path, '/htmx/tax/') => $this->dispatchHtmxTaxRoutes($path, $request),
82            str_starts_with($path, '/htmx/discount/') => $this->dispatchHtmxDiscountRoutes($path, $request),
83            str_starts_with($path, '/htmx/inventory-fields/') =>
84                $this->dispatchHtmxInventoryFieldRoutes($path, $request),
85            str_starts_with($path, '/htmx/inventory/') =>
86                $this->dispatchHtmxInventoryItemRoutes($path, $request),
87            str_starts_with($path, '/htmx/data-mapping/') =>
88                $this->dispatchHtmxDataMappingRoutes($path, $request),
89            (bool) preg_match('#^/settings/modules/(\d+)/inventory-fields$#', $path, $m) =>
90                $this->dispatchInventoryFieldsWebRoute((int) $m[1], $request),
91            (bool) preg_match('#^/quotes/(\d+)/convert-to-order$#', $path, $m) =>
92                $this->dispatchQuoteConversionRoute((int) $m[1], $request),
93            default => null,
94        };
95    }
96
97    private function dispatchTaxWebRoute(ServerRequestInterface $request): ResponseInterface
98    {
99        if ($this->taxWebController === null) {
100            return $this->psr17Factory->createResponse(404);
101        }
102
103        return $this->authMiddleware->process(
104            $request,
105            new class($this->taxWebController) implements RequestHandlerInterface {
106                public function __construct(private TaxSettingsWebController $controller)
107                {
108                }
109
110                public function handle(ServerRequestInterface $req): ResponseInterface
111                {
112                    return $this->controller->index($req);
113                }
114            }
115        );
116    }
117
118    private function dispatchDiscountWebRoute(ServerRequestInterface $request): ResponseInterface
119    {
120        if ($this->discountWebController === null) {
121            return $this->psr17Factory->createResponse(404);
122        }
123
124        return $this->authMiddleware->process(
125            $request,
126            new class($this->discountWebController) implements RequestHandlerInterface {
127                public function __construct(private DiscountSettingsWebController $controller)
128                {
129                }
130
131                public function handle(ServerRequestInterface $req): ResponseInterface
132                {
133                    return $this->controller->index($req);
134                }
135            }
136        );
137    }
138
139    private function dispatchDataMappingWebRoute(ServerRequestInterface $request): ResponseInterface
140    {
141        if ($this->dataMappingWebController === null) {
142            return $this->psr17Factory->createResponse(404);
143        }
144
145        return $this->authMiddleware->process(
146            $request,
147            new class($this->dataMappingWebController) implements RequestHandlerInterface {
148                public function __construct(private DataMappingWebController $controller)
149                {
150                }
151
152                public function handle(ServerRequestInterface $req): ResponseInterface
153                {
154                    return $this->controller->index($req);
155                }
156            }
157        );
158    }
159
160    private function dispatchInventoryFieldsWebRoute(int $moduleId, ServerRequestInterface $request): ResponseInterface
161    {
162        if ($this->inventoryWebController === null) {
163            return $this->psr17Factory->createResponse(404);
164        }
165
166        return $this->authMiddleware->process(
167            $request,
168            new class($this->inventoryWebController, $moduleId) implements RequestHandlerInterface {
169                public function __construct(
170                    private InventorySettingsWebController $controller,
171                    private int $moduleId
172                ) {
173                }
174
175                public function handle(ServerRequestInterface $req): ResponseInterface
176                {
177                    return $this->controller->index($req, $this->moduleId);
178                }
179            }
180        );
181    }
182
183    private function dispatchQuoteConversionRoute(int $quoteId, ServerRequestInterface $request): ResponseInterface
184    {
185        if ($this->quoteConversionWebController === null) {
186            return $this->psr17Factory->createResponse(404);
187        }
188
189        return $this->authMiddleware->process(
190            $request,
191            new class($this->quoteConversionWebController, $quoteId) implements RequestHandlerInterface {
192                public function __construct(
193                    private QuoteConversionWebController $controller,
194                    private int $quoteId
195                ) {
196                }
197
198                public function handle(ServerRequestInterface $req): ResponseInterface
199                {
200                    return $this->controller->convert($req, $this->quoteId);
201                }
202            }
203        );
204    }
205
206    private function dispatchHtmxTaxRoutes(string $path, ServerRequestInterface $request): ResponseInterface
207    {
208        if ($this->taxHtmxController === null) {
209            return $this->psr17Factory->createResponse(404);
210        }
211
212        $ctrl = $this->taxHtmxController;
213        $psr17 = $this->psr17Factory;
214
215        return $this->authMiddleware->process(
216            $request,
217            new class($ctrl, $path, $psr17) implements RequestHandlerInterface {
218                public function __construct(
219                    private TaxHtmxController $ctrl,
220                    private string $path,
221                    private Psr17Factory $psr17
222                ) {
223                }
224
225                public function handle(ServerRequestInterface $req): ResponseInterface
226                {
227                    return match (true) {
228                        $this->path === '/htmx/tax/tab-rates' => $this->ctrl->tabRates($req),
229                        $this->path === '/htmx/tax/tab-groups' => $this->ctrl->tabGroups($req),
230                        $this->path === '/htmx/tax/tab-rules' => $this->ctrl->tabRules($req),
231                        $this->path === '/htmx/tax/tab-settings' => $this->ctrl->tabSettings($req),
232                        $this->path === '/htmx/tax/tab-simulator' => $this->ctrl->tabSimulator($req),
233                        $this->path === '/htmx/tax/rate-modal' => $this->ctrl->rateModal($req),
234                        (bool) preg_match('#^/htmx/tax/rate-modal/(\d+)$#', $this->path, $m) =>
235                            $this->ctrl->rateModal($req->withAttribute('id', (int) $m[1])),
236                        $this->path === '/htmx/tax/rate-save' && $req->getMethod() === 'POST' =>
237                            $this->ctrl->rateSave($req),
238                        (bool) preg_match('#^/htmx/tax/rate-toggle/(\d+)$#', $this->path, $m)
239                            && $req->getMethod() === 'POST' =>
240                            $this->ctrl->rateToggle($req->withAttribute('id', (int) $m[1])),
241                        (bool) preg_match('#^/htmx/tax/rate-delete/(\d+)$#', $this->path, $m) =>
242                            $this->ctrl->rateDelete($req->withAttribute('id', (int) $m[1])),
243                        $this->path === '/htmx/tax/rule-modal' => $this->ctrl->ruleModal($req),
244                        (bool) preg_match('#^/htmx/tax/rule-modal/(\d+)$#', $this->path, $m) =>
245                            $this->ctrl->ruleModal($req->withAttribute('id', (int) $m[1])),
246                        $this->path === '/htmx/tax/rule-save' && $req->getMethod() === 'POST' =>
247                            $this->ctrl->ruleSave($req),
248                        (bool) preg_match('#^/htmx/tax/rule-delete/(\d+)$#', $this->path, $m) =>
249                            $this->ctrl->ruleDelete($req->withAttribute('id', (int) $m[1])),
250                        $this->path === '/htmx/tax/simulate' && $req->getMethod() === 'POST' =>
251                            $this->ctrl->simulate($req),
252                        default => $this->psr17->createResponse(404),
253                    };
254                }
255            }
256        );
257    }
258
259    private function dispatchHtmxDiscountRoutes(string $path, ServerRequestInterface $request): ResponseInterface
260    {
261        if ($this->discountHtmxController === null) {
262            return $this->psr17Factory->createResponse(404);
263        }
264
265        $ctrl = $this->discountHtmxController;
266        $psr17 = $this->psr17Factory;
267
268        return $this->authMiddleware->process(
269            $request,
270            new class($ctrl, $path, $psr17) implements RequestHandlerInterface {
271                public function __construct(
272                    private DiscountHtmxController $ctrl,
273                    private string $path,
274                    private Psr17Factory $psr17
275                ) {
276                }
277
278                public function handle(ServerRequestInterface $req): ResponseInterface
279                {
280                    return match (true) {
281                        $this->path === '/htmx/discount/tab-discounts' => $this->ctrl->tabDiscounts($req),
282                        $this->path === '/htmx/discount/rate-modal' => $this->ctrl->rateModal($req),
283                        (bool) preg_match('#^/htmx/discount/rate-modal/(\d+)$#', $this->path, $m) =>
284                            $this->ctrl->rateModal($req->withAttribute('id', (int) $m[1])),
285                        $this->path === '/htmx/discount/rate-save' && $req->getMethod() === 'POST' =>
286                            $this->ctrl->rateSave($req),
287                        (bool) preg_match('#^/htmx/discount/rate-save/(\d+)$#', $this->path, $m)
288                            && $req->getMethod() === 'POST' =>
289                            $this->ctrl->rateSave($req->withAttribute('id', (int) $m[1])),
290                        (bool) preg_match('#^/htmx/discount/rate-toggle/(\d+)$#', $this->path, $m)
291                            && $req->getMethod() === 'POST' =>
292                            $this->ctrl->rateToggle($req->withAttribute('id', (int) $m[1])),
293                        (bool) preg_match('#^/htmx/discount/rate-delete/(\d+)$#', $this->path, $m) =>
294                            $this->ctrl->rateDelete($req->withAttribute('id', (int) $m[1])),
295                        default => $this->psr17->createResponse(404),
296                    };
297                }
298            }
299        );
300    }
301
302    private function dispatchHtmxInventoryFieldRoutes(
303        string $path,
304        ServerRequestInterface $request
305    ): ResponseInterface {
306        if ($this->inventoryFieldsHtmxController === null) {
307            return $this->psr17Factory->createResponse(404);
308        }
309
310        $ctrl = $this->inventoryFieldsHtmxController;
311        $psr17 = $this->psr17Factory;
312
313        return $this->authMiddleware->process(
314            $request,
315            new class($ctrl, $path, $psr17) implements RequestHandlerInterface {
316                public function __construct(
317                    private InventorySettingsHtmxController $ctrl,
318                    private string $path,
319                    private Psr17Factory $psr17
320                ) {
321                }
322
323                public function handle(ServerRequestInterface $req): ResponseInterface
324                {
325                    return match (true) {
326                        (bool) preg_match('#^/htmx/inventory-fields/(\d+)/table$#', $this->path, $m) =>
327                            $this->ctrl->table($req, (int) $m[1]),
328                        (bool) preg_match('#^/htmx/inventory-fields/(\d+)/modal$#', $this->path, $m) =>
329                            $this->ctrl->modal($req, (int) $m[1]),
330                        (bool) preg_match('#^/htmx/inventory-fields/(\d+)/save$#', $this->path, $m)
331                            && $req->getMethod() === 'POST' =>
332                            $this->ctrl->save($req, (int) $m[1]),
333                        (bool) preg_match('#^/htmx/inventory-fields/(\d+)/toggle/(\d+)$#', $this->path, $m)
334                            && $req->getMethod() === 'POST' =>
335                            $this->ctrl->toggle($req, (int) $m[1], (int) $m[2]),
336                        (bool) preg_match('#^/htmx/inventory-fields/(\d+)/delete/(\d+)$#', $this->path, $m)
337                            && in_array($req->getMethod(), ['POST', 'DELETE'], true) =>
338                            $this->ctrl->delete($req, (int) $m[1], (int) $m[2]),
339                        default => $this->psr17->createResponse(404),
340                    };
341                }
342            }
343        );
344    }
345
346    private function dispatchHtmxInventoryItemRoutes(
347        string $path,
348        ServerRequestInterface $request
349    ): ResponseInterface {
350        if ($this->inventoryItemsHtmxController === null) {
351            return $this->psr17Factory->createResponse(404);
352        }
353
354        $ctrl = $this->inventoryItemsHtmxController;
355        $psr17 = $this->psr17Factory;
356
357        return $this->authMiddleware->process(
358            $request,
359            new class($ctrl, $path, $psr17) implements RequestHandlerInterface {
360                public function __construct(
361                    private InventoryItemsHtmxController $ctrl,
362                    private string $path,
363                    private Psr17Factory $psr17
364                ) {
365                }
366
367                public function handle(ServerRequestInterface $req): ResponseInterface
368                {
369                    return match (true) {
370                        (bool) preg_match('#^/htmx/inventory/([a-zA-Z0-9_-]+)/(\d+)/items$#', $this->path, $m) =>
371                            $this->ctrl->items($req, (string) $m[1], (int) $m[2]),
372                        (bool) preg_match(
373                            '#^/htmx/inventory/([a-zA-Z0-9_-]+)/(\d+)/calculate$#',
374                            $this->path,
375                            $m
376                        ) && $req->getMethod() === 'POST' =>
377                            $this->ctrl->calculate($req, (string) $m[1], (int) $m[2]),
378                        (bool) preg_match(
379                            '#^/htmx/inventory/([a-zA-Z0-9_-]+)/(\d+)/save-items$#',
380                            $this->path,
381                            $m
382                        ) && $req->getMethod() === 'POST' =>
383                            $this->ctrl->save($req, (string) $m[1], (int) $m[2]),
384                        default => $this->psr17->createResponse(404),
385                    };
386                }
387            }
388        );
389    }
390
391    private function dispatchHtmxDataMappingRoutes(
392        string $path,
393        ServerRequestInterface $request
394    ): ResponseInterface {
395        if ($this->dataMappingHtmxController === null) {
396            return $this->psr17Factory->createResponse(404);
397        }
398
399        $ctrl = $this->dataMappingHtmxController;
400        $psr17 = $this->psr17Factory;
401
402        return $this->authMiddleware->process(
403            $request,
404            new class($ctrl, $path, $psr17) implements RequestHandlerInterface {
405                public function __construct(
406                    private DataMappingHtmxController $ctrl,
407                    private string $path,
408                    private Psr17Factory $psr17
409                ) {
410                }
411
412                public function handle(ServerRequestInterface $req): ResponseInterface
413                {
414                    return match (true) {
415                        $this->path === '/htmx/data-mapping/editor' => $this->ctrl->editor($req),
416                        $this->path === '/htmx/data-mapping/save' && $req->getMethod() === 'POST' =>
417                            $this->ctrl->save($req),
418                        default => $this->psr17->createResponse(404),
419                    };
420                }
421            }
422        );
423    }
424
425    private function dispatchCurrenciesWebRoute(ServerRequestInterface $request): ResponseInterface
426    {
427        if ($this->currenciesWebController === null) {
428            return $this->psr17Factory->createResponse(404);
429        }
430
431        return $this->authMiddleware->process(
432            $request,
433            new class($this->currenciesWebController) implements RequestHandlerInterface {
434                public function __construct(private CurrenciesWebController $controller)
435                {
436                }
437
438                public function handle(ServerRequestInterface $req): ResponseInterface
439                {
440                    return $this->controller->index($req);
441                }
442            }
443        );
444    }
445
446    private function dispatchCurrenciesAddRoute(ServerRequestInterface $request): ResponseInterface
447    {
448        if ($this->currenciesWebController === null) {
449            return $this->psr17Factory->createResponse(404);
450        }
451
452        return $this->authMiddleware->process(
453            $request,
454            new class($this->currenciesWebController) implements RequestHandlerInterface {
455                public function __construct(private CurrenciesWebController $controller)
456                {
457                }
458
459                public function handle(ServerRequestInterface $req): ResponseInterface
460                {
461                    return $this->controller->add($req);
462                }
463            }
464        );
465    }
466
467    private function dispatchCurrenciesDeleteRoute(ServerRequestInterface $request): ResponseInterface
468    {
469        if ($this->currenciesWebController === null) {
470            return $this->psr17Factory->createResponse(404);
471        }
472
473        return $this->authMiddleware->process(
474            $request,
475            new class($this->currenciesWebController) implements RequestHandlerInterface {
476                public function __construct(private CurrenciesWebController $controller)
477                {
478                }
479
480                public function handle(ServerRequestInterface $req): ResponseInterface
481                {
482                    return $this->controller->delete($req);
483                }
484            }
485        );
486    }
487}
488