Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.35% covered (warning)
82.35%
70 / 85
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
TransactionRepository
82.35% covered (warning)
82.35%
70 / 85
14.29% covered (danger)
14.29%
1 / 7
25.91
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
3.02
 findByAccountId
94.44% covered (success)
94.44%
34 / 36
0.00% covered (danger)
0.00%
0 / 1
8.01
 findById
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 accountExists
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 getAccountStatus
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 getAccountAvailableBalance
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\Transaction\Repository;
6
7use App\Domain\Transaction\Data\TransactionData;
8use App\Support\Row;
9use PDO;
10use RuntimeException;
11
12/**
13 * Repository class for managing transactions.
14 */
15class TransactionRepository
16{
17    public function __construct(
18        private readonly PDO $pdo,
19    ) {}
20
21    public function create(
22        int $accountId,
23        string $transactionType,
24        string $amount,
25        ?string $description = null,
26    ): TransactionData {
27        $sql = 'INSERT INTO transactions (account_id, transaction_type, amount, description, status)
28                VALUES (:account_id, :transaction_type, :amount, :description, :status)
29                RETURNING
30                    transaction_id AS "transactionId",
31                    account_id AS "accountId",
32                    transaction_type AS "transactionType",
33                    amount,
34                    balance_after AS "balanceAfter",
35                    description,
36                    reference_number AS "referenceNumber",
37                    created_at AS "createdAt",
38                    status';
39
40        $stmt = $this->pdo->prepare($sql);
41        if ($stmt === false) {
42            throw new RuntimeException('Failed to prepare statement');
43        }
44        $stmt->execute([
45            'account_id' => $accountId,
46            'transaction_type' => $transactionType,
47            'amount' => $amount,
48            'description' => $description,
49            'status' => 'completed',
50        ]);
51
52        $row = $stmt->fetch(PDO::FETCH_ASSOC);
53
54        if (!is_array($row)) {
55            throw new RuntimeException('Failed to retrieve created transaction');
56        }
57
58        return TransactionData::fromRow($row);
59    }
60
61    /**
62     * @param int $accountId
63     * @param int $page
64     * @param int $limit
65     * @param ?string $type
66     * @param ?string $startDate
67     * @param ?string $endDate
68     * @return array{data: list<TransactionData>, total: int}
69     */
70    public function findByAccountId(
71        int $accountId,
72        int $page = 1,
73        int $limit = 10,
74        ?string $type = null,
75        ?string $startDate = null,
76        ?string $endDate = null,
77    ): array {
78        $offset = ($page - 1) * $limit;
79
80        $conditions = ['account_id = :account_id'];
81        $params = ['account_id' => $accountId];
82
83        if ($type !== null) {
84            $conditions[] = 'transaction_type = :type';
85            $params['type'] = $type;
86        }
87
88        if ($startDate !== null) {
89            $conditions[] = 'created_at >= :start_date';
90            $params['start_date'] = $startDate . ' 00:00:00';
91        }
92
93        if ($endDate !== null) {
94            $conditions[] = 'created_at <= :end_date';
95            $params['end_date'] = $endDate . ' 23:59:59';
96        }
97
98        $whereClause = implode(' AND ', $conditions);
99
100        $countStmt = $this->pdo->prepare("SELECT COUNT(*) FROM transactions WHERE {$whereClause}");
101
102        if ($countStmt === false) {
103            throw new RuntimeException('Failed to prepare count statement');
104        }
105        $countStmt->execute($params);
106        $total = (int)$countStmt->fetchColumn();
107
108        $sql = "SELECT
109                    transaction_id AS \"transactionId\",
110                    account_id AS \"accountId\",
111                    transaction_type AS \"transactionType\",
112                    amount,
113                    balance_after AS \"balanceAfter\",
114                    description,
115                    reference_number AS \"referenceNumber\",
116                    created_at AS \"createdAt\",
117                    status
118                FROM transactions
119                WHERE {$whereClause}
120                ORDER BY created_at DESC
121                LIMIT :limit OFFSET :offset";
122
123        $stmt = $this->pdo->prepare($sql);
124
125        if ($stmt === false) {
126            throw new RuntimeException('Failed to prepare statement');
127        }
128        foreach ($params as $key => $value) {
129            $stmt->bindValue($key, $value);
130        }
131        $stmt->bindValue('limit', $limit, PDO::PARAM_INT);
132        $stmt->bindValue('offset', $offset, PDO::PARAM_INT);
133        $stmt->execute();
134
135        $data = [];
136        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
137            $data[] = TransactionData::fromRow(Row::from($row));
138        }
139
140        return [
141            'data' => $data,
142            'total' => $total,
143        ];
144    }
145
146    public function findById(int $transactionId): ?TransactionData
147    {
148        $stmt = $this->pdo->prepare(
149            'SELECT
150                    transaction_id AS "transactionId",
151                    account_id AS "accountId",
152                    transaction_type AS "transactionType",
153                    amount,
154                    balance_after AS "balanceAfter",
155                    description,
156                    reference_number AS "referenceNumber",
157                    created_at AS "createdAt",
158                    status
159                FROM transactions
160            WHERE transaction_id = :transaction_id',
161        );
162
163        if ($stmt === false) {
164            throw new RuntimeException('Failed to prepare statement');
165        }
166        $stmt->execute(['transaction_id' => $transactionId]);
167
168        $row = $stmt->fetch(PDO::FETCH_ASSOC);
169
170        return is_array($row) ? TransactionData::fromRow($row) : null;
171    }
172
173    public function accountExists(int $accountId): bool
174    {
175        $stmt = $this->pdo->prepare(
176            'SELECT EXISTS(SELECT 1 FROM accounts WHERE account_id = :account_id)',
177        );
178
179        if ($stmt === false) {
180            throw new RuntimeException('Failed to prepare statement');
181        }
182        $stmt->execute(['account_id' => $accountId]);
183
184        return (bool)$stmt->fetchColumn();
185    }
186
187    public function getAccountStatus(int $accountId): ?string
188    {
189        $stmt = $this->pdo->prepare(
190            'SELECT status FROM accounts WHERE account_id = :account_id',
191        );
192
193        if ($stmt === false) {
194            throw new RuntimeException('Failed to prepare statement');
195        }
196
197        $stmt->execute(['account_id' => $accountId]);
198
199        $result = $stmt->fetchColumn();
200
201        return $result !== false ? (string)$result : null;
202    }
203
204    public function getAccountAvailableBalance(int $accountId): string
205    {
206        $stmt = $this->pdo->prepare(
207            'SELECT available_balance FROM accounts WHERE account_id = :account_id',
208        );
209
210        if ($stmt === false) {
211            throw new RuntimeException('Failed to prepare statement');
212        }
213        $stmt->execute(['account_id' => $accountId]);
214
215        $result = $stmt->fetchColumn();
216
217        return $result !== false ? (string)$result : '0.00';
218    }
219}