Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.86% covered (success)
90.86%
159 / 175
13.33% covered (danger)
13.33%
2 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
SuperAdminRepository
90.86% covered (success)
90.86%
159 / 175
13.33% covered (danger)
13.33%
2 / 15
34.88
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
 createUser
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
2.00
 createInvestor
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
2
 createAccount
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 createTransaction
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
2.00
 getAccountById
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 getAccountBalance
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 updateAccountBalance
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
2.00
 updateAccountStatus
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
2.00
 getAllAccountsWithInvestors
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 emailExistsInUsers
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 emailExistsInInvestors
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 countTestData
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
5.00
 deleteTestData
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 generateUniqueAccountNumber
69.23% covered (warning)
69.23%
9 / 13
0.00% covered (danger)
0.00%
0 / 1
4.47
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\SuperAdmin\Repository;
6
7use PDO;
8use RuntimeException;
9
10use function sprintf;
11
12/**
13 * Repository for Super Admin test data generation.
14 */
15final class SuperAdminRepository
16{
17    public function __construct(
18        private readonly PDO $pdo,
19    ) {}
20
21    public function createUser(
22        string $email,
23        string $username,
24        string $passwordHash,
25        string $role = 'investor',
26    ): int {
27        $stmt = $this->pdo->prepare(
28            'INSERT INTO users (email, username, password_hash, role, is_active)
29                VALUES (:email, :username, :password_hash, :role, true)
30             RETURNING user_id',
31        );
32
33        if ($stmt === false) {
34            throw new RuntimeException('Failed to prepare statement');
35        }
36        $stmt->execute([
37            'email' => $email,
38            'username' => $username,
39            'password_hash' => $passwordHash,
40            'role' => $role,
41        ]);
42
43        return (int)$stmt->fetchColumn();
44    }
45
46    /**
47     * @param array<string, mixed> $data
48     */
49    public function createInvestor(array $data): int
50    {
51        $stmt = $this->pdo->prepare(
52            'INSERT INTO investors (
53                    first_name, last_name, email, phone, date_of_birth,
54                    address_line1, city, state, zip_code, country,
55                    kyc_status, status, created_at
56                ) VALUES (
57                    :first_name, :last_name, :email, :phone, :date_of_birth,
58                    :address_line1, :city, :state, :zip_code, :country,
59                    :kyc_status, :status, :created_at
60                )
61            RETURNING investor_id',
62        );
63
64        if ($stmt === false) {
65            throw new RuntimeException('Failed to prepare statement');
66        }
67        $stmt->execute([
68            'first_name' => $data['firstName'],
69            'last_name' => $data['lastName'],
70            'email' => $data['email'],
71            'phone' => $data['phone'] ?? null,
72            'date_of_birth' => $data['dateOfBirth'],
73            'address_line1' => $data['addressLine1'] ?? null,
74            'city' => $data['city'] ?? null,
75            'state' => $data['state'] ?? null,
76            'zip_code' => $data['zipCode'] ?? null,
77            'country' => $data['country'] ?? 'USA',
78            'kyc_status' => $data['kycStatus'] ?? 'verified',
79            'status' => $data['status'] ?? 'active',
80            'created_at' => $data['createdAt'] ?? date('Y-m-d H:i:s'),
81        ]);
82
83        return (int)$stmt->fetchColumn();
84    }
85
86    /**
87     * @param int $investorId
88     * @param string $status
89     * @param ?string $createdAt
90     * @return array{accountId: int, accountNumber: string}
91     */
92    public function createAccount(
93        int $investorId,
94        string $status = 'pending',
95        ?string $createdAt = null,
96    ): array {
97        $accountNumber = $this->generateUniqueAccountNumber();
98        $createdDate = $createdAt ?? date('Y-m-d H:i:s');
99
100        $stmt = $this->pdo->prepare(
101            'INSERT INTO accounts (
102                    investor_id, account_number,
103                    interest_rate, loan_to_value_ratio, status, opened_date, created_at
104                ) VALUES (
105                    :investor_id, :account_number,
106                    :interest_rate, :loan_to_value_ratio, :status, :opened_date, :created_at
107                )
108            RETURNING account_id',
109        );
110
111        if ($stmt === false) {
112            throw new RuntimeException('Failed to prepare statement');
113        }
114
115        $stmt->execute([
116            'investor_id' => $investorId,
117            'account_number' => $accountNumber,
118            'interest_rate' => 0.08,
119            'loan_to_value_ratio' => 0.80,
120            'status' => $status,
121            'opened_date' => substr($createdDate, 0, 10),
122            'created_at' => $createdDate,
123        ]);
124
125        return [
126            'accountId' => (int)$stmt->fetchColumn(),
127            'accountNumber' => $accountNumber,
128        ];
129    }
130
131    public function createTransaction(
132        int $accountId,
133        string $type,
134        float $amount,
135        float $balanceAfter,
136        string $description,
137        ?string $referenceNumber,
138        string $createdAt,
139    ): int {
140        $stmt = $this->pdo->prepare(
141            'INSERT INTO transactions (
142                    account_id, transaction_type, amount, balance_after,
143                    description, reference_number, status, created_at
144                ) VALUES (
145                    :account_id, :transaction_type, :amount, :balance_after,
146                    :description, :reference_number, :status, :created_at
147                )
148            RETURNING transaction_id',
149        );
150
151        if ($stmt === false) {
152            throw new RuntimeException('Failed to prepare statement');
153        }
154        $stmt->execute([
155            'account_id' => $accountId,
156            'transaction_type' => $type,
157            'amount' => $amount,
158            'balance_after' => $balanceAfter,
159            'description' => $description,
160            'reference_number' => $referenceNumber,
161            'status' => 'completed',
162            'created_at' => $createdAt,
163        ]);
164
165        return (int)$stmt->fetchColumn();
166    }
167
168    /**
169     * @param int $accountId
170     * @return array<string, mixed>|null
171     */
172    public function getAccountById(int $accountId): ?array
173    {
174        $stmt = $this->pdo->prepare(
175            'SELECT
176                    account_id AS "accountId",
177                    investor_id AS "investorId",
178                    account_number AS "accountNumber",
179                    balance::TEXT AS balance,
180                    status,
181                    created_at AS "createdAt"
182                FROM accounts
183            WHERE account_id = :account_id',
184        );
185
186        if ($stmt === false) {
187            throw new RuntimeException('Failed to prepare statement');
188        }
189        $stmt->execute(['account_id' => $accountId]);
190
191        $row = $stmt->fetch(PDO::FETCH_ASSOC);
192
193        return $row !== false ? $row : null;
194    }
195
196    public function getAccountBalance(int $accountId): string
197    {
198        $stmt = $this->pdo->prepare(
199            'SELECT balance::TEXT FROM accounts WHERE account_id = :account_id',
200        );
201
202        if ($stmt === false) {
203            throw new RuntimeException('Failed to prepare statement');
204        }
205        $stmt->execute(['account_id' => $accountId]);
206
207        return (string)$stmt->fetchColumn();
208    }
209
210    public function updateAccountBalance(int $accountId, float $newBalance): bool
211    {
212        $stmt = $this->pdo->prepare(
213            'UPDATE accounts
214                SET balance = :balance,
215                    available_balance = :balance,
216                    updated_at = CURRENT_TIMESTAMP
217             WHERE account_id = :account_id',
218        );
219
220        if ($stmt === false) {
221            throw new RuntimeException('Failed to prepare statement');
222        }
223
224        return $stmt->execute([
225            'balance' => $newBalance,
226            'account_id' => $accountId,
227        ]);
228    }
229
230    public function updateAccountStatus(int $accountId, string $status): bool
231    {
232        $stmt = $this->pdo->prepare(
233            'UPDATE accounts SET status = :status WHERE account_id = :account_id',
234        );
235
236        if ($stmt === false) {
237            throw new RuntimeException('Failed to prepare statement');
238        }
239        $stmt->execute([
240            'account_id' => $accountId,
241            'status' => $status,
242        ]);
243
244        return $stmt->rowCount() > 0;
245    }
246
247    /**
248     * @return array<int, array<string, mixed>>
249     */
250    public function getAllAccountsWithInvestors(): array
251    {
252        $stmt = $this->pdo->query(
253            'SELECT
254                    a.account_id AS "accountId",
255                    a.account_number AS "accountNumber",
256                    a.balance::TEXT AS balance,
257                    a.status,
258                    i.first_name || \' \' || i.last_name AS "investorName",
259                    i.email
260                FROM accounts a
261                INNER JOIN investors i ON i.investor_id = a.investor_id
262            ORDER BY a.created_at DESC',
263        );
264
265        if ($stmt === false) {
266            throw new RuntimeException('Failed to execute query');
267        }
268
269        return $stmt->fetchAll(PDO::FETCH_ASSOC);
270    }
271
272    public function emailExistsInUsers(string $email): bool
273    {
274        $stmt = $this->pdo->prepare(
275            'SELECT EXISTS(SELECT 1 FROM users WHERE email = :email)',
276        );
277
278        if ($stmt === false) {
279            throw new RuntimeException('Failed to prepare statement');
280        }
281        $stmt->execute(['email' => $email]);
282
283        return (bool)$stmt->fetchColumn();
284    }
285
286    public function emailExistsInInvestors(string $email): bool
287    {
288        $stmt = $this->pdo->prepare(
289            'SELECT EXISTS(SELECT 1 FROM investors WHERE email = :email)',
290        );
291
292        if ($stmt === false) {
293            throw new RuntimeException('Failed to prepare statement');
294        }
295        $stmt->execute(['email' => $email]);
296
297        return (bool)$stmt->fetchColumn();
298    }
299
300    /**
301     * @param string $emailPattern
302     * @return array{users: int, investors: int, accounts: int, transactions: int}
303     */
304    public function countTestData(string $emailPattern = '%@testdata.local'): array
305    {
306        $users = $this->pdo->prepare('SELECT COUNT(*) FROM users WHERE email LIKE :pattern');
307
308        $investors = $this->pdo->prepare('SELECT COUNT(*) FROM investors WHERE email LIKE :pattern');
309
310        $accounts = $this->pdo->prepare('
311            SELECT COUNT(*) FROM accounts WHERE investor_id IN (
312                SELECT investor_id FROM investors WHERE email LIKE :pattern
313            )
314        ');
315
316        $transactions = $this->pdo->prepare('
317            SELECT COUNT(*) FROM transactions WHERE account_id IN (
318                SELECT account_id FROM accounts WHERE investor_id IN (
319                    SELECT investor_id FROM investors WHERE email LIKE :pattern
320                )
321            )
322        ');
323        if ($users === false || $investors === false || $accounts === false || $transactions === false) {
324            throw new RuntimeException('Failed to prepare statement');
325        }
326
327        $users->execute(['pattern' => $emailPattern]);
328        $investors->execute(['pattern' => $emailPattern]);
329        $accounts->execute(['pattern' => $emailPattern]);
330        $transactions->execute(['pattern' => $emailPattern]);
331
332        return [
333            'users' => (int)$users->fetchColumn(),
334            'investors' => (int)$investors->fetchColumn(),
335            'accounts' => (int)$accounts->fetchColumn(),
336            'transactions' => (int)$transactions->fetchColumn(),
337        ];
338    }
339
340    /**
341     * @param string $emailPattern
342     * @return array{users: int, investors: int, accounts: int, transactions: int}
343     */
344    public function deleteTestData(string $emailPattern = '%@testdata.local'): array
345    {
346        $counts = $this->countTestData($emailPattern);
347
348        $this->pdo->exec("
349            DELETE FROM transactions WHERE account_id IN (
350                SELECT account_id FROM accounts WHERE investor_id IN (
351                    SELECT investor_id FROM investors WHERE email LIKE '{$emailPattern}'
352                )
353            )
354        ");
355
356        $this->pdo->exec("
357            DELETE FROM accounts WHERE investor_id IN (
358                SELECT investor_id FROM investors WHERE email LIKE '{$emailPattern}'
359            )
360        ");
361
362        $this->pdo->exec("
363            DELETE FROM user_sessions WHERE user_id IN (
364                SELECT user_id FROM users WHERE email LIKE '{$emailPattern}'
365            )
366        ");
367
368        $this->pdo->exec("DELETE FROM users WHERE email LIKE '{$emailPattern}'");
369        $this->pdo->exec("DELETE FROM investors WHERE email LIKE '{$emailPattern}'");
370
371        return $counts;
372    }
373
374    private function generateUniqueAccountNumber(int $maxAttempts = 10): string
375    {
376        for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
377            $accountNumber = sprintf('INV-%05d', random_int(1, 99999));
378
379            $stmt = $this->pdo->prepare(
380                'SELECT EXISTS(SELECT 1 FROM accounts WHERE account_number = :account_number)',
381            );
382
383            if ($stmt === false) {
384                throw new RuntimeException('Failed to prepare statement');
385            }
386            $stmt->execute(['account_number' => $accountNumber]);
387
388            if (!$stmt->fetchColumn()) {
389                return $accountNumber;
390            }
391        }
392
393        throw new RuntimeException(
394            "Failed to generate unique account number after {$maxAttempts} attempts",
395        );
396    }
397}