Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.66% covered (success)
92.66%
101 / 109
11.11% covered (danger)
11.11%
1 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
InvestorRepository
92.66% covered (success)
92.66%
101 / 109
11.11% covered (danger)
11.11%
1 / 9
25.25
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
 createInvestor
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
2
 findInvestorById
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 findInvestorByEmail
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 findInvestorByUserId
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 emailExists
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 updateInvestor
96.77% covered (success)
96.77%
30 / 31
0.00% covered (danger)
0.00%
0 / 1
5
 updateKycStatus
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 updateInvestorStatus
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Investor\Repository;
6
7use App\Domain\Investor\Data\InvestorData;
8use PDO;
9use RuntimeException;
10
11use function in_array;
12
13final class InvestorRepository
14{
15    public function __construct(
16        private readonly PDO $pdo,
17    ) {}
18
19    /**
20     * @param array<string, mixed> $data
21     */
22    public function createInvestor(array $data): int
23    {
24        $sql = 'INSERT INTO investors (
25                    first_name, last_name, email, phone, date_of_birth,
26                    ssn_encrypted, address_line1, address_line2, city, state,
27                    zip_code, country, kyc_status, status
28                ) VALUES (
29                    :first_name, :last_name, :email, :phone, :date_of_birth,
30                    :ssn_encrypted, :address_line1, :address_line2, :city, :state,
31                    :zip_code, :country, :kyc_status, :status
32                ) RETURNING investor_id';
33
34        $stmt = $this->pdo->prepare($sql);
35
36        if ($stmt === false) {
37            throw new RuntimeException('Failed to prepare statement');
38        }
39        $stmt->execute([
40            'first_name' => $data['firstName'],
41            'last_name' => $data['lastName'],
42            'email' => $data['email'],
43            'phone' => $data['phone'] ?? null,
44            'date_of_birth' => $data['dateOfBirth'],
45            'ssn_encrypted' => $data['ssnEncrypted'] ?? null,
46            'address_line1' => $data['addressLine1'] ?? null,
47            'address_line2' => $data['addressLine2'] ?? null,
48            'city' => $data['city'] ?? null,
49            'state' => $data['state'] ?? null,
50            'zip_code' => $data['zipCode'] ?? null,
51            'country' => $data['country'] ?? 'USA',
52            'kyc_status' => $data['kycStatus'] ?? 'pending',
53            'status' => $data['status'] ?? 'active',
54        ]);
55
56        return (int)$stmt->fetchColumn();
57    }
58
59    public function findInvestorById(int $investorId): ?InvestorData
60    {
61        $sql = 'SELECT
62                    investor_id as "investorId",
63                    first_name as "firstName",
64                    last_name as "lastName",
65                    email,
66                    phone,
67                    date_of_birth as "dateOfBirth",
68                    ssn_encrypted as "ssnEncrypted",
69                    address_line1 as "addressLine1",
70                    address_line2 as "addressLine2",
71                    city,
72                    state,
73                    zip_code as "zipCode",
74                    country,
75                    kyc_status as "kycStatus",
76                    status,
77                    accredited,
78                    accredited_expires as "accreditedExpires",
79                    created_at as "createdAt",
80                    updated_at as "updatedAt"
81                FROM investors
82                WHERE investor_id = :investor_id';
83        $stmt = $this->pdo->prepare($sql);
84        if ($stmt === false) {
85            throw new RuntimeException('Failed to prepare statement');
86        }
87        $stmt->execute(['investor_id' => $investorId]);
88
89        $row = $stmt->fetch(PDO::FETCH_ASSOC);
90
91        return $row !== false ? InvestorData::fromRow($row) : null;
92    }
93
94    public function findInvestorByEmail(string $email): ?InvestorData
95    {
96        $sql = 'SELECT
97                    investor_id as "investorId",
98                    first_name as "firstName",
99                    last_name as "lastName",
100                    email,
101                    phone,
102                    date_of_birth as "dateOfBirth",
103                    ssn_encrypted as "ssnEncrypted",
104                    address_line1 as "addressLine1",
105                    address_line2 as "addressLine2",
106                    city,
107                    state,
108                    zip_code as "zipCode",
109                    country,
110                    kyc_status as "kycStatus",
111                    status,
112                    accredited,
113                    accredited_expires as "accreditedExpires",
114                    created_at as "createdAt",
115                    updated_at as "updatedAt"
116                FROM investors
117                WHERE email = :email';
118        $stmt = $this->pdo->prepare($sql);
119        if ($stmt === false) {
120            throw new RuntimeException('Failed to prepare statement');
121        }
122        $stmt->execute(['email' => $email]);
123
124        $row = $stmt->fetch(PDO::FETCH_ASSOC);
125
126        return $row !== false ? InvestorData::fromRow($row) : null;
127    }
128
129    public function findInvestorByUserId(int $userId): ?InvestorData
130    {
131        $sql = 'SELECT i.investor_id AS "investorId",
132                       i.first_name AS "firstName",
133                       i.last_name AS "lastName",
134                       i.email,
135                       i.phone,
136                       i.date_of_birth AS "dateOfBirth",
137                       i.address_line1 AS "addressLine1",
138                       i.address_line2 AS "addressLine2",
139                       i.city,
140                       i.state,
141                       i.zip_code AS "zipCode",
142                       i.country,
143                       i.kyc_status AS "kycStatus",
144                       i.status,
145                       i.accredited,
146                       i.accredited_expires AS "accreditedExpires",
147                       i.created_at AS "createdAt",
148                       i.updated_at AS "updatedAt"
149                FROM investors i
150                INNER JOIN users u ON u.investor_id = i.investor_id
151                WHERE u.user_id = :user_id';
152
153        $stmt = $this->pdo->prepare($sql);
154
155        if ($stmt === false) {
156            throw new RuntimeException('Failed to prepare statement');
157        }
158
159        $stmt->execute(['user_id' => $userId]);
160        $row = $stmt->fetch(PDO::FETCH_ASSOC);
161
162        return $row !== false ? InvestorData::fromRow($row) : null;
163    }
164
165    public function emailExists(string $email): bool
166    {
167        $stmt = $this->pdo->prepare(
168            'SELECT EXISTS(SELECT 1 FROM investors WHERE email = :email)',
169        );
170
171        if ($stmt === false) {
172            throw new RuntimeException('Failed to prepare statement');
173        }
174        $stmt->execute(['email' => $email]);
175
176        return (bool)$stmt->fetchColumn();
177    }
178
179    /**
180     * @param array<string, mixed> $data
181     * @param int $investorId
182     */
183    public function updateInvestor(int $investorId, array $data): bool
184    {
185        $fieldMapping = [
186            'firstName' => 'first_name',
187            'lastName' => 'last_name',
188            'phone' => 'phone',
189            'addressLine1' => 'address_line1',
190            'addressLine2' => 'address_line2',
191            'city' => 'city',
192            'state' => 'state',
193            'zipCode' => 'zip_code',
194            'country' => 'country',
195            'kycStatus' => 'kyc_status',
196            'status' => 'status',
197            'accredited' => 'accredited',
198            'accreditedExpires' => 'accredited_expires',
199        ];
200
201        $updates = [];
202        $params = ['investor_id' => $investorId];
203
204        foreach ($data as $camelKey => $value) {
205            if (isset($fieldMapping[$camelKey])) {
206                $snakeKey = $fieldMapping[$camelKey];
207                $updates[] = "{$snakeKey} = :{$snakeKey}";
208                $params[$snakeKey] = $value;
209            }
210        }
211
212        if (empty($updates)) {
213            return false;
214        }
215
216        $updates[] = 'updated_at = CURRENT_TIMESTAMP';
217
218        $stmt = $this->pdo->prepare(
219            'UPDATE investors SET ' . implode(', ', $updates) . ' WHERE investor_id = :investor_id',
220        );
221
222        if ($stmt === false) {
223            throw new RuntimeException('Failed to prepare statement');
224        }
225
226        return $stmt->execute($params);
227    }
228
229    public function updateKycStatus(int $investorId, string $status): bool
230    {
231        if (!in_array($status, ['pending', 'verified', 'rejected'], true)) {
232            return false;
233        }
234
235        $stmt = $this->pdo->prepare(
236            'UPDATE investors SET kyc_status = :status, updated_at = CURRENT_TIMESTAMP
237             WHERE investor_id = :investor_id',
238        );
239
240        if ($stmt === false) {
241            throw new RuntimeException('Failed to prepare statement');
242        }
243
244        return $stmt->execute([
245            'status' => $status,
246            'investor_id' => $investorId,
247        ]);
248    }
249
250    public function updateInvestorStatus(int $investorId, string $status): bool
251    {
252        if (!in_array($status, ['active', 'inactive', 'suspended'], true)) {
253            return false;
254        }
255
256        $stmt = $this->pdo->prepare(
257            'UPDATE investors SET status = :status, updated_at = CURRENT_TIMESTAMP
258             WHERE investor_id = :investor_id',
259        );
260
261        if ($stmt === false) {
262            throw new RuntimeException('Failed to prepare statement');
263        }
264
265        return $stmt->execute([
266            'status' => $status,
267            'investor_id' => $investorId,
268        ]);
269    }
270}