1623 lines
71 KiB
PHP
1623 lines
71 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Modules\MiningChecker\Infrastructure;
|
|
|
|
use Modules\MiningChecker\Support\DebugTrace;
|
|
use PDO;
|
|
|
|
final class MiningRepository
|
|
{
|
|
private PDO $pdo;
|
|
private string $prefix;
|
|
private string $driver;
|
|
private ?DebugTrace $debug;
|
|
private string $ownerSub;
|
|
|
|
public function __construct(PDO $pdo, string $prefix = 'miningcheck_', ?DebugTrace $debug = null, ?string $ownerSub = null)
|
|
{
|
|
$this->pdo = $pdo;
|
|
$this->prefix = $prefix;
|
|
$this->driver = strtolower((string) $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME));
|
|
$this->debug = $debug;
|
|
$this->ownerSub = trim((string) $ownerSub) !== '' ? trim((string) $ownerSub) : 'local';
|
|
}
|
|
|
|
public function ensureProject(string $projectKey, ?string $projectName = null): void
|
|
{
|
|
$stmt = $this->pdo->prepare($this->driver === 'pgsql'
|
|
? 'INSERT INTO ' . $this->table('projects') . ' (project_key, project_name)
|
|
VALUES (:project_key, :project_name)
|
|
ON CONFLICT (project_key) DO UPDATE
|
|
SET project_name = COALESCE(' . $this->table('projects') . '.project_name, EXCLUDED.project_name)'
|
|
: 'INSERT INTO ' . $this->table('projects') . ' (project_key, project_name)
|
|
VALUES (:project_key, :project_name)
|
|
ON DUPLICATE KEY UPDATE project_name = COALESCE(project_name, VALUES(project_name))'
|
|
);
|
|
$stmt->execute([
|
|
'project_key' => $projectKey,
|
|
'project_name' => $projectName ?: strtoupper($projectKey),
|
|
]);
|
|
}
|
|
|
|
public function getProject(string $projectKey): ?array
|
|
{
|
|
$stmt = $this->pdo->prepare('SELECT * FROM ' . $this->table('projects') . ' WHERE project_key = :project_key LIMIT 1');
|
|
$stmt->execute(['project_key' => $projectKey]);
|
|
$project = $stmt->fetch();
|
|
return is_array($project) ? $this->normalizeRow($project) : null;
|
|
}
|
|
|
|
public function getSettings(string $projectKey): ?array
|
|
{
|
|
$stmt = $this->pdo->prepare('SELECT * FROM ' . $this->table('settings') . ' WHERE project_key = :project_key AND owner_sub = :owner_sub LIMIT 1');
|
|
$stmt->execute(['project_key' => $projectKey, 'owner_sub' => $this->ownerSub]);
|
|
$row = $stmt->fetch();
|
|
return is_array($row) ? $this->normalizeRow($row) : null;
|
|
}
|
|
|
|
public function saveSettings(string $projectKey, array $settings): void
|
|
{
|
|
$stmt = $this->pdo->prepare($this->driver === 'pgsql'
|
|
? 'INSERT INTO ' . $this->table('settings') . ' (
|
|
project_key, owner_sub, baseline_measured_at, baseline_coins_total, daily_cost_amount, daily_cost_currency, report_currency, crypto_currency, display_timezone, fx_max_age_hours, module_theme_mode, module_theme_accent, preferred_currencies
|
|
) VALUES (
|
|
:project_key, :owner_sub, :baseline_measured_at, :baseline_coins_total, :daily_cost_amount, :daily_cost_currency, :report_currency, :crypto_currency, :display_timezone, :fx_max_age_hours, :module_theme_mode, :module_theme_accent, CAST(:preferred_currencies AS jsonb)
|
|
)
|
|
ON CONFLICT (project_key, owner_sub) DO UPDATE SET
|
|
baseline_measured_at = EXCLUDED.baseline_measured_at,
|
|
baseline_coins_total = EXCLUDED.baseline_coins_total,
|
|
daily_cost_amount = EXCLUDED.daily_cost_amount,
|
|
daily_cost_currency = EXCLUDED.daily_cost_currency,
|
|
report_currency = EXCLUDED.report_currency,
|
|
crypto_currency = EXCLUDED.crypto_currency,
|
|
display_timezone = EXCLUDED.display_timezone,
|
|
fx_max_age_hours = EXCLUDED.fx_max_age_hours,
|
|
module_theme_mode = EXCLUDED.module_theme_mode,
|
|
module_theme_accent = EXCLUDED.module_theme_accent,
|
|
preferred_currencies = EXCLUDED.preferred_currencies'
|
|
: 'INSERT INTO ' . $this->table('settings') . ' (
|
|
project_key, owner_sub, baseline_measured_at, baseline_coins_total, daily_cost_amount, daily_cost_currency, report_currency, crypto_currency, display_timezone, fx_max_age_hours, module_theme_mode, module_theme_accent, preferred_currencies
|
|
) VALUES (
|
|
:project_key, :owner_sub, :baseline_measured_at, :baseline_coins_total, :daily_cost_amount, :daily_cost_currency, :report_currency, :crypto_currency, :display_timezone, :fx_max_age_hours, :module_theme_mode, :module_theme_accent, :preferred_currencies
|
|
)
|
|
ON DUPLICATE KEY UPDATE
|
|
baseline_measured_at = VALUES(baseline_measured_at),
|
|
baseline_coins_total = VALUES(baseline_coins_total),
|
|
daily_cost_amount = VALUES(daily_cost_amount),
|
|
daily_cost_currency = VALUES(daily_cost_currency),
|
|
report_currency = VALUES(report_currency),
|
|
crypto_currency = VALUES(crypto_currency),
|
|
display_timezone = VALUES(display_timezone),
|
|
fx_max_age_hours = VALUES(fx_max_age_hours),
|
|
module_theme_mode = VALUES(module_theme_mode),
|
|
module_theme_accent = VALUES(module_theme_accent),
|
|
preferred_currencies = VALUES(preferred_currencies)'
|
|
);
|
|
$stmt->execute([
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'baseline_measured_at' => $settings['baseline_measured_at'],
|
|
'baseline_coins_total' => $settings['baseline_coins_total'],
|
|
'daily_cost_amount' => $settings['daily_cost_amount'],
|
|
'daily_cost_currency' => $settings['daily_cost_currency'],
|
|
'report_currency' => $settings['report_currency'] ?? 'EUR',
|
|
'crypto_currency' => $settings['crypto_currency'] ?? 'DOGE',
|
|
'display_timezone' => $settings['display_timezone'] ?? 'Europe/Berlin',
|
|
'fx_max_age_hours' => $settings['fx_max_age_hours'] ?? 3,
|
|
'module_theme_mode' => $settings['module_theme_mode'] ?? 'inherit',
|
|
'module_theme_accent' => $settings['module_theme_accent'] ?? 'teal',
|
|
'preferred_currencies' => json_encode($settings['preferred_currencies'] ?? [], JSON_UNESCAPED_UNICODE),
|
|
]);
|
|
}
|
|
|
|
public function listCurrencies(): array
|
|
{
|
|
$this->debug?->add('db.listCurrencies.start');
|
|
$stmt = $this->pdo->query(
|
|
'SELECT * FROM ' . $this->table('currencies') . ' WHERE ' . ($this->driver === 'pgsql' ? 'is_active = TRUE' : 'is_active = 1') . ' ORDER BY sort_order ASC, code ASC'
|
|
);
|
|
$rows = $this->normalizeRows($stmt->fetchAll() ?: []);
|
|
$this->debug?->add('db.listCurrencies.end', ['rows' => count($rows)]);
|
|
return $rows;
|
|
}
|
|
|
|
public function listCurrencyAliases(): array
|
|
{
|
|
$stmt = $this->pdo->query(
|
|
'SELECT
|
|
a.alias_code,
|
|
a.currency_code,
|
|
c.name AS currency_name,
|
|
a.created_at
|
|
FROM ' . $this->table('currency_aliases') . ' a
|
|
INNER JOIN ' . $this->table('currencies') . ' c ON c.code = a.currency_code
|
|
ORDER BY a.alias_code ASC'
|
|
);
|
|
return $this->normalizeRows($stmt->fetchAll() ?: []);
|
|
}
|
|
|
|
public function resolveCurrencyCode(string $code): ?array
|
|
{
|
|
$normalizedCode = strtoupper(trim($code));
|
|
if ($normalizedCode === '') {
|
|
return null;
|
|
}
|
|
|
|
$currencyStmt = $this->pdo->prepare('SELECT * FROM ' . $this->table('currencies') . ' WHERE code = :code LIMIT 1');
|
|
$currencyStmt->execute(['code' => $normalizedCode]);
|
|
$currency = $currencyStmt->fetch();
|
|
if (is_array($currency)) {
|
|
return [
|
|
'input_code' => $normalizedCode,
|
|
'code' => $normalizedCode,
|
|
'matched_via' => 'code',
|
|
'currency' => $this->normalizeRow($currency),
|
|
];
|
|
}
|
|
|
|
if (!$this->tableExists('currency_aliases')) {
|
|
return null;
|
|
}
|
|
|
|
$aliasStmt = $this->pdo->prepare(
|
|
'SELECT
|
|
a.alias_code,
|
|
a.currency_code,
|
|
c.name AS currency_name,
|
|
c.symbol AS currency_symbol,
|
|
c.is_active,
|
|
c.sort_order
|
|
FROM ' . $this->table('currency_aliases') . ' a
|
|
INNER JOIN ' . $this->table('currencies') . ' c ON c.code = a.currency_code
|
|
WHERE a.alias_code = :alias_code
|
|
LIMIT 1'
|
|
);
|
|
$aliasStmt->execute(['alias_code' => $normalizedCode]);
|
|
$alias = $aliasStmt->fetch();
|
|
if (!is_array($alias)) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'input_code' => $normalizedCode,
|
|
'code' => (string) $alias['currency_code'],
|
|
'matched_via' => 'alias',
|
|
'alias_code' => (string) $alias['alias_code'],
|
|
'currency' => $this->normalizeRow([
|
|
'code' => $alias['currency_code'],
|
|
'name' => $alias['currency_name'],
|
|
'symbol' => $alias['currency_symbol'] ?? null,
|
|
'is_active' => $alias['is_active'] ?? 1,
|
|
'sort_order' => $alias['sort_order'] ?? 1000,
|
|
]),
|
|
];
|
|
}
|
|
|
|
public function saveCurrencyAlias(string $aliasCode, string $currencyCode): array
|
|
{
|
|
$normalizedAlias = strtoupper(trim($aliasCode));
|
|
$normalizedCurrency = strtoupper(trim($currencyCode));
|
|
|
|
$stmt = $this->pdo->prepare($this->driver === 'pgsql'
|
|
? 'INSERT INTO ' . $this->table('currency_aliases') . ' (alias_code, currency_code)
|
|
VALUES (:alias_code, :currency_code)
|
|
ON CONFLICT (alias_code) DO UPDATE SET currency_code = EXCLUDED.currency_code
|
|
RETURNING *'
|
|
: 'INSERT INTO ' . $this->table('currency_aliases') . ' (alias_code, currency_code)
|
|
VALUES (:alias_code, :currency_code)
|
|
ON DUPLICATE KEY UPDATE currency_code = VALUES(currency_code)'
|
|
);
|
|
$stmt->execute([
|
|
'alias_code' => $normalizedAlias,
|
|
'currency_code' => $normalizedCurrency,
|
|
]);
|
|
|
|
if ($this->driver === 'pgsql') {
|
|
$row = $stmt->fetch();
|
|
return is_array($row) ? $this->normalizeRow($row) : ['alias_code' => $normalizedAlias, 'currency_code' => $normalizedCurrency];
|
|
}
|
|
|
|
return ['alias_code' => $normalizedAlias, 'currency_code' => $normalizedCurrency];
|
|
}
|
|
|
|
public function saveCurrency(array $currency): void
|
|
{
|
|
$this->debug?->add('db.saveCurrency.start', ['code' => $currency['code'] ?? null]);
|
|
$stmt = $this->pdo->prepare($this->driver === 'pgsql'
|
|
? 'INSERT INTO ' . $this->table('currencies') . ' (code, name, symbol, is_active, is_crypto, sort_order)
|
|
VALUES (:code, :name, :symbol, :is_active, :is_crypto, :sort_order)
|
|
ON CONFLICT (code) DO UPDATE SET
|
|
name = EXCLUDED.name,
|
|
symbol = EXCLUDED.symbol,
|
|
is_active = EXCLUDED.is_active,
|
|
is_crypto = EXCLUDED.is_crypto,
|
|
sort_order = EXCLUDED.sort_order'
|
|
: 'INSERT INTO ' . $this->table('currencies') . ' (code, name, symbol, is_active, is_crypto, sort_order)
|
|
VALUES (:code, :name, :symbol, :is_active, :is_crypto, :sort_order)
|
|
ON DUPLICATE KEY UPDATE
|
|
name = VALUES(name),
|
|
symbol = VALUES(symbol),
|
|
is_active = VALUES(is_active),
|
|
is_crypto = VALUES(is_crypto),
|
|
sort_order = VALUES(sort_order)'
|
|
);
|
|
$stmt->execute([
|
|
'code' => $currency['code'],
|
|
'name' => $currency['name'],
|
|
'symbol' => $currency['symbol'],
|
|
'is_active' => $currency['is_active'],
|
|
'is_crypto' => $currency['is_crypto'] ?? 0,
|
|
'sort_order' => $currency['sort_order'],
|
|
]);
|
|
$this->debug?->add('db.saveCurrency.end', ['code' => $currency['code'] ?? null]);
|
|
}
|
|
|
|
public function saveCurrencies(array $currencies): int
|
|
{
|
|
if ($currencies === []) {
|
|
return 0;
|
|
}
|
|
|
|
$this->debug?->add('db.saveCurrencies.start', ['count' => count($currencies)]);
|
|
|
|
$statement = $this->pdo->prepare($this->driver === 'pgsql'
|
|
? 'INSERT INTO ' . $this->table('currencies') . ' (code, name, symbol, is_active, is_crypto, sort_order)
|
|
VALUES (:code, :name, :symbol, :is_active, :is_crypto, :sort_order)
|
|
ON CONFLICT (code) DO UPDATE SET
|
|
name = EXCLUDED.name,
|
|
symbol = EXCLUDED.symbol,
|
|
is_active = EXCLUDED.is_active,
|
|
is_crypto = EXCLUDED.is_crypto,
|
|
sort_order = EXCLUDED.sort_order'
|
|
: 'INSERT INTO ' . $this->table('currencies') . ' (code, name, symbol, is_active, is_crypto, sort_order)
|
|
VALUES (:code, :name, :symbol, :is_active, :is_crypto, :sort_order)
|
|
ON DUPLICATE KEY UPDATE
|
|
name = VALUES(name),
|
|
symbol = VALUES(symbol),
|
|
is_active = VALUES(is_active),
|
|
is_crypto = VALUES(is_crypto),
|
|
sort_order = VALUES(sort_order)'
|
|
);
|
|
|
|
$count = 0;
|
|
$startedTransaction = false;
|
|
if (!$this->pdo->inTransaction()) {
|
|
$this->pdo->beginTransaction();
|
|
$startedTransaction = true;
|
|
}
|
|
|
|
try {
|
|
foreach ($currencies as $currency) {
|
|
if (!is_array($currency) || empty($currency['code'])) {
|
|
continue;
|
|
}
|
|
|
|
$statement->execute([
|
|
'code' => $currency['code'],
|
|
'name' => $currency['name'] ?? $currency['code'],
|
|
'symbol' => $currency['symbol'] ?? $currency['code'],
|
|
'is_active' => $currency['is_active'] ?? 1,
|
|
'is_crypto' => $currency['is_crypto'] ?? 0,
|
|
'sort_order' => $currency['sort_order'] ?? 1000,
|
|
]);
|
|
$count++;
|
|
}
|
|
|
|
if ($startedTransaction) {
|
|
$this->pdo->commit();
|
|
}
|
|
$this->debug?->add('db.saveCurrencies.end', ['count' => $count]);
|
|
} catch (\Throwable $exception) {
|
|
if ($startedTransaction && $this->pdo->inTransaction()) {
|
|
$this->pdo->rollBack();
|
|
}
|
|
$this->debug?->add('db.saveCurrencies.error', ['message' => $exception->getMessage()]);
|
|
throw $exception;
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
public function ensureCurrencyCode(string $code, ?string $name = null): void
|
|
{
|
|
$normalizedCode = strtoupper(trim($code));
|
|
if ($normalizedCode === '') {
|
|
return;
|
|
}
|
|
|
|
$this->saveCurrency([
|
|
'code' => substr($normalizedCode, 0, 10),
|
|
'name' => $name !== null && trim($name) !== '' ? trim($name) : $normalizedCode,
|
|
'symbol' => substr($normalizedCode, 0, 8),
|
|
'is_active' => 1,
|
|
'is_crypto' => $this->isCryptoCode($normalizedCode) ? 1 : 0,
|
|
'sort_order' => 1000,
|
|
]);
|
|
}
|
|
|
|
public function tableExists(string $logicalName): bool
|
|
{
|
|
$tableName = $this->table($logicalName);
|
|
$schemaCondition = $this->driver === 'pgsql'
|
|
? 'table_schema = current_schema()'
|
|
: 'table_schema = DATABASE()';
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT table_name
|
|
FROM information_schema.tables
|
|
WHERE ' . $schemaCondition . ' AND table_name = :table_name
|
|
LIMIT 1'
|
|
);
|
|
$stmt->execute(['table_name' => $tableName]);
|
|
return (bool) $stmt->fetchColumn();
|
|
}
|
|
|
|
public function listCostPlans(string $projectKey): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT * FROM ' . $this->table('cost_plans') . ' WHERE project_key = :project_key AND owner_sub = :owner_sub ORDER BY starts_at DESC, id DESC'
|
|
);
|
|
$stmt->execute(['project_key' => $projectKey, 'owner_sub' => $this->ownerSub]);
|
|
return $this->normalizeRows($stmt->fetchAll() ?: []);
|
|
}
|
|
|
|
public function saveCostPlan(string $projectKey, array $payload): array
|
|
{
|
|
if ($this->driver === 'pgsql') {
|
|
$stmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('cost_plans') . ' (
|
|
project_key, owner_sub, label, starts_at, runtime_months, mining_speed_value, mining_speed_unit,
|
|
bonus_speed_value, bonus_speed_unit, auto_renew, base_price_amount, payment_type, total_cost_amount, currency, note, is_active
|
|
) VALUES (
|
|
:project_key, :owner_sub, :label, :starts_at, :runtime_months, :mining_speed_value, :mining_speed_unit,
|
|
:bonus_speed_value, :bonus_speed_unit, :auto_renew, :base_price_amount, :payment_type, :total_cost_amount, :currency, :note, :is_active
|
|
)
|
|
RETURNING *'
|
|
);
|
|
$stmt->execute($this->normalizeInsertPayload($projectKey, $payload));
|
|
return $this->normalizeRow($stmt->fetch() ?: []);
|
|
}
|
|
|
|
$stmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('cost_plans') . ' (
|
|
project_key, owner_sub, label, starts_at, runtime_months, mining_speed_value, mining_speed_unit,
|
|
bonus_speed_value, bonus_speed_unit, auto_renew, base_price_amount, payment_type, total_cost_amount, currency, note, is_active
|
|
) VALUES (
|
|
:project_key, :owner_sub, :label, :starts_at, :runtime_months, :mining_speed_value, :mining_speed_unit,
|
|
:bonus_speed_value, :bonus_speed_unit, :auto_renew, :base_price_amount, :payment_type, :total_cost_amount, :currency, :note, :is_active
|
|
)'
|
|
);
|
|
$stmt->execute($this->normalizeInsertPayload($projectKey, $payload));
|
|
|
|
$id = (int) $this->pdo->lastInsertId();
|
|
$fetch = $this->pdo->prepare('SELECT * FROM ' . $this->table('cost_plans') . ' WHERE id = :id LIMIT 1');
|
|
$fetch->execute(['id' => $id]);
|
|
return $this->normalizeRow($fetch->fetch() ?: []);
|
|
}
|
|
|
|
public function listMeasurements(string $projectKey, int $limit = 200): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT * FROM ' . $this->table('measurements') . '
|
|
WHERE project_key = :project_key AND owner_sub = :owner_sub
|
|
ORDER BY measured_at ASC
|
|
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 listRecentMeasurements(string $projectKey, int $limit = 200): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT * FROM ' . $this->table('measurements') . '
|
|
WHERE project_key = :project_key AND owner_sub = :owner_sub
|
|
ORDER BY measured_at 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();
|
|
|
|
$rows = $this->normalizeRows($stmt->fetchAll() ?: []);
|
|
return array_reverse($rows);
|
|
}
|
|
|
|
public function listAllMeasurements(string $projectKey): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT * FROM ' . $this->table('measurements') . '
|
|
WHERE project_key = :project_key AND owner_sub = :owner_sub
|
|
ORDER BY measured_at ASC'
|
|
);
|
|
$stmt->execute([
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
]);
|
|
return $this->normalizeRows($stmt->fetchAll() ?: []);
|
|
}
|
|
|
|
public function createMeasurement(string $projectKey, array $payload): array
|
|
{
|
|
$this->debug?->add('db.createMeasurement.start', [
|
|
'project_key' => $projectKey,
|
|
'measured_at' => $payload['measured_at'] ?? null,
|
|
'price_currency' => $payload['price_currency'] ?? null,
|
|
'fx_fetch_id' => $payload['fx_fetch_id'] ?? null,
|
|
]);
|
|
$params = [
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'measured_at' => $payload['measured_at'],
|
|
'coins_total' => $payload['coins_total'],
|
|
'price_per_coin' => $payload['price_per_coin'],
|
|
'price_currency' => $payload['price_currency'],
|
|
'fx_fetch_id' => $payload['fx_fetch_id'] ?? null,
|
|
'note' => $payload['note'],
|
|
'source' => $payload['source'],
|
|
'image_path' => $payload['image_path'],
|
|
'ocr_raw_text' => $payload['ocr_raw_text'],
|
|
'ocr_confidence' => $payload['ocr_confidence'],
|
|
'ocr_flags' => $payload['ocr_flags'] === null ? null : json_encode($payload['ocr_flags'], JSON_UNESCAPED_UNICODE),
|
|
];
|
|
|
|
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,
|
|
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,
|
|
:source, :image_path, :ocr_raw_text, :ocr_confidence, CAST(:ocr_flags AS jsonb)
|
|
)
|
|
RETURNING *'
|
|
);
|
|
$stmt->execute($params);
|
|
$row = $this->normalizeRow($stmt->fetch() ?: []);
|
|
$this->debug?->add('db.createMeasurement.end', ['id' => $row['id'] ?? null]);
|
|
return $row;
|
|
}
|
|
|
|
$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,
|
|
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,
|
|
: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('measurements') . ' WHERE id = :id LIMIT 1');
|
|
$fetch->execute(['id' => $id]);
|
|
$row = $this->normalizeRow($fetch->fetch() ?: []);
|
|
$this->debug?->add('db.createMeasurement.end', ['id' => $row['id'] ?? null]);
|
|
return $row;
|
|
}
|
|
|
|
public function createMeasurementIfNotExists(string $projectKey, array $payload): ?array
|
|
{
|
|
try {
|
|
return $this->createMeasurement($projectKey, $payload);
|
|
} catch (\PDOException $exception) {
|
|
$sqlState = (string) ($exception->getCode() ?? '');
|
|
if (in_array($sqlState, ['23000', '23505'], true)) {
|
|
return null;
|
|
}
|
|
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
public function setMeasurementFxFetchId(string $projectKey, int $measurementId, ?int $fxFetchId): ?array
|
|
{
|
|
if ($measurementId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$stmt = $this->pdo->prepare(
|
|
'UPDATE ' . $this->table('measurements') . '
|
|
SET fx_fetch_id = :fx_fetch_id
|
|
WHERE project_key = :project_key AND owner_sub = :owner_sub AND id = :id'
|
|
);
|
|
$stmt->execute([
|
|
'fx_fetch_id' => $fxFetchId,
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'id' => $measurementId,
|
|
]);
|
|
|
|
$fetch = $this->pdo->prepare(
|
|
'SELECT * FROM ' . $this->table('measurements') . '
|
|
WHERE project_key = :project_key AND owner_sub = :owner_sub AND id = :id LIMIT 1'
|
|
);
|
|
$fetch->execute([
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'id' => $measurementId,
|
|
]);
|
|
|
|
$row = $fetch->fetch();
|
|
return is_array($row) ? $this->normalizeRow($row) : null;
|
|
}
|
|
|
|
public function deleteMeasurement(string $projectKey, int $measurementId): void
|
|
{
|
|
if ($measurementId <= 0) {
|
|
return;
|
|
}
|
|
|
|
$deleteRates = $this->pdo->prepare(
|
|
'DELETE FROM ' . $this->table('measurement_rates') . '
|
|
WHERE measurement_id = :measurement_id AND owner_sub = :owner_sub'
|
|
);
|
|
$deleteRates->execute([
|
|
'measurement_id' => $measurementId,
|
|
'owner_sub' => $this->ownerSub,
|
|
]);
|
|
|
|
$deleteMeasurement = $this->pdo->prepare(
|
|
'DELETE FROM ' . $this->table('measurements') . '
|
|
WHERE id = :id AND project_key = :project_key AND owner_sub = :owner_sub'
|
|
);
|
|
$deleteMeasurement->execute([
|
|
'id' => $measurementId,
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
]);
|
|
}
|
|
|
|
public function replaceMeasurementRates(int $measurementId, string $projectKey, array $rates): void
|
|
{
|
|
$delete = $this->pdo->prepare('DELETE FROM ' . $this->table('measurement_rates') . ' WHERE measurement_id = :measurement_id AND owner_sub = :owner_sub');
|
|
$delete->execute(['measurement_id' => $measurementId, 'owner_sub' => $this->ownerSub]);
|
|
|
|
if ($rates === []) {
|
|
return;
|
|
}
|
|
|
|
$insert = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('measurement_rates') . ' (
|
|
measurement_id, project_key, owner_sub, base_currency, quote_currency, rate, provider
|
|
) VALUES (
|
|
:measurement_id, :project_key, :owner_sub, :base_currency, :quote_currency, :rate, :provider
|
|
)'
|
|
);
|
|
|
|
foreach ($rates as $rate) {
|
|
$insert->execute([
|
|
'measurement_id' => $measurementId,
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'base_currency' => strtoupper((string) $rate['base_currency']),
|
|
'quote_currency' => strtoupper((string) $rate['quote_currency']),
|
|
'rate' => $rate['rate'],
|
|
'provider' => $rate['provider'] ?? 'derived',
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function listMeasurementRates(string $projectKey): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT * FROM ' . $this->table('measurement_rates') . ' WHERE project_key = :project_key AND owner_sub = :owner_sub ORDER BY measurement_id ASC, base_currency ASC, quote_currency ASC'
|
|
);
|
|
$stmt->execute(['project_key' => $projectKey, 'owner_sub' => $this->ownerSub]);
|
|
return $this->normalizeRows($stmt->fetchAll() ?: []);
|
|
}
|
|
|
|
public function listPayouts(string $projectKey): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT * FROM ' . $this->table('payouts') . ' WHERE project_key = :project_key AND owner_sub = :owner_sub ORDER BY payout_at ASC, id ASC'
|
|
);
|
|
$stmt->execute(['project_key' => $projectKey, 'owner_sub' => $this->ownerSub]);
|
|
return $this->normalizeRows($stmt->fetchAll() ?: []);
|
|
}
|
|
|
|
public function savePayout(string $projectKey, array $payload): array
|
|
{
|
|
if ($this->driver === 'pgsql') {
|
|
$stmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('payouts') . ' (
|
|
project_key, owner_sub, payout_at, coins_amount, payout_currency, note
|
|
) VALUES (
|
|
:project_key, :owner_sub, :payout_at, :coins_amount, :payout_currency, :note
|
|
)
|
|
RETURNING *'
|
|
);
|
|
$stmt->execute([
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'payout_at' => $payload['payout_at'],
|
|
'coins_amount' => $payload['coins_amount'],
|
|
'payout_currency' => $payload['payout_currency'],
|
|
'note' => $payload['note'],
|
|
]);
|
|
return $this->normalizeRow($stmt->fetch() ?: []);
|
|
}
|
|
|
|
$stmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('payouts') . ' (
|
|
project_key, owner_sub, payout_at, coins_amount, payout_currency, note
|
|
) VALUES (
|
|
:project_key, :owner_sub, :payout_at, :coins_amount, :payout_currency, :note
|
|
)'
|
|
);
|
|
$stmt->execute([
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'payout_at' => $payload['payout_at'],
|
|
'coins_amount' => $payload['coins_amount'],
|
|
'payout_currency' => $payload['payout_currency'],
|
|
'note' => $payload['note'],
|
|
]);
|
|
|
|
$id = (int) $this->pdo->lastInsertId();
|
|
$fetch = $this->pdo->prepare('SELECT * FROM ' . $this->table('payouts') . ' WHERE id = :id LIMIT 1');
|
|
$fetch->execute(['id' => $id]);
|
|
return $this->normalizeRow($fetch->fetch() ?: []);
|
|
}
|
|
|
|
public function listTargets(string $projectKey): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT * FROM ' . $this->table('targets') . ' WHERE project_key = :project_key AND owner_sub = :owner_sub ORDER BY sort_order ASC, id ASC'
|
|
);
|
|
$stmt->execute(['project_key' => $projectKey, 'owner_sub' => $this->ownerSub]);
|
|
return $this->normalizeRows($stmt->fetchAll() ?: []);
|
|
}
|
|
|
|
public function saveTarget(string $projectKey, array $payload): array
|
|
{
|
|
$stmt = $this->pdo->prepare($this->driver === 'pgsql'
|
|
? 'INSERT INTO ' . $this->table('targets') . ' (project_key, owner_sub, label, target_amount_fiat, currency, miner_offer_id, is_active, sort_order)
|
|
VALUES (:project_key, :owner_sub, :label, :target_amount_fiat, :currency, :miner_offer_id, :is_active, :sort_order)
|
|
ON CONFLICT (project_key, owner_sub, label) DO UPDATE SET
|
|
target_amount_fiat = EXCLUDED.target_amount_fiat,
|
|
currency = EXCLUDED.currency,
|
|
miner_offer_id = EXCLUDED.miner_offer_id,
|
|
is_active = EXCLUDED.is_active,
|
|
sort_order = EXCLUDED.sort_order'
|
|
: 'INSERT INTO ' . $this->table('targets') . ' (project_key, owner_sub, label, target_amount_fiat, currency, miner_offer_id, is_active, sort_order)
|
|
VALUES (:project_key, :owner_sub, :label, :target_amount_fiat, :currency, :miner_offer_id, :is_active, :sort_order)
|
|
ON DUPLICATE KEY UPDATE
|
|
target_amount_fiat = VALUES(target_amount_fiat),
|
|
currency = VALUES(currency),
|
|
miner_offer_id = VALUES(miner_offer_id),
|
|
is_active = VALUES(is_active),
|
|
sort_order = VALUES(sort_order)'
|
|
);
|
|
$stmt->execute([
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'label' => $payload['label'],
|
|
'target_amount_fiat' => $payload['target_amount_fiat'],
|
|
'currency' => $payload['currency'],
|
|
'miner_offer_id' => $payload['miner_offer_id'] ?? null,
|
|
'is_active' => $payload['is_active'],
|
|
'sort_order' => $payload['sort_order'],
|
|
]);
|
|
$fetch = $this->pdo->prepare('SELECT * FROM ' . $this->table('targets') . ' WHERE project_key = :project_key AND owner_sub = :owner_sub AND label = :label LIMIT 1');
|
|
$fetch->execute(['project_key' => $projectKey, 'owner_sub' => $this->ownerSub, 'label' => $payload['label']]);
|
|
return $this->normalizeRow($fetch->fetch() ?: []);
|
|
}
|
|
|
|
public function updateTarget(string $projectKey, int $targetId, array $payload): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'UPDATE ' . $this->table('targets') . '
|
|
SET label = :label, target_amount_fiat = :target_amount_fiat, currency = :currency, miner_offer_id = :miner_offer_id,
|
|
is_active = :is_active, sort_order = :sort_order
|
|
WHERE id = :id AND project_key = :project_key AND owner_sub = :owner_sub'
|
|
);
|
|
$stmt->execute([
|
|
'id' => $targetId,
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'label' => $payload['label'],
|
|
'target_amount_fiat' => $payload['target_amount_fiat'],
|
|
'currency' => $payload['currency'],
|
|
'miner_offer_id' => $payload['miner_offer_id'] ?? null,
|
|
'is_active' => $payload['is_active'],
|
|
'sort_order' => $payload['sort_order'],
|
|
]);
|
|
$fetch = $this->pdo->prepare('SELECT * FROM ' . $this->table('targets') . ' WHERE id = :id AND owner_sub = :owner_sub LIMIT 1');
|
|
$fetch->execute(['id' => $targetId, 'owner_sub' => $this->ownerSub]);
|
|
return $this->normalizeRow($fetch->fetch() ?: []);
|
|
}
|
|
|
|
public function deleteTarget(string $projectKey, int $targetId): void
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'DELETE FROM ' . $this->table('targets') . ' WHERE id = :id AND project_key = :project_key AND owner_sub = :owner_sub'
|
|
);
|
|
$stmt->execute([
|
|
'id' => $targetId,
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
]);
|
|
}
|
|
|
|
public function listDashboards(string $projectKey): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT * FROM ' . $this->table('dashboard_definitions') . '
|
|
WHERE project_key = :project_key AND owner_sub = :owner_sub
|
|
ORDER BY id ASC'
|
|
);
|
|
$stmt->execute(['project_key' => $projectKey, 'owner_sub' => $this->ownerSub]);
|
|
return $this->normalizeRows($stmt->fetchAll() ?: []);
|
|
}
|
|
|
|
public function saveDashboard(string $projectKey, array $payload): array
|
|
{
|
|
$stmt = $this->pdo->prepare($this->driver === 'pgsql'
|
|
? 'INSERT INTO ' . $this->table('dashboard_definitions') . ' (
|
|
project_key, owner_sub, name, chart_type, x_field, y_field, aggregation, filters_json, is_active
|
|
) VALUES (
|
|
:project_key, :owner_sub, :name, :chart_type, :x_field, :y_field, :aggregation, CAST(:filters_json AS jsonb), :is_active
|
|
)
|
|
ON CONFLICT (project_key, owner_sub, name) DO UPDATE SET
|
|
chart_type = EXCLUDED.chart_type,
|
|
x_field = EXCLUDED.x_field,
|
|
y_field = EXCLUDED.y_field,
|
|
aggregation = EXCLUDED.aggregation,
|
|
filters_json = EXCLUDED.filters_json,
|
|
is_active = EXCLUDED.is_active'
|
|
: 'INSERT INTO ' . $this->table('dashboard_definitions') . ' (
|
|
project_key, owner_sub, name, chart_type, x_field, y_field, aggregation, filters_json, is_active
|
|
) VALUES (
|
|
:project_key, :owner_sub, :name, :chart_type, :x_field, :y_field, :aggregation, :filters_json, :is_active
|
|
)
|
|
ON DUPLICATE KEY UPDATE
|
|
chart_type = VALUES(chart_type),
|
|
x_field = VALUES(x_field),
|
|
y_field = VALUES(y_field),
|
|
aggregation = VALUES(aggregation),
|
|
filters_json = VALUES(filters_json),
|
|
is_active = VALUES(is_active)'
|
|
);
|
|
$stmt->execute([
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'name' => $payload['name'],
|
|
'chart_type' => $payload['chart_type'],
|
|
'x_field' => $payload['x_field'],
|
|
'y_field' => $payload['y_field'],
|
|
'aggregation' => $payload['aggregation'],
|
|
'filters_json' => json_encode($payload['filters'], JSON_UNESCAPED_UNICODE),
|
|
'is_active' => $payload['is_active'],
|
|
]);
|
|
|
|
$fetch = $this->pdo->prepare('SELECT * FROM ' . $this->table('dashboard_definitions') . ' WHERE project_key = :project_key AND owner_sub = :owner_sub AND name = :name LIMIT 1');
|
|
$fetch->execute(['project_key' => $projectKey, 'owner_sub' => $this->ownerSub, 'name' => $payload['name']]);
|
|
return $this->normalizeRow($fetch->fetch() ?: []);
|
|
}
|
|
|
|
public function listMinerOffers(string $projectKey): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT * FROM ' . $this->table('miner_offers') . ' WHERE project_key = :project_key ORDER BY created_at DESC, id DESC'
|
|
);
|
|
$stmt->execute(['project_key' => $projectKey]);
|
|
return $this->normalizeRows($stmt->fetchAll() ?: []);
|
|
}
|
|
|
|
public function saveMinerOffer(string $projectKey, array $payload): array
|
|
{
|
|
if ($this->driver === 'pgsql') {
|
|
$stmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('miner_offers') . ' (
|
|
project_key, label, runtime_months, mining_speed_value, mining_speed_unit,
|
|
bonus_speed_value, bonus_speed_unit, base_price_amount, base_price_currency,
|
|
payment_type, auto_renew, note, is_active
|
|
) VALUES (
|
|
:project_key, :label, :runtime_months, :mining_speed_value, :mining_speed_unit,
|
|
:bonus_speed_value, :bonus_speed_unit, :base_price_amount, :base_price_currency,
|
|
:payment_type, :auto_renew, :note, :is_active
|
|
)
|
|
RETURNING *'
|
|
);
|
|
$stmt->execute($this->normalizeOfferPayload($projectKey, $payload));
|
|
return $this->normalizeRow($stmt->fetch() ?: []);
|
|
}
|
|
|
|
$stmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('miner_offers') . ' (
|
|
project_key, label, runtime_months, mining_speed_value, mining_speed_unit,
|
|
bonus_speed_value, bonus_speed_unit, base_price_amount, base_price_currency,
|
|
payment_type, auto_renew, note, is_active
|
|
) VALUES (
|
|
:project_key, :label, :runtime_months, :mining_speed_value, :mining_speed_unit,
|
|
:bonus_speed_value, :bonus_speed_unit, :base_price_amount, :base_price_currency,
|
|
:payment_type, :auto_renew, :note, :is_active
|
|
)'
|
|
);
|
|
$stmt->execute($this->normalizeOfferPayload($projectKey, $payload));
|
|
$id = (int) $this->pdo->lastInsertId();
|
|
$fetch = $this->pdo->prepare('SELECT * FROM ' . $this->table('miner_offers') . ' WHERE id = :id LIMIT 1');
|
|
$fetch->execute(['id' => $id]);
|
|
return $this->normalizeRow($fetch->fetch() ?: []);
|
|
}
|
|
|
|
public function listPurchasedMiners(string $projectKey): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT * FROM ' . $this->table('purchased_miners') . ' WHERE project_key = :project_key AND owner_sub = :owner_sub ORDER BY purchased_at DESC, id DESC'
|
|
);
|
|
$stmt->execute(['project_key' => $projectKey, 'owner_sub' => $this->ownerSub]);
|
|
return $this->normalizeRows($stmt->fetchAll() ?: []);
|
|
}
|
|
|
|
public function getPurchasedMiner(string $projectKey, int $minerId): ?array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT * FROM ' . $this->table('purchased_miners') . ' WHERE project_key = :project_key AND owner_sub = :owner_sub AND id = :id LIMIT 1'
|
|
);
|
|
$stmt->execute([
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'id' => $minerId,
|
|
]);
|
|
$row = $stmt->fetch();
|
|
return is_array($row) ? $this->normalizeRow($row) : null;
|
|
}
|
|
|
|
public function purchaseMiner(string $projectKey, int $offerId, array $payload): array
|
|
{
|
|
if ($this->driver === 'pgsql') {
|
|
$stmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('purchased_miners') . ' (
|
|
project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months,
|
|
mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit,
|
|
total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, auto_renew, note, is_active
|
|
) VALUES (
|
|
:project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months,
|
|
:mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit,
|
|
:total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, :auto_renew, :note, :is_active
|
|
)
|
|
RETURNING *'
|
|
);
|
|
$stmt->execute($this->normalizePurchasedPayload($projectKey, $offerId, $payload));
|
|
return $this->normalizeRow($stmt->fetch() ?: []);
|
|
}
|
|
|
|
$stmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('purchased_miners') . ' (
|
|
project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months,
|
|
mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit,
|
|
total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, auto_renew, note, is_active
|
|
) VALUES (
|
|
:project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months,
|
|
:mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit,
|
|
:total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, :auto_renew, :note, :is_active
|
|
)'
|
|
);
|
|
$stmt->execute($this->normalizePurchasedPayload($projectKey, $offerId, $payload));
|
|
$id = (int) $this->pdo->lastInsertId();
|
|
$fetch = $this->pdo->prepare('SELECT * FROM ' . $this->table('purchased_miners') . ' WHERE id = :id LIMIT 1');
|
|
$fetch->execute(['id' => $id]);
|
|
return $this->normalizeRow($fetch->fetch() ?: []);
|
|
}
|
|
|
|
public function restorePurchasedMiner(string $projectKey, array $payload): array
|
|
{
|
|
$params = [
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'miner_offer_id' => $payload['miner_offer_id'] ?? null,
|
|
'purchased_at' => $payload['purchased_at'],
|
|
'label' => $payload['label'],
|
|
'runtime_months' => $payload['runtime_months'] ?? null,
|
|
'mining_speed_value' => $payload['mining_speed_value'] ?? null,
|
|
'mining_speed_unit' => $payload['mining_speed_unit'] ?? null,
|
|
'bonus_speed_value' => $payload['bonus_speed_value'] ?? null,
|
|
'bonus_speed_unit' => $payload['bonus_speed_unit'] ?? null,
|
|
'total_cost_amount' => $payload['total_cost_amount'],
|
|
'currency' => $payload['currency'],
|
|
'usd_reference_amount' => $payload['usd_reference_amount'] ?? null,
|
|
'reference_price_amount' => $payload['reference_price_amount'] ?? null,
|
|
'reference_price_currency' => $payload['reference_price_currency'] ?? null,
|
|
'auto_renew' => $payload['auto_renew'] ?? 0,
|
|
'note' => $payload['note'] ?? null,
|
|
'is_active' => $payload['is_active'] ?? 1,
|
|
];
|
|
|
|
if ($this->driver === 'pgsql') {
|
|
$stmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('purchased_miners') . ' (
|
|
project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months,
|
|
mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit,
|
|
total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, auto_renew, note, is_active
|
|
) VALUES (
|
|
:project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months,
|
|
:mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit,
|
|
:total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, :auto_renew, :note, :is_active
|
|
)
|
|
RETURNING *'
|
|
);
|
|
$stmt->execute($params);
|
|
return $this->normalizeRow($stmt->fetch() ?: []);
|
|
}
|
|
|
|
$stmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('purchased_miners') . ' (
|
|
project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months,
|
|
mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit,
|
|
total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, auto_renew, note, is_active
|
|
) VALUES (
|
|
:project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months,
|
|
:mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit,
|
|
:total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, :auto_renew, :note, :is_active
|
|
)'
|
|
);
|
|
$stmt->execute($params);
|
|
$id = (int) $this->pdo->lastInsertId();
|
|
$fetch = $this->pdo->prepare('SELECT * FROM ' . $this->table('purchased_miners') . ' WHERE id = :id LIMIT 1');
|
|
$fetch->execute(['id' => $id]);
|
|
return $this->normalizeRow($fetch->fetch() ?: []);
|
|
}
|
|
|
|
public function updatePurchasedMinerAutoRenew(string $projectKey, int $minerId, bool $autoRenew): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'UPDATE ' . $this->table('purchased_miners') . '
|
|
SET auto_renew = :auto_renew
|
|
WHERE project_key = :project_key AND owner_sub = :owner_sub AND id = :id'
|
|
);
|
|
$stmt->execute([
|
|
'auto_renew' => $autoRenew ? 1 : 0,
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'id' => $minerId,
|
|
]);
|
|
|
|
$fetch = $this->pdo->prepare(
|
|
'SELECT * FROM ' . $this->table('purchased_miners') . ' WHERE project_key = :project_key AND owner_sub = :owner_sub AND id = :id LIMIT 1'
|
|
);
|
|
$fetch->execute([
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'id' => $minerId,
|
|
]);
|
|
return $this->normalizeRow($fetch->fetch() ?: []);
|
|
}
|
|
|
|
public function getMinerOffer(string $projectKey, int $offerId): ?array
|
|
{
|
|
$stmt = $this->pdo->prepare('SELECT * FROM ' . $this->table('miner_offers') . ' WHERE project_key = :project_key AND id = :id LIMIT 1');
|
|
$stmt->execute(['project_key' => $projectKey, 'id' => $offerId]);
|
|
$row = $stmt->fetch();
|
|
return is_array($row) ? $this->normalizeRow($row) : null;
|
|
}
|
|
|
|
public function getLatestFxRate(string $baseCurrency, string $targetCurrency): ?array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT
|
|
r.id,
|
|
f.id AS fetch_id,
|
|
f.base_currency,
|
|
r.currency_code AS target_currency,
|
|
r.current_value AS rate,
|
|
f.rate_date,
|
|
f.provider,
|
|
f.fetched_at
|
|
FROM ' . $this->table('fx_rates') . ' r
|
|
INNER JOIN ' . $this->table('fx_fetches') . ' f ON f.id = r.fetch_id
|
|
WHERE f.base_currency = :base_currency AND r.currency_code = :target_currency
|
|
ORDER BY f.rate_date DESC, f.fetched_at DESC, r.id DESC
|
|
LIMIT 1'
|
|
);
|
|
$stmt->execute([
|
|
'base_currency' => strtoupper($baseCurrency),
|
|
'target_currency' => strtoupper($targetCurrency),
|
|
]);
|
|
$row = $stmt->fetch();
|
|
return is_array($row) ? $this->normalizeRow($row) : null;
|
|
}
|
|
|
|
public function getLatestFxFetch(?string $baseCurrency = null): ?array
|
|
{
|
|
$sql = 'SELECT id, provider, base_currency, rate_date, fetched_at
|
|
FROM ' . $this->table('fx_fetches');
|
|
$params = [];
|
|
|
|
if ($baseCurrency !== null && trim($baseCurrency) !== '') {
|
|
$sql .= ' WHERE base_currency = :base_currency';
|
|
$params['base_currency'] = strtoupper(trim($baseCurrency));
|
|
}
|
|
|
|
$sql .= ' ORDER BY fetched_at DESC, id DESC LIMIT 1';
|
|
$stmt = $this->pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
$row = $stmt->fetch();
|
|
return is_array($row) ? $this->normalizeRow($row) : null;
|
|
}
|
|
|
|
public function listFxRates(int $limit = 30): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT
|
|
r.id,
|
|
r.fetch_id,
|
|
f.base_currency,
|
|
r.currency_code AS target_currency,
|
|
r.current_value AS rate,
|
|
f.rate_date,
|
|
f.provider,
|
|
f.fetched_at
|
|
FROM ' . $this->table('fx_rates') . ' r
|
|
INNER JOIN ' . $this->table('fx_fetches') . ' f ON f.id = r.fetch_id
|
|
WHERE f.id IN (
|
|
SELECT id
|
|
FROM ' . $this->table('fx_fetches') . '
|
|
ORDER BY fetched_at DESC, id DESC
|
|
LIMIT :limit
|
|
)
|
|
ORDER BY f.fetched_at DESC, f.id DESC, r.currency_code ASC'
|
|
);
|
|
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
|
$stmt->execute();
|
|
return $this->normalizeRows($stmt->fetchAll() ?: []);
|
|
}
|
|
|
|
public function listAllFxRates(): array
|
|
{
|
|
$stmt = $this->pdo->query(
|
|
'SELECT
|
|
r.id,
|
|
r.fetch_id,
|
|
f.base_currency,
|
|
r.currency_code AS target_currency,
|
|
r.current_value AS rate,
|
|
f.rate_date,
|
|
f.provider,
|
|
f.fetched_at
|
|
FROM ' . $this->table('fx_rates') . ' r
|
|
INNER JOIN ' . $this->table('fx_fetches') . ' f ON f.id = r.fetch_id
|
|
ORDER BY f.fetched_at ASC, f.id ASC, r.currency_code ASC'
|
|
);
|
|
return $this->normalizeRows($stmt->fetchAll() ?: []);
|
|
}
|
|
|
|
public function restoreFxFetch(string $baseCurrency, string $provider, string $rateDate, ?string $fetchedAt, array $rates): array
|
|
{
|
|
$baseCurrency = strtoupper(trim($baseCurrency));
|
|
$provider = trim($provider) !== '' ? trim($provider) : 'currencyapi';
|
|
$fetchedAt = trim((string) $fetchedAt) !== '' ? (string) $fetchedAt : $this->currentUtcTimestamp();
|
|
|
|
if ($rates === []) {
|
|
return ['fetch' => null, 'rates' => []];
|
|
}
|
|
|
|
$currenciesToEnsure = [
|
|
[
|
|
'code' => substr($baseCurrency, 0, 10),
|
|
'name' => $baseCurrency,
|
|
'symbol' => substr($baseCurrency, 0, 8),
|
|
'is_active' => 1,
|
|
'is_crypto' => $this->isCryptoCode($baseCurrency) ? 1 : 0,
|
|
'sort_order' => 1000,
|
|
],
|
|
];
|
|
foreach (array_keys($rates) as $currencyCode) {
|
|
$normalizedCurrencyCode = strtoupper(trim((string) $currencyCode));
|
|
if ($normalizedCurrencyCode === '') {
|
|
continue;
|
|
}
|
|
$currenciesToEnsure[] = [
|
|
'code' => substr($normalizedCurrencyCode, 0, 10),
|
|
'name' => $normalizedCurrencyCode,
|
|
'symbol' => substr($normalizedCurrencyCode, 0, 8),
|
|
'is_active' => 1,
|
|
'is_crypto' => $this->isCryptoCode($normalizedCurrencyCode) ? 1 : 0,
|
|
'sort_order' => 1000,
|
|
];
|
|
}
|
|
$this->saveCurrencies($currenciesToEnsure);
|
|
|
|
if ($this->driver === 'pgsql') {
|
|
$fetchStmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('fx_fetches') . ' (
|
|
provider, base_currency, rate_date, fetched_at
|
|
) VALUES (
|
|
:provider, :base_currency, :rate_date, :fetched_at
|
|
)
|
|
RETURNING *'
|
|
);
|
|
$fetchStmt->execute([
|
|
'provider' => $provider,
|
|
'base_currency' => $baseCurrency,
|
|
'rate_date' => $rateDate,
|
|
'fetched_at' => $fetchedAt,
|
|
]);
|
|
$fetch = $this->normalizeRow($fetchStmt->fetch() ?: []);
|
|
} else {
|
|
$fetchStmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('fx_fetches') . ' (
|
|
provider, base_currency, rate_date, fetched_at
|
|
) VALUES (
|
|
:provider, :base_currency, :rate_date, :fetched_at
|
|
)'
|
|
);
|
|
$fetchStmt->execute([
|
|
'provider' => $provider,
|
|
'base_currency' => $baseCurrency,
|
|
'rate_date' => $rateDate,
|
|
'fetched_at' => $fetchedAt,
|
|
]);
|
|
$fetchId = (int) $this->pdo->lastInsertId();
|
|
$fetchLookup = $this->pdo->prepare('SELECT * FROM ' . $this->table('fx_fetches') . ' WHERE id = :id LIMIT 1');
|
|
$fetchLookup->execute(['id' => $fetchId]);
|
|
$fetch = $this->normalizeRow($fetchLookup->fetch() ?: []);
|
|
}
|
|
|
|
$placeholders = [];
|
|
$params = ['fetch_id' => $fetch['id']];
|
|
$savedRates = [];
|
|
$index = 0;
|
|
foreach ($rates as $currencyCode => $rate) {
|
|
if (!is_numeric($rate)) {
|
|
continue;
|
|
}
|
|
$codeKey = 'currency_code_' . $index;
|
|
$valueKey = 'current_value_' . $index;
|
|
$targetCurrency = strtoupper(trim((string) $currencyCode));
|
|
if ($targetCurrency === '') {
|
|
continue;
|
|
}
|
|
$placeholders[] = "(:fetch_id, :{$codeKey}, :{$valueKey})";
|
|
$params[$codeKey] = $targetCurrency;
|
|
$params[$valueKey] = (float) $rate;
|
|
$savedRates[] = [
|
|
'fetch_id' => $fetch['id'],
|
|
'base_currency' => $baseCurrency,
|
|
'target_currency' => $targetCurrency,
|
|
'rate' => (float) $rate,
|
|
'rate_date' => $rateDate,
|
|
'provider' => $provider,
|
|
'fetched_at' => $fetch['fetched_at'] ?? $fetchedAt,
|
|
];
|
|
$index++;
|
|
}
|
|
|
|
if ($placeholders !== []) {
|
|
$insert = $this->pdo->prepare('INSERT INTO ' . $this->table('fx_rates') . ' (fetch_id, currency_code, current_value) VALUES ' . implode(', ', $placeholders));
|
|
$insert->execute($params);
|
|
}
|
|
|
|
return [
|
|
'fetch' => $fetch,
|
|
'rates' => $savedRates,
|
|
];
|
|
}
|
|
|
|
public function getLatestMeasurementRate(string $baseCurrency, string $targetCurrency): ?array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
'SELECT
|
|
id,
|
|
measurement_id,
|
|
base_currency,
|
|
quote_currency AS target_currency,
|
|
rate,
|
|
provider,
|
|
created_at
|
|
FROM ' . $this->table('measurement_rates') . '
|
|
WHERE owner_sub = :owner_sub AND base_currency = :base_currency AND quote_currency = :target_currency
|
|
ORDER BY measurement_id DESC, id DESC
|
|
LIMIT 1'
|
|
);
|
|
$stmt->execute([
|
|
'owner_sub' => $this->ownerSub,
|
|
'base_currency' => strtoupper($baseCurrency),
|
|
'target_currency' => strtoupper($targetCurrency),
|
|
]);
|
|
$row = $stmt->fetch();
|
|
return is_array($row) ? $this->normalizeRow($row) : null;
|
|
}
|
|
|
|
public function saveFxRate(string $baseCurrency, string $targetCurrency, float $rate, string $rateDate, string $provider = 'currencyapi'): array
|
|
{
|
|
$result = $this->saveFxFetch($baseCurrency, $provider, $rateDate, [
|
|
strtoupper($targetCurrency) => $rate,
|
|
]);
|
|
return $result['rates'][0] ?? [
|
|
'fetch_id' => $result['fetch']['id'] ?? null,
|
|
'base_currency' => strtoupper($baseCurrency),
|
|
'target_currency' => strtoupper($targetCurrency),
|
|
'rate' => $rate,
|
|
'rate_date' => $rateDate,
|
|
'provider' => $provider,
|
|
];
|
|
}
|
|
|
|
public function saveFxFetch(string $baseCurrency, string $provider, string $rateDate, array $rates): array
|
|
{
|
|
$baseCurrency = strtoupper(trim($baseCurrency));
|
|
$provider = trim($provider) !== '' ? trim($provider) : 'currencyapi';
|
|
$fetchedAt = $this->currentUtcTimestamp();
|
|
$normalizedRates = [];
|
|
$currenciesToEnsure = [
|
|
[
|
|
'code' => substr($baseCurrency, 0, 10),
|
|
'name' => $baseCurrency,
|
|
'symbol' => substr($baseCurrency, 0, 8),
|
|
'is_active' => 1,
|
|
'is_crypto' => $this->isCryptoCode($baseCurrency) ? 1 : 0,
|
|
'sort_order' => 1000,
|
|
],
|
|
];
|
|
|
|
foreach ($rates as $currencyCode => $rate) {
|
|
if (!is_numeric($rate)) {
|
|
continue;
|
|
}
|
|
|
|
$normalizedCurrencyCode = strtoupper(trim((string) $currencyCode));
|
|
if ($normalizedCurrencyCode === '' || $normalizedCurrencyCode === $baseCurrency) {
|
|
continue;
|
|
}
|
|
|
|
$normalizedRates[$normalizedCurrencyCode] = (float) $rate;
|
|
$currenciesToEnsure[] = [
|
|
'code' => substr($normalizedCurrencyCode, 0, 10),
|
|
'name' => $normalizedCurrencyCode,
|
|
'symbol' => substr($normalizedCurrencyCode, 0, 8),
|
|
'is_active' => 1,
|
|
'is_crypto' => $this->isCryptoCode($normalizedCurrencyCode) ? 1 : 0,
|
|
'sort_order' => 1000,
|
|
];
|
|
}
|
|
|
|
$this->debug?->add('db.saveFxFetch.start', [
|
|
'base_currency' => $baseCurrency,
|
|
'provider' => $provider,
|
|
'rate_date' => $rateDate,
|
|
'rate_count' => count($normalizedRates),
|
|
'fetched_at' => $fetchedAt,
|
|
]);
|
|
|
|
$startedTransaction = false;
|
|
if (!$this->pdo->inTransaction()) {
|
|
$this->debug?->add('db.saveFxFetch.transaction.begin.start', [
|
|
'already_in_transaction' => false,
|
|
]);
|
|
$this->pdo->beginTransaction();
|
|
$startedTransaction = true;
|
|
$this->debug?->add('db.saveFxFetch.transaction.begin.end', [
|
|
'started_transaction' => true,
|
|
]);
|
|
} else {
|
|
$this->debug?->add('db.saveFxFetch.transaction.reuse', [
|
|
'already_in_transaction' => true,
|
|
]);
|
|
}
|
|
|
|
try {
|
|
$this->debug?->add('db.saveFxFetch.saveCurrencies.start', [
|
|
'currency_count' => count($currenciesToEnsure),
|
|
]);
|
|
$this->saveCurrencies($currenciesToEnsure);
|
|
$this->debug?->add('db.saveFxFetch.saveCurrencies.end', [
|
|
'currency_count' => count($currenciesToEnsure),
|
|
]);
|
|
|
|
if ($this->driver === 'pgsql') {
|
|
$this->debug?->add('db.saveFxFetch.insertFetch.start', [
|
|
'driver' => $this->driver,
|
|
]);
|
|
$fetchStmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('fx_fetches') . ' (
|
|
provider, base_currency, rate_date, fetched_at
|
|
) VALUES (
|
|
:provider, :base_currency, :rate_date, :fetched_at
|
|
)
|
|
RETURNING *'
|
|
);
|
|
$fetchStmt->execute([
|
|
'provider' => $provider,
|
|
'base_currency' => $baseCurrency,
|
|
'rate_date' => $rateDate,
|
|
'fetched_at' => $fetchedAt,
|
|
]);
|
|
$fetch = $this->normalizeRow($fetchStmt->fetch() ?: []);
|
|
$this->debug?->add('db.saveFxFetch.insertFetch.end', [
|
|
'driver' => $this->driver,
|
|
'fetch_id' => $fetch['id'] ?? null,
|
|
]);
|
|
} else {
|
|
$this->debug?->add('db.saveFxFetch.insertFetch.start', [
|
|
'driver' => $this->driver,
|
|
]);
|
|
$fetchStmt = $this->pdo->prepare(
|
|
'INSERT INTO ' . $this->table('fx_fetches') . ' (
|
|
provider, base_currency, rate_date, fetched_at
|
|
) VALUES (
|
|
:provider, :base_currency, :rate_date, :fetched_at
|
|
)'
|
|
);
|
|
$fetchStmt->execute([
|
|
'provider' => $provider,
|
|
'base_currency' => $baseCurrency,
|
|
'rate_date' => $rateDate,
|
|
'fetched_at' => $fetchedAt,
|
|
]);
|
|
$fetchId = (int) $this->pdo->lastInsertId();
|
|
$fetchLookup = $this->pdo->prepare('SELECT * FROM ' . $this->table('fx_fetches') . ' WHERE id = :id LIMIT 1');
|
|
$fetchLookup->execute(['id' => $fetchId]);
|
|
$fetch = $this->normalizeRow($fetchLookup->fetch() ?: []);
|
|
$this->debug?->add('db.saveFxFetch.insertFetch.end', [
|
|
'driver' => $this->driver,
|
|
'fetch_id' => $fetch['id'] ?? null,
|
|
]);
|
|
}
|
|
|
|
if ($normalizedRates === []) {
|
|
if ($startedTransaction) {
|
|
$this->debug?->add('db.saveFxFetch.commit.start', [
|
|
'fetch_id' => $fetch['id'] ?? null,
|
|
'rate_count' => 0,
|
|
]);
|
|
$this->pdo->commit();
|
|
$this->debug?->add('db.saveFxFetch.commit.end', [
|
|
'fetch_id' => $fetch['id'] ?? null,
|
|
'rate_count' => 0,
|
|
]);
|
|
}
|
|
|
|
$this->debug?->add('db.saveFxFetch.end', [
|
|
'fetch_id' => $fetch['id'] ?? null,
|
|
'rate_count' => 0,
|
|
]);
|
|
|
|
return [
|
|
'fetch' => $fetch,
|
|
'rates' => [],
|
|
];
|
|
}
|
|
|
|
$placeholders = [];
|
|
$params = ['fetch_id' => $fetch['id']];
|
|
$savedRates = [];
|
|
$index = 0;
|
|
foreach ($normalizedRates as $currencyCode => $rate) {
|
|
$codeKey = 'currency_code_' . $index;
|
|
$valueKey = 'current_value_' . $index;
|
|
$placeholders[] = "(:fetch_id, :{$codeKey}, :{$valueKey})";
|
|
$params[$codeKey] = $currencyCode;
|
|
$params[$valueKey] = $rate;
|
|
$savedRates[] = [
|
|
'fetch_id' => $fetch['id'],
|
|
'base_currency' => $baseCurrency,
|
|
'target_currency' => $currencyCode,
|
|
'rate' => $rate,
|
|
'rate_date' => $rateDate,
|
|
'provider' => $provider,
|
|
'fetched_at' => $fetch['fetched_at'] ?? null,
|
|
];
|
|
$index++;
|
|
}
|
|
|
|
$this->debug?->add('db.saveFxFetch.insertRates.start', [
|
|
'fetch_id' => $fetch['id'] ?? null,
|
|
'rate_count' => count($savedRates),
|
|
]);
|
|
$sql = 'INSERT INTO ' . $this->table('fx_rates') . ' (fetch_id, currency_code, current_value) VALUES ' . implode(', ', $placeholders);
|
|
$insert = $this->pdo->prepare($sql);
|
|
$insert->execute($params);
|
|
$this->debug?->add('db.saveFxFetch.insertRates.end', [
|
|
'fetch_id' => $fetch['id'] ?? null,
|
|
'rate_count' => count($savedRates),
|
|
]);
|
|
|
|
if ($startedTransaction) {
|
|
$this->debug?->add('db.saveFxFetch.commit.start', [
|
|
'fetch_id' => $fetch['id'] ?? null,
|
|
'rate_count' => count($savedRates),
|
|
]);
|
|
$this->pdo->commit();
|
|
$this->debug?->add('db.saveFxFetch.commit.end', [
|
|
'fetch_id' => $fetch['id'] ?? null,
|
|
'rate_count' => count($savedRates),
|
|
]);
|
|
}
|
|
|
|
$this->debug?->add('db.saveFxFetch.end', [
|
|
'fetch_id' => $fetch['id'] ?? null,
|
|
'rate_count' => count($savedRates),
|
|
]);
|
|
|
|
return [
|
|
'fetch' => $fetch,
|
|
'rates' => $savedRates,
|
|
];
|
|
} catch (\Throwable $exception) {
|
|
if ($startedTransaction && $this->pdo->inTransaction()) {
|
|
$this->debug?->add('db.saveFxFetch.rollback.start', [
|
|
'message' => $exception->getMessage(),
|
|
]);
|
|
$this->pdo->rollBack();
|
|
$this->debug?->add('db.saveFxFetch.rollback.end', [
|
|
'message' => $exception->getMessage(),
|
|
]);
|
|
}
|
|
|
|
$this->debug?->add('db.saveFxFetch.error', [
|
|
'message' => $exception->getMessage(),
|
|
]);
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
public function listActiveFiatCurrencies(): array
|
|
{
|
|
$currencies = $this->listCurrencies();
|
|
|
|
return array_values(array_filter($currencies, static function (array $currency): bool {
|
|
$code = strtoupper((string) ($currency['code'] ?? ''));
|
|
return $code !== '' && empty($currency['is_crypto']);
|
|
}));
|
|
}
|
|
|
|
private function table(string $logicalName): string
|
|
{
|
|
return match ($logicalName) {
|
|
'projects' => $this->prefix . 'projects',
|
|
'currencies' => $this->prefix . 'currencies',
|
|
'settings' => $this->prefix . 'settings',
|
|
'cost_plans' => $this->prefix . 'cost_plans',
|
|
'measurements' => $this->prefix . 'measurements',
|
|
'targets' => $this->prefix . 'targets',
|
|
'dashboard_definitions' => $this->prefix . 'dashboard_definitions',
|
|
'fx_fetches' => $this->prefix . 'fx_fetches',
|
|
'fx_rates' => $this->prefix . 'fx_rates',
|
|
'measurement_rates' => $this->prefix . 'measurement_rates',
|
|
'payouts' => $this->prefix . 'payouts',
|
|
'miner_offers' => $this->prefix . 'miner_offers',
|
|
'purchased_miners' => $this->prefix . 'purchased_miners',
|
|
default => throw new \RuntimeException('Unknown mining table: ' . $logicalName),
|
|
};
|
|
}
|
|
|
|
private function normalizeInsertPayload(string $projectKey, array $payload): array
|
|
{
|
|
return [
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'label' => $payload['label'],
|
|
'starts_at' => $payload['starts_at'],
|
|
'runtime_months' => $payload['runtime_months'],
|
|
'mining_speed_value' => $payload['mining_speed_value'],
|
|
'mining_speed_unit' => $payload['mining_speed_unit'],
|
|
'bonus_speed_value' => $payload['bonus_speed_value'],
|
|
'bonus_speed_unit' => $payload['bonus_speed_unit'],
|
|
'auto_renew' => $payload['auto_renew'],
|
|
'base_price_amount' => $payload['base_price_amount'] ?? $payload['total_cost_amount'],
|
|
'payment_type' => $payload['payment_type'] ?? 'fiat',
|
|
'total_cost_amount' => $payload['total_cost_amount'],
|
|
'currency' => $payload['currency'],
|
|
'note' => $payload['note'],
|
|
'is_active' => $payload['is_active'],
|
|
];
|
|
}
|
|
|
|
private function normalizeOfferPayload(string $projectKey, array $payload): array
|
|
{
|
|
return [
|
|
'project_key' => $projectKey,
|
|
'label' => $payload['label'],
|
|
'runtime_months' => $payload['runtime_months'],
|
|
'mining_speed_value' => $payload['mining_speed_value'],
|
|
'mining_speed_unit' => $payload['mining_speed_unit'],
|
|
'bonus_speed_value' => $payload['bonus_speed_value'],
|
|
'bonus_speed_unit' => $payload['bonus_speed_unit'],
|
|
'base_price_amount' => $payload['base_price_amount'],
|
|
'base_price_currency' => $payload['base_price_currency'],
|
|
'payment_type' => $payload['payment_type'] ?? 'fiat',
|
|
'auto_renew' => $payload['auto_renew'] ?? 0,
|
|
'note' => $payload['note'],
|
|
'is_active' => $payload['is_active'],
|
|
];
|
|
}
|
|
|
|
private function normalizePurchasedPayload(string $projectKey, int $offerId, array $payload): array
|
|
{
|
|
return [
|
|
'project_key' => $projectKey,
|
|
'owner_sub' => $this->ownerSub,
|
|
'miner_offer_id' => $offerId,
|
|
'purchased_at' => $payload['purchased_at'],
|
|
'label' => $payload['label'],
|
|
'runtime_months' => $payload['runtime_months'],
|
|
'mining_speed_value' => $payload['mining_speed_value'],
|
|
'mining_speed_unit' => $payload['mining_speed_unit'],
|
|
'bonus_speed_value' => $payload['bonus_speed_value'],
|
|
'bonus_speed_unit' => $payload['bonus_speed_unit'],
|
|
'total_cost_amount' => $payload['total_cost_amount'],
|
|
'currency' => $payload['currency'],
|
|
'usd_reference_amount' => $payload['usd_reference_amount'],
|
|
'reference_price_amount' => $payload['reference_price_amount'] ?? null,
|
|
'reference_price_currency' => $payload['reference_price_currency'] ?? null,
|
|
'auto_renew' => $payload['auto_renew'] ?? 0,
|
|
'note' => $payload['note'],
|
|
'is_active' => $payload['is_active'],
|
|
];
|
|
}
|
|
|
|
private function normalizeRows(array $rows): array
|
|
{
|
|
return array_map(fn (array $row): array => $this->normalizeRow($row), $rows);
|
|
}
|
|
|
|
private function normalizeRow(array $row): array
|
|
{
|
|
foreach (['ocr_flags', 'filters_json', 'preferred_currencies'] 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) {
|
|
$row[$jsonField] = $decoded;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (['is_active', 'auto_renew'] as $booleanField) {
|
|
if (array_key_exists($booleanField, $row)) {
|
|
$row[$booleanField] = $this->normalizeBoolean($row[$booleanField]);
|
|
}
|
|
}
|
|
|
|
if (array_key_exists('is_crypto', $row)) {
|
|
$row['is_crypto'] = (bool) $this->normalizeBoolean($row['is_crypto']);
|
|
}
|
|
|
|
return $row;
|
|
}
|
|
|
|
private function isCryptoCode(string $code): bool
|
|
{
|
|
return in_array(strtoupper($code), [
|
|
'ADA', 'ARB', 'BNB', 'BTC', 'DAI', 'DOGE', 'DOT', 'ETH', 'LINK', 'LTC',
|
|
'SOL', 'USDC', 'USDT', 'XRP',
|
|
], true);
|
|
}
|
|
|
|
private function normalizeBoolean(mixed $value): bool|int|null
|
|
{
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
|
|
if (is_bool($value) || is_int($value)) {
|
|
return $value;
|
|
}
|
|
|
|
if (is_string($value)) {
|
|
$normalized = strtolower(trim($value));
|
|
if (in_array($normalized, ['t', 'true', '1', 'y', 'yes'], true)) {
|
|
return true;
|
|
}
|
|
if (in_array($normalized, ['f', 'false', '0', 'n', 'no'], true)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return (int) $value;
|
|
}
|
|
|
|
private function currentUtcTimestamp(): string
|
|
{
|
|
$timezone = new \DateTimeZone('UTC');
|
|
return (new \DateTimeImmutable('now', $timezone))->format('Y-m-d H:i:s');
|
|
}
|
|
}
|