feat: add Plaid transaction handling and async queue processing

- introduced new migration to add Cursor column to AccountMetadata.
- updated AppDbContext model snapshot to reflect changes in the database schema.
- implemented async queue processing with ChannelAsyncQueue and AsyncQueueHostedService.
- created Plaid transaction handlers for added, modified, and removed transactions.
- developed PlaidTransactionService and PlaidTransactionSyncer for syncing transactions.
- added SyncUpdatesProcessor to handle sync updates from Plaid.
- updated tests to accommodate changes in institution and account metadata handling.
This commit is contained in:
Stevan Freeborn
2026-03-09 04:29:01 -05:00
parent 48e95e5242
commit a9f1ced0d5
62 changed files with 3625 additions and 68 deletions
@@ -172,7 +172,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
await context.AddAsync(user, ct);
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
var plaidMetadata = PlaidMetadata.From("alreadyExists", "Some Bank", encryptedAccessToken);
var plaidMetadata = PlaidInstitutionMetadata.From("alreadyExists", "Some Bank", encryptedAccessToken);
var institution = Institution.From("Some Bank", plaidMetadata);
await context.AddAsync(institution, ct);
@@ -192,7 +192,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
.WithUserId(user.Id)
.WithBody(new
{
plaidInstitutionId = ((PlaidMetadata)institution.Metadata!).PlaidId,
plaidInstitutionId = ((PlaidInstitutionMetadata)institution.Metadata!).PlaidId,
plaidAccountId = ((PlaidAccountMetadata)account.Metadata!).PlaidId,
plaidAccountName = ((PlaidAccountMetadata)account.Metadata).PlaidName,
accountCurrencyCode = "USD",
@@ -217,7 +217,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
var plaidMetadata = PlaidMetadata.From("id", "Some Bank", encryptedAccessToken);
var plaidMetadata = PlaidInstitutionMetadata.From("id", "Some Bank", encryptedAccessToken);
var institution = Institution.From("Some Bank", plaidMetadata);
user.AddInstitution(institution);
@@ -237,7 +237,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi)
.WithUserId(user.Id)
.WithBody(new
{
plaidInstitutionId = ((PlaidMetadata)institution.Metadata!).PlaidId,
plaidInstitutionId = ((PlaidInstitutionMetadata)institution.Metadata!).PlaidId,
plaidAccountId = newAccountId,
plaidAccountName = newAccountName,
accountCurrentBalance = expectedBalance,
@@ -101,7 +101,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
var plaidMetadata = PlaidMetadata.From("alreadyExists", "Some Bank", encryptedAccessToken);
var plaidMetadata = PlaidInstitutionMetadata.From("alreadyExists", "Some Bank", encryptedAccessToken);
var institution = Institution.From("Some Bank", plaidMetadata);
user.AddInstitution(institution);
@@ -117,7 +117,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
.WithBody(new
{
publicToken = "token",
plaidInstitutionId = ((PlaidMetadata)institution.Metadata!).PlaidId,
plaidInstitutionId = ((PlaidInstitutionMetadata)institution.Metadata!).PlaidId,
})
.Build();
@@ -179,7 +179,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi)
var institution = updatedUser.Institutions.First();
institution.Name.Should().NotBeNullOrEmpty();
var metadata = institution.Metadata.As<PlaidMetadata>();
var metadata = institution.Metadata.As<PlaidInstitutionMetadata>();
metadata.PlaidId.Should().Be(plaidInstitutionId);
metadata.PlaidName.Should().Be(institution.Name);
metadata.EncryptedAccessToken.Should().NotBeNullOrEmpty();
@@ -92,7 +92,7 @@ public class GetAvailableTests(TestApi testApi) : IntegrationTest(testApi)
AccessToken = exchangeTokenResponse.AccessToken,
});
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, exchangeTokenResponse.AccessToken, ct);
var institutionMetadata = PlaidMetadata.From(institutionId, "Some Bank", encryptedAccessToken);
var institutionMetadata = PlaidInstitutionMetadata.From(institutionId, "Some Bank", encryptedAccessToken);
var institution = Institution.From("Some Bank", institutionMetadata);
user.AddInstitution(institution);
@@ -45,7 +45,7 @@ public class GetTests(TestApi testApi) : IntegrationTest(testApi)
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, "accessToken", ct);
var plaidMetadata = PlaidMetadata.From("alreadyExists", "Some Bank", encryptedAccessToken);
var plaidMetadata = PlaidInstitutionMetadata.From("alreadyExists", "Some Bank", encryptedAccessToken);
var institution = Institution.From("Some Bank", plaidMetadata);
user.AddInstitution(institution);