Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
InvestorAgreementService
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
2 / 2
7
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
 listForInvestor
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Document\Service;
6
7use App\Domain\Document\Data\PandaDocDocumentData;
8use App\Domain\Document\Repository\InvestorAgreementRepository;
9use RuntimeException;
10
11/**
12 * Reads an investor's outbound PandaDoc agreements and refreshes their live
13 * status from PandaDoc on every read, syncing changes back to the local
14 * investor_documents mirror.
15 *
16 * Used by both the investor-facing list (caller's own agreements) and the
17 * admin progress view (a specific investor's agreements), so the refresh +
18 * KYC-bridge logic lives in one place.
19 */
20final readonly class InvestorAgreementService
21{
22    public function __construct(
23        private PandaDocClientInterface $pandaDocClient,
24        private IdentityVerificationService $identityVerification,
25        private InvestorAgreementRepository $repository,
26    ) {}
27
28    /**
29     * @param int $investorId
30     * @return list<array<string, mixed>>
31     */
32    public function listForInvestor(int $investorId): array
33    {
34        $storedDocs = $this->repository->findByInvestorId($investorId);
35
36        $documents = [];
37        foreach ($storedDocs as $storedDoc) {
38            try {
39                $details = $this->pandaDocClient->getDocumentDetails($storedDoc['pandadocId']);
40                $documents[] = $details->toArray();
41
42                // Sync status back to the local mirror if it changed.
43                // Both sides use the full PandaDoc form ('document.sent', etc.).
44                if ($details->status !== '' && $details->status !== $storedDoc['status']) {
45                    $this->repository->updateStatus($storedDoc['pandadocId'], $details->status);
46
47                    // A document reaching completion behind an ID Check is proof
48                    // the recipient passed identity verification — surface them
49                    // for admin KYC review. No-op for non-ID-verification docs.
50                    if ($details->status === 'document.completed') {
51                        $this->identityVerification->markIdentityVerifiedIfApplicable(
52                            $investorId,
53                            $storedDoc['pandadocId'],
54                        );
55                    }
56                }
57            } catch (RuntimeException) {
58                // If PandaDoc fetch fails, return what we have locally.
59                // Stored status is already in the full form the frontend expects.
60                $fallback = PandaDocDocumentData::fromStoredFallback(
61                    pandadocId: $storedDoc['pandadocId'],
62                    documentName: $storedDoc['documentName'],
63                    status: $storedDoc['status'],
64                    createdAt: $storedDoc['createdAt'],
65                    updatedAt: $storedDoc['updatedAt'],
66                );
67                $documents[] = $fallback->toArray();
68            }
69        }
70
71        return $documents;
72    }
73}