From c4a8aada6b279f7d776b9d214e41796202fc8050 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:48:49 -0600 Subject: [PATCH] feat(infra): implement plaid service --- src/Directory.Packages.props | 1 + .../Accounts/Plaid/PlaidClientOptions.cs | 32 +++++++++++++ .../Accounts/Plaid/PlaidException.cs | 16 +++++++ .../Accounts/Plaid/PlaidMetadata.cs | 8 +++- .../Accounts/Plaid/PlaidService.cs | 47 +++++++++++++++++++ .../ServiceCollectionExtensions.cs | 14 +++++- src/FiscalOS.Infra/FiscalOS.Infra.csproj | 1 + src/FiscalOS.Infra/Usings.cs | 4 ++ 8 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 src/FiscalOS.Infra/Accounts/Plaid/PlaidClientOptions.cs create mode 100644 src/FiscalOS.Infra/Accounts/Plaid/PlaidException.cs create mode 100644 src/FiscalOS.Infra/Accounts/Plaid/PlaidService.cs diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 667c66e..2591312 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -3,6 +3,7 @@ true + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/FiscalOS.Infra/Accounts/Plaid/PlaidClientOptions.cs b/src/FiscalOS.Infra/Accounts/Plaid/PlaidClientOptions.cs new file mode 100644 index 0000000..b137e63 --- /dev/null +++ b/src/FiscalOS.Infra/Accounts/Plaid/PlaidClientOptions.cs @@ -0,0 +1,32 @@ +namespace FiscalOS.Infra.Accounts.Plaid; + +public sealed record PlaidClientOptions +{ + public string ClientId { get; init; } = string.Empty; + public string Secret { get; init; } = string.Empty; + + public IOptions ToPlaidOptions() + { + return Options.Create(new() + { + ClientId = ClientId, + Secret = Secret + }); + } +} + +public sealed record PlaidClientOptionsSetup : IConfigureOptions +{ + private const string SectionName = nameof(PlaidClientOptions); + private readonly IConfiguration _configuration; + + public PlaidClientOptionsSetup(IConfiguration configuration) + { + _configuration = configuration; + } + + public void Configure(PlaidClientOptions options) + { + _configuration.GetSection(SectionName).Bind(options); + } +} \ No newline at end of file diff --git a/src/FiscalOS.Infra/Accounts/Plaid/PlaidException.cs b/src/FiscalOS.Infra/Accounts/Plaid/PlaidException.cs new file mode 100644 index 0000000..668bc34 --- /dev/null +++ b/src/FiscalOS.Infra/Accounts/Plaid/PlaidException.cs @@ -0,0 +1,16 @@ +namespace FiscalOS.Infra.Accounts.Plaid; + +public class PlaidException : Exception +{ + public PlaidException() + { + } + + public PlaidException(string message) : base(message) + { + } + + public PlaidException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/FiscalOS.Infra/Accounts/Plaid/PlaidMetadata.cs b/src/FiscalOS.Infra/Accounts/Plaid/PlaidMetadata.cs index 002cc66..f488428 100644 --- a/src/FiscalOS.Infra/Accounts/Plaid/PlaidMetadata.cs +++ b/src/FiscalOS.Infra/Accounts/Plaid/PlaidMetadata.cs @@ -12,11 +12,15 @@ public sealed class PlaidMetadata : InstitutionMetadata } public static PlaidMetadata From( - string plaidId, - string plaidName, + string? plaidId, + string? plaidName, string encryptedAccessToken ) { + ArgumentNullException.ThrowIfNull(plaidId, nameof(plaidId)); + ArgumentNullException.ThrowIfNull(plaidName, nameof(plaidName)); + ArgumentNullException.ThrowIfNull(encryptedAccessToken, nameof(encryptedAccessToken)); + return new PlaidMetadata { PlaidId = plaidId, diff --git a/src/FiscalOS.Infra/Accounts/Plaid/PlaidService.cs b/src/FiscalOS.Infra/Accounts/Plaid/PlaidService.cs new file mode 100644 index 0000000..7447c28 --- /dev/null +++ b/src/FiscalOS.Infra/Accounts/Plaid/PlaidService.cs @@ -0,0 +1,47 @@ +namespace FiscalOS.Infra.Accounts.Plaid; + +public sealed class PlaidService +{ + private readonly PlaidClient _client; + + private PlaidService(PlaidClient client) + { + _client = client; + } + + public static PlaidService From(IServiceProvider sp) + { + var client = sp.GetRequiredService(); + return new(client); + } + + public async Task<(string ItemId, string AccessToken)> ExchangeTokenAsync(string publicToken) + { + var ptr = await _client.ItemPublicTokenExchangeAsync(new() + { + PublicToken = publicToken, + }).ConfigureAwait(false); + + if (ptr.IsSuccessStatusCode is false) + { + throw new PlaidException("Unable to exchange public token for access token"); + } + + return (ptr.ItemId, ptr.AccessToken); + } + + public async Task GetItemAsync(string accessToken) + { + var ar = await _client.ItemGetAsync(new() + { + AccessToken = accessToken, + }).ConfigureAwait(false); + + if (ar.IsSuccessStatusCode is false) + { + throw new PlaidException("Unable to retrieve item"); + } + + return ar.Item; + } +} diff --git a/src/FiscalOS.Infra/DependencyInjection/ServiceCollectionExtensions.cs b/src/FiscalOS.Infra/DependencyInjection/ServiceCollectionExtensions.cs index 8a03bbc..631215d 100644 --- a/src/FiscalOS.Infra/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/FiscalOS.Infra/DependencyInjection/ServiceCollectionExtensions.cs @@ -5,7 +5,8 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddInfrastructure(this IServiceCollection services) { services.AddSingleton(); - + + services.AddHttpClient(); services.AddSingleton(TimeProvider.System); services.AddSingleton(); @@ -23,6 +24,17 @@ public static class ServiceCollectionExtensions services.AddDbContext(); services.AddHostedService(); + services.ConfigureOptions(); + services.AddSingleton(static sp => + { + var factory = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var clientOptions = sp.GetRequiredService>(); + var options = clientOptions.Value.ToPlaidOptions(); + return new PlaidClient(options, factory, logger); + }); + services.AddSingleton(PlaidService.From); + return services; } } \ No newline at end of file diff --git a/src/FiscalOS.Infra/FiscalOS.Infra.csproj b/src/FiscalOS.Infra/FiscalOS.Infra.csproj index 3d93af5..bd785b7 100644 --- a/src/FiscalOS.Infra/FiscalOS.Infra.csproj +++ b/src/FiscalOS.Infra/FiscalOS.Infra.csproj @@ -1,6 +1,7 @@  + diff --git a/src/FiscalOS.Infra/Usings.cs b/src/FiscalOS.Infra/Usings.cs index 8d87713..f939a62 100644 --- a/src/FiscalOS.Infra/Usings.cs +++ b/src/FiscalOS.Infra/Usings.cs @@ -10,11 +10,15 @@ global using FiscalOS.Core.Authentication; global using FiscalOS.Core.Data; global using FiscalOS.Core.Identity; global using FiscalOS.Core.Security; +global using FiscalOS.Infra.Accounts.Plaid; global using FiscalOS.Infra.Authentication; global using FiscalOS.Infra.Authorization; global using FiscalOS.Infra.Data; global using FiscalOS.Infra.Security; +global using Going.Plaid; +global using Going.Plaid.Entity; + global using Microsoft.AspNetCore.Authentication.JwtBearer; global using Microsoft.AspNetCore.Authorization; global using Microsoft.AspNetCore.Authorization.Policy;