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 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
InvestorFundsService
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 4
306
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
 getPublishedFunds
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
72
 getPublishedFundDetail
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
56
 getUserAllocations
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Funds\Service;
6
7use App\Domain\Exception\NotFoundException;
8use App\Domain\Funds\Repository\FundsRepository;
9use InvalidArgumentException;
10
11final readonly class InvestorFundsService
12{
13    private const VALID_TYPES = [
14        'Private Equity',
15        'Credit',
16        'Real Assets',
17        'Real Estate',
18        'Hedge',
19        'Other',
20    ];
21
22    public function __construct(
23        private FundsRepository $repository,
24    ) {}
25
26    /**
27     * @return array{
28     *   funds: list<array<string, mixed>>,
29     *   pagination: array{total: int, page: int, limit: int, totalPages: int}
30     * }
31     */
32    public function getPublishedFunds(
33        int $page,
34        int $limit,
35        ?string $search = null,
36        ?string $type = null,
37        ?int $userId = null,
38    ): array {
39        $page = max(1, $page);
40        $limit = min(100, max(1, $limit));
41
42        if ($type !== null && $type !== '' && !in_array($type, self::VALID_TYPES, true)) {
43            throw new InvalidArgumentException('Invalid fund type');
44        }
45
46        $result = $this->repository->findPublishedFunds(
47            page: $page,
48            limit: $limit,
49            search: $search,
50            type: $type,
51        );
52
53        $funds = array_map(
54            static fn($fund): array => $fund->toArray(),
55            $result['data'],
56        );
57
58        $fundIds = array_map(static fn(array $fund): string => (string)$fund['id'], $funds);
59        $allocations = $userId !== null
60            ? $this->repository->findUserFundAllocations($fundIds, (string)$userId)
61            : [];
62
63        $funds = array_map(
64            static function (array $fund) use ($allocations): array {
65                $allocation = $allocations[(string)$fund['id']] ?? null;
66                $invested = $allocation['investedAmount'] ?? null;
67                $current = $allocation['currentValue'] ?? null;
68
69                $returnPct = null;
70                if ($invested !== null && $current !== null) {
71                    $investedNum = (float)$invested;
72                    $currentNum = (float)$current;
73                    if ($investedNum > 0) {
74                        $returnPct = (($currentNum - $investedNum) / $investedNum) * 100;
75                    }
76                }
77
78                $fund['yourInvestedAmount'] = $invested;
79                $fund['yourCurrentValue'] = $current;
80                $fund['yourReturnPct'] = $returnPct;
81
82                return $fund;
83            },
84            $funds,
85        );
86
87        $totalPages = (int)ceil($result['total'] / $limit);
88
89        return [
90            'funds' => $funds,
91            'pagination' => [
92                'total' => $result['total'],
93                'page' => $page,
94                'limit' => $limit,
95                'totalPages' => $totalPages,
96            ],
97        ];
98    }
99
100    /**
101     * @return array{fund: array<string, mixed>, performance: list<array{date: string, returnPct: float|null, benchmarkPct: float|null, commentary: ?string}>, yourPosition: array{investedAmount: string|null, currentValue: string|null, gainLoss: string|null, returnPct: float|null}, distributions: list<array{paymentDate: string, amount: string, type: string, description: ?string}>}
102     */
103    public function getPublishedFundDetail(string $fundId, ?int $userId = null): array
104    {
105        $fund = $this->repository->findPublishedFundById($fundId);
106        if ($fund === null) {
107            throw new NotFoundException('Fund not found');
108        }
109
110        $performance = $this->repository->findFundPerformance($fundId);
111        $distributions = $userId !== null
112            ? $this->repository->findUserDistributions($fundId, (string)$userId)
113            : [];
114
115        $fundArray = $fund->toArray();
116        $allocationMap = $userId !== null
117            ? $this->repository->findUserFundAllocations([$fundId], (string)$userId)
118            : [];
119        $allocation = $allocationMap[$fundId] ?? ['investedAmount' => null, 'currentValue' => null];
120
121        $invested = $allocation['investedAmount'];
122        $current = $allocation['currentValue'];
123        $gainLoss = null;
124        $returnPct = null;
125        if ($invested !== null && $current !== null) {
126            $investedNum = (float)$invested;
127            $currentNum = (float)$current;
128            $gainLoss = (string)($currentNum - $investedNum);
129            if ($investedNum > 0) {
130                $returnPct = (($currentNum - $investedNum) / $investedNum) * 100;
131            }
132        }
133
134        return [
135            'fund' => $fundArray,
136            'performance' => $performance,
137            'yourPosition' => [
138                'investedAmount' => $invested,
139                'currentValue' => $current,
140                'gainLoss' => $gainLoss,
141                'returnPct' => $returnPct,
142            ],
143            'distributions' => $distributions,
144        ];
145    }
146
147    /**
148     * @return list<array{fundId: string, investedAmount: string, currentValue: string, units: string|null, createdAt: string}>
149     */
150    public function getUserAllocations(int $userId): array
151    {
152        return $this->repository->findAllUserAllocations((int)$userId);
153    }
154}
155