Skip to content

Commit

Permalink
Update authentication request retrieval logic
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinTTY committed Nov 1, 2024
1 parent d9b077d commit 3c2a68b
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using HotChocolate.Types;
using HotChocolate.Data;
using HotChocolate.Types;
using RobinTTY.PersonalFinanceDashboard.Core.Models;
using RobinTTY.PersonalFinanceDashboard.Infrastructure.Repositories;

Expand All @@ -15,8 +16,10 @@ public class AuthenticationRequestQueryResolvers
/// </summary>
/// <param name="repository">The injected repository to use for data retrieval.</param>
/// <param name="authenticationId">The id of the authentication request to retrieve.</param>
public async Task<AuthenticationRequest?> GetAuthenticationRequest(AuthenticationRequestRepository repository,
string authenticationId)
[UseSingleOrDefault]
[UseProjection]
public async Task<IQueryable<AuthenticationRequest?>> GetAuthenticationRequest(
AuthenticationRequestRepository repository, string authenticationId)
{
return await repository.GetAuthenticationRequest(authenticationId);
}
Expand All @@ -25,7 +28,8 @@ public class AuthenticationRequestQueryResolvers
/// Look up authentication requests.
/// </summary>
/// <param name="repository">The injected repository to use for data retrieval.</param>
public async Task<IEnumerable<AuthenticationRequest>> GetAuthenticationRequests(
[UseProjection]
public async Task<IQueryable<AuthenticationRequest>> GetAuthenticationRequests(
AuthenticationRequestRepository repository)
{
return await repository.GetAuthenticationRequests();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ public class Account
/// Transactions that are associated with this account.
/// </summary>
public List<Transaction> Transactions { get; set; }

// TODO
public Account()

Check warning on line 36 in src/server/RobinTTY.PersonalFinanceDashboard.Core/Models/Account.cs

View workflow job for this annotation

GitHub Actions / Build library

Non-nullable property 'Transactions' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
{

}

/// <summary>
/// Creates a new instance of <see cref="Account"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class AuthenticationRequest
/// <summary>
/// The ids of the accounts associated with this authentication request.
/// </summary>
public List<BankAccount> AssociatedAccounts { get; set; }
public ICollection<BankAccount> AssociatedAccounts { get; set; } = new List<BankAccount>();

/// <summary>
/// Creates a new instance of <see cref="AuthenticationRequest"/>.
Expand All @@ -31,7 +31,6 @@ public AuthenticationRequest(string id, AuthenticationStatus status, Uri authent
Id = id;
Status = status;
AuthenticationLink = authenticationLink;
AssociatedAccounts = [];
}

/// <summary>
Expand All @@ -42,7 +41,7 @@ public AuthenticationRequest(string id, AuthenticationStatus status, Uri authent
/// <param name="authenticationLink">A <see cref="Uri"/> which can be used to start the authentication via the third party provider.</param>
/// <param name="associatedAccounts">The ids of the accounts associated with this authentication request.</param>
public AuthenticationRequest(string id, AuthenticationStatus status, Uri authenticationLink,
List<BankAccount> associatedAccounts)
ICollection<BankAccount> associatedAccounts)
{
Id = id;
Status = status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public class BankAccount : Account
/// </summary>
public BankingInstitution? AssociatedInstitution { get; set; }

// TODO
public BankAccount()
{
}

/// <summary>
/// Creates a new instance of <see cref="BankAccount"/>.
/// </summary>
Expand All @@ -46,8 +51,9 @@ public class BankAccount : Account
/// <param name="ownerName">Name of the legal account owner. If there is more than one owner,
/// then two names might be noted here.</param>
/// <param name="accountType">Specifies the nature, or use, of the account.</param>
public BankAccount(Guid id, string? name, string? description, decimal? balance, string? currency, string? iban,
string? bic, string? bban, string? ownerName, string? accountType) : base(id, name, description, balance, currency)
public BankAccount(Guid id, string? name = null, string? description = null, decimal? balance = null,
string? currency = null, string? iban = null, string? bic = null, string? bban = null, string? ownerName = null,
string? accountType = null) : base(id, name, description, balance, currency)
{
Iban = iban;
Bic = bic;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
using RobinTTY.NordigenApiClient.Models.Responses;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using RobinTTY.NordigenApiClient.Models.Responses;
using RobinTTY.PersonalFinanceDashboard.Core.Models;
using RobinTTY.PersonalFinanceDashboard.Infrastructure.Services;
using RobinTTY.PersonalFinanceDashboard.ThirdPartyDataProviders;
using RobinTTY.PersonalFinanceDashboard.ThirdPartyDataProviders.Models;

namespace RobinTTY.PersonalFinanceDashboard.Infrastructure.Repositories;

Expand All @@ -9,42 +13,54 @@ namespace RobinTTY.PersonalFinanceDashboard.Infrastructure.Repositories;
/// </summary>
public class AuthenticationRequestRepository
{
private readonly ILogger<AuthenticationRequestRepository> _logger;
private readonly ApplicationDbContext _dbContext;
private readonly GoCardlessDataProviderService _dataProviderService;
private readonly ThirdPartyDataRetrievalMetadataService _dataRetrievalMetadataService;

/// <summary>
/// Creates a new instance of <see cref="AuthenticationRequestRepository"/>.
/// </summary>
/// <param name="logger">Logger used for monitoring purposes.</param>
/// <param name="dbContext">The <see cref="ApplicationDbContext"/> to use for data retrieval.</param>
/// <param name="dataProviderService">The data provider to use for data retrieval.</param>
public AuthenticationRequestRepository(ApplicationDbContext dbContext, GoCardlessDataProviderService dataProviderService)
/// <param name="dataRetrievalMetadataService">Service used to determine if the database data is stale.</param>
public AuthenticationRequestRepository(
ILogger<AuthenticationRequestRepository> logger,
ApplicationDbContext dbContext,
GoCardlessDataProviderService dataProviderService,
ThirdPartyDataRetrievalMetadataService dataRetrievalMetadataService)
{
_logger = logger;
_dbContext = dbContext;
_dataProviderService = dataProviderService;
_dataRetrievalMetadataService = dataRetrievalMetadataService;
}

/// <summary>
/// Gets the <see cref="AuthenticationRequest"/> matching the specified id.
/// </summary>
/// <param name="authenticationId">The id of the <see cref="AuthenticationRequest"/> to retrieve.</param>
/// <returns>The <see cref="AuthenticationRequest"/> if one ist matched otherwise <see langword="null"/>.</returns>
public async Task<AuthenticationRequest?> GetAuthenticationRequest(string authenticationId)
public async Task<IQueryable<AuthenticationRequest?>> GetAuthenticationRequest(string authenticationId)
{
var requests = await _dataProviderService.GetAuthenticationRequest(authenticationId);
return requests.Result!;
await RefreshAuthenticationRequestsIfStale();

return _dbContext.AuthenticationRequests.Where(authentication => authentication.Id == authenticationId);
}

/// <summary>
/// Gets a list of <see cref="AuthenticationRequest"/>s.
/// </summary>
/// <returns>A list of <see cref="AuthenticationRequest"/>s.</returns>
public async Task<IEnumerable<AuthenticationRequest>> GetAuthenticationRequests()
public async Task<IQueryable<AuthenticationRequest>> GetAuthenticationRequests()
{
await RefreshAuthenticationRequestsIfStale();

// TODO: limit
var requests = await _dataProviderService.GetAuthenticationRequests(100);
return requests.Result!;
return _dbContext.AuthenticationRequests;
}

/// <summary>
/// Adds a new <see cref="AuthenticationRequest"/>.
/// </summary>
Expand All @@ -68,4 +84,67 @@ public async Task<BasicResponse> DeleteAuthenticationRequest(string authenticati
var request = await _dataProviderService.DeleteAuthenticationRequest(authenticationId);
return request.Result!;
}
}

/// <summary>
/// Adds a list of new <see cref="AuthenticationRequest"/>s.
/// </summary>
/// <param name="authenticationRequests">The list of <see cref="AuthenticationRequest"/>s to add.</param>
/// <returns>The number of records that were added.</returns>
private async Task<int> AddAuthenticationRequests(IEnumerable<AuthenticationRequest> authenticationRequests)
{
await _dbContext.AuthenticationRequests.AddRangeAsync(authenticationRequests);
return await _dbContext.SaveChangesAsync();
}

private async Task Test(AuthenticationRequest authenticationRequest)
{
await _dbContext.AuthenticationRequests.AddAsync(authenticationRequest);
await _dbContext.SaveChangesAsync();
}

/// <summary>
/// Deletes all existing <see cref="AuthenticationRequest"/>s.
/// </summary>
/// <returns>The number of deleted records.</returns>
private async Task<int> DeleteAuthenticationRequests()
{
return await _dbContext.AuthenticationRequests.ExecuteDeleteAsync();
}

/// <summary>
///
/// </summary>
/// <exception cref="NotImplementedException">TODO</exception>
// TODO: This is basically the same logic as other repositories with external data
// Can this be generalized enough even with small differences in the way the database is updated?
// e.g. full deletion of current data and reinsert vs upsert...
private async Task RefreshAuthenticationRequestsIfStale()
{
var dataIsStale = await _dataRetrievalMetadataService.DataIsStale(ThirdPartyDataType.AuthenticationRequests);
if (dataIsStale)
{
var response = await _dataProviderService.GetAuthenticationRequests(100);
if (response.IsSuccessful)
{
await DeleteAuthenticationRequests();
foreach (var authenticationRequest in response.Result)
{
await Test(authenticationRequest);
}

await AddAuthenticationRequests(response.Result);
await _dataRetrievalMetadataService.ResetDataExpiry(ThirdPartyDataType.AuthenticationRequests);

_logger.LogInformation(
"Refreshed stale Authentication request data. {updateRecords} records were updated.",
response.Result.Count());
}
else
{
// TODO: What to do in case of failure should depend on if we already have data
// Log failure and continue, maybe also send a notification to frontend
throw new NotImplementedException();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace RobinTTY.PersonalFinanceDashboard.Infrastructure.Repositories;
/// </summary>
public class BankingInstitutionRepository
{
private readonly ILogger _logger;
private readonly ILogger<BankingInstitutionRepository> _logger;
private readonly ApplicationDbContext _dbContext;
private readonly GoCardlessDataProviderService _dataProviderService;
private readonly ThirdPartyDataRetrievalMetadataService _dataRetrievalMetadataService;
Expand Down Expand Up @@ -65,16 +65,18 @@ public async Task<IQueryable<BankingInstitution>> GetBankingInstitutions(string?
/// Adds a list of new <see cref="BankingInstitution"/>s.
/// </summary>
/// <param name="bankingInstitutions">The list of <see cref="BankingInstitution"/>s to add.</param>
public async Task AddBankingInstitutions(IEnumerable<BankingInstitution> bankingInstitutions)
/// <returns>The number of records that were added.</returns>
public async Task<int> AddBankingInstitutions(IEnumerable<BankingInstitution> bankingInstitutions)
{
await _dbContext.BankingInstitutions.AddRangeAsync(bankingInstitutions);
await _dbContext.SaveChangesAsync();
return await _dbContext.SaveChangesAsync();
}

/// <summary>
/// Adds a new <see cref="BankingInstitution"/>.
/// </summary>
/// <param name="bankingInstitution">The <see cref="BankingInstitution"/> to add.</param>
/// <returns>The added <see cref="BankingInstitution"/>.</returns>
public async Task<BankingInstitution> AddBankingInstitution(BankingInstitution bankingInstitution)
{
var result = await _dbContext.BankingInstitutions.AddAsync(bankingInstitution);
Expand Down Expand Up @@ -128,6 +130,7 @@ private async Task RefreshBankingInstitutionsIfStale()
var response = await _dataProviderService.GetBankingInstitutions();
if (response.IsSuccessful)
{
// TODO: This needs to be updated to account for user generated banking institutions
await DeleteBankingInstitutions();
await AddBankingInstitutions(response.Result);
await _dataRetrievalMetadataService.ResetDataExpiry(ThirdPartyDataType.BankingInstitutions);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using RobinTTY.NordigenApiClient;
using RobinTTY.NordigenApiClient.Models;
using RobinTTY.NordigenApiClient.Models.Errors;
using RobinTTY.NordigenApiClient.Models.Requests;
using RobinTTY.NordigenApiClient.Models.Responses;
using RobinTTY.PersonalFinanceDashboard.Core.Models;
using BankAccount = RobinTTY.PersonalFinanceDashboard.Core.Models.BankAccount;
Expand Down Expand Up @@ -40,7 +39,7 @@ public GoCardlessDataProviderService(HttpClient httpClient, NordigenClientCreden
/// </summary>
/// <param name="country">Optional country filter to apply to the query.</param>
/// <returns>The available banking <see cref="Institution"/>s.</returns>
public async Task<ThirdPartyResponse<IQueryable<BankingInstitution>, BasicResponse>> GetBankingInstitutions(
public async Task<ThirdPartyResponse<IEnumerable<BankingInstitution>, BasicResponse>> GetBankingInstitutions(
string? country = null)
{
var response = await _client.InstitutionsEndpoint.GetInstitutions(country);
Expand All @@ -50,11 +49,11 @@ public async Task<ThirdPartyResponse<IQueryable<BankingInstitution>, BasicRespon
var institutions =
response.Result.Select(inst =>
new BankingInstitution(inst.Id, inst.Bic, inst.Name, inst.Logo, inst.Countries));
return new ThirdPartyResponse<IQueryable<BankingInstitution>, BasicResponse>(true,
institutions.AsQueryable(), null);
return new ThirdPartyResponse<IEnumerable<BankingInstitution>, BasicResponse>(true,
institutions, null);
}

return new ThirdPartyResponse<IQueryable<BankingInstitution>, BasicResponse>(false, null, response.Error);
return new ThirdPartyResponse<IEnumerable<BankingInstitution>, BasicResponse>(false, null, response.Error);
}

/// <summary>
Expand All @@ -69,8 +68,8 @@ public async Task<ThirdPartyResponse<IQueryable<BankingInstitution>, BasicRespon
// TODO: handle request failure
var requisition = response.Result!;
var result = new AuthenticationRequest(requisition.Id.ToString(),
requisition.Accounts.Select(guid => guid.ToString()),
ConvertRequisitionStatus(requisition.Status), requisition.AuthenticationLink);
ConvertRequisitionStatus(requisition.Status), requisition.AuthenticationLink,
requisition.Accounts.Select(accountId => new BankAccount(accountId)).ToList());
return new ThirdPartyResponse<AuthenticationRequest?, BasicResponse?>(response.IsSuccess, result,
response.Error);
}
Expand All @@ -81,17 +80,16 @@ public async Task<ThirdPartyResponse<IQueryable<BankingInstitution>, BasicRespon
/// </summary>
/// <param name="requisitionLimit">The maximum number of requisitions to get.</param>
/// <returns>All existing <see cref="Requisition"/>s.</returns>
public async Task<ThirdPartyResponse<IQueryable<AuthenticationRequest>, BasicResponse?>> GetAuthenticationRequests(
public async Task<ThirdPartyResponse<IEnumerable<AuthenticationRequest>, BasicResponse?>> GetAuthenticationRequests(
int requisitionLimit)
{
var response = await _client.RequisitionsEndpoint.GetRequisitions(requisitionLimit, 0);
// TODO: handle request failure
var requisitions = response.Result!.Results;
var result = requisitions.Select(req => new AuthenticationRequest(req.Id.ToString(),
req.Accounts.Select(guid => guid.ToString()), ConvertRequisitionStatus(req.Status),
req.AuthenticationLink))
.AsQueryable();
return new ThirdPartyResponse<IQueryable<AuthenticationRequest>, BasicResponse?>(response.IsSuccess, result,
ConvertRequisitionStatus(req.Status),
req.AuthenticationLink, req.Accounts.Select(accountId => new BankAccount(accountId)).ToList()));
return new ThirdPartyResponse<IEnumerable<AuthenticationRequest>, BasicResponse?>(response.IsSuccess, result,
response.Error);
}

Expand All @@ -112,8 +110,9 @@ await _client.RequisitionsEndpoint.CreateRequisition(institutionId, redirectUri,
{
var requisition = response.Result;
var authenticationRequest = new AuthenticationRequest(requisition.Id.ToString(),
requisition.Accounts.Select(guid => guid.ToString()),
ConvertRequisitionStatus(requisition.Status), requisition.AuthenticationLink);
ConvertRequisitionStatus(requisition.Status), requisition.AuthenticationLink,
requisition.Accounts.Select(accountId => new BankAccount(accountId)).ToList());

return new ThirdPartyResponse<AuthenticationRequest, CreateRequisitionError>(response.IsSuccess,
authenticationRequest, null);
}
Expand Down

0 comments on commit 3c2a68b

Please sign in to comment.