Miner-Upgrade
All checks were successful
Deploy / deploy-staging (push) Successful in 7s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-05-09 00:58:48 +02:00
parent ee5a46254f
commit fc95898a9d
11 changed files with 976 additions and 131 deletions

View File

@@ -253,6 +253,15 @@ final class Router
Http::json(['data' => $this->savePayout($projectKey, Http::input())], 201);
}
if ($resource === 'wallet-snapshots' && $method === 'GET') {
Http::json(['data' => $this->walletSnapshots($projectKey)]);
}
if ($resource === 'wallet-snapshots' && $method === 'POST') {
$this->repository()->ensureProject($projectKey);
Http::json(['data' => $this->saveWalletSnapshot($projectKey, Http::input())], 201);
}
if ($resource === 'miner-offers' && $method === 'GET') {
Http::json(['data' => $this->minerOffers($projectKey)]);
}
@@ -329,6 +338,10 @@ final class Router
'purchased_miners' => [],
'measurement_rates' => [],
], ['project_key' => $projectKey]);
$walletSnapshots = $this->safeTimed('bootstrap.wallet_snapshots', fn () => $this->bootstrapWalletSnapshots($projectKey, $view), [], [
'project_key' => $projectKey,
'view' => $view,
]);
$measurements = $this->safeTimed('bootstrap.measurements', fn () => $this->bootstrapMeasurements($projectKey, $settings, $view), [], [
'project_key' => $projectKey,
'view' => $view,
@@ -369,6 +382,7 @@ final class Router
return [
'project' => $this->repository()->getProject($projectKey),
'settings' => $settings,
'wallet_snapshots' => $walletSnapshots,
'measurements' => $measurements,
'targets' => $targets,
'dashboards' => $dashboards,
@@ -483,6 +497,7 @@ final class Router
'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), []),
'wallet_snapshots' => $this->safeRead(fn () => $this->repository()->listWalletSnapshots($projectKey, 500), []),
'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), []),
@@ -587,6 +602,23 @@ final class Router
]);
}
foreach ($backup['wallet_snapshots'] as $snapshot) {
$this->repository()->saveWalletSnapshot($projectKey, [
'measured_at' => $snapshot['measured_at'],
'total_value_amount' => $snapshot['total_value_amount'] ?? null,
'total_value_currency' => $snapshot['total_value_currency'] ?? null,
'wallet_balance' => $snapshot['wallet_balance'] ?? null,
'wallet_currency' => $snapshot['wallet_currency'] ?? ($backup['settings']['crypto_currency'] ?? 'DOGE'),
'balances_json' => $snapshot['balances_json'] ?? [],
'note' => $snapshot['note'] ?? null,
'source' => $snapshot['source'] ?? 'manual',
'image_path' => $snapshot['image_path'] ?? null,
'ocr_raw_text' => $snapshot['ocr_raw_text'] ?? null,
'ocr_confidence' => $snapshot['ocr_confidence'] ?? null,
'ocr_flags' => $snapshot['ocr_flags'] ?? [],
]);
}
$minerOfferIdMap = [];
foreach ($backup['miner_offers'] as $offer) {
$savedOffer = $this->repository()->saveMinerOffer($projectKey, [
@@ -694,6 +726,7 @@ final class Router
'purchased_miners' => count($backup['purchased_miners']),
'cost_plans' => count($backup['cost_plans']),
'payouts' => count($backup['payouts']),
'wallet_snapshots' => count($backup['wallet_snapshots']),
'targets' => count($backup['targets']),
'dashboards' => count($backup['dashboards']),
'miner_offers' => count($backup['miner_offers']),
@@ -1191,7 +1224,7 @@ final class Router
private function bootstrapMeasurements(string $projectKey, array $settings, string $view): array
{
if (in_array($view, ['settings', 'dashboards'], true)) {
if (in_array($view, ['settings', 'dashboards', 'wallet'], true)) {
return [];
}
@@ -1212,6 +1245,15 @@ final class Router
return $this->analytics()->enrichMeasurements($rows, $settings);
}
private function bootstrapWalletSnapshots(string $projectKey, string $view): array
{
if (!in_array($view, ['wallet'], true)) {
return [];
}
return $this->repository()->listWalletSnapshots($projectKey, 50);
}
private function bootstrapTargets(string $projectKey, string $view): array
{
return in_array($view, ['overview', 'mining'], true) ? $this->targets($projectKey) : [];
@@ -1285,6 +1327,7 @@ final class Router
? $this->requiredDateTime($input['measured_at'] ?? null, 'measured_at', $projectTimezone)
: $this->currentTimestamp(),
'coins_total' => $this->requiredDecimal($input['coins_total'] ?? null, 'coins_total'),
'coin_currency' => $this->measurementCoinCurrency($projectKey, $input),
'price_per_coin' => $this->optionalDecimal($input['price_per_coin'] ?? null),
'price_currency' => $this->optionalCurrency($input['price_currency'] ?? null),
'note' => $this->optionalString($input['note'] ?? null, 2000),
@@ -1330,7 +1373,7 @@ final class Router
}
try {
$payload = $this->parseImportLine($trimmed, $defaultCurrency, $defaultSource, $this->projectTimezone($projectKey));
$payload = $this->parseImportLine($projectKey, $trimmed, $defaultCurrency, $defaultSource, $this->projectTimezone($projectKey));
$this->syncCurrencyCatalogForMeasurement($payload);
$payload['fx_fetch_id'] = $this->resolveMeasurementFxFetchId($projectKey, $payload, false);
$result = $this->repository()->createMeasurementIfNotExists($projectKey, $payload);
@@ -1368,6 +1411,15 @@ final class Router
private function syncCurrencyCatalogForMeasurement(array $payload): void
{
$coinCurrency = strtoupper(trim((string) ($payload['coin_currency'] ?? '')));
if ($coinCurrency !== '' && $this->currencyCatalogEntry($coinCurrency) === null) {
throw new ApiException(
'Coin-Waehrung ist im fx-rates Katalog nicht vorhanden.',
422,
['currency' => $coinCurrency]
);
}
$priceCurrency = strtoupper(trim((string) ($payload['price_currency'] ?? '')));
if ($priceCurrency === '') {
return;
@@ -1382,7 +1434,7 @@ final class Router
}
}
private function parseImportLine(string $line, ?string $defaultCurrency, string $defaultSource, string $projectTimezone): array
private function parseImportLine(string $projectKey, string $line, ?string $defaultCurrency, string $defaultSource, string $projectTimezone): array
{
$parts = array_map('trim', explode('|', $line));
if (count($parts) < 2) {
@@ -1402,6 +1454,7 @@ final class Router
return [
'measured_at' => $measuredAt,
'coins_total' => $coinsTotal,
'coin_currency' => $this->measurementCoinCurrency($projectKey),
'price_per_coin' => $pricePerCoin,
'price_currency' => $priceCurrency,
'note' => $note,
@@ -1629,6 +1682,36 @@ final class Router
return $this->repository()->savePayout($projectKey, $payload);
}
private function walletSnapshots(string $projectKey): array
{
return $this->repository()->listWalletSnapshots($projectKey, 100);
}
private function saveWalletSnapshot(string $projectKey, array $input): array
{
$balances = $input['balances_json'] ?? [];
if (!is_array($balances)) {
$balances = [];
}
$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'),
'balances_json' => $balances,
'note' => $this->optionalString($input['note'] ?? null, 1000),
'source' => $this->enumValue($input['source'] ?? 'manual', ['manual', 'image_ocr', 'seed_import'], 'source'),
'image_path' => $this->optionalString($input['image_path'] ?? null, 255),
'ocr_raw_text' => $this->optionalString($input['ocr_raw_text'] ?? null, 20000),
'ocr_confidence' => $this->optionalDecimal($input['ocr_confidence'] ?? null),
'ocr_flags' => is_array($input['ocr_flags'] ?? null) ? $input['ocr_flags'] : [],
];
return $this->repository()->saveWalletSnapshot($projectKey, $payload);
}
private function saveMinerOffer(string $projectKey, array $input): array
{
$payload = [
@@ -1658,9 +1741,16 @@ final class Router
throw new ApiException('Miner-Angebot nicht gefunden.', 404);
}
$settings = $this->settings($projectKey);
$cryptoCurrency = $this->requiredCurrency($settings['crypto_currency'] ?? 'DOGE', 'crypto_currency');
$isAutoRenew = array_key_exists('auto_renew', $input) ? !empty($input['auto_renew']) : !empty($offer['auto_renew']);
$purchaseCurrency = $this->optionalCurrency($input['currency'] ?? null) ?? (string) ($offer['effective_price_currency'] ?? $offer['price_currency'] ?? $offer['base_price_currency'] ?? '');
if (!$isAutoRenew) {
$purchaseCurrency = $cryptoCurrency;
}
$purchaseCost = $this->optionalDecimal($input['total_cost_amount'] ?? null);
if ($purchaseCost === null) {
if ($purchaseCost === null || !$isAutoRenew) {
$purchaseCost = $this->resolveOfferPurchaseCost(array_merge($offer, [
'price_currency' => $purchaseCurrency !== '' ? $purchaseCurrency : ($offer['price_currency'] ?? ''),
]));
@@ -1684,7 +1774,7 @@ final class Router
'usd_reference_amount' => $offer['usd_reference_amount'] ?? null,
'reference_price_amount' => $referencePriceAmount,
'reference_price_currency' => $referencePriceCurrency,
'auto_renew' => array_key_exists('auto_renew', $input) ? (!empty($input['auto_renew']) ? 1 : 0) : (!empty($offer['auto_renew']) ? 1 : 0),
'auto_renew' => $isAutoRenew ? 1 : 0,
'note' => $this->optionalString($input['note'] ?? ($offer['note'] ?? null), 1000),
'is_active' => 1,
]);
@@ -2171,6 +2261,17 @@ final class Router
return null;
}
private function measurementCoinCurrency(string $projectKey, array $input = []): string
{
$requested = $this->optionalCurrency($input['coin_currency'] ?? null);
if ($requested !== null) {
return $requested;
}
$settings = $this->settings($projectKey);
return $this->requiredCurrency($settings['crypto_currency'] ?? 'DOGE', 'crypto_currency');
}
private function ensureMeasurementFxReferences(string $projectKey, array $rows, ?array $settings = null): array
{
$maxAgeHours = self::FX_FETCH_MAX_AGE_HOURS;

View File

@@ -34,23 +34,26 @@ final class AnalyticsService
$previousIntervalRate = null;
$result = [];
$payoutIndex = 0;
$cumulativePayouts = 0.0;
$lastPayoutTs = null;
$payoutsByAsset = [];
$latestPriceByCurrency = [];
foreach ($measurements as $row) {
$measuredTs = $this->utcTimestamp((string) $row['measured_at']);
$coinCurrency = strtoupper(trim((string) ($row['coin_currency'] ?? $settings['crypto_currency'] ?? 'DOGE')));
while (isset($payouts[$payoutIndex])) {
$payoutTs = $this->utcTimestamp((string) ($payouts[$payoutIndex]['payout_at'] ?? ''));
if ($payoutTs <= 0 || $payoutTs > $measuredTs) {
break;
}
$cumulativePayouts += (float) ($payouts[$payoutIndex]['coins_amount'] ?? 0);
$payoutAsset = strtoupper(trim((string) ($payouts[$payoutIndex]['payout_currency'] ?? $coinCurrency)));
$payoutsByAsset[$payoutAsset] = ($payoutsByAsset[$payoutAsset] ?? 0.0) + (float) ($payouts[$payoutIndex]['coins_amount'] ?? 0);
$lastPayoutTs = $payoutTs;
$payoutIndex++;
}
$cumulativePayouts = (float) ($payoutsByAsset[$coinCurrency] ?? 0.0);
$visibleCoinsTotal = (float) $row['coins_total'];
$effectiveCoinsTotal = $visibleCoinsTotal + $cumulativePayouts;
$growth = $effectiveCoinsTotal - $baselineCoins;
@@ -120,7 +123,7 @@ final class AnalyticsService
}
if ($price === null) {
foreach (['USD', 'EUR'] as $fallbackCurrency) {
$fxPrice = $this->convertAmount(1.0, 'DOGE', $fallbackCurrency, $row);
$fxPrice = $this->convertAmount(1.0, $coinCurrency, $fallbackCurrency, $row);
if ($fxPrice !== null && $fxPrice > 0) {
$latestPriceByCurrency[$fallbackCurrency] = $fxPrice;
}
@@ -159,6 +162,7 @@ final class AnalyticsService
$result[] = array_merge($row, [
'coins_total_visible' => $this->roundOrNull($visibleCoinsTotal, 6),
'coins_total_effective' => $this->roundOrNull($effectiveCoinsTotal, 6),
'coin_currency' => $coinCurrency,
'payouts_cumulative' => $this->roundOrNull($cumulativePayouts, 6),
'growth_since_baseline' => $this->roundOrNull($growth, 6),
'hours_since_baseline' => $this->roundOrNull($hoursSinceBaseline, 4),
@@ -296,26 +300,60 @@ final class AnalyticsService
}
$latestCurrency = (string) ($latest['effective_price_currency'] ?? $latest['price_currency'] ?? '');
$investedCapital = $latestCurrency !== ''
$latestMeasuredTs = $this->utcTimestamp((string) ($latest['measured_at'] ?? ''));
$cashInvestedCapital = $latestCurrency !== ''
? $this->totalInvestmentBasis(
is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [],
$purchasedMiners,
$this->utcTimestamp((string) ($latest['measured_at'] ?? '')),
$latestMeasuredTs,
$latestCurrency,
$latest
$latest,
'cash'
)
: null;
$reinvestedCapital = $latestCurrency !== ''
? $this->totalInvestmentBasis(
is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [],
$purchasedMiners,
$latestMeasuredTs,
$latestCurrency,
$latest,
'reinvest'
)
: null;
$walletBalances = $this->walletBalances($payouts, $purchasedMiners, $latestMeasuredTs);
$walletBalanceCurrentAsset = (float) ($walletBalances[strtoupper(trim((string) ($latest['coin_currency'] ?? $settings['crypto_currency'] ?? 'DOGE')))] ?? 0.0);
$holdingsCurrentAsset = $walletBalanceCurrentAsset + (float) ($latest['coins_total_visible'] ?? $latest['coins_total'] ?? 0);
$walletValue = $latestCurrency !== '' ? $this->walletBalanceValue($walletBalances, $latestCurrency, $latest) : null;
$currentVisibleValue = is_numeric($latest['current_value'] ?? null) ? (float) $latest['current_value'] : null;
$totalHoldingsValue = ($walletValue !== null || $currentVisibleValue !== null)
? (float) ($walletValue ?? 0.0) + (float) ($currentVisibleValue ?? 0.0)
: null;
$currentDailyRevenue = is_numeric($latest['theoretical_daily_revenue'] ?? null) ? (float) $latest['theoretical_daily_revenue'] : null;
$breakEvenRemainingAmount = $investedCapital;
$breakEvenDaysOverall = (
$investedCapital !== null &&
$currentDailyRevenue !== null &&
$currentDailyRevenue > 0
) ? ($investedCapital / $currentDailyRevenue) : null;
$breakEvenRemainingAmount = ($cashInvestedCapital !== null && $totalHoldingsValue !== null)
? max(0.0, $cashInvestedCapital - $totalHoldingsValue)
: $cashInvestedCapital;
$breakEvenProjection = (
$cashInvestedCapital !== null &&
$breakEvenRemainingAmount !== null
) ? $this->projectBreakEvenDate(
is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [],
$purchasedMiners,
$latest,
$breakEvenRemainingAmount
) : ['days' => null, 'eta' => null];
$breakEvenDaysOverall = is_numeric($breakEvenProjection['days'] ?? null) ? (float) $breakEvenProjection['days'] : null;
$latestSummary = array_merge($latest, [
'invested_capital' => $this->roundOrNull($investedCapital, 8),
'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),
'holdings_current_asset' => $this->roundOrNull($holdingsCurrentAsset, 6),
'wallet_value' => $this->roundOrNull($walletValue, 8),
'total_holdings_value' => $this->roundOrNull($totalHoldingsValue, 8),
'break_even_remaining_amount' => $this->roundOrNull($breakEvenRemainingAmount, 8),
'break_even_days_overall' => $this->roundOrNull($breakEvenDaysOverall, 4),
'break_even_eta_at' => $breakEvenProjection['eta'] ?? null,
]);
$currentProjection = $this->projectPerformance(
is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [],
@@ -349,6 +387,9 @@ final class AnalyticsService
'total_coins' => $this->roundOrNull(array_sum(array_map(static fn (array $payout): float => (float) ($payout['coins_amount'] ?? 0), $payouts)), 6),
'current_visible_coins' => $this->roundOrNull((float) ($latest['coins_total_visible'] ?? $latest['coins_total']), 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),
'holdings_current_asset' => $this->roundOrNull($holdingsCurrentAsset, 6),
],
'current_hashrate_mh' => $this->roundOrNull($currentHashrateMh, 4),
'miner_offers' => $offerSummary,
@@ -628,7 +669,7 @@ final class AnalyticsService
if (array_key_exists('is_active', $miner) && empty($miner['is_active'])) {
continue;
}
if (!$this->entryIsCovered($miner, $measurementTs > 0 ? $measurementTs : null, 'purchased_at')) {
if ($measurementTs > 0 && $this->utcTimestamp((string) ($miner['purchased_at'] ?? '')) > $measurementTs) {
continue;
}
@@ -650,7 +691,7 @@ final class AnalyticsService
return $matched ? $total : null;
}
private function totalInvestmentBasis(array $costPlans, array $purchasedMiners, int $measurementTs, string $targetCurrency, ?array $fxContext = null): ?float
private function totalInvestmentBasis(array $costPlans, array $purchasedMiners, int $measurementTs, string $targetCurrency, ?array $fxContext = null, ?string $fundingSource = null): ?float
{
$target = strtoupper(trim($targetCurrency));
if ($target === '') {
@@ -661,26 +702,16 @@ final class AnalyticsService
$matched = false;
$purchasedTotal = $this->totalPurchasedMinerCost($purchasedMiners, $target, $measurementTs, $fxContext);
if ($purchasedTotal !== null) {
if ($fundingSource === null && $purchasedTotal !== null) {
$matched = true;
$total += $purchasedTotal;
}
foreach ($costPlans as $plan) {
if (empty($plan['is_active'])) {
if ($measurementTs > 0 && $this->utcTimestamp((string) ($plan['starts_at'] ?? '')) > $measurementTs) {
continue;
}
$startTs = $this->utcTimestamp((string) ($plan['starts_at'] ?? ''));
$runtimeMonths = (int) ($plan['runtime_months'] ?? 0);
if ($startTs <= 0 || $runtimeMonths <= 0 || ($measurementTs > 0 && $measurementTs < $startTs)) {
continue;
}
$runtimeDays = $runtimeMonths * 30.4375;
$endTs = (int) round($startTs + ($runtimeDays * 86400));
$isCovered = !empty($plan['auto_renew']) || $measurementTs <= 0 || $measurementTs <= $endTs;
if (!$isCovered) {
if ($fundingSource !== null && $this->entryFundingSource($plan) !== $fundingSource) {
continue;
}
@@ -699,6 +730,31 @@ final class AnalyticsService
$total += $converted;
}
if ($fundingSource !== null) {
foreach ($purchasedMiners as $miner) {
if ($measurementTs > 0 && $this->utcTimestamp((string) ($miner['purchased_at'] ?? '')) > $measurementTs) {
continue;
}
if ($this->entryFundingSource($miner) !== $fundingSource) {
continue;
}
$amount = $this->investmentBasisAmount($miner);
$currency = strtoupper(trim((string) ($miner['reference_price_currency'] ?? $miner['currency'] ?? '')));
if ($amount === null || $amount <= 0 || $currency === '') {
continue;
}
$converted = $this->convertAmount($amount, $currency, $target, $fxContext);
if ($converted === null) {
continue;
}
$matched = true;
$total += $converted;
}
}
return $matched ? $total : null;
}
@@ -1014,6 +1070,153 @@ final class AnalyticsService
return $endIndex - $startIndex + 1;
}
private function walletBalances(array $payouts, array $purchasedMiners, int $measurementTs): array
{
$balances = [];
foreach ($payouts as $payout) {
$payoutTs = $this->utcTimestamp((string) ($payout['payout_at'] ?? ''));
if ($measurementTs > 0 && $payoutTs > $measurementTs) {
continue;
}
$currency = strtoupper(trim((string) ($payout['payout_currency'] ?? '')));
$amount = is_numeric($payout['coins_amount'] ?? null) ? (float) $payout['coins_amount'] : null;
if ($currency === '' || $amount === null) {
continue;
}
$balances[$currency] = ($balances[$currency] ?? 0.0) + $amount;
}
foreach ($purchasedMiners as $miner) {
$purchaseTs = $this->utcTimestamp((string) ($miner['purchased_at'] ?? ''));
if ($measurementTs > 0 && $purchaseTs > $measurementTs) {
continue;
}
if ($this->entryFundingSource($miner) !== 'reinvest') {
continue;
}
$currency = strtoupper(trim((string) ($miner['currency'] ?? '')));
$amount = is_numeric($miner['total_cost_amount'] ?? null) ? (float) $miner['total_cost_amount'] : null;
if ($currency === '' || $amount === null) {
continue;
}
$balances[$currency] = ($balances[$currency] ?? 0.0) - $amount;
}
ksort($balances);
return array_map(fn (float $value): float => round($value, 8), $balances);
}
private function walletBalanceValue(array $balances, string $targetCurrency, ?array $fxContext = null): ?float
{
$target = strtoupper(trim($targetCurrency));
if ($target === '' || $balances === []) {
return null;
}
$total = 0.0;
$matched = false;
foreach ($balances as $currency => $amount) {
if (!is_numeric($amount)) {
continue;
}
$numericAmount = (float) $amount;
if (strtoupper((string) $currency) === $target) {
$matched = true;
$total += $numericAmount;
continue;
}
$converted = $this->convertAmount($numericAmount, (string) $currency, $target, $fxContext);
if ($converted === null) {
continue;
}
$matched = true;
$total += $converted;
}
return $matched ? $total : null;
}
private function entryFundingSource(array $entry): string
{
return !empty($entry['auto_renew']) ? 'cash' : 'reinvest';
}
private function investmentBasisAmount(array $entry): ?float
{
if (is_numeric($entry['reference_price_amount'] ?? null)) {
return (float) $entry['reference_price_amount'];
}
if (is_numeric($entry['base_price_amount'] ?? null)) {
return (float) $entry['base_price_amount'];
}
if (is_numeric($entry['total_cost_amount'] ?? null)) {
return (float) $entry['total_cost_amount'];
}
return null;
}
private function projectBreakEvenDate(array $costPlans, array $purchasedMiners, array $latest, float $remainingAmount): array
{
if ($remainingAmount <= 0) {
return ['days' => 0.0, 'eta' => (string) ($latest['measured_at'] ?? null)];
}
$currency = strtoupper(trim((string) ($latest['effective_price_currency'] ?? $latest['price_currency'] ?? '')));
$pricePerCoin = is_numeric($latest['effective_price_per_coin'] ?? null) ? (float) $latest['effective_price_per_coin'] : null;
$dogePerDay = is_numeric($latest['doge_per_day_interval'] ?? null) ? (float) $latest['doge_per_day_interval'] : null;
$currentHashrateMh = $this->totalHashrateMh(array_merge($costPlans, $purchasedMiners));
$baseTs = $this->utcTimestamp((string) ($latest['measured_at'] ?? ''));
if ($currency === '' || $pricePerCoin === null || $dogePerDay === null || $currentHashrateMh <= 0 || $baseTs <= 0) {
return ['days' => null, 'eta' => null];
}
$dogePerDayPerMh = $dogePerDay / $currentHashrateMh;
$cumulativeRevenue = 0.0;
$maxDays = 3650;
for ($day = 0; $day <= $maxDays; $day++) {
$dayHashrate = 0.0;
$dayTs = $baseTs + ($day * 86400);
foreach ($costPlans as $plan) {
if (!empty($plan['is_active']) && $this->entryIsCovered($plan, $dayTs)) {
$dayHashrate += $this->normalizeHashrateMh($plan['mining_speed_value'] ?? null, $plan['mining_speed_unit'] ?? null);
$dayHashrate += $this->normalizeHashrateMh($plan['bonus_speed_value'] ?? null, $plan['bonus_speed_unit'] ?? null);
}
}
foreach ($purchasedMiners as $miner) {
if ((array_key_exists('is_active', $miner) && empty($miner['is_active'])) || !$this->entryIsCovered($miner, $dayTs, 'purchased_at')) {
continue;
}
$dayHashrate += $this->normalizeHashrateMh($miner['mining_speed_value'] ?? null, $miner['mining_speed_unit'] ?? null);
$dayHashrate += $this->normalizeHashrateMh($miner['bonus_speed_value'] ?? null, $miner['bonus_speed_unit'] ?? null);
}
if ($dayHashrate <= 0) {
continue;
}
$dayRevenue = $dayHashrate * $dogePerDayPerMh * $pricePerCoin;
$cumulativeRevenue += $dayRevenue;
if ($cumulativeRevenue >= $remainingAmount) {
$etaTs = (int) round($baseTs + ($day * 86400));
return [
'days' => (float) $day,
'eta' => $this->formatUtcTimestamp($etaTs),
];
}
}
return ['days' => null, 'eta' => null];
}
private function utcTimestamp(?string $value): int
{
$normalized = trim((string) $value);

View File

@@ -46,7 +46,11 @@ final class OcrService
$flags[] = 'ocr_hint_text_used';
}
$parsed = $this->parseText($rawText, (string) ($input['date_context'] ?? date('Y-m-d')));
$parsed = $this->parseText(
$rawText,
(string) ($input['date_context'] ?? date('Y-m-d')),
strtoupper(trim((string) ($input['wallet_currency_hint'] ?? '')))
);
$parsed['image_path'] = $targetFile;
$parsed['raw_text'] = $rawText;
$parsed['flags'] = array_values(array_unique(array_merge($flags, $parsed['flags'])));
@@ -291,7 +295,27 @@ final class OcrService
return $binary !== '' && trim((string) shell_exec('command -v ' . escapeshellarg($binary) . ' 2>/dev/null')) !== '';
}
private function parseText(string $rawText, string $dateContext): array
private function parseText(string $rawText, string $dateContext, string $walletCurrencyHint = ''): array
{
$measurement = $this->parseMeasurementText($rawText, $dateContext);
$wallet = $this->parseWalletText($rawText, $dateContext, $walletCurrencyHint);
$isWallet = ($wallet['score'] ?? 0) > ($measurement['score'] ?? 0)
&& (
($wallet['suggested_wallet']['wallet_balance'] ?? null) !== null
|| ($wallet['suggested_wallet']['total_value_amount'] ?? null) !== null
);
return [
'kind' => $isWallet ? 'wallet' : 'measurement',
'suggested' => $measurement['suggested'],
'suggested_wallet' => $wallet['suggested_wallet'],
'confidence' => round((float) ($isWallet ? ($wallet['confidence'] ?? 0.0) : ($measurement['confidence'] ?? 0.0)), 4),
'flags' => $isWallet ? $wallet['flags'] : $measurement['flags'],
];
}
private function parseMeasurementText(string $rawText, string $dateContext): array
{
$flags = [];
$suggestedTime = null;
@@ -433,6 +457,107 @@ final class OcrService
],
'confidence' => round($confidence, 4),
'flags' => $flags,
'score' => $matchedFields,
];
}
private function parseWalletText(string $rawText, string $dateContext, string $walletCurrencyHint = ''): array
{
$flags = [];
$suggestedTime = null;
$totalValueAmount = null;
$totalValueCurrency = null;
$walletBalance = null;
$walletCurrency = $walletCurrencyHint !== '' ? $walletCurrencyHint : 'DOGE';
$balances = [];
$normalizedText = preg_replace('/[[:space:]]+/u', ' ', trim($rawText)) ?: '';
$lines = array_values(array_filter(array_map(
static fn (string $line): string => trim($line),
preg_split('/\R/u', $rawText) ?: []
), static fn (string $line): bool => $line !== ''));
if (preg_match('/\b([01]?\d|2[0-3]):([0-5]\d)\b/', $normalizedText, $timeMatch)) {
$suggestedTime = sprintf('%s %02d:%02d:00', $dateContext, (int) $timeMatch[1], (int) $timeMatch[2]);
}
if (
preg_match('/GESAMTSALDO[^\d]{0,24}(\d+(?:[.,]\d+)?)\s*(USD|EUR|USDT|USDC|BTC|ETH|DOGE|CTC|HSH)/i', $normalizedText, $totalMatch)
|| preg_match('/(\d+(?:[.,]\d+)?)\s*(USD|EUR)\b.*GESAMTSALDO/i', $normalizedText, $totalMatch)
) {
$totalValueAmount = round((float) str_replace(',', '.', $totalMatch[1]), 8);
$totalValueCurrency = strtoupper((string) $totalMatch[2]);
} else {
$flags[] = 'wallet_total_missing';
}
preg_match_all('/(\d+(?:[.,]\d+)?)\s*([A-Z]{2,10})\b/u', $normalizedText, $balanceMatches, PREG_SET_ORDER);
foreach ($balanceMatches as $match) {
$amount = round((float) str_replace(',', '.', $match[1]), 10);
$currency = strtoupper((string) $match[2]);
if ($amount <= 0 || $currency === '') {
continue;
}
if (!isset($balances[$currency]) || $amount > (float) $balances[$currency]) {
$balances[$currency] = $amount;
}
}
if ($walletCurrencyHint !== '' && array_key_exists($walletCurrencyHint, $balances)) {
$walletCurrency = $walletCurrencyHint;
$walletBalance = (float) $balances[$walletCurrencyHint];
} elseif ($balances !== []) {
foreach (['DOGE', 'BTC', 'ETH', 'CTC', 'HSH', 'LTC', 'USDT', 'USDC'] as $preferredCurrency) {
if (array_key_exists($preferredCurrency, $balances)) {
$walletCurrency = $preferredCurrency;
$walletBalance = (float) $balances[$preferredCurrency];
break;
}
}
if ($walletBalance === null) {
$firstCurrency = array_key_first($balances);
if (is_string($firstCurrency)) {
$walletCurrency = $firstCurrency;
$walletBalance = (float) $balances[$firstCurrency];
}
}
} else {
$flags[] = 'wallet_balance_missing';
}
$walletIndicators = 0;
$normalizedLower = strtolower($normalizedText);
foreach (['wallets', 'gesamtsaldo', 'alle münzen', 'alle munzen', 'letzte transaktion'] as $indicator) {
if (str_contains($normalizedLower, $indicator)) {
$walletIndicators++;
}
}
$matchedFields = 0;
foreach ([$totalValueAmount, $walletBalance, $walletCurrency] as $field) {
if ($field !== null && $field !== '') {
$matchedFields++;
}
}
$score = $matchedFields + ($walletIndicators * 2);
$confidence = max(0.05, min(0.99, ($matchedFields / 3) + (min(3, $walletIndicators) * 0.12) - (count($flags) * 0.03)));
ksort($balances);
return [
'suggested_wallet' => [
'measured_at' => $suggestedTime,
'total_value_amount' => $totalValueAmount,
'total_value_currency' => $totalValueCurrency,
'wallet_balance' => $walletBalance,
'wallet_currency' => $walletCurrency,
'balances_json' => $balances,
'note' => null,
'source' => 'image_ocr',
],
'confidence' => round($confidence, 4),
'flags' => array_values(array_unique($flags)),
'score' => $score,
];
}
}

View File

@@ -229,6 +229,7 @@ final class MiningRepository
'owner_sub' => $this->ownerSub,
'measured_at' => $payload['measured_at'],
'coins_total' => $payload['coins_total'],
'coin_currency' => $payload['coin_currency'] ?? 'DOGE',
'price_per_coin' => $payload['price_per_coin'],
'price_currency' => $payload['price_currency'],
'fx_fetch_id' => $payload['fx_fetch_id'] ?? null,
@@ -243,10 +244,10 @@ final class MiningRepository
if ($this->driver === 'pgsql') {
$stmt = $this->pdo->prepare(
'INSERT INTO ' . $this->table('measurements') . ' (
project_key, owner_sub, measured_at, coins_total, price_per_coin, price_currency, fx_fetch_id, note,
project_key, owner_sub, measured_at, coins_total, coin_currency, price_per_coin, price_currency, fx_fetch_id, note,
source, image_path, ocr_raw_text, ocr_confidence, ocr_flags
) VALUES (
:project_key, :owner_sub, :measured_at, :coins_total, :price_per_coin, :price_currency, :fx_fetch_id, :note,
:project_key, :owner_sub, :measured_at, :coins_total, :coin_currency, :price_per_coin, :price_currency, :fx_fetch_id, :note,
:source, :image_path, :ocr_raw_text, :ocr_confidence, CAST(:ocr_flags AS jsonb)
)
RETURNING *'
@@ -259,10 +260,10 @@ final class MiningRepository
$stmt = $this->pdo->prepare(
'INSERT INTO ' . $this->table('measurements') . ' (
project_key, owner_sub, measured_at, coins_total, price_per_coin, price_currency, fx_fetch_id, note,
project_key, owner_sub, measured_at, coins_total, coin_currency, price_per_coin, price_currency, fx_fetch_id, note,
source, image_path, ocr_raw_text, ocr_confidence, ocr_flags
) VALUES (
:project_key, :owner_sub, :measured_at, :coins_total, :price_per_coin, :price_currency, :fx_fetch_id, :note,
:project_key, :owner_sub, :measured_at, :coins_total, :coin_currency, :price_per_coin, :price_currency, :fx_fetch_id, :note,
:source, :image_path, :ocr_raw_text, :ocr_confidence, :ocr_flags
)'
);
@@ -441,6 +442,71 @@ final class MiningRepository
return $this->normalizeRow($fetch->fetch() ?: []);
}
public function listWalletSnapshots(string $projectKey, int $limit = 100): array
{
$stmt = $this->pdo->prepare(
'SELECT * FROM ' . $this->table('wallet_snapshots') . '
WHERE project_key = :project_key AND owner_sub = :owner_sub
ORDER BY measured_at DESC, id DESC
LIMIT :limit'
);
$stmt->bindValue(':project_key', $projectKey, PDO::PARAM_STR);
$stmt->bindValue(':owner_sub', $this->ownerSub, PDO::PARAM_STR);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $this->normalizeRows($stmt->fetchAll() ?: []);
}
public function saveWalletSnapshot(string $projectKey, array $payload): array
{
$params = [
'project_key' => $projectKey,
'owner_sub' => $this->ownerSub,
'measured_at' => $payload['measured_at'],
'total_value_amount' => $payload['total_value_amount'] ?? null,
'total_value_currency' => $payload['total_value_currency'] ?? null,
'wallet_balance' => $payload['wallet_balance'] ?? null,
'wallet_currency' => $payload['wallet_currency'],
'balances_json' => json_encode($payload['balances_json'] ?? [], JSON_UNESCAPED_UNICODE),
'note' => $payload['note'] ?? null,
'source' => $payload['source'] ?? 'manual',
'image_path' => $payload['image_path'] ?? null,
'ocr_raw_text' => $payload['ocr_raw_text'] ?? null,
'ocr_confidence' => $payload['ocr_confidence'] ?? null,
'ocr_flags' => json_encode($payload['ocr_flags'] ?? [], JSON_UNESCAPED_UNICODE),
];
if ($this->driver === 'pgsql') {
$stmt = $this->pdo->prepare(
'INSERT INTO ' . $this->table('wallet_snapshots') . ' (
project_key, owner_sub, measured_at, total_value_amount, total_value_currency, wallet_balance,
wallet_currency, balances_json, note, source, image_path, ocr_raw_text, ocr_confidence, ocr_flags
) VALUES (
:project_key, :owner_sub, :measured_at, :total_value_amount, :total_value_currency, :wallet_balance,
:wallet_currency, CAST(:balances_json AS jsonb), :note, :source, :image_path, :ocr_raw_text, :ocr_confidence, CAST(:ocr_flags AS jsonb)
)
RETURNING *'
);
$stmt->execute($params);
return $this->normalizeRow($stmt->fetch() ?: []);
}
$stmt = $this->pdo->prepare(
'INSERT INTO ' . $this->table('wallet_snapshots') . ' (
project_key, owner_sub, measured_at, total_value_amount, total_value_currency, wallet_balance,
wallet_currency, balances_json, note, source, image_path, ocr_raw_text, ocr_confidence, ocr_flags
) VALUES (
:project_key, :owner_sub, :measured_at, :total_value_amount, :total_value_currency, :wallet_balance,
:wallet_currency, :balances_json, :note, :source, :image_path, :ocr_raw_text, :ocr_confidence, :ocr_flags
)'
);
$stmt->execute($params);
$id = (int) $this->pdo->lastInsertId();
$fetch = $this->pdo->prepare('SELECT * FROM ' . $this->table('wallet_snapshots') . ' WHERE id = :id LIMIT 1');
$fetch->execute(['id' => $id]);
return $this->normalizeRow($fetch->fetch() ?: []);
}
public function listTargets(string $projectKey): array
{
$stmt = $this->pdo->prepare(
@@ -1195,6 +1261,7 @@ final class MiningRepository
'fx_rates' => $this->prefix . 'fx_rates',
'measurement_rates' => $this->prefix . 'measurement_rates',
'payouts' => $this->prefix . 'payouts',
'wallet_snapshots' => $this->prefix . 'wallet_snapshots',
'miner_offers' => $this->prefix . 'miner_offers',
'purchased_miners' => $this->prefix . 'purchased_miners',
default => throw new \RuntimeException('Unknown mining table: ' . $logicalName),
@@ -1273,7 +1340,7 @@ final class MiningRepository
private function normalizeRow(array $row): array
{
foreach (['ocr_flags', 'filters_json', 'preferred_currencies'] as $jsonField) {
foreach (['ocr_flags', 'filters_json', 'preferred_currencies', 'balances_json'] as $jsonField) {
if (array_key_exists($jsonField, $row) && is_string($row[$jsonField]) && trim($row[$jsonField]) !== '') {
$decoded = json_decode($row[$jsonField], true);
if (json_last_error() === JSON_ERROR_NONE) {

View File

@@ -188,6 +188,10 @@ final class SchemaManager
$this->ensureMeasurementFxReferenceColumn();
$applied[] = 'measurement_fx_reference';
}
if ($this->tableExists($this->prefix . 'measurements') && !$this->columnExists($this->prefix . 'measurements', 'coin_currency')) {
$this->ensureMeasurementCoinCurrencyColumn();
$applied[] = 'measurement_coin_currency';
}
if (!$this->tableExists($this->prefix . 'measurement_rates')) {
$this->ensureMeasurementRatesTable();
@@ -197,6 +201,10 @@ final class SchemaManager
$this->ensurePayoutsTable();
$applied[] = 'payouts_table';
}
if (!$this->tableExists($this->prefix . 'wallet_snapshots')) {
$this->ensureWalletSnapshotsTable();
$applied[] = 'wallet_snapshots_table';
}
if (!$this->tableExists($this->prefix . 'fx_fetches') || !$this->tableExists($this->prefix . 'fx_rates')) {
$this->ensureFxRatesTable();
$applied[] = 'fx_rates_table';
@@ -293,6 +301,10 @@ final class SchemaManager
$this->ensureMeasurementFxReferenceColumn();
$applied[] = 'measurement_fx_reference';
}
if ($this->tableExists($this->prefix . 'measurements') && !$this->columnExists($this->prefix . 'measurements', 'coin_currency')) {
$this->ensureMeasurementCoinCurrencyColumn();
$applied[] = 'measurement_coin_currency';
}
if (!$this->tableExists($this->prefix . 'fx_fetches') || !$this->tableExists($this->prefix . 'fx_rates')) {
$this->ensureFxRatesTable();
@@ -308,6 +320,10 @@ final class SchemaManager
$this->ensurePayoutsTable();
$applied[] = 'payouts_table';
}
if (!$this->tableExists($this->prefix . 'wallet_snapshots')) {
$this->ensureWalletSnapshotsTable();
$applied[] = 'wallet_snapshots_table';
}
if (!$this->tableExists($this->prefix . 'miner_offers')) {
$this->upgradeMinerOffersTable();
@@ -421,6 +437,54 @@ final class SchemaManager
}
}
private function ensureMeasurementCoinCurrencyColumn(): void
{
$table = $this->prefix . 'measurements';
$settingsTable = $this->prefix . 'settings';
if (!$this->tableExists($table)) {
return;
}
$statements = $this->driver === 'pgsql'
? [
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS coin_currency VARCHAR(10)',
'UPDATE ' . $table . ' AS m
SET coin_currency = COALESCE(NULLIF(BTRIM(s.crypto_currency), \'\'), \'DOGE\')
FROM ' . $settingsTable . ' AS s
WHERE s.project_key = m.project_key
AND (m.coin_currency IS NULL OR BTRIM(m.coin_currency) = \'\')',
'UPDATE ' . $table . ' SET coin_currency = \'DOGE\' WHERE coin_currency IS NULL OR BTRIM(coin_currency) = \'\'',
'ALTER TABLE ' . $table . ' ALTER COLUMN coin_currency SET DEFAULT \'DOGE\'',
'ALTER TABLE ' . $table . ' ALTER COLUMN coin_currency SET NOT NULL',
]
: [
'ALTER TABLE `' . $table . '` ADD COLUMN coin_currency VARCHAR(10) NOT NULL DEFAULT \'DOGE\' AFTER coins_total',
'UPDATE `' . $table . '` m
LEFT JOIN `' . $settingsTable . '` s ON s.project_key = m.project_key
SET m.coin_currency = COALESCE(NULLIF(TRIM(s.crypto_currency), \'\'), \'DOGE\')
WHERE m.coin_currency IS NULL OR TRIM(m.coin_currency) = \'\'',
];
foreach ($statements as $index => $statement) {
try {
if ($this->driver === 'mysql' && $index === 0 && $this->columnExists($table, 'coin_currency')) {
continue;
}
$this->executeUpgradeStatements([$statement], 'Messpunkt-Coin-Waehrung konnte nicht angelegt werden.');
} catch (\Throwable $exception) {
$message = strtolower($exception->getMessage());
if (
($this->driver === 'mysql' && $index === 0 && str_contains($message, 'duplicate column')) ||
($this->driver === 'pgsql' && str_contains($message, 'already exists'))
) {
continue;
}
throw $exception;
}
}
}
private function ensureLegacyMinerOfferImportColumns(): void
{
$table = $this->prefix . 'miner_offers';
@@ -599,6 +663,9 @@ final class SchemaManager
if ($this->tableExists($this->prefix . 'measurements') && !$this->columnExists($this->prefix . 'measurements', 'fx_fetch_id')) {
$upgrades[] = 'measurement_fx_reference';
}
if ($this->tableExists($this->prefix . 'measurements') && !$this->columnExists($this->prefix . 'measurements', 'coin_currency')) {
$upgrades[] = 'measurement_coin_currency';
}
if (!$this->tableExists($this->prefix . 'fx_fetches') || !$this->tableExists($this->prefix . 'fx_rates')) {
$upgrades[] = 'fx_rates_table';
@@ -609,6 +676,9 @@ final class SchemaManager
if (!$this->tableExists($this->prefix . 'payouts')) {
$upgrades[] = 'payouts_table';
}
if (!$this->tableExists($this->prefix . 'wallet_snapshots')) {
$upgrades[] = 'wallet_snapshots_table';
}
if (!$this->tableExists($this->prefix . 'miner_offers')) {
$upgrades[] = 'miner_offers_table';
}
@@ -849,6 +919,65 @@ final class SchemaManager
$this->executeUpgradeStatements($statements, 'Schema-Upgrade fuer Auszahlungen fehlgeschlagen.');
}
public function ensureWalletSnapshotsTable(): void
{
if ($this->tableExists($this->prefix . 'wallet_snapshots')) {
return;
}
$table = $this->prefix . 'wallet_snapshots';
$projectTable = $this->prefix . 'projects';
$statements = $this->driver === 'pgsql'
? [
'CREATE TABLE IF NOT EXISTS ' . $table . ' (
id BIGSERIAL PRIMARY KEY,
project_key VARCHAR(64) NOT NULL,
owner_sub VARCHAR(128) NOT NULL,
measured_at TIMESTAMP NOT NULL,
total_value_amount NUMERIC(20,8),
total_value_currency VARCHAR(10),
wallet_balance NUMERIC(28,10),
wallet_currency VARCHAR(10) NOT NULL,
balances_json JSONB,
note TEXT,
source VARCHAR(16) NOT NULL,
image_path VARCHAR(255),
ocr_raw_text TEXT,
ocr_confidence NUMERIC(6,4),
ocr_flags JSONB,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_mining_wallet_snapshots_project FOREIGN KEY (project_key) REFERENCES ' . $projectTable . '(project_key) ON DELETE CASCADE
)',
'CREATE INDEX IF NOT EXISTS idx_miningcheck_wallet_snapshots_project_measured_at ON ' . $table . ' (project_key, owner_sub, measured_at)',
]
: [
'CREATE TABLE IF NOT EXISTS `' . $table . '` (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
project_key VARCHAR(64) NOT NULL,
owner_sub VARCHAR(128) NOT NULL,
measured_at TIMESTAMP NOT NULL,
total_value_amount DECIMAL(20,8) NULL,
total_value_currency VARCHAR(10) NULL,
wallet_balance DECIMAL(28,10) NULL,
wallet_currency VARCHAR(10) NOT NULL,
balances_json JSON NULL,
note TEXT NULL,
source ENUM(\'manual\', \'image_ocr\', \'seed_import\') NOT NULL,
image_path VARCHAR(255) NULL,
ocr_raw_text MEDIUMTEXT NULL,
ocr_confidence DECIMAL(6,4) NULL,
ocr_flags JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_mining_wallet_snapshots_project FOREIGN KEY (project_key) REFERENCES `' . $projectTable . '`(project_key) ON DELETE CASCADE,
KEY idx_miningcheck_wallet_snapshots_project_measured_at (project_key, owner_sub, measured_at)
)',
];
$this->executeUpgradeStatements($statements, 'Schema-Upgrade fuer Wallet-Snapshots fehlgeschlagen.');
}
public function ensureMinerTables(): void
{
if (!$this->tableExists($this->prefix . 'miner_offers')) {
@@ -1321,6 +1450,7 @@ final class SchemaManager
$this->prefix . 'fx_rates',
$this->prefix . 'measurement_rates',
$this->prefix . 'payouts',
$this->prefix . 'wallet_snapshots',
$this->prefix . 'miner_offers',
$this->prefix . 'purchased_miners',
];
@@ -1340,6 +1470,7 @@ final class SchemaManager
$this->prefix . 'measurements',
$this->prefix . 'measurement_rates',
$this->prefix . 'payouts',
$this->prefix . 'wallet_snapshots',
$this->prefix . 'miner_offers',
$this->prefix . 'targets',
$this->prefix . 'dashboard_definitions',