diff --git a/src/Meezi.Infrastructure/Data/PlatformDataSeeder.cs b/src/Meezi.Infrastructure/Data/PlatformDataSeeder.cs index a22bde2..a2ecb1e 100644 --- a/src/Meezi.Infrastructure/Data/PlatformDataSeeder.cs +++ b/src/Meezi.Infrastructure/Data/PlatformDataSeeder.cs @@ -29,15 +29,24 @@ public static class PlatformDataSeeder // fresh deploy. Idempotent. Phone is overridable via "Seed:SystemAdminPhone". await EnsureOwnerAdminAsync(db, config, logger); - // Production-safe: give cafés without a map pin an approximate location - // from their city, so the public map lights up. Idempotent (fills nulls). - await BackfillCafeLocationsAsync(db, logger); + // Best-effort, NON-FATAL seeding. These steps populate convenience data + // (map pins, plan/feature catalog) and must never crash-loop the API on + // boot — a failure is logged and startup continues so the service serves. + try + { + // Give cafés without a map pin an approximate location from their + // city so the public map lights up. Idempotent (fills nulls). + await BackfillCafeLocationsAsync(db, logger); - // Subscription plans + feature flags are platform config the admin panel - // reads in every environment. Idempotent: adds any rows that are missing - // (so prod, which only had the Free plan, gets Pro/Business/Enterprise). - await SeedPlansAsync(db, logger); - await SeedFeaturesAsync(db, logger); + // Subscription plans + feature flags the admin panel reads in every + // environment. Idempotent: adds any tiers/keys that are missing. + await SeedPlansAsync(db, logger); + await SeedFeaturesAsync(db, logger); + } + catch (Exception ex) + { + logger.LogError(ex, "Non-fatal platform seeding step failed; continuing startup"); + } if (!env.IsDevelopment()) { @@ -425,7 +434,12 @@ public static class PlatformDataSeeder // Tier (not Id) carries the unique constraint, so dedupe on Tier — an // existing Free plan may have a different Id, and inserting another // Free-tier row would violate IX_PlatformPlanDefinitions_Tier. - var existingTiers = (await db.PlatformPlanDefinitions.Select(p => p.Tier).ToListAsync()) + // IgnoreQueryFilters: a SOFT-DELETED plan still occupies its Tier in the + // unique index, so it must be counted or the insert collides on boot. + var existingTiers = (await db.PlatformPlanDefinitions + .IgnoreQueryFilters() + .Select(p => p.Tier) + .ToListAsync()) .ToHashSet(); var missing = plans.Where(p => !existingTiers.Contains(p.Tier)).ToArray(); if (missing.Length == 0) return; @@ -463,7 +477,11 @@ public static class PlatformDataSeeder }; // Key carries the unique constraint, so dedupe on Key (not Id). - var existingKeys = (await db.PlatformFeatures.Select(f => f.Key).ToListAsync()) + // IgnoreQueryFilters so a soft-deleted feature's Key is still counted. + var existingKeys = (await db.PlatformFeatures + .IgnoreQueryFilters() + .Select(f => f.Key) + .ToListAsync()) .ToHashSet(StringComparer.Ordinal); var missing = features.Where(f => !existingKeys.Contains(f.Key)).ToArray(); if (missing.Length == 0) return;