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

124 lines
3.6 KiB
PHP

<?php
namespace App\Services\Banking;
use App\Contracts\BankingProviderInterface;
use App\Models\Account;
use Illuminate\Support\Facades\Log;
class BalanceSyncService
{
/** Balance types in preference order */
private const PREFERRED_BALANCE_TYPES = ['CLBD', 'ITAV', 'ITBD', 'OPBD', 'XPCD'];
public function __construct(
private BankingProviderInterface $provider,
) {}
/**
* Sync balances for a connected account.
*/
public function sync(Account $account): void
{
if (! $account->external_account_id) {
return;
}
$result = $this->provider->getBalances($account->external_account_id);
$balances = $result['balances'];
if (empty($balances)) {
return;
}
$balance = $this->selectPreferredBalance($balances);
if (! $balance) {
return;
}
$amount = (int) round(floatval($balance['balance_amount']['amount']) * 100);
$date = $balance['reference_date'] ?? now()->toDateString();
$account->balances()->updateOrCreate(
['balance_date' => $date],
['balance' => $amount],
);
Log::info('Synced balance', [
'account_id' => $account->id,
'balance' => $amount,
'date' => $date,
'type' => $balance['balance_type'],
]);
}
/**
* Calculate historical daily balances by working backwards from the latest known balance.
* Uses transaction amounts to derive end-of-day balances for dates without direct balance data.
*/
public function calculateHistoricalBalances(Account $account): void
{
$referenceBalance = $account->balances()
->orderByDesc('balance_date')
->first();
if (! $referenceBalance) {
return;
}
$existingDates = $account->balances()
->pluck('balance_date')
->map(fn (mixed $date) => $date instanceof \Carbon\Carbon ? $date->toDateString() : (string) $date)
->flip()
->all();
$dailyTotals = $account->transactions()
->where('transaction_date', '<=', $referenceBalance->balance_date)
->selectRaw('transaction_date, SUM(amount) as daily_total')
->groupBy('transaction_date')
->orderByDesc('transaction_date')
->pluck('daily_total', 'transaction_date');
if ($dailyTotals->isEmpty()) {
return;
}
$runningBalance = $referenceBalance->balance;
$referenceDate = $referenceBalance->balance_date->toDateString();
foreach ($dailyTotals as $date => $sum) {
if ($date < $referenceDate && ! isset($existingDates[$date])) {
$account->balances()->create([
'balance_date' => $date,
'balance' => $runningBalance,
]);
}
$runningBalance -= (int) $sum;
}
Log::info('Calculated historical balances', [
'account_id' => $account->id,
'reference_date' => $referenceDate,
'reference_balance' => $referenceBalance->balance,
]);
}
/**
* Select the most useful balance from the list based on preferred types.
*/
private function selectPreferredBalance(array $balances): ?array
{
foreach (self::PREFERRED_BALANCE_TYPES as $type) {
foreach ($balances as $balance) {
if (($balance['balance_type'] ?? null) === $type) {
return $balance;
}
}
}
return $balances[0] ?? null;
}
}