Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
GetDocumentsAction
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 5
420
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __invoke
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
30
 getStoredDocuments
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 mapPandaDocStatus
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
72
 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\Service\PandaDocService;
9use App\Renderer\JsonRenderer;
10use PDO;
11use Psr\Http\Message\ResponseInterface;
12use Psr\Http\Message\ServerRequestInterface;
13use RuntimeException;
14
15final readonly class GetDocumentsAction
16{
17    public function __construct(
18        private PandaDocService $pandaDocService,
19        private JsonRenderer $renderer,
20        private PDO $pdo,
21    ) {}
22
23    public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
24    {
25        /** @var UserAuthData $user */
26        $user = $request->getAttribute('user');
27
28        $storedDocs = $this->getStoredDocuments($user->investorId);
29
30        $documents = [];
31        foreach ($storedDocs as $storedDoc) {
32            try {
33                $details = $this->pandaDocService->getDocumentDetails($storedDoc['pandadocId']);
34                $documents[] = $details;
35
36                // Sync status back to local DB if it changed
37                $pandadocStatus = $this->mapPandaDocStatus($details['status'] ?? '');
38                if ($pandadocStatus !== '' && $pandadocStatus !== $storedDoc['status']) {
39                    $this->updateDocumentStatus($storedDoc['pandadocId'], $pandadocStatus);
40                }
41            } catch (RuntimeException) {
42                // If PandaDoc fetch fails, return what we have locally
43                $documents[] = [
44                    'id' => $storedDoc['pandadocId'],
45                    'name' => $storedDoc['documentName'],
46                    'status' => $storedDoc['status'],
47                    'dateSent' => null,
48                    'dateCompleted' => null,
49                    'expirationDate' => null,
50                    'createdAt' => $storedDoc['createdAt'],
51                    'updatedAt' => $storedDoc['updatedAt'],
52                    'recipients' => [],
53                    'signingUrl' => null,
54                    'downloadUrl' => null,
55                ];
56            }
57        }
58
59        return $this->renderer->json($response, [
60            'success' => true,
61            'data' => ['documents' => $documents],
62        ]);
63    }
64
65    /**
66     * @param ?int $investorId
67     * @return array<int, array{pandadocId: string, documentName: string, status: string, createdAt: string, updatedAt: string}>
68     */
69    private function getStoredDocuments(?int $investorId): array
70    {
71        if ($investorId === null) {
72            return [];
73        }
74
75        $stmt = $this->pdo->prepare(
76            'SELECT pandadoc_id AS "pandadocId", document_name AS "documentName",
77                    status, created_at AS "createdAt", updated_at AS "updatedAt"
78             FROM investor_documents
79             WHERE investor_id = :investorId
80             ORDER BY created_at DESC',
81        );
82
83        if ($stmt === false) {
84            throw new RuntimeException('Failed to prepare statement');
85        }
86
87        $stmt->execute(['investorId' => $investorId]);
88
89        return $stmt->fetchAll() ?: [];
90    }
91
92    private function mapPandaDocStatus(string $pandadocStatus): string
93    {
94        return match ($pandadocStatus) {
95            'document.draft' => 'draft',
96            'document.sent', 'document.viewed', 'document.waiting_approval', 'document.in_progress' => 'sent',
97            'document.completed' => 'completed',
98            'document.declined' => 'declined',
99            'document.expired' => 'expired',
100            'document.voided' => 'voided',
101            default => '',
102        };
103    }
104
105    private function updateDocumentStatus(string $pandadocId, string $status): void
106    {
107        $stmt = $this->pdo->prepare(
108            'UPDATE investor_documents SET status = :status, updated_at = CURRENT_TIMESTAMP WHERE pandadoc_id = :pandadocId',
109        );
110
111        if ($stmt === false) {
112            throw new RuntimeException('Failed to prepare statement');
113        }
114
115        $stmt->execute(['status' => $status, 'pandadocId' => $pandadocId]);
116    }
117}