Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.52% covered (success)
92.52%
99 / 107
11.11% covered (danger)
11.11%
1 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
InvestorRepository
92.52% covered (success)
92.52%
99 / 107
11.11% covered (danger)
11.11%
1 / 9
25.26
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.55% covered (success)
96.55%
28 / 29
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                    created_at as "createdAt",
78                    updated_at as "updatedAt"
79                FROM investors
80                WHERE investor_id = :investor_id';
81        $stmt = $this->pdo->prepare($sql);
82        if ($stmt === false) {
83            throw new RuntimeException('Failed to prepare statement');
84        }
85        $stmt->execute(['investor_id' => $investorId]);
86
87        $row = $stmt->fetch(PDO::FETCH_ASSOC);
88
89        return $row !== false ? InvestorData::fromRow($row) : null;
90    }
91
92    public function findInvestorByEmail(string $email): ?InvestorData
93    {
94        $sql = 'SELECT
95                    investor_id as "investorId",
96                    first_name as "firstName",
97                    last_name as "lastName",
98                    email,
99                    phone,
100                    date_of_birth as "dateOfBirth",
101                    ssn_encrypted as "ssnEncrypted",
102                    address_line1 as "addressLine1",
103                    address_line2 as "addressLine2",
104                    city,
105                    state,
106                    zip_code as "zipCode",
107                    country,
108                    kyc_status as "kycStatus",
109                    status,
110                    created_at as "createdAt",
111                    updated_at as "updatedAt"
112                FROM investors
113                WHERE email = :email';
114        $stmt = $this->pdo->prepare($sql);
115        if ($stmt === false) {
116            throw new RuntimeException('Failed to prepare statement');
117        }
118        $stmt->execute(['email' => $email]);
119
120        $row = $stmt->fetch(PDO::FETCH_ASSOC);
121
122        return $row !== false ? InvestorData::fromRow($row) : null;
123    }
124
125    public function findInvestorByUserId(int $userId): ?InvestorData
126    {
127        $sql = 'SELECT i.investor_id AS "investorId",
128                       i.first_name AS "firstName",
129                       i.last_name AS "lastName",
130                       i.email,
131                       i.phone,
132                       i.date_of_birth AS "dateOfBirth",
133                       i.address_line1 AS "addressLine1",
134                       i.address_line2 AS "addressLine2",
135                       i.city,
136                       i.state,
137                       i.zip_code AS "zipCode",
138                       i.country,
139                       i.kyc_status AS "kycStatus",
140                       i.status,
141                       i.created_at AS "createdAt",
142                       i.updated_at AS "updatedAt"
143                FROM investors i
144                INNER JOIN users u ON u.investor_id = i.investor_id
145                WHERE u.user_id = :user_id';
146
147        $stmt = $this->pdo->prepare($sql);
148
149        if ($stmt === false) {
150            throw new RuntimeException('Failed to prepare statement');
151        }
152
153        $stmt->execute(['user_id' => $userId]);
154        $row = $stmt->fetch(PDO::FETCH_ASSOC);
155
156        return $row !== false ? InvestorData::fromRow($row) : null;
157    }
158
159    public function emailExists(string $email): bool
160    {
161        $stmt = $this->pdo->prepare(
162            'SELECT EXISTS(SELECT 1 FROM investors WHERE email = :email)',
163        );
164
165        if ($stmt === false) {
166            throw new RuntimeException('Failed to prepare statement');
167        }
168        $stmt->execute(['email' => $email]);
169
170        return (bool)$stmt->fetchColumn();
171    }
172
173    /**
174     * @param array<string, mixed> $data
175     * @param int $investorId
176     */
177    public function updateInvestor(int $investorId, array $data): bool
178    {
179        $fieldMapping = [
180            'firstName' => 'first_name',
181            'lastName' => 'last_name',
182            'phone' => 'phone',
183            'addressLine1' => 'address_line1',
184            'addressLine2' => 'address_line2',
185            'city' => 'city',
186            'state' => 'state',
187            'zipCode' => 'zip_code',
188            'country' => 'country',
189            'kycStatus' => 'kyc_status',
190            'status' => 'status',
191        ];
192
193        $updates = [];
194        $params = ['investor_id' => $investorId];
195
196        foreach ($data as $camelKey => $value) {
197            if (isset($fieldMapping[$camelKey])) {
198                $snakeKey = $fieldMapping[$camelKey];
199                $updates[] = "{$snakeKey} = :{$snakeKey}";
200                $params[$snakeKey] = $value;
201            }
202        }
203
204        if (empty($updates)) {
205            return false;
206        }
207
208        $updates[] = 'updated_at = CURRENT_TIMESTAMP';
209
210        $stmt = $this->pdo->prepare(
211            'UPDATE investors SET ' . implode(', ', $updates) . ' WHERE investor_id = :investor_id',
212        );
213
214        if ($stmt === false) {
215            throw new RuntimeException('Failed to prepare statement');
216        }
217
218        return $stmt->execute($params);
219    }
220
221    public function updateKycStatus(int $investorId, string $status): bool
222    {
223        if (!in_array($status, ['pending', 'verified', 'rejected'], true)) {
224            return false;
225        }
226
227        $stmt = $this->pdo->prepare(
228            'UPDATE investors SET kyc_status = :status, updated_at = CURRENT_TIMESTAMP
229             WHERE investor_id = :investor_id',
230        );
231
232        if ($stmt === false) {
233            throw new RuntimeException('Failed to prepare statement');
234        }
235
236        return $stmt->execute([
237            'status' => $status,
238            'investor_id' => $investorId,
239        ]);
240    }
241
242    public function updateInvestorStatus(int $investorId, string $status): bool
243    {
244        if (!in_array($status, ['active', 'inactive', 'suspended'], true)) {
245            return false;
246        }
247
248        $stmt = $this->pdo->prepare(
249            'UPDATE investors SET status = :status, updated_at = CURRENT_TIMESTAMP
250             WHERE investor_id = :investor_id',
251        );
252
253        if ($stmt === false) {
254            throw new RuntimeException('Failed to prepare statement');
255        }
256
257        return $stmt->execute([
258            'status' => $status,
259            'investor_id' => $investorId,
260        ]);
261    }
262}