From 4f188125474c4026025e4f1ed06350e4929954ec Mon Sep 17 00:00:00 2001 From: erlendoksvoll Date: Thu, 19 Dec 2024 13:04:06 +0100 Subject: [PATCH] Feature/remove bankconfiguration from bankservice (#14) * moved configuration of banks from bankservice to plugin * Refactor BankService and Plugin for async and clarity Refactor BankService to separate account and transaction logic: - Rename `GetTransactions` to `GetAccounts` and update functionality. - Update `InvokeBank` to call new `GetAllAccounts` method. - Enhance `GetAccountDetailsV2` to handle balances and transactions. - Add helper methods: `GetAllAccounts`, `GetAccountById`, `ListTransactionsForAccount`. - Update `MapToInternalV2` for additional account details. - Modify `GetTransactionsForAccount` to use single `BankConfig`. - Update `IBankService` interface for method changes. - Improve logging with structured logging and named placeholders. - Refactor `KARService` for structured logging and initialize cache. Update Plugin for async processing and improved readability: - Make `ProcessResponse` method asynchronous in `BankClient_v2.cs`. - Simplify `ProcessResponse` in `Bankv2.cs`. - Remove `SkipKAR` parameter in `Metadata.cs`. - Introduce `IHttpClientFactory` in `Plugin.cs` and refactor methods. - Rename `GetEndpoints` to `GetBankEndpoints`. - Refactor `CreateBankConfigurations` for multiple configurations. - Improve readability and logging in `ReadEndpointsAndCache`. - Remove and redefine several methods with updated logic. --------- Co-authored-by: DanRJ --- .../Altinn.Dan.Plugin.Banking.csproj | 2 +- .../Clients/V2/BankClient_v2.cs | 8 +- .../Clients/V2/Bankv2.cs | 14 +- src/Altinn.Dan.Plugin.Banking/Metadata.cs | 12 - src/Altinn.Dan.Plugin.Banking/Plugin.cs | 232 +++++++++-------- .../Services/BankService.cs | 239 ++++++++---------- .../Services/Interfaces/IBankService.cs | 4 +- .../Services/KARService.cs | 6 +- 8 files changed, 235 insertions(+), 282 deletions(-) diff --git a/src/Altinn.Dan.Plugin.Banking/Altinn.Dan.Plugin.Banking.csproj b/src/Altinn.Dan.Plugin.Banking/Altinn.Dan.Plugin.Banking.csproj index 0fc90ec..281fd58 100644 --- a/src/Altinn.Dan.Plugin.Banking/Altinn.Dan.Plugin.Banking.csproj +++ b/src/Altinn.Dan.Plugin.Banking/Altinn.Dan.Plugin.Banking.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Altinn.Dan.Plugin.Banking/Clients/V2/BankClient_v2.cs b/src/Altinn.Dan.Plugin.Banking/Clients/V2/BankClient_v2.cs index f3d1d44..ecd2e66 100644 --- a/src/Altinn.Dan.Plugin.Banking/Clients/V2/BankClient_v2.cs +++ b/src/Altinn.Dan.Plugin.Banking/Clients/V2/BankClient_v2.cs @@ -384,7 +384,7 @@ public virtual async System.Threading.Tasks.Task ShowAccountById headers_[item_.Key] = item_.Value; } - ProcessResponse(client_, response_); + await ProcessResponse(client_, response_); var status_ = (int)response_.StatusCode; if (status_ == 200) @@ -606,7 +606,7 @@ public virtual async System.Threading.Tasks.Task ListTransactionsA headers_[item_.Key] = item_.Value; } - ProcessResponse(client_, response_); + await ProcessResponse(client_, response_); var status_ = (int)response_.StatusCode; if (status_ == 200) @@ -829,7 +829,7 @@ public virtual async System.Threading.Tasks.Task ListCardsAsync(string ac headers_[item_.Key] = item_.Value; } - ProcessResponse(client_, response_); + await ProcessResponse(client_, response_); var status_ = (int)response_.StatusCode; if (status_ == 200) @@ -1051,7 +1051,7 @@ public virtual async System.Threading.Tasks.Task ListRolesAsync(string ac headers_[item_.Key] = item_.Value; } - ProcessResponse(client_, response_); + await ProcessResponse(client_, response_); var status_ = (int)response_.StatusCode; if (status_ == 200) diff --git a/src/Altinn.Dan.Plugin.Banking/Clients/V2/Bankv2.cs b/src/Altinn.Dan.Plugin.Banking/Clients/V2/Bankv2.cs index 34997df..16ef425 100644 --- a/src/Altinn.Dan.Plugin.Banking/Clients/V2/Bankv2.cs +++ b/src/Altinn.Dan.Plugin.Banking/Clients/V2/Bankv2.cs @@ -26,20 +26,8 @@ public async partial Task ProcessResponse(HttpClient client, HttpResponseMessage if (!response.IsSuccessStatusCode) return; - bool isAppJose = false; - - if (response.Headers.TryGetValues("content-type", out IEnumerable headervalues)) - { - isAppJose = headervalues.Any(x => x == "application/jose"); - } - var jwt = await response.Content.ReadAsStringAsync(); - - var decryptedContent = - JWT.Decode(jwt, - DecryptionCertificate - .GetRSAPrivateKey()); //, JweAlgorithm.RSA_OAEP_256, JweEncryption.A128CBC_HS256); - + var decryptedContent = JWT.Decode(jwt, DecryptionCertificate.GetRSAPrivateKey()); //, JweAlgorithm.RSA_OAEP_256, JweEncryption.A128CBC_HS256); response.Content = new StringContent(decryptedContent, Encoding.UTF8); } diff --git a/src/Altinn.Dan.Plugin.Banking/Metadata.cs b/src/Altinn.Dan.Plugin.Banking/Metadata.cs index 2e9d83a..af42082 100644 --- a/src/Altinn.Dan.Plugin.Banking/Metadata.cs +++ b/src/Altinn.Dan.Plugin.Banking/Metadata.cs @@ -154,12 +154,6 @@ public List GetEvidenceCodes() Required = false }, new EvidenceParameter() - { - EvidenceParamName = "SkipKAR", - ParamType = EvidenceParamType.Boolean, - Required = false - }, - new EvidenceParameter() { EvidenceParamName = "ReferanseId", ParamType = EvidenceParamType.String, @@ -216,12 +210,6 @@ public List GetEvidenceCodes() Required = false }, new EvidenceParameter() - { - EvidenceParamName = "SkipKAR", - ParamType = EvidenceParamType.Boolean, - Required = false - }, - new EvidenceParameter() { EvidenceParamName = "ReferanseId", ParamType = EvidenceParamType.String, diff --git a/src/Altinn.Dan.Plugin.Banking/Plugin.cs b/src/Altinn.Dan.Plugin.Banking/Plugin.cs index 8dcd00d..0944d40 100644 --- a/src/Altinn.Dan.Plugin.Banking/Plugin.cs +++ b/src/Altinn.Dan.Plugin.Banking/Plugin.cs @@ -1,6 +1,7 @@ using Altinn.Dan.Plugin.Banking.Config; using Altinn.Dan.Plugin.Banking.Exceptions; using Altinn.Dan.Plugin.Banking.Models; +using Altinn.Dan.Plugin.Banking.Services; using Altinn.Dan.Plugin.Banking.Services.Interfaces; using Azure.Core.Serialization; using Dan.Common; @@ -29,6 +30,7 @@ public class Plugin { private readonly IBankService _bankService; private readonly IKARService _karService; + private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; private readonly HttpClient _client; private readonly ApplicationSettings _settings; @@ -39,6 +41,7 @@ public Plugin(IOptions settings, ILoggerFactory loggerFacto { _bankService = bankService; _karService = karService; + _httpClientFactory = httpClientFactory; _client = httpClientFactory.CreateClient("SafeHttpClient"); _settings = settings.Value; _logger = loggerFactory.CreateLogger(); @@ -50,58 +53,74 @@ public async Task GetBanktransaksjoner( [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, FunctionContext context) { - var evidenceHarvesterRequest = await req.ReadFromJsonAsync(); - - // For local debug, returns unenveloped JSON (but doesn't handle exceptions) - /* - var ret = await GetEvidenceValuesBankTransaksjoner(evidenceHarvesterRequest); - var response = HttpResponseData.CreateResponse(req); - response.Headers.Add("Content-Type", "application/json"); - var list = new List(); - - ret.ForEach(item => - { - list.Add(item.Value.ToString()); - }); - - var responseItems = JsonConvert.SerializeObject(ret); - - await response.WriteStringAsync(responseItems); - return response; */ - return await EvidenceSourceResponse.CreateResponse(req, () => GetEvidenceValuesBankTransaksjoner(evidenceHarvesterRequest)); } [Function("Kundeforhold")] public async Task GetKundeforhold( - [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, - FunctionContext context) + [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, + FunctionContext context) { - - var evidenceHarvesterRequest = await req.ReadFromJsonAsync(); - + var evidenceHarvesterRequest = await req.ReadFromJsonAsync(); return await EvidenceSourceResponse.CreateResponse(req, () => GetKundeforhold(evidenceHarvesterRequest)); } [Function("Kontotransaksjoner")] public async Task GetKontotransaksjoner( -[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, -FunctionContext context) + [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, + FunctionContext context) + { + var evidenceHarvesterRequest = await req.ReadFromJsonAsync(); + return await EvidenceSourceResponse.CreateResponse(req, () => GetKontotransaksjoner(evidenceHarvesterRequest)); + } + + [Function("Kontodetaljer")] + public async Task GetBankRelasjon( + [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, + FunctionContext context) { + var evidenceHarvesterRequest = await req.ReadFromJsonAsync(); + return await EvidenceSourceResponse.CreateResponse(req, () => GetKontodetaljer(evidenceHarvesterRequest)); + } + [Function("Kontrollinformasjon")] + public async Task GetKontrollinformasjon( + [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, + FunctionContext context) + { var evidenceHarvesterRequest = await req.ReadFromJsonAsync(); - return await EvidenceSourceResponse.CreateResponse(req, () => GetKontotransaksjoner(evidenceHarvesterRequest)); + return await EvidenceSourceResponse.CreateResponse(req, () => GetEvidenceValuesKontrollinformasjon()); + } + + [Function("OppdaterKontrollinformasjon")] + public async Task UpdateKontrollinformasjon( + [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData req, + FunctionContext context) + { + var endpoints = await ReadEndpointsAndCache(); + + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(new EndpointsList() { Endpoints = endpoints, Total = endpoints.Count }); + return response; + } + + [Function(Constants.EvidenceSourceMetadataFunctionName)] + public async Task Metadata( + [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req, FunctionContext context) + { + _logger.LogInformation($"Running metadata for {Constants.EvidenceSourceMetadataFunctionName}"); + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(new Metadata().GetEvidenceCodes(), new NewtonsoftJsonObjectSerializer(new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Auto })); + return response; } private async Task> GetKontotransaksjoner(EvidenceHarvesterRequest evidenceHarvesterRequest) { var accountInfoRequestId = Guid.NewGuid(); var correlationId = Guid.NewGuid(); - - var endpoints = await GetEndpoints(); - + var bankEndpoints = await GetBankEndpoints(); var ssn = evidenceHarvesterRequest.SubjectParty?.NorwegianSocialSecurityNumber; try @@ -115,24 +134,18 @@ private async Task> GetKontotransaksjoner(EvidenceHarvesterR ? paramToDate : DateTime.Now; - bool skipKAR = evidenceHarvesterRequest.TryGetParameter("SkipKAR", out bool paramSkipKAR) ? paramSkipKAR : false; - //accountinforequestid must be provided in parameter in order to maintain the correct use across requests from different users of digitalt dødsbo accountInfoRequestId = evidenceHarvesterRequest.TryGetParameter("ReferanseId", out string accountInfoRequestIdFromParam) ? new Guid(accountInfoRequestIdFromParam) : accountInfoRequestId; - var accountRef = evidenceHarvesterRequest.TryGetParameter("Kontoreferanse", out string accountRefParam) ? accountRefParam : string.Empty; - var orgno = evidenceHarvesterRequest.TryGetParameter("Organisasjonsnummer", out string orgnoParam) ? orgnoParam : string.Empty; - - var filteredEndpoints = endpoints.Where(item => _settings.ImplementedBanks.Contains(item.OrgNo) && item.OrgNo == orgno && item.Version.ToUpper() == "V2").ToList(); - - + var filteredEndpoint = bankEndpoints.Where(item => _settings.ImplementedBanks.Contains(item.OrgNo) && item.OrgNo == orgno && item.Version.ToUpper() == "V2").FirstOrDefault(); var ecb = new EvidenceBuilder(new Metadata(), "Kontotransaksjoner"); - var transactions = await _bankService.GetTransactionsForAccount(ssn, filteredEndpoints, fromDate, toDate, accountInfoRequestId, accountRef); - ecb.AddEvidenceValue("default", JsonConvert.SerializeObject(transactions), "", false); + var bankConfig = CreateBankConfigurations(filteredEndpoint); + var transactions = await _bankService.GetTransactionsForAccount(ssn, bankConfig, fromDate, toDate, accountInfoRequestId, accountRef); + ecb.AddEvidenceValue("default", JsonConvert.SerializeObject(transactions), "", false); return ecb.GetEvidenceValues(); } catch (Exception e) @@ -151,9 +164,7 @@ private async Task> GetKundeforhold(EvidenceHarvesterRequest { var accountInfoRequestId = Guid.NewGuid(); var correlationId = Guid.NewGuid(); - - var endpoints = await GetEndpoints(); - + var endpoints = await GetBankEndpoints(); var ssn = evidenceHarvesterRequest.SubjectParty?.NorwegianSocialSecurityNumber; try @@ -189,11 +200,19 @@ private async Task> GetKundeforhold(EvidenceHarvesterRequest throw new EvidenceSourceTransientException(Banking.Metadata.ERROR_KAR_NOT_AVAILABLE_ERROR, $"Request to KAR timed out (accountInfoRequestId: {accountInfoRequestId}, correlationID: {correlationId})"); } - var filteredEndpoints = endpoints.Where(p => karResponse.Banks.Select(e => e.OrganizationID).ToHashSet().Contains(p.OrgNo)).Where(item => _settings.ImplementedBanks.Contains(item.OrgNo)).ToList(); - - //We are only legally allowed to use endpoints with version V2 due to the onlyPrimaryOwner flag - filteredEndpoints.RemoveAll(p => p.Version.ToUpper() == "V1"); - filteredEndpoints.ForEach(p => p.Url = null); + var filteredEndpoints = endpoints + .Where(x => karResponse.Banks.Select(e => e.OrganizationID).ToHashSet().Contains(x.OrgNo)) + .Where(x => _settings.ImplementedBanks.Contains(x.OrgNo)) + .Where(x => x.Version.ToUpper() != "V1") + .Select(x => new EndpointExternal + { + OrgNo = x.OrgNo, + Env = x.Env, + Name = x.Name, + Url = null, + Version = x.Version + }) + .ToList(); var ecb = new EvidenceBuilder(new Metadata(), "Kundeforhold"); @@ -209,29 +228,14 @@ private async Task> GetKundeforhold(EvidenceHarvesterRequest "Kundeforhold failed unexpectedly for {Subject}, error {Error} (accountInfoRequestId: {AccountInfoRequestId}, correlationID: {CorrelationId})", evidenceHarvesterRequest.SubjectParty.GetAsString(), e.Message, accountInfoRequestId, correlationId); throw new EvidenceSourceTransientException(Banking.Metadata.ERROR_BANK_REQUEST_ERROR, "Could not retrieve bank transactions"); - } } - - [Function("Kontodetaljer")] - public async Task GetBankRelasjon( - [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, - FunctionContext context) - { - - var evidenceHarvesterRequest = await req.ReadFromJsonAsync(); - - return await EvidenceSourceResponse.CreateResponse(req, () => GetKontodetaljer(evidenceHarvesterRequest)); - } - private async Task> GetKontodetaljer(EvidenceHarvesterRequest evidenceHarvesterRequest) { var accountInfoRequestId = Guid.NewGuid(); var correlationId = Guid.NewGuid(); - - var endpoints = await GetEndpoints(); - + var endpoints = await GetBankEndpoints(); var ssn = evidenceHarvesterRequest.SubjectParty?.NorwegianSocialSecurityNumber; try @@ -245,28 +249,23 @@ private async Task> GetKontodetaljer(EvidenceHarvesterReques ? paramToDate : DateTime.Now; - bool skipKAR = evidenceHarvesterRequest.TryGetParameter("SkipKAR", out bool paramSkipKAR) ? paramSkipKAR : false; - var orgno = evidenceHarvesterRequest.TryGetParameter("Organisasjonsnummer", out string paramOrgNo) ? paramOrgNo : null; - - bool includeTransactions = evidenceHarvesterRequest.TryGetParameter("InkluderTransaksjoner", out bool paramIncludeTransactions) ? paramIncludeTransactions : true; + var includeTransactions = evidenceHarvesterRequest.TryGetParameter("InkluderTransaksjoner", out bool paramIncludeTransactions) ? paramIncludeTransactions : true; //accountinforequestid must be provided in parameter in order to maintain the correct use across requests from different users of digitalt dødsbo accountInfoRequestId = evidenceHarvesterRequest.TryGetParameter("ReferanseId", out string accountInfoRequestIdFromParam) ? new Guid(accountInfoRequestIdFromParam) : accountInfoRequestId; - - var filteredEndpoints = endpoints.Where(item => _settings.ImplementedBanks.Contains(item.OrgNo) && item.OrgNo == orgno && item.Version.ToUpper() == "V2").ToList(); - if (string.IsNullOrEmpty(orgno) || filteredEndpoints.Count() != 1) + if (string.IsNullOrEmpty(orgno) || filteredEndpoints.Count != 1) { - throw new EvidenceSourceTransientException(Banking.Metadata.ERROR_BANK_REQUEST_ERROR, "Invalid organisation number provided"); + throw new EvidenceSourceTransientException(Banking.Metadata.ERROR_BANK_REQUEST_ERROR, "Invalid organisation number provided"); } var ecb = new EvidenceBuilder(new Metadata(), "Kontodetaljer"); + var bankConfigs = CreateBankConfigurations(filteredEndpoints); + var bankResult = await _bankService.GetAccounts(ssn, bankConfigs, fromDate, toDate, accountInfoRequestId, includeTransactions); - BankResponse bankResult = await _bankService.GetTransactions(ssn, filteredEndpoints, fromDate, toDate, accountInfoRequestId, includeTransactions); ecb.AddEvidenceValue("default", JsonConvert.SerializeObject(bankResult), "", false); - return ecb.GetEvidenceValues(); } catch (Exception e) @@ -277,29 +276,18 @@ private async Task> GetKontodetaljer(EvidenceHarvesterReques "Kontodetaljer failed unexpectedly for {Subject}, error {Error} (accountInfoRequestId: {AccountInfoRequestId}, correlationID: {CorrelationId})", evidenceHarvesterRequest.SubjectParty.GetAsString(), e.Message, accountInfoRequestId, correlationId); throw new EvidenceSourceTransientException(Banking.Metadata.ERROR_BANK_REQUEST_ERROR, "Could not retrieve account details"); - } } - [Function("Kontrollinformasjon")] - public async Task GetKontrollinformasjon( - [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, - FunctionContext context) - { - var evidenceHarvesterRequest = await req.ReadFromJsonAsync(); - - return await EvidenceSourceResponse.CreateResponse(req, () => GetEvidenceValuesKontrollinformasjon()); - } - private async Task> GetEvidenceValuesKontrollinformasjon() { var ecb = new EvidenceBuilder(new Metadata(), "Kontrollinformasjon"); - ecb.AddEvidenceValue("default", JsonConvert.SerializeObject(await GetEndpoints()), "BITS", false); + ecb.AddEvidenceValue("default", JsonConvert.SerializeObject(await GetBankEndpoints()), "BITS", false); return ecb.GetEvidenceValues(); } - private async Task> GetEndpoints() + private async Task> GetBankEndpoints() { (bool hasCachedValue, var endpoints) = await _memCache.TryGetEndpoints(ENDPOINTS_KEY); @@ -311,10 +299,35 @@ private async Task> GetEndpoints() return endpoints; } + private Dictionary CreateBankConfigurations(List banks) + { + Dictionary bankConfigs = []; + foreach (var bank in banks) + { + var httpClient = _httpClientFactory.CreateClient(bank.OrgNo); + httpClient.BaseAddress = new Uri(bank.Url); + + bankConfigs.Add(bank.OrgNo, new BankConfig() + { + BankAudience = bank.Url, + Client = httpClient, + MaskinportenEnv = _settings.MaskinportenEnvironment, + ApiVersion = bank.Version, + Name = bank.Name, + OrgNo = bank.OrgNo + }); + } + + return bankConfigs; + } + + private BankConfig CreateBankConfigurations(EndpointExternal bank) + => CreateBankConfigurations([bank]).Single().Value; + private async Task> ReadEndpointsAndCache() { var file = await GetFileFromGithub(); - // var file = Encoding.UTF8.GetString(bytes,0, bytes.Length); + // var file = Encoding.UTF8.GetString(bytes,0, bytes.Length); var engine = new DelimitedFileEngine(Encoding.UTF8); var endpoints = engine.ReadString(file).ToList(); @@ -322,11 +335,12 @@ private async Task> ReadEndpointsAndCache() List result = new List(); _logger.LogInformation($"Endpoints parsed from csv - {engine.TotalRecords} to be cached"); - if (engine.TotalRecords > 0 && endpoints.Count>0) - { + if (engine.TotalRecords > 0 && endpoints.Count > 0) + { result = await _memCache.SetEndpointsCache(ENDPOINTS_KEY, endpoints, TimeSpan.FromMinutes(60)); _logger.LogInformation($"Cache refresh completed - total of {engine.TotalRecords} cached"); - } else + } + else { _logger.LogCritical($"Plugin func-es-banking no endpoints found in csv!!!"); } @@ -355,25 +369,12 @@ private async Task GetFileFromGithub() } } - - [Function("OppdaterKontrollinformasjon")] - public async Task UpdateKontrollinformasjon( - [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData req, - FunctionContext context) - { - var endpoints = await ReadEndpointsAndCache(); - - var response = req.CreateResponse(HttpStatusCode.OK); - await response.WriteAsJsonAsync(new EndpointsList() { Endpoints = endpoints, Total = endpoints.Count }); - return response; - } - private async Task> GetEvidenceValuesBankTransaksjoner(EvidenceHarvesterRequest evidenceHarvesterRequest) { var accountInfoRequestId = Guid.NewGuid(); var correlationId = Guid.NewGuid(); - var endpoints = await GetEndpoints(); + var endpoints = await GetBankEndpoints(); var ssn = evidenceHarvesterRequest.SubjectParty?.NorwegianSocialSecurityNumber; @@ -410,14 +411,21 @@ private async Task> GetEvidenceValuesBankTransaksjoner(Evide throw new EvidenceSourceTransientException(Banking.Metadata.ERROR_KAR_NOT_AVAILABLE_ERROR, $"Request to KAR timed out (accountInfoRequestId: {accountInfoRequestId}, correlationID: {correlationId})"); } - var filteredEndpoints = endpoints.Where(p => karResponse.Banks.Select(e => e.OrganizationID).ToHashSet().Contains(p.OrgNo)).Where(item =>_settings.ImplementedBanks.Contains(item.OrgNo)).ToList(); + var filteredEndpoints = endpoints.Where(p => karResponse.Banks.Select(e => e.OrganizationID).ToHashSet().Contains(p.OrgNo)).Where(item => _settings.ImplementedBanks.Contains(item.OrgNo)).ToList(); //We are only legally allowed to use endpoints with version V2 due to the onlyPrimaryOwner flag filteredEndpoints.RemoveAll(p => p.Version.ToUpper() == "V1"); var ecb = new EvidenceBuilder(new Metadata(), "Banktransaksjoner"); - BankResponse bankResult = karResponse.Banks.Count > 0 ? await _bankService.GetTransactions(ssn, filteredEndpoints, fromDate, toDate, accountInfoRequestId) : new() { BankAccounts = new()}; + var bankConfigs = CreateBankConfigurations(filteredEndpoints); + BankResponse bankResult = karResponse.Banks.Count > 0 ? await _bankService.GetAccounts(ssn, bankConfigs, fromDate, toDate, accountInfoRequestId) : new() { BankAccounts = new() }; + + //Add banks with implemented = false if they are in the response from KAR but not supported by digitalt dødsbo + foreach (var bank in karResponse.Banks.Where(p => !filteredEndpoints.Select(e => e.OrgNo).Contains(p.OrganizationID))) + { + bankResult.BankAccounts.Add(new BankInfo() { BankName = bank.BankName, IsImplemented = false }); + } ecb.AddEvidenceValue("default", JsonConvert.SerializeObject(bankResult), "", false); return ecb.GetEvidenceValues(); @@ -433,15 +441,5 @@ private async Task> GetEvidenceValuesBankTransaksjoner(Evide } } - - [Function(Constants.EvidenceSourceMetadataFunctionName)] - public async Task Metadata( - [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req, FunctionContext context) - { - _logger.LogInformation($"Running metadata for {Constants.EvidenceSourceMetadataFunctionName}"); - var response = req.CreateResponse(HttpStatusCode.OK); - await response.WriteAsJsonAsync(new Metadata().GetEvidenceCodes(), new NewtonsoftJsonObjectSerializer(new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Auto })); - return response; - } } } diff --git a/src/Altinn.Dan.Plugin.Banking/Services/BankService.cs b/src/Altinn.Dan.Plugin.Banking/Services/BankService.cs index 6509920..096a8e0 100644 --- a/src/Altinn.Dan.Plugin.Banking/Services/BankService.cs +++ b/src/Altinn.Dan.Plugin.Banking/Services/BankService.cs @@ -22,8 +22,6 @@ public class BankService : IBankService private readonly IMaskinportenService _maskinportenService; private readonly ILogger _logger; private readonly ApplicationSettings _settings; - private readonly Dictionary _bankConfigs = []; - private const int TransactionRequestTimeoutSecs = 30; private const int AccountDetailsRequestTimeoutSecs = 30; private const int AccountListRequestTimeoutSecs = 30; @@ -35,33 +33,23 @@ public BankService(ILoggerFactory loggerFactory, IMaskinportenService maskinport _settings = applicationSettings.Value; } - public async Task GetTransactions(string ssn, List bankList, DateTimeOffset? fromDate, DateTimeOffset? toDate, Guid accountInfoRequestId, bool includeTransactions = true) + public async Task GetAccounts(string ssn, Dictionary bankList, DateTimeOffset? fromDate, DateTimeOffset? toDate, Guid accountInfoRequestId, bool includeTransactions = true) { - Configure(bankList); - - - BankResponse bankResponse = new BankResponse { BankAccounts = new List() }; + BankResponse bankResponse = new BankResponse { BankAccounts = [] }; var bankTasks = new List>(); foreach (var bank in bankList) { - var correlationId = Guid.NewGuid(); bankTasks.Add(Task.Run(async () => { - string orgnr = bank.OrgNo; - string name = bank.Name; - BankInfo bankInfo; try { - bankList.ForEach(bank => - _logger.LogInformation($"Preparing request to bank {bank.Name}, url {bank.Url}, version {bank.Version}, accountinforequestid {accountInfoRequestId}, correlationid {correlationId}, fromdate {fromDate}, todate {toDate}") - ); - bankInfo = await InvokeBank(ssn, orgnr, fromDate, toDate, accountInfoRequestId, correlationId, includeTransactions); + bankInfo = await InvokeBank(ssn, bank.Value, fromDate, toDate, accountInfoRequestId, includeTransactions); } catch (Exception e) { - bankInfo = new BankInfo { Accounts = new List(), HasErrors = true }; + bankInfo = new BankInfo { Accounts = [], HasErrors = true }; string correlationId = string.Empty; if (e is ApiException k) { @@ -69,12 +57,10 @@ public async Task GetTransactions(string ssn, List GetTransactions(string ssn, List InvokeBank(string ssn, string orgnr, DateTimeOffset? fromDate, DateTimeOffset? toDate, Guid accountInfoRequestId, Guid correlationId, bool includeTransactions = true) + private async Task InvokeBank(string ssn, BankConfig bank, DateTimeOffset? fromDate, DateTimeOffset? toDate, Guid accountInfoRequestId, bool includeTransactions = true) { - if (!_bankConfigs.ContainsKey(orgnr)) - return new BankInfo { Accounts = new List(), IsImplemented = false }; - - BankConfig bankConfig = _bankConfigs[orgnr]; - var token = await _maskinportenService.GetToken(_settings.Jwk, bankConfig.MaskinportenEnv, _settings.ClientId, _settings.BankScope, bankConfig.BankAudience); + var token = await _maskinportenService.GetToken(_settings.Jwk, bank.MaskinportenEnv, _settings.ClientId, _settings.BankScope, bank.BankAudience); + bank.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); - bankConfig.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); - - var bankClient = new Bank_v2.Bank_v2(bankConfig.Client, _settings) + var bankClient = new Bank_v2.Bank_v2(bank.Client, _settings) { - BaseUrl = bankConfig.Client.BaseAddress?.ToString(), + BaseUrl = bank.Client.BaseAddress?.ToString(), DecryptionCertificate = _settings.OedDecryptCert }; var accountListTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(AccountListRequestTimeoutSecs)); + var accounts = await GetAllAccounts(bankClient, bank, accountInfoRequestId, ssn, fromDate, toDate); - var accounts = await bankClient.ListAccountsAsync(accountInfoRequestId, correlationId, "OED", ssn, true, null, null, null, fromDate, toDate); - - _logger.LogInformation("Found {0} accounts for {1} in bank {2} with accountinforequestid{3} and correlationid {4}", accounts.Accounts1.Count, ssn.Substring(0, 6), orgnr, accountInfoRequestId, correlationId); - return await GetAccountDetailsV2(bankClient, accounts, accountInfoRequestId, fromDate, toDate, includeTransactions); //application/jose + var x = await GetAccountDetailsV2(bankClient, accounts, bank, accountInfoRequestId, fromDate, toDate, includeTransactions); + return x; } - private async Task GetAccountDetailsV2(Bank_v2.Bank_v2 bankClient, Bank_v2.Accounts accounts, Guid accountInfoRequestId, DateTimeOffset? fromDate, DateTimeOffset? toDate, bool includeTransactions = true) + + private async Task GetAccountDetailsV2(Bank_v2.Bank_v2 bankClient, Accounts accounts, BankConfig bank, Guid accountInfoRequestId, DateTimeOffset? fromDate, DateTimeOffset? toDate, bool includeTransactions = true) { - BankInfo bankInfo = new BankInfo() { Accounts = new List() }; - var accountDetailsTasks = new List>(); + var bankInfo = new BankInfo() { Accounts = [] }; + var transactions = new Transactions(); foreach (Bank_v2.Account account in accounts.Accounts1) { - Guid correlationIdDetails = Guid.NewGuid(); - Guid correlationIdTransactions = Guid.NewGuid(); - Task transactionsTask = null; + var details = await GetAccountById(bankClient, account, bank, accountInfoRequestId, fromDate, toDate); - accountDetailsTasks.Add(Task.Run(async () => + if (details.Account == null) { - if (includeTransactions) - { - // Start fetching transactions concurrently - var transactionsTimeout = - new CancellationTokenSource(TimeSpan.FromSeconds(TransactionRequestTimeoutSecs)); + // Some test accounts come up with an empty response from the bank here (just '{ "responseStatus": "complete" }'. + // We skip those by returning an empty AccountDto. + continue; + } - _logger.LogInformation("Getting transactions: bank {0} account {1} dob {2} accountinforequestid {3} correlationid {4}", - account.Servicer.Name, account.AccountIdentifier, account.PrimaryOwner?.Identifier?.Value?.Substring(0, 6), accountInfoRequestId, correlationIdTransactions); + var availableCredit = details.Account.Balances.FirstOrDefault(b => + b.Type == BalanceType.AvailableBalance && b.CreditDebitIndicator == CreditOrDebit.Credit) + ?.Amount ?? 0; + var availableDebit = details.Account.Balances.FirstOrDefault(b => + b.Type == BalanceType.AvailableBalance && b.CreditDebitIndicator == CreditOrDebit.Debit) + ?.Amount ?? 0; + + var bookedCredit = details.Account.Balances.FirstOrDefault(b => + b.Type == BalanceType.BookedBalance && b.CreditDebitIndicator == CreditOrDebit.Credit) + ?.Amount ?? 0; + var bookedDebit = details.Account.Balances.FirstOrDefault(b => + b.Type == BalanceType.BookedBalance && b.CreditDebitIndicator == CreditOrDebit.Debit) + ?.Amount ?? 0; + + if (includeTransactions) + { + transactions = await ListTransactionsForAccount(bankClient, account, bank, accountInfoRequestId, fromDate, toDate); + } - transactionsTask = bankClient.ListTransactionsAsync(account.AccountReference, accountInfoRequestId, - correlationIdTransactions, "OED", null, null, null, fromDate, toDate, transactionsTimeout.Token); - } + var internalAccount = MapToInternalV2(account, details.Account, transactions?.Transactions1, availableCredit - availableDebit, bookedCredit - bookedDebit); + if (internalAccount.AccountDetail != null) + { + bankInfo.Accounts.Add(internalAccount); + } + } - _logger.LogInformation("Getting account details: bank {0} account {1} dob {2} accountinforequestid {4} correlationid {5}", - account.Servicer.Name, account.AccountIdentifier, account.PrimaryOwner?.Identifier?.Value?.Substring(0, 6), accountInfoRequestId, correlationIdDetails); + return bankInfo; + } + private async Task GetAllAccounts(Bank_v2.Bank_v2 bankClient, BankConfig bank, Guid accountInfoRequestId, string ssn, DateTimeOffset? fromDate, DateTimeOffset? toDate) + { + var correlationId = Guid.NewGuid(); - 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("Preparing request to {BankName}, url {BankAudience}, version {BankApiVersion}, accountinforequestid {AccountInfoRequestId}, correlationid {CorrelationId}, fromdate {FromDate}, todate {ToDate}", + bank.Name, bank.BankAudience, bank.ApiVersion, accountInfoRequestId, correlationId, fromDate, toDate); - _logger.LogInformation("Retrieved account details: bank {0} account {1} dob {2} details {3} accountinforequestid {4} correlationid {5}", - account.Servicer.Name, account.AccountIdentifier, account.PrimaryOwner?.Identifier?.Value?.Substring(0, 6), details.ResponseDetails.Status, accountInfoRequestId, correlationIdDetails); + var accounts = await bankClient.ListAccountsAsync(accountInfoRequestId, correlationId, "OED", ssn, true, null, null, null, fromDate, toDate); - if (details.Account == null) - { - // Some test accounts come up with an empty response from the bank here (just '{ "responseStatus": "complete" }'. - // We skip those by returning an empty AccountDto. - return new AccountDtoV2(); - } + _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); - var availableCredit = details.Account.Balances.FirstOrDefault(b => - b.Type == Altinn.Dan.Plugin.Banking.Clients.V2.BalanceType.AvailableBalance && b.CreditDebitIndicator == Altinn.Dan.Plugin.Banking.Clients.V2.CreditOrDebit.Credit) - ?.Amount ?? 0; - var availableDebit = details.Account.Balances.FirstOrDefault(b => - b.Type == Altinn.Dan.Plugin.Banking.Clients.V2.BalanceType.AvailableBalance && b.CreditDebitIndicator == Altinn.Dan.Plugin.Banking.Clients.V2.CreditOrDebit.Debit) - ?.Amount ?? 0; - - var bookedCredit = details.Account.Balances.FirstOrDefault(b => - b.Type == Altinn.Dan.Plugin.Banking.Clients.V2.BalanceType.BookedBalance && b.CreditDebitIndicator == Altinn.Dan.Plugin.Banking.Clients.V2.CreditOrDebit.Credit) - ?.Amount ?? 0; - var bookedDebit = details.Account.Balances.FirstOrDefault(b => - b.Type == Altinn.Dan.Plugin.Banking.Clients.V2.BalanceType.BookedBalance && b.CreditDebitIndicator == Altinn.Dan.Plugin.Banking.Clients.V2.CreditOrDebit.Debit) - ?.Amount ?? 0; - - if (includeTransactions) - { - await transactionsTask; - _logger.LogInformation("Retrieved transactions: bank {0} account {1} dob {2} transaction count {3} accountinforequestid {4} correlationid {5}", - account.Servicer.Name, account.AccountIdentifier, account.PrimaryOwner?.Identifier?.Value?.Substring(0, 6), transactionsTask.Result.Transactions1?.Count, accountInfoRequestId, correlationIdTransactions); - } + return accounts; + } + private async Task GetAccountById(Bank_v2.Bank_v2 bankClient, Bank_v2.Account account, BankConfig bank, Guid accountInfoRequestId, DateTimeOffset? fromDate, DateTimeOffset? toDate) + { + Guid correlationIdDetails = Guid.NewGuid(); - return MapToInternalV2(account.Type, details.Account, transactionsTask?.Result?.Transactions1, availableCredit - availableDebit, bookedCredit - bookedDebit); - })); - } + _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); - await Task.WhenAll(accountDetailsTasks); + var detailsTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(AccountDetailsRequestTimeoutSecs)); + var details = await bankClient.ShowAccountByIdAsync(account.AccountReference, accountInfoRequestId, + correlationIdDetails, "OED", null, null, null, fromDate, toDate, detailsTimeout.Token); - foreach (var accountDetailsTask in accountDetailsTasks.Where(accountDetailsTask => accountDetailsTask.Result.AccountDetail != null)) - { - bankInfo.Accounts.Add(accountDetailsTask.Result); - } + _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); - return bankInfo; + return details; + } + + private async Task ListTransactionsForAccount(Bank_v2.Bank_v2 bankClient, Bank_v2.Account account, BankConfig bank, Guid accountInfoRequestId, DateTimeOffset? fromDate, DateTimeOffset? toDate) + { + Guid correlationIdTransactions = Guid.NewGuid(); + + // 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); + + 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); + + return transactions; } /*private AccountDto MapFromAccountDTOV2TOV1(AccountDtoV2 result) @@ -269,17 +265,20 @@ private async Task GetAccountDetailsV2(Bank_v2.Bank_v2 bankClient, Ban } */ private AccountDtoV2 MapToInternalV2( - Bank_v2.AccountType accountType, - Bank_v2.AccountDetail detail, - ICollection transactions, + Bank_v2.Account account, + AccountDetail detail, + ICollection transactions, decimal availableBalance, decimal bookedBalance) { - detail.Type = accountType; + detail.Type = account.Type; + detail.AccountIdentifier = account.AccountIdentifier; + detail.AccountReference = account.AccountReference; + // P.t. almost passthrough mapping return new AccountDtoV2 { - AccountNumber = detail.AccountIdentifier, + AccountNumber = account.AccountIdentifier, AccountDetail = detail, Transactions = transactions, AccountAvailableBalance = availableBalance, @@ -287,36 +286,13 @@ private AccountDtoV2 MapToInternalV2( }; } - private void Configure(List banks) + public async Task GetTransactionsForAccount(string ssn, BankConfig bankConfig, DateTime fromDate, DateTime toDate, Guid accountInfoRequestId, string accountReference) { - foreach (var bank in banks) - { - _bankConfigs.Add(bank.OrgNo, new BankConfig() - { - BankAudience = bank.Url,//.ToUpper().Replace("V1", "V2"), - Client = new HttpClient { BaseAddress = new Uri(bank.Url) }, - MaskinportenEnv = _settings.MaskinportenEnvironment, - ApiVersion = bank.Version - }); - } - } - - public async Task GetTransactionsForAccount(string ssn, List endpoints, DateTime fromDate, DateTime toDate, Guid accountInfoRequestId, string accountReference) - { - Configure(endpoints); - var correlationId = Guid.NewGuid(); - var transactionsTimeout = - new CancellationTokenSource(TimeSpan.FromSeconds(TransactionRequestTimeoutSecs)); - var endpoint = endpoints.First(); - - _logger.LogInformation("Getting transactions: bank {0} accountrefence {1} dob {2} accountinforequestid {3} correlationid {4}", - endpoint.Name, accountReference, ssn.Substring(0, 6), accountInfoRequestId, correlationId); - - if (!_bankConfigs.ContainsKey(endpoint.OrgNo)) - return new Transactions(); + var transactionsTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(TransactionRequestTimeoutSecs)); - BankConfig bankConfig = _bankConfigs[endpoint.OrgNo]; + _logger.LogInformation("Getting transactions: bank {BankName} accountrefence {AccountReference} dob {DateOfBirth} accountinforequestid {AccountInfoRequestId} correlationid {CorrelationId}", + bankConfig.Name, accountReference, ssn[..6], accountInfoRequestId, correlationId); var token = await _maskinportenService.GetToken(_settings.Jwk, bankConfig.MaskinportenEnv, _settings.ClientId, _settings.BankScope, bankConfig.BankAudience); bankConfig.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); @@ -326,17 +302,16 @@ public async Task GetTransactionsForAccount(string ssn, List GetTransactions(string ssn, List bankList, DateTimeOffset? fromDate, DateTimeOffset? toDate, Guid accountInfoRequestId, bool includeTransactions = true); - Task GetTransactionsForAccount(string ssn, List filteredEndpoints, DateTime fromDate, DateTime toDate, Guid accountInfoRequestId, string accountReference); + Task GetAccounts(string ssn, Dictionary bankList, DateTimeOffset? fromDate, DateTimeOffset? toDate, Guid accountInfoRequestId, bool includeTransactions = true); + Task GetTransactionsForAccount(string ssn, BankConfig bankConfig, DateTime fromDate, DateTime toDate, Guid accountInfoRequestId, string accountReference); } diff --git a/src/Altinn.Dan.Plugin.Banking/Services/KARService.cs b/src/Altinn.Dan.Plugin.Banking/Services/KARService.cs index 47944f4..2f7e113 100644 --- a/src/Altinn.Dan.Plugin.Banking/Services/KARService.cs +++ b/src/Altinn.Dan.Plugin.Banking/Services/KARService.cs @@ -36,7 +36,7 @@ public async Task GetBanksForCustomer(string ssn, DateTimeOffset fr { if (skipKAR) { - _Logger.LogInformation("Skipping KAR for accountinforequestid {accountinforequestid} and correlationid {correlationId}", accountInfoRequestId.ToString(), correlationId.ToString()); + _Logger.LogInformation("Skipping KAR for accountinforequestid {AccountInfoRequestId} and correlationid {CorrelationId}", accountInfoRequestId, correlationId); return await GetAllImplementedBanks(); } @@ -53,12 +53,12 @@ public async Task GetBanksForCustomer(string ssn, DateTimeOffset fr var result = await kar.Get(ssn, token.AccessToken, fromDate.ToString("yyyy-MM-dd"), toDate.ToString("yyyy-MM-dd"), accountInfoRequestId, correlationId, karTimeout.Token); - _Logger.LogInformation("Retrieved from Kar {accountsCount} from accountinforequestid {accountinforequestid} and correlationid {correlationid}", result.Banks.Count, accountInfoRequestId.ToString(), correlationId.ToString()); + _Logger.LogInformation("Retrieved from Kar {NumberOfBanks} from accountinforequestid {AccountInfoRequestId} and correlationid {CorrelationId}", result.Banks.Count, accountInfoRequestId, correlationId); return result; } - private static readonly KARResponse ImplementedBanksCache = new() { Banks = new List() }; + private static readonly KARResponse ImplementedBanksCache = new() { Banks = [] }; private async Task GetAllImplementedBanks() { if (ImplementedBanksCache.Banks.Count > 0) return ImplementedBanksCache;