Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.00% covered (success)
96.00%
24 / 25
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
UploadDocumentAction
96.00% covered (success)
96.00%
24 / 25
50.00% covered (danger)
50.00%
1 / 2
5
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
 __invoke
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace App\Action\Document\Submission;
6
7use App\Domain\Document\Submission\Service\InvestorDocumentService;
8use App\Domain\Exception\BadRequestException;
9use App\Domain\Exception\ForbiddenException;
10use App\Renderer\JsonRenderer;
11use App\Support\Row;
12use Psr\Http\Message\ResponseInterface;
13use Psr\Http\Message\ServerRequestInterface;
14use Psr\Http\Message\UploadedFileInterface;
15
16/**
17 * Multipart upload entry point.
18 *
19 * Expected form fields:
20 *   - documentTypeId  (string|int)
21 *   - file            (single uploaded file under the field name "file")
22 *
23 * Re-upload semantics: if the investor already has a rejected document
24 * of this type, the parent row is reused and its status flips back to
25 * pending_review (preserving the document_id chain through admin review).
26 */
27final readonly class UploadDocumentAction
28{
29    public function __construct(
30        private InvestorDocumentService $service,
31        private JsonRenderer $renderer,
32    ) {}
33
34    public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
35    {
36        $attributes = $request->getAttributes();
37        $investorId = Row::nullableInt($attributes, 'investorId');
38        $userId = Row::int($attributes, 'userId');
39
40        if ($investorId === null) {
41            throw new ForbiddenException('This endpoint requires an investor account.');
42        }
43
44        $body = (array)$request->getParsedBody();
45        $documentTypeId = Row::nullableInt($body, 'documentTypeId');
46        if ($documentTypeId === null) {
47            throw new BadRequestException('documentTypeId is required.');
48        }
49
50        $files = $request->getUploadedFiles();
51        $file = $files['file'] ?? null;
52        if (!$file instanceof UploadedFileInterface) {
53            throw new BadRequestException('A file is required (form field "file").');
54        }
55
56        $document = $this->service->uploadForInvestor(
57            investorId: $investorId,
58            userId: $userId,
59            documentTypeId: $documentTypeId,
60            file: $file,
61        );
62
63        return $this->renderer->json($response, [
64            'success' => true,
65            'message' => 'Document uploaded and pending review.',
66            'data' => ['document' => $document->toArray()],
67        ], 201);
68    }
69}