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

This commit is contained in:
2026-07-13 23:08:56 +02:00
parent 554664737a
commit 328a40eedc
4 changed files with 296 additions and 32 deletions

View File

@@ -1262,8 +1262,10 @@ final class Router
$this->assertCurrencyType($settings['report_currency'], false, 'report_currency');
$this->assertCurrencyType($settings['crypto_currency'], true, 'crypto_currency');
$currencyChange = $this->prepareMiningCurrencyChange($projectKey, $existingSettings, $settings);
$this->repository()->saveSettings($projectKey, $settings);
$this->applyPreparedMiningCurrencyChange($projectKey, $currencyChange);
$this->syncFxRatesPreferredCurrencies($settings['preferred_currencies']);
return $this->settings($projectKey);
}
@@ -1973,12 +1975,17 @@ final class Router
$balances = [];
}
$walletCurrency = $this->optionalCurrency($input['wallet_currency'] ?? null)
?? $this->inferWalletCurrencyFromBalances($balances)
?? $this->latestWalletCurrency($projectKey)
?? 'DOGE';
$payload = [
'measured_at' => $this->requiredDateTime($input['measured_at'] ?? null, 'measured_at', $this->projectTimezone($projectKey)),
'total_value_amount' => $this->optionalDecimal($input['total_value_amount'] ?? null),
'total_value_currency' => $this->optionalCurrency($input['total_value_currency'] ?? null),
'wallet_balance' => $this->optionalDecimal($input['wallet_balance'] ?? null),
'wallet_currency' => $this->requiredCurrency($input['wallet_currency'] ?? ($this->settings($projectKey)['crypto_currency'] ?? 'DOGE'), 'wallet_currency'),
'wallet_currency' => $this->requiredCurrency($walletCurrency, 'wallet_currency'),
'balances_json' => $balances,
'note' => $this->optionalString($input['note'] ?? null, 1000),
'source' => $this->enumValue($input['source'] ?? 'manual', ['manual', 'image_ocr', 'seed_import'], 'source'),
@@ -2002,10 +2009,14 @@ final class Router
private function saveWalletWithdrawal(string $projectKey, array $input): array
{
$withdrawalCurrency = $this->optionalCurrency($input['withdrawal_currency'] ?? null)
?? $this->latestWalletCurrency($projectKey)
?? 'DOGE';
$payload = [
'withdrawal_at' => $this->requiredDateTime($input['withdrawal_at'] ?? null, 'withdrawal_at', $this->projectTimezone($projectKey)),
'coins_amount' => $this->requiredDecimal($input['coins_amount'] ?? null, 'coins_amount'),
'withdrawal_currency' => $this->requiredCurrency($input['withdrawal_currency'] ?? 'DOGE', 'withdrawal_currency'),
'withdrawal_currency' => $this->requiredCurrency($withdrawalCurrency, 'withdrawal_currency'),
'note' => $this->optionalString($input['note'] ?? null, 1000),
];
@@ -2765,6 +2776,177 @@ final class Router
return $this->requiredCurrency($settings['crypto_currency'] ?? 'DOGE', 'crypto_currency');
}
private function prepareMiningCurrencyChange(string $projectKey, array $existingSettings, array &$nextSettings): ?array
{
$previousCurrency = $this->requiredCurrency($existingSettings['crypto_currency'] ?? ($nextSettings['crypto_currency'] ?? 'DOGE'), 'crypto_currency');
$nextCurrency = $this->requiredCurrency($nextSettings['crypto_currency'] ?? 'DOGE', 'crypto_currency');
if ($previousCurrency === $nextCurrency) {
return null;
}
$latestMeasurement = $this->repository()->listRecentMeasurements($projectKey, 1)[0] ?? null;
$fetchId = null;
try {
$fresh = $this->fx()->refreshLatestRates(null, 'USD');
$fetchId = is_numeric($fresh['fetch_id'] ?? null) ? (int) $fresh['fetch_id'] : null;
} catch (\Throwable) {
$fetchId = null;
}
$baselineAmount = $this->optionalDecimal($nextSettings['baseline_coins_total'] ?? null);
if ($baselineAmount !== null && $baselineAmount > 0) {
$convertedBaseline = $this->convertMiningAssetAmount(
$baselineAmount,
$previousCurrency,
$nextCurrency,
(string) ($nextSettings['baseline_measured_at'] ?? $existingSettings['baseline_measured_at'] ?? ''),
$fetchId
);
if ($convertedBaseline === null) {
throw new ApiException('Baseline konnte nicht in die neue Mining-Waehrung umgerechnet werden.', 422, [
'from_currency' => $previousCurrency,
'to_currency' => $nextCurrency,
]);
}
$nextSettings['baseline_coins_total'] = $convertedBaseline;
}
return [
'previous_currency' => $previousCurrency,
'next_currency' => $nextCurrency,
'latest_measurement' => is_array($latestMeasurement) ? $latestMeasurement : null,
'fetch_id' => $fetchId,
];
}
private function applyPreparedMiningCurrencyChange(string $projectKey, ?array $change): void
{
if (!is_array($change)) {
return;
}
$latestMeasurement = is_array($change['latest_measurement'] ?? null) ? $change['latest_measurement'] : null;
if (!is_array($latestMeasurement)) {
return;
}
$sourceCurrency = $this->requiredCurrency($latestMeasurement['coin_currency'] ?? ($change['previous_currency'] ?? null), 'coin_currency');
$targetCurrency = $this->requiredCurrency($change['next_currency'] ?? null, 'crypto_currency');
if ($sourceCurrency === $targetCurrency) {
return;
}
$visibleCoins = $this->optionalDecimal($latestMeasurement['coins_total'] ?? null) ?? 0.0;
$convertedCoins = $this->convertMiningAssetAmount(
$visibleCoins,
$sourceCurrency,
$targetCurrency,
(string) ($latestMeasurement['measured_at'] ?? $this->currentTimestamp()),
is_numeric($change['fetch_id'] ?? null) ? (int) $change['fetch_id'] : null
);
if ($convertedCoins === null) {
throw new ApiException('Aktueller Miner-Bestand konnte nicht in die neue Mining-Waehrung umgerechnet werden.', 422, [
'from_currency' => $sourceCurrency,
'to_currency' => $targetCurrency,
]);
}
$priceCurrency = $this->optionalCurrency($latestMeasurement['price_currency'] ?? null) ?? 'USD';
$convertedPricePerCoin = $this->convertMiningAssetAmount(
1.0,
$targetCurrency,
$priceCurrency,
$this->currentTimestamp(),
is_numeric($change['fetch_id'] ?? null) ? (int) $change['fetch_id'] : null
);
$this->repository()->createMeasurement($projectKey, [
'measured_at' => $this->currentTimestamp(),
'coins_total' => $convertedCoins,
'coin_currency' => $targetCurrency,
'price_per_coin' => $convertedPricePerCoin,
'price_currency' => $priceCurrency,
'fx_fetch_id' => is_numeric($change['fetch_id'] ?? null) ? (int) $change['fetch_id'] : null,
'note' => sprintf(
'Mining-Waehrung umgestellt: %s -> %s. Vorhandener Miner-Bestand wurde automatisch umgerechnet.',
$sourceCurrency,
$targetCurrency
),
'source' => 'manual',
'image_path' => null,
'ocr_raw_text' => null,
'ocr_confidence' => null,
'ocr_flags' => ['currency_change'],
]);
}
private function convertMiningAssetAmount(float $amount, string $fromCurrency, string $toCurrency, string $at = '', ?int $fetchId = null): ?float
{
if ($amount === 0.0) {
return 0.0;
}
$from = $this->requiredCurrency($fromCurrency, 'from_currency');
$to = $this->requiredCurrency($toCurrency, 'to_currency');
if ($from === $to) {
return $amount;
}
$converted = $this->fx()->convertAt(
$amount,
$from,
$to,
$at !== '' ? $at : null,
null,
$fetchId
);
return is_numeric($converted) ? (float) $converted : null;
}
private function latestWalletCurrency(string $projectKey): ?string
{
$latestSnapshot = $this->repository()->listWalletSnapshots($projectKey, 1)[0] ?? null;
$snapshotCurrency = $this->optionalCurrency(is_array($latestSnapshot) ? ($latestSnapshot['wallet_currency'] ?? null) : null);
if ($snapshotCurrency !== null) {
return $snapshotCurrency;
}
if ($this->repository()->tableExists('wallet_withdrawals')) {
$withdrawals = $this->repository()->listWalletWithdrawals($projectKey);
if ($withdrawals !== []) {
$withdrawalCurrency = $this->optionalCurrency($withdrawals[array_key_last($withdrawals)]['withdrawal_currency'] ?? null);
if ($withdrawalCurrency !== null) {
return $withdrawalCurrency;
}
}
}
$payouts = $this->repository()->listPayouts($projectKey);
if ($payouts !== []) {
return $this->optionalCurrency($payouts[array_key_last($payouts)]['payout_currency'] ?? null);
}
return null;
}
private function inferWalletCurrencyFromBalances(array $balances): ?string
{
foreach ($balances as $code => $asset) {
$currency = $this->optionalCurrency($code);
if ($currency === null) {
continue;
}
$balance = is_array($asset) ? ($asset['balance'] ?? null) : $asset;
if (is_numeric($balance)) {
return $currency;
}
}
return null;
}
private function ensureMeasurementFxReferences(string $projectKey, array $rows, ?array $settings = null): array
{
$maxAgeHours = self::FX_FETCH_MAX_AGE_HOURS;