whisper-money/app/Services/Banking/BitpandaBalanceSyncService.php

190 lines
6.1 KiB
PHP

<?php
namespace App\Services\Banking;
use App\Models\Account;
use Illuminate\Support\Facades\Log;
class BitpandaBalanceSyncService
{
/**
* Sync the total portfolio value for a Bitpanda account.
* Uses Bitpanda's own ticker prices to match the values shown in the Bitpanda dashboard.
* Also calculates the invested amount from fiat deposit/withdrawal history.
*/
public function sync(Account $account, BitpandaClient $client): void
{
if (! $account->external_account_id) {
return;
}
$investedAmountCents = $this->calculateInvestedAmount($client, strtoupper($account->currency_code));
$this->syncCurrentBalance($account, $client, $investedAmountCents);
}
/**
* Sync today's balance by fetching all wallets and converting to target currency
* using Bitpanda's own ticker prices.
*/
public function syncCurrentBalance(Account $account, BitpandaClient $client, ?int $investedAmountCents = null): void
{
$targetCurrency = strtoupper($account->currency_code);
$ticker = $client->getTickerPrices();
$totalValue = 0.0;
$totalValue += $this->sumCryptoWallets($client, $ticker, $targetCurrency);
$totalValue += $this->sumFiatWallets($client, $targetCurrency);
$totalValueCents = (int) round($totalValue * 100);
$account->balances()->updateOrCreate(
['balance_date' => now()->toDateString()],
[
'balance' => $totalValueCents,
...($investedAmountCents !== null ? ['invested_amount' => $investedAmountCents] : []),
],
);
}
/**
* Sum all crypto wallet balances using Bitpanda's ticker prices.
*
* @param array<string, array<string, string>> $ticker
*/
private function sumCryptoWallets(BitpandaClient $client, array $ticker, string $targetCurrency): float
{
$wallets = $client->getCryptoWallets();
$total = 0.0;
foreach ($wallets['data'] as $wallet) {
$attributes = $wallet['attributes'];
$balance = (float) $attributes['balance'];
$symbol = $attributes['cryptocoin_symbol'];
$deleted = $attributes['deleted'];
if ($balance <= 0 || ! $symbol || $deleted) {
continue;
}
$price = $this->getTickerPrice($ticker, $symbol, $targetCurrency);
if ($price === null) {
Log::warning('Bitpanda ticker price not found for asset', [
'asset' => $symbol,
'target_currency' => $targetCurrency,
]);
continue;
}
$total += $balance * $price;
}
return $total;
}
/**
* Sum all fiat wallet balances, using ticker to convert if needed.
*/
private function sumFiatWallets(BitpandaClient $client, string $targetCurrency): float
{
$wallets = $client->getFiatWallets();
$total = 0.0;
foreach ($wallets['data'] as $wallet) {
$attributes = $wallet['attributes'];
$balance = (float) $attributes['balance'];
$symbol = strtoupper($attributes['fiat_symbol']);
if ($balance <= 0 || ! $symbol) {
continue;
}
if ($symbol === $targetCurrency) {
$total += $balance;
} else {
// Fiat-to-fiat conversion is rare on Bitpanda; use simple rate if available
Log::warning('Bitpanda fiat wallet in different currency than target', [
'fiat_symbol' => $symbol,
'target_currency' => $targetCurrency,
'balance' => $balance,
]);
}
}
return $total;
}
/**
* Get the ticker price for an asset in the target currency.
*
* @param array<string, array<string, string>> $ticker
*/
private function getTickerPrice(array $ticker, string $symbol, string $targetCurrency): ?float
{
$price = $ticker[$symbol][$targetCurrency] ?? null;
if ($price === null) {
return null;
}
return (float) $price;
}
/**
* Calculate net invested amount from fiat deposit and withdrawal history.
* Net invested = total deposits - total withdrawals (in cents).
*/
private function calculateInvestedAmount(BitpandaClient $client, string $targetCurrency): ?int
{
$deposits = $client->getAllFiatTransactions('deposit');
$withdrawals = $client->getAllFiatTransactions('withdrawal');
$totalDeposited = $this->sumFiatTransactions($deposits, $targetCurrency);
$totalWithdrawn = $this->sumFiatTransactions($withdrawals, $targetCurrency);
if ($totalDeposited === 0.0 && $totalWithdrawn === 0.0) {
return null;
}
return (int) round(($totalDeposited - $totalWithdrawn) * 100);
}
/**
* Sum the amounts of fiat transactions in the target currency.
* Only considers finished transactions whose fiat_id matches the target currency.
*
* @param array<int, array{type: string, id: string, attributes: array}> $transactions
*/
private function sumFiatTransactions(array $transactions, string $targetCurrency): float
{
$total = 0.0;
foreach ($transactions as $transaction) {
$attributes = $transaction['attributes'];
$status = $attributes['status'] ?? '';
$amount = (float) ($attributes['amount'] ?? 0);
if ($status !== 'finished' || $amount <= 0) {
continue;
}
$fiatId = strtoupper($attributes['fiat_id'] ?? '');
if ($fiatId !== $targetCurrency) {
Log::warning('Bitpanda fiat transaction in different currency than target', [
'fiat_id' => $fiatId,
'target_currency' => $targetCurrency,
'amount' => $amount,
]);
continue;
}
$total += $amount;
}
return $total;
}
}