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;
|
2026-05-31 19:58:54 +03:30
|
|
|
using Meezi.Core.Utilities;
|
2026-05-27 21:33:48 +03:30
|
|
|
using Microsoft.EntityFrameworkCore;
|
2026-05-31 19:58:54 +03:30
|
|
|
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>();
|
2026-05-31 19:58:54 +03:30
|
|
|
var config = scope.ServiceProvider.GetRequiredService<IConfiguration>();
|
2026-05-27 21:33:48 +03:30
|
|
|
|
2026-05-31 19:58:54 +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())
|
2026-05-31 19:58:54 +03:30
|
|
|
{
|
|
|
|
|
// 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-31 19:58:54 +03:30
|
|
|
}
|
2026-05-27 21:33:48 +03:30
|
|
|
|
2026-05-31 19:58:54 +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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:58:54 +03:30
|
|
|
/// <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";
|
2026-05-31 20:07:33 +03:30
|
|
|
const string DefaultAdminUsername = "admin";
|
|
|
|
|
|
|
|
|
|
var configuredPhone = config["Seed:SystemAdminPhone"];
|
2026-05-31 19:58:54 +03:30
|
|
|
var phone = PhoneNormalizer.Normalize(
|
2026-05-31 20:07:33 +03:30
|
|
|
string.IsNullOrWhiteSpace(configuredPhone) ? DefaultOwnerPhone : configuredPhone);
|
2026-05-31 19:58:54 +03:30
|
|
|
|
|
|
|
|
if (!PhoneNormalizer.IsValidIranMobile(phone))
|
|
|
|
|
{
|
|
|
|
|
logger.LogWarning("Owner system-admin seed skipped — invalid phone '{Phone}'", phone);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 20:07:33 +03:30
|
|
|
var configuredUsername = config["Seed:SystemAdminUsername"];
|
|
|
|
|
var username = string.IsNullOrWhiteSpace(configuredUsername) ? DefaultAdminUsername : configuredUsername.Trim().ToLowerInvariant();
|
|
|
|
|
var defaultPassword = config["Seed:SystemAdminPassword"]; // optional — only set if provided
|
|
|
|
|
|
|
|
|
|
var existing = await db.SystemAdmins.FirstOrDefaultAsync(a => a.Phone == phone);
|
2026-05-31 19:58:54 +03:30
|
|
|
|
2026-05-31 20:07:33 +03:30
|
|
|
if (existing is null)
|
2026-05-31 19:58:54 +03:30
|
|
|
{
|
2026-05-31 20:07:33 +03:30
|
|
|
var admin = new SystemAdmin
|
|
|
|
|
{
|
|
|
|
|
Id = "sysadmin_owner",
|
|
|
|
|
Name = "مدیر سامانه",
|
|
|
|
|
Phone = phone,
|
|
|
|
|
IsActive = true,
|
|
|
|
|
Username = username,
|
|
|
|
|
PasswordHash = string.IsNullOrWhiteSpace(defaultPassword) ? null : PasswordHasher.Hash(defaultPassword)
|
|
|
|
|
};
|
|
|
|
|
db.SystemAdmins.Add(admin);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
|
logger.LogInformation("Seeded owner system admin with phone {Phone}, username '{Username}'", phone, username);
|
|
|
|
|
}
|
|
|
|
|
catch (DbUpdateException)
|
|
|
|
|
{
|
|
|
|
|
logger.LogInformation("Owner system admin already seeded by another instance");
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-31 19:58:54 +03:30
|
|
|
|
2026-05-31 20:07:33 +03:30
|
|
|
// Patch existing admin: fill in missing username / password without overwriting set values
|
|
|
|
|
var patched = false;
|
|
|
|
|
if (string.IsNullOrWhiteSpace(existing.Username))
|
2026-05-31 19:58:54 +03:30
|
|
|
{
|
2026-05-31 20:07:33 +03:30
|
|
|
existing.Username = username;
|
|
|
|
|
patched = true;
|
2026-05-31 19:58:54 +03:30
|
|
|
}
|
2026-05-31 20:07:33 +03:30
|
|
|
if (string.IsNullOrWhiteSpace(existing.PasswordHash) && !string.IsNullOrWhiteSpace(defaultPassword))
|
2026-05-31 19:58:54 +03:30
|
|
|
{
|
2026-05-31 20:07:33 +03:30
|
|
|
existing.PasswordHash = PasswordHasher.Hash(defaultPassword);
|
|
|
|
|
patched = true;
|
|
|
|
|
}
|
|
|
|
|
if (patched)
|
|
|
|
|
{
|
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
|
logger.LogInformation("Patched owner system admin credentials (username/password)");
|
2026-05-31 19:58:54 +03:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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", "فعال کاوهنگار"),
|
2026-05-31 19:58:54 +03:30
|
|
|
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", "فعال کاوهنگار"),
|
2026-05-31 19:58:54 +03:30
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
}
|