Skip to content

Commit

Permalink
Fix all relevant bank models (#19)
Browse files Browse the repository at this point in the history
* Change Singleton to Transient

* Update account details model
Change to WhenAll for fetching account details

* Remove uneccessary properties from bank models
Add log text that explains which field went wrong when serializing

* Refactor and enhance banking service and tests

- Update `BankClient_v2.cs` to adjust JSON properties and make `CreditDebitIndicator` nullable.
- Add `AccountExtensions.cs` with error logging method.
- Add `HasErrors` property to `AccountV2` in `BankResponse.cs`.
- Enhance `GetAccountDetailsV2` in `BankService.cs` for better error handling and logging.
- Update `Altinn.Dan.Plugin.Banking.Test.csproj` to target `net8.0`, add `FakeItEasy`, and reference `Altinn.Dan.Plugin.Banking`.
- Add `FakeableHttpMessageHandler.cs` for handling HTTP requests in tests and generating certificates.
- Add `BankServiceTests.cs` with comprehensive unit tests for `BankService`.
- Remove obsolete `UnitTest1.cs`.

* Fix failed tests

* Nullable enabled
Remove all required and add back some
Remove roles
Remove unused extension
Remove unused usings
Add tests

* Fix property references a bit after introducing nullable enable

* Fix balances
  • Loading branch information
DanRJ authored Feb 10, 2025
1 parent f939aac commit c7f3179
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 518 deletions.
559 changes: 133 additions & 426 deletions src/Altinn.Dan.Plugin.Banking/Clients/V2/BankClient_v2.cs

Large diffs are not rendered by default.

5 changes: 0 additions & 5 deletions src/Altinn.Dan.Plugin.Banking/Clients/V2/Bankv2.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Altinn.Dan.Plugin.Banking.Config;
using Jose;
using Microsoft.Extensions.Options;

namespace Altinn.Dan.Plugin.Banking.Clients.V2;
public partial class Bank_v2
Expand Down
24 changes: 23 additions & 1 deletion src/Altinn.Dan.Plugin.Banking/Extensions/AccountExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Altinn.Dan.Plugin.Banking.Clients.V2;
using Altinn.Dan.Plugin.Banking.Models;
using Altinn.Dan.Plugin.Banking.Services;
using Microsoft.Extensions.Logging;
using System;
Expand All @@ -8,7 +9,7 @@ namespace Altinn.Dan.Plugin.Banking.Extensions
public static class AccountExtensions
{
public static void LogGetAccountByIdError(
this Account account,
this Clients.V2.Account account,
ILogger logger,
Exception e,
BankConfig bank,
Expand All @@ -32,5 +33,26 @@ public static void LogGetAccountByIdError(
e.Source,
innerExceptionMsg);
}

public static AccountV2 ToDefaultDto(
this Clients.V2.Account account)
=> new AccountV2
{
AccountAvailableBalance = 0,
AccountBookedBalance = 0,
AccountDetail = new AccountDetail
{
Balances = null,
PrimaryOwner = account.PrimaryOwner,
Servicer = account.Servicer,
Status = account.Status,
AccountIdentifier = account.AccountIdentifier,
AccountReference = account.AccountReference,
Type = account.Type
},
AccountNumber = account.AccountIdentifier,
Transactions = null,
HasErrors = true
};
}
}
108 changes: 51 additions & 57 deletions src/Altinn.Dan.Plugin.Banking/Services/BankService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#nullable enable
using Altinn.ApiClients.Maskinporten.Interfaces;
using Altinn.Dan.Plugin.Banking.Clients.V2;
using Altinn.Dan.Plugin.Banking.Config;
Expand Down Expand Up @@ -57,7 +58,7 @@ public async Task<BankResponse> GetAccounts(string ssn, Dictionary<string, BankC
if (e is ApiException k)
{
correlationId = k.CorrelationId;
innerExceptionMsg = k.InnerException?.Message;
innerExceptionMsg = k.InnerException?.Message ?? string.Empty;
}
_logger.LogError(
"Bank failed while processing bank {Bank} ({OrgNo}) for {Subject}, error {Error}, accountInfoRequestId: {AccountInfoRequestId}, CorrelationId: {CorrelationId}, source: {source}, innerExceptionMessage: {innerExceptionMessage}",
Expand Down Expand Up @@ -95,7 +96,7 @@ private async Task<BankInfo> InvokeBank(string ssn, BankConfig bank, DateTimeOff

var bankClient = new Bank_v2.Bank_v2(bank.Client, _settings)
{
BaseUrl = bank.Client.BaseAddress?.ToString(),
BaseUrl = bank.Client.BaseAddress!.ToString(),
DecryptionCertificate = _settings.OedDecryptCert
};
var accountListTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(AccountListRequestTimeoutSecs));
Expand All @@ -108,39 +109,47 @@ private async Task<BankInfo> GetAccountDetailsV2(Bank_v2.Bank_v2 bankClient, Acc
var bankInfo = new BankInfo() { Accounts = [] };
var transactions = new Transactions();

if (accounts.Accounts1 == null ||
accounts.Accounts1.Count == 0)
{
return bankInfo;
}

Task<AccountDetails>[] accountsDetailsTasks = accounts.Accounts1.Select(x => GetAccountById(bankClient, x, bank, accountInfoRequestId, fromDate, toDate)).ToArray();

var results = Task.WhenAll(accountsDetailsTasks);
try
{
AccountDetails[] accountsDetails = await results;
foreach (var accountDetails in accountsDetails)
{
if (accountDetails.Account == null) continue;
if (accountDetails?.Account == null) continue;

var balances = accountDetails.Account?.Balances;

var availableCredit = accountDetails.Account.Balances.FirstOrDefault(b =>
b.Type == BalanceType.AvailableBalance && b.CreditDebitIndicator == CreditOrDebit.Credit)
var availableCredit = balances?.FirstOrDefault(b =>
b?.Type == BalanceType.AvailableBalance && b.CreditDebitIndicator == CreditOrDebit.Credit)
?.Amount ?? 0;
var availableDebit = accountDetails.Account.Balances.FirstOrDefault(b =>
b.Type == BalanceType.AvailableBalance && b.CreditDebitIndicator == CreditOrDebit.Debit)
var availableDebit = balances?.FirstOrDefault(b =>
b?.Type == BalanceType.AvailableBalance && b.CreditDebitIndicator == CreditOrDebit.Debit)
?.Amount ?? 0;

var bookedCredit = accountDetails.Account.Balances.FirstOrDefault(b =>
b.Type == BalanceType.BookedBalance && b.CreditDebitIndicator == CreditOrDebit.Credit)
var bookedCredit = balances?.FirstOrDefault(b =>
b?.Type == BalanceType.BookedBalance && b.CreditDebitIndicator == CreditOrDebit.Credit)
?.Amount ?? 0;
var bookedDebit = accountDetails.Account.Balances.FirstOrDefault(b =>
b.Type == BalanceType.BookedBalance && b.CreditDebitIndicator == CreditOrDebit.Debit)
var bookedDebit = balances?.FirstOrDefault(b =>
b?.Type == BalanceType.BookedBalance && b.CreditDebitIndicator == CreditOrDebit.Debit)
?.Amount ?? 0;

if (includeTransactions)
{
transactions = await ListTransactionsForAccount(bankClient, accountDetails, bank, accountInfoRequestId, fromDate, toDate);
}

var internalAccount = MapToInternalV2(accountDetails, accountDetails.Account, transactions?.Transactions1, availableCredit - availableDebit, bookedCredit - bookedDebit);
if (internalAccount.AccountDetail != null)
{
bankInfo.Accounts.Add(internalAccount);
}
var internalAccount = MapToInternalV2(accountDetails.Account!, transactions?.Transactions1, availableCredit - availableDebit, bookedCredit - bookedDebit);
if (internalAccount.AccountDetail == null) continue;

bankInfo.Accounts.Add(internalAccount);
}
}
catch (Exception e)
Expand All @@ -149,29 +158,12 @@ private async Task<BankInfo> GetAccountDetailsV2(Bank_v2.Bank_v2 bankClient, Acc
{
var successfulTasks = accountsDetailsTasks.Where(x => x.IsCompletedSuccessfully).ToArray();
var successfulAccounts = await Task.WhenAll(successfulTasks);
var faultedAccounts = accounts.Accounts1.Where(x => !successfulAccounts.Any(y => y.Account.AccountReference == x.AccountReference)).ToList();
var faultedAccounts = accounts.Accounts1.Where(x => !successfulAccounts.Any(y => y.Account!.AccountReference == x.AccountReference)).ToList();

foreach (var faultedAccount in faultedAccounts)
{
faultedAccount.LogGetAccountByIdError(_logger, k, bank, accountInfoRequestId);
bankInfo.Accounts.Add(new AccountDtoV2
{
AccountAvailableBalance = 0,
AccountBookedBalance = 0,
AccountDetail = new AccountDetail
{
Balances = null,
PrimaryOwner = faultedAccount.PrimaryOwner,
Servicer = faultedAccount.Servicer,
Status = faultedAccount.Status,
AccountIdentifier = faultedAccount.AccountIdentifier,
AccountReference = faultedAccount.AccountReference,
Type = faultedAccount.Type
},
AccountNumber = faultedAccount.AccountIdentifier,
Transactions = null,
HasErrors = true
});
bankInfo.Accounts.Add(faultedAccount.ToDefaultDto());
}
}
else
Expand Down Expand Up @@ -199,7 +191,7 @@ private async Task<Accounts> GetAllAccounts(Bank_v2.Bank_v2 bankClient, BankConf
var accounts = await bankClient.ListAccountsAsync(accountInfoRequestId, correlationId, "OED", ssn, true, null, null, null, fromDate, toDate);

_logger.LogInformation("Found {NumberOfAccounts} accounts for {DeceasedNin} in bank {OrganisationNumber} with accountinforequestid {AccountInfoRequestId} and correlationid {CorrelationId}",
accounts.Accounts1.Count, ssn[..6], bank.OrgNo, accountInfoRequestId, correlationId);
accounts.Accounts1?.Count, ssn[..6], bank.OrgNo, accountInfoRequestId, correlationId);

return accounts;
}
Expand All @@ -208,56 +200,57 @@ private async Task<AccountDetails> GetAccountById(Bank_v2.Bank_v2 bankClient, Ba
{
Guid correlationIdDetails = Guid.NewGuid();

var primaryOwnerNin = account.PrimaryOwner?.Identifier?.Value != null ? account.PrimaryOwner.Identifier.Value[..6] : string.Empty;
_logger.LogInformation("Getting account details: bank {BankName} accountreference {AccountReference} dob {DateOfBirth} accountinforequestid {AccountInfoRequestId} correlationid {CorrelationId}",
bank.Name, account.AccountReference, account.PrimaryOwner?.Identifier?.Value?[..6], accountInfoRequestId, correlationIdDetails);
bank.Name, account.AccountReference, primaryOwnerNin, accountInfoRequestId, correlationIdDetails);

var detailsTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(AccountDetailsRequestTimeoutSecs));
var details = await bankClient.ShowAccountByIdAsync(account.AccountReference, accountInfoRequestId,
correlationIdDetails, "OED", null, null, null, fromDate, toDate, detailsTimeout.Token);

_logger.LogInformation("Retrieved account details: bank {BankName} accountreference {AccountReference} dob {DateOfBirth} responseDetailsStatus {ResponseDetailsStatus} accountinforequestid {AccountInfoRequestId} correlationid {CorrelationId}",
bank.Name, account.AccountReference, account.PrimaryOwner?.Identifier?.Value?[..6], details.ResponseDetails.Status, accountInfoRequestId, correlationIdDetails);
bank.Name, account.AccountReference, primaryOwnerNin, details.ResponseDetails?.Status, accountInfoRequestId, correlationIdDetails);

return details;
}

private async Task<Transactions> ListTransactionsForAccount(Bank_v2.Bank_v2 bankClient, AccountDetails accountDetails, BankConfig bank, Guid accountInfoRequestId, DateTimeOffset? fromDate, DateTimeOffset? toDate)
{
var account = accountDetails.Account;

Guid correlationIdTransactions = Guid.NewGuid();
var primaryOwnerNin = account?.PrimaryOwner?.Identifier?.Value != null ? account.PrimaryOwner.Identifier.Value[..6] : string.Empty;

// Start fetching transactions concurrently
var transactionsTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(TransactionRequestTimeoutSecs));

_logger.LogInformation("Getting transactions: bank {BankName} accountreference {AccountReference} dob {DateOfBirth} accountinforequestid {AccountInfoRequestId} correlationid {CorrelationId}",
bank.Name, account.AccountReference, account.PrimaryOwner?.Identifier?.Value?[..6], accountInfoRequestId, correlationIdTransactions);
bank.Name, account!.AccountReference, primaryOwnerNin, accountInfoRequestId, correlationIdTransactions);

var transactions = await bankClient.ListTransactionsAsync(account.AccountReference, accountInfoRequestId,
correlationIdTransactions, "OED", null, null, null, fromDate, toDate, transactionsTimeout.Token);

_logger.LogInformation("Retrieved transactions: bank {BankName} accountreference {AccountReference} dob {DateOfBirth} transaction count {NumberOfTransactions} accountinforequestid {AccountInfoRequestId} correlationid {CorrelationId}",
bank.Name, account.AccountIdentifier, account.PrimaryOwner?.Identifier?.Value?.Substring(0, 6), transactions.Transactions1?.Count, accountInfoRequestId, correlationIdTransactions);
bank.Name, account.AccountIdentifier, primaryOwnerNin, transactions.Transactions1?.Count, accountInfoRequestId, correlationIdTransactions);

return transactions;
}

private static AccountDtoV2 MapToInternalV2(
AccountDetails accountDetails,
AccountDetail detail,
ICollection<Transaction> transactions,
AccountDetail account,
ICollection<Transaction>? transactions,
decimal availableBalance,
decimal bookedBalance)
{
var account = accountDetails.Account;
detail.Type = account.Type;
detail.AccountIdentifier = account.AccountIdentifier;
detail.AccountReference = account.AccountReference;
account.Type = account.Type;
account.AccountIdentifier = account.AccountIdentifier;
account.AccountReference = account.AccountReference;

// P.t. almost passthrough mapping
return new AccountDtoV2
{
AccountNumber = account.AccountIdentifier,
AccountDetail = detail,
AccountDetail = account,
Transactions = transactions,
AccountAvailableBalance = availableBalance,
AccountBookedBalance = bookedBalance
Expand All @@ -278,10 +271,10 @@ public async Task<Transactions> GetTransactionsForAccount(string ssn, BankConfig

var bankClient = new Bank_v2.Bank_v2(bankConfig.Client, _settings)
{
BaseUrl = bankConfig.Client.BaseAddress?.ToString(),
BaseUrl = bankConfig.Client.BaseAddress!.ToString(),
DecryptionCertificate = _settings.OedDecryptCert
};
Transactions transactions = null;
Transactions transactions;
try
{
transactions = await bankClient.ListTransactionsAsync(accountReference, accountInfoRequestId, correlationId, "OED", null, null, null, fromDate, toDate, transactionsTimeout.Token);
Expand All @@ -291,7 +284,7 @@ public async Task<Transactions> GetTransactionsForAccount(string ssn, BankConfig
string innerExceptionMsg = string.Empty;
if (e is ApiException k)
{
innerExceptionMsg = k.InnerException?.Message;
innerExceptionMsg = k.InnerException?.Message ?? string.Empty;
}
_logger.LogError(
"GetTransactionsForAccount failed while processing bank {Bank} ({OrgNo}) for {Subject}, error {Error}, accountInfoRequestId: {AccountInfoRequestId}, CorrelationId: {CorrelationId}, source: {source}, innerExceptionMessage: {innerExceptionMessage})",
Expand All @@ -308,15 +301,16 @@ public async Task<Transactions> GetTransactionsForAccount(string ssn, BankConfig

public class BankConfig
{
public HttpClient Client { get; init; }
public string BankAudience { get; init; }
public required HttpClient Client { get; init; }

public required string BankAudience { get; init; }

public string MaskinportenEnv { get; init; }
public required string MaskinportenEnv { get; init; }

public string ApiVersion { get; init; }
public string? ApiVersion { get; init; }

public string Name { get; init; }
public required string Name { get; init; }

public string OrgNo { get; init; }
public required string OrgNo { get; init; }
}
}
19 changes: 0 additions & 19 deletions src/Altinn.Dan.Plugin.Banking/Utils/Extensions.cs

This file was deleted.

Loading

0 comments on commit c7f3179

Please sign in to comment.