From 74793b580048753d21aae546655ac9ffeb53df31 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 3 Mar 2026 07:36:09 -0600 Subject: [PATCH] feat: continued wip on supporting transactions --- .vscode/settings.json | 5 +- src/FiscalOS.API/Program.cs | 2 - .../Transactions/FireWebhook/Endpoint.cs | 3 - src/FiscalOS.API/Usings.cs | 3 +- src/FiscalOS.Core/Accounts/Account.cs | 16 +- src/FiscalOS.Core/Accounts/Balance.cs | 2 + src/FiscalOS.Core/Accounts/Institution.cs | 7 + src/FiscalOS.Core/Identity/User.cs | 10 + src/FiscalOS.Core/Transactions/Transaction.cs | 15 +- src/FiscalOS.Core/Usings.cs | 3 +- .../Accounts/Plaid/PlaidAccountMetadata.cs | 3 - .../Plaid/PlaidInstitutionMetadata.cs | 2 - src/FiscalOS.Infra/Data/AppDbContext.cs | 15 +- .../ServiceCollectionExtensions.cs | 5 - .../20260212215015_InstitutionAndMetadata.cs | 4 +- .../20260213130218_AccountAndMetadata.cs | 4 +- ...234_AddBalancesWithConnectionToAccounts.cs | 4 +- ...TransactionAndTransactionMetadataModels.cs | 3 +- ...ctionsNavigationPropertyToUser.Designer.cs | 518 ++++++++++++++++++ ...AddTransactionsNavigationPropertyToUser.cs | 48 ++ ...ransactionNavigationProperties.Designer.cs | 500 +++++++++++++++++ ...oIncludeTransactionNavigationProperties.cs | 78 +++ .../Migrations/AppDbContextModelSnapshot.cs | 15 +- .../Transactions/Plaid/PlaidExtensions.cs | 2 +- .../Plaid/PlaidRemovedTransactionHandler.cs | 8 + .../Plaid/PlaidTransactionMetadata.cs | 5 +- src/FiscalOS.Infra/Usings.cs | 3 + .../ServiceDefaultsExtensions.cs | 2 +- .../Infra/AspireFixture.cs | 3 - tests/FiscalOS.AppHost.Tests/Usings.cs | 4 +- .../Integration/IntegrationTest.cs | 44 ++ .../PlaidRemovedTransactionHandlerTests.cs | 59 ++ tests/FiscalOS.Infra.Tests/Usings.cs | 7 + 33 files changed, 1335 insertions(+), 67 deletions(-) create mode 100644 src/FiscalOS.Infra/Migrations/20260303124406_AddTransactionsNavigationPropertyToUser.Designer.cs create mode 100644 src/FiscalOS.Infra/Migrations/20260303124406_AddTransactionsNavigationPropertyToUser.cs create mode 100644 src/FiscalOS.Infra/Migrations/20260303130637_UpdateAppDbContextToIncludeTransactionNavigationProperties.Designer.cs create mode 100644 src/FiscalOS.Infra/Migrations/20260303130637_UpdateAppDbContextToIncludeTransactionNavigationProperties.cs create mode 100644 tests/FiscalOS.Infra.Tests/Integration/IntegrationTest.cs create mode 100644 tests/FiscalOS.Infra.Tests/Integration/PlaidRemovedTransactionHandlerTests.cs diff --git a/.vscode/settings.json b/.vscode/settings.json index dd8e581..bb59eac 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,5 +12,8 @@ "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "cSpell.words": ["Encryptor"], - "python-envs.defaultEnvManager": "ms-python.python:system" + "python-envs.defaultEnvManager": "ms-python.python:system", + "search.exclude": { + "**/Migrations": true + } } diff --git a/src/FiscalOS.API/Program.cs b/src/FiscalOS.API/Program.cs index 10f6b96..2154197 100644 --- a/src/FiscalOS.API/Program.cs +++ b/src/FiscalOS.API/Program.cs @@ -1,5 +1,3 @@ -using Going.Plaid.Converters; - var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); diff --git a/src/FiscalOS.API/Transactions/FireWebhook/Endpoint.cs b/src/FiscalOS.API/Transactions/FireWebhook/Endpoint.cs index fcc431d..670ca30 100644 --- a/src/FiscalOS.API/Transactions/FireWebhook/Endpoint.cs +++ b/src/FiscalOS.API/Transactions/FireWebhook/Endpoint.cs @@ -1,6 +1,3 @@ -using Going.Plaid; -using Going.Plaid.Entity; - namespace FiscalOS.API.Transactions.FireWebhook; internal static class Endpoint diff --git a/src/FiscalOS.API/Usings.cs b/src/FiscalOS.API/Usings.cs index a2e3208..7927f96 100644 --- a/src/FiscalOS.API/Usings.cs +++ b/src/FiscalOS.API/Usings.cs @@ -21,7 +21,6 @@ global using FiscalOS.Core.Authentication; global using FiscalOS.Core.Identity; global using FiscalOS.Core.Queuing; global using FiscalOS.Core.Security; -global using FiscalOS.Core.Transactions; global using FiscalOS.Infra.Accounts.Plaid; global using FiscalOS.Infra.Authentication; global using FiscalOS.Infra.Data; @@ -30,6 +29,8 @@ global using FiscalOS.Infra.Transactions.Plaid; global using FiscalOS.ServiceDefaults; global using Going.Plaid; +global using Going.Plaid.Converters; +global using Going.Plaid.Entity; global using Going.Plaid.Webhook; global using Microsoft.AspNetCore.Mvc; diff --git a/src/FiscalOS.Core/Accounts/Account.cs b/src/FiscalOS.Core/Accounts/Account.cs index ed7f585..0795aae 100644 --- a/src/FiscalOS.Core/Accounts/Account.cs +++ b/src/FiscalOS.Core/Accounts/Account.cs @@ -1,5 +1,3 @@ -using FiscalOS.Core.Transactions; - namespace FiscalOS.Core.Accounts; public sealed class Account : Entity @@ -16,11 +14,17 @@ public sealed class Account : Entity private readonly List _transactions = []; public IEnumerable Transactions => _transactions; - public static Account From(Guid institutionId, string name, AccountMetadata accountMetadata) + private Account() + { + } + + public static Account From( + string name, + AccountMetadata accountMetadata + ) { return new() { - InstitutionId = institutionId, Name = name, Metadata = accountMetadata, }; @@ -28,14 +32,14 @@ public sealed class Account : Entity public void AddBalance(Balance balance) { - ArgumentNullException.ThrowIfNull(balance); + ArgumentNullException.ThrowIfNull(balance, nameof(balance)); _balances.Add(balance); } public void AddTransaction(Transaction transaction) { - ArgumentNullException.ThrowIfNull(transaction); + ArgumentNullException.ThrowIfNull(transaction, nameof(transaction)); _transactions.Add(transaction); } diff --git a/src/FiscalOS.Core/Accounts/Balance.cs b/src/FiscalOS.Core/Accounts/Balance.cs index 2679666..98b12bc 100644 --- a/src/FiscalOS.Core/Accounts/Balance.cs +++ b/src/FiscalOS.Core/Accounts/Balance.cs @@ -17,6 +17,8 @@ public sealed class Balance : Entity string currencyCode ) { + ArgumentNullException.ThrowIfNull(currencyCode, nameof(currencyCode)); + return new() { Current = current, diff --git a/src/FiscalOS.Core/Accounts/Institution.cs b/src/FiscalOS.Core/Accounts/Institution.cs index 0bdc031..60984dd 100644 --- a/src/FiscalOS.Core/Accounts/Institution.cs +++ b/src/FiscalOS.Core/Accounts/Institution.cs @@ -25,4 +25,11 @@ public sealed class Institution : Entity Metadata = metadata }; } + + public void AddAccount(Account account) + { + ArgumentNullException.ThrowIfNull(account, nameof(account)); + + _accounts.Add(account); + } } \ No newline at end of file diff --git a/src/FiscalOS.Core/Identity/User.cs b/src/FiscalOS.Core/Identity/User.cs index bd3ff2d..f87112d 100644 --- a/src/FiscalOS.Core/Identity/User.cs +++ b/src/FiscalOS.Core/Identity/User.cs @@ -16,6 +16,9 @@ public sealed class User : Entity private readonly List _accounts = []; public IEnumerable Accounts => _accounts; + private readonly List _transactions = []; + public IEnumerable Transactions => _transactions; + private User() { } @@ -60,4 +63,11 @@ public sealed class User : Entity _accounts.Add(account); } + + public void AddTransaction(Transaction transaction) + { + ArgumentNullException.ThrowIfNull(transaction, nameof(transaction)); + + _transactions.Add(transaction); + } } \ No newline at end of file diff --git a/src/FiscalOS.Core/Transactions/Transaction.cs b/src/FiscalOS.Core/Transactions/Transaction.cs index b6ae962..6ad05e1 100644 --- a/src/FiscalOS.Core/Transactions/Transaction.cs +++ b/src/FiscalOS.Core/Transactions/Transaction.cs @@ -19,6 +19,17 @@ public sealed class Transaction : Entity { } + public static Transaction From( + string merchantName, + string description, + decimal? amount, + DateTimeOffset? date, + TransactionMetadata metadata + ) + { + return From(Guid.Empty, Guid.Empty, merchantName, description, amount, date, metadata); + } + public static Transaction From( Guid userId, Guid accountId, @@ -29,8 +40,8 @@ public sealed class Transaction : Entity TransactionMetadata metadata ) { - ArgumentNullException.ThrowIfNull(merchantName); - ArgumentNullException.ThrowIfNull(description); + ArgumentNullException.ThrowIfNull(merchantName, nameof(merchantName)); + ArgumentNullException.ThrowIfNull(description, nameof(description)); if (amount.HasValue is false) { diff --git a/src/FiscalOS.Core/Usings.cs b/src/FiscalOS.Core/Usings.cs index b04d87e..aed32e3 100644 --- a/src/FiscalOS.Core/Usings.cs +++ b/src/FiscalOS.Core/Usings.cs @@ -1,4 +1,5 @@ global using FiscalOS.Core.Accounts; global using FiscalOS.Core.Data; global using FiscalOS.Core.Identity; -global using FiscalOS.Core.Security; \ No newline at end of file +global using FiscalOS.Core.Security; +global using FiscalOS.Core.Transactions; \ No newline at end of file diff --git a/src/FiscalOS.Infra/Accounts/Plaid/PlaidAccountMetadata.cs b/src/FiscalOS.Infra/Accounts/Plaid/PlaidAccountMetadata.cs index 2c1bd4b..9d9f674 100644 --- a/src/FiscalOS.Infra/Accounts/Plaid/PlaidAccountMetadata.cs +++ b/src/FiscalOS.Infra/Accounts/Plaid/PlaidAccountMetadata.cs @@ -1,6 +1,3 @@ - -using FiscalOS.Infra.Common; - namespace FiscalOS.Infra.Accounts.Plaid; public sealed class PlaidAccountMetadata : AccountMetadata diff --git a/src/FiscalOS.Infra/Accounts/Plaid/PlaidInstitutionMetadata.cs b/src/FiscalOS.Infra/Accounts/Plaid/PlaidInstitutionMetadata.cs index b40f639..4bdbf0b 100644 --- a/src/FiscalOS.Infra/Accounts/Plaid/PlaidInstitutionMetadata.cs +++ b/src/FiscalOS.Infra/Accounts/Plaid/PlaidInstitutionMetadata.cs @@ -1,5 +1,3 @@ -using FiscalOS.Infra.Common; - namespace FiscalOS.Infra.Accounts.Plaid; public sealed class PlaidInstitutionMetadata : InstitutionMetadata diff --git a/src/FiscalOS.Infra/Data/AppDbContext.cs b/src/FiscalOS.Infra/Data/AppDbContext.cs index 3db167d..c81d4f5 100644 --- a/src/FiscalOS.Infra/Data/AppDbContext.cs +++ b/src/FiscalOS.Infra/Data/AppDbContext.cs @@ -1,6 +1,3 @@ -using FiscalOS.Core.Transactions; -using FiscalOS.Infra.Transactions.Plaid; - using Account = FiscalOS.Core.Accounts.Account; using Institution = FiscalOS.Core.Accounts.Institution; using Transaction = FiscalOS.Core.Transactions.Transaction; @@ -167,14 +164,14 @@ public sealed class AppDbContext( modelBuilder.Entity(static eb => { eb.HasOne(static t => t.User) - .WithMany() - .HasForeignKey(static t => t.UserId) - .OnDelete(DeleteBehavior.Cascade); + .WithMany(static u => u.Transactions) + .HasForeignKey(static t => t.UserId) + .OnDelete(DeleteBehavior.Cascade); eb.HasOne(static t => t.Account) - .WithMany() - .HasForeignKey(static t => t.AccountId) - .OnDelete(DeleteBehavior.Cascade); + .WithMany(static a => a.Transactions) + .HasForeignKey(static t => t.AccountId) + .OnDelete(DeleteBehavior.Cascade); eb.Property(static t => t.MerchantName); eb.Property(static t => t.Description); diff --git a/src/FiscalOS.Infra/DependencyInjection/ServiceCollectionExtensions.cs b/src/FiscalOS.Infra/DependencyInjection/ServiceCollectionExtensions.cs index 244cd01..5ef4362 100644 --- a/src/FiscalOS.Infra/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/FiscalOS.Infra/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,10 +1,5 @@ namespace FiscalOS.Infra.DependencyInjection; -using FiscalOS.Core.Queuing; -using FiscalOS.Infra.Accounts.Plaid; -using FiscalOS.Infra.Queuing; -using FiscalOS.Infra.Transactions.Plaid; - public static class ServiceCollectionExtensions { public static IServiceCollection AddInfrastructure(this IServiceCollection services) diff --git a/src/FiscalOS.Infra/Migrations/20260212215015_InstitutionAndMetadata.cs b/src/FiscalOS.Infra/Migrations/20260212215015_InstitutionAndMetadata.cs index e67468b..2063727 100644 --- a/src/FiscalOS.Infra/Migrations/20260212215015_InstitutionAndMetadata.cs +++ b/src/FiscalOS.Infra/Migrations/20260212215015_InstitutionAndMetadata.cs @@ -1,6 +1,4 @@ -using System; - -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/FiscalOS.Infra/Migrations/20260213130218_AccountAndMetadata.cs b/src/FiscalOS.Infra/Migrations/20260213130218_AccountAndMetadata.cs index c536214..ac14e59 100644 --- a/src/FiscalOS.Infra/Migrations/20260213130218_AccountAndMetadata.cs +++ b/src/FiscalOS.Infra/Migrations/20260213130218_AccountAndMetadata.cs @@ -1,6 +1,4 @@ -using System; - -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/FiscalOS.Infra/Migrations/20260214112234_AddBalancesWithConnectionToAccounts.cs b/src/FiscalOS.Infra/Migrations/20260214112234_AddBalancesWithConnectionToAccounts.cs index 82ed630..34006f2 100644 --- a/src/FiscalOS.Infra/Migrations/20260214112234_AddBalancesWithConnectionToAccounts.cs +++ b/src/FiscalOS.Infra/Migrations/20260214112234_AddBalancesWithConnectionToAccounts.cs @@ -1,6 +1,4 @@ -using System; - -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/FiscalOS.Infra/Migrations/20260228123108_AddTransactionAndTransactionMetadataModels.cs b/src/FiscalOS.Infra/Migrations/20260228123108_AddTransactionAndTransactionMetadataModels.cs index 331dcd8..eb1b600 100644 --- a/src/FiscalOS.Infra/Migrations/20260228123108_AddTransactionAndTransactionMetadataModels.cs +++ b/src/FiscalOS.Infra/Migrations/20260228123108_AddTransactionAndTransactionMetadataModels.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/FiscalOS.Infra/Migrations/20260303124406_AddTransactionsNavigationPropertyToUser.Designer.cs b/src/FiscalOS.Infra/Migrations/20260303124406_AddTransactionsNavigationPropertyToUser.Designer.cs new file mode 100644 index 0000000..e014e61 --- /dev/null +++ b/src/FiscalOS.Infra/Migrations/20260303124406_AddTransactionsNavigationPropertyToUser.Designer.cs @@ -0,0 +1,518 @@ +// +using System; +using FiscalOS.Infra.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FiscalOS.Infra.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260303124406_AddTransactionsNavigationPropertyToUser")] + partial class AddTransactionsNavigationPropertyToUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.2"); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("InstitutionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InstitutionId"); + + b.HasIndex("UserId"); + + b.ToTable("Account"); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId") + .IsUnique(); + + b.ToTable("AccountMetadata"); + + b.HasDiscriminator("Type").HasValue("AccountMetadata"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("Available") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CurrencyCode") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Current") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("Balance"); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Institutions"); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("InstitutionId") + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InstitutionId") + .IsUnique(); + + b.ToTable("InstitutionMetadata"); + + b.HasDiscriminator("Type").HasValue("InstitutionMetadata"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Revoked") + .HasColumnType("INTEGER"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Token") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("FiscalOS.Core.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("EncryptedDataKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EncryptionKeyId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("HashedPassword") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("AccountId1") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MerchantName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("UserId1") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("AccountId1"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("Transactions"); + }); + + modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("TransactionId") + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TransactionId") + .IsUnique(); + + b.ToTable("TransactionMetadata"); + + b.HasDiscriminator("Type").HasValue("Plaid"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b => + { + b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata"); + + b.Property("Cursor") + .HasColumnType("TEXT"); + + b.Property("PlaidId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlaidName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasDiscriminator().HasValue("Plaid"); + }); + + modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b => + { + b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata"); + + b.Property("EncryptedAccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlaidId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlaidName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasDiscriminator().HasValue("Plaid"); + }); + + modelBuilder.Entity("FiscalOS.Infra.Transactions.Plaid.PlaidTransactionMetadata", b => + { + b.HasBaseType("FiscalOS.Core.Transactions.TransactionMetadata"); + + b.Property("PlaidId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasDiscriminator().HasValue("Plaid"); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b => + { + b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution") + .WithMany("Accounts") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FiscalOS.Core.Identity.User", null) + .WithMany("Accounts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b => + { + b.HasOne("FiscalOS.Core.Accounts.Account", null) + .WithOne("Metadata") + .HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b => + { + b.HasOne("FiscalOS.Core.Accounts.Account", null) + .WithMany("Balances") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b => + { + b.HasOne("FiscalOS.Core.Identity.User", "User") + .WithMany("Institutions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b => + { + b.HasOne("FiscalOS.Core.Accounts.Institution", null) + .WithOne("Metadata") + .HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b => + { + b.HasOne("FiscalOS.Core.Identity.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b => + { + b.HasOne("FiscalOS.Core.Accounts.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FiscalOS.Core.Accounts.Account", null) + .WithMany("Transactions") + .HasForeignKey("AccountId1"); + + b.HasOne("FiscalOS.Core.Identity.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FiscalOS.Core.Identity.User", null) + .WithMany("Transactions") + .HasForeignKey("UserId1"); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b => + { + b.HasOne("FiscalOS.Core.Transactions.Transaction", null) + .WithOne("Metadata") + .HasForeignKey("FiscalOS.Core.Transactions.TransactionMetadata", "TransactionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b => + { + b.Navigation("Balances"); + + b.Navigation("Metadata"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b => + { + b.Navigation("Accounts"); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("FiscalOS.Core.Identity.User", b => + { + b.Navigation("Accounts"); + + b.Navigation("Institutions"); + + b.Navigation("RefreshTokens"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b => + { + b.Navigation("Metadata"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/FiscalOS.Infra/Migrations/20260303124406_AddTransactionsNavigationPropertyToUser.cs b/src/FiscalOS.Infra/Migrations/20260303124406_AddTransactionsNavigationPropertyToUser.cs new file mode 100644 index 0000000..bbf55f7 --- /dev/null +++ b/src/FiscalOS.Infra/Migrations/20260303124406_AddTransactionsNavigationPropertyToUser.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FiscalOS.Infra.Migrations +{ + /// + public partial class AddTransactionsNavigationPropertyToUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UserId1", + table: "Transactions", + type: "TEXT", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Transactions_UserId1", + table: "Transactions", + column: "UserId1"); + + migrationBuilder.AddForeignKey( + name: "FK_Transactions_Users_UserId1", + table: "Transactions", + column: "UserId1", + principalTable: "Users", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Transactions_Users_UserId1", + table: "Transactions"); + + migrationBuilder.DropIndex( + name: "IX_Transactions_UserId1", + table: "Transactions"); + + migrationBuilder.DropColumn( + name: "UserId1", + table: "Transactions"); + } + } +} diff --git a/src/FiscalOS.Infra/Migrations/20260303130637_UpdateAppDbContextToIncludeTransactionNavigationProperties.Designer.cs b/src/FiscalOS.Infra/Migrations/20260303130637_UpdateAppDbContextToIncludeTransactionNavigationProperties.Designer.cs new file mode 100644 index 0000000..f6dc3c4 --- /dev/null +++ b/src/FiscalOS.Infra/Migrations/20260303130637_UpdateAppDbContextToIncludeTransactionNavigationProperties.Designer.cs @@ -0,0 +1,500 @@ +// +using System; +using FiscalOS.Infra.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FiscalOS.Infra.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260303130637_UpdateAppDbContextToIncludeTransactionNavigationProperties")] + partial class UpdateAppDbContextToIncludeTransactionNavigationProperties + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.2"); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("InstitutionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InstitutionId"); + + b.HasIndex("UserId"); + + b.ToTable("Account"); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId") + .IsUnique(); + + b.ToTable("AccountMetadata"); + + b.HasDiscriminator("Type").HasValue("AccountMetadata"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("Available") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CurrencyCode") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Current") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("Balance"); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Institutions"); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("InstitutionId") + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InstitutionId") + .IsUnique(); + + b.ToTable("InstitutionMetadata"); + + b.HasDiscriminator("Type").HasValue("InstitutionMetadata"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Revoked") + .HasColumnType("INTEGER"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Token") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("FiscalOS.Core.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("EncryptedDataKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EncryptionKeyId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("HashedPassword") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MerchantName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("Transactions"); + }); + + modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("TransactionId") + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TransactionId") + .IsUnique(); + + b.ToTable("TransactionMetadata"); + + b.HasDiscriminator("Type").HasValue("Plaid"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b => + { + b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata"); + + b.Property("Cursor") + .HasColumnType("TEXT"); + + b.Property("PlaidId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlaidName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasDiscriminator().HasValue("Plaid"); + }); + + modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidInstitutionMetadata", b => + { + b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata"); + + b.Property("EncryptedAccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlaidId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlaidName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasDiscriminator().HasValue("Plaid"); + }); + + modelBuilder.Entity("FiscalOS.Infra.Transactions.Plaid.PlaidTransactionMetadata", b => + { + b.HasBaseType("FiscalOS.Core.Transactions.TransactionMetadata"); + + b.Property("PlaidId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasDiscriminator().HasValue("Plaid"); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b => + { + b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution") + .WithMany("Accounts") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FiscalOS.Core.Identity.User", null) + .WithMany("Accounts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b => + { + b.HasOne("FiscalOS.Core.Accounts.Account", null) + .WithOne("Metadata") + .HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b => + { + b.HasOne("FiscalOS.Core.Accounts.Account", null) + .WithMany("Balances") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b => + { + b.HasOne("FiscalOS.Core.Identity.User", "User") + .WithMany("Institutions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b => + { + b.HasOne("FiscalOS.Core.Accounts.Institution", null) + .WithOne("Metadata") + .HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b => + { + b.HasOne("FiscalOS.Core.Identity.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b => + { + b.HasOne("FiscalOS.Core.Accounts.Account", "Account") + .WithMany("Transactions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FiscalOS.Core.Identity.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("FiscalOS.Core.Transactions.TransactionMetadata", b => + { + b.HasOne("FiscalOS.Core.Transactions.Transaction", null) + .WithOne("Metadata") + .HasForeignKey("FiscalOS.Core.Transactions.TransactionMetadata", "TransactionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b => + { + b.Navigation("Balances"); + + b.Navigation("Metadata"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b => + { + b.Navigation("Accounts"); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("FiscalOS.Core.Identity.User", b => + { + b.Navigation("Accounts"); + + b.Navigation("Institutions"); + + b.Navigation("RefreshTokens"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b => + { + b.Navigation("Metadata"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/FiscalOS.Infra/Migrations/20260303130637_UpdateAppDbContextToIncludeTransactionNavigationProperties.cs b/src/FiscalOS.Infra/Migrations/20260303130637_UpdateAppDbContextToIncludeTransactionNavigationProperties.cs new file mode 100644 index 0000000..dac3d54 --- /dev/null +++ b/src/FiscalOS.Infra/Migrations/20260303130637_UpdateAppDbContextToIncludeTransactionNavigationProperties.cs @@ -0,0 +1,78 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FiscalOS.Infra.Migrations +{ + /// + public partial class UpdateAppDbContextToIncludeTransactionNavigationProperties : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Transactions_Account_AccountId1", + table: "Transactions"); + + migrationBuilder.DropForeignKey( + name: "FK_Transactions_Users_UserId1", + table: "Transactions"); + + migrationBuilder.DropIndex( + name: "IX_Transactions_AccountId1", + table: "Transactions"); + + migrationBuilder.DropIndex( + name: "IX_Transactions_UserId1", + table: "Transactions"); + + migrationBuilder.DropColumn( + name: "AccountId1", + table: "Transactions"); + + migrationBuilder.DropColumn( + name: "UserId1", + table: "Transactions"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AccountId1", + table: "Transactions", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "UserId1", + table: "Transactions", + type: "TEXT", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Transactions_AccountId1", + table: "Transactions", + column: "AccountId1"); + + migrationBuilder.CreateIndex( + name: "IX_Transactions_UserId1", + table: "Transactions", + column: "UserId1"); + + migrationBuilder.AddForeignKey( + name: "FK_Transactions_Account_AccountId1", + table: "Transactions", + column: "AccountId1", + principalTable: "Account", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Transactions_Users_UserId1", + table: "Transactions", + column: "UserId1", + principalTable: "Users", + principalColumn: "Id"); + } + } +} diff --git a/src/FiscalOS.Infra/Migrations/AppDbContextModelSnapshot.cs b/src/FiscalOS.Infra/Migrations/AppDbContextModelSnapshot.cs index f111fb6..f948f88 100644 --- a/src/FiscalOS.Infra/Migrations/AppDbContextModelSnapshot.cs +++ b/src/FiscalOS.Infra/Migrations/AppDbContextModelSnapshot.cs @@ -250,9 +250,6 @@ namespace FiscalOS.Infra.Migrations b.Property("AccountId") .HasColumnType("TEXT"); - b.Property("AccountId1") - .HasColumnType("TEXT"); - b.Property("Amount") .HasColumnType("TEXT"); @@ -280,8 +277,6 @@ namespace FiscalOS.Infra.Migrations b.HasIndex("AccountId"); - b.HasIndex("AccountId1"); - b.HasIndex("UserId"); b.ToTable("Transactions"); @@ -440,17 +435,13 @@ namespace FiscalOS.Infra.Migrations modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b => { b.HasOne("FiscalOS.Core.Accounts.Account", "Account") - .WithMany() + .WithMany("Transactions") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("FiscalOS.Core.Accounts.Account", null) - .WithMany("Transactions") - .HasForeignKey("AccountId1"); - b.HasOne("FiscalOS.Core.Identity.User", "User") - .WithMany() + .WithMany("Transactions") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -492,6 +483,8 @@ namespace FiscalOS.Infra.Migrations b.Navigation("Institutions"); b.Navigation("RefreshTokens"); + + b.Navigation("Transactions"); }); modelBuilder.Entity("FiscalOS.Core.Transactions.Transaction", b => diff --git a/src/FiscalOS.Infra/Transactions/Plaid/PlaidExtensions.cs b/src/FiscalOS.Infra/Transactions/Plaid/PlaidExtensions.cs index 4661d54..66619aa 100644 --- a/src/FiscalOS.Infra/Transactions/Plaid/PlaidExtensions.cs +++ b/src/FiscalOS.Infra/Transactions/Plaid/PlaidExtensions.cs @@ -3,7 +3,7 @@ namespace FiscalOS.Infra.Transactions.Plaid; public static class PlaidExtensions { #pragma warning disable CA1034 - extension(Transaction transaction) + extension(Going.Plaid.Entity.Transaction transaction) { public string Merchant => transaction.MerchantName ?? "Unknown merchant"; public string Description => transaction.OriginalDescription ?? ""; diff --git a/src/FiscalOS.Infra/Transactions/Plaid/PlaidRemovedTransactionHandler.cs b/src/FiscalOS.Infra/Transactions/Plaid/PlaidRemovedTransactionHandler.cs index 35f7062..571bc9f 100644 --- a/src/FiscalOS.Infra/Transactions/Plaid/PlaidRemovedTransactionHandler.cs +++ b/src/FiscalOS.Infra/Transactions/Plaid/PlaidRemovedTransactionHandler.cs @@ -19,6 +19,14 @@ internal sealed class PlaidRemovedTransactionHandler : IPlaidRemovedTransactionH _logger = logger; } + public static PlaidRemovedTransactionHandler From( + AppDbContext appDbContext, + ILogger logger + ) + { + return new(appDbContext, logger); + } + public static PlaidRemovedTransactionHandler From(IServiceProvider serviceProvider) { return new( diff --git a/src/FiscalOS.Infra/Transactions/Plaid/PlaidTransactionMetadata.cs b/src/FiscalOS.Infra/Transactions/Plaid/PlaidTransactionMetadata.cs index a39730d..f820157 100644 --- a/src/FiscalOS.Infra/Transactions/Plaid/PlaidTransactionMetadata.cs +++ b/src/FiscalOS.Infra/Transactions/Plaid/PlaidTransactionMetadata.cs @@ -1,6 +1,3 @@ -using FiscalOS.Core.Transactions; -using FiscalOS.Infra.Common; - namespace FiscalOS.Infra.Transactions.Plaid; public sealed class PlaidTransactionMetadata : TransactionMetadata @@ -14,7 +11,7 @@ public sealed class PlaidTransactionMetadata : TransactionMetadata public static PlaidTransactionMetadata From(string? plaidId) { - ArgumentNullException.ThrowIfNull(plaidId); + ArgumentNullException.ThrowIfNull(plaidId, nameof(plaidId)); return new() { diff --git a/src/FiscalOS.Infra/Usings.cs b/src/FiscalOS.Infra/Usings.cs index 3d60f10..3138499 100644 --- a/src/FiscalOS.Infra/Usings.cs +++ b/src/FiscalOS.Infra/Usings.cs @@ -13,12 +13,15 @@ global using FiscalOS.Core.Data; global using FiscalOS.Core.Identity; global using FiscalOS.Core.Queuing; global using FiscalOS.Core.Security; +global using FiscalOS.Core.Transactions; global using FiscalOS.Infra.Accounts.Plaid; global using FiscalOS.Infra.Authentication; global using FiscalOS.Infra.Authorization; global using FiscalOS.Infra.Common; global using FiscalOS.Infra.Data; +global using FiscalOS.Infra.Queuing; global using FiscalOS.Infra.Security; +global using FiscalOS.Infra.Transactions.Plaid; global using Going.Plaid; global using Going.Plaid.Entity; diff --git a/src/FiscalOS.ServiceDefaults/ServiceDefaultsExtensions.cs b/src/FiscalOS.ServiceDefaults/ServiceDefaultsExtensions.cs index c965e8b..3fe1393 100644 --- a/src/FiscalOS.ServiceDefaults/ServiceDefaultsExtensions.cs +++ b/src/FiscalOS.ServiceDefaults/ServiceDefaultsExtensions.cs @@ -59,7 +59,7 @@ public static class ServiceDefaultsExtensions public static WebApplication MapDefaultEndpoints(this WebApplication app) { - ArgumentNullException.ThrowIfNull(app); + ArgumentNullException.ThrowIfNull(app, nameof(app)); if (app.Environment.IsDevelopment()) { diff --git a/tests/FiscalOS.AppHost.Tests/Infra/AspireFixture.cs b/tests/FiscalOS.AppHost.Tests/Infra/AspireFixture.cs index 723d40d..9a4484d 100644 --- a/tests/FiscalOS.AppHost.Tests/Infra/AspireFixture.cs +++ b/tests/FiscalOS.AppHost.Tests/Infra/AspireFixture.cs @@ -1,6 +1,3 @@ -using Aspire.Hosting; -using Aspire.Hosting.Testing; - namespace FiscalOS.AppHost.Tests.Infra; public sealed class AspireFixture : IAsyncLifetime diff --git a/tests/FiscalOS.AppHost.Tests/Usings.cs b/tests/FiscalOS.AppHost.Tests/Usings.cs index 80c8ec2..e892f9c 100644 --- a/tests/FiscalOS.AppHost.Tests/Usings.cs +++ b/tests/FiscalOS.AppHost.Tests/Usings.cs @@ -1,4 +1,6 @@ +global using Aspire.Hosting; +global using Aspire.Hosting.Testing; + global using FiscalOS.AppHost.Tests.Infra; global using Microsoft.Playwright; -global using Microsoft.Playwright.Xunit.v3; \ No newline at end of file diff --git a/tests/FiscalOS.Infra.Tests/Integration/IntegrationTest.cs b/tests/FiscalOS.Infra.Tests/Integration/IntegrationTest.cs new file mode 100644 index 0000000..6ca1dda --- /dev/null +++ b/tests/FiscalOS.Infra.Tests/Integration/IntegrationTest.cs @@ -0,0 +1,44 @@ +namespace FiscalOS.Infra.Tests.Integration; + +public abstract class IntegrationTest : IAsyncLifetime +{ + private bool _isDisposed; + private readonly FileSystem _fileSystem = new(); + protected AppDbContext AppDbContext { get; } + + protected IntegrationTest() + { + var options = Options.Create(new AppDbContextOptions() + { + DatabaseFilePath = $"{Guid.NewGuid()}.db", + }); + + AppDbContext = new(options, _fileSystem); + } + + public async ValueTask InitializeAsync() + { + await AppDbContext.Database.MigrateAsync(); + } + + public async ValueTask DisposeAsync() + { + await DisposeAsync(true); + GC.SuppressFinalize(this); + } + + protected virtual async Task DisposeAsync(bool disposing) + { + if (_isDisposed) + { + return; + } + + if (disposing) + { + await AppDbContext.DisposeAsync(); + } + + _isDisposed = true; + } +} \ No newline at end of file diff --git a/tests/FiscalOS.Infra.Tests/Integration/PlaidRemovedTransactionHandlerTests.cs b/tests/FiscalOS.Infra.Tests/Integration/PlaidRemovedTransactionHandlerTests.cs new file mode 100644 index 0000000..e545a42 --- /dev/null +++ b/tests/FiscalOS.Infra.Tests/Integration/PlaidRemovedTransactionHandlerTests.cs @@ -0,0 +1,59 @@ +namespace FiscalOS.Infra.Tests.Integration; + +public class PlaidRemovedTransactionHandlerTests : IntegrationTest +{ + private readonly Mock> _mockLogger = new(); + private readonly PlaidRemovedTransactionHandler _sut; + + public PlaidRemovedTransactionHandlerTests() + { + _sut = PlaidRemovedTransactionHandler.From(AppDbContext, _mockLogger.Object); + } + + [Fact] + public async Task HandleAsync_WhenCalled_ItShouldRemoveTheCorrectTransactions() + { + var user = User.From( + "username", + "hashedPassword", + EncryptedDataKey.From("keyUsed", "encryptedKey") + ); + + var institutionMetadata = PlaidInstitutionMetadata.From( + "plaidId", + "plaidName", + "encryptedAccessToken", + "itemId" + ); + var institution = Institution.From("institutionName", institutionMetadata); + user.AddInstitution(institution); + + var accountMetadata = PlaidAccountMetadata.From("plaidId", "plaidName"); + var account = Account.From("some account", accountMetadata); + user.AddAccount(account); + institution.AddAccount(account); + + var transactionMetadata = PlaidTransactionMetadata.From("plaidId"); + var transaction = Transaction.From( + "merchantName", + "description", + 100, + DateTimeOffset.UtcNow, + transactionMetadata + ); + user.AddTransaction(transaction); + account.AddTransaction(transaction); + + await AppDbContext.AddAsync(user, TestContext.Current.CancellationToken); + await AppDbContext.SaveChangesAsync(TestContext.Current.CancellationToken); + + var transactionToRemove = new Going.Plaid.Entity.RemovedTransaction() + { + TransactionId = transactionMetadata.PlaidId, + }; + + var result = await _sut.HandleAsync([transactionToRemove], TestContext.Current.CancellationToken); + + result.Should().Be(1); + } +} \ No newline at end of file diff --git a/tests/FiscalOS.Infra.Tests/Usings.cs b/tests/FiscalOS.Infra.Tests/Usings.cs index 03815c3..e022772 100644 --- a/tests/FiscalOS.Infra.Tests/Usings.cs +++ b/tests/FiscalOS.Infra.Tests/Usings.cs @@ -7,19 +7,26 @@ global using System.Text.Json; global using AwesomeAssertions.Primitives; +global using FiscalOS.Core.Accounts; global using FiscalOS.Core.Identity; global using FiscalOS.Core.Security; +global using FiscalOS.Core.Transactions; +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 FiscalOS.Infra.Tests.Assertions; global using FiscalOS.Infra.Tests.Mocks; +global using FiscalOS.Infra.Transactions.Plaid; global using Microsoft.AspNetCore.Authentication.JwtBearer; global using Microsoft.AspNetCore.Authorization; global using Microsoft.AspNetCore.Authorization.Policy; global using Microsoft.AspNetCore.Http; global using Microsoft.AspNetCore.Mvc; +global using Microsoft.EntityFrameworkCore; global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.Logging; global using Microsoft.Extensions.Options; global using Microsoft.IdentityModel.Tokens; \ No newline at end of file