Importer
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-11 03:02:39 +02:00
parent 394935a53b
commit 7400caa687
3 changed files with 282 additions and 21 deletions

View File

@@ -388,13 +388,16 @@ final class Router
'project' => $this->repository()->getProject($projectKey),
'settings' => $this->safeRead(fn () => $this->repository()->getSettings($projectKey)),
'currencies' => $this->safeRead(fn () => $this->repository()->listCurrencies(), []),
'currency_aliases' => $this->safeRead(fn () => $this->repository()->listCurrencyAliases(), []),
'cost_plans' => $this->safeRead(fn () => $this->repository()->listCostPlans($projectKey), []),
'measurements' => $this->safeRead(fn () => $this->repository()->listMeasurements($projectKey, 5000), []),
'measurements' => $this->safeRead(fn () => $this->repository()->listAllMeasurements($projectKey), []),
'measurement_rates' => $this->safeRead(fn () => $this->repository()->listMeasurementRates($projectKey), []),
'payouts' => $this->safeRead(fn () => $this->repository()->listPayouts($projectKey), []),
'targets' => $this->safeRead(fn () => $this->repository()->listTargets($projectKey), []),
'dashboards' => $this->safeRead(fn () => $this->repository()->listDashboards($projectKey), []),
'miner_offers' => $this->safeRead(fn () => $this->repository()->listMinerOffers($projectKey), []),
'purchased_miners' => $this->safeRead(fn () => $this->repository()->listPurchasedMiners($projectKey), []),
'fx_rates' => $this->safeRead(fn () => $this->repository()->listAllFxRates(), []),
];
$result = $this->schemaManager()->rebuildSchemaDirect();
@@ -415,10 +418,17 @@ final class Router
'name' => $currency['name'],
'symbol' => $currency['symbol'] ?? null,
'is_active' => !empty($currency['is_active']) ? 1 : 0,
'is_crypto' => !empty($currency['is_crypto']) ? 1 : 0,
'sort_order' => (int) ($currency['sort_order'] ?? 0),
]);
}
foreach ($backup['currency_aliases'] as $alias) {
if (!empty($alias['alias_code']) && !empty($alias['currency_code'])) {
$this->repository()->saveCurrencyAlias((string) $alias['alias_code'], (string) $alias['currency_code']);
}
}
if (is_array($backup['settings'])) {
$this->repository()->saveSettings($projectKey, [
'baseline_measured_at' => $backup['settings']['baseline_measured_at'],
@@ -429,6 +439,8 @@ final class Router
'crypto_currency' => $backup['settings']['crypto_currency'] ?? 'DOGE',
'display_timezone' => $backup['settings']['display_timezone'] ?? 'Europe/Berlin',
'fx_max_age_hours' => $backup['settings']['fx_max_age_hours'] ?? 3,
'module_theme_mode' => $backup['settings']['module_theme_mode'] ?? 'inherit',
'module_theme_accent' => $backup['settings']['module_theme_accent'] ?? 'teal',
'preferred_currencies' => $backup['settings']['preferred_currencies'] ?? ['DOGE', 'USD', 'EUR'],
]);
}
@@ -450,6 +462,21 @@ final class Router
]);
}
$measurementRatesByOldId = [];
foreach ($backup['measurement_rates'] as $rate) {
$oldMeasurementId = (int) ($rate['measurement_id'] ?? 0);
if ($oldMeasurementId > 0) {
$measurementRatesByOldId[$oldMeasurementId][] = [
'base_currency' => $rate['base_currency'] ?? null,
'quote_currency' => $rate['quote_currency'] ?? null,
'rate' => $rate['rate'] ?? null,
'provider' => $rate['provider'] ?? 'derived',
];
}
}
$measurementIdMap = [];
$restoredMeasurementRates = 0;
foreach ($backup['measurements'] as $measurement) {
$created = $this->repository()->createMeasurementIfNotExists($projectKey, [
'measured_at' => $measurement['measured_at'],
@@ -464,7 +491,17 @@ final class Router
'ocr_flags' => $measurement['ocr_flags'] ?? null,
]);
if (is_array($created)) {
$this->captureMeasurementRates($projectKey, $created);
$oldMeasurementId = (int) ($measurement['id'] ?? 0);
$newMeasurementId = (int) ($created['id'] ?? 0);
if ($oldMeasurementId > 0 && $newMeasurementId > 0) {
$measurementIdMap[$oldMeasurementId] = $newMeasurementId;
}
if ($oldMeasurementId > 0 && isset($measurementRatesByOldId[$oldMeasurementId])) {
$this->repository()->replaceMeasurementRates($newMeasurementId, $projectKey, $measurementRatesByOldId[$oldMeasurementId]);
$restoredMeasurementRates += count($measurementRatesByOldId[$oldMeasurementId]);
} else {
$this->captureMeasurementRates($projectKey, $created);
}
}
}
@@ -477,12 +514,36 @@ final class Router
]);
}
$minerOfferIdMap = [];
foreach ($backup['miner_offers'] as $offer) {
$savedOffer = $this->repository()->saveMinerOffer($projectKey, [
'label' => $offer['label'],
'runtime_months' => $offer['runtime_months'] ?? null,
'mining_speed_value' => $offer['mining_speed_value'] ?? null,
'mining_speed_unit' => $offer['mining_speed_unit'] ?? null,
'bonus_speed_value' => $offer['bonus_speed_value'] ?? null,
'bonus_speed_unit' => $offer['bonus_speed_unit'] ?? null,
'base_price_amount' => $offer['base_price_amount'] ?? $offer['reference_price_amount'] ?? $offer['usd_reference_amount'] ?? $offer['price_amount'],
'base_price_currency' => $offer['base_price_currency'] ?? $offer['reference_price_currency'] ?? (is_numeric($offer['usd_reference_amount'] ?? null) ? 'USD' : ($offer['price_currency'] ?? null)),
'payment_type' => $offer['payment_type'] ?? (!empty($offer['price_currency']) && in_array(strtoupper((string) $offer['price_currency']), ['ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP'], true) ? 'crypto' : 'fiat'),
'auto_renew' => !empty($offer['auto_renew']) ? 1 : 0,
'note' => $offer['note'] ?? null,
'is_active' => !empty($offer['is_active']) ? 1 : 0,
]);
$oldOfferId = (int) ($offer['id'] ?? 0);
$newOfferId = (int) ($savedOffer['id'] ?? 0);
if ($oldOfferId > 0 && $newOfferId > 0) {
$minerOfferIdMap[$oldOfferId] = $newOfferId;
}
}
foreach ($backup['targets'] as $target) {
$oldOfferId = (int) ($target['miner_offer_id'] ?? 0);
$this->repository()->saveTarget($projectKey, [
'label' => $target['label'],
'target_amount_fiat' => $target['target_amount_fiat'],
'currency' => $target['currency'],
'miner_offer_id' => $target['miner_offer_id'] ?? null,
'miner_offer_id' => $oldOfferId > 0 ? ($minerOfferIdMap[$oldOfferId] ?? null) : null,
'is_active' => !empty($target['is_active']) ? 1 : 0,
'sort_order' => (int) ($target['sort_order'] ?? 0),
]);
@@ -500,26 +561,10 @@ final class Router
]);
}
foreach ($backup['miner_offers'] as $offer) {
$this->repository()->saveMinerOffer($projectKey, [
'label' => $offer['label'],
'runtime_months' => $offer['runtime_months'] ?? null,
'mining_speed_value' => $offer['mining_speed_value'] ?? null,
'mining_speed_unit' => $offer['mining_speed_unit'] ?? null,
'bonus_speed_value' => $offer['bonus_speed_value'] ?? null,
'bonus_speed_unit' => $offer['bonus_speed_unit'] ?? null,
'base_price_amount' => $offer['base_price_amount'] ?? $offer['reference_price_amount'] ?? $offer['usd_reference_amount'] ?? $offer['price_amount'],
'base_price_currency' => $offer['base_price_currency'] ?? $offer['reference_price_currency'] ?? (is_numeric($offer['usd_reference_amount'] ?? null) ? 'USD' : ($offer['price_currency'] ?? null)),
'payment_type' => $offer['payment_type'] ?? (!empty($offer['price_currency']) && in_array(strtoupper((string) $offer['price_currency']), ['ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP'], true) ? 'crypto' : 'fiat'),
'auto_renew' => !empty($offer['auto_renew']) ? 1 : 0,
'note' => $offer['note'] ?? null,
'is_active' => !empty($offer['is_active']) ? 1 : 0,
]);
}
foreach ($backup['purchased_miners'] as $miner) {
$oldOfferId = (int) ($miner['miner_offer_id'] ?? 0);
$this->repository()->restorePurchasedMiner($projectKey, [
'miner_offer_id' => null,
'miner_offer_id' => $oldOfferId > 0 ? ($minerOfferIdMap[$oldOfferId] ?? null) : null,
'purchased_at' => $miner['purchased_at'],
'label' => $miner['label'],
'runtime_months' => $miner['runtime_months'] ?? null,
@@ -538,15 +583,49 @@ final class Router
]);
}
$fxRatesByFetch = [];
foreach ($backup['fx_rates'] as $rate) {
$fetchId = (int) ($rate['fetch_id'] ?? 0);
$targetCurrency = strtoupper(trim((string) ($rate['target_currency'] ?? '')));
if ($fetchId <= 0 || $targetCurrency === '' || !is_numeric($rate['rate'] ?? null)) {
continue;
}
if (!isset($fxRatesByFetch[$fetchId])) {
$fxRatesByFetch[$fetchId] = [
'base_currency' => $rate['base_currency'] ?? '',
'provider' => $rate['provider'] ?? 'currencyapi',
'rate_date' => $rate['rate_date'] ?? date('Y-m-d'),
'fetched_at' => $rate['fetched_at'] ?? null,
'rates' => [],
];
}
$fxRatesByFetch[$fetchId]['rates'][$targetCurrency] = (float) $rate['rate'];
}
$restoredFxRates = 0;
foreach ($fxRatesByFetch as $fetch) {
$restored = $this->repository()->restoreFxFetch(
(string) $fetch['base_currency'],
(string) $fetch['provider'],
(string) $fetch['rate_date'],
is_string($fetch['fetched_at'] ?? null) ? $fetch['fetched_at'] : null,
$fetch['rates']
);
$restoredFxRates += count($restored['rates'] ?? []);
}
return array_merge($result, [
'restored' => [
'measurements' => count($backup['measurements']),
'measurement_rates' => $restoredMeasurementRates,
'purchased_miners' => count($backup['purchased_miners']),
'cost_plans' => count($backup['cost_plans']),
'payouts' => count($backup['payouts']),
'targets' => count($backup['targets']),
'dashboards' => count($backup['dashboards']),
'miner_offers' => count($backup['miner_offers']),
'currency_aliases' => count($backup['currency_aliases']),
'fx_rates' => $restoredFxRates,
],
]);
}

View File

@@ -410,6 +410,20 @@ final class MiningRepository
return $this->normalizeRows($stmt->fetchAll() ?: []);
}
public function listAllMeasurements(string $projectKey): array
{
$stmt = $this->pdo->prepare(
'SELECT * FROM ' . $this->table('measurements') . '
WHERE project_key = :project_key AND owner_sub = :owner_sub
ORDER BY measured_at ASC'
);
$stmt->execute([
'project_key' => $projectKey,
'owner_sub' => $this->ownerSub,
]);
return $this->normalizeRows($stmt->fetchAll() ?: []);
}
public function createMeasurement(string $projectKey, array $payload): array
{
$this->debug?->add('db.createMeasurement.start', [
@@ -979,6 +993,137 @@ final class MiningRepository
return $this->normalizeRows($stmt->fetchAll() ?: []);
}
public function listAllFxRates(): array
{
$stmt = $this->pdo->query(
'SELECT
r.id,
r.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('fx_rates') . ' r
INNER JOIN ' . $this->table('fx_fetches') . ' f ON f.id = r.fetch_id
ORDER BY f.fetched_at ASC, f.id ASC, r.currency_code ASC'
);
return $this->normalizeRows($stmt->fetchAll() ?: []);
}
public function restoreFxFetch(string $baseCurrency, string $provider, string $rateDate, ?string $fetchedAt, array $rates): array
{
$baseCurrency = strtoupper(trim($baseCurrency));
$provider = trim($provider) !== '' ? trim($provider) : 'currencyapi';
$fetchedAt = trim((string) $fetchedAt) !== '' ? (string) $fetchedAt : $this->currentUtcTimestamp();
if ($rates === []) {
return ['fetch' => null, 'rates' => []];
}
$currenciesToEnsure = [
[
'code' => substr($baseCurrency, 0, 10),
'name' => $baseCurrency,
'symbol' => substr($baseCurrency, 0, 8),
'is_active' => 1,
'is_crypto' => $this->isCryptoCode($baseCurrency) ? 1 : 0,
'sort_order' => 1000,
],
];
foreach (array_keys($rates) as $currencyCode) {
$normalizedCurrencyCode = strtoupper(trim((string) $currencyCode));
if ($normalizedCurrencyCode === '') {
continue;
}
$currenciesToEnsure[] = [
'code' => substr($normalizedCurrencyCode, 0, 10),
'name' => $normalizedCurrencyCode,
'symbol' => substr($normalizedCurrencyCode, 0, 8),
'is_active' => 1,
'is_crypto' => $this->isCryptoCode($normalizedCurrencyCode) ? 1 : 0,
'sort_order' => 1000,
];
}
$this->saveCurrencies($currenciesToEnsure);
if ($this->driver === 'pgsql') {
$fetchStmt = $this->pdo->prepare(
'INSERT INTO ' . $this->table('fx_fetches') . ' (
provider, base_currency, rate_date, fetched_at
) VALUES (
:provider, :base_currency, :rate_date, :fetched_at
)
RETURNING *'
);
$fetchStmt->execute([
'provider' => $provider,
'base_currency' => $baseCurrency,
'rate_date' => $rateDate,
'fetched_at' => $fetchedAt,
]);
$fetch = $this->normalizeRow($fetchStmt->fetch() ?: []);
} else {
$fetchStmt = $this->pdo->prepare(
'INSERT INTO ' . $this->table('fx_fetches') . ' (
provider, base_currency, rate_date, fetched_at
) VALUES (
:provider, :base_currency, :rate_date, :fetched_at
)'
);
$fetchStmt->execute([
'provider' => $provider,
'base_currency' => $baseCurrency,
'rate_date' => $rateDate,
'fetched_at' => $fetchedAt,
]);
$fetchId = (int) $this->pdo->lastInsertId();
$fetchLookup = $this->pdo->prepare('SELECT * FROM ' . $this->table('fx_fetches') . ' WHERE id = :id LIMIT 1');
$fetchLookup->execute(['id' => $fetchId]);
$fetch = $this->normalizeRow($fetchLookup->fetch() ?: []);
}
$placeholders = [];
$params = ['fetch_id' => $fetch['id']];
$savedRates = [];
$index = 0;
foreach ($rates as $currencyCode => $rate) {
if (!is_numeric($rate)) {
continue;
}
$codeKey = 'currency_code_' . $index;
$valueKey = 'current_value_' . $index;
$targetCurrency = strtoupper(trim((string) $currencyCode));
if ($targetCurrency === '') {
continue;
}
$placeholders[] = "(:fetch_id, :{$codeKey}, :{$valueKey})";
$params[$codeKey] = $targetCurrency;
$params[$valueKey] = (float) $rate;
$savedRates[] = [
'fetch_id' => $fetch['id'],
'base_currency' => $baseCurrency,
'target_currency' => $targetCurrency,
'rate' => (float) $rate,
'rate_date' => $rateDate,
'provider' => $provider,
'fetched_at' => $fetch['fetched_at'] ?? $fetchedAt,
];
$index++;
}
if ($placeholders !== []) {
$insert = $this->pdo->prepare('INSERT INTO ' . $this->table('fx_rates') . ' (fetch_id, currency_code, current_value) VALUES ' . implode(', ', $placeholders));
$insert->execute($params);
}
return [
'fetch' => $fetch,
'rates' => $savedRates,
];
}
public function getLatestMeasurementRate(string $baseCurrency, string $targetCurrency): ?array
{
$stmt = $this->pdo->prepare(