Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
CreateRegistrationDocumentsAction
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 5
240
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 / 38
0.00% covered (danger)
0.00%
0 / 1
42
 getInvestor
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 saveDocument
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 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 CreateRegistrationDocumentsAction
16{
17    private const string W9_TEMPLATE_ID = 'EhTXUKKhtSMXCf74rJkcGD';
18
19    public function __construct(
20        private PandaDocService $pandaDocService,
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        $investor = $this->getInvestor($user->investorId);
31
32        $fields = [
33            'Name' => ['value' => $investor['firstName'] . ' ' . $investor['lastName']],
34            'Street, Apt, Suite' => ['value' => $investor['addressLine1']
35                . ($investor['addressLine2'] !== '' ? ', ' . $investor['addressLine2'] : '')],
36            'City, State, Zip' => ['value' => sprintf(
37                '%s, %s %s',
38                $investor['city'],
39                $investor['state'],
40                $investor['zipCode'],
41            )],
42        ];
43
44        $doc = $this->pandaDocService->createDocumentFromTemplate(
45            templateId: self::W9_TEMPLATE_ID,
46            documentName: sprintf('W-9 — %s %s', $investor['firstName'], $investor['lastName']),
47            recipientEmail: $user->email,
48            firstName: $investor['firstName'],
49            lastName: $investor['lastName'],
50            fields: $fields,
51        );
52
53        $documentName = sprintf('W-9 — %s %s', $investor['firstName'], $investor['lastName']);
54
55        // Save document reference locally
56        if ($doc['id'] !== '') {
57            $this->saveDocument($user->investorId, $doc['id'], $documentName, self::W9_TEMPLATE_ID);
58
59            // Wait for PandaDoc to finish processing, then send
60            for ($attempt = 0; $attempt < 10; $attempt++) {
61                sleep(2);
62                $status = $this->pandaDocService->getDocumentStatus($doc['id']);
63                if ($status === 'document.draft') {
64                    try {
65                        $this->pandaDocService->sendDocument($doc['id']);
66                        $this->updateDocumentStatus($doc['id'], 'sent');
67                    } catch (RuntimeException) {
68                        // Send failed despite draft status
69                    }
70                    break;
71                }
72            }
73        }
74
75        return $this->renderer->json($response, [
76            'success' => true,
77            'data' => [
78                'documentId' => $doc['id'],
79            ],
80        ]);
81    }
82
83    /**
84     * @param ?int $investorId
85     * @return array{firstName: string, lastName: string, addressLine1: string, addressLine2: string, city: string, state: string, zipCode: string}
86     */
87    private function getInvestor(?int $investorId): array
88    {
89        if ($investorId === null) {
90            throw new RuntimeException('User is not linked to an investor profile');
91        }
92
93        $stmt = $this->pdo->prepare(
94            'SELECT first_name AS "firstName", last_name AS "lastName",
95                    address_line1 AS "addressLine1", COALESCE(address_line2, \'\') AS "addressLine2",
96                    city, state, zip_code AS "zipCode"
97             FROM investors WHERE investor_id = :id',
98        );
99
100        if ($stmt === false) {
101            throw new RuntimeException('Failed to prepare statement');
102        }
103
104        $stmt->execute(['id' => $investorId]);
105        $row = $stmt->fetch();
106
107        if ($row === false) {
108            throw new RuntimeException('Investor not found');
109        }
110
111        return $row;
112    }
113
114    private function saveDocument(?int $investorId, string $pandadocId, string $documentName, string $templateId): void
115    {
116        $stmt = $this->pdo->prepare(
117            'INSERT INTO investor_documents (investor_id, pandadoc_id, document_name, template_id, status)
118             VALUES (:investorId, :pandadocId, :documentName, :templateId, :status)',
119        );
120
121        if ($stmt === false) {
122            throw new RuntimeException('Failed to prepare statement');
123        }
124
125        $stmt->execute([
126            'investorId' => $investorId,
127            'pandadocId' => $pandadocId,
128            'documentName' => $documentName,
129            'templateId' => $templateId,
130            'status' => 'draft',
131        ]);
132    }
133
134    private function updateDocumentStatus(string $pandadocId, string $status): void
135    {
136        $stmt = $this->pdo->prepare(
137            'UPDATE investor_documents SET status = :status, updated_at = CURRENT_TIMESTAMP WHERE pandadoc_id = :pandadocId',
138        );
139
140        if ($stmt === false) {
141            throw new RuntimeException('Failed to prepare statement');
142        }
143
144        $stmt->execute(['status' => $status, 'pandadocId' => $pandadocId]);
145    }
146}