using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using ROLAC.API.Authorization; using ROLAC.API.Entities; namespace ROLAC.API.Data; public static class DbSeeder { private static readonly (string En, string Zh, int Sort)[] GivingCategorySeed = [ ("Tithe", "什一奉獻", 1), ("General Offering", "一般奉獻", 2), ("Special Offering", "特別奉獻", 3), ("Building Fund", "建堂基金", 4), ("Mission", "宣教奉獻", 5), ]; private static readonly (string En, string Zh, int Sort)[] MinistrySeed = [ ("Administration", "行政", 1), ("Preaching", "講道", 2), ("Emcee", "司會", 3), ("Worship", "敬拜", 4), ("PPT/Media", "PPT/影音", 5), ("Sound", "音控", 6), ("Facility", "場地組", 7), ("Hospitality", "招待", 8), ("Children", "兒牧", 9), ("Catering", "餐飲", 10), ]; // (GroupEn, GroupZh, Sort, SubItems[(SubEn, SubZh)]) private static readonly (string En, string Zh, int Sort, (string En, string Zh)[] Subs)[] ExpenseCategorySeed = [ ("Equipment", "設備", 1, [("Purchase","購置"),("Rental","租借"),("Maintenance & Repair","維修")]), ("Consumables", "消耗品", 2, [("Batteries","電池"),("Accessories","配件"),("Cleaning Supplies","清潔用品"),("Office Supplies","文具")]), ("Food & Beverage", "餐飲", 3, [("Catering","出餐費用"),("Food Ingredients","食材採購"),("Utensils","器具"),("Consumables","消耗品")]), ("Training", "培訓", 4, [("Course Fees","課程費用"),("Books","書籍"),("Conference","研討會"),("Travel","差旅")]), ("Materials", "教材", 5, [("Printing","印刷費用"),("Craft Supplies","手工材料"),("Copyright & Licensing","版權購買")]), ("Facility", "場地", 6, [("Rent","場地租金"),("Utilities","水電"),("Property Insurance","財產保險"),("Decoration","裝飾")]), ("Printing", "印刷", 7, [("Bulletins","週報"),("Order of Service","程序單"),("Posters","海報")]), ("Missions", "宣教", 8, [("Offering Transfer","奉獻轉帳"),("Missionary Support","宣教士支援"),("Travel","差旅")]), ("Benevolence", "關懷救助", 9, [("Emergency Aid","急難救助"),("Condolence Gifts","慰問禮品"),("Visit Expenses","探訪費用")]), ("Other", "其他", 10, [("Miscellaneous","雜支")]), ("Personnel", "人事", 11, [("Salary & Wages","薪資"),("Payroll Taxes","薪資稅費"),("Employee Benefits","員工福利"),("Workers Compensation","勞工保險"),("Honorarium","酬庸"),("Staff Training","同工進修"),("Contract Labor","外包勞務")]), ]; private static readonly (string Name, string Description)[] Roles = [ ("super_admin", "System administrator — full access"), ("pastor", "Pastor — full member and financial overview"), ("board_member", "Board member — church governance"), ("coworker_chair", "Coworker chair — coordinates ministry leaders"), ("ministry_leader", "Ministry leader — scoped to own ministry"), ("district_leader", "District leader — manages multiple cell groups"), ("cell_leader", "Cell leader — scoped to own cell group"), ("coworker", "Coworker — general worker in assigned ministry"), ("finance", "Finance — manages giving and expense reports"), ("secretary", "Secretary — manages member data and scheduling"), ("worship_leader", "Worship leader — manages song library and setlists (Phase deferred)"), ("member", "Member — views own profile and service roster"), ("visitor", "Visitor — public pages only"), ]; // Default permission matrix — mirrors the hard-coded [Authorize(Roles=...)] rules that // existed before the configurable RBAC system, so day-one behavior is unchanged. // super_admin is intentionally absent: it bypasses all checks (see PermissionAuthorizationHandler). // R=Read, W=Write, D=Delete, A=Approve. Rows are inserted only if missing, so an admin's // later edits via the Permissions UI are never clobbered on restart. private static readonly (string Role, string Module, bool R, bool W, bool D, bool A)[] RolePermissionSeed = [ // Secretary — manages member data. ("secretary", Modules.Members, true, true, true, false), // Pastor — read-only overview of members and all expenses. ("pastor", Modules.Members, true, false, false, false), ("pastor", Modules.Expenses, true, false, false, false), // Finance — full control over the finance modules. ("finance", Modules.Givings, true, true, true, false), ("finance", Modules.GivingCategories, true, true, true, false), ("finance", Modules.Expenses, true, true, true, true), ("finance", Modules.ExpenseCategories, true, true, true, false), ("finance", Modules.OfferingSessions, true, true, true, true), ("finance", Modules.FinanceDashboard, true, false, false, false), ("finance", Modules.MonthlyStatements, true, true, false, true), ("finance", Modules.ChurchProfile, true, true, false, false), ("finance", Modules.Disbursements, true, true, true, true), ]; public static async Task SeedRolePermissionsAsync(AppDbContext db) { var rolesByName = await db.Roles .Where(r => r.Name != null) .ToDictionaryAsync(r => r.Name!, r => r.Id); foreach (var (role, module, read, write, delete, approve) in RolePermissionSeed) { if (!rolesByName.TryGetValue(role, out var roleId)) continue; var exists = await db.RolePermissions.AnyAsync(p => p.RoleId == roleId && p.Module == module); if (exists) continue; // never clobber an admin's edit db.RolePermissions.Add(new RolePermission { RoleId = roleId, Module = module, CanRead = read, CanWrite = write, CanDelete = delete, CanApprove = approve, }); } await db.SaveChangesAsync(); } public static async Task SeedRolesAsync(RoleManager roleManager) { foreach (var (name, description) in Roles) { if (!await roleManager.RoleExistsAsync(name)) { await roleManager.CreateAsync(new AppRole { Name = name, Description = description, }); } } } public static async Task SeedGivingCategoriesAsync(AppDbContext db) { foreach (var (en, zh, sort) in GivingCategorySeed) { if (!await db.GivingCategories.AnyAsync(c => c.Name_en == en)) { db.GivingCategories.Add(new GivingCategory { Name_en = en, Name_zh = zh, SortOrder = sort, IsActive = true, // Audit fields are stamped by AuditSaveChangesInterceptor on save. }); } } await db.SaveChangesAsync(); } public static async Task SeedMinistriesAsync(AppDbContext db) { foreach (var (en, zh, sort) in MinistrySeed) { if (!await db.Ministries.AnyAsync(m => m.Name_en == en)) db.Ministries.Add(new Ministry { Name_en = en, Name_zh = zh, SortOrder = sort, IsActive = true }); } await db.SaveChangesAsync(); } public static async Task SeedExpenseCategoriesAsync(AppDbContext db) { foreach (var (gEn, gZh, gSort, subs) in ExpenseCategorySeed) { var group = await db.ExpenseCategoryGroups.FirstOrDefaultAsync(g => g.Name_en == gEn); if (group is null) { group = new ExpenseCategoryGroup { Name_en = gEn, Name_zh = gZh, SortOrder = gSort, IsActive = true }; db.ExpenseCategoryGroups.Add(group); await db.SaveChangesAsync(); // assign group.Id } var sub = 1; foreach (var (sEn, sZh) in subs) { if (!await db.ExpenseSubCategories.AnyAsync(s => s.GroupId == group.Id && s.Name_en == sEn)) db.ExpenseSubCategories.Add(new ExpenseSubCategory { GroupId = group.Id, Name_en = sEn, Name_zh = sZh, SortOrder = sub, IsActive = true }); sub++; } } await db.SaveChangesAsync(); } public static async Task SeedChurchProfileAsync(AppDbContext db) { // Singleton row used by the disbursement module (issuer info + check counter). if (!await db.ChurchProfiles.AnyAsync()) { db.ChurchProfiles.Add(new ChurchProfile { Name = "River Of Life Christian Church", City = "Arcadia", State = "CA", NextCheckNumber = 1001, }); await db.SaveChangesAsync(); } } /// /// Seeds roles and (in Development) the default admin account. /// Called once on application startup after migrations have been applied. /// public static async Task SeedAsync(IServiceProvider services) { var roleManager = services.GetRequiredService>(); var userManager = services.GetRequiredService>(); var env = services.GetRequiredService(); await SeedRolesAsync(roleManager); var db = services.GetRequiredService(); await SeedRolePermissionsAsync(db); await SeedGivingCategoriesAsync(db); await SeedMinistriesAsync(db); await SeedExpenseCategoriesAsync(db); await SeedChurchProfileAsync(db); if (env.IsDevelopment()) await SeedAdminUserAsync(userManager); } /// /// Creates a super_admin test account for local development. /// DO NOT call this in production — remove or guard with IsDevelopment(). /// Credentials: admin@rolac.org / Admin1234! /// public static async Task SeedAdminUserAsync(UserManager userManager) { const string adminEmail = "admin@rolac.org"; const string adminPassword = "Admin1234!"; if (await userManager.FindByEmailAsync(adminEmail) is null) { var admin = new AppUser { UserName = adminEmail, Email = adminEmail, EmailConfirmed = true, IsActive = true, LanguagePreference = "en", CreatedAt = DateTime.UtcNow, }; var result = await userManager.CreateAsync(admin, adminPassword); if (result.Succeeded) await userManager.AddToRoleAsync(admin, "super_admin"); } } }