Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
FundData
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 4
210
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
 fromRow
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
42
 toArray
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
2
 decodeInvestmentHighlights
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Funds\Data;
6
7/**
8 * Immutable DTO representing a single record from the `funds` table.
9 */
10final readonly class FundData
11{
12    public function __construct(
13        public string $id,
14        public string $name,
15        public string $type,
16        public string $status,
17        public ?string $descriptionShort = null,
18        public ?string $descriptionFull = null,
19        public ?string $heroImageUrl = null,
20        public ?string $aum = null,
21        public ?string $targetReturn = null,
22        public ?string $minInvestment = null,
23        public ?string $managerName = null,
24        public ?string $irEmail = null,
25        public bool $published = false,
26        public ?int $vintageYear = null,
27        public ?string $fundLife = null,
28        public ?string $geographicFocus = null,
29        public ?string $targetIrr = null,
30        public ?string $managementFeePct = null,
31        public ?string $carriedInterestPct = null,
32        public ?array $investmentHighlights = null,
33        public ?string $riskFactors = null,
34        public ?string $managerBio = null,
35        public ?string $managerPhotoUrl = null,
36        public ?string $irContactName = null,
37        public ?string $irContactPhone = null,
38        public ?string $nextDistributionDate = null,
39        public ?string $liquidity = null,
40        public ?string $heroImageAlt = null,
41        public int $investorCount = 0,
42        public ?string $lastUpdated = null,
43        public ?array $performance = null,
44    ) {}
45
46    /**
47     * @param array<string, mixed> $row
48     */
49    public static function fromRow(array $row): self
50    {
51        return new self(
52            id: (string)($row['id'] ?? ''),
53            name: (string)($row['name'] ?? ''),
54            type: (string)($row['type'] ?? ''),
55            status: (string)($row['status'] ?? ''),
56            descriptionShort: $row['descriptionShort'] ?? null,
57            descriptionFull: $row['descriptionFull'] ?? null,
58            heroImageUrl: $row['heroImageUrl'] ?? null,
59            aum: $row['aum'] ?? null,
60            targetReturn: $row['targetReturn'] ?? null,
61            minInvestment: $row['minInvestment'] ?? null,
62            managerName: $row['managerName'] ?? null,
63            irEmail: $row['irEmail'] ?? null,
64            published: isset($row['published']) ? (bool)$row['published'] : false,
65            vintageYear: isset($row['vintageYear']) ? (int)$row['vintageYear'] : null,
66            fundLife: $row['fundLife'] ?? null,
67            geographicFocus: $row['geographicFocus'] ?? null,
68            targetIrr: $row['targetIrr'] ?? null,
69            managementFeePct: $row['managementFeePct'] ?? null,
70            carriedInterestPct: $row['carriedInterestPct'] ?? null,
71            investmentHighlights: self::decodeInvestmentHighlights($row['investmentHighlights'] ?? null),
72            riskFactors: $row['riskFactors'] ?? null,
73            managerBio: $row['managerBio'] ?? null,
74            managerPhotoUrl: $row['managerPhotoUrl'] ?? null,
75            irContactName: $row['irContactName'] ?? null,
76            irContactPhone: $row['irContactPhone'] ?? null,
77            nextDistributionDate: $row['nextDistributionDate'] ?? null,
78            liquidity: $row['liquidity'] ?? null,
79            heroImageAlt: $row['heroImageAlt'] ?? null,
80            investorCount: isset($row['investor_count']) ? (int)$row['investor_count'] : (isset($row['investorCount']) ? (int)$row['investorCount'] : 0),
81            lastUpdated: $row['last_updated'] ?? ($row['lastUpdated'] ?? null),
82            performance: isset($row['performance']) ? json_decode($row['performance'], true) : null,
83        );
84    }
85
86    /**
87     * @return array<string, mixed>
88     */
89    public function toArray(): array
90    {
91        $data = [
92            'id' => $this->id,
93            'name' => $this->name,
94            'type' => $this->type,
95            'status' => $this->status,
96            'descriptionShort' => $this->descriptionShort,
97            'descriptionFull' => $this->descriptionFull,
98            'heroImageUrl' => $this->heroImageUrl,
99            'aum' => $this->aum,
100            'targetReturn' => $this->targetReturn,
101            'minInvestment' => $this->minInvestment,
102            'managerName' => $this->managerName,
103            'irEmail' => $this->irEmail,
104            'published' => $this->published,
105            'vintageYear' => $this->vintageYear,
106            'fundLife' => $this->fundLife,
107            'geographicFocus' => $this->geographicFocus,
108            'targetIrr' => $this->targetIrr,
109            'managementFeePct' => $this->managementFeePct,
110            'carriedInterestPct' => $this->carriedInterestPct,
111            'investmentHighlights' => $this->investmentHighlights,
112            'riskFactors' => $this->riskFactors,
113            'managerBio' => $this->managerBio,
114            'managerPhotoUrl' => $this->managerPhotoUrl,
115            'irContactName' => $this->irContactName,
116            'irContactPhone' => $this->irContactPhone,
117            'nextDistributionDate' => $this->nextDistributionDate,
118            'liquidity' => $this->liquidity,
119            'heroImageAlt' => $this->heroImageAlt,
120            'investorCount' => $this->investorCount,
121            'lastUpdated' => $this->lastUpdated,
122            'performance' => $this->performance,
123        ];
124
125        // Return all keys (including nulls) so API consumers receive a complete shape
126        // and can distinguish between missing and explicitly empty fields.
127        return $data;
128    }
129
130    /**
131     * @param mixed $value
132     * @return array<string>|null
133     */
134    private static function decodeInvestmentHighlights(mixed $value): ?array
135    {
136        if ($value === null || $value === '') {
137            return null;
138        }
139
140        if (is_array($value)) {
141            return $value;
142        }
143
144        try {
145            $decoded = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
146        } catch (\JsonException) {
147            return null;
148        }
149
150        return is_array($decoded) ? $decoded : null;
151    }
152}
153