Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
AdminFundPerformanceService
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 5
702
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 recordMonthlyPerformance
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 1
342
 get12MonthPerformance
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getAllFundsForPerformance
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 calculateYtdReturn
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Admin\Service;
6
7use App\Domain\Funds\Repository\FundsRepository;
8use InvalidArgumentException;
9use PDO;
10
11final 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}