Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.06% covered (success)
97.06%
66 / 68
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
RecordBulkActionsApiController
98.51% covered (success)
98.51%
66 / 67
80.00% covered (warning)
80.00%
4 / 5
16
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
 actionBulkActionsList
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 actionBulkEditableFields
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
8
 actionBulkAction
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
4
 handleException
100.00% covered (success)
100.00%
1 / 1
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\Core\Engine\Presentation\Api;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Action\Application\Service\ActionRegistryService;
12use App\Core\Action\Infrastructure\Repository\SqlActionRepository;
13use App\Shared\Infrastructure\Http\ApiResponseTrait;
14use App\Core\Engine\Application\Service\BulkActionService;
15use App\Core\Engine\Application\Service\UniversalCrudService;
16use App\Core\Engine\Domain\Exception\ModuleNotFoundException;
17use App\Core\Engine\Domain\Exception\PermissionDeniedException;
18use App\Core\Engine\Domain\Exception\RecordNotFoundException;
19use App\Modules\Structure\Domain\Exception\StructureReassignRequiredException;
20use App\Core\Engine\Domain\Exception\ValidationException;
21use App\Core\Engine\Domain\Model\PermissionContext;
22use App\Modules\Automation\Queue\Infrastructure\Repository\SqlQueueRepository;
23use Nyholm\Psr7\Factory\Psr17Factory;
24use Psr\Http\Message\ResponseInterface;
25use Psr\Http\Message\ServerRequestInterface;
26
27/**
28 * Handles bulk actions, bulk editable fields, and enqueueing mass operations.
29 */
30final readonly class RecordBulkActionsApiController
31{
32    use ApiResponseTrait;
33
34    public function __construct(
35        private UniversalCrudService $crudService,
36        private Psr17Factory $psr17,
37        private ?BulkActionService $bulkActionService = null,
38        private ?ActionRegistryService $actionRegistryService = null
39    ) {
40    }
41
42    /**
43     * Lists active bulk actions configured for the module.
44     *
45     * @param string            $moduleName Module machine name.
46     * @param PermissionContext $context    Security context.
47     * @return ResponseInterface JSON response with bulk actions array.
48     */
49    public function actionBulkActionsList(string $moduleName, PermissionContext $context): ResponseInterface
50    {
51        try {
52            $module = $this->crudService->getMetadataRepository()->findModule($moduleName);
53            $this->crudService->getGuard()->assertReadAccess($module, $context);
54
55            $actionRegistry = $this->actionRegistryService ?? new ActionRegistryService(
56                new SqlActionRepository(
57                    $this->crudService->getPersistenceManager()->getPdo()
58                ),
59                $this->crudService->getMetadataRepository()
60            );
61
62            $actions = $actionRegistry->getBulkActions($moduleName);
63            $serialized = array_map(static fn($a): array => $a->toArray(), $actions);
64
65            return $this->jsonSuccess($this->psr17, [
66                'actions' => $serialized,
67                'total'   => count($serialized),
68            ]);
69        } catch (\Throwable $e) {
70            return $this->handleException($e);
71        }
72    }
73
74    /**
75     * Lists all picklist / editable fields suitable for bulk editing in the module.
76     *
77     * @param string            $moduleName Module machine name.
78     * @param PermissionContext $context    Security context.
79     * @return ResponseInterface JSON response with editable fields array.
80     */
81    public function actionBulkEditableFields(string $moduleName, PermissionContext $context): ResponseInterface
82    {
83        try {
84            $module = $this->crudService->getMetadataRepository()->findModule($moduleName);
85            $this->crudService->getGuard()->assertReadAccess($module, $context);
86
87            $fields = $this->crudService->getMetadataRepository()->findFields($module->id);
88            $editable = [];
89
90            foreach ($fields as $field) {
91                $isPicklist = in_array($field->uitypeName, ['single_select', 'picklist'], true)
92                    || !empty($field->filterOptions);
93                $isAllowedStatus = $field->fieldKey === 'record_status';
94
95                if (($isPicklist || $isAllowedStatus) && (!$field->isReadonly || $isAllowedStatus)) {
96                    $editable[] = [
97                        'field_key'  => $field->fieldKey,
98                        'label'      => $field->label,
99                        'uitype'     => $field->uitypeName,
100                        'options'    => $field->filterOptions,
101                        'mandatory'  => $field->isMandatory,
102                    ];
103                }
104            }
105
106            return $this->jsonSuccess($this->psr17, [
107                'fields' => $editable,
108                'total'  => count($editable),
109            ]);
110        } catch (\Throwable $e) {
111            return $this->handleException($e);
112        }
113    }
114
115    /**
116     * Enqueues a mass bulk action (edit, archive, restore, delete) for background execution.
117     *
118     * @param ServerRequestInterface $request    PSR-7 request.
119     * @param string                 $moduleName Module machine name.
120     * @param PermissionContext      $context    Security context.
121     * @return ResponseInterface JSON response with job details.
122     */
123    public function actionBulkAction(
124        ServerRequestInterface $request,
125        string $moduleName,
126        PermissionContext $context
127    ): ResponseInterface {
128        try {
129            $module = $this->crudService->getMetadataRepository()->findModule($moduleName);
130            $this->crudService->getGuard()->assertWriteAccess($module, $context);
131
132            $rawBody = (string) $request->getBody();
133            $data = json_decode($rawBody, true);
134            if (!is_array($data)) {
135                return $this->jsonError($this->psr17, 'Invalid JSON body for bulk action.', 400);
136            }
137
138            $actionType = (string) ($data['action'] ?? $data['action_type'] ?? '');
139            $recordIds  = (array) ($data['record_ids'] ?? []);
140            $updates    = (array) ($data['updates'] ?? $data['fields'] ?? []);
141
142            if ($actionType === 'bulk_delete') {
143                $this->crudService->getGuard()->assertDeleteAccess($module, $context);
144            }
145
146            $pdo = $this->crudService->getPersistenceManager()->getPdo();
147            $bulkService = $this->bulkActionService ?? new BulkActionService(
148                new SqlQueueRepository($pdo),
149                $this->crudService->getMetadataRepository(),
150                $pdo
151            );
152
153            $result = $bulkService->executeBulkAction(
154                $moduleName,
155                $actionType,
156                $recordIds,
157                $updates,
158                $context->actorUserId
159            );
160
161            return $this->jsonSuccess($this->psr17, $result, 200);
162        } catch (\Throwable $e) {
163            return $this->handleException($e);
164        }
165    }
166
167    /**
168     * Maps thrown domain exceptions to PSR-7 JSON responses.
169     */
170    private function handleException(\Throwable $e): ResponseInterface
171    {
172        return $this->handleApiException($this->psr17, $e);
173    }
174}