using System.Net.Http.Json; using System.Text.Json; using Asentrafin.Api.Configuration; using Microsoft.Extensions.Options; namespace Asentrafin.Api.Services.Plaid; public class PlaidClient(HttpClient httpClient, IOptions options) : IPlaidClient { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); private readonly PlaidOptions _options = options.Value; public Task AccountsGetAsync(string accessToken, CancellationToken cancellationToken) { return PostAsync("/accounts/get", BuildAuthedRequest(accessToken), cancellationToken); } public Task TransactionsSyncAsync(string accessToken, string? cursor, CancellationToken cancellationToken) { var body = BuildAuthedRequest(accessToken); body["cursor"] = cursor; body["count"] = 500; body["options"] = new Dictionary { ["include_original_description"] = true, ["personal_finance_category_version"] = "v2" }; return PostAsync("/transactions/sync", body, cancellationToken); } public Task InvestmentsHoldingsGetAsync(string accessToken, CancellationToken cancellationToken) { return PostAsync("/investments/holdings/get", BuildAuthedRequest(accessToken), cancellationToken); } public Task InvestmentsTransactionsGetAsync( string accessToken, DateOnly startDate, DateOnly endDate, int count, int offset, CancellationToken cancellationToken) { var body = BuildAuthedRequest(accessToken); body["start_date"] = startDate.ToString("yyyy-MM-dd"); body["end_date"] = endDate.ToString("yyyy-MM-dd"); body["options"] = new Dictionary { ["count"] = count, ["offset"] = offset }; return PostAsync("/investments/transactions/get", body, cancellationToken); } private async Task PostAsync( string path, Dictionary body, CancellationToken cancellationToken) { EnsureConfigured(); using var response = await httpClient.PostAsJsonAsync(path, body, JsonOptions, cancellationToken); var responseBody = await response.Content.ReadAsStringAsync(cancellationToken); if (!response.IsSuccessStatusCode) { throw new HttpRequestException( $"Plaid request to {path} failed with {(int)response.StatusCode}: {responseBody}", null, response.StatusCode); } return JsonDocument.Parse(responseBody); } private Dictionary BuildAuthedRequest(string accessToken) { return new Dictionary { ["client_id"] = _options.ClientId, ["secret"] = _options.Secret, ["access_token"] = accessToken }; } private void EnsureConfigured() { if (string.IsNullOrWhiteSpace(_options.ClientId) || string.IsNullOrWhiteSpace(_options.Secret)) { throw new InvalidOperationException("Plaid ClientId and Secret must be configured."); } } }