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;