Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.69% covered (success)
97.69%
127 / 130
85.71% covered (warning)
85.71%
12 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
SqlCurrencyRepository
97.67% covered (success)
97.67%
126 / 129
85.71% covered (warning)
85.71%
12 / 14
30
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
 getTableName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findAll
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 findActive
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
3.10
 findByCode
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 findById
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 save
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
6
 updateRate
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 deleteByCode
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 recordRateHistory
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 getRateHistory
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getAllRateHistory
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 mapRowToEntity
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Modules\Currencies\Infrastructure\Repository;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Instance\Application\Service\InstanceContextManagerInterface;
12use App\Core\Database\Repository\TenantAwareRepositoryTrait;
13use App\Modules\Currencies\Domain\Model\Currency;
14use App\Modules\Currencies\Domain\Repository\CurrencyRepositoryInterface;
15use PDO;
16
17/**
18 * SQL implementation of CurrencyRepositoryInterface.
19 */
20final readonly class SqlCurrencyRepository implements CurrencyRepositoryInterface
21{
22    use TenantAwareRepositoryTrait;
23
24    private const string COLUMNS = '`id`, `code`, `name`, `symbol`, `exchange_rate`, '
25        . '`nbp_table_no`, `effective_date`, `is_base`, `is_active`, `sort_order`';
26
27    public function __construct(
28        protected PDO $pdo,
29        private string $tablePrefix = 'a_',
30        protected ?InstanceContextManagerInterface $instanceManager = null,
31        protected ?PDO $clientPdo = null
32    ) {
33    }
34
35    private function getTableName(): string
36    {
37        return $this->tablePrefix . 'mod_currencies_records';
38    }
39
40    public function findAll(): array
41    {
42        $table = $this->getTableName();
43        $cols = self::COLUMNS;
44        $sql = "SELECT {$cols} FROM `{$table}` ORDER BY `sort_order` ASC, `code` ASC";
45        $stmt = $this->getPdo()->query($sql);
46        if ($stmt === false) {
47            return [];
48        }
49
50        $currencies = [];
51        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
52            $currencies[] = $this->mapRowToEntity($row);
53        }
54
55        return $currencies;
56    }
57
58    public function findActive(): array
59    {
60        $table = $this->getTableName();
61        $cols = self::COLUMNS;
62        $sql = "SELECT {$cols} FROM `{$table}` WHERE `is_active` = 1 ORDER BY `sort_order` ASC";
63        $stmt = $this->getPdo()->query($sql);
64        if ($stmt === false) {
65            return [];
66        }
67
68        $currencies = [];
69        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
70            $currencies[] = $this->mapRowToEntity($row);
71        }
72
73        return $currencies;
74    }
75
76    public function findByCode(string $code): ?Currency
77    {
78        $table = $this->getTableName();
79        $cols = self::COLUMNS;
80        $sql = "SELECT {$cols} FROM `{$table}` WHERE `code` = :code LIMIT 1";
81        $stmt = $this->getPdo()->prepare($sql);
82        $stmt->execute(['code' => strtoupper($code)]);
83        $row = $stmt->fetch(PDO::FETCH_ASSOC);
84
85        return is_array($row) ? $this->mapRowToEntity($row) : null;
86    }
87
88    public function findById(int $id): ?Currency
89    {
90        $table = $this->getTableName();
91        $cols = self::COLUMNS;
92        $sql = "SELECT {$cols} FROM `{$table}` WHERE `id` = :id LIMIT 1";
93        $stmt = $this->getPdo()->prepare($sql);
94        $stmt->execute(['id' => $id]);
95        $row = $stmt->fetch(PDO::FETCH_ASSOC);
96
97        return is_array($row) ? $this->mapRowToEntity($row) : null;
98    }
99
100    public function save(Currency $currency): void
101    {
102        $table = $this->getTableName();
103        if ($currency->getId() === null) {
104            $sql = "INSERT INTO `{$table}` (`code`, `name`, `symbol`, `exchange_rate`, "
105                . "`nbp_table_no`, `effective_date`, `is_base`, `is_active`, `sort_order`) "
106                . "VALUES (:code, :name, :symbol, :rate, :tbl, :dt, :base, :act, :sort)";
107            $stmt = $this->getPdo()->prepare($sql);
108            $stmt->execute([
109                'code' => $currency->getCode(),
110                'name' => $currency->getName(),
111                'symbol' => $currency->getSymbol(),
112                'rate' => $currency->getExchangeRate(),
113                'tbl' => $currency->getNbpTableNo(),
114                'dt' => $currency->getEffectiveDate(),
115                'base' => $currency->isBase() ? 1 : 0,
116                'act' => $currency->isActive() ? 1 : 0,
117                'sort' => $currency->getSortOrder(),
118            ]);
119            $currency->setId((int) $this->getPdo()->lastInsertId());
120            return;
121        }
122
123        $sql = "UPDATE `{$table}` SET `code` = :code, `name` = :name, `symbol` = :symbol, "
124            . "`exchange_rate` = :rate, `nbp_table_no` = :tbl, `effective_date` = :dt, "
125            . "`is_base` = :base, `is_active` = :act, `sort_order` = :sort WHERE `id` = :id";
126        $stmt = $this->getPdo()->prepare($sql);
127        $stmt->execute([
128            'id' => $currency->getId(),
129            'code' => $currency->getCode(),
130            'name' => $currency->getName(),
131            'symbol' => $currency->getSymbol(),
132            'rate' => $currency->getExchangeRate(),
133            'tbl' => $currency->getNbpTableNo(),
134            'dt' => $currency->getEffectiveDate(),
135            'base' => $currency->isBase() ? 1 : 0,
136            'act' => $currency->isActive() ? 1 : 0,
137            'sort' => $currency->getSortOrder(),
138        ]);
139    }
140
141    public function updateRate(string $code, float $exchangeRate, ?string $tableNo, ?string $effectiveDate): void
142    {
143        $table = $this->getTableName();
144        $sql = "UPDATE `{$table}` SET `exchange_rate` = :rate, `nbp_table_no` = :tbl, `effective_date` = :dt "
145            . "WHERE `code` = :code AND `is_base` = 0";
146        $stmt = $this->getPdo()->prepare($sql);
147        $stmt->execute([
148            'code' => strtoupper($code),
149            'rate' => $exchangeRate,
150            'tbl' => $tableNo,
151            'dt' => $effectiveDate,
152        ]);
153    }
154
155    public function delete(int $id): bool
156    {
157        $table = $this->getTableName();
158        $sql = "DELETE FROM `{$table}` WHERE `id` = :id AND `is_base` = 0";
159        $stmt = $this->getPdo()->prepare($sql);
160        $stmt->execute(['id' => $id]);
161
162        return $stmt->rowCount() > 0;
163    }
164
165    public function deleteByCode(string $code): bool
166    {
167        $table = $this->getTableName();
168        $sql = "DELETE FROM `{$table}` WHERE `code` = :code AND `is_base` = 0";
169        $stmt = $this->getPdo()->prepare($sql);
170        $stmt->execute(['code' => strtoupper($code)]);
171
172        return $stmt->rowCount() > 0;
173    }
174
175    public function recordRateHistory(
176        string $code,
177        float $exchangeRate,
178        string $tableNo,
179        string $effectiveDate,
180        string $source = 'NBP'
181    ): void {
182        $table = $this->tablePrefix . 'mod_currency_rates_history';
183        $sql = "INSERT INTO `{$table}"
184            . "(`currency_code`, `exchange_rate`, `nbp_table_no`, `effective_date`, `source`, `created_at`) "
185            . "VALUES (:code, :rate, :tbl, :dt, :src, NOW()) "
186            . "ON DUPLICATE KEY UPDATE `exchange_rate` = VALUES(`exchange_rate`), `created_at` = NOW()";
187        $stmt = $this->getPdo()->prepare($sql);
188        $stmt->execute([
189            'code' => strtoupper($code),
190            'rate' => $exchangeRate,
191            'tbl'  => $tableNo,
192            'dt'   => $effectiveDate,
193            'src'  => $source,
194        ]);
195    }
196
197    /**
198     * @return list<array<string, mixed>>
199     */
200    public function getRateHistory(string $code, int $limit = 30): array
201    {
202        $table = $this->tablePrefix . 'mod_currency_rates_history';
203        $limit = max(1, min(200, $limit));
204        $sql = "SELECT `id`, `currency_code`, `exchange_rate`, `nbp_table_no`, "
205            . "`effective_date`, `source`, `created_at` "
206            . "FROM `{$table}` WHERE `currency_code` = :code ORDER BY `effective_date` DESC, `id` DESC LIMIT {$limit}";
207        $stmt = $this->getPdo()->prepare($sql);
208        $stmt->execute(['code' => strtoupper($code)]);
209
210        /** @var list<array<string, mixed>> */
211        return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
212    }
213
214    /**
215     * @return list<array<string, mixed>>
216     */
217    public function getAllRateHistory(int $limit = 50): array
218    {
219        $table = $this->tablePrefix . 'mod_currency_rates_history';
220        $limit = max(1, min(200, $limit));
221        $sql = "SELECT `id`, `currency_code`, `exchange_rate`, `nbp_table_no`, "
222            . "`effective_date`, `source`, `created_at` "
223            . "FROM `{$table}` ORDER BY `effective_date` DESC, `id` DESC LIMIT {$limit}";
224        $stmt = $this->getPdo()->query($sql);
225        if ($stmt === false) {
226            return [];
227        }
228
229        /** @var list<array<string, mixed>> */
230        return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
231    }
232
233    /**
234     * @param array<string, mixed> $row
235     */
236    private function mapRowToEntity(array $row): Currency
237    {
238        return new Currency(
239            id: (int) $row['id'],
240            code: (string) $row['code'],
241            name: (string) $row['name'],
242            symbol: (string) $row['symbol'],
243            exchangeRate: (float) $row['exchange_rate'],
244            nbpTableNo: isset($row['nbp_table_no']) ? (string) $row['nbp_table_no'] : null,
245            effectiveDate: isset($row['effective_date']) ? (string) $row['effective_date'] : null,
246            isBase: (bool) $row['is_base'],
247            isActive: (bool) $row['is_active'],
248            sortOrder: (int) $row['sort_order']
249        );
250    }
251}