Files
meezi/src/Meezi.Infrastructure/Data/PlatformDataSeeder.cs
T

373 lines
16 KiB
C#
Raw Normal View History

2026-05-27 21:33:48 +03:30
using System.Text.Json;
using Meezi.Core.Constants;
using Meezi.Core.Entities;
using Meezi.Core.Enums;
using Meezi.Core.Platform;
using Meezi.Core.Utilities;
2026-05-27 21:33:48 +03:30
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
2026-05-27 21:33:48 +03:30
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Meezi.Infrastructure.Data;
public static class PlatformDataSeeder
{
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
public static async Task SeedAsync(IServiceProvider services)
{
var env = services.GetRequiredService<IHostEnvironment>();
var logger = services.GetRequiredService<ILoggerFactory>().CreateLogger("PlatformDataSeeder");
await using var scope = services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var config = scope.ServiceProvider.GetRequiredService<IConfiguration>();
2026-05-27 21:33:48 +03:30
// Production-safe: ensure the platform owner's system-admin account exists
// on every boot (ALL environments) so the admin panel is reachable on a
// fresh deploy. Idempotent. Phone is overridable via "Seed:SystemAdminPhone".
await EnsureOwnerAdminAsync(db, config, logger);
2026-05-27 21:33:48 +03:30
if (!env.IsDevelopment())
{
// Production: also ensure integration settings (Kavenegar enabled/template,
// etc.) exist so the admin Integrations page is populated. Idempotent.
await EnsureIntegrationSettingsAsync(db, logger);
2026-05-27 21:33:48 +03:30
return;
}
2026-05-27 21:33:48 +03:30
await EnsureCatalogUpgradesAsync(db, logger);
2026-05-27 21:33:48 +03:30
await SeedSystemAdminAsync(db, logger);
await SeedPlansAsync(db, logger);
await SeedFeaturesAsync(db, logger);
await SeedSettingsAsync(db, logger);
await EnsureIntegrationSettingsAsync(db, logger);
}
/// <summary>
/// Ensures the platform owner's system-admin account exists in EVERY environment
/// (including production), so the admin panel is reachable on a fresh deploy.
/// The phone is configurable via "Seed:SystemAdminPhone" (env Seed__SystemAdminPhone)
/// and defaults to the platform owner's number. Idempotent — never duplicates.
/// </summary>
private static async Task EnsureOwnerAdminAsync(AppDbContext db, IConfiguration config, ILogger logger)
{
const string DefaultOwnerPhone = "09190345606";
var configured = config["Seed:SystemAdminPhone"];
var phone = PhoneNormalizer.Normalize(
string.IsNullOrWhiteSpace(configured) ? DefaultOwnerPhone : configured);
if (!PhoneNormalizer.IsValidIranMobile(phone))
{
logger.LogWarning("Owner system-admin seed skipped — invalid phone '{Phone}'", phone);
return;
}
if (await db.SystemAdmins.AnyAsync(a => a.Phone == phone))
return;
db.SystemAdmins.Add(new SystemAdmin
{
Id = "sysadmin_owner",
Name = "مدیر سامانه",
Phone = phone,
IsActive = true
});
try
{
await db.SaveChangesAsync();
logger.LogInformation("Seeded owner system admin with phone {Phone}", phone);
}
catch (DbUpdateException)
{
// api + admin-api boot concurrently against the same DB; another instance
// already inserted this admin. Safe to ignore.
logger.LogInformation("Owner system admin already seeded by another instance");
}
}
2026-05-27 21:33:48 +03:30
/// <summary>Idempotent plan/feature upgrades for all environments (including production).</summary>
public static async Task EnsureCatalogUpgradesAsync(IServiceProvider services)
{
await using var scope = services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("PlatformDataSeeder");
await EnsureCatalogUpgradesAsync(db, logger);
}
private static async Task EnsureCatalogUpgradesAsync(AppDbContext db, ILogger logger)
{
var featureAdds = new[]
{
("menu_3d", "منوی سه‌بعدی", "3D menu", "growth"),
("menu_3d_ai", "تولید ۳D با هوش مصنوعی", "AI 3D menu", "growth"),
("discover_profile", "پروفایل کشف", "Discover profile", "growth")
};
var existingKeys = await db.PlatformFeatures.Select(f => f.Key).ToListAsync();
var newFeatures = featureAdds
.Where(f => !existingKeys.Contains(f.Item1))
.Select(f => F(f.Item1, f.Item2, f.Item3, f.Item4))
.ToList();
if (newFeatures.Count > 0)
{
db.PlatformFeatures.AddRange(newFeatures);
await db.SaveChangesAsync();
logger.LogInformation("Platform upgrade: added {Count} features", newFeatures.Count);
}
var plans = await db.PlatformPlanDefinitions.ToListAsync();
var changed = 0;
foreach (var plan in plans)
{
if (plan.Tier is PlanTier.Free or PlanTier.Enterprise)
continue;
var keys = plan.Tier == PlanTier.Business || plan.Tier == PlanTier.Enterprise
? new[] { "menu_3d", "menu_3d_ai", "discover_profile" }
: new[] { "menu_3d", "discover_profile" };
var merged = MergeFeaturesJson(plan.FeaturesJson ?? "[]", keys);
if (merged == plan.FeaturesJson) continue;
plan.FeaturesJson = merged;
changed++;
}
if (changed > 0)
{
await db.SaveChangesAsync();
logger.LogInformation("Platform upgrade: updated features on {Count} plans", changed);
}
await EnsureIntegrationSettingsAsync(db, logger);
}
private static string MergeFeaturesJson(string json, params string[] keys)
{
var list = JsonSerializer.Deserialize<List<string>>(json, JsonOpts) ?? [];
if (list.Contains("*"))
return json;
var updated = false;
foreach (var key in keys)
{
if (!list.Contains(key))
{
list.Add(key);
updated = true;
}
}
return updated ? JsonSerializer.Serialize(list, JsonOpts) : json;
}
private static async Task EnsureIntegrationSettingsAsync(AppDbContext db, ILogger logger)
{
var defaults = new[]
{
S("payment.activeGateway", "zarinpal", "payment", "درگاه پیش‌فرض اشتراک"),
S("payment.zarinpal.enabled", "true", "payment", "فعال زرین‌پال"),
S("payment.zarinpal.sandbox", "true", "payment", "حالت تست زرین‌پال"),
S("payment.tara.enabled", "false", "payment", "فعال تارا"),
S("payment.tara.sandbox", "true", "payment", "حالت تست تارا"),
S("payment.snapppay.enabled", "false", "payment", "فعال اسنپ‌پی"),
S("payment.snapppay.sandbox", "true", "payment", "حالت تست اسنپ‌پی"),
S("payment.nextpay.enabled", "false", "payment", "فعال نکست‌پی"),
S("payment.nextpay.sandbox", "true", "payment", "حالت تست نکست‌پی"),
S("payment.vandar.enabled", "false", "payment", "فعال وندار"),
S("payment.vandar.sandbox", "true", "payment", "حالت تست وندار"),
S("integrations.kavenegar.enabled", "true", "integrations", "فعال کاوه‌نگار"),
S("integrations.kavenegar.otpTemplate", "meeziotp", "integrations", "قالب OTP"),
2026-05-27 21:33:48 +03:30
S("integrations.openai.enabled", "false", "integrations", "فعال OpenAI"),
S("integrations.openai.model", "gpt-4o-mini", "integrations", "مدل OpenAI"),
S("integrations.openai.coffeeAdvisor.enabled", "true", "integrations", "مشاور قهوه"),
S("integrations.meshy.enabled", "false", "integrations", "فعال Meshy"),
S("integrations.meshy.menu3d.enabled", "true", "integrations", "ساخت ۳D منو")
};
var existing = await db.PlatformSettings.Select(s => s.Key).ToListAsync();
var missing = defaults.Where(d => !existing.Contains(d.Key)).ToList();
if (missing.Count == 0) return;
db.PlatformSettings.AddRange(missing);
await db.SaveChangesAsync();
logger.LogInformation("Platform seed: added {Count} integration settings", missing.Count);
}
private static async Task SeedSystemAdminAsync(AppDbContext db, ILogger logger)
{
const string phone = "09120000001";
if (await db.SystemAdmins.AnyAsync(a => a.Phone == phone))
return;
db.SystemAdmins.Add(new SystemAdmin
{
Id = "sysadmin_demo",
Name = "مدیر سامانه",
Phone = phone,
IsActive = true
});
await db.SaveChangesAsync();
logger.LogInformation("Platform seed: system admin phone {Phone}", phone);
}
private static async Task SeedPlansAsync(AppDbContext db, ILogger logger)
{
if (await db.PlatformPlanDefinitions.AnyAsync())
return;
var plans = new[]
{
new PlatformPlanDefinition
{
Id = "plan_free",
Tier = PlanTier.Free,
DisplayNameFa = "رایگان",
DisplayNameEn = "Free",
MonthlyPriceToman = 0,
IsBillableOnline = false,
SortOrder = 0,
LimitsJson = JsonSerializer.Serialize(PlanLimitsData.ForTier(PlanTier.Free), JsonOpts),
FeaturesJson = JsonSerializer.Serialize(new[] { "pos", "menu", "tables", "qr_menu" }, JsonOpts)
},
new PlatformPlanDefinition
{
Id = "plan_pro",
Tier = PlanTier.Pro,
DisplayNameFa = "حرفه‌ای",
DisplayNameEn = "Pro",
MonthlyPriceToman = PlanPricing.MonthlyToman(PlanTier.Pro),
IsBillableOnline = true,
SortOrder = 1,
LimitsJson = JsonSerializer.Serialize(PlanLimitsData.ForTier(PlanTier.Pro), JsonOpts),
FeaturesJson = JsonSerializer.Serialize(new[]
{
"pos", "menu", "tables", "qr_menu", "crm", "coupons", "reports", "kds", "inventory",
"menu_3d", "discover_profile"
}, JsonOpts)
},
new PlatformPlanDefinition
{
Id = "plan_business",
Tier = PlanTier.Business,
DisplayNameFa = "کسب‌وکار",
DisplayNameEn = "Business",
MonthlyPriceToman = PlanPricing.MonthlyToman(PlanTier.Business),
IsBillableOnline = true,
SortOrder = 2,
LimitsJson = JsonSerializer.Serialize(PlanLimitsData.ForTier(PlanTier.Business), JsonOpts),
FeaturesJson = JsonSerializer.Serialize(new[]
{
"pos", "menu", "tables", "qr_menu", "crm", "coupons", "reports", "kds", "inventory",
"hr", "sms", "reservations", "delivery", "expenses", "branches",
"menu_3d", "menu_3d_ai", "discover_profile"
}, JsonOpts)
},
new PlatformPlanDefinition
{
Id = "plan_enterprise",
Tier = PlanTier.Enterprise,
DisplayNameFa = "سازمانی",
DisplayNameEn = "Enterprise",
MonthlyPriceToman = 0,
IsBillableOnline = false,
SortOrder = 3,
LimitsJson = JsonSerializer.Serialize(PlanLimitsData.ForTier(PlanTier.Enterprise), JsonOpts),
FeaturesJson = JsonSerializer.Serialize(new[] { "*" }, JsonOpts)
}
};
db.PlatformPlanDefinitions.AddRange(plans);
await db.SaveChangesAsync();
logger.LogInformation("Platform seed: {Count} subscription plans", plans.Length);
}
private static async Task SeedFeaturesAsync(AppDbContext db, ILogger logger)
{
if (await db.PlatformFeatures.AnyAsync())
return;
var features = new[]
{
F("pos", "صندوق", "POS", "core"),
F("menu", "منو", "Menu", "core"),
F("tables", "میزها", "Tables", "core"),
F("qr_menu", "منوی QR", "QR menu", "core"),
F("kds", "آشپزخانه", "KDS", "operations"),
F("crm", "مشتریان", "CRM", "growth"),
F("coupons", "کوپن", "Coupons", "growth"),
F("reports", "گزارش‌ها", "Reports", "analytics"),
F("inventory", "انبار", "Inventory", "operations"),
F("hr", "منابع انسانی", "HR", "operations"),
F("sms", "پیامک", "SMS", "growth"),
F("reservations", "رزرو", "Reservations", "growth"),
F("delivery", "پذیرش آنلاین", "Delivery", "integrations"),
F("expenses", "هزینه‌ها", "Expenses", "analytics"),
F("branches", "چند شعبه", "Branches", "core"),
F("taxes", "مالیات", "Taxes", "compliance"),
F("reviews", "نظرات", "Reviews", "growth"),
F("queue", "صف", "Queue", "operations"),
F("menu_3d", "منوی سه‌بعدی", "3D menu", "growth"),
F("menu_3d_ai", "تولید ۳D با هوش مصنوعی", "AI 3D menu", "growth"),
F("discover_profile", "پروفایل کشف", "Discover profile", "growth")
};
db.PlatformFeatures.AddRange(features);
await db.SaveChangesAsync();
logger.LogInformation("Platform seed: {Count} feature flags", features.Length);
}
private static PlatformFeature F(string key, string fa, string en, string group) => new()
{
Id = $"feat_{key}",
Key = key,
DisplayNameFa = fa,
DisplayNameEn = en,
ModuleGroup = group,
IsEnabledGlobally = true
};
private static async Task SeedSettingsAsync(AppDbContext db, ILogger logger)
{
if (await db.PlatformSettings.AnyAsync())
return;
var settings = new[]
{
S("app.name", "میزی", "branding", "نام اپلیکیشن"),
S("app.tagline", "میزت منتظرته", "branding", "شعار"),
S("auth.maxOtpPerHour", "5", "auth", "حداکثر OTP در ساعت"),
S("billing.zarinpalSandbox", "true", "billing", "درگاه تست زرین‌پال"),
S("support.autoCloseDays", "14", "support", "بستن خودکار تیکت پس از روز"),
S("payment.activeGateway", "zarinpal", "payment", "درگاه فعال اشتراک"),
S("payment.zarinpal.enabled", "true", "payment", "فعال زرین‌پال"),
S("payment.zarinpal.sandbox", "true", "payment", "حالت تست زرین‌پال"),
S("payment.nextpay.enabled", "false", "payment", "فعال نکست‌پی"),
S("payment.nextpay.sandbox", "true", "payment", "حالت تست نکست‌پی"),
S("payment.vandar.enabled", "false", "payment", "فعال وندار"),
S("payment.vandar.sandbox", "true", "payment", "حالت تست وندار"),
S("integrations.kavenegar.enabled", "true", "integrations", "فعال کاوه‌نگار"),
S("integrations.kavenegar.otpTemplate", "meeziotp", "integrations", "قالب OTP"),
2026-05-27 21:33:48 +03:30
S("integrations.openai.enabled", "false", "integrations", "فعال OpenAI"),
S("integrations.openai.model", "gpt-4o-mini", "integrations", "مدل OpenAI"),
S("integrations.openai.coffeeAdvisor.enabled", "true", "integrations", "مشاور قهوه"),
S("integrations.meshy.enabled", "false", "integrations", "فعال Meshy"),
S("integrations.meshy.menu3d.enabled", "true", "integrations", "ساخت ۳D منو")
};
db.PlatformSettings.AddRange(settings);
await db.SaveChangesAsync();
logger.LogInformation("Platform seed: {Count} platform settings", settings.Length);
}
private static PlatformSetting S(string key, string value, string category, string desc) => new()
{
Id = $"cfg_{key.Replace('.', '_')}",
Key = key,
Value = value,
Category = category,
DescriptionFa = desc
};
}