diff --git a/src/Purrse.Data/Migrations/20260208151153_InitialCreate.Designer.cs b/src/Purrse.Data/Migrations/20260208151153_InitialCreate.Designer.cs
new file mode 100644
index 0000000..a6296b6
--- /dev/null
+++ b/src/Purrse.Data/Migrations/20260208151153_InitialCreate.Designer.cs
@@ -0,0 +1,1146 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using Purrse.Data;
+
+#nullable disable
+
+namespace Purrse.Data.Migrations
+{
+ [DbContext(typeof(PurrseDbContext))]
+ [Migration("20260208151153_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.6")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Purrse.Core.Models.Account", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AccountNumber")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Balance")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreditLimit")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("Institution")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("InterestRate")
+ .HasPrecision(8, 4)
+ .HasColumnType("numeric(8,4)");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean");
+
+ b.Property("IsClosed")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("Notes")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.Property("Type")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("accounts", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.AmortizationEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("EscrowAmount")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("InterestAmount")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("LoanDetailId")
+ .HasColumnType("uuid");
+
+ b.Property("PaymentAmount")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("PaymentDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("PaymentNumber")
+ .HasColumnType("integer");
+
+ b.Property("PrincipalAmount")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("RemainingBalance")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LoanDetailId");
+
+ b.ToTable("amortization_entries", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Budget", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Month")
+ .HasColumnType("integer");
+
+ b.Property("Notes")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.Property("Year")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Year", "Month")
+ .IsUnique();
+
+ b.ToTable("budgets", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.BudgetItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("BudgetId")
+ .HasColumnType("uuid");
+
+ b.Property("BudgetedAmount")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("CategoryId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BudgetId");
+
+ b.HasIndex("CategoryId");
+
+ b.ToTable("budget_items", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Category", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean");
+
+ b.Property("IsSystem")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("ParentId")
+ .HasColumnType("uuid");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.Property("Type")
+ .HasColumnType("integer");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("UserId", "Name", "ParentId")
+ .IsUnique();
+
+ b.ToTable("categories", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.ImportBatch", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AccountId")
+ .HasColumnType("uuid");
+
+ b.Property("DuplicateCount")
+ .HasColumnType("integer");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("FileType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("ImportedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ImportedCount")
+ .HasColumnType("integer");
+
+ b.Property("SkippedCount")
+ .HasColumnType("integer");
+
+ b.Property("Status")
+ .HasColumnType("integer");
+
+ b.Property("TotalCount")
+ .HasColumnType("integer");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccountId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("import_batches", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.InvestmentHolding", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AccountId")
+ .HasColumnType("uuid");
+
+ b.Property("AsOfDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CostBasis")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("SecurityId")
+ .HasColumnType("uuid");
+
+ b.Property("Shares")
+ .HasPrecision(18, 6)
+ .HasColumnType("numeric(18,6)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccountId");
+
+ b.HasIndex("SecurityId");
+
+ b.ToTable("investment_holdings", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.LoanDetail", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AccountId")
+ .HasColumnType("uuid");
+
+ b.Property("EscrowAmount")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("ExtraPayment")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("InterestRate")
+ .HasPrecision(8, 4)
+ .HasColumnType("numeric(8,4)");
+
+ b.Property("MaturityDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("MonthlyPayment")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("OriginalBalance")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("OriginationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("TermMonths")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccountId")
+ .IsUnique();
+
+ b.ToTable("loan_details", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Payee", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("DefaultCategoryId")
+ .HasColumnType("uuid");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DefaultCategoryId");
+
+ b.HasIndex("UserId", "Name")
+ .IsUnique();
+
+ b.ToTable("payees", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.PayeeAlias", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Alias")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)");
+
+ b.Property("PayeeId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Alias");
+
+ b.HasIndex("PayeeId");
+
+ b.ToTable("payee_aliases", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.PluginConfiguration", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("PluginRegistrationId")
+ .HasColumnType("uuid");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PluginRegistrationId", "Key")
+ .IsUnique();
+
+ b.ToTable("plugin_configurations", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.PluginRegistration", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("EntryAssembly")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("EntryType")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("InstalledAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("PluginId")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("Version")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PluginId")
+ .IsUnique();
+
+ b.ToTable("plugin_registrations", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Reconciliation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AccountId")
+ .HasColumnType("uuid");
+
+ b.Property("CompletedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsCompleted")
+ .HasColumnType("boolean");
+
+ b.Property("StatementBalance")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("StatementDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccountId");
+
+ b.ToTable("reconciliations", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.ScheduledTransaction", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AccountId")
+ .HasColumnType("uuid");
+
+ b.Property("Amount")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("AutoPost")
+ .HasColumnType("boolean");
+
+ b.Property("CategoryId")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EndDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Frequency")
+ .HasColumnType("integer");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean");
+
+ b.Property("Memo")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("NextDueDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("PayeeId")
+ .HasColumnType("uuid");
+
+ b.Property("PayeeName")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("ReminderDaysBefore")
+ .HasColumnType("integer");
+
+ b.Property("Type")
+ .HasColumnType("integer");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccountId");
+
+ b.HasIndex("CategoryId");
+
+ b.HasIndex("PayeeId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("scheduled_transactions", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Security", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("SecurityType")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Symbol")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Symbol")
+ .IsUnique();
+
+ b.ToTable("securities", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.SecurityPrice", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Date")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Price")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("SecurityId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SecurityId", "Date")
+ .IsUnique();
+
+ b.ToTable("security_prices", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Transaction", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AccountId")
+ .HasColumnType("uuid");
+
+ b.Property("Amount")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("CategoryId")
+ .HasColumnType("uuid");
+
+ b.Property("CheckNumber")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Date")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Fingerprint")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("FitId")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("ImportBatchId")
+ .HasColumnType("uuid");
+
+ b.Property("IsVoid")
+ .HasColumnType("boolean");
+
+ b.Property("Memo")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("PayeeId")
+ .HasColumnType("uuid");
+
+ b.Property("PayeeName")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("ReconciliationId")
+ .HasColumnType("uuid");
+
+ b.Property("ReferenceNumber")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Status")
+ .HasColumnType("integer");
+
+ b.Property("TransferTransactionId")
+ .HasColumnType("uuid");
+
+ b.Property("Type")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccountId");
+
+ b.HasIndex("CategoryId");
+
+ b.HasIndex("Date");
+
+ b.HasIndex("Fingerprint");
+
+ b.HasIndex("FitId");
+
+ b.HasIndex("ImportBatchId");
+
+ b.HasIndex("PayeeId");
+
+ b.HasIndex("ReconciliationId");
+
+ b.HasIndex("TransferTransactionId")
+ .IsUnique();
+
+ b.ToTable("transactions", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.TransactionSplit", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Amount")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("CategoryId")
+ .HasColumnType("uuid");
+
+ b.Property("Memo")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("TransactionId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CategoryId");
+
+ b.HasIndex("TransactionId");
+
+ b.ToTable("transaction_splits", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("LastLoginAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("PasswordHash")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("RefreshToken")
+ .HasColumnType("text");
+
+ b.Property("RefreshTokenExpiresAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Email")
+ .IsUnique();
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Account", b =>
+ {
+ b.HasOne("Purrse.Core.Models.User", "User")
+ .WithMany("Accounts")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.AmortizationEntry", b =>
+ {
+ b.HasOne("Purrse.Core.Models.LoanDetail", "LoanDetail")
+ .WithMany("AmortizationEntries")
+ .HasForeignKey("LoanDetailId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("LoanDetail");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Budget", b =>
+ {
+ b.HasOne("Purrse.Core.Models.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.BudgetItem", b =>
+ {
+ b.HasOne("Purrse.Core.Models.Budget", "Budget")
+ .WithMany("Items")
+ .HasForeignKey("BudgetId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Purrse.Core.Models.Category", "Category")
+ .WithMany("BudgetItems")
+ .HasForeignKey("CategoryId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Budget");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Category", b =>
+ {
+ b.HasOne("Purrse.Core.Models.Category", "Parent")
+ .WithMany("Children")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("Purrse.Core.Models.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Parent");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.ImportBatch", b =>
+ {
+ b.HasOne("Purrse.Core.Models.Account", "Account")
+ .WithMany()
+ .HasForeignKey("AccountId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Purrse.Core.Models.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Account");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.InvestmentHolding", b =>
+ {
+ b.HasOne("Purrse.Core.Models.Account", "Account")
+ .WithMany("InvestmentHoldings")
+ .HasForeignKey("AccountId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Purrse.Core.Models.Security", "Security")
+ .WithMany("Holdings")
+ .HasForeignKey("SecurityId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Account");
+
+ b.Navigation("Security");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.LoanDetail", b =>
+ {
+ b.HasOne("Purrse.Core.Models.Account", "Account")
+ .WithOne("LoanDetail")
+ .HasForeignKey("Purrse.Core.Models.LoanDetail", "AccountId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Account");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Payee", b =>
+ {
+ b.HasOne("Purrse.Core.Models.Category", "DefaultCategory")
+ .WithMany()
+ .HasForeignKey("DefaultCategoryId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Purrse.Core.Models.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("DefaultCategory");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.PayeeAlias", b =>
+ {
+ b.HasOne("Purrse.Core.Models.Payee", "Payee")
+ .WithMany("Aliases")
+ .HasForeignKey("PayeeId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Payee");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.PluginConfiguration", b =>
+ {
+ b.HasOne("Purrse.Core.Models.PluginRegistration", "PluginRegistration")
+ .WithMany("Configurations")
+ .HasForeignKey("PluginRegistrationId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("PluginRegistration");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Reconciliation", b =>
+ {
+ b.HasOne("Purrse.Core.Models.Account", "Account")
+ .WithMany("Reconciliations")
+ .HasForeignKey("AccountId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Account");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.ScheduledTransaction", b =>
+ {
+ b.HasOne("Purrse.Core.Models.Account", "Account")
+ .WithMany()
+ .HasForeignKey("AccountId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Purrse.Core.Models.Category", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Purrse.Core.Models.Payee", "Payee")
+ .WithMany()
+ .HasForeignKey("PayeeId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Purrse.Core.Models.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Account");
+
+ b.Navigation("Category");
+
+ b.Navigation("Payee");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.SecurityPrice", b =>
+ {
+ b.HasOne("Purrse.Core.Models.Security", "Security")
+ .WithMany("Prices")
+ .HasForeignKey("SecurityId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Security");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Transaction", b =>
+ {
+ b.HasOne("Purrse.Core.Models.Account", "Account")
+ .WithMany("Transactions")
+ .HasForeignKey("AccountId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Purrse.Core.Models.Category", "Category")
+ .WithMany("Transactions")
+ .HasForeignKey("CategoryId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Purrse.Core.Models.ImportBatch", "ImportBatch")
+ .WithMany("Transactions")
+ .HasForeignKey("ImportBatchId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Purrse.Core.Models.Payee", "Payee")
+ .WithMany("Transactions")
+ .HasForeignKey("PayeeId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Purrse.Core.Models.Reconciliation", "Reconciliation")
+ .WithMany("Transactions")
+ .HasForeignKey("ReconciliationId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Purrse.Core.Models.Transaction", "TransferTransaction")
+ .WithOne()
+ .HasForeignKey("Purrse.Core.Models.Transaction", "TransferTransactionId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.Navigation("Account");
+
+ b.Navigation("Category");
+
+ b.Navigation("ImportBatch");
+
+ b.Navigation("Payee");
+
+ b.Navigation("Reconciliation");
+
+ b.Navigation("TransferTransaction");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.TransactionSplit", b =>
+ {
+ b.HasOne("Purrse.Core.Models.Category", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Purrse.Core.Models.Transaction", "Transaction")
+ .WithMany("Splits")
+ .HasForeignKey("TransactionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Category");
+
+ b.Navigation("Transaction");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Account", b =>
+ {
+ b.Navigation("InvestmentHoldings");
+
+ b.Navigation("LoanDetail");
+
+ b.Navigation("Reconciliations");
+
+ b.Navigation("Transactions");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Budget", b =>
+ {
+ b.Navigation("Items");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Category", b =>
+ {
+ b.Navigation("BudgetItems");
+
+ b.Navigation("Children");
+
+ b.Navigation("Transactions");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.ImportBatch", b =>
+ {
+ b.Navigation("Transactions");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.LoanDetail", b =>
+ {
+ b.Navigation("AmortizationEntries");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Payee", b =>
+ {
+ b.Navigation("Aliases");
+
+ b.Navigation("Transactions");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.PluginRegistration", b =>
+ {
+ b.Navigation("Configurations");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Reconciliation", b =>
+ {
+ b.Navigation("Transactions");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Security", b =>
+ {
+ b.Navigation("Holdings");
+
+ b.Navigation("Prices");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.Transaction", b =>
+ {
+ b.Navigation("Splits");
+ });
+
+ modelBuilder.Entity("Purrse.Core.Models.User", b =>
+ {
+ b.Navigation("Accounts");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Purrse.Data/Migrations/20260208151153_InitialCreate.cs b/src/Purrse.Data/Migrations/20260208151153_InitialCreate.cs
new file mode 100644
index 0000000..e284b74
--- /dev/null
+++ b/src/Purrse.Data/Migrations/20260208151153_InitialCreate.cs
@@ -0,0 +1,800 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Purrse.Data.Migrations
+{
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "plugin_registrations",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ PluginId = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ Version = table.Column(type: "character varying(50)", maxLength: 50, nullable: false),
+ EntryAssembly = table.Column(type: "character varying(500)", maxLength: 500, nullable: false),
+ EntryType = table.Column(type: "character varying(500)", maxLength: 500, nullable: false),
+ IsEnabled = table.Column(type: "boolean", nullable: false),
+ InstalledAt = table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_plugin_registrations", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "securities",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ Symbol = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ SecurityType = table.Column(type: "character varying(50)", maxLength: 50, nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_securities", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "users",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ Username = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
+ Email = table.Column(type: "character varying(255)", maxLength: 255, nullable: false),
+ PasswordHash = table.Column(type: "character varying(255)", maxLength: 255, nullable: false),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false),
+ LastLoginAt = table.Column(type: "timestamp with time zone", nullable: true),
+ RefreshToken = table.Column(type: "text", nullable: true),
+ RefreshTokenExpiresAt = table.Column(type: "timestamp with time zone", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_users", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "plugin_configurations",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ PluginRegistrationId = table.Column(type: "uuid", nullable: false),
+ Key = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ Value = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_plugin_configurations", x => x.Id);
+ table.ForeignKey(
+ name: "FK_plugin_configurations_plugin_registrations_PluginRegistrati~",
+ column: x => x.PluginRegistrationId,
+ principalTable: "plugin_registrations",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "security_prices",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ SecurityId = table.Column(type: "uuid", nullable: false),
+ Date = table.Column(type: "timestamp with time zone", nullable: false),
+ Price = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_security_prices", x => x.Id);
+ table.ForeignKey(
+ name: "FK_security_prices_securities_SecurityId",
+ column: x => x.SecurityId,
+ principalTable: "securities",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "accounts",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ UserId = table.Column(type: "uuid", nullable: false),
+ Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ Type = table.Column(type: "integer", nullable: false),
+ Institution = table.Column(type: "character varying(200)", maxLength: 200, nullable: true),
+ AccountNumber = table.Column(type: "character varying(50)", maxLength: 50, nullable: true),
+ Balance = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
+ CreditLimit = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true),
+ InterestRate = table.Column(type: "numeric(8,4)", precision: 8, scale: 4, nullable: true),
+ IsActive = table.Column(type: "boolean", nullable: false),
+ IsClosed = table.Column(type: "boolean", nullable: false),
+ Notes = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true),
+ SortOrder = table.Column(type: "integer", nullable: false),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false),
+ UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_accounts", x => x.Id);
+ table.ForeignKey(
+ name: "FK_accounts_users_UserId",
+ column: x => x.UserId,
+ principalTable: "users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "budgets",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ UserId = table.Column(type: "uuid", nullable: false),
+ Year = table.Column(type: "integer", nullable: false),
+ Month = table.Column(type: "integer", nullable: false),
+ Notes = table.Column(type: "character varying(500)", maxLength: 500, nullable: true),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_budgets", x => x.Id);
+ table.ForeignKey(
+ name: "FK_budgets_users_UserId",
+ column: x => x.UserId,
+ principalTable: "users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "categories",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ UserId = table.Column(type: "uuid", nullable: false),
+ Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ Type = table.Column(type: "integer", nullable: false),
+ ParentId = table.Column(type: "uuid", nullable: true),
+ SortOrder = table.Column(type: "integer", nullable: false),
+ IsSystem = table.Column(type: "boolean", nullable: false),
+ IsActive = table.Column(type: "boolean", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_categories", x => x.Id);
+ table.ForeignKey(
+ name: "FK_categories_categories_ParentId",
+ column: x => x.ParentId,
+ principalTable: "categories",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ table.ForeignKey(
+ name: "FK_categories_users_UserId",
+ column: x => x.UserId,
+ principalTable: "users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "import_batches",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ UserId = table.Column(type: "uuid", nullable: false),
+ AccountId = table.Column(type: "uuid", nullable: false),
+ FileName = table.Column(type: "character varying(500)", maxLength: 500, nullable: false),
+ FileType = table.Column(type: "character varying(50)", maxLength: 50, nullable: false),
+ TotalCount = table.Column(type: "integer", nullable: false),
+ ImportedCount = table.Column(type: "integer", nullable: false),
+ DuplicateCount = table.Column(type: "integer", nullable: false),
+ SkippedCount = table.Column(type: "integer", nullable: false),
+ Status = table.Column(type: "integer", nullable: false),
+ ImportedAt = table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_import_batches", x => x.Id);
+ table.ForeignKey(
+ name: "FK_import_batches_accounts_AccountId",
+ column: x => x.AccountId,
+ principalTable: "accounts",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_import_batches_users_UserId",
+ column: x => x.UserId,
+ principalTable: "users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "investment_holdings",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ AccountId = table.Column(type: "uuid", nullable: false),
+ SecurityId = table.Column(type: "uuid", nullable: false),
+ Shares = table.Column(type: "numeric(18,6)", precision: 18, scale: 6, nullable: false),
+ CostBasis = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
+ AsOfDate = table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_investment_holdings", x => x.Id);
+ table.ForeignKey(
+ name: "FK_investment_holdings_accounts_AccountId",
+ column: x => x.AccountId,
+ principalTable: "accounts",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_investment_holdings_securities_SecurityId",
+ column: x => x.SecurityId,
+ principalTable: "securities",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "loan_details",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ AccountId = table.Column(type: "uuid", nullable: false),
+ OriginalBalance = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
+ InterestRate = table.Column(type: "numeric(8,4)", precision: 8, scale: 4, nullable: false),
+ TermMonths = table.Column(type: "integer", nullable: false),
+ MonthlyPayment = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
+ OriginationDate = table.Column(type: "timestamp with time zone", nullable: false),
+ MaturityDate = table.Column(type: "timestamp with time zone", nullable: false),
+ EscrowAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true),
+ ExtraPayment = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_loan_details", x => x.Id);
+ table.ForeignKey(
+ name: "FK_loan_details_accounts_AccountId",
+ column: x => x.AccountId,
+ principalTable: "accounts",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "reconciliations",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ AccountId = table.Column(type: "uuid", nullable: false),
+ StatementDate = table.Column(type: "timestamp with time zone", nullable: false),
+ StatementBalance = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
+ IsCompleted = table.Column(type: "boolean", nullable: false),
+ CompletedAt = table.Column(type: "timestamp with time zone", nullable: true),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_reconciliations", x => x.Id);
+ table.ForeignKey(
+ name: "FK_reconciliations_accounts_AccountId",
+ column: x => x.AccountId,
+ principalTable: "accounts",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "budget_items",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ BudgetId = table.Column(type: "uuid", nullable: false),
+ CategoryId = table.Column(type: "uuid", nullable: false),
+ BudgetedAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_budget_items", x => x.Id);
+ table.ForeignKey(
+ name: "FK_budget_items_budgets_BudgetId",
+ column: x => x.BudgetId,
+ principalTable: "budgets",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_budget_items_categories_CategoryId",
+ column: x => x.CategoryId,
+ principalTable: "categories",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "payees",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ UserId = table.Column(type: "uuid", nullable: false),
+ Name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false),
+ DefaultCategoryId = table.Column(type: "uuid", nullable: true),
+ IsActive = table.Column(type: "boolean", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_payees", x => x.Id);
+ table.ForeignKey(
+ name: "FK_payees_categories_DefaultCategoryId",
+ column: x => x.DefaultCategoryId,
+ principalTable: "categories",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.SetNull);
+ table.ForeignKey(
+ name: "FK_payees_users_UserId",
+ column: x => x.UserId,
+ principalTable: "users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "amortization_entries",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ LoanDetailId = table.Column