From 09bba5f8cda972a992c42430fa46319d4e7a917a Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Tue, 2 Jun 2026 02:26:11 +0330 Subject: [PATCH] fix(seed): count soft-deleted rows + make platform seeding non-fatal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the crash-loop: a soft-deleted Free plan still occupies its Tier in the unique index, but the existing-row check queried THROUGH the soft-delete global filter and missed it, so the seeder re-inserted Free and violated IX_PlatformPlanDefinitions_Tier on boot. Fixes: (1) IgnoreQueryFilters() on the plan/feature existing-checks so soft-deleted tiers/keys are counted; (2) wrap plan/feature/location seeding in try/catch so any seeding failure logs and startup continues — non-essential seeding must never crash-loop the API. Co-Authored-By: Claude Opus 4.8 --- .../Data/PlatformDataSeeder.cs | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) 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;