Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
90.91% |
10 / 11 |
|
50.00% |
1 / 2 |
CRAP | |
0.00% |
0 / 1 |
| IdentityVerificationService | |
90.91% |
10 / 11 |
|
50.00% |
1 / 2 |
3.01 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| markIdentityVerifiedIfApplicable | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
2.00 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace App\Domain\Document\Service; |
| 6 | |
| 7 | use PDO; |
| 8 | use RuntimeException; |
| 9 | |
| 10 | /** |
| 11 | * FSC-138: bridges a completed ID-verified document to the KYC workflow. |
| 12 | * |
| 13 | * PandaDoc ID Check gates signing behind a government-ID proof, so a document |
| 14 | * reaching `document.completed` means the recipient passed identity |
| 15 | * verification. When that document's template required ID verification, we |
| 16 | * advance the investor pending → in_review so they surface in the admin KYC |
| 17 | * queue. The verified/rejected decision remains a manual admin step — ID Check |
| 18 | * is identity proofing only, not AML or accreditation. |
| 19 | * |
| 20 | * The advance is idempotent: it only moves investors still in `pending`, so |
| 21 | * repeated reads (this runs as a side effect of fetching document status) never |
| 22 | * clobber a decision an admin has already made. |
| 23 | */ |
| 24 | final readonly class IdentityVerificationService |
| 25 | { |
| 26 | public function __construct( |
| 27 | private PDO $pdo, |
| 28 | ) {} |
| 29 | |
| 30 | /** |
| 31 | * Advance the investor to `in_review` when a completed document belongs to |
| 32 | * an ID-verification template. No-op otherwise. |
| 33 | * |
| 34 | * @param int $investorId |
| 35 | * @param string $pandadocId The document that just reached completion |
| 36 | * @throws RuntimeException on statement failure |
| 37 | */ |
| 38 | public function markIdentityVerifiedIfApplicable(int $investorId, string $pandadocId): void |
| 39 | { |
| 40 | $stmt = $this->pdo->prepare( |
| 41 | "UPDATE public.investors |
| 42 | SET kyc_status = 'in_review', updated_at = CURRENT_TIMESTAMP |
| 43 | WHERE investor_id = :investorId |
| 44 | AND kyc_status = 'pending' |
| 45 | AND EXISTS ( |
| 46 | SELECT 1 |
| 47 | FROM public.investor_documents d |
| 48 | JOIN public.document_templates t ON t.template_id = d.template_id |
| 49 | WHERE d.pandadoc_id = :pandadocId |
| 50 | AND t.requires_id_verification = true |
| 51 | )", |
| 52 | ); |
| 53 | |
| 54 | if ($stmt === false) { |
| 55 | throw new RuntimeException('Failed to prepare identity verification statement'); |
| 56 | } |
| 57 | |
| 58 | $stmt->execute([ |
| 59 | 'investorId' => $investorId, |
| 60 | 'pandadocId' => $pandadocId, |
| 61 | ]); |
| 62 | } |
| 63 | } |