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