Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
55 / 55
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
DataExchangeApiController
100.00% covered (success)
100.00%
54 / 54
100.00% covered (success)
100.00%
5 / 5
16
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
 actionExport
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
4
 actionAnalyze
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 actionExecuteImport
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 actionJobStatus
100.00% covered (success)
100.00%
12 / 12
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\DataExchange\Presentation\Api;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\DataExchange\Application\Service\DataExportServiceInterface;
12use App\Core\DataExchange\Application\Service\DataImportServiceInterface;
13use App\Core\DataExchange\Domain\Model\ExportConfigDto;
14use App\Core\DataExchange\Domain\Model\ImportMappingConfig;
15use App\Core\Engine\Domain\Model\PermissionContext;
16use App\Shared\Infrastructure\Http\ApiResponseTrait;
17use Nyholm\Psr7\Factory\Psr17Factory;
18use PDO;
19use Psr\Http\Message\ResponseInterface;
20use Psr\Http\Message\ServerRequestInterface;
21use Throwable;
22
23/**
24 * REST API controller serving versioned data exchange endpoints (/api/v1/engine/{module}/...).
25 *
26 * @package App\Core\DataExchange\Presentation\Api
27 */
28final readonly class DataExchangeApiController
29{
30    use ApiResponseTrait;
31
32    /**
33     * DataExchangeApiController constructor.
34     */
35    public function __construct(
36        private DataExportServiceInterface $exportService,
37        private DataImportServiceInterface $importService,
38        private Psr17Factory $psr17,
39        private PDO $pdo,
40        private string $tablePrefix = 'a_'
41    ) {
42    }
43
44    /**
45     * POST /api/v1/engine/{module}/export
46     */
47    public function actionExport(
48        ServerRequestInterface $request,
49        string $moduleName,
50        PermissionContext $context
51    ): ResponseInterface {
52        try {
53            $body = (array) $request->getParsedBody();
54            $config = new ExportConfigDto(
55                moduleName: $moduleName,
56                format: (string) ($body['format'] ?? 'xlsx'),
57                scope: (string) ($body['scope'] ?? 'all'),
58                recordIds: !empty($body['selected_ids']) ? (array) $body['selected_ids'] : [],
59                selectedColumns: !empty($body['fields']) ? (array) $body['fields'] : [],
60                csvDelimiter: (string) ($body['delimiter'] ?? ','),
61                includeBom: !empty($body['include_bom'])
62            );
63
64            $result = $this->exportService->export($moduleName, $config, $context);
65
66            return $this->jsonSuccess($this->psr17, [
67                'file_name' => $result['file_name'],
68                'file_path' => $result['file_path'],
69                'total_rows' => $result['total_rows'],
70                'size_bytes' => $result['size_bytes'],
71            ]);
72        } catch (Throwable $e) {
73            return $this->jsonError($this->psr17, $e->getMessage(), 400);
74        }
75    }
76
77    /**
78     * POST /api/v1/engine/{module}/import/analyze
79     */
80    public function actionAnalyze(
81        ServerRequestInterface $request,
82        string $moduleName
83    ): ResponseInterface {
84        try {
85            $body = (array) $request->getParsedBody();
86            $filePath = (string) ($body['file_path'] ?? '');
87            if ($filePath === '' || !file_exists($filePath)) {
88                return $this->jsonError($this->psr17, 'Valid file_path is required.', 422);
89            }
90
91            $analysis = $this->importService->analyzeFile($filePath, $moduleName);
92
93            return $this->jsonSuccess($this->psr17, $analysis);
94        } catch (Throwable $e) {
95            return $this->jsonError($this->psr17, $e->getMessage(), 400);
96        }
97    }
98
99    /**
100     * POST /api/v1/engine/{module}/import/execute
101     */
102    public function actionExecuteImport(
103        ServerRequestInterface $request,
104        string $moduleName,
105        PermissionContext $context
106    ): ResponseInterface {
107        try {
108            $body = (array) $request->getParsedBody();
109            $filePath = (string) ($body['file_path'] ?? '');
110            if ($filePath === '' || !file_exists($filePath)) {
111                return $this->jsonError($this->psr17, 'Valid file_path is required.', 422);
112            }
113
114            $config = new ImportMappingConfig(
115                columnMapping: (array) ($body['column_mapping'] ?? []),
116                deduplicationStrategy: (string) ($body['deduplication_strategy'] ?? 'skip_duplicates'),
117                uniqueIdentifierField: !empty($body['unique_field']) ? (string) $body['unique_field'] : null,
118                isDryRun: !empty($body['is_dry_run'])
119            );
120
121            $result = $this->importService->executeImport($moduleName, $filePath, $config, $context);
122
123            return $this->jsonSuccess($this->psr17, $result->toArray());
124        } catch (Throwable $e) {
125            return $this->jsonError($this->psr17, $e->getMessage(), 400);
126        }
127    }
128
129    /**
130     * GET /api/v1/engine/{module}/import/jobs/{id}
131     */
132    public function actionJobStatus(
133        string $moduleName,
134        int $jobId
135    ): ResponseInterface {
136        $sql = "SELECT id, module_name, file_name, total_rows, processed_rows, imported_rows, " .
137            "updated_rows, skipped_rows, failed_rows, status, created_at, updated_at " .
138            "FROM {$this->tablePrefix}mod_import_jobs_records WHERE id = :id AND module_name = :module LIMIT 1";
139
140        $stmt = $this->pdo->prepare($sql);
141        $stmt->execute([
142            ':id' => $jobId,
143            ':module' => $moduleName,
144        ]);
145
146        $record = $stmt->fetch(PDO::FETCH_ASSOC);
147        if ($record === false) {
148            return $this->jsonError($this->psr17, 'Import job not found.', 404);
149        }
150
151        return $this->jsonSuccess($this->psr17, $record);
152    }
153}