From 5d6ccaa6df76e5a131800fde949e6a2c5c189e38 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:05:30 -0600 Subject: [PATCH] feat(infra): added plaid specific metadata and updated context to model account --- .../Accounts/Plaid/PlaidAccountMetadata.cs | 21 ++ .../Accounts/Plaid/PlaidMetadata.cs | 2 +- src/FiscalOS.Infra/Accounts/Providers.cs | 6 + src/FiscalOS.Infra/Data/AppDbContext.cs | 38 ++ ...60213130218_AccountAndMetadata.Designer.cs | 327 ++++++++++++++++++ .../20260213130218_AccountAndMetadata.cs | 92 +++++ .../Migrations/AppDbContextModelSnapshot.cs | 112 ++++++ 7 files changed, 597 insertions(+), 1 deletion(-) create mode 100644 src/FiscalOS.Infra/Accounts/Plaid/PlaidAccountMetadata.cs create mode 100644 src/FiscalOS.Infra/Accounts/Providers.cs create mode 100644 src/FiscalOS.Infra/Migrations/20260213130218_AccountAndMetadata.Designer.cs create mode 100644 src/FiscalOS.Infra/Migrations/20260213130218_AccountAndMetadata.cs diff --git a/src/FiscalOS.Infra/Accounts/Plaid/PlaidAccountMetadata.cs b/src/FiscalOS.Infra/Accounts/Plaid/PlaidAccountMetadata.cs new file mode 100644 index 0000000..bd1d7ff --- /dev/null +++ b/src/FiscalOS.Infra/Accounts/Plaid/PlaidAccountMetadata.cs @@ -0,0 +1,21 @@ +namespace FiscalOS.Infra.Accounts.Plaid; + +public sealed class PlaidAccountMetadata : AccountMetadata +{ + public const string TypeValue = Providers.Plaid; + public string PlaidId { get; init; } = string.Empty; + public string PlaidName { get; init; } = string.Empty; + + private PlaidAccountMetadata() : base(TypeValue) + { + } + + public static PlaidAccountMetadata From(string plaidId, string plaidName) + { + return new() + { + PlaidId = plaidId, + PlaidName = plaidName, + }; + } +} \ 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 f488428..0237570 100644 --- a/src/FiscalOS.Infra/Accounts/Plaid/PlaidMetadata.cs +++ b/src/FiscalOS.Infra/Accounts/Plaid/PlaidMetadata.cs @@ -2,7 +2,7 @@ namespace FiscalOS.Infra.Accounts.Plaid; public sealed class PlaidMetadata : InstitutionMetadata { - public const string TypeValue = "Plaid"; + public const string TypeValue = Providers.Plaid; public string PlaidId { get; init; } = string.Empty; public string PlaidName { get; init; } = string.Empty; public string EncryptedAccessToken { get; init; } = string.Empty; diff --git a/src/FiscalOS.Infra/Accounts/Providers.cs b/src/FiscalOS.Infra/Accounts/Providers.cs new file mode 100644 index 0000000..8cc1a2d --- /dev/null +++ b/src/FiscalOS.Infra/Accounts/Providers.cs @@ -0,0 +1,6 @@ +namespace FiscalOS.Infra.Accounts; + +public static class Providers +{ + public const string Plaid = nameof(Plaid); +} \ No newline at end of file diff --git a/src/FiscalOS.Infra/Data/AppDbContext.cs b/src/FiscalOS.Infra/Data/AppDbContext.cs index 65ea02c..ed01484 100644 --- a/src/FiscalOS.Infra/Data/AppDbContext.cs +++ b/src/FiscalOS.Infra/Data/AppDbContext.cs @@ -1,3 +1,4 @@ +using Account = FiscalOS.Core.Accounts.Account; using Institution = FiscalOS.Core.Accounts.Institution; namespace FiscalOS.Infra.Data; @@ -65,6 +66,11 @@ public sealed class AppDbContext( .WithOne() .HasForeignKey(static i => i.UserId) .OnDelete(DeleteBehavior.Cascade); + + eb.HasMany(static u => u.Accounts) + .WithOne() + .HasForeignKey(static a => a.UserId) + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity(static eb => @@ -104,5 +110,37 @@ public sealed class AppDbContext( eb.Property(static m => m.PlaidName); eb.Property(static m => m.EncryptedAccessToken); }); + + modelBuilder.Entity(static eb => + { + eb.Property(static a => a.Name); + + eb.HasOne(static a => a.Institution) + .WithMany() + .HasForeignKey(static a => a.InstituionId) + .OnDelete(DeleteBehavior.Cascade); + + eb.HasOne(static a => a.Metadata) + .WithOne() + .HasForeignKey(static m => m.AccountId) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(static eb => + { + eb.HasDiscriminator(static m => m.Type) + .HasValue(PlaidAccountMetadata.TypeValue); + + eb.Property(static m => m.AccountId); + eb.Property(static m => m.Type); + }); + + modelBuilder.Entity(static eb => + { + eb.HasBaseType(); + + eb.Property(static m => m.PlaidId); + eb.Property(static m => m.PlaidName); + }); } } \ No newline at end of file diff --git a/src/FiscalOS.Infra/Migrations/20260213130218_AccountAndMetadata.Designer.cs b/src/FiscalOS.Infra/Migrations/20260213130218_AccountAndMetadata.Designer.cs new file mode 100644 index 0000000..d93f449 --- /dev/null +++ b/src/FiscalOS.Infra/Migrations/20260213130218_AccountAndMetadata.Designer.cs @@ -0,0 +1,327 @@ +// +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("20260213130218_AccountAndMetadata")] + partial class AccountAndMetadata + { + /// + 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("InstituionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InstituionId"); + + 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.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("Institution"); + }); + + 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.Infra.Accounts.Plaid.PlaidAccountMetadata", b => + { + b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata"); + + b.Property("PlaidId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlaidName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasDiscriminator().HasValue("Plaid"); + }); + + modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b => + { + b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata"); + + b.Property("EncryptedAccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlaidId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlaidName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasDiscriminator().HasValue("Plaid"); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b => + { + b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution") + .WithMany() + .HasForeignKey("InstituionId") + .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.Institution", b => + { + b.HasOne("FiscalOS.Core.Identity.User", null) + .WithMany("Institutions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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.Accounts.Account", b => + { + b.Navigation("Metadata") + .IsRequired(); + }); + + modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b => + { + b.Navigation("Metadata") + .IsRequired(); + }); + + modelBuilder.Entity("FiscalOS.Core.Identity.User", b => + { + b.Navigation("Accounts"); + + b.Navigation("Institutions"); + + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/FiscalOS.Infra/Migrations/20260213130218_AccountAndMetadata.cs b/src/FiscalOS.Infra/Migrations/20260213130218_AccountAndMetadata.cs new file mode 100644 index 0000000..20e0e04 --- /dev/null +++ b/src/FiscalOS.Infra/Migrations/20260213130218_AccountAndMetadata.cs @@ -0,0 +1,92 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FiscalOS.Infra.Migrations +{ + /// + public partial class AccountAndMetadata : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Account", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + InstituionId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Account", x => x.Id); + table.ForeignKey( + name: "FK_Account_Institution_InstituionId", + column: x => x.InstituionId, + principalTable: "Institution", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Account_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AccountMetadata", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Type = table.Column(type: "TEXT", maxLength: 21, nullable: false), + AccountId = table.Column(type: "TEXT", nullable: false), + PlaidId = table.Column(type: "TEXT", nullable: true), + PlaidName = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccountMetadata", x => x.Id); + table.ForeignKey( + name: "FK_AccountMetadata_Account_AccountId", + column: x => x.AccountId, + principalTable: "Account", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Account_InstituionId", + table: "Account", + column: "InstituionId"); + + migrationBuilder.CreateIndex( + name: "IX_Account_UserId", + table: "Account", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AccountMetadata_AccountId", + table: "AccountMetadata", + column: "AccountId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AccountMetadata"); + + migrationBuilder.DropTable( + name: "Account"); + } + } +} diff --git a/src/FiscalOS.Infra/Migrations/AppDbContextModelSnapshot.cs b/src/FiscalOS.Infra/Migrations/AppDbContextModelSnapshot.cs index d6bdd49..d1cbc38 100644 --- a/src/FiscalOS.Infra/Migrations/AppDbContextModelSnapshot.cs +++ b/src/FiscalOS.Infra/Migrations/AppDbContextModelSnapshot.cs @@ -17,6 +17,69 @@ namespace FiscalOS.Infra.Migrations #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("InstituionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InstituionId"); + + 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.Institution", b => { b.Property("Id") @@ -146,6 +209,21 @@ namespace FiscalOS.Infra.Migrations b.ToTable("Users"); }); + modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b => + { + b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata"); + + b.Property("PlaidId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlaidName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasDiscriminator().HasValue("Plaid"); + }); + modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b => { b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata"); @@ -165,6 +243,32 @@ namespace FiscalOS.Infra.Migrations b.HasDiscriminator().HasValue("Plaid"); }); + modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b => + { + b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution") + .WithMany() + .HasForeignKey("InstituionId") + .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.Institution", b => { b.HasOne("FiscalOS.Core.Identity.User", null) @@ -194,6 +298,12 @@ namespace FiscalOS.Infra.Migrations b.Navigation("User"); }); + modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b => + { + b.Navigation("Metadata") + .IsRequired(); + }); + modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b => { b.Navigation("Metadata") @@ -202,6 +312,8 @@ namespace FiscalOS.Infra.Migrations modelBuilder.Entity("FiscalOS.Core.Identity.User", b => { + b.Navigation("Accounts"); + b.Navigation("Institutions"); b.Navigation("RefreshTokens");