547 lines
22 KiB
PHP
547 lines
22 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Modules\FxRates\Infrastructure;
|
|
|
|
use PDO;
|
|
|
|
final class FxRatesRepository
|
|
{
|
|
private string $driver;
|
|
|
|
public function __construct(
|
|
private PDO $pdo,
|
|
private string $tablePrefix = 'fxrate_'
|
|
) {
|
|
$this->driver = strtolower((string) $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME));
|
|
}
|
|
|
|
public function ensureSchema(): void
|
|
{
|
|
$fetchTable = $this->table('fetches');
|
|
$rateTable = $this->table('rates');
|
|
|
|
if ($this->driver === 'pgsql') {
|
|
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$fetchTable} (
|
|
id SERIAL PRIMARY KEY,
|
|
provider VARCHAR(64) NOT NULL,
|
|
trigger_source VARCHAR(32) NOT NULL DEFAULT 'manual',
|
|
base_currency VARCHAR(10) NOT NULL,
|
|
rate_date DATE NOT NULL,
|
|
fetched_at TIMESTAMP NOT NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
$this->pdo->exec("ALTER TABLE {$fetchTable} ADD COLUMN IF NOT EXISTS trigger_source VARCHAR(32) NOT NULL DEFAULT 'manual'");
|
|
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$rateTable} (
|
|
id SERIAL PRIMARY KEY,
|
|
fetch_id INTEGER NOT NULL REFERENCES {$fetchTable}(id) ON DELETE CASCADE,
|
|
currency_code VARCHAR(10) NOT NULL,
|
|
current_value NUMERIC(20,10) NOT NULL
|
|
)");
|
|
$this->pdo->exec("CREATE INDEX IF NOT EXISTS {$fetchTable}_base_fetch_idx ON {$fetchTable} (base_currency, fetched_at DESC, id DESC)");
|
|
$this->pdo->exec("CREATE INDEX IF NOT EXISTS {$fetchTable}_rate_date_idx ON {$fetchTable} (rate_date DESC)");
|
|
$this->pdo->exec("CREATE INDEX IF NOT EXISTS {$rateTable}_fetch_idx ON {$rateTable} (fetch_id)");
|
|
$this->pdo->exec("CREATE INDEX IF NOT EXISTS {$rateTable}_currency_idx ON {$rateTable} (currency_code)");
|
|
} elseif ($this->driver === 'mysql') {
|
|
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$fetchTable} (
|
|
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
|
provider VARCHAR(64) NOT NULL,
|
|
trigger_source VARCHAR(32) NOT NULL DEFAULT 'manual',
|
|
base_currency VARCHAR(10) NOT NULL,
|
|
rate_date DATE NOT NULL,
|
|
fetched_at DATETIME NOT NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
KEY {$fetchTable}_base_fetch_idx (base_currency, fetched_at, id),
|
|
KEY {$fetchTable}_rate_date_idx (rate_date)
|
|
)");
|
|
$this->ensureColumn($fetchTable, 'trigger_source', "ALTER TABLE {$fetchTable} ADD COLUMN trigger_source VARCHAR(32) NOT NULL DEFAULT 'manual'");
|
|
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$rateTable} (
|
|
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
|
fetch_id INTEGER NOT NULL,
|
|
currency_code VARCHAR(10) NOT NULL,
|
|
current_value DECIMAL(20,10) NOT NULL,
|
|
KEY {$rateTable}_fetch_idx (fetch_id),
|
|
KEY {$rateTable}_currency_idx (currency_code)
|
|
)");
|
|
} else {
|
|
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$fetchTable} (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
provider VARCHAR(64) NOT NULL,
|
|
trigger_source VARCHAR(32) NOT NULL DEFAULT 'manual',
|
|
base_currency VARCHAR(10) NOT NULL,
|
|
rate_date DATE NOT NULL,
|
|
fetched_at DATETIME NOT NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
$this->ensureColumn($fetchTable, 'trigger_source', "ALTER TABLE {$fetchTable} ADD COLUMN trigger_source VARCHAR(32) NOT NULL DEFAULT 'manual'");
|
|
$this->pdo->exec("CREATE TABLE IF NOT EXISTS {$rateTable} (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
fetch_id INTEGER NOT NULL,
|
|
currency_code VARCHAR(10) NOT NULL,
|
|
current_value DECIMAL(20,10) NOT NULL
|
|
)");
|
|
$this->pdo->exec("CREATE INDEX IF NOT EXISTS {$fetchTable}_base_fetch_idx ON {$fetchTable} (base_currency, fetched_at DESC, id DESC)");
|
|
$this->pdo->exec("CREATE INDEX IF NOT EXISTS {$fetchTable}_rate_date_idx ON {$fetchTable} (rate_date DESC)");
|
|
$this->pdo->exec("CREATE INDEX IF NOT EXISTS {$rateTable}_fetch_idx ON {$rateTable} (fetch_id)");
|
|
$this->pdo->exec("CREATE INDEX IF NOT EXISTS {$rateTable}_currency_idx ON {$rateTable} (currency_code)");
|
|
}
|
|
}
|
|
|
|
public function getLatestFetch(?string $baseCurrency = null): ?array
|
|
{
|
|
$sql = 'SELECT id, provider, trigger_source, base_currency, rate_date, fetched_at, created_at FROM ' . $this->table('fetches');
|
|
$params = [];
|
|
if ($baseCurrency !== null && trim($baseCurrency) !== '') {
|
|
$sql .= ' WHERE base_currency = :base_currency';
|
|
$params['base_currency'] = strtoupper(trim($baseCurrency));
|
|
}
|
|
$sql .= ' ORDER BY fetched_at DESC, id DESC LIMIT 1';
|
|
$stmt = $this->pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
return is_array($row) ? $this->normalizeFetch($row) : null;
|
|
}
|
|
|
|
public function listLatestFetches(): array
|
|
{
|
|
$stmt = $this->pdo->query(
|
|
'SELECT id, provider, trigger_source, base_currency, rate_date, fetched_at, created_at
|
|
FROM ' . $this->table('fetches') . '
|
|
ORDER BY fetched_at DESC, id DESC'
|
|
);
|
|
|
|
$latestByBase = [];
|
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) ?: [] as $row) {
|
|
$base = strtoupper(trim((string) ($row['base_currency'] ?? '')));
|
|
if ($base === '' || isset($latestByBase[$base])) {
|
|
continue;
|
|
}
|
|
$latestByBase[$base] = $this->normalizeFetch($row);
|
|
}
|
|
|
|
ksort($latestByBase);
|
|
return array_values($latestByBase);
|
|
}
|
|
|
|
public function listRecentFetches(int $limit = 20): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT id, provider, trigger_source, base_currency, rate_date, fetched_at, created_at
|
|
FROM ' . $this->table('fetches') . '
|
|
ORDER BY fetched_at DESC, id DESC
|
|
LIMIT :limit'
|
|
);
|
|
$stmt->bindValue(':limit', max(1, $limit), PDO::PARAM_INT);
|
|
$stmt->execute();
|
|
|
|
return array_map(
|
|
fn (array $row): array => $this->normalizeFetch($row),
|
|
$stmt->fetchAll(PDO::FETCH_ASSOC) ?: []
|
|
);
|
|
}
|
|
|
|
public function getSnapshotByFetchId(int $fetchId, ?array $symbols = null): ?array
|
|
{
|
|
$fetch = $this->getFetchById($fetchId);
|
|
if ($fetch === null) {
|
|
return null;
|
|
}
|
|
|
|
return $fetch + [
|
|
'rates' => $this->ratesForFetch($fetchId, $symbols),
|
|
];
|
|
}
|
|
|
|
public function findNearestFetch(?string $baseCurrency, string $timestamp, ?int $windowMinutes = null): ?array
|
|
{
|
|
$targetTs = strtotime($timestamp);
|
|
if ($targetTs === false) {
|
|
return null;
|
|
}
|
|
|
|
if ($baseCurrency !== null && trim($baseCurrency) !== '') {
|
|
return $this->getNearestFetch(strtoupper(trim($baseCurrency)), $timestamp, $windowMinutes);
|
|
}
|
|
|
|
$candidates = [];
|
|
foreach (['<=', '>='] as $operator) {
|
|
$order = $operator === '<=' ? 'DESC' : 'ASC';
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT id, provider, trigger_source, base_currency, rate_date, fetched_at, created_at
|
|
FROM ' . $this->table('fetches') . '
|
|
WHERE fetched_at ' . $operator . ' :target_at
|
|
ORDER BY fetched_at ' . $order . ', id ' . $order . '
|
|
LIMIT 1'
|
|
);
|
|
$stmt->execute(['target_at' => $timestamp]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if (is_array($row)) {
|
|
$candidate = $this->normalizeFetch($row);
|
|
$candidateTs = strtotime((string) ($candidate['fetched_at'] ?? ''));
|
|
if ($candidateTs !== false) {
|
|
$candidate['distance_seconds'] = abs($candidateTs - $targetTs);
|
|
$candidates[] = $candidate;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($candidates === []) {
|
|
return null;
|
|
}
|
|
|
|
usort($candidates, static function (array $left, array $right): int {
|
|
return ((int) ($left['distance_seconds'] ?? PHP_INT_MAX)) <=> ((int) ($right['distance_seconds'] ?? PHP_INT_MAX));
|
|
});
|
|
|
|
$selected = $candidates[0];
|
|
if ($windowMinutes !== null && $windowMinutes > 0 && (int) ($selected['distance_seconds'] ?? 0) > ($windowMinutes * 60)) {
|
|
return null;
|
|
}
|
|
|
|
return $selected;
|
|
}
|
|
|
|
public function getNearestFetch(string $baseCurrency, string $timestamp, ?int $windowMinutes = null): ?array
|
|
{
|
|
$baseCurrency = strtoupper(trim($baseCurrency));
|
|
if ($baseCurrency === '') {
|
|
return null;
|
|
}
|
|
|
|
$before = $this->findNeighborFetch($baseCurrency, $timestamp, '<=');
|
|
$after = $this->findNeighborFetch($baseCurrency, $timestamp, '>=');
|
|
$targetTs = strtotime($timestamp);
|
|
if ($targetTs === false) {
|
|
return null;
|
|
}
|
|
|
|
$selected = null;
|
|
$selectedDiff = null;
|
|
foreach ([$before, $after] as $candidate) {
|
|
if (!is_array($candidate)) {
|
|
continue;
|
|
}
|
|
$candidateTs = strtotime((string) ($candidate['fetched_at'] ?? ''));
|
|
if ($candidateTs === false) {
|
|
continue;
|
|
}
|
|
$diffSeconds = abs($candidateTs - $targetTs);
|
|
if ($selected === null || $diffSeconds < (int) $selectedDiff) {
|
|
$selected = $candidate;
|
|
$selectedDiff = $diffSeconds;
|
|
}
|
|
}
|
|
|
|
if ($selected === null) {
|
|
return null;
|
|
}
|
|
|
|
if ($windowMinutes !== null && $windowMinutes > 0 && $selectedDiff !== null && $selectedDiff > ($windowMinutes * 60)) {
|
|
return null;
|
|
}
|
|
|
|
return $selected + ['distance_seconds' => $selectedDiff];
|
|
}
|
|
|
|
public function listDirectHistory(string $baseCurrency, string $targetCurrency, ?string $from = null, ?string $to = null, int $limit = 200): array
|
|
{
|
|
$sql = 'SELECT
|
|
r.id,
|
|
f.id AS fetch_id,
|
|
f.base_currency,
|
|
r.currency_code AS target_currency,
|
|
r.current_value AS rate,
|
|
f.rate_date,
|
|
f.provider,
|
|
f.fetched_at
|
|
FROM ' . $this->table('rates') . ' r
|
|
INNER JOIN ' . $this->table('fetches') . ' f ON f.id = r.fetch_id
|
|
WHERE f.base_currency = :base_currency
|
|
AND r.currency_code = :target_currency';
|
|
$params = [
|
|
'base_currency' => strtoupper(trim($baseCurrency)),
|
|
'target_currency' => strtoupper(trim($targetCurrency)),
|
|
];
|
|
|
|
if ($from !== null && trim($from) !== '') {
|
|
$sql .= ' AND f.fetched_at >= :from_at';
|
|
$params['from_at'] = $from;
|
|
}
|
|
if ($to !== null && trim($to) !== '') {
|
|
$sql .= ' AND f.fetched_at <= :to_at';
|
|
$params['to_at'] = $to;
|
|
}
|
|
|
|
$sql .= ' ORDER BY f.fetched_at DESC, r.id DESC LIMIT :limit';
|
|
$stmt = $this->pdo->prepare($sql);
|
|
foreach ($params as $key => $value) {
|
|
$stmt->bindValue(':' . $key, $value);
|
|
}
|
|
$stmt->bindValue(':limit', max(1, $limit), PDO::PARAM_INT);
|
|
$stmt->execute();
|
|
|
|
return array_map(fn (array $row): array => $this->normalizeRate($row), $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []);
|
|
}
|
|
|
|
public function saveFetch(string $baseCurrency, string $provider, string $rateDate, array $rates, ?string $fetchedAt = null, string $triggerSource = 'manual'): array
|
|
{
|
|
$baseCurrency = strtoupper(trim($baseCurrency));
|
|
$provider = trim($provider) !== '' ? trim($provider) : 'currencyapi';
|
|
$fetchedAt = trim((string) $fetchedAt) !== '' ? trim((string) $fetchedAt) : gmdate('Y-m-d H:i:s');
|
|
$triggerSource = $this->normalizeTriggerSource($triggerSource);
|
|
$normalizedRates = [];
|
|
foreach ($rates as $currencyCode => $rate) {
|
|
$currencyCode = strtoupper(trim((string) $currencyCode));
|
|
if ($currencyCode === '' || $currencyCode === $baseCurrency || !is_numeric($rate)) {
|
|
continue;
|
|
}
|
|
$normalizedRates[$currencyCode] = (float) $rate;
|
|
}
|
|
|
|
$startedTransaction = false;
|
|
if (!$this->pdo->inTransaction()) {
|
|
$this->pdo->beginTransaction();
|
|
$startedTransaction = true;
|
|
}
|
|
|
|
try {
|
|
if ($this->driver === 'pgsql') {
|
|
$fetchStmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('fetches') . ' (
|
|
provider, trigger_source, base_currency, rate_date, fetched_at
|
|
) VALUES (
|
|
:provider, :trigger_source, :base_currency, :rate_date, :fetched_at
|
|
)
|
|
RETURNING *'
|
|
);
|
|
$fetchStmt->execute([
|
|
'provider' => $provider,
|
|
'trigger_source' => $triggerSource,
|
|
'base_currency' => $baseCurrency,
|
|
'rate_date' => $rateDate,
|
|
'fetched_at' => $fetchedAt,
|
|
]);
|
|
$fetch = $this->normalizeFetch($fetchStmt->fetch(PDO::FETCH_ASSOC) ?: []);
|
|
} else {
|
|
$fetchStmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('fetches') . ' (
|
|
provider, trigger_source, base_currency, rate_date, fetched_at
|
|
) VALUES (
|
|
:provider, :trigger_source, :base_currency, :rate_date, :fetched_at
|
|
)'
|
|
);
|
|
$fetchStmt->execute([
|
|
'provider' => $provider,
|
|
'trigger_source' => $triggerSource,
|
|
'base_currency' => $baseCurrency,
|
|
'rate_date' => $rateDate,
|
|
'fetched_at' => $fetchedAt,
|
|
]);
|
|
$fetch = $this->getFetchById((int) $this->pdo->lastInsertId()) ?? [];
|
|
}
|
|
|
|
$savedRates = [];
|
|
if ($normalizedRates !== []) {
|
|
$placeholders = [];
|
|
$params = ['fetch_id' => (int) ($fetch['id'] ?? 0)];
|
|
$index = 0;
|
|
foreach ($normalizedRates as $currencyCode => $rate) {
|
|
$codeKey = 'currency_code_' . $index;
|
|
$valueKey = 'current_value_' . $index;
|
|
$placeholders[] = "(:fetch_id, :{$codeKey}, :{$valueKey})";
|
|
$params[$codeKey] = $currencyCode;
|
|
$params[$valueKey] = $rate;
|
|
$savedRates[] = [
|
|
'fetch_id' => $fetch['id'] ?? null,
|
|
'base_currency' => $baseCurrency,
|
|
'target_currency' => $currencyCode,
|
|
'rate' => $rate,
|
|
'rate_date' => $rateDate,
|
|
'provider' => $provider,
|
|
'fetched_at' => $fetchedAt,
|
|
];
|
|
$index++;
|
|
}
|
|
|
|
$insert = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('rates') . ' (fetch_id, currency_code, current_value) VALUES ' . implode(', ', $placeholders)
|
|
);
|
|
$insert->execute($params);
|
|
}
|
|
|
|
if ($startedTransaction) {
|
|
$this->pdo->commit();
|
|
}
|
|
|
|
return [
|
|
'fetch' => $fetch,
|
|
'rates' => $savedRates,
|
|
];
|
|
} catch (\Throwable $exception) {
|
|
if ($startedTransaction && $this->pdo->inTransaction()) {
|
|
$this->pdo->rollBack();
|
|
}
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
public function findFetchByBaseAndFetchedAt(string $baseCurrency, string $fetchedAt): ?array
|
|
{
|
|
$baseCurrency = strtoupper(trim($baseCurrency));
|
|
$fetchedAt = trim($fetchedAt);
|
|
if ($baseCurrency === '' || $fetchedAt === '') {
|
|
return null;
|
|
}
|
|
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT id, provider, trigger_source, base_currency, rate_date, fetched_at, created_at
|
|
FROM ' . $this->table('fetches') . '
|
|
WHERE base_currency = :base_currency
|
|
AND fetched_at = :fetched_at
|
|
ORDER BY id ASC
|
|
LIMIT 1'
|
|
);
|
|
$stmt->execute([
|
|
'base_currency' => $baseCurrency,
|
|
'fetched_at' => $fetchedAt,
|
|
]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
return is_array($row) ? $this->normalizeFetch($row) : null;
|
|
}
|
|
|
|
private function getFetchById(int $fetchId): ?array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT id, provider, trigger_source, base_currency, rate_date, fetched_at, created_at
|
|
FROM ' . $this->table('fetches') . '
|
|
WHERE id = :id
|
|
LIMIT 1'
|
|
);
|
|
$stmt->execute(['id' => $fetchId]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
return is_array($row) ? $this->normalizeFetch($row) : null;
|
|
}
|
|
|
|
private function findNeighborFetch(string $baseCurrency, string $timestamp, string $operator): ?array
|
|
{
|
|
$order = $operator === '<=' ? 'DESC' : 'ASC';
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT id, provider, trigger_source, base_currency, rate_date, fetched_at, created_at
|
|
FROM ' . $this->table('fetches') . '
|
|
WHERE base_currency = :base_currency
|
|
AND fetched_at ' . $operator . ' :target_at
|
|
ORDER BY fetched_at ' . $order . ', id ' . $order . '
|
|
LIMIT 1'
|
|
);
|
|
$stmt->execute([
|
|
'base_currency' => $baseCurrency,
|
|
'target_at' => $timestamp,
|
|
]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
return is_array($row) ? $this->normalizeFetch($row) : null;
|
|
}
|
|
|
|
private function ratesForFetch(int $fetchId, ?array $symbols = null): array
|
|
{
|
|
$sql = 'SELECT currency_code, current_value FROM ' . $this->table('rates') . ' WHERE fetch_id = :fetch_id';
|
|
$params = ['fetch_id' => $fetchId];
|
|
|
|
$normalizedSymbols = [];
|
|
if (is_array($symbols)) {
|
|
foreach ($symbols as $symbol) {
|
|
$symbol = strtoupper(trim((string) $symbol));
|
|
if ($symbol !== '') {
|
|
$normalizedSymbols[] = $symbol;
|
|
}
|
|
}
|
|
$normalizedSymbols = array_values(array_unique($normalizedSymbols));
|
|
}
|
|
|
|
if ($normalizedSymbols !== []) {
|
|
$placeholders = [];
|
|
foreach ($normalizedSymbols as $index => $symbol) {
|
|
$key = 'symbol_' . $index;
|
|
$placeholders[] = ':' . $key;
|
|
$params[$key] = $symbol;
|
|
}
|
|
$sql .= ' AND currency_code IN (' . implode(', ', $placeholders) . ')';
|
|
}
|
|
|
|
$sql .= ' ORDER BY currency_code ASC';
|
|
$stmt = $this->pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
|
|
$rates = [];
|
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) ?: [] as $row) {
|
|
$code = strtoupper(trim((string) ($row['currency_code'] ?? '')));
|
|
$rate = $row['current_value'] ?? null;
|
|
if ($code === '' || !is_numeric($rate)) {
|
|
continue;
|
|
}
|
|
$rates[$code] = (float) $rate;
|
|
}
|
|
|
|
return $rates;
|
|
}
|
|
|
|
private function normalizeFetch(array $row): array
|
|
{
|
|
return [
|
|
'id' => isset($row['id']) ? (int) $row['id'] : null,
|
|
'provider' => (string) ($row['provider'] ?? ''),
|
|
'trigger_source' => (string) ($row['trigger_source'] ?? 'manual'),
|
|
'base_currency' => strtoupper((string) ($row['base_currency'] ?? '')),
|
|
'rate_date' => (string) ($row['rate_date'] ?? ''),
|
|
'fetched_at' => (string) ($row['fetched_at'] ?? ''),
|
|
'created_at' => (string) ($row['created_at'] ?? ''),
|
|
];
|
|
}
|
|
|
|
private function ensureColumn(string $table, string $column, string $alterSql): void
|
|
{
|
|
try {
|
|
$stmt = $this->pdo->query('SELECT * FROM ' . $table . ' LIMIT 1');
|
|
if ($stmt instanceof \PDOStatement) {
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
|
if (in_array(strtolower($column), array_map('strtolower', array_keys($row)), true)) {
|
|
return;
|
|
}
|
|
}
|
|
} catch (\Throwable) {
|
|
}
|
|
|
|
try {
|
|
$this->pdo->exec($alterSql);
|
|
} catch (\Throwable) {
|
|
}
|
|
}
|
|
|
|
private function normalizeTriggerSource(string $source): string
|
|
{
|
|
$source = strtolower(trim($source));
|
|
return match ($source) {
|
|
'cron', 'manual', 'api', 'migration' => $source,
|
|
default => 'manual',
|
|
};
|
|
}
|
|
|
|
private function normalizeRate(array $row): array
|
|
{
|
|
return [
|
|
'id' => isset($row['id']) ? (int) $row['id'] : null,
|
|
'fetch_id' => isset($row['fetch_id']) ? (int) $row['fetch_id'] : null,
|
|
'base_currency' => strtoupper((string) ($row['base_currency'] ?? '')),
|
|
'target_currency' => strtoupper((string) ($row['target_currency'] ?? '')),
|
|
'rate' => is_numeric($row['rate'] ?? null) ? (float) $row['rate'] : null,
|
|
'rate_date' => (string) ($row['rate_date'] ?? ''),
|
|
'provider' => (string) ($row['provider'] ?? ''),
|
|
'fetched_at' => (string) ($row['fetched_at'] ?? ''),
|
|
];
|
|
}
|
|
|
|
private function table(string $logicalName): string
|
|
{
|
|
return $this->tablePrefix . preg_replace('/[^a-zA-Z0-9_]/', '', $logicalName);
|
|
}
|
|
}
|