diff --git a/src/Purrse.Api/Services/SimpleFinConnectionMigration.cs b/src/Purrse.Api/Services/SimpleFinConnectionMigration.cs index f9d03ce..fd52edc 100644 --- a/src/Purrse.Api/Services/SimpleFinConnectionMigration.cs +++ b/src/Purrse.Api/Services/SimpleFinConnectionMigration.cs @@ -10,13 +10,28 @@ namespace Purrse.Api.Services; /// /// One-time data migration that splits SimpleFIN connections containing accounts /// from multiple institutions into separate connections per institution. +/// Tracks completion in a `data_migrations` table so it only runs once. /// public static class SimpleFinConnectionMigration { + private const string MigrationName = "SplitSimpleFinConnectionsByInstitution"; + public static async Task RunAsync(IServiceProvider services, ILogger logger) { using var scope = services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); + + // Create tracking table if it doesn't exist, then check if already applied + await db.Database.ExecuteSqlRawAsync( + "CREATE TABLE IF NOT EXISTS data_migrations (name TEXT PRIMARY KEY, applied_at TIMESTAMP NOT NULL)"); + + var alreadyApplied = await db.Database + .SqlQueryRaw($"SELECT 1 AS \"Value\" FROM data_migrations WHERE name = '{MigrationName}'") + .AnyAsync(); + + if (alreadyApplied) + return; + var encryption = scope.ServiceProvider.GetRequiredService(); var simpleFin = scope.ServiceProvider.GetRequiredService(); @@ -26,7 +41,10 @@ public static class SimpleFinConnectionMigration .ToListAsync(); if (connections.Count == 0) + { + await MarkCompleteAsync(db); return; + } var splitCount = 0; @@ -113,5 +131,13 @@ public static class SimpleFinConnectionMigration await db.SaveChangesAsync(); logger.LogInformation("SimpleFIN connection migration complete: created {Count} new connections", splitCount); } + + await MarkCompleteAsync(db); + } + + private static async Task MarkCompleteAsync(PurrseDbContext db) + { + await db.Database.ExecuteSqlRawAsync( + $"INSERT INTO data_migrations (name, applied_at) VALUES ('{MigrationName}', NOW()) ON CONFLICT DO NOTHING"); } }