fix(seed): count soft-deleted rows + make platform seeding non-fatal
CI/CD / CI · API (dotnet build + test) (push) Successful in 45s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 33s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m4s
CI/CD / CI · Admin Web (tsc) (push) Successful in 34s
CI/CD / CI · Website (tsc) (push) Successful in 43s
CI/CD / CI · Koja (tsc) (push) Successful in 48s
CI/CD / Deploy · all services (push) Successful in 1m20s
CI/CD / CI · API (dotnet build + test) (push) Successful in 45s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 33s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m4s
CI/CD / CI · Admin Web (tsc) (push) Successful in 34s
CI/CD / CI · Website (tsc) (push) Successful in 43s
CI/CD / CI · Koja (tsc) (push) Successful in 48s
CI/CD / Deploy · all services (push) Successful in 1m20s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -29,15 +29,24 @@ public static class PlatformDataSeeder
|
|||||||
// fresh deploy. Idempotent. Phone is overridable via "Seed:SystemAdminPhone".
|
// fresh deploy. Idempotent. Phone is overridable via "Seed:SystemAdminPhone".
|
||||||
await EnsureOwnerAdminAsync(db, config, logger);
|
await EnsureOwnerAdminAsync(db, config, logger);
|
||||||
|
|
||||||
// Production-safe: give cafés without a map pin an approximate location
|
// Best-effort, NON-FATAL seeding. These steps populate convenience data
|
||||||
// from their city, so the public map lights up. Idempotent (fills nulls).
|
// (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);
|
await BackfillCafeLocationsAsync(db, logger);
|
||||||
|
|
||||||
// Subscription plans + feature flags are platform config the admin panel
|
// Subscription plans + feature flags the admin panel reads in every
|
||||||
// reads in every environment. Idempotent: adds any rows that are missing
|
// environment. Idempotent: adds any tiers/keys that are missing.
|
||||||
// (so prod, which only had the Free plan, gets Pro/Business/Enterprise).
|
|
||||||
await SeedPlansAsync(db, logger);
|
await SeedPlansAsync(db, logger);
|
||||||
await SeedFeaturesAsync(db, logger);
|
await SeedFeaturesAsync(db, logger);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Non-fatal platform seeding step failed; continuing startup");
|
||||||
|
}
|
||||||
|
|
||||||
if (!env.IsDevelopment())
|
if (!env.IsDevelopment())
|
||||||
{
|
{
|
||||||
@@ -425,7 +434,12 @@ public static class PlatformDataSeeder
|
|||||||
// Tier (not Id) carries the unique constraint, so dedupe on Tier — an
|
// Tier (not Id) carries the unique constraint, so dedupe on Tier — an
|
||||||
// existing Free plan may have a different Id, and inserting another
|
// existing Free plan may have a different Id, and inserting another
|
||||||
// Free-tier row would violate IX_PlatformPlanDefinitions_Tier.
|
// 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();
|
.ToHashSet();
|
||||||
var missing = plans.Where(p => !existingTiers.Contains(p.Tier)).ToArray();
|
var missing = plans.Where(p => !existingTiers.Contains(p.Tier)).ToArray();
|
||||||
if (missing.Length == 0) return;
|
if (missing.Length == 0) return;
|
||||||
@@ -463,7 +477,11 @@ public static class PlatformDataSeeder
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Key carries the unique constraint, so dedupe on Key (not Id).
|
// 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);
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
var missing = features.Where(f => !existingKeys.Contains(f.Key)).ToArray();
|
var missing = features.Where(f => !existingKeys.Contains(f.Key)).ToArray();
|
||||||
if (missing.Length == 0) return;
|
if (missing.Length == 0) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user