Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.33% covered (warning)
83.33%
40 / 48
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
GetDocumentsAction
83.33% covered (warning)
83.33%
40 / 48
50.00% covered (danger)
50.00%
2 / 4
12.67
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
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
5
 getStoredDocuments
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
4.02
 updateDocumentStatus
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace App\Action\Document;
6
7use App\Domain\Auth\Data\UserAuthData;
8use App\Domain\Document\Data\PandaDocDocumentData;
9use App\Domain\Document\Service\PandaDocClientInterface;
10use App\Renderer\JsonRenderer;
11use App\Support\Row;
12use PDO;
13use Psr\Http\Message\ResponseInterface;
14use Psr\Http\Message\ServerRequestInterface;
15use RuntimeException;
16
17final readonly class GetDocumentsAction
18{
19    public function __construct(
20        private PandaDocClientInterface $pandaDocClient,
21        private JsonRenderer $renderer,
22        private PDO $pdo,
23    ) {}
24
25    public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
26    {
27        /** @var UserAuthData $user */
28        $user = $request->getAttribute('user');
29
30        $storedDocs = $this->getStoredDocuments($user->investorId);
31
32        $documents = [];
33        foreach ($storedDocs as $storedDoc) {
34            try {
35                $details = $this->pandaDocClient->getDocumentDetails($storedDoc['pandadocId']);
36                $documents[] = $details->toArray();
37
38                // Sync status back to local DB if it changed.
39                // Both sides now use the full PandaDoc form ('document.sent', etc.).
40                if ($details->status !== '' && $details->status !== $storedDoc['status']) {
41                    $this->updateDocumentStatus($storedDoc['pandadocId'], $details->status);
42                }
43            } catch (RuntimeException) {
44                // If PandaDoc fetch fails, return what we have locally.
45                // Stored status is already in the full form the frontend expects.
46                $fallback = PandaDocDocumentData::fromStoredFallback(
47                    pandadocId: $storedDoc['pandadocId'],
48                    documentName: $storedDoc['documentName'],
49                    status: $storedDoc['status'],
50                    createdAt: $storedDoc['createdAt'],
51                    updatedAt: $storedDoc['updatedAt'],
52                );
53                $documents[] = $fallback->toArray();
54            }
55        }
56
57        return $this->renderer->json($response, [
58            'success' => true,
59            'data' => ['documents' => $documents],
60        ]);
61    }
62
63    /**
64     * @param ?int $investorId
65     * @return list<array{pandadocId: string, documentName: string, status: string, createdAt: string, updatedAt: string}>
66     */
67    private function getStoredDocuments(?int $investorId): array
68    {
69        if ($investorId === null) {
70            return [];
71        }
72
73        $stmt = $this->pdo->prepare(
74            'SELECT pandadoc_id AS "pandadocId", document_name AS "documentName",
75                    status, created_at AS "createdAt", updated_at AS "updatedAt"
76             FROM investor_documents
77             WHERE investor_id = :investorId
78             ORDER BY created_at DESC',
79        );
80
81        if ($stmt === false) {
82            throw new RuntimeException('Failed to prepare statement');
83        }
84
85        $stmt->execute(['investorId' => $investorId]);
86
87        $documents = [];
88        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
89            $rowArray = Row::from($row);
90            $documents[] = [
91                'pandadocId' => Row::string($rowArray, 'pandadocId'),
92                'documentName' => Row::string($rowArray, 'documentName'),
93                'status' => Row::string($rowArray, 'status'),
94                'createdAt' => Row::string($rowArray, 'createdAt'),
95                'updatedAt' => Row::string($rowArray, 'updatedAt'),
96            ];
97        }
98
99        return $documents;
100    }
101
102    private function updateDocumentStatus(string $pandadocId, string $status): void
103    {
104        $stmt = $this->pdo->prepare(
105            'UPDATE investor_documents SET status = :status, updated_at = CURRENT_TIMESTAMP WHERE pandadoc_id = :pandadocId',
106        );
107
108        if ($stmt === false) {
109            throw new RuntimeException('Failed to prepare statement');
110        }
111
112        $stmt->execute(['status' => $status, 'pandadocId' => $pandadocId]);
113    }
114}