124 lines
3.6 KiB
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;
|
|
}
|
|
}
|