Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.88% covered (success)
93.88%
46 / 49
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
AdminRepository
93.88% covered (success)
93.88%
46 / 49
25.00% covered (danger)
25.00%
1 / 4
16.06
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
 getStats
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 getInvestorsList
96.97% covered (success)
96.97%
32 / 33
0.00% covered (danger)
0.00%
0 / 1
10
 getInvestorDetail
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Admin\Repository;
6
7use App\Domain\Admin\Data\AdminStatsData;
8use PDO;
9use RuntimeException;
10
11use function count;
12
13/**
14 * Repository for admin-specific data access operations.
15 */
16final class AdminRepository
17{
18    public function __construct(
19        private readonly PDO $pdo
20    ) {}
21
22    /**
23     * Get platform-wide statistics for admin dashboard.
24     */
25    public function getStats(): AdminStatsData
26    {
27        $sql = "
28            SELECT
29                (SELECT COUNT(*) FROM investors) as \"totalInvestors\",
30                (SELECT COUNT(*) FROM investors WHERE status = 'active') as \"activeInvestors\",
31                (SELECT COUNT(*) FROM investors WHERE kyc_status = 'pending') as \"pendingKyc\",
32                (SELECT COUNT(*) FROM accounts) as \"totalAccounts\",
33                (SELECT COUNT(*) FROM accounts WHERE status = 'active') as \"activeAccounts\",
34                (SELECT COUNT(*) FROM accounts WHERE status = 'pending') as \"pendingAccounts\",
35                (SELECT COALESCE(SUM(balance), 0)::TEXT FROM accounts) as \"totalAum\",
36                (SELECT COALESCE(SUM(available_for_loan), 0)::TEXT FROM accounts) as \"totalAvailableForLoan\"
37        ";
38
39        $stmt = $this->pdo->query($sql);
40
41        if ($stmt === false) {
42            throw new RuntimeException('Failed to execute stats query');
43        }
44
45        $row = $stmt->fetch(PDO::FETCH_ASSOC);
46
47        return new AdminStatsData($row);
48    }
49
50    /**
51     * Get paginated list of all investors with account info.
52     *
53     * @param int $page
54     * @param int $limit
55     * @param string|null $search Search by name or email
56     * @param string|null $kycStatus Filter by KYC status
57     * @param string|null $status Filter by investor status
58     *
59     * @return array{investors: array<int, array<string, mixed>>, total: int}
60     */
61    public function getInvestorsList(
62        int $page = 1,
63        int $limit = 20,
64        ?string $search = null,
65        ?string $kycStatus = null,
66        ?string $status = null,
67    ): array {
68        $offset = ($page - 1) * $limit;
69        $params = [];
70        $whereClauses = [];
71
72        // Build WHERE clauses
73        if ($search !== null && $search !== '') {
74            $whereClauses[] = '(i.first_name ILIKE :search OR i.last_name ILIKE :search OR i.email ILIKE :search)';
75            $params['search'] = '%' . $search . '%';
76        }
77
78        if ($kycStatus !== null && $kycStatus !== '') {
79            $whereClauses[] = 'i.kyc_status = :kyc_status';
80            $params['kyc_status'] = $kycStatus;
81        }
82
83        if ($status !== null && $status !== '') {
84            $whereClauses[] = 'i.status = :status';
85            $params['status'] = $status;
86        }
87
88        $whereClause = count($whereClauses) > 0 ? 'WHERE ' . implode(' AND ', $whereClauses) : '';
89
90        // Count total
91        $countSql = "SELECT COUNT(*) FROM investors i {$whereClause}";
92        $countStmt = $this->pdo->prepare($countSql);
93        $countStmt->execute($params);
94        $total = (int)$countStmt->fetchColumn();
95
96        // Get investors with account data
97        $sql = "
98            SELECT
99                i.investor_id as \"investorId\",
100                i.first_name as \"firstName\",
101                i.last_name as \"lastName\",
102                i.email,
103                i.phone,
104                i.kyc_status as \"kycStatus\",
105                i.status,
106                i.created_at as \"createdAt\",
107                a.account_id as \"accountId\",
108                a.account_number as \"accountNumber\",
109                a.balance::TEXT as balance,
110                a.status as \"accountStatus\",
111                u.user_id as \"userId\"
112            FROM investors i
113            LEFT JOIN accounts a ON i.investor_id = a.investor_id
114            LEFT JOIN users u ON i.investor_id = u.investor_id
115            {$whereClause}
116            ORDER BY i.created_at DESC
117            LIMIT :limit OFFSET :offset
118        ";
119
120        $stmt = $this->pdo->prepare($sql);
121
122        if ($stmt === false) {
123            throw new RuntimeException('Failed to prepare investors list statement');
124        }
125
126        foreach ($params as $key => $value) {
127            $stmt->bindValue($key, $value);
128        }
129        $stmt->bindValue('limit', $limit, PDO::PARAM_INT);
130        $stmt->bindValue('offset', $offset, PDO::PARAM_INT);
131
132        $stmt->execute();
133        $investors = $stmt->fetchAll(PDO::FETCH_ASSOC);
134
135        return [
136            'investors' => $investors,
137            'total' => $total,
138        ];
139    }
140
141    /**
142     * Get detailed investor info for admin view.
143     *
144     * @param int $investorId
145     *
146     * @return array<string, mixed>|null
147     */
148    public function getInvestorDetail(int $investorId): ?array
149    {
150        $sql = '
151            SELECT
152                i.investor_id as "investorId",
153                i.first_name as "firstName",
154                i.last_name as "lastName",
155                i.email,
156                i.phone,
157                i.date_of_birth as "dateOfBirth",
158                i.address_line1 as "addressLine1",
159                i.address_line2 as "addressLine2",
160                i.city,
161                i.state,
162                i.zip_code as "zipCode",
163                i.country,
164                i.kyc_status as "kycStatus",
165                i.status,
166                i.created_at as "createdAt",
167                i.updated_at as "updatedAt",
168                a.account_id as "accountId",
169                a.account_number as "accountNumber",
170                a.balance::TEXT as balance,
171                a.available_balance::TEXT as "availableBalance",
172                a.available_for_loan::TEXT as "availableForLoan",
173                a.interest_rate::TEXT as "interestRate",
174                a.loan_to_value_ratio::TEXT as "loanToValueRatio",
175                a.status as "accountStatus",
176                a.opened_date as "openedDate",
177                u.user_id as "userId",
178                u.username,
179                u.role
180            FROM investors i
181            LEFT JOIN accounts a ON i.investor_id = a.investor_id
182            LEFT JOIN users u ON i.investor_id = u.investor_id
183            WHERE i.investor_id = :investor_id
184        ';
185
186        $stmt = $this->pdo->prepare($sql);
187
188        if ($stmt === false) {
189            throw new RuntimeException('Failed to prepare investor detail statement');
190        }
191
192        $stmt->execute(['investor_id' => $investorId]);
193
194        $row = $stmt->fetch(PDO::FETCH_ASSOC);
195
196        return $row ?: null;
197    }
198}