ad276c01f3
- DB_SCHEMA.md §8: add Form1099Box catalog table, Payee1099 recipient master (with TIN at-rest encryption note), and new FK columns on Expenses / ExpenseSubCategories / ExpenseCategoryGroups; update TOC and Seed Data section - DbSeeder.cs: grant Modules.Form1099 to finance (R/W/D), pastor (R), and board_member (R), mirroring the Form990Report + Disbursements pattern; idempotent (only inserts if row absent, never clobbers admin edits) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
535 lines
28 KiB
C#
535 lines
28 KiB
C#
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),
|
|
("Cell Groups", "小組牧養", 11),
|
|
("Special Events", "特別活動", 12),
|
|
];
|
|
|
|
// (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","器具"),("Disposable Tableware","一次性餐具")]),
|
|
("Training", "培訓", 4, [("Course Fees","課程費用"),("Books","書籍"),("Conference","研討會"),("Travel","差旅")]),
|
|
("Materials", "教材", 5, [("Curriculum Printing","教材印刷"),("Craft Supplies","手工材料"),("Copyright & Licensing","版權購買")]),
|
|
("Facility", "場地", 6, [("Rent","場地租金"),("Utilities","水電"),("Property Insurance","財產保險"),("Decoration","裝飾"),("Repairs & Maintenance","修繕維護")]),
|
|
("Printing", "印刷", 7, [("Bulletins","週報"),("Order of Service","程序單"),("Posters","海報"),("Advertising & Promotion","廣告推廣")]),
|
|
("Missions", "宣教", 8, [("Offering Transfer","奉獻轉帳"),("Missionary Support","宣教士支援"),("Foreign Missions Support","國外宣教支援"),("Travel","差旅")]),
|
|
("Benevolence", "關懷救助", 9, [("Emergency Aid","急難救助"),("Condolence Gifts","慰問禮品"),("Visit Expenses","探訪費用")]),
|
|
("Other", "其他", 10, [("Miscellaneous","雜支"),("Gifts","禮品")]),
|
|
("Personnel", "人事", 11, [("Officer / Key Employee Compensation","主要職員薪酬"),("Salary & Wages","薪資"),("Payroll Taxes","薪資稅費"),("Employee Benefits","員工福利"),("Retirement / Pension","退休金"),("Workers Compensation","勞工保險"),("Honorarium","酬庸"),("Staff Training","同工進修"),("Contract Labor","外包勞務")]),
|
|
("Professional Services", "專業服務", 12, [("Legal","法律服務"),("Accounting & Audit","會計與審計"),("Other Professional","其他專業服務")]),
|
|
("Information Technology", "資訊科技", 13, [("Software & Subscriptions","軟體與訂閱"),("Website & Hosting","網站與主機"),("Internet & Telecom","網路與電信")]),
|
|
("Finance & Banking", "財務與銀行", 14, [("Interest","利息支出"),("Bank & Processing Fees","銀行/金流手續費")]),
|
|
];
|
|
|
|
// (LineCode, Name_en, Name_zh, Sort)
|
|
private static readonly (string Code, string En, string Zh, int Sort)[] Form990LineSeed =
|
|
[
|
|
("1", "Grants to domestic organizations", "對國內機構之捐贈", 1),
|
|
("2", "Grants to domestic individuals", "對國內個人之捐贈", 2),
|
|
("3", "Grants to foreign organizations/individuals", "對國外之捐贈", 3),
|
|
("5", "Compensation of current officers / key employees", "主要職員/負責人薪酬", 4),
|
|
("7", "Other salaries and wages", "薪資", 5),
|
|
("8", "Pension plan accruals and contributions", "退休金提撥", 6),
|
|
("9", "Other employee benefits", "員工福利", 7),
|
|
("10", "Payroll taxes", "薪資稅", 8),
|
|
("11b", "Legal fees", "法律服務費", 9),
|
|
("11c", "Accounting fees", "會計與審計費", 10),
|
|
("11g", "Other fees for services (non-employee)", "其他勞務報酬(非員工)", 11),
|
|
("12", "Advertising and promotion", "廣告與推廣", 12),
|
|
("13", "Office expenses", "辦公費用", 13),
|
|
("14", "Information technology", "資訊科技", 14),
|
|
("16", "Occupancy", "場地佔用", 15),
|
|
("17", "Travel", "差旅", 16),
|
|
("19", "Conferences, conventions, and meetings", "會議與研習", 17),
|
|
("20", "Interest", "利息", 18),
|
|
("22", "Depreciation", "折舊", 19),
|
|
("23", "Insurance", "保險", 20),
|
|
("24", "Other expenses", "其他費用", 21),
|
|
];
|
|
|
|
// (GroupEn, SubEn, LineCode) — default natural-category → 990 line mapping.
|
|
private static readonly (string GroupEn, string SubEn, string Code)[] Form990SubMappingSeed =
|
|
[
|
|
("Personnel", "Officer / Key Employee Compensation", "5"),
|
|
("Personnel", "Salary & Wages", "7"),
|
|
("Personnel", "Payroll Taxes", "10"),
|
|
("Personnel", "Employee Benefits", "9"),
|
|
("Personnel", "Retirement / Pension","8"),
|
|
("Personnel", "Workers Compensation","9"),
|
|
("Personnel", "Honorarium", "11g"),
|
|
("Personnel", "Contract Labor", "11g"),
|
|
("Personnel", "Staff Training", "19"),
|
|
("Facility", "Rent", "16"),
|
|
("Facility", "Utilities", "16"),
|
|
("Facility", "Property Insurance", "23"),
|
|
("Facility", "Decoration", "24"),
|
|
// Building repairs & maintenance (plumbing, electrical, painting) are part of Occupancy.
|
|
("Facility", "Repairs & Maintenance", "16"),
|
|
("Training", "Course Fees", "19"),
|
|
("Training", "Conference", "19"),
|
|
("Training", "Books", "24"),
|
|
("Training", "Travel", "17"),
|
|
("Missions", "Travel", "17"),
|
|
// Domestic missions support is paid to individual missionaries/families → line 2 (grants to individuals).
|
|
("Missions", "Offering Transfer", "2"),
|
|
("Missions", "Missionary Support", "2"),
|
|
("Missions", "Foreign Missions Support", "3"),
|
|
("Benevolence", "Emergency Aid", "2"),
|
|
("Benevolence", "Condolence Gifts", "2"),
|
|
// Visitation is the church's own travel/program cost, not a grant to an individual.
|
|
("Benevolence", "Visit Expenses", "17"),
|
|
("Consumables", "Office Supplies", "13"),
|
|
// General supplies belong with office expenses (line 13), not the "Other" catch-all.
|
|
("Consumables", "Batteries", "13"),
|
|
("Consumables", "Accessories", "13"),
|
|
("Consumables", "Cleaning Supplies", "13"),
|
|
// IRS line 13 covers equipment rental and maintenance.
|
|
("Equipment", "Rental", "13"),
|
|
("Equipment", "Maintenance & Repair", "13"),
|
|
("Printing", "Bulletins", "13"),
|
|
("Printing", "Order of Service", "13"),
|
|
("Printing", "Posters", "12"),
|
|
("Printing", "Advertising & Promotion", "12"),
|
|
("Materials", "Curriculum Printing", "13"),
|
|
// Classroom/craft supplies fall under IRS line 13 office expenses ("supplies… classroom…").
|
|
("Materials", "Craft Supplies", "13"),
|
|
("Professional Services", "Legal", "11b"),
|
|
("Professional Services", "Accounting & Audit", "11c"),
|
|
("Professional Services", "Other Professional", "11g"),
|
|
("Information Technology", "Software & Subscriptions", "14"),
|
|
("Information Technology", "Website & Hosting", "14"),
|
|
("Information Technology", "Internet & Telecom", "14"),
|
|
("Finance & Banking", "Interest", "20"),
|
|
// Bank/processing fees are office expenses per IRS line 13 (consistent with Interest → 20).
|
|
("Finance & Banking", "Bank & Processing Fees", "13"),
|
|
// Appreciation/outreach gifts have no natural 990 line; mapped to 24 explicitly so this
|
|
// deliberate "Other" choice doesn't inflate UnmappedExpenseCount. (Benevolence gifts → line 2.)
|
|
("Other", "Gifts", "24"),
|
|
];
|
|
|
|
private static readonly (string Code, string En, string Zh, string FormType, int Sort)[] Form1099BoxSeed =
|
|
[
|
|
(Form1099.BoxNec1, "Nonemployee compensation", "非員工報酬", "1099-NEC", 1),
|
|
(Form1099.BoxMisc1, "Rents", "租金", "1099-MISC", 2),
|
|
];
|
|
|
|
// Only service/rent subcategories get a box. Everything else stays unmapped (not reportable).
|
|
private static readonly (string GroupEn, string SubEn, string Code)[] Form1099SubMappingSeed =
|
|
[
|
|
("Personnel", "Honorarium", Form1099.BoxNec1),
|
|
("Personnel", "Contract Labor", Form1099.BoxNec1),
|
|
("Professional Services", "Legal", Form1099.BoxNec1),
|
|
("Professional Services", "Accounting & Audit", Form1099.BoxNec1),
|
|
("Professional Services", "Other Professional", Form1099.BoxNec1),
|
|
("Facility", "Rent", Form1099.BoxMisc1),
|
|
];
|
|
|
|
// One-time corrections for subcategories that were mapped to the WRONG line in an earlier
|
|
// seed. The normal mapping loop below only fills NULLs, so it cannot fix an existing bad
|
|
// value — this block does. Idempotent: each row fires only while the subcategory still holds
|
|
// the OLD line, so it never clobbers a deliberate admin re-mapping. (GroupEn, SubEn, Old, New)
|
|
private static readonly (string GroupEn, string SubEn, string OldCode, string NewCode)[] Form990RemapSeed =
|
|
[
|
|
("Benevolence", "Visit Expenses", "2", "17"),
|
|
("Missions", "Missionary Support", "1", "2"),
|
|
("Missions", "Offering Transfer", "1", "2"),
|
|
];
|
|
|
|
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),
|
|
("finance", Modules.Form990Report, true, false, false, false),
|
|
// Form1099 — finance manages recipients and tracks filings; pastor and board_member
|
|
// get read-only oversight (same pattern as Form990Report). No Approve semantics.
|
|
("finance", Modules.Form1099, true, true, true, false),
|
|
("pastor", Modules.Form1099, true, false, false, false),
|
|
("board_member", Modules.Form1099, true, false, false, false),
|
|
|
|
// Logs — read-only. System logs are technical (pastor only); audit logs have
|
|
// governance value, so finance and board members can read them too.
|
|
("pastor", Modules.SystemLogs, true, false, false, false),
|
|
("pastor", Modules.AuditLogs, true, false, false, false),
|
|
("finance", Modules.AuditLogs, true, false, false, false),
|
|
("board_member", Modules.AuditLogs, true, false, false, false),
|
|
("pastor", Modules.Form990Report, true, false, false, false),
|
|
("board_member", Modules.Form990Report, true, false, false, false),
|
|
|
|
// Ministries — secretary maintains the list; coworker_chair edits; ministry
|
|
// leaders and pastor read.
|
|
("secretary", Modules.Ministries, true, true, true, false),
|
|
("coworker_chair", Modules.Ministries, true, true, false, false),
|
|
("ministry_leader", Modules.Ministries, true, false, false, false),
|
|
("pastor", Modules.Ministries, true, false, false, false),
|
|
|
|
// Meal attendance — secretary and coworkers record; finance and pastor read.
|
|
("secretary", Modules.MealAttendance, true, true, false, false),
|
|
("coworker", Modules.MealAttendance, true, true, false, false),
|
|
("finance", Modules.MealAttendance, true, false, false, false),
|
|
("pastor", Modules.MealAttendance, true, false, false, false),
|
|
|
|
// Users, Permissions, and Settings are intentionally super_admin-only:
|
|
// super_admin bypasses all checks, so no seed rows are needed here.
|
|
];
|
|
|
|
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<AppRole> 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,
|
|
DefaultFunctionalClass = en == "Administration"
|
|
? FunctionalClasses.ManagementGeneral
|
|
: FunctionalClasses.Program,
|
|
});
|
|
}
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
public static async Task SeedExpenseCategoriesAsync(AppDbContext db)
|
|
{
|
|
// One-time renames to remove same-name-different-parent ambiguity. Idempotent:
|
|
// only fires while the old name still exists. (New installs never hit this.)
|
|
var renames = new (string GroupEn, string OldSub, string NewEn, string NewZh)[]
|
|
{
|
|
("Food & Beverage", "Consumables", "Disposable Tableware", "一次性餐具"),
|
|
("Materials", "Printing", "Curriculum Printing", "教材印刷"),
|
|
};
|
|
foreach (var (groupEn, oldSub, newEn, newZh) in renames)
|
|
{
|
|
var grp = await db.ExpenseCategoryGroups.FirstOrDefaultAsync(g => g.Name_en == groupEn);
|
|
if (grp is null) continue;
|
|
var sub = await db.ExpenseSubCategories.FirstOrDefaultAsync(s => s.GroupId == grp.Id && s.Name_en == oldSub);
|
|
if (sub is not null) { sub.Name_en = newEn; sub.Name_zh = newZh; }
|
|
}
|
|
await db.SaveChangesAsync();
|
|
|
|
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 SeedForm990ExpenseLinesAsync(AppDbContext db)
|
|
{
|
|
foreach (var (code, en, zh, sort) in Form990LineSeed)
|
|
{
|
|
if (!await db.Form990ExpenseLines.AnyAsync(l => l.LineCode == code))
|
|
db.Form990ExpenseLines.Add(new Form990ExpenseLine
|
|
{ LineCode = code, Name_en = en, Name_zh = zh, SortOrder = sort, IsActive = true });
|
|
}
|
|
await db.SaveChangesAsync();
|
|
|
|
var linesByCode = await db.Form990ExpenseLines.ToDictionaryAsync(l => l.LineCode, l => l.Id);
|
|
var fallbackId = linesByCode["24"];
|
|
|
|
// Every group defaults to line 24 (safety net); precise mapping lives on subcategories.
|
|
foreach (var group in await db.ExpenseCategoryGroups.ToListAsync())
|
|
group.Form990LineId ??= fallbackId;
|
|
|
|
// Subcategory default mappings — only set when not already mapped (never clobber an admin edit).
|
|
var subsByKey = await db.ExpenseSubCategories.Include(s => s.Group).ToListAsync();
|
|
foreach (var (groupEn, subEn, code) in Form990SubMappingSeed)
|
|
{
|
|
var sub = subsByKey.FirstOrDefault(s => s.Group!.Name_en == groupEn && s.Name_en == subEn);
|
|
if (sub is not null && sub.Form990LineId is null && linesByCode.TryGetValue(code, out var lineId))
|
|
sub.Form990LineId = lineId;
|
|
}
|
|
|
|
// Correct earlier mis-mappings on existing databases (see Form990RemapSeed). Only fires
|
|
// while the subcategory still holds the OLD line, so a later admin edit is never clobbered.
|
|
foreach (var (groupEn, subEn, oldCode, newCode) in Form990RemapSeed)
|
|
{
|
|
var sub = subsByKey.FirstOrDefault(s => s.Group!.Name_en == groupEn && s.Name_en == subEn);
|
|
if (sub is null) continue;
|
|
if (linesByCode.TryGetValue(oldCode, out var oldId)
|
|
&& linesByCode.TryGetValue(newCode, out var newId)
|
|
&& sub.Form990LineId == oldId)
|
|
sub.Form990LineId = newId;
|
|
}
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
public static async Task SeedForm1099BoxesAsync(AppDbContext db)
|
|
{
|
|
foreach (var (code, en, zh, formType, sort) in Form1099BoxSeed)
|
|
if (!await db.Form1099Boxes.AnyAsync(b => b.BoxCode == code))
|
|
db.Form1099Boxes.Add(new Form1099Box
|
|
{ BoxCode = code, Name_en = en, Name_zh = zh, FormType = formType, SortOrder = sort, IsActive = true });
|
|
await db.SaveChangesAsync();
|
|
|
|
var boxesByCode = await db.Form1099Boxes.ToDictionaryAsync(b => b.BoxCode, b => b.Id);
|
|
var subs = await db.ExpenseSubCategories.Include(s => s.Group).ToListAsync();
|
|
foreach (var (groupEn, subEn, code) in Form1099SubMappingSeed)
|
|
{
|
|
var sub = subs.FirstOrDefault(s => s.Group!.Name_en == groupEn && s.Name_en == subEn);
|
|
if (sub is not null && sub.Form1099BoxId is null && boxesByCode.TryGetValue(code, out var boxId))
|
|
sub.Form1099BoxId = boxId;
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
|
|
public static async Task SeedSiteSettingAsync(AppDbContext db)
|
|
{
|
|
// Singleton row holding site-wide presentation/locale settings.
|
|
if (!await db.SiteSettings.AnyAsync())
|
|
{
|
|
db.SiteSettings.Add(new SiteSetting
|
|
{
|
|
SiteTitle = "River Of Life Christian Church",
|
|
SiteTitleZh = "生命河靈糧堂",
|
|
DefaultLanguage = "en",
|
|
TimeZone = "America/Los_Angeles",
|
|
DateFormat = "yyyy-MM-dd",
|
|
Currency = "USD",
|
|
});
|
|
await db.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
public static async Task SeedNotificationSettingAsync(AppDbContext db, IConfiguration config)
|
|
{
|
|
// Singleton row that becomes the runtime source of truth for SMTP + Line. Seed it once
|
|
// from the legacy "Smtp"/"Line" appsettings sections so existing config carries over.
|
|
if (!await db.NotificationSettings.AnyAsync())
|
|
{
|
|
var smtp = config.GetSection("Smtp");
|
|
var line = config.GetSection("Line");
|
|
db.NotificationSettings.Add(new NotificationSetting
|
|
{
|
|
EnableEmail = !string.IsNullOrWhiteSpace(smtp["Host"]),
|
|
SmtpHost = smtp["Host"] ?? "",
|
|
SmtpPort = int.TryParse(smtp["Port"], out var port) ? port : 587,
|
|
SmtpUseSsl = !bool.TryParse(smtp["UseSsl"], out var ssl) || ssl,
|
|
SmtpUser = smtp["User"] ?? "",
|
|
SmtpPassword = smtp["Password"] ?? "",
|
|
FromAddress = smtp["FromAddress"] ?? "",
|
|
FromName = smtp["FromName"] ?? "",
|
|
EnableLine = !string.IsNullOrWhiteSpace(line["ChannelAccessToken"]),
|
|
LineChannelAccessToken = line["ChannelAccessToken"] ?? "",
|
|
LineChannelSecret = line["ChannelSecret"] ?? "",
|
|
});
|
|
await db.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seeds roles and (in Development) the default admin account.
|
|
/// Called once on application startup after migrations have been applied.
|
|
/// </summary>
|
|
public static async Task SeedAsync(IServiceProvider services)
|
|
{
|
|
var roleManager = services.GetRequiredService<RoleManager<AppRole>>();
|
|
var userManager = services.GetRequiredService<UserManager<AppUser>>();
|
|
var env = services.GetRequiredService<IWebHostEnvironment>();
|
|
var config = services.GetRequiredService<IConfiguration>();
|
|
|
|
await SeedRolesAsync(roleManager);
|
|
|
|
var db = services.GetRequiredService<AppDbContext>();
|
|
await SeedRolePermissionsAsync(db);
|
|
await SeedGivingCategoriesAsync(db);
|
|
await SeedMinistriesAsync(db);
|
|
await SeedExpenseCategoriesAsync(db);
|
|
await SeedForm990ExpenseLinesAsync(db);
|
|
await SeedForm1099BoxesAsync(db);
|
|
await SeedChurchProfileAsync(db);
|
|
await SeedSiteSettingAsync(db);
|
|
await SeedNotificationSettingAsync(db, config);
|
|
|
|
if (env.IsDevelopment())
|
|
await SeedAdminUserAsync(userManager);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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!
|
|
/// </summary>
|
|
public static async Task SeedAdminUserAsync(UserManager<AppUser> 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");
|
|
}
|
|
}
|
|
}
|