fix(open-banking): never store the XXX no-currency placeholder on imported accounts
Banks may report the ISO 4217 "XXX" (no currency) code, which has no exchange rate, so every conversion for such an account fell back to an unconverted (wrong) amount and spammed warning logs. Resolve a bank-reported currency to the user's base currency, then the app default, when it is XXX/empty/missing.
This commit is contained in:
parent
e4be39be12
commit
c18debf7da
|
|
@ -88,7 +88,7 @@ class AccountMappingController extends Controller
|
|||
}
|
||||
|
||||
if ($action === 'create') {
|
||||
$currency = $accountData['currency'] ?? 'EUR';
|
||||
$currency = $accountUserCurrencyService->resolveImportedCurrency($accountData['currency'] ?? null, $user);
|
||||
$name = $accountData['name']
|
||||
?? $accountData['account_id']['iban']
|
||||
?? $connection->aspsp_name.' Account';
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ trait CreatesAccountsFromPending
|
|||
continue;
|
||||
}
|
||||
|
||||
$currency = $accountData['currency'] ?? 'EUR';
|
||||
$currency = $accountUserCurrencyService->resolveImportedCurrency($accountData['currency'] ?? null, $user);
|
||||
$name = $accountData['name']
|
||||
?? $accountData['account_id']['iban']
|
||||
?? $connection->aspsp_name.' Account';
|
||||
|
|
|
|||
|
|
@ -7,6 +7,26 @@ use App\Models\User;
|
|||
|
||||
class AccountUserCurrencyService
|
||||
{
|
||||
/**
|
||||
* Resolve the currency code to store for a bank-imported account.
|
||||
*
|
||||
* Providers may report "XXX" (ISO 4217 "no currency") or omit the field;
|
||||
* in those cases fall back to the user's base currency, then to the app
|
||||
* default, so amounts stay convertible.
|
||||
*/
|
||||
public function resolveImportedCurrency(?string $reported, User $user): string
|
||||
{
|
||||
foreach ([$reported, $user->currency_code] as $candidate) {
|
||||
$candidate = strtoupper(trim((string) $candidate));
|
||||
|
||||
if ($candidate !== '' && $candidate !== 'XXX') {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return strtoupper(config('cashier.currency', 'eur'));
|
||||
}
|
||||
|
||||
public function syncFromFirstAccount(Account $account): void
|
||||
{
|
||||
$user = $account->user;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\AccountUserCurrencyService;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->service = app(AccountUserCurrencyService::class);
|
||||
config(['cashier.currency' => 'eur']);
|
||||
});
|
||||
|
||||
test('keeps a valid reported currency', function () {
|
||||
$user = User::factory()->make(['currency_code' => 'USD']);
|
||||
|
||||
expect($this->service->resolveImportedCurrency('GBP', $user))->toBe('GBP');
|
||||
});
|
||||
|
||||
test('uppercases the reported currency', function () {
|
||||
$user = User::factory()->make(['currency_code' => 'USD']);
|
||||
|
||||
expect($this->service->resolveImportedCurrency('gbp', $user))->toBe('GBP');
|
||||
});
|
||||
|
||||
test('falls back to the user currency for XXX, empty or missing codes', function (?string $reported) {
|
||||
$user = User::factory()->make(['currency_code' => 'USD']);
|
||||
|
||||
expect($this->service->resolveImportedCurrency($reported, $user))->toBe('USD');
|
||||
})->with(['XXX', 'xxx', '', null]);
|
||||
|
||||
test('falls back to the app default when both the bank and the user lack a currency', function () {
|
||||
$user = User::factory()->make(['currency_code' => 'XXX']);
|
||||
|
||||
expect($this->service->resolveImportedCurrency('XXX', $user))->toBe('EUR');
|
||||
});
|
||||
|
|
@ -182,6 +182,45 @@ test('store creates investment accounts for crypto provider connections', functi
|
|||
'coinbase' => ['coinbase', 'Coinbase', 'coinbase-portfolio'],
|
||||
]);
|
||||
|
||||
test('store falls back to the user currency when the bank reports XXX', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->onboarded()->create(['currency_code' => 'USD']);
|
||||
Account::factory()->create(['user_id' => $user->id, 'currency_code' => 'USD']);
|
||||
|
||||
$connection = BankingConnection::factory()->awaitingMapping()->create([
|
||||
'user_id' => $user->id,
|
||||
'aspsp_name' => 'Test Bank',
|
||||
'pending_accounts_data' => [
|
||||
[
|
||||
'uid' => 'ext-1',
|
||||
'currency' => 'XXX',
|
||||
'name' => 'No-currency Account',
|
||||
'account_id' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('open-banking.map-accounts.store', $connection), [
|
||||
'mappings' => [
|
||||
[
|
||||
'bank_account_uid' => 'ext-1',
|
||||
'action' => 'create',
|
||||
'existing_account_id' => null,
|
||||
],
|
||||
],
|
||||
])
|
||||
->assertRedirect(route('settings.connections.index'));
|
||||
|
||||
$this->assertDatabaseHas('accounts', [
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => 'ext-1',
|
||||
'currency_code' => 'USD',
|
||||
]);
|
||||
$this->assertDatabaseMissing('accounts', ['currency_code' => 'XXX']);
|
||||
});
|
||||
|
||||
test('store updates user currency from first account created from mapping', function () {
|
||||
Queue::fake();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue