Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
NbpExchangeRatesClient
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
3 / 3
11
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDriver
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fetchExchangeRates
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
9
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\Integrations\Infrastructure\Currency;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Integrations\Domain\Contract\ExchangeRatesProviderInterface;
12use App\Modules\Integrations\Domain\Exception\ExchangeRatesException;
13use App\Modules\Integrations\Domain\Exception\IntegrationNetworkException;
14use App\Modules\Integrations\Infrastructure\Security\SsrfProtectionClient;
15use App\Modules\Integrations\Infrastructure\Security\SsrfProtectionClientInterface;
16
17/**
18 * NBP (Narodowy Bank Polski) Exchange Rates API client (Table A - mid rates).
19 *
20 * Documentation: https://api.nbp.pl/
21 */
22final readonly class NbpExchangeRatesClient implements ExchangeRatesProviderInterface
23{
24    private const string DEFAULT_API_URL = 'https://api.nbp.pl/api/exchangerates/tables/a/?format=json';
25
26    public function __construct(
27        private SsrfProtectionClientInterface $httpClient = new SsrfProtectionClient(),
28        private string $apiUrl = self::DEFAULT_API_URL
29    ) {
30    }
31
32    public function getDriver(): string
33    {
34        return 'nbp_rates_a';
35    }
36
37    /**
38     * Fetches Table A mid exchange rates from NBP REST API.
39     *
40     * @return array{table: string, effective_date: string, rates: array<string, float>}
41     * @throws ExchangeRatesException If JSON payload is invalid or empty.
42     */
43    public function fetchExchangeRates(): array
44    {
45        try {
46            $response = $this->httpClient->get(
47                $this->apiUrl,
48                ['Accept' => 'application/json', 'User-Agent' => 'Ammonly-ERP/1.0'],
49                5
50            );
51        } catch (IntegrationNetworkException $e) {
52            throw new ExchangeRatesException('Failed to connect to NBP Exchange Rates API: ' . $e->getMessage(), 0, $e);
53        }
54
55        if ($response['status'] !== 200) {
56            throw new ExchangeRatesException(sprintf('NBP API returned HTTP status %d.', $response['status']));
57        }
58
59        /** @var array<int, array<string, mixed>>|null $data */
60        $data = json_decode($response['body'], true);
61        if (!is_array($data) || empty($data[0])) {
62            throw new ExchangeRatesException('Invalid or empty response format from NBP API.');
63        }
64
65        $tableData = $data[0];
66        $tableNo = (string) ($tableData['no'] ?? '');
67        $effectiveDate = (string) ($tableData['effectiveDate'] ?? date('Y-m-d'));
68        /** @var list<array{currency: string, code: string, mid: float}> $rawRates */
69        $rawRates = is_array($tableData['rates'] ?? null) ? $tableData['rates'] : [];
70
71        $rates = ['PLN' => 1.0];
72        foreach ($rawRates as $rateRow) {
73            $code = strtoupper(trim((string) ($rateRow['code'] ?? '')));
74            $mid = (float) ($rateRow['mid'] ?? 0.0);
75            if ($code !== '' && $mid > 0.0) {
76                $rates[$code] = $mid;
77            }
78        }
79
80        return [
81            'table' => $tableNo,
82            'effective_date' => $effectiveDate,
83            'rates' => $rates,
84        ];
85    }
86}