Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
KanbanApiController
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
5 / 5
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 actionKanbanData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 actionUpdateStatus
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 extractStatusFromRequest
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 performStatusUpdate
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\Engine\Presentation\Api;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Application\Service\KanbanBoardQueryService;
12use App\Core\Engine\Application\Service\KanbanStatusTransitionService;
13use App\Modules\Projects\Application\Service\ProjectProgressCalculator;
14use App\Shared\Infrastructure\Http\ApiResponseTrait;
15use Nyholm\Psr7\Factory\Psr17Factory;
16use PDO;
17use Psr\Http\Message\ResponseInterface;
18use Psr\Http\Message\ServerRequestInterface;
19
20/**
21 * Universal Kanban Board REST API Controller.
22 *
23 * Endpoints:
24 * - GET   /api/v1/{module}/kanban             -> Returns columns with picklist stages and rich cards
25 * - PATCH /api/v1/{module}/{id}/status        -> Updates record status and cascades rollup progress
26 *
27 * @package App\Core\Engine\Presentation\Api
28 */
29final readonly class KanbanApiController
30{
31    use ApiResponseTrait;
32
33    private const string ERR_MODULE_NOT_SUPPORTED = 'Module does not support Kanban view.';
34    private const string ERR_RECORD_NOT_FOUND = 'Record not found.';
35    private const string ERR_STATUS_REQUIRED = 'Target status key is required.';
36
37    private KanbanBoardQueryService $queryService;
38    private KanbanStatusTransitionService $transitionService;
39
40    /**
41     * KanbanApiController constructor.
42     *
43     * @param PDO                                $pdo               Active database PDO handle.
44     * @param ProjectProgressCalculator          $calculator        Project progress calculator service.
45     * @param Psr17Factory                       $psr17             PSR-17 response factory.
46     * @param KanbanBoardQueryService|null       $queryService      Optional query service.
47     * @param KanbanStatusTransitionService|null $transitionService Optional transition service.
48     */
49    public function __construct(
50        private PDO $pdo,
51        private ProjectProgressCalculator $calculator,
52        private Psr17Factory $psr17,
53        ?KanbanBoardQueryService $queryService = null,
54        ?KanbanStatusTransitionService $transitionService = null
55    ) {
56        $this->queryService = $queryService ?? new KanbanBoardQueryService($this->pdo);
57        $this->transitionService = $transitionService
58            ?? new KanbanStatusTransitionService($this->pdo, $this->calculator);
59    }
60
61    /**
62     * Returns Kanban board columns and cards for the requested module.
63     *
64     * @param string $module Module machine name.
65     * @return ResponseInterface JSON response.
66     */
67    public function actionKanbanData(string $module): ResponseInterface
68    {
69        $data = $this->queryService->fetchBoardData($module);
70        if ($data === null) {
71            return $this->jsonError($this->psr17, self::ERR_MODULE_NOT_SUPPORTED, 400);
72        }
73
74        return $this->jsonSuccess($this->psr17, $data);
75    }
76
77    /**
78     * Updates record status upon Kanban card Drag & Drop, cascading progress rollups.
79     *
80     * @param ServerRequestInterface $request Active PSR-7 HTTP request.
81     * @param string                 $module  Module machine name.
82     * @param int                    $id      Record primary key.
83     * @return ResponseInterface JSON response with updated state.
84     */
85    public function actionUpdateStatus(ServerRequestInterface $request, string $module, int $id): ResponseInterface
86    {
87        $config = $this->queryService->resolveModuleConfig($module);
88        if ($config === null) {
89            return $this->jsonError($this->psr17, self::ERR_MODULE_NOT_SUPPORTED, 400);
90        }
91
92        $newStatus = $this->extractStatusFromRequest($request);
93        if ($newStatus === '') {
94            return $this->jsonError($this->psr17, self::ERR_STATUS_REQUIRED, 422);
95        }
96
97        return $this->performStatusUpdate($config, $module, $id, $newStatus);
98    }
99
100    /**
101     * Extracts target status string from PSR-7 request payload.
102     *
103     * @param ServerRequestInterface $request HTTP request.
104     * @return string Status value or empty string.
105     */
106    private function extractStatusFromRequest(ServerRequestInterface $request): string
107    {
108        $body = (string) $request->getBody();
109        $payload = json_decode($body, true);
110
111        return is_array($payload) ? (string) ($payload['status'] ?? '') : '';
112    }
113
114    /**
115     * Executes status update and cascading progress rollup.
116     *
117     * @param array<string, mixed> $config Module configuration.
118     * @param string               $module Module name.
119     * @param int                  $id     Record ID.
120     * @param string               $status Target status.
121     * @return ResponseInterface JSON response.
122     */
123    private function performStatusUpdate(
124        array $config,
125        string $module,
126        int $id,
127        string $status
128    ): ResponseInterface {
129        $result = $this->transitionService->updateStatus($config, $module, $id, $status);
130        if (!$result['success']) {
131            return $this->jsonError($this->psr17, self::ERR_RECORD_NOT_FOUND, 404);
132        }
133
134        return $this->jsonSuccess($this->psr17, [
135            'module'     => $module,
136            'record_id'  => $id,
137            'new_status' => $status,
138            'rollup'     => $result['rollup'],
139        ]);
140    }
141}