asdasd
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -429,8 +429,15 @@ final class AnalyticsService
|
||||
$postMeasurementPayoutAmount = $this->postMeasurementPayoutAmount($payouts, $latestMeasuredTs, $latestAsset);
|
||||
$visibleCoinsAtMeasurement = (float) ($latest['coins_total_visible'] ?? $latest['coins_total'] ?? 0.0);
|
||||
$adjustedVisibleCoins = max(0.0, $visibleCoinsAtMeasurement - $postMeasurementPayoutAmount);
|
||||
$walletBalanceCurrentAsset = (float) ($walletBalances[$latestAsset] ?? 0.0);
|
||||
$holdingsCurrentAsset = $walletBalanceCurrentAsset + $adjustedVisibleCoins;
|
||||
$walletBalanceCurrentAssetDirect = (float) ($walletBalances[$latestAsset] ?? 0.0);
|
||||
$walletBalanceCurrentAssetEquivalent = $latestAsset !== ''
|
||||
? (
|
||||
$useWalletSnapshot
|
||||
? $this->walletSnapshotValue($latestWalletSnapshot, $latestAsset, $latest)
|
||||
: $this->walletBalanceValue($walletBalances, $latestAsset, $latest)
|
||||
)
|
||||
: null;
|
||||
$holdingsCurrentAsset = ($walletBalanceCurrentAssetEquivalent !== null ? $walletBalanceCurrentAssetEquivalent : 0.0) + $adjustedVisibleCoins;
|
||||
$walletValue = $latestCurrency !== ''
|
||||
? (
|
||||
$useWalletSnapshot
|
||||
@@ -478,7 +485,8 @@ final class AnalyticsService
|
||||
'invested_capital' => $this->roundOrNull($cashInvestedCapital, 8),
|
||||
'cash_invested_capital' => $this->roundOrNull($cashInvestedCapital, 8),
|
||||
'reinvested_capital' => $this->roundOrNull($reinvestedCapital, 8),
|
||||
'wallet_balance_current_asset' => $this->roundOrNull($walletBalanceCurrentAsset, 6),
|
||||
'wallet_balance_current_asset' => $this->roundOrNull($walletBalanceCurrentAssetEquivalent, 6),
|
||||
'wallet_balance_current_asset_direct' => $this->roundOrNull($walletBalanceCurrentAssetDirect, 6),
|
||||
'holdings_current_asset' => $this->roundOrNull($holdingsCurrentAsset, 6),
|
||||
'wallet_value' => $this->roundOrNull($walletValue, 8),
|
||||
'wallet_snapshot_measured_at' => $useWalletSnapshot ? (string) ($latestWalletSnapshot['measured_at'] ?? '') : null,
|
||||
@@ -533,7 +541,8 @@ final class AnalyticsService
|
||||
'current_visible_coins' => $this->roundOrNull($adjustedVisibleCoins, 6),
|
||||
'current_effective_coins' => $this->roundOrNull((float) ($latest['coins_total_effective'] ?? $latest['coins_total']), 6),
|
||||
'wallet_balances' => $walletBalances,
|
||||
'wallet_balance_current_asset' => $this->roundOrNull($walletBalanceCurrentAsset, 6),
|
||||
'wallet_balance_current_asset' => $this->roundOrNull($walletBalanceCurrentAssetEquivalent, 6),
|
||||
'wallet_balance_current_asset_direct' => $this->roundOrNull($walletBalanceCurrentAssetDirect, 6),
|
||||
'holdings_current_asset' => $this->roundOrNull($holdingsCurrentAsset, 6),
|
||||
],
|
||||
'current_hashrate_mh' => $this->roundOrNull($currentHashrateMh, 4),
|
||||
|
||||
@@ -49,7 +49,8 @@ final class OcrService
|
||||
$parsed = $this->parseText(
|
||||
$rawText,
|
||||
(string) ($input['date_context'] ?? date('Y-m-d')),
|
||||
strtoupper(trim((string) ($input['wallet_currency_hint'] ?? '')))
|
||||
strtoupper(trim((string) ($input['wallet_currency_hint'] ?? ''))),
|
||||
strtoupper(trim((string) ($input['mining_currency_hint'] ?? '')))
|
||||
);
|
||||
$parsed['image_path'] = $targetFile;
|
||||
$parsed['raw_text'] = $rawText;
|
||||
@@ -295,9 +296,9 @@ final class OcrService
|
||||
return $binary !== '' && trim((string) shell_exec('command -v ' . escapeshellarg($binary) . ' 2>/dev/null')) !== '';
|
||||
}
|
||||
|
||||
private function parseText(string $rawText, string $dateContext, string $walletCurrencyHint = ''): array
|
||||
private function parseText(string $rawText, string $dateContext, string $walletCurrencyHint = '', string $miningCurrencyHint = ''): array
|
||||
{
|
||||
$measurement = $this->parseMeasurementText($rawText, $dateContext);
|
||||
$measurement = $this->parseMeasurementText($rawText, $dateContext, $miningCurrencyHint);
|
||||
$wallet = $this->parseWalletText($rawText, $dateContext, $walletCurrencyHint);
|
||||
|
||||
$isWallet = ($wallet['score'] ?? 0) > ($measurement['score'] ?? 0)
|
||||
@@ -315,7 +316,7 @@ final class OcrService
|
||||
];
|
||||
}
|
||||
|
||||
private function parseMeasurementText(string $rawText, string $dateContext): array
|
||||
private function parseMeasurementText(string $rawText, string $dateContext, string $miningCurrencyHint = ''): array
|
||||
{
|
||||
$flags = [];
|
||||
$suggestedTime = null;
|
||||
@@ -352,7 +353,11 @@ final class OcrService
|
||||
];
|
||||
}
|
||||
|
||||
if (preg_match('/DOGE\s*\/\s*(USD|EUR|USDT|USDC|BTC|ETH|LTC)/i', $normalizedText, $pairMatch)) {
|
||||
$assetPattern = $miningCurrencyHint !== ''
|
||||
? preg_quote($miningCurrencyHint, '/')
|
||||
: '(?:DOGE|BTC|ETH|LTC|XRP|ADA|SOL|USDT|USDC|TRX|XMR|DOT|ARB|BNB|AVAX|MATIC)';
|
||||
|
||||
if (preg_match('/' . $assetPattern . '\s*\/\s*(USD|EUR|USDT|USDC|BTC|ETH|LTC)/i', $normalizedText, $pairMatch)) {
|
||||
$currency = strtoupper((string) $pairMatch[1]);
|
||||
} elseif (preg_match('/\b(EUR|USD|USDT|USDC|BTC|ETH|LTC)\b/i', $normalizedText, $currencyMatch)) {
|
||||
$currency = strtoupper((string) $currencyMatch[1]);
|
||||
@@ -362,7 +367,7 @@ final class OcrService
|
||||
$flags[] = 'currency_missing';
|
||||
}
|
||||
|
||||
if (preg_match('/DOGE\s*\/\s*(?:USD|EUR|USDT|USDC|BTC|ETH|LTC)[^\d]{0,20}(\d+[.,]\d{3,8})/i', $normalizedText, $priceMatch)) {
|
||||
if (preg_match('/' . $assetPattern . '\s*\/\s*(?:USD|EUR|USDT|USDC|BTC|ETH|LTC)[^\d]{0,20}(\d+[.,]\d{3,8})/i', $normalizedText, $priceMatch)) {
|
||||
$price = round((float) str_replace(',', '.', $priceMatch[1]), 8);
|
||||
}
|
||||
|
||||
@@ -380,7 +385,7 @@ final class OcrService
|
||||
}
|
||||
}
|
||||
|
||||
if ($coinsTotal === null && preg_match('/(\d+[.,]\d{4,8})\s*(?:DOGE)?\s*(?:MINING[- ]?GUTHABEN|MINING[- ]?BALANCE|GUTHABEN|BALANCE)/i', $normalizedText, $coinsMatch)) {
|
||||
if ($coinsTotal === null && preg_match('/(\d+[.,]\d{4,8})\s*(?:' . $assetPattern . ')?\s*(?:MINING[- ]?GUTHABEN|MINING[- ]?BALANCE|GUTHABEN|BALANCE)/i', $normalizedText, $coinsMatch)) {
|
||||
$coinsTotal = round((float) str_replace(',', '.', $coinsMatch[1]), 6);
|
||||
$flags[] = 'coins_from_balance_context';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user