diff --git a/src/Meezi.Infrastructure/Data/PlatformDataSeeder.cs b/src/Meezi.Infrastructure/Data/PlatformDataSeeder.cs index 78b81da..0500a87 100644 --- a/src/Meezi.Infrastructure/Data/PlatformDataSeeder.cs +++ b/src/Meezi.Infrastructure/Data/PlatformDataSeeder.cs @@ -29,6 +29,10 @@ 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); + if (!env.IsDevelopment()) { // Production: also ensure integration settings (Kavenegar enabled/template, @@ -45,6 +49,79 @@ public static class PlatformDataSeeder await EnsureIntegrationSettingsAsync(db, logger); } + // Approximate centres for the major Iranian cities cafés sign up from. + private static readonly Dictionary CityCentres = new(StringComparer.Ordinal) + { + ["تهران"] = (35.70, 51.39), + ["کرج"] = (35.84, 50.99), + ["مشهد"] = (36.30, 59.61), + ["اصفهان"] = (32.66, 51.67), + ["شیراز"] = (29.59, 52.53), + ["تبریز"] = (38.08, 46.29), + ["قم"] = (34.64, 50.88), + ["اهواز"] = (31.32, 48.67), + ["کرمانشاه"] = (34.31, 47.07), + ["رشت"] = (37.28, 49.58), + ["ارومیه"] = (37.55, 45.07), + ["همدان"] = (34.80, 48.52), + ["یزد"] = (31.90, 54.37), + ["اراک"] = (34.09, 49.69), + ["کرمان"] = (30.28, 57.08), + ["بندرعباس"] = (27.18, 56.27), + ["قزوین"] = (36.28, 50.00), + ["ساری"] = (36.57, 53.06), + ["گرگان"] = (36.84, 54.44), + ["زنجان"] = (36.68, 48.49), + ["کیش"] = (26.56, 53.98), + }; + + /// + /// Gives cafés that have no map pin an approximate location at their city + /// centre (plus a small deterministic per-café offset so multiple cafés in + /// one city don't stack on a single point). Only fills rows where Latitude or + /// Longitude is null and the city is recognised; owners can drop an exact pin + /// later from Settings. Idempotent — never overwrites an existing pin. + /// + private static async Task BackfillCafeLocationsAsync(AppDbContext db, ILogger logger) + { + var cafes = await db.Cafes + .Where(c => c.DeletedAt == null + && (c.Latitude == null || c.Longitude == null) + && c.City != null) + .ToListAsync(); + if (cafes.Count == 0) return; + + var updated = 0; + foreach (var cafe in cafes) + { + var city = cafe.City!.Trim(); + if (!CityCentres.TryGetValue(city, out var centre)) continue; + var (lat, lng) = ScatterAround(cafe.Id, centre.Lat, centre.Lng, 0.05); + cafe.Latitude = lat; + cafe.Longitude = lng; + updated++; + } + + if (updated > 0) + { + await db.SaveChangesAsync(); + logger.LogInformation( + "Cafe location backfill: set approximate coordinates for {Count} café(s) from city centre", updated); + } + } + + private static (double Lat, double Lng) ScatterAround(string id, double lat, double lng, double spread) + { + unchecked + { + var h = 17; + foreach (var ch in id) h = (h * 31) + ch; + var ox = (((h & 0xFFFF) / 65535.0) - 0.5) * 2 * spread; + var oy = ((((h >> 16) & 0xFFFF) / 65535.0) - 0.5) * 2 * spread; + return (Math.Round(lat + oy, 5), Math.Round(lng + ox, 5)); + } + } + /// /// Ensures the platform owner's system-admin account exists in EVERY environment /// (including production), so the admin panel is reachable on a fresh deploy.