Miner-Upgrade
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user