From 83ef8480c128f277ce1c56b88cc58b40d97cee42 Mon Sep 17 00:00:00 2001 From: AJ Date: Sat, 11 Oct 2025 20:53:58 -0400 Subject: [PATCH] Add migration to make AccountId required on transactions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed Transaction.AccountId from optional to required, reflecting that every transaction must belong to an account (the source of the CSV file). Cards remain optional as they represent the payment method used. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ...keAccountRequiredOnTransaction.Designer.cs | 528 ++++++++++++++++++ ...231349_MakeAccountRequiredOnTransaction.cs | 89 +++ .../MoneyMapContextModelSnapshot.cs | 18 +- 3 files changed, 632 insertions(+), 3 deletions(-) create mode 100644 MoneyMap/Migrations/20251010231349_MakeAccountRequiredOnTransaction.Designer.cs create mode 100644 MoneyMap/Migrations/20251010231349_MakeAccountRequiredOnTransaction.cs diff --git a/MoneyMap/Migrations/20251010231349_MakeAccountRequiredOnTransaction.Designer.cs b/MoneyMap/Migrations/20251010231349_MakeAccountRequiredOnTransaction.Designer.cs new file mode 100644 index 0000000..28828c7 --- /dev/null +++ b/MoneyMap/Migrations/20251010231349_MakeAccountRequiredOnTransaction.Designer.cs @@ -0,0 +1,528 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MoneyMap.Data; + +#nullable disable + +namespace MoneyMap.Migrations +{ + [DbContext(typeof(MoneyMapContext))] + [Migration("20251010231349_MakeAccountRequiredOnTransaction")] + partial class MakeAccountRequiredOnTransaction + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("MoneyMap.Models.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountType") + .HasColumnType("int"); + + b.Property("Institution") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Last4") + .IsRequired() + .HasMaxLength(4) + .HasColumnType("nvarchar(4)"); + + b.Property("Nickname") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Owner") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("Institution", "Last4", "Owner"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MoneyMap.Models.Card", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("Issuer") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Last4") + .IsRequired() + .HasMaxLength(4) + .HasColumnType("nvarchar(4)"); + + b.Property("Nickname") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Owner") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("Issuer", "Last4", "Owner"); + + b.ToTable("Cards"); + }); + + modelBuilder.Entity("MoneyMap.Models.Receipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasDefaultValue("application/octet-stream"); + + b.Property("Currency") + .HasMaxLength(8) + .HasColumnType("nvarchar(8)"); + + b.Property("FileHashSha256") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("nvarchar(260)"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("Merchant") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ReceiptDate") + .HasColumnType("datetime2"); + + b.Property("StoragePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("Subtotal") + .HasColumnType("decimal(18,2)"); + + b.Property("Tax") + .HasColumnType("decimal(18,2)"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.Property("TransactionId") + .HasColumnType("bigint"); + + b.Property("UploadedAtUtc") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("TransactionId", "FileHashSha256") + .IsUnique(); + + b.ToTable("Receipts"); + }); + + modelBuilder.Entity("MoneyMap.Models.ReceiptLineItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("LineNumber") + .HasColumnType("int"); + + b.Property("LineTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,4)"); + + b.Property("ReceiptId") + .HasColumnType("bigint"); + + b.Property("Sku") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Unit") + .HasMaxLength(16) + .HasColumnType("nvarchar(16)"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,4)"); + + b.HasKey("Id"); + + b.HasIndex("ReceiptId", "LineNumber"); + + b.ToTable("ReceiptLineItems"); + }); + + modelBuilder.Entity("MoneyMap.Models.ReceiptParseLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompletedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Confidence") + .HasColumnType("decimal(5,4)"); + + b.Property("Error") + .HasColumnType("nvarchar(max)"); + + b.Property("ExtractedTextPath") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("Model") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProviderJobId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RawProviderPayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReceiptId") + .HasColumnType("bigint"); + + b.Property("StartedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("ReceiptId", "StartedAtUtc"); + + b.ToTable("ReceiptParseLogs"); + }); + + modelBuilder.Entity("MoneyMap.Models.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CardId") + .HasColumnType("int"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("Last4") + .HasMaxLength(4) + .HasColumnType("nvarchar(4)"); + + b.Property("Memo") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasDefaultValue(""); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Notes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TransactionType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("Amount"); + + b.HasIndex("CardId"); + + b.HasIndex("Category"); + + b.HasIndex("Date"); + + b.HasIndex("Date", "Amount", "Name", "Memo", "AccountId", "CardId") + .IsUnique() + .HasFilter("[CardId] IS NOT NULL"); + + b.ToTable("Transactions"); + }); + + modelBuilder.Entity("MoneyMap.Models.Transfer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("DestinationAccountId") + .HasColumnType("int"); + + b.Property("Notes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OriginalTransactionId") + .HasColumnType("bigint"); + + b.Property("SourceAccountId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("DestinationAccountId"); + + b.HasIndex("OriginalTransactionId"); + + b.HasIndex("SourceAccountId"); + + b.ToTable("Transfers"); + }); + + modelBuilder.Entity("MoneyMap.Services.CategoryMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Pattern") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Priority") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("CategoryMappings"); + }); + + modelBuilder.Entity("MoneyMap.Models.Card", b => + { + b.HasOne("MoneyMap.Models.Account", "Account") + .WithMany("Cards") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("MoneyMap.Models.Receipt", b => + { + b.HasOne("MoneyMap.Models.Transaction", "Transaction") + .WithMany("Receipts") + .HasForeignKey("TransactionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Transaction"); + }); + + modelBuilder.Entity("MoneyMap.Models.ReceiptLineItem", b => + { + b.HasOne("MoneyMap.Models.Receipt", "Receipt") + .WithMany("LineItems") + .HasForeignKey("ReceiptId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Receipt"); + }); + + modelBuilder.Entity("MoneyMap.Models.ReceiptParseLog", b => + { + b.HasOne("MoneyMap.Models.Receipt", "Receipt") + .WithMany("ParseLogs") + .HasForeignKey("ReceiptId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Receipt"); + }); + + modelBuilder.Entity("MoneyMap.Models.Transaction", b => + { + b.HasOne("MoneyMap.Models.Account", "Account") + .WithMany("Transactions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("MoneyMap.Models.Card", "Card") + .WithMany("Transactions") + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Account"); + + b.Navigation("Card"); + }); + + modelBuilder.Entity("MoneyMap.Models.Transfer", b => + { + b.HasOne("MoneyMap.Models.Account", "DestinationAccount") + .WithMany("DestinationTransfers") + .HasForeignKey("DestinationAccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("MoneyMap.Models.Transaction", "OriginalTransaction") + .WithMany() + .HasForeignKey("OriginalTransactionId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("MoneyMap.Models.Account", "SourceAccount") + .WithMany("SourceTransfers") + .HasForeignKey("SourceAccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DestinationAccount"); + + b.Navigation("OriginalTransaction"); + + b.Navigation("SourceAccount"); + }); + + modelBuilder.Entity("MoneyMap.Models.Account", b => + { + b.Navigation("Cards"); + + b.Navigation("DestinationTransfers"); + + b.Navigation("SourceTransfers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("MoneyMap.Models.Card", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("MoneyMap.Models.Receipt", b => + { + b.Navigation("LineItems"); + + b.Navigation("ParseLogs"); + }); + + modelBuilder.Entity("MoneyMap.Models.Transaction", b => + { + b.Navigation("Receipts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MoneyMap/Migrations/20251010231349_MakeAccountRequiredOnTransaction.cs b/MoneyMap/Migrations/20251010231349_MakeAccountRequiredOnTransaction.cs new file mode 100644 index 0000000..780776e --- /dev/null +++ b/MoneyMap/Migrations/20251010231349_MakeAccountRequiredOnTransaction.cs @@ -0,0 +1,89 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MoneyMap.Migrations +{ + /// + public partial class MakeAccountRequiredOnTransaction : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Transactions_Date_Amount_Name_Memo_CardId_AccountId", + table: "Transactions"); + + // For existing transactions with CardId but no AccountId: + // Set AccountId to the card's linked account if available + migrationBuilder.Sql(@" + UPDATE Transactions + SET AccountId = c.AccountId + FROM Transactions t + INNER JOIN Cards c ON t.CardId = c.Id + WHERE t.AccountId IS NULL AND t.CardId IS NOT NULL AND c.AccountId IS NOT NULL + "); + + // For card transactions where card has no linked account, we need to handle this + // Delete or move to a default account - for now, we'll prevent the migration if this case exists + migrationBuilder.Sql(@" + IF EXISTS ( + SELECT 1 FROM Transactions t + INNER JOIN Cards c ON t.CardId = c.Id + WHERE t.AccountId IS NULL AND c.AccountId IS NULL + ) + BEGIN + RAISERROR('Cannot migrate: Some transactions have cards that are not linked to accounts. Please link all cards to accounts first.', 16, 1) + END + "); + + // For remaining transactions with no AccountId and no CardId, they must be deleted or assigned + migrationBuilder.Sql(@" + IF EXISTS (SELECT 1 FROM Transactions WHERE AccountId IS NULL AND CardId IS NULL) + BEGIN + RAISERROR('Cannot migrate: Some transactions have neither AccountId nor CardId. Please fix these transactions first.', 16, 1) + END + "); + + // Now make AccountId required + migrationBuilder.AlterColumn( + name: "AccountId", + table: "Transactions", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Transactions_Date_Amount_Name_Memo_AccountId_CardId", + table: "Transactions", + columns: new[] { "Date", "Amount", "Name", "Memo", "AccountId", "CardId" }, + unique: true, + filter: "[CardId] IS NOT NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Transactions_Date_Amount_Name_Memo_AccountId_CardId", + table: "Transactions"); + + migrationBuilder.AlterColumn( + name: "AccountId", + table: "Transactions", + type: "int", + nullable: true, + oldClrType: typeof(int), + oldType: "int"); + + migrationBuilder.CreateIndex( + name: "IX_Transactions_Date_Amount_Name_Memo_CardId_AccountId", + table: "Transactions", + columns: new[] { "Date", "Amount", "Name", "Memo", "CardId", "AccountId" }, + unique: true, + filter: "[CardId] IS NOT NULL AND [AccountId] IS NOT NULL"); + } + } +} diff --git a/MoneyMap/Migrations/MoneyMapContextModelSnapshot.cs b/MoneyMap/Migrations/MoneyMapContextModelSnapshot.cs index 710abf5..14ab618 100644 --- a/MoneyMap/Migrations/MoneyMapContextModelSnapshot.cs +++ b/MoneyMap/Migrations/MoneyMapContextModelSnapshot.cs @@ -275,7 +275,7 @@ namespace MoneyMap.Migrations SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("AccountId") + b.Property("AccountId") .HasColumnType("int"); b.Property("Amount") @@ -317,6 +317,9 @@ namespace MoneyMap.Migrations .HasMaxLength(20) .HasColumnType("nvarchar(20)"); + b.Property("TransferToAccountId") + .HasColumnType("int"); + b.HasKey("Id"); b.HasIndex("AccountId"); @@ -329,9 +332,11 @@ namespace MoneyMap.Migrations b.HasIndex("Date"); - b.HasIndex("Date", "Amount", "Name", "Memo", "CardId", "AccountId") + b.HasIndex("TransferToAccountId"); + + b.HasIndex("Date", "Amount", "Name", "Memo", "AccountId", "CardId") .IsUnique() - .HasFilter("[CardId] IS NOT NULL AND [AccountId] IS NOT NULL"); + .HasFilter("[CardId] IS NOT NULL"); b.ToTable("Transactions"); }); @@ -463,9 +468,15 @@ namespace MoneyMap.Migrations .HasForeignKey("CardId") .OnDelete(DeleteBehavior.Restrict); + b.HasOne("MoneyMap.Models.Account", "TransferToAccount") + .WithMany() + .HasForeignKey("TransferToAccountId"); + b.Navigation("Account"); b.Navigation("Card"); + + b.Navigation("TransferToAccount"); }); modelBuilder.Entity("MoneyMap.Models.Transfer", b => @@ -503,6 +514,7 @@ namespace MoneyMap.Migrations b.Navigation("Transactions"); }); + modelBuilder.Entity("MoneyMap.Models.Card", b => { b.Navigation("Transactions");