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

191 lines
5.6 KiB
PHP

<?php
namespace App\Services\Banking;
use App\Contracts\BankingProviderInterface;
use App\Enums\TransactionSource;
use App\Models\Account;
use Illuminate\Support\Facades\Log;
class TransactionSyncService
{
public function __construct(
private BankingProviderInterface $provider,
private TransactionDescriptionFormatter $descriptionFormatter,
) {}
/**
* Sync transactions for a connected account.
*
* @return int Number of new transactions created
*/
public function sync(Account $account, string $dateFrom, string $dateTo, ?string $strategy = null, bool $saveDailyBalances = true): int
{
if (! $account->external_account_id) {
return 0;
}
$created = 0;
$continuationKey = null;
$dailyBalances = [];
$bankName = $account->bank?->name;
do {
$result = $this->provider->getTransactions(
$account->external_account_id,
$dateFrom,
$dateTo,
$continuationKey,
$strategy,
);
foreach ($result['transactions'] as $transaction) {
if ($this->importTransaction($account, $transaction, $bankName)) {
$created++;
}
if ($saveDailyBalances) {
$this->trackDailyBalance($transaction, $dailyBalances);
}
}
$continuationKey = $result['continuation_key'];
} while ($continuationKey);
if ($saveDailyBalances) {
$this->saveDailyBalances($account, $dailyBalances);
}
Log::info('Synced transactions', [
'account_id' => $account->id,
'new_transactions' => $created,
'date_from' => $dateFrom,
'date_to' => $dateTo,
]);
return $created;
}
/**
* Import a single transaction, skipping duplicates.
*/
private function importTransaction(Account $account, array $data, ?string $bankName): bool
{
$externalId = $data['transaction_id'] ?? $data['entry_reference'] ?? null;
if ($externalId) {
$exists = $account->transactions()
->withTrashed()
->where('external_transaction_id', $externalId)
->exists();
if ($exists) {
return false;
}
}
$amount = $this->parseAmount($data);
$rawDescription = $this->parseDescription($data);
$formatted = $this->descriptionFormatter->format($rawDescription, $bankName);
$transactionDate = $this->parseDate($data);
$currency = $data['transaction_amount']['currency'] ?? $account->currency_code;
$account->transactions()->create([
'user_id' => $account->user_id,
'description' => $formatted['description'],
'description_iv' => null,
'original_description' => $formatted['original_description'],
'transaction_date' => $transactionDate,
'amount' => $amount,
'currency_code' => $currency,
'notes' => null,
'notes_iv' => null,
'source' => TransactionSource::EnableBanking,
'external_transaction_id' => $externalId,
'raw_data' => $data,
]);
return true;
}
/**
* Parse amount from EnableBanking transaction data.
* Returns amount in cents (bigint). Debits are negative.
*/
private function parseAmount(array $data): int
{
$rawAmount = $data['transaction_amount']['amount'] ?? '0';
$cents = (int) round(floatval($rawAmount) * 100);
$indicator = $data['credit_debit_indicator'] ?? null;
if ($indicator === 'DBIT') {
return -abs($cents);
}
return abs($cents);
}
/**
* Parse description from EnableBanking transaction data.
*/
private function parseDescription(array $data): string
{
$remittanceInfo = $data['remittance_information'] ?? [];
if (! empty($remittanceInfo)) {
return implode(' ', $remittanceInfo);
}
return $data['creditor']['name']
?? $data['debtor']['name']
?? 'Bank transaction';
}
/**
* Parse transaction date, preferring booking_date.
*/
private function parseDate(array $data): string
{
return $data['booking_date']
?? $data['transaction_date']
?? $data['value_date']
?? now()->toDateString();
}
/**
* Track the balance after transaction for each day.
* Overwrites so only the last transaction's balance per day is kept.
*
* @param array<string, int> $dailyBalances
*/
private function trackDailyBalance(array $transaction, array &$dailyBalances): void
{
$balanceAfter = $transaction['balance_after_transaction'] ?? null;
if (! $balanceAfter || ! isset($balanceAfter['amount'])) {
return;
}
$date = $this->parseDate($transaction);
$amount = (int) round(floatval($balanceAfter['amount']) * 100);
$dailyBalances[$date] = $amount;
}
/**
* Save tracked daily balances to the account.
*
* @param array<string, int> $dailyBalances
*/
private function saveDailyBalances(Account $account, array $dailyBalances): void
{
foreach ($dailyBalances as $date => $balance) {
$account->balances()->updateOrCreate(
['balance_date' => $date],
['balance' => $balance],
);
}
}
}