Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 81 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
| AdminFundPerformanceService | |
0.00% |
0 / 81 |
|
0.00% |
0 / 5 |
702 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| recordMonthlyPerformance | |
0.00% |
0 / 58 |
|
0.00% |
0 / 1 |
342 | |||
| get12MonthPerformance | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
| getAllFundsForPerformance | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| calculateYtdReturn | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace App\Domain\Admin\Service; |
| 6 | |
| 7 | use App\Domain\Funds\Repository\FundsRepository; |
| 8 | use InvalidArgumentException; |
| 9 | use PDO; |
| 10 | |
| 11 | final readonly class AdminFundPerformanceService |
| 12 | { |
| 13 | public function __construct( |
| 14 | private PDO $pdo, |
| 15 | private FundsRepository $fundsRepository, |
| 16 | ) { |
| 17 | } |
| 18 | |
| 19 | /** |
| 20 | * Record monthly fund performance |
| 21 | */ |
| 22 | public function recordMonthlyPerformance( |
| 23 | string $fundId, |
| 24 | int $year, |
| 25 | int $month, |
| 26 | mixed $returnPercentage, |
| 27 | mixed $benchmarkPercentage = null, |
| 28 | ?string $commentary = null, |
| 29 | ): array { |
| 30 | // Validate inputs |
| 31 | if ($month < 1 || $month > 12) { |
| 32 | throw new InvalidArgumentException('Invalid month: ' . $month); |
| 33 | } |
| 34 | |
| 35 | if ($year < 2000 || $year > 2099) { |
| 36 | throw new InvalidArgumentException('Invalid year: ' . $year); |
| 37 | } |
| 38 | |
| 39 | if ($fundId === '') { |
| 40 | throw new InvalidArgumentException('Fund ID is required'); |
| 41 | } |
| 42 | |
| 43 | if ($returnPercentage === null) { |
| 44 | throw new InvalidArgumentException('Return percentage is required'); |
| 45 | } |
| 46 | |
| 47 | if (!is_numeric($returnPercentage)) { |
| 48 | throw new InvalidArgumentException('Return percentage must be a valid number'); |
| 49 | } |
| 50 | |
| 51 | if ($benchmarkPercentage !== null && !is_numeric($benchmarkPercentage)) { |
| 52 | throw new InvalidArgumentException('Benchmark percentage must be a valid number'); |
| 53 | } |
| 54 | |
| 55 | $fund = $this->fundsRepository->findFundById($fundId); |
| 56 | if (!$fund) { |
| 57 | throw new InvalidArgumentException('Fund not found: ' . $fundId); |
| 58 | } |
| 59 | |
| 60 | $returnPercentage = (float)$returnPercentage; |
| 61 | $benchmarkPercentage = $benchmarkPercentage !== null ? (float)$benchmarkPercentage : null; |
| 62 | |
| 63 | $sql = ' |
| 64 | UPDATE fund_performance SET |
| 65 | return_pct = :return_pct, |
| 66 | benchmark_pct = :benchmark_pct, |
| 67 | commentary = :commentary, |
| 68 | updated_at = CURRENT_TIMESTAMP |
| 69 | WHERE fund_id = :fund_id |
| 70 | AND year = :year |
| 71 | AND month = :month |
| 72 | '; |
| 73 | |
| 74 | $stmt = $this->pdo->prepare($sql); |
| 75 | if ($stmt === false) { |
| 76 | throw new InvalidArgumentException('Failed to prepare update statement'); |
| 77 | } |
| 78 | |
| 79 | $stmt->execute([ |
| 80 | 'fund_id' => $fundId, |
| 81 | 'year' => $year, |
| 82 | 'month' => $month, |
| 83 | 'return_pct' => $returnPercentage, |
| 84 | 'benchmark_pct' => $benchmarkPercentage, |
| 85 | 'commentary' => $commentary, |
| 86 | ]); |
| 87 | |
| 88 | if ($stmt->rowCount() === 0) { |
| 89 | $sql = ' |
| 90 | INSERT INTO fund_performance ( |
| 91 | id, |
| 92 | fund_id, |
| 93 | year, |
| 94 | month, |
| 95 | return_pct, |
| 96 | benchmark_pct, |
| 97 | commentary, |
| 98 | created_at, |
| 99 | updated_at |
| 100 | ) VALUES ( |
| 101 | gen_random_uuid(), |
| 102 | :fund_id, |
| 103 | :year, |
| 104 | :month, |
| 105 | :return_pct, |
| 106 | :benchmark_pct, |
| 107 | :commentary, |
| 108 | CURRENT_TIMESTAMP, |
| 109 | CURRENT_TIMESTAMP |
| 110 | ) |
| 111 | RETURNING |
| 112 | id::TEXT as "id", |
| 113 | fund_id::TEXT as "fundId", |
| 114 | year, |
| 115 | month, |
| 116 | return_pct as "returnPct", |
| 117 | benchmark_pct as "benchmarkPct", |
| 118 | commentary, |
| 119 | created_at as "createdAt" |
| 120 | '; |
| 121 | |
| 122 | $stmt = $this->pdo->prepare($sql); |
| 123 | if ($stmt === false) { |
| 124 | throw new InvalidArgumentException('Failed to prepare insert statement'); |
| 125 | } |
| 126 | |
| 127 | $stmt->execute([ |
| 128 | 'fund_id' => $fundId, |
| 129 | 'year' => $year, |
| 130 | 'month' => $month, |
| 131 | 'return_pct' => $returnPercentage, |
| 132 | 'benchmark_pct' => $benchmarkPercentage, |
| 133 | 'commentary' => $commentary, |
| 134 | ]); |
| 135 | |
| 136 | $result = $stmt->fetch(PDO::FETCH_ASSOC); |
| 137 | return $result ?: []; |
| 138 | } |
| 139 | |
| 140 | $sql = ' |
| 141 | SELECT |
| 142 | id::TEXT as "id", |
| 143 | fund_id::TEXT as "fundId", |
| 144 | year, |
| 145 | month, |
| 146 | return_pct as "returnPct", |
| 147 | benchmark_pct as "benchmarkPct", |
| 148 | commentary, |
| 149 | created_at as "createdAt" |
| 150 | FROM fund_performance |
| 151 | WHERE fund_id = :fund_id |
| 152 | AND year = :year |
| 153 | AND month = :month |
| 154 | '; |
| 155 | |
| 156 | $stmt = $this->pdo->prepare($sql); |
| 157 | if ($stmt === false) { |
| 158 | throw new InvalidArgumentException('Failed to prepare select statement'); |
| 159 | } |
| 160 | |
| 161 | $stmt->execute([ |
| 162 | 'fund_id' => $fundId, |
| 163 | 'year' => $year, |
| 164 | 'month' => $month, |
| 165 | ]); |
| 166 | |
| 167 | $result = $stmt->fetch(PDO::FETCH_ASSOC); |
| 168 | |
| 169 | return $result ?: []; |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * Get 12-month performance overview for a fund |
| 174 | */ |
| 175 | public function get12MonthPerformance(string $fundId): array |
| 176 | { |
| 177 | $sql = ' |
| 178 | SELECT |
| 179 | id::TEXT as "id", |
| 180 | year, |
| 181 | month, |
| 182 | return_pct as "returnPct", |
| 183 | benchmark_pct as "benchmarkPct", |
| 184 | commentary |
| 185 | FROM fund_performance |
| 186 | WHERE fund_id = :fund_id |
| 187 | ORDER BY year DESC, month DESC |
| 188 | LIMIT 12 |
| 189 | '; |
| 190 | |
| 191 | $stmt = $this->pdo->prepare($sql); |
| 192 | if ($stmt === false) { |
| 193 | throw new InvalidArgumentException('Failed to prepare statement'); |
| 194 | } |
| 195 | |
| 196 | $stmt->execute(['fund_id' => $fundId]); |
| 197 | $results = $stmt->fetchAll(PDO::FETCH_ASSOC); |
| 198 | |
| 199 | return array_reverse($results ?: []); |
| 200 | } |
| 201 | |
| 202 | /** |
| 203 | * Get all available funds for performance entry |
| 204 | */ |
| 205 | public function getAllFundsForPerformance(): array |
| 206 | { |
| 207 | return $this->fundsRepository->findAll(); |
| 208 | } |
| 209 | |
| 210 | /** |
| 211 | * Calculate YTD return for a fund |
| 212 | */ |
| 213 | public function calculateYtdReturn(string $fundId, ?int $year = null): ?string |
| 214 | { |
| 215 | if ($year === null) { |
| 216 | $year = (int)date('Y'); |
| 217 | } |
| 218 | |
| 219 | $sql = ' |
| 220 | SELECT |
| 221 | SUM(return_pct) as "ytdReturn" |
| 222 | FROM fund_performance |
| 223 | WHERE fund_id = :fund_id |
| 224 | AND year = :year |
| 225 | '; |
| 226 | |
| 227 | $stmt = $this->pdo->prepare($sql); |
| 228 | if ($stmt === false) { |
| 229 | throw new InvalidArgumentException('Failed to prepare statement'); |
| 230 | } |
| 231 | |
| 232 | $stmt->execute([ |
| 233 | 'fund_id' => $fundId, |
| 234 | 'year' => $year, |
| 235 | ]); |
| 236 | |
| 237 | $result = $stmt->fetch(PDO::FETCH_ASSOC); |
| 238 | |
| 239 | return $result['ytdReturn'] ?? null; |
| 240 | } |
| 241 | } |