This commit is contained in:
2026-06-03 01:27:00 +02:00
parent 99cf5eaef2
commit 7783f65b26
3 changed files with 303 additions and 59 deletions

View File

@@ -24,6 +24,10 @@ final class Router
private const LONG_REQUEST_BUDGET_SECONDS = 8.0;
private const OVERVIEW_WINDOW_DAYS = 15;
private const FX_FETCH_MAX_AGE_HOURS = 3.0;
private const BASE_OFFER_SPEEDS = [
'fiat' => ['value' => 50.0, 'unit' => 'kH/s'],
'crypto' => ['value' => 75.0, 'unit' => 'kH/s'],
];
private string $moduleBasePath;
private ModuleConfig $config;
@@ -1679,14 +1683,18 @@ final class Router
private function saveCostPlan(string $projectKey, array $input): array
{
$projectTimezone = $this->projectTimezone($projectKey);
$miningSpeedValue = $this->optionalDecimal($input['mining_speed_value'] ?? null);
$miningSpeedUnit = $this->optionalSpeedUnit($input['mining_speed_unit'] ?? null);
$bonusPercent = $this->optionalDecimal($input['bonus_percent'] ?? null);
$payload = [
'label' => $this->requiredString($input['label'] ?? null, 'label', 120),
'purchased_at' => $this->requiredDateTime($input['starts_at'] ?? null, 'starts_at', $projectTimezone),
'runtime_months' => $this->requiredPositiveInt($input['runtime_months'] ?? null, 'runtime_months'),
'mining_speed_value' => $this->optionalDecimal($input['mining_speed_value'] ?? null),
'mining_speed_unit' => $this->optionalSpeedUnit($input['mining_speed_unit'] ?? null),
'bonus_speed_value' => $this->optionalDecimal($input['bonus_speed_value'] ?? null),
'bonus_speed_unit' => $this->optionalSpeedUnit($input['bonus_speed_unit'] ?? null),
'mining_speed_value' => $miningSpeedValue,
'mining_speed_unit' => $miningSpeedUnit,
'bonus_speed_value' => $this->bonusSpeedFromPercent($miningSpeedValue, $miningSpeedUnit, $bonusPercent),
'bonus_speed_unit' => $bonusPercent !== null ? $miningSpeedUnit : null,
'bonus_percent' => $bonusPercent,
'auto_renew' => !empty($input['auto_renew']) ? 1 : 0,
'base_price_amount' => $this->requiredDecimal($input['base_price_amount'] ?? ($input['total_cost_amount'] ?? null), 'base_price_amount'),
'payment_type' => $this->enumValue($input['payment_type'] ?? 'fiat', ['fiat', 'crypto'], 'payment_type'),
@@ -1698,8 +1706,11 @@ final class Router
throw new ApiException('Mining-Geschwindigkeit und Einheit muessen gemeinsam gesetzt oder beide leer sein.', 422);
}
if (($payload['bonus_speed_value'] === null) xor ($payload['bonus_speed_unit'] === null)) {
throw new ApiException('Bonus-Geschwindigkeit und Einheit muessen gemeinsam gesetzt oder beide leer sein.', 422);
if ($bonusPercent !== null && ($miningSpeedValue === null || $miningSpeedUnit === null)) {
throw new ApiException('Bonus-Prozent setzt eine Mining-Geschwindigkeit voraus.', 422);
}
if ($bonusPercent !== null && $bonusPercent < 0) {
throw new ApiException('Bonus-Prozent darf nicht negativ sein.', 422);
}
$settings = $this->settings($projectKey);
@@ -1787,22 +1798,31 @@ final class Router
private function saveMinerOffer(string $projectKey, array $input): array
{
$paymentType = $this->enumValue($input['payment_type'] ?? 'fiat', ['fiat', 'crypto'], 'payment_type');
$baseSpeed = self::BASE_OFFER_SPEEDS[$paymentType] ?? self::BASE_OFFER_SPEEDS['fiat'];
$bonusPercent = $this->optionalDecimal($input['bonus_percent'] ?? null);
$basePriceCurrency = $this->requiredCurrency($input['base_price_currency'] ?? ($input['reference_price_currency'] ?? $input['price_currency'] ?? null), 'base_price_currency');
$payload = [
'label' => $this->requiredString($input['label'] ?? null, 'label', 120),
'runtime_months' => $this->optionalPositiveInt($input['runtime_months'] ?? null),
'mining_speed_value' => $this->optionalDecimal($input['mining_speed_value'] ?? null),
'mining_speed_unit' => $this->optionalSpeedUnit($input['mining_speed_unit'] ?? null),
'bonus_speed_value' => $this->optionalDecimal($input['bonus_speed_value'] ?? null),
'bonus_speed_unit' => $this->optionalSpeedUnit($input['bonus_speed_unit'] ?? null),
'mining_speed_value' => $baseSpeed['value'],
'mining_speed_unit' => $baseSpeed['unit'],
'bonus_speed_value' => $this->bonusSpeedFromPercent((float) $baseSpeed['value'], (string) $baseSpeed['unit'], $bonusPercent),
'bonus_speed_unit' => $bonusPercent !== null ? (string) $baseSpeed['unit'] : null,
'bonus_percent' => $bonusPercent,
'base_price_amount' => $this->requiredDecimal($input['base_price_amount'] ?? ($input['reference_price_amount'] ?? $input['price_amount'] ?? null), 'base_price_amount'),
'base_price_currency' => $this->requiredCurrency($input['base_price_currency'] ?? ($input['reference_price_currency'] ?? $input['price_currency'] ?? null), 'base_price_currency'),
'payment_type' => $this->enumValue($input['payment_type'] ?? 'fiat', ['fiat', 'crypto'], 'payment_type'),
'base_price_currency' => $basePriceCurrency,
'payment_type' => $paymentType,
'auto_renew' => !empty($input['auto_renew']) ? 1 : 0,
'note' => $this->optionalString($input['note'] ?? null, 1000),
'is_active' => !empty($input['is_active']) ? 1 : 0,
];
$this->assertCurrencyType($payload['base_price_currency'], false, 'base_price_currency');
if ($bonusPercent !== null && $bonusPercent < 0) {
throw new ApiException('Bonus-Prozent darf nicht negativ sein.', 422);
}
$this->assertCurrencyType($payload['base_price_currency'], $paymentType === 'crypto', 'base_price_currency');
return $this->repository()->saveMinerOffer($projectKey, $payload);
}
@@ -1817,14 +1837,31 @@ final class Router
$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']);
$selectedSpeedValue = $this->optionalDecimal($input['mining_speed_value'] ?? null);
$selectedSpeedUnit = $this->optionalSpeedUnit($input['mining_speed_unit'] ?? null);
if (($selectedSpeedValue === null) xor ($selectedSpeedUnit === null)) {
throw new ApiException('Mining-Geschwindigkeit und Einheit muessen gemeinsam gesetzt oder beide leer sein.', 422);
}
$resolvedSpeedValue = $selectedSpeedValue ?? $this->optionalDecimal($offer['mining_speed_value'] ?? null);
$resolvedSpeedUnit = $selectedSpeedUnit ?? $this->optionalSpeedUnit($offer['mining_speed_unit'] ?? null);
$resolvedBonusPercent = $this->offerBonusPercent($offer);
$resolvedBonusValue = $this->bonusSpeedFromPercent($resolvedSpeedValue, $resolvedSpeedUnit, $resolvedBonusPercent);
$resolvedBonusUnit = $resolvedBonusValue !== null ? $resolvedSpeedUnit : null;
$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 || !$isAutoRenew) {
if ($purchaseCost === null) {
$purchaseCost = $this->resolveOfferPurchaseCost(array_merge($offer, [
'mining_speed_value' => $resolvedSpeedValue,
'mining_speed_unit' => $resolvedSpeedUnit,
'bonus_speed_value' => $resolvedBonusValue,
'bonus_speed_unit' => $resolvedBonusUnit,
'base_price_amount' => $this->optionalDecimal($input['reference_price_amount'] ?? null) ?? ($offer['base_price_amount'] ?? null),
'base_price_currency' => $this->optionalCurrency($input['reference_price_currency'] ?? null) ?? ($offer['base_price_currency'] ?? null),
'price_currency' => $purchaseCurrency !== '' ? $purchaseCurrency : ($offer['price_currency'] ?? ''),
]));
}
@@ -1836,12 +1873,12 @@ final class Router
return $this->repository()->purchaseMiner($projectKey, $offerId, [
'purchased_at' => $purchasedAt,
'label' => $offer['label'],
'label' => $this->optionalString($input['label'] ?? ($offer['label'] ?? null), 120) ?? $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,
'mining_speed_value' => $resolvedSpeedValue,
'mining_speed_unit' => $resolvedSpeedUnit,
'bonus_speed_value' => $resolvedBonusValue,
'bonus_speed_unit' => $resolvedBonusUnit,
'total_cost_amount' => $purchaseCost,
'currency' => $purchaseCurrency !== '' ? $purchaseCurrency : $offer['price_currency'],
'usd_reference_amount' => $offer['usd_reference_amount'] ?? null,
@@ -2440,6 +2477,39 @@ final class Router
return (float) ($baseAmount ?? $offer['price_amount'] ?? 0);
}
private function bonusSpeedFromPercent(?float $speedValue, ?string $speedUnit, ?float $bonusPercent): ?float
{
if ($speedValue === null || $speedUnit === null || $bonusPercent === null) {
return null;
}
return round($speedValue * ($bonusPercent / 100), 4);
}
private function offerBonusPercent(array $offer): ?float
{
$speedValue = $this->optionalDecimal($offer['mining_speed_value'] ?? null);
$speedUnit = $this->optionalSpeedUnit($offer['mining_speed_unit'] ?? null);
$bonusValue = $this->optionalDecimal($offer['bonus_speed_value'] ?? null);
$bonusUnit = $this->optionalSpeedUnit($offer['bonus_speed_unit'] ?? null);
if ($speedValue === null || $speedUnit === null || $bonusValue === null || $bonusUnit === null) {
return null;
}
$speedMh = $this->hashrateToMh($speedValue, $speedUnit);
$bonusMh = $this->hashrateToMh($bonusValue, $bonusUnit);
if ($speedMh <= 0 || $bonusMh <= 0) {
return null;
}
return ($bonusMh / $speedMh) * 100;
}
private function hashrateToMh(float $value, string $unit): float
{
return $unit === 'kH/s' ? $value / 1000 : $value;
}
private function pdo(): PDO
{
if ($this->pdo === null) {