Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 83
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
AdminBalanceUpdateService
0.00% covered (danger)
0.00%
0 / 83
0.00% covered (danger)
0.00%
0 / 6
342
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
 adjustBalance
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
12
 createTransaction
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 updateAllocationCurrentValue
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 getAdjustmentHistory
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 getInvestorAllocations
0.00% covered (danger)
0.00%
0 / 8
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 App\Domain\Transaction\Repository\TransactionRepository;
9use InvalidArgumentException;
10use PDO;
11
12final readonly class AdminBalanceUpdateService
13{
14    public function __construct(
15        private PDO $pdo,
16        private TransactionRepository $transactionRepository,
17        private FundsRepository $fundsRepository,
18    ) {
19    }
20
21    /**
22     * Manually adjust investor balance in a fund
23     */
24    public function adjustBalance(
25        int $userId,
26        string $fundId,
27        string $transactionType,
28        string $amount,
29        string $effectiveDate,
30        string $internalNote,
31        int $adminUserId,
32        bool $notifyInvestor = false,
33    ): array {
34        // Validate transaction type
35        $validTypes = ['deposit', 'withdrawal', 'gain', 'loss', 'fee', 'adjustment'];
36        if (!in_array($transactionType, $validTypes)) {
37            throw new InvalidArgumentException('Invalid transaction type: ' . $transactionType);
38        }
39
40        // Verify fund exists
41        $fund = $this->fundsRepository->findFundById($fundId);
42        if (!$fund) {
43            throw new InvalidArgumentException('Fund not found: ' . $fundId);
44        }
45
46        // Create transaction directly
47        $transactionId = $this->createTransaction(
48            userId: $userId,
49            fundId: $fundId,
50            type: ucfirst($transactionType),
51            amount: $amount,
52            note: $internalNote,
53            effectiveDate: $effectiveDate,
54            adminUserId: $adminUserId,
55        );
56
57        // Update allocation current_value
58        $this->updateAllocationCurrentValue($userId, $fundId, (float)$amount, $transactionType);
59
60        return [
61            'id' => $transactionId,
62            'userId' => $userId,
63            'fundId' => $fundId,
64            'type' => $transactionType,
65            'amount' => $amount,
66            'status' => 'completed',
67            'effectiveDate' => $effectiveDate,
68            'notifyInvestor' => $notifyInvestor,
69            'createdAt' => date('c'),
70        ];
71    }
72
73    /**
74     * Create a transaction directly in the database
75     */
76    private function createTransaction(
77        int $userId,
78        string $fundId,
79        string $type,
80        string $amount,
81        string $note,
82        string $effectiveDate,
83        int $adminUserId,
84    ): string {
85        $sql = '
86            INSERT INTO fund_transactions (
87                user_id, fund_id, type, amount, status, note, effective_date, created_by
88            ) VALUES (
89                :user_id, :fund_id, :type, :amount, :status, :note, :effective_date, :created_by
90            )
91            RETURNING id
92        ';
93
94        $stmt = $this->pdo->prepare($sql);
95        if ($stmt === false) {
96            throw new InvalidArgumentException('Failed to prepare transaction insert statement');
97        }
98
99        $stmt->bindValue(':user_id', $userId);
100        $stmt->bindValue(':fund_id', $fundId);
101        $stmt->bindValue(':type', $type);
102        $stmt->bindValue(':amount', $amount);
103        $stmt->bindValue(':status', 'Completed');
104        $stmt->bindValue(':note', $note);
105        $stmt->bindValue(':effective_date', $effectiveDate);
106        $stmt->bindValue(':created_by', $adminUserId);
107
108        $stmt->execute();
109        $result = $stmt->fetch(PDO::FETCH_ASSOC);
110
111        if (!$result) {
112            throw new InvalidArgumentException('Failed to create transaction');
113        }
114
115        return (string) $result['id'];
116    }
117
118    /**
119     * Update allocation current value
120     */
121    private function updateAllocationCurrentValue(int $userId, string $fundId, float $amount, string $transactionType): void
122    {
123        // Determine if this is an addition or subtraction to current_value
124        $adjustment = $amount;
125        if (in_array($transactionType, ['withdrawal', 'loss', 'fee'])) {
126            $adjustment = -$amount;
127        }
128
129        $sql = '
130            UPDATE allocations
131            SET current_value = current_value + :adjustment
132            WHERE user_id = :userId
133              AND fund_id = :fundId
134        ';
135
136        $stmt = $this->pdo->prepare($sql);
137        if ($stmt === false) {
138            throw new InvalidArgumentException('Failed to prepare allocation update statement');
139        }
140
141        $stmt->execute([
142            'adjustment' => $adjustment,
143            'userId' => $userId,
144            'fundId' => $fundId,
145        ]);
146    }
147
148    /**
149     * Get adjustment history for auditing
150     */
151    public function getAdjustmentHistory(int $userId, ?string $fundId = null, int $limit = 50): array
152    {
153        $sql = '
154            SELECT
155                t.id::TEXT as "id",
156                t.user_id::TEXT as "userId",
157                t.fund_id::TEXT as "fundId",
158                t.type as "type",
159                t.amount::TEXT as "amount",
160                t.status,
161                t.note as "note",
162                t.effective_date as "effectiveDate",
163                t.created_at as "createdAt",
164                f.name as "fundName"
165            FROM fund_transactions t
166            JOIN funds f ON t.fund_id = f.id
167            WHERE t.user_id = :user_id
168        ';
169
170        $params = ['user_id' => $userId];
171
172        if ($fundId) {
173            $sql .= ' AND t.fund_id = :fund_id';
174            $params['fund_id'] = $fundId;
175        }
176
177        $sql .= ' ORDER BY t.created_at DESC LIMIT :limit';
178
179        $stmt = $this->pdo->prepare($sql);
180        if ($stmt === false) {
181            throw new InvalidArgumentException('Failed to prepare query');
182        }
183
184        foreach ($params as $key => $value) {
185            $stmt->bindValue($key, $value);
186        }
187        $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
188
189        $stmt->execute();
190        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
191
192        return $rows ?: [];
193    }
194
195    /**
196     * Get investor's current allocations
197     */
198    public function getInvestorAllocations(int $userId): array
199    {
200        $sql = '
201            SELECT
202                a.id::TEXT as "id",
203                a.fund_id::TEXT as "fundId",
204                f.name as "fundName",
205                a.invested_amount::TEXT as "investedAmount",
206                a.current_value::TEXT as "currentValue",
207                a.units::TEXT as "units",
208                a.created_at as "createdAt"
209            FROM allocations a
210            JOIN funds f ON a.fund_id = f.id
211            WHERE a.user_id = :user_id
212            ORDER BY a.created_at DESC
213        ';
214
215        $stmt = $this->pdo->prepare($sql);
216        if ($stmt === false) {
217            throw new InvalidArgumentException('Failed to prepare query');
218        }
219
220        $stmt->execute(['user_id' => $userId]);
221        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
222
223        return $rows ?: [];
224    }
225}