Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.80% covered (warning)
89.80%
44 / 49
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ProvidedDocumentRepository
89.80% covered (warning)
89.80%
44 / 49
16.67% covered (danger)
16.67%
1 / 6
16.27
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
 create
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
2.00
 findByInvestorId
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 findById
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 findByIdForInvestor
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 delete
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 listSelect
n/a
0 / 0
n/a
0 / 0
1
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Document\Provided\Repository;
6
7use App\Domain\Document\Provided\Data\ProvidedDocumentData;
8use PDO;
9use RuntimeException;
10
11/**
12 * CRUD for admin_provided_documents — documents an admin attaches for an
13 * investor to download (FSC-57). No state machine; metadata is inline.
14 */
15final readonly class ProvidedDocumentRepository
16{
17    public function __construct(
18        private PDO $pdo,
19    ) {}
20
21    public function create(
22        int $investorId,
23        string $title,
24        ?string $description,
25        string $fileStorageKey,
26        string $originalFilename,
27        string $mimeType,
28        int $sizeBytes,
29        string $sha256,
30        ?int $uploadedByUserId,
31    ): int {
32        $stmt = $this->pdo->prepare(
33            'INSERT INTO admin_provided_documents (
34                investor_id, title, description, file_storage_key,
35                original_filename, mime_type, size_bytes, sha256, uploaded_by_user_id
36             ) VALUES (
37                :investorId, :title, :description, :fileStorageKey,
38                :originalFilename, :mimeType, :sizeBytes, :sha256, :uploadedByUserId
39             ) RETURNING provided_document_id',
40        );
41
42        if ($stmt === false) {
43            throw new RuntimeException('Failed to prepare statement');
44        }
45
46        $stmt->execute([
47            'investorId' => $investorId,
48            'title' => $title,
49            'description' => $description,
50            'fileStorageKey' => $fileStorageKey,
51            'originalFilename' => $originalFilename,
52            'mimeType' => $mimeType,
53            'sizeBytes' => $sizeBytes,
54            'sha256' => $sha256,
55            'uploadedByUserId' => $uploadedByUserId,
56        ]);
57
58        return (int)$stmt->fetchColumn();
59    }
60
61    /**
62     * @param int $investorId
63     * @return list<ProvidedDocumentData>
64     */
65    public function findByInvestorId(int $investorId): array
66    {
67        $stmt = $this->pdo->prepare($this->listSelect() . <<<SQL
68                WHERE d.investor_id = :investorId
69                ORDER BY d.created_at DESC
70            SQL);
71
72        if ($stmt === false) {
73            throw new RuntimeException('Failed to prepare statement');
74        }
75
76        $stmt->execute(['investorId' => $investorId]);
77
78        $documents = [];
79        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
80            if (is_array($row)) {
81                $documents[] = ProvidedDocumentData::fromRow($row);
82            }
83        }
84
85        return $documents;
86    }
87
88    public function findById(int $providedDocumentId): ?ProvidedDocumentData
89    {
90        $stmt = $this->pdo->prepare($this->listSelect() . <<<SQL
91                WHERE d.provided_document_id = :id
92            SQL);
93
94        if ($stmt === false) {
95            throw new RuntimeException('Failed to prepare statement');
96        }
97
98        $stmt->execute(['id' => $providedDocumentId]);
99        $row = $stmt->fetch(PDO::FETCH_ASSOC);
100
101        return is_array($row) ? ProvidedDocumentData::fromRow($row) : null;
102    }
103
104    public function findByIdForInvestor(int $providedDocumentId, int $investorId): ?ProvidedDocumentData
105    {
106        $stmt = $this->pdo->prepare($this->listSelect() . <<<SQL
107                WHERE d.provided_document_id = :id AND d.investor_id = :investorId
108            SQL);
109
110        if ($stmt === false) {
111            throw new RuntimeException('Failed to prepare statement');
112        }
113
114        $stmt->execute(['id' => $providedDocumentId, 'investorId' => $investorId]);
115        $row = $stmt->fetch(PDO::FETCH_ASSOC);
116
117        return is_array($row) ? ProvidedDocumentData::fromRow($row) : null;
118    }
119
120    public function delete(int $providedDocumentId): void
121    {
122        $stmt = $this->pdo->prepare(
123            'DELETE FROM admin_provided_documents WHERE provided_document_id = :id',
124        );
125
126        if ($stmt === false) {
127            throw new RuntimeException('Failed to prepare statement');
128        }
129
130        $stmt->execute(['id' => $providedDocumentId]);
131    }
132
133    private function listSelect(): string
134    {
135        return <<<SQL
136                SELECT
137                    d.provided_document_id AS "providedDocumentId",
138                    d.investor_id          AS "investorId",
139                    d.title,
140                    d.description,
141                    d.file_storage_key     AS "fileStorageKey",
142                    d.original_filename    AS "originalFilename",
143                    d.mime_type            AS "mimeType",
144                    d.size_bytes           AS "sizeBytes",
145                    d.sha256,
146                    u.username             AS "uploadedByName",
147                    d.created_at           AS "createdAt"
148                FROM admin_provided_documents d
149                LEFT JOIN users u ON u.user_id = d.uploaded_by_user_id
150
151            SQL;
152    }
153}