Compare commits
2 Commits
99cf5eaef2
...
aa7ec1d321
| Author | SHA1 | Date | |
|---|---|---|---|
| aa7ec1d321 | |||
| 7783f65b26 |
@@ -805,8 +805,7 @@
|
||||
runtime_months: 1,
|
||||
mining_speed_value: '',
|
||||
mining_speed_unit: 'MH/s',
|
||||
bonus_speed_value: '',
|
||||
bonus_speed_unit: 'MH/s',
|
||||
bonus_percent: '',
|
||||
auto_renew: true,
|
||||
base_price_amount: '',
|
||||
payment_type: 'fiat',
|
||||
@@ -826,10 +825,7 @@
|
||||
const [minerOfferForm, setMinerOfferForm] = useState({
|
||||
label: '',
|
||||
runtime_months: '',
|
||||
mining_speed_value: '',
|
||||
mining_speed_unit: 'MH/s',
|
||||
bonus_speed_value: '',
|
||||
bonus_speed_unit: 'MH/s',
|
||||
bonus_percent: '',
|
||||
base_price_amount: '',
|
||||
base_price_currency: 'USD',
|
||||
payment_type: 'fiat',
|
||||
@@ -841,9 +837,14 @@
|
||||
const [purchaseMinerModalOpen, setPurchaseMinerModalOpen] = useState(false);
|
||||
const [purchaseMinerForm, setPurchaseMinerForm] = useState({
|
||||
offer_id: '',
|
||||
base_offer_id: '',
|
||||
purchased_at: '',
|
||||
label: '',
|
||||
mining_speed_value: '',
|
||||
mining_speed_unit: '',
|
||||
bonus_percent: '',
|
||||
total_cost_amount: '',
|
||||
currency: 'USD',
|
||||
currency: '',
|
||||
reference_price_amount: '',
|
||||
reference_price_currency: '',
|
||||
auto_renew: false,
|
||||
@@ -884,6 +885,9 @@
|
||||
const currentWalletSnapshots = Array.isArray(payload?.wallet_snapshots) ? payload.wallet_snapshots : [];
|
||||
const currentMinerOffers = Array.isArray(currentSettings.miner_offers) ? currentSettings.miner_offers : [];
|
||||
const currentPurchasedMiners = Array.isArray(currentSettings.purchased_miners) ? currentSettings.purchased_miners : [];
|
||||
const currentMiningCurrency = String((latest && latest.coin_currency) || currentSettings.crypto_currency || 'DOGE').toUpperCase();
|
||||
const latestWalletSnapshot = currentWalletSnapshots.length ? currentWalletSnapshots[0] : null;
|
||||
const currentWalletMiningBalance = walletAssetBalance(latestWalletSnapshot, currentMiningCurrency);
|
||||
const renewableOfferIds = new Set(
|
||||
currentMinerOffers
|
||||
.filter((offer) => !!offer.auto_renew)
|
||||
@@ -899,7 +903,7 @@
|
||||
: currencies;
|
||||
const selectableCurrencies = preferredSelectableCurrencies.length ? preferredSelectableCurrencies : currencies;
|
||||
const evaluatedMinerOffers = Array.isArray(payload?.summary?.miner_offers) ? payload.summary.miner_offers : [];
|
||||
const availableMinerOffers = evaluatedMinerOffers.filter((offer) => !!offer.is_active);
|
||||
const availableMinerOffers = evaluatedMinerOffers.filter((offer) => isOfferAvailableForWallet(offer, currentMiningCurrency, currentWalletMiningBalance));
|
||||
const filteredMinerOffers = availableMinerOffers.filter((offer) => {
|
||||
const speedMin = minerOfferFilters.speed_min === '' ? null : Number(minerOfferFilters.speed_min);
|
||||
const speedUnit = String(minerOfferFilters.speed_unit || 'auto');
|
||||
@@ -941,7 +945,7 @@
|
||||
: cryptoCurrencies;
|
||||
const selectableFiatCurrencies = preferredSelectableFiatCurrencies.length ? preferredSelectableFiatCurrencies : fiatCurrencies;
|
||||
const selectableCryptoCurrencies = preferredSelectableCryptoCurrencies.length ? preferredSelectableCryptoCurrencies : cryptoCurrencies;
|
||||
const selectedMinerScenario = availableMinerOffers.find((offer) => Number(offer.id) === Number(selectedMinerScenarioId)) || null;
|
||||
const selectedMinerScenario = availableMinerOffers.find((offer) => String(offer.id) === String(selectedMinerScenarioId)) || null;
|
||||
const activeMinerRows = currentPurchasedMiners.map((miner) => ({
|
||||
id: `purchase-${miner.id}`,
|
||||
source: 'miete',
|
||||
@@ -962,7 +966,7 @@
|
||||
: 'fiat',
|
||||
is_active: miner.is_active !== false,
|
||||
can_toggle_auto_renew: Number(miner.runtime_months) > 0 && renewableOfferIds.has(Number(miner.miner_offer_id)),
|
||||
hashrate_text: `${fmtNumber(((Number(miner.mining_speed_value) || 0) + (Number(miner.bonus_speed_value) || 0)), 4)} ${miner.mining_speed_unit || miner.bonus_speed_unit || ''}`.trim(),
|
||||
hashrate_text: formatHashrateWithBonus(miner.mining_speed_value, miner.mining_speed_unit, miner.bonus_speed_value, miner.bonus_speed_unit),
|
||||
type_label: 'Aus Angebot gemietet',
|
||||
})).concat(currentCostPlans.map((plan) => ({
|
||||
id: `plan-${plan.id}`,
|
||||
@@ -978,10 +982,7 @@
|
||||
payment_type: plan.payment_type,
|
||||
is_active: !!plan.is_active,
|
||||
can_toggle_auto_renew: false,
|
||||
hashrate_text: [
|
||||
formatSpeed(plan.mining_speed_value, plan.mining_speed_unit, 'Basis'),
|
||||
formatSpeed(plan.bonus_speed_value, plan.bonus_speed_unit, 'Bonus'),
|
||||
].filter(Boolean).join(' · ') || 'n/a',
|
||||
hashrate_text: formatHashrateWithBonus(plan.mining_speed_value, plan.mining_speed_unit, plan.bonus_speed_value, plan.bonus_speed_unit),
|
||||
type_label: 'Manuell eingetragen',
|
||||
}))).sort((left, right) => String(right.starts_at || '').localeCompare(String(left.starts_at || '')));
|
||||
|
||||
@@ -996,7 +997,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const exists = availableMinerOffers.some((offer) => Number(offer.id) === Number(selectedMinerScenarioId));
|
||||
const exists = availableMinerOffers.some((offer) => String(offer.id) === String(selectedMinerScenarioId));
|
||||
if (!exists) {
|
||||
setSelectedMinerScenarioId(null);
|
||||
}
|
||||
@@ -1017,6 +1018,11 @@
|
||||
setPurchaseMinerForm((current) => ({
|
||||
...current,
|
||||
offer_id: current.offer_id || String(selectedOffer.id),
|
||||
base_offer_id: current.base_offer_id || String(selectedOffer.base_offer_id || selectedOffer.id || ''),
|
||||
label: current.label || String(selectedOffer.label || ''),
|
||||
mining_speed_value: current.mining_speed_value || String(selectedOffer.mining_speed_value || ''),
|
||||
mining_speed_unit: current.mining_speed_unit || String(selectedOffer.mining_speed_unit || ''),
|
||||
bonus_percent: current.bonus_percent || (selectedOffer.bonus_percent !== null && selectedOffer.bonus_percent !== undefined ? String(selectedOffer.bonus_percent) : ''),
|
||||
currency: current.currency || (!selectedOffer.auto_renew ? (currentSettings.crypto_currency || 'DOGE') : (selectedOffer.effective_price_currency || selectedOffer.base_price_currency || 'USD')),
|
||||
total_cost_amount: current.total_cost_amount || (selectedOffer.effective_price_amount !== null && selectedOffer.effective_price_amount !== undefined ? String(selectedOffer.effective_price_amount) : ''),
|
||||
reference_price_amount: current.reference_price_amount || (selectedOffer.reference_price_amount !== null && selectedOffer.reference_price_amount !== undefined ? String(selectedOffer.reference_price_amount) : ''),
|
||||
@@ -1121,6 +1127,43 @@
|
||||
return rate === null ? null : numericValue * rate;
|
||||
}
|
||||
|
||||
function walletAssetBalance(snapshot, currency) {
|
||||
const code = String(currency || '').toUpperCase();
|
||||
if (!snapshot || !code) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assets = snapshot.balances_json && typeof snapshot.balances_json === 'object' ? snapshot.balances_json : {};
|
||||
const asset = assets[code] ?? assets[code.toLowerCase()] ?? assets[code.toUpperCase()];
|
||||
const snapshotCurrency = String(snapshot.wallet_currency || '').toUpperCase();
|
||||
const rawBalance = asset && typeof asset === 'object'
|
||||
? asset.balance
|
||||
: (asset ?? (snapshotCurrency === code ? snapshot.wallet_balance : null));
|
||||
const balance = Number(rawBalance);
|
||||
return Number.isFinite(balance) ? balance : null;
|
||||
}
|
||||
|
||||
function isOfferAvailableForWallet(offer, miningCurrency, walletBalance) {
|
||||
if (!offer || !offer.is_active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (String(offer.payment_type || '').toLowerCase() !== 'crypto') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currency = String(offer.effective_price_currency || offer.price_currency || '').toUpperCase();
|
||||
const expectedCurrency = String(miningCurrency || '').toUpperCase();
|
||||
const price = Number(offer.effective_price_amount);
|
||||
const balance = Number(walletBalance);
|
||||
|
||||
if (!currency || !expectedCurrency || currency !== expectedCurrency || !Number.isFinite(price) || !Number.isFinite(balance)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return price <= balance + 0.00000001;
|
||||
}
|
||||
|
||||
function latestFxHistoryRate(fromCurrency, toCurrency) {
|
||||
const from = String(fromCurrency || '').toUpperCase();
|
||||
const to = String(toCurrency || '').toUpperCase();
|
||||
@@ -1672,8 +1715,7 @@
|
||||
runtime_months: 1,
|
||||
mining_speed_value: '',
|
||||
mining_speed_unit: 'MH/s',
|
||||
bonus_speed_value: '',
|
||||
bonus_speed_unit: 'MH/s',
|
||||
bonus_percent: '',
|
||||
auto_renew: true,
|
||||
base_price_amount: '',
|
||||
payment_type: 'fiat',
|
||||
@@ -1717,19 +1759,20 @@
|
||||
setSaving(true);
|
||||
setError('');
|
||||
try {
|
||||
const offerPayload = {
|
||||
...minerOfferForm,
|
||||
base_price_currency: minerOfferForm.payment_type === 'crypto' ? 'USD' : minerOfferForm.base_price_currency,
|
||||
};
|
||||
await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/miner-offers`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(minerOfferForm),
|
||||
body: JSON.stringify(offerPayload),
|
||||
});
|
||||
setMessage('Miner-Angebot gespeichert.');
|
||||
setMinerOfferForm({
|
||||
label: '',
|
||||
runtime_months: '',
|
||||
mining_speed_value: '',
|
||||
mining_speed_unit: 'MH/s',
|
||||
bonus_speed_value: '',
|
||||
bonus_speed_unit: 'MH/s',
|
||||
bonus_percent: '',
|
||||
base_price_amount: '',
|
||||
base_price_currency: 'USD',
|
||||
payment_type: 'fiat',
|
||||
@@ -1756,6 +1799,21 @@
|
||||
body: JSON.stringify(overrides || { purchased_at: nowDateTimeLocalValue() }),
|
||||
});
|
||||
setMessage('Miner als gemietet erfasst.');
|
||||
setPurchaseMinerForm({
|
||||
offer_id: '',
|
||||
base_offer_id: '',
|
||||
purchased_at: '',
|
||||
label: '',
|
||||
mining_speed_value: '',
|
||||
mining_speed_unit: '',
|
||||
bonus_percent: '',
|
||||
total_cost_amount: '',
|
||||
currency: '',
|
||||
reference_price_amount: '',
|
||||
reference_price_currency: '',
|
||||
auto_renew: false,
|
||||
note: '',
|
||||
});
|
||||
setPurchaseMinerModalOpen(false);
|
||||
await loadBootstrap(projectKey);
|
||||
} catch (err) {
|
||||
@@ -1772,7 +1830,17 @@
|
||||
return;
|
||||
}
|
||||
|
||||
await purchaseMinerOffer(purchaseMinerForm.offer_id, {
|
||||
const selectedOffer = availableMinerOffers.find((offer) => String(offer.id) === String(purchaseMinerForm.offer_id));
|
||||
if (!selectedOffer) {
|
||||
setError('Bitte ein gueltiges Miner-Angebot auswaehlen.');
|
||||
return;
|
||||
}
|
||||
|
||||
await purchaseMinerOffer(String(selectedOffer.base_offer_id || selectedOffer.id), {
|
||||
label: purchaseMinerForm.label || null,
|
||||
mining_speed_value: purchaseMinerForm.mining_speed_value || null,
|
||||
mining_speed_unit: purchaseMinerForm.mining_speed_unit || null,
|
||||
bonus_percent: purchaseMinerForm.bonus_percent || null,
|
||||
purchased_at: purchaseMinerForm.purchased_at || nowDateTimeLocalValue(),
|
||||
total_cost_amount: purchaseMinerForm.total_cost_amount || null,
|
||||
currency: purchaseMinerForm.currency || null,
|
||||
@@ -2066,7 +2134,7 @@
|
||||
]);
|
||||
|
||||
function renderTab() {
|
||||
const currentCoinCurrency = String((latest && latest.coin_currency) || currentSettings.crypto_currency || 'DOGE').toUpperCase();
|
||||
const currentCoinCurrency = currentMiningCurrency;
|
||||
const perDayLabel = `${currentCoinCurrency} pro Tag`;
|
||||
|
||||
if (activeTab === 'overview') {
|
||||
@@ -2413,7 +2481,24 @@
|
||||
key: 'rent-miner',
|
||||
type: 'button',
|
||||
className: 'mc-button mc-button--ghost',
|
||||
onClick: () => setPurchaseMinerModalOpen(true),
|
||||
onClick: () => {
|
||||
setPurchaseMinerForm({
|
||||
offer_id: '',
|
||||
base_offer_id: '',
|
||||
purchased_at: '',
|
||||
label: '',
|
||||
mining_speed_value: '',
|
||||
mining_speed_unit: '',
|
||||
bonus_percent: '',
|
||||
total_cost_amount: '',
|
||||
currency: '',
|
||||
reference_price_amount: '',
|
||||
reference_price_currency: '',
|
||||
auto_renew: false,
|
||||
note: '',
|
||||
});
|
||||
setPurchaseMinerModalOpen(true);
|
||||
},
|
||||
disabled: !availableMinerOffers.length,
|
||||
}, 'Neuen Miner mieten'),
|
||||
]),
|
||||
@@ -2493,14 +2578,17 @@
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
panel('Miner-Angebote', 'Angebote fuer neue Miner und eine grobe Reinvestitionsbewertung auf Basis der aktuellen Leistung.', [
|
||||
panel('Miner-Angebote', currentWalletMiningBalance !== null
|
||||
? `Hier werden nur Basis-Miner gepflegt. Krypto-Angebote werden gegen deinen aktuellen Wallet-Bestand (${fmtNumber(currentWalletMiningBalance, 8)} ${currentMiningCurrency}) gefiltert.`
|
||||
: 'Hier werden nur Basis-Miner gepflegt. Alle vorgegebenen Geschwindigkeiten und Preise werden daraus automatisch abgeleitet.',
|
||||
[
|
||||
h('div', { key: 'actions', className: 'mc-inline-row' }, [
|
||||
h('button', {
|
||||
key: 'add-offer',
|
||||
type: 'button',
|
||||
className: 'mc-button mc-button--secondary',
|
||||
onClick: () => setMinerOfferModalOpen(true),
|
||||
}, 'Miner-Angebot anlegen'),
|
||||
}, 'Basis-Angebot anlegen'),
|
||||
]),
|
||||
h('div', { key: 'filters', className: 'mc-filter-grid' }, [
|
||||
inputField(`Min. Geschwindigkeit (${minerOfferFilters.speed_unit === 'kh' ? 'kH/s' : 'MH/s'})`, 'number', minerOfferFilters.speed_min, (value) => setMinerOfferFilters({ ...minerOfferFilters, speed_min: value }), '0.0001'),
|
||||
@@ -2538,7 +2626,7 @@
|
||||
h('td', { key: 'break' }, offer.break_even_days !== null ? `${fmtNumber(offer.break_even_days, 2)} Tage` : 'n/a'),
|
||||
h('td', { key: 'rec' }, [
|
||||
h('div', { key: 'rec-main' }, offer.recommendation),
|
||||
offer.base_price_amount !== null && offer.base_price_currency && offer.payment_type !== 'crypto'
|
||||
offer.base_price_amount !== null && offer.base_price_currency
|
||||
? h('div', { key: 'rec-ref', className: 'mc-kicker' }, `Basis ${fmtNumber(offer.base_price_amount, 6)} ${offer.base_price_currency}`)
|
||||
: null,
|
||||
offer.payment_type ? h('div', { key: 'paytype', className: 'mc-kicker' }, offer.payment_type === 'crypto' ? `Zahlung in Krypto (${currentSettings.crypto_currency || 'DOGE'})` : `Zahlung in FIAT (${offer.base_price_currency || 'EUR'})`) : null,
|
||||
@@ -2548,7 +2636,7 @@
|
||||
h('button', {
|
||||
key: 'scenario',
|
||||
type: 'button',
|
||||
className: cx('mc-button', Number(selectedMinerScenario?.id) === Number(offer.id) ? 'mc-button--secondary' : 'mc-button--ghost'),
|
||||
className: cx('mc-button', String(selectedMinerScenario?.id) === String(offer.id) ? 'mc-button--secondary' : 'mc-button--ghost'),
|
||||
onClick: () => setSelectedMinerScenarioId(offer.id),
|
||||
}, 'Szenario'),
|
||||
h('button', {
|
||||
@@ -2560,7 +2648,7 @@
|
||||
label: offer.label,
|
||||
target_amount_fiat: String(offer.base_price_amount ?? offer.effective_price_amount ?? ''),
|
||||
currency: offer.base_price_currency || offer.effective_price_currency || 'EUR',
|
||||
miner_offer_id: String(offer.id),
|
||||
miner_offer_id: '',
|
||||
is_active: true,
|
||||
sort_order: 0,
|
||||
});
|
||||
@@ -2574,7 +2662,12 @@
|
||||
onClick: () => {
|
||||
setPurchaseMinerForm({
|
||||
offer_id: String(offer.id),
|
||||
base_offer_id: String(offer.base_offer_id || offer.id || ''),
|
||||
purchased_at: nowDateTimeLocalValue(),
|
||||
label: String(offer.label || ''),
|
||||
mining_speed_value: String(offer.mining_speed_value || ''),
|
||||
mining_speed_unit: String(offer.mining_speed_unit || ''),
|
||||
bonus_percent: offer.bonus_percent !== null && offer.bonus_percent !== undefined ? String(offer.bonus_percent) : '',
|
||||
total_cost_amount: offer.effective_price_amount !== null && offer.effective_price_amount !== undefined ? String(offer.effective_price_amount) : '',
|
||||
currency: offer.effective_price_currency || offer.base_price_currency || 'USD',
|
||||
reference_price_amount: offer.base_price_amount !== null && offer.base_price_amount !== undefined ? String(offer.base_price_amount) : '',
|
||||
@@ -2702,8 +2795,7 @@
|
||||
inputField('Laufzeit in Monaten', 'number', String(costPlanForm.runtime_months), (value) => setCostPlanForm({ ...costPlanForm, runtime_months: Number(value) || 0 })),
|
||||
inputField('Mining-Geschwindigkeit', 'number', costPlanForm.mining_speed_value, (value) => setCostPlanForm({ ...costPlanForm, mining_speed_value: value }), '0.0001'),
|
||||
selectField('Mining-Einheit', costPlanForm.mining_speed_unit, speedUnits, (value) => setCostPlanForm({ ...costPlanForm, mining_speed_unit: value })),
|
||||
inputField('Bonus-Geschwindigkeit', 'number', costPlanForm.bonus_speed_value, (value) => setCostPlanForm({ ...costPlanForm, bonus_speed_value: value }), '0.0001'),
|
||||
selectField('Bonus-Einheit', costPlanForm.bonus_speed_unit, speedUnits, (value) => setCostPlanForm({ ...costPlanForm, bonus_speed_unit: value })),
|
||||
inputField('Bonus-Hashrate in %', 'number', costPlanForm.bonus_percent, (value) => setCostPlanForm({ ...costPlanForm, bonus_percent: value }), '0.01'),
|
||||
inputField(`Basispreis in ${settingsForm.report_currency || 'EUR'}`, 'number', costPlanForm.base_price_amount, (value) => setCostPlanForm({ ...costPlanForm, base_price_amount: value }), '0.000001'),
|
||||
selectField('Zahlungsart', costPlanForm.payment_type, [{ value: 'fiat', label: 'FIAT' }, { value: 'crypto', label: 'Krypto' }], (value) => setCostPlanForm({ ...costPlanForm, payment_type: value })),
|
||||
textareaField('Notiz', costPlanForm.note, (value) => setCostPlanForm({ ...costPlanForm, note: value })),
|
||||
@@ -2733,17 +2825,23 @@
|
||||
]),
|
||||
]),
|
||||
], () => setPayoutModalOpen(false)) : null,
|
||||
minerOfferModalOpen ? renderModal('Miner-Angebot anlegen', [
|
||||
minerOfferModalOpen ? renderModal('Basis-Miner-Angebot anlegen', [
|
||||
h('form', { key: 'form', className: 'mc-form', onSubmit: submitMinerOffer }, [
|
||||
inputField('Label', 'text', minerOfferForm.label, (value) => setMinerOfferForm({ ...minerOfferForm, label: value })),
|
||||
inputField('Laufzeit in Monaten', 'number', minerOfferForm.runtime_months, (value) => setMinerOfferForm({ ...minerOfferForm, runtime_months: value })),
|
||||
inputField('Mining-Geschwindigkeit', 'number', minerOfferForm.mining_speed_value, (value) => setMinerOfferForm({ ...minerOfferForm, mining_speed_value: value }), '0.0001'),
|
||||
selectField('Mining-Einheit', minerOfferForm.mining_speed_unit, speedUnits, (value) => setMinerOfferForm({ ...minerOfferForm, mining_speed_unit: value })),
|
||||
inputField('Bonus-Geschwindigkeit', 'number', minerOfferForm.bonus_speed_value, (value) => setMinerOfferForm({ ...minerOfferForm, bonus_speed_value: value }), '0.0001'),
|
||||
selectField('Bonus-Einheit', minerOfferForm.bonus_speed_unit, speedUnits, (value) => setMinerOfferForm({ ...minerOfferForm, bonus_speed_unit: value })),
|
||||
inputField('Basispreis', 'number', minerOfferForm.base_price_amount, (value) => setMinerOfferForm({ ...minerOfferForm, base_price_amount: value }), '0.000001'),
|
||||
selectField('Basiswährung', minerOfferForm.base_price_currency, selectableFiatCurrencies.map((currency) => currency.code), (value) => setMinerOfferForm({ ...minerOfferForm, base_price_currency: value })),
|
||||
selectField('Zahlungsart', minerOfferForm.payment_type, [{ value: 'fiat', label: 'FIAT' }, { value: 'crypto', label: 'Krypto' }], (value) => setMinerOfferForm({ ...minerOfferForm, payment_type: value })),
|
||||
displayField('Basis-Geschwindigkeit', baseOfferSpeedLabel(minerOfferForm.payment_type)),
|
||||
inputField('Bonus-Hashrate in %', 'number', minerOfferForm.bonus_percent, (value) => setMinerOfferForm({ ...minerOfferForm, bonus_percent: value }), '0.01'),
|
||||
inputField(minerOfferForm.payment_type === 'crypto' ? 'Basispreis in USD' : 'Basispreis', 'number', minerOfferForm.base_price_amount, (value) => setMinerOfferForm({ ...minerOfferForm, base_price_amount: value }), '0.000001'),
|
||||
selectField('Zahlungsart', minerOfferForm.payment_type, [{ value: 'fiat', label: 'FIAT' }, { value: 'crypto', label: 'Krypto' }], (value) => setMinerOfferForm({
|
||||
...minerOfferForm,
|
||||
payment_type: value,
|
||||
base_price_currency: value === 'crypto'
|
||||
? 'USD'
|
||||
: (selectableFiatCurrencies[0]?.code || 'USD'),
|
||||
})),
|
||||
minerOfferForm.payment_type === 'crypto'
|
||||
? displayField('Kostenwaehrung', 'USD')
|
||||
: selectField('Basiswährung', minerOfferForm.base_price_currency, selectableFiatCurrencies.map((currency) => currency.code), (value) => setMinerOfferForm({ ...minerOfferForm, base_price_currency: value })),
|
||||
h('label', { className: 'mc-checkbox' }, [
|
||||
h('input', { type: 'checkbox', checked: !!minerOfferForm.auto_renew, onChange: (event) => setMinerOfferForm({ ...minerOfferForm, auto_renew: event.target.checked }) }),
|
||||
'Automatische Verlängerung',
|
||||
@@ -2768,7 +2866,12 @@
|
||||
const offer = availableMinerOffers.find((item) => String(item.id) === String(value));
|
||||
setPurchaseMinerForm({
|
||||
offer_id: value,
|
||||
base_offer_id: offer ? String(offer.base_offer_id || offer.id || '') : '',
|
||||
purchased_at: purchaseMinerForm.purchased_at || nowDateTimeLocalValue(),
|
||||
label: offer ? String(offer.label || '') : '',
|
||||
mining_speed_value: offer && offer.mining_speed_value !== null && offer.mining_speed_value !== undefined ? String(offer.mining_speed_value) : '',
|
||||
mining_speed_unit: offer?.mining_speed_unit || '',
|
||||
bonus_percent: offer && offer.bonus_percent !== null && offer.bonus_percent !== undefined ? String(offer.bonus_percent) : '',
|
||||
total_cost_amount: offer && offer.effective_price_amount !== null && offer.effective_price_amount !== undefined ? String(offer.effective_price_amount) : '',
|
||||
currency: offer?.effective_price_currency || offer?.base_price_currency || 'USD',
|
||||
reference_price_amount: offer && offer.reference_price_amount !== null && offer.reference_price_amount !== undefined ? String(offer.reference_price_amount) : '',
|
||||
@@ -2778,6 +2881,10 @@
|
||||
});
|
||||
}),
|
||||
inputField('Mietdatum/-zeit', 'datetime-local', purchaseMinerForm.purchased_at, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, purchased_at: value })),
|
||||
inputField('Label', 'text', purchaseMinerForm.label, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, label: value })),
|
||||
inputField('Mining-Geschwindigkeit', 'number', purchaseMinerForm.mining_speed_value, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, mining_speed_value: value }), '0.0001'),
|
||||
selectField('Mining-Einheit', purchaseMinerForm.mining_speed_unit, speedUnits, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, mining_speed_unit: value })),
|
||||
inputField('Bonus-Hashrate in %', 'number', purchaseMinerForm.bonus_percent, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, bonus_percent: value }), '0.01'),
|
||||
inputField('Exakter Mietpreis', 'number', purchaseMinerForm.total_cost_amount, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, total_cost_amount: value }), '0.000001'),
|
||||
selectField('Mietwährung', purchaseMinerForm.currency, selectableCurrencies.map((currency) => currency.code), (value) => setPurchaseMinerForm({ ...purchaseMinerForm, currency: value })),
|
||||
inputField('Referenzpreis', 'number', purchaseMinerForm.reference_price_amount, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, reference_price_amount: value }), '0.000001'),
|
||||
@@ -2796,11 +2903,11 @@
|
||||
targetModalOpen ? renderModal('Ziel anlegen', [
|
||||
h('form', { key: 'form', className: 'mc-form', onSubmit: submitTarget }, [
|
||||
inputField('Label', 'text', targetForm.label, (value) => setTargetForm({ ...targetForm, label: value })),
|
||||
selectField('Angebots-Verknuepfung', targetForm.miner_offer_id || '', [{ value: '', label: 'Kein verknuepftes Angebot' }].concat(availableMinerOffers.map((offer) => ({
|
||||
selectField('Angebots-Verknuepfung', targetForm.miner_offer_id || '', [{ value: '', label: 'Kein verknuepftes Angebot' }].concat(currentMinerOffers.map((offer) => ({
|
||||
value: String(offer.id),
|
||||
label: `${offer.label} · ${fmtNumber(offer.base_price_amount ?? offer.effective_price_amount, 6)} ${offer.base_price_currency || offer.effective_price_currency}`,
|
||||
label: `${offer.label} · ${fmtNumber(offer.base_price_amount, 6)} ${offer.base_price_currency}`,
|
||||
}))), (value) => {
|
||||
const offer = availableMinerOffers.find((item) => String(item.id) === String(value));
|
||||
const offer = currentMinerOffers.find((item) => String(item.id) === String(value));
|
||||
setTargetForm({
|
||||
...targetForm,
|
||||
miner_offer_id: value,
|
||||
@@ -3051,6 +3158,18 @@
|
||||
return `${label ? label + ' ' : ''}${fmtNumber(value, 4)} ${unit}`;
|
||||
}
|
||||
|
||||
function baseOfferSpeedLabel(paymentType) {
|
||||
return String(paymentType || 'fiat') === 'crypto' ? '75 kH/s' : '50 kH/s';
|
||||
}
|
||||
|
||||
function formatHashrateWithBonus(speedValue, speedUnit, bonusValue, bonusUnit) {
|
||||
const parts = [
|
||||
formatSpeed(speedValue, speedUnit, 'Basis'),
|
||||
Number(bonusValue) > 0 ? formatSpeed(bonusValue, bonusUnit, 'Bonus') : '',
|
||||
].filter(Boolean);
|
||||
return parts.length ? parts.join(' · ') : 'n/a';
|
||||
}
|
||||
|
||||
function formatAdaptiveSpeed(valueMh) {
|
||||
const numericValue = Number(valueMh);
|
||||
if (!Number.isFinite(numericValue)) {
|
||||
|
||||
@@ -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,21 +1798,32 @@ 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 = $paymentType === 'crypto'
|
||||
? 'USD'
|
||||
: $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,
|
||||
];
|
||||
|
||||
if ($bonusPercent !== null && $bonusPercent < 0) {
|
||||
throw new ApiException('Bonus-Prozent darf nicht negativ sein.', 422);
|
||||
}
|
||||
|
||||
$this->assertCurrencyType($payload['base_price_currency'], false, 'base_price_currency');
|
||||
|
||||
return $this->repository()->saveMinerOffer($projectKey, $payload);
|
||||
@@ -1817,31 +1839,53 @@ 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']);
|
||||
$paymentType = $this->enumValue($offer['payment_type'] ?? 'fiat', ['fiat', 'crypto'], 'payment_type');
|
||||
$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);
|
||||
$selectedBonusPercent = $this->optionalDecimal($input['bonus_percent'] ?? null);
|
||||
if ($selectedBonusPercent !== null && $selectedBonusPercent < 0) {
|
||||
throw new ApiException('Bonus-Prozent darf nicht negativ sein.', 422);
|
||||
}
|
||||
$resolvedBonusPercent = $selectedBonusPercent ?? $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) {
|
||||
if ($paymentType === 'crypto' || !$isAutoRenew) {
|
||||
$purchaseCurrency = $cryptoCurrency;
|
||||
}
|
||||
$purchaseCost = $this->optionalDecimal($input['total_cost_amount'] ?? null);
|
||||
if ($purchaseCost === null || !$isAutoRenew) {
|
||||
$purchaseCost = $this->resolveOfferPurchaseCost(array_merge($offer, [
|
||||
if ($purchaseCost === null) {
|
||||
$purchaseCost = $this->resolveOfferPurchaseCost($projectKey, 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'] ?? ''),
|
||||
]));
|
||||
}
|
||||
$referencePriceAmount = $this->optionalDecimal($input['reference_price_amount'] ?? ($offer['reference_price_amount'] ?? $offer['usd_reference_amount'] ?? null));
|
||||
$referencePriceCurrency = $this->optionalCurrency($input['reference_price_currency'] ?? ($offer['reference_price_currency'] ?? (is_numeric($offer['usd_reference_amount'] ?? null) ? 'USD' : null)));
|
||||
$referencePriceAmount = $this->optionalDecimal($input['reference_price_amount'] ?? ($offer['base_price_amount'] ?? $offer['reference_price_amount'] ?? $offer['usd_reference_amount'] ?? null));
|
||||
$referencePriceCurrency = $this->optionalCurrency($input['reference_price_currency'] ?? ($offer['base_price_currency'] ?? $offer['reference_price_currency'] ?? (is_numeric($offer['usd_reference_amount'] ?? null) ? 'USD' : null)));
|
||||
$purchasedAt = array_key_exists('purchased_at', $input)
|
||||
? $this->requiredDateTime($input['purchased_at'], 'purchased_at', $this->projectTimezone($projectKey))
|
||||
: $this->currentTimestamp();
|
||||
|
||||
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,
|
||||
@@ -2420,7 +2464,7 @@ final class Router
|
||||
return abs(time() - $parsed) <= (int) round(max(0.25, $maxAgeHours) * 3600);
|
||||
}
|
||||
|
||||
private function resolveOfferPurchaseCost(array $offer): float
|
||||
private function resolveOfferPurchaseCost(string $projectKey, array $offer): float
|
||||
{
|
||||
$purchaseCurrency = (string) ($offer['price_currency'] ?? '');
|
||||
$baseAmount = is_numeric($offer['base_price_amount'] ?? null)
|
||||
@@ -2431,6 +2475,11 @@ final class Router
|
||||
$baseCurrency = (string) ($offer['base_price_currency'] ?? $offer['reference_price_currency'] ?? (is_numeric($offer['usd_reference_amount'] ?? null) ? 'USD' : ''));
|
||||
|
||||
if ($purchaseCurrency !== '' && $baseAmount !== null && $baseAmount > 0 && $baseCurrency !== '') {
|
||||
$latestConverted = $this->convertWithLatestMeasurement($projectKey, $baseAmount, $baseCurrency, $purchaseCurrency);
|
||||
if ($latestConverted !== null && $latestConverted > 0) {
|
||||
return $latestConverted;
|
||||
}
|
||||
|
||||
$converted = $this->fx()->convert($baseAmount, $baseCurrency, $purchaseCurrency);
|
||||
if (is_numeric($converted) && (float) $converted > 0) {
|
||||
return (float) $converted;
|
||||
@@ -2440,6 +2489,89 @@ final class Router
|
||||
return (float) ($baseAmount ?? $offer['price_amount'] ?? 0);
|
||||
}
|
||||
|
||||
private function convertWithLatestMeasurement(string $projectKey, float $amount, string $fromCurrency, string $toCurrency): ?float
|
||||
{
|
||||
$from = strtoupper(trim($fromCurrency));
|
||||
$to = strtoupper(trim($toCurrency));
|
||||
if ($from === '' || $to === '' || $amount <= 0) {
|
||||
return null;
|
||||
}
|
||||
if ($from === $to) {
|
||||
return $amount;
|
||||
}
|
||||
|
||||
$latestRows = $this->repository()->listRecentMeasurements($projectKey, 1);
|
||||
$latest = $latestRows[0] ?? null;
|
||||
if (!is_array($latest)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$coinCurrency = strtoupper(trim((string) ($latest['coin_currency'] ?? $this->settings($projectKey)['crypto_currency'] ?? '')));
|
||||
$priceCurrency = strtoupper(trim((string) ($latest['price_currency'] ?? '')));
|
||||
$pricePerCoin = is_numeric($latest['price_per_coin'] ?? null) ? (float) $latest['price_per_coin'] : null;
|
||||
if ($coinCurrency === '' || $priceCurrency === '' || $pricePerCoin === null || $pricePerCoin <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($from === $priceCurrency && $to === $coinCurrency) {
|
||||
return $amount / $pricePerCoin;
|
||||
}
|
||||
|
||||
if ($from === $coinCurrency && $to === $priceCurrency) {
|
||||
return $amount * $pricePerCoin;
|
||||
}
|
||||
|
||||
if ($to === $coinCurrency) {
|
||||
$convertedFiat = $this->fx()->convert($amount, $from, $priceCurrency);
|
||||
if (is_numeric($convertedFiat) && (float) $convertedFiat > 0) {
|
||||
return (float) $convertedFiat / $pricePerCoin;
|
||||
}
|
||||
}
|
||||
|
||||
if ($from === $coinCurrency) {
|
||||
$convertedFiat = $amount * $pricePerCoin;
|
||||
$convertedTarget = $this->fx()->convert($convertedFiat, $priceCurrency, $to);
|
||||
if (is_numeric($convertedTarget)) {
|
||||
return (float) $convertedTarget;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -7,6 +7,16 @@ use Modules\MiningChecker\Support\ApiException;
|
||||
|
||||
final class AnalyticsService
|
||||
{
|
||||
private const BASE_OFFER_SPEEDS = [
|
||||
'fiat' => ['value' => 50.0, 'unit' => 'kH/s'],
|
||||
'crypto' => ['value' => 75.0, 'unit' => 'kH/s'],
|
||||
];
|
||||
|
||||
private const OFFER_SPEED_MULTIPLIERS = [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
||||
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
||||
];
|
||||
|
||||
private ?FxService $fx;
|
||||
|
||||
public function __construct(?FxService $fx = null)
|
||||
@@ -245,7 +255,7 @@ final class AnalyticsService
|
||||
$purchasedMiners
|
||||
));
|
||||
$offerSummary = [];
|
||||
foreach ($minerOffers as $offer) {
|
||||
foreach ($this->expandOfferVariants($minerOffers, $settings) as $offer) {
|
||||
$offerSummary[] = $this->evaluateMinerOffer($offer, $latest, $latestPriceByCurrency, $currentHashrateMh, $settings);
|
||||
}
|
||||
|
||||
@@ -256,7 +266,7 @@ final class AnalyticsService
|
||||
$linkedOffer = null;
|
||||
if (is_numeric($target['miner_offer_id'] ?? null)) {
|
||||
foreach ($offerSummary as $offer) {
|
||||
if ((int) ($offer['id'] ?? 0) === (int) $target['miner_offer_id']) {
|
||||
if ((int) ($offer['base_offer_id'] ?? $offer['id'] ?? 0) === (int) $target['miner_offer_id']) {
|
||||
$linkedOffer = $offer;
|
||||
break;
|
||||
}
|
||||
@@ -624,6 +634,11 @@ final class AnalyticsService
|
||||
return $amount;
|
||||
}
|
||||
|
||||
$measurementConverted = $this->convertAmountFromMeasurementPrice($amount, $from, $to, $fxContext);
|
||||
if ($measurementConverted !== null) {
|
||||
return $measurementConverted;
|
||||
}
|
||||
|
||||
if ($this->fx === null) {
|
||||
return null;
|
||||
}
|
||||
@@ -636,6 +651,62 @@ final class AnalyticsService
|
||||
return $this->fx->convertAt($amount, $from, $to, $at, null, $fetchId);
|
||||
}
|
||||
|
||||
private function convertAmountFromMeasurementPrice(float $amount, string $from, string $to, ?array $fxContext = null): ?float
|
||||
{
|
||||
if (!is_array($fxContext)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$coinCurrency = strtoupper(trim((string) ($fxContext['coin_currency'] ?? '')));
|
||||
$priceCurrency = strtoupper(trim((string) ($fxContext['effective_price_currency'] ?? $fxContext['price_currency'] ?? '')));
|
||||
$pricePerCoin = is_numeric($fxContext['effective_price_per_coin'] ?? null)
|
||||
? (float) $fxContext['effective_price_per_coin']
|
||||
: (is_numeric($fxContext['price_per_coin'] ?? null) ? (float) $fxContext['price_per_coin'] : null);
|
||||
|
||||
if ($coinCurrency === '' || $priceCurrency === '' || $pricePerCoin === null || $pricePerCoin <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($from === $coinCurrency && $to === $priceCurrency) {
|
||||
return $amount * $pricePerCoin;
|
||||
}
|
||||
|
||||
if ($from === $priceCurrency && $to === $coinCurrency) {
|
||||
return $amount / $pricePerCoin;
|
||||
}
|
||||
|
||||
if ($to === $coinCurrency && $this->fx !== null) {
|
||||
$convertedFiat = $this->fx->convertAt(
|
||||
$amount,
|
||||
$from,
|
||||
$priceCurrency,
|
||||
is_string($fxContext['measured_at'] ?? null) ? (string) $fxContext['measured_at'] : null,
|
||||
null,
|
||||
isset($fxContext['fx_fetch_id']) && is_numeric($fxContext['fx_fetch_id']) ? (int) $fxContext['fx_fetch_id'] : null
|
||||
);
|
||||
if (is_numeric($convertedFiat) && (float) $convertedFiat > 0) {
|
||||
return (float) $convertedFiat / $pricePerCoin;
|
||||
}
|
||||
}
|
||||
|
||||
if ($from === $coinCurrency && $this->fx !== null) {
|
||||
$convertedFiat = $amount * $pricePerCoin;
|
||||
$convertedTarget = $this->fx->convertAt(
|
||||
$convertedFiat,
|
||||
$priceCurrency,
|
||||
$to,
|
||||
is_string($fxContext['measured_at'] ?? null) ? (string) $fxContext['measured_at'] : null,
|
||||
null,
|
||||
isset($fxContext['fx_fetch_id']) && is_numeric($fxContext['fx_fetch_id']) ? (int) $fxContext['fx_fetch_id'] : null
|
||||
);
|
||||
if (is_numeric($convertedTarget)) {
|
||||
return (float) $convertedTarget;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function totalHashrateMh(array $entries): float
|
||||
{
|
||||
$total = 0.0;
|
||||
@@ -823,6 +894,102 @@ final class AnalyticsService
|
||||
};
|
||||
}
|
||||
|
||||
private function expandOfferVariants(array $offers, array $settings): array
|
||||
{
|
||||
$expanded = [];
|
||||
foreach ($offers as $offer) {
|
||||
$paymentType = $this->normalizeOfferPaymentType($offer);
|
||||
$baseSpeed = self::BASE_OFFER_SPEEDS[$paymentType] ?? self::BASE_OFFER_SPEEDS['fiat'];
|
||||
$baseSpeedValue = (float) $baseSpeed['value'];
|
||||
$baseSpeedUnit = (string) $baseSpeed['unit'];
|
||||
$baseBonusPercent = $this->deriveBonusPercent($offer, $baseSpeedValue, $baseSpeedUnit);
|
||||
$basePriceAmount = is_numeric($offer['base_price_amount'] ?? null)
|
||||
? (float) $offer['base_price_amount']
|
||||
: (is_numeric($offer['reference_price_amount'] ?? null)
|
||||
? (float) $offer['reference_price_amount']
|
||||
: (is_numeric($offer['usd_reference_amount'] ?? null)
|
||||
? (float) $offer['usd_reference_amount']
|
||||
: (is_numeric($offer['price_amount'] ?? null) ? (float) $offer['price_amount'] : null)));
|
||||
|
||||
foreach (self::OFFER_SPEED_MULTIPLIERS as $multiplier) {
|
||||
$derivedSpeedValue = $baseSpeedValue * $multiplier;
|
||||
$derivedBonusValue = $baseBonusPercent !== null
|
||||
? round($derivedSpeedValue * ($baseBonusPercent / 100), 4)
|
||||
: null;
|
||||
$derivedPriceAmount = $basePriceAmount !== null
|
||||
? round($basePriceAmount * $multiplier, 8)
|
||||
: null;
|
||||
|
||||
$expanded[] = array_merge($offer, [
|
||||
'id' => sprintf('%s:%s:%s', (string) ($offer['id'] ?? 'offer'), $paymentType, rtrim(rtrim(number_format($derivedSpeedValue, 4, '.', ''), '0'), '.')),
|
||||
'base_offer_id' => $offer['id'] ?? null,
|
||||
'base_offer_label' => $offer['label'] ?? null,
|
||||
'payment_type' => $paymentType,
|
||||
'label' => trim(((string) ($offer['label'] ?? 'Miner-Angebot')) . ' · ' . $this->formatSpeedLabel($derivedSpeedValue, $baseSpeedUnit)),
|
||||
'mining_speed_value' => $derivedSpeedValue,
|
||||
'mining_speed_unit' => $baseSpeedUnit,
|
||||
'bonus_speed_value' => $derivedBonusValue,
|
||||
'bonus_speed_unit' => $derivedBonusValue !== null ? $baseSpeedUnit : null,
|
||||
'bonus_percent' => $baseBonusPercent,
|
||||
'speed_factor' => $multiplier,
|
||||
'base_speed_value' => $baseSpeedValue,
|
||||
'base_speed_unit' => $baseSpeedUnit,
|
||||
'base_price_amount' => $derivedPriceAmount,
|
||||
'reference_price_amount' => $derivedPriceAmount,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
usort($expanded, static function (array $left, array $right): int {
|
||||
$paymentCompare = strcmp((string) ($left['payment_type'] ?? ''), (string) ($right['payment_type'] ?? ''));
|
||||
if ($paymentCompare !== 0) {
|
||||
return $paymentCompare;
|
||||
}
|
||||
|
||||
$runtimeCompare = ((int) ($left['runtime_months'] ?? 0)) <=> ((int) ($right['runtime_months'] ?? 0));
|
||||
if ($runtimeCompare !== 0) {
|
||||
return $runtimeCompare;
|
||||
}
|
||||
|
||||
return ((int) ($left['speed_factor'] ?? 0)) <=> ((int) ($right['speed_factor'] ?? 0));
|
||||
});
|
||||
|
||||
return $expanded;
|
||||
}
|
||||
|
||||
private function normalizeOfferPaymentType(array $offer): string
|
||||
{
|
||||
$paymentType = strtolower(trim((string) ($offer['payment_type'] ?? '')));
|
||||
if (in_array($paymentType, ['fiat', 'crypto'], true)) {
|
||||
return $paymentType;
|
||||
}
|
||||
|
||||
return !empty($offer['price_currency']) && in_array(strtoupper((string) $offer['price_currency']), ['ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP'], true)
|
||||
? 'crypto'
|
||||
: 'fiat';
|
||||
}
|
||||
|
||||
private function deriveBonusPercent(array $offer, float $speedValue, string $speedUnit): ?float
|
||||
{
|
||||
if (is_numeric($offer['bonus_percent'] ?? null)) {
|
||||
$numeric = (float) $offer['bonus_percent'];
|
||||
return $numeric >= 0 ? $numeric : null;
|
||||
}
|
||||
|
||||
$baseSpeedMh = $this->normalizeHashrateMh($speedValue, $speedUnit);
|
||||
$bonusMh = $this->normalizeHashrateMh($offer['bonus_speed_value'] ?? null, $offer['bonus_speed_unit'] ?? null);
|
||||
if ($baseSpeedMh <= 0 || $bonusMh <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ($bonusMh / $baseSpeedMh) * 100;
|
||||
}
|
||||
|
||||
private function formatSpeedLabel(float $value, string $unit): string
|
||||
{
|
||||
return rtrim(rtrim(number_format($value, 4, '.', ''), '0'), '.') . ' ' . $unit;
|
||||
}
|
||||
|
||||
private function evaluateMinerOffer(array $offer, array $latest, array $latestPriceByCurrency, float $currentHashrateMh, array $settings): array
|
||||
{
|
||||
$offerHashrateMh = $this->normalizeHashrateMh($offer['mining_speed_value'] ?? null, $offer['mining_speed_unit'] ?? null)
|
||||
@@ -850,6 +1017,8 @@ final class AnalyticsService
|
||||
$convertedReference = $this->convertAmount($basePriceAmount, $basePriceCurrency, $effectivePriceCurrency, $latest);
|
||||
if ($convertedReference !== null && $convertedReference > 0) {
|
||||
$effectivePriceAmount = $convertedReference;
|
||||
} else {
|
||||
$effectivePriceAmount = null;
|
||||
}
|
||||
}
|
||||
$referencePriceAmount = $basePriceAmount;
|
||||
|
||||
Reference in New Issue
Block a user