Audit-readiness at Form 990 Part IX level: functional class (Program/M&G/ Fundraising) via Ministry default + per-expense override, Form990ExpenseLine catalog + subcategory/group mapping, duplicate-category cleanup, and the Part IX functional-expense matrix report. 1099 (B) and revenue Part VIII (C) are separate specs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 KiB
子專案 A — 支出 990 化(Functional Expenses / Part IX)設計
日期: 2026-06-24 狀態: Draft(待 user review) 範圍: 僅子專案 A。1099 收款人(B)、收入端 Part VIII(C)為獨立 spec,不在此。
1. 目標與背景
教會依 IRC §6033(a)(3)(A) 免於申報 Form 990,但本系統要做到 990 查帳就緒(audit-readiness):在 IRS 檢查時,能依需求產出等同 Form 990 Part IX — Statement of Functional Expenses 的功能性費用表及佐證明細。
Part IX 本質是一個矩陣:
Program | Mgmt & General | Fundraising | Total
自然費用行(990 line)
7 Salaries & wages ... ... ... ...
9 Employee benefits ... ... ... ...
10 Payroll taxes ... ... ... ...
16 Occupancy ... ... ... ...
...
24 Other expenses ... ... ... ...
─────────────────────────────────────────────────────────────
Total ... ... ... ...
現況:FinanceDashboardService 已能按 Ministry → CategoryGroup → SubCategory 彙總(Paid+Approved 口徑),但沒有功能別維度,自然類別也未對應 990 行。本子專案在現有兩條軸之上疊一層「990 對照」,不重寫分類樹。
設計原則
- 維持現有兩條軸:
Ministry= 功能別來源;ExpenseCategoryGroup → ExpenseSubCategory= 自然科目。 - 990 對照以映射欄位 + 參考表實現(資料驅動,與 RolePermission / Categories 同風格)。
- 單筆支出單一功能別(direct-charge),不做跨功能比例分攤。
- 向後相容:新增欄位皆 nullable,既有資料不破。
2. 資料模型變更
2.1 功能別(Part IX 三欄)
功能別值域(字串,沿用 codebase 用 string 表 Type/Status 的慣例):
"Program" | "ManagementGeneral" | "Fundraising"
Ministry(新增欄位)
| 欄位 | 型別 | 說明 |
|---|---|---|
| DefaultFunctionalClass | varchar(20) NOT NULL DEFAULT 'Program' | 該事工支出的預設功能別 |
Expense(新增欄位)
| 欄位 | 型別 | 說明 |
|---|---|---|
| FunctionalClass | varchar(20)? | 覆寫;null = 繼承 Ministry |
有效功能別(報表計算用):
EffectiveFunctionalClass = Expense.FunctionalClass
?? Ministry.DefaultFunctionalClass
?? "Program" // 最終保底
Ministry 預設 seed:Administration → ManagementGeneral,其餘 9 個事工 → Program。目前無 Fundraising 事工(教會少見),需要時用單筆覆寫。
2.2 990 Part IX 行目錄 + 映射
新表 Form990ExpenseLine(參考表 / 資料驅動)
| 欄位 | 型別 | 說明 |
|---|---|---|
| Id | int PK | |
| LineCode | varchar(10) NOT NULL UNIQUE | 990 行編號,如 "7"、"11g"、"16"、"24" |
| Name_en | varchar(200) NOT NULL | 如 "Other salaries and wages" |
| Name_zh | varchar(200)? | |
| SortOrder | int NOT NULL | 報表列順序 |
| IsActive | bool NOT NULL DEFAULT true |
繼承 AuditableEntity(與其他類別表一致)。
映射欄位(兩處,nullable):
ExpenseSubCategory.Form990LineId(int? FK → Form990ExpenseLine.Id)— 主要映射ExpenseCategoryGroup.Form990LineId(int? FK)— 大類預設(便利用)
有效 990 行:
EffectiveLine = SubCategory.Form990LineId
?? Group.Form990LineId
?? <line "24 Other expenses"> // 保底,確保無漏列
映射必須能下到子項目層:Personnel 大類底下 Salary→line 7、Payroll Taxes→line 10、Benefits→line 9 是三個不同 990 行,大類層無法表達。
seed 的 990 行子集(教會常用):
| LineCode | Name_en | Name_zh |
|---|---|---|
| 1 | Grants to domestic organizations | 對國內機構之捐贈 |
| 2 | Grants to domestic individuals | 對國內個人之捐贈 |
| 3 | Grants to foreign organizations/individuals | 對國外之捐贈 |
| 7 | Other salaries and wages | 薪資 |
| 9 | Other employee benefits | 員工福利 |
| 10 | Payroll taxes | 薪資稅 |
| 11g | Other fees for services (non-employee) | 其他勞務報酬(非員工) |
| 12 | Advertising and promotion | 廣告與推廣 |
| 13 | Office expenses | 辦公費用 |
| 14 | Information technology | 資訊科技 |
| 16 | Occupancy | 場地佔用 |
| 17 | Travel | 差旅 |
| 19 | Conferences, conventions, and meetings | 會議與研習 |
| 22 | Depreciation | 折舊(本子專案不映射,留行供未來資本化使用) |
| 23 | Insurance | 保險 |
| 24 | Other expenses | 其他費用 |
現有子項目 → 990 行的預設映射 seed:
| 子項目(大類) | 990 行 |
|---|---|
| Personnel > Salary & Wages | 7 |
| Personnel > Payroll Taxes | 10 |
| Personnel > Employee Benefits | 9 |
| 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 |
| Training > Course Fees | 19 |
| Training > Conference | 19 |
| Training > Books | 24 |
| Training > Travel | 17 |
| Missions > Travel | 17 |
| Missions > Offering Transfer | 1 |
| Missions > Missionary Support | 1 |
| Benevolence > Emergency Aid | 2 |
| Benevolence > Condolence Gifts | 2 |
| Benevolence > Visit Expenses | 2 |
| Consumables > Office Supplies | 13 |
| Consumables > Batteries / Accessories / Cleaning Supplies | 24 |
| Printing > Bulletins / Order of Service | 13 |
| Printing > Posters | 12 |
| Materials > Curriculum Printing(見 §2.3) | 13 |
| Materials > Craft Supplies / Copyright & Licensing | 24 |
| Food & Beverage > 全部子項目 | 24 |
| Equipment > 全部子項目 | 24 |
| Other > Miscellaneous | 24 |
大類層 Form990LineId 一律 seed 為 24(保底),確保未細映的子項目仍落在 line 24。
2.3 類別清理(互斥化)
只用改名解決真正的歧義(不需搬移既有支出),把同名收斂成唯一含義:
| 現況 | 問題 | 解法 |
|---|---|---|
Food & Beverage > Consumables |
與大類 Consumables 同名 |
改名 → "Disposable Tableware" / 一次性餐具 |
Materials > Printing |
與大類 Printing 同名 |
改名 → "Curriculum Printing" / 教材印刷 |
Training > Travel、Missions > Travel |
同名但父類不同 | 不算衝突,維持原樣;兩者都映射到 990 line 17,報表自動合併 |
改名落地方式:
- 新安裝:更新
DbSeeder的 seed 字串。 - 既有 DB:一次性資料 migration,依
(GroupId, 舊 Name_en)定位後更新Name_en/Name_zh(seed 採 insert-if-not-exists,不會自動改既有列,故需 migration)。
無 SubCategoryId 搬移,既有 Expense 不受影響。
3. 報表層
新服務 Form990ReportService(與 FinanceDashboardService 並列,讀取為主)。
Task<FunctionalExpenseStatementDto> GetFunctionalExpenseStatementAsync(
DateOnly? from, DateOnly? to);
- 支出口徑沿用
FinanceDashboardService既有約定:Status == "Paid" || "Approved",選用ExpenseDate區間。 - 對每筆支出計算
EffectiveFunctionalClass與EffectiveLine,彙總成矩陣。
FunctionalExpenseStatementDto
Rows: [ { LineCode, Name_en, Name_zh,
Program, ManagementGeneral, Fundraising, Total } ] // 依 SortOrder
ColumnTotals: { Program, ManagementGeneral, Fundraising, GrandTotal }
UnmappedExpenseCount: int // 落到保底 line 24 的「未明確映射」筆數,提示待補映射
UnmappedExpenseCount 讓財務知道哪些還沒細映(治理用),但金額仍計入 line 24,不漏帳。
匯出: 沿用既有報表/DevExpress 管道,輸出可交付會計師的表格(PDF/試算表)。
4. 前端(Angular,admin)
沿用既有 portal 慣例(UserPortalComponent 導覽、unified header、Kendo UI、表單版面用 Tailwind utilities、行動裝置友善 hidden md:block + md:hidden 卡片)。
- Part IX 報表頁:Kendo Grid 矩陣(列=990 行,欄=三功能別+Total),年度/區間篩選,雙語,行動裝置卡片版;
UnmappedExpenseCount以提示列顯示;匯出鈕。 - 支出表單(
expense-form-dialog):新增FunctionalClass下拉(可空=繼承事工);Kendo DropdownList 設[valuePrimitive]="true"。 - 類別維護頁(
expense-categories-page):每個大類/子項目可設Form990LineId(990 行下拉)。 - 事工維護:
DefaultFunctionalClass下拉。
5. 測試
沿用既有測試模式(ExpenseServiceTests 等;受測元件用 inline template,Edge via CHROME_BIN,以 --include 縮限)。
EffectiveFunctionalClass解析:覆寫優先、否則繼承事工、再保底 Program。EffectiveLine解析:子項目優先、否則大類、再保底 line 24。- 矩陣彙總:多筆跨功能別/跨行正確加總;欄合計與總計一致。
- 保底行為:未映射子項目進 line 24 且
UnmappedExpenseCount正確。 - 狀態口徑:僅 Paid+Approved 計入;區間篩選正確。
6. Migration / 落地
EF Core code-first:
- 新表
Form990ExpenseLines。 - 新欄
Ministries.DefaultFunctionalClass、Expenses.FunctionalClass、ExpenseSubCategories.Form990LineId、ExpenseCategoryGroups.Form990LineId(後二者建 FK)。 - 資料 migration:類別改名(§2.3)。
DbSeeder:seedForm990ExpenseLine、子項目→行的預設映射、Ministry 預設功能別、更新改名後的 seed 字串。
DB_SCHEMA.md 同步更新(新表 + 新欄 + §8 備注)。
7. 不在此範圍(已知缺口)
- 資本化 / 折舊(line 22):line 已 seed 但不映射;Equipment 購置暫入 line 24。需要時另開。
- 1099 收款人追蹤:子專案 B。
- 收入端 Part VIII:子專案 C。
- 跨功能比例分攤:本系統維持單筆單一功能別,不支援拆分。
8. 驗收標準
- 可在報表頁選定年度,產出 Part IX 矩陣(三功能別 × 990 行),欄合計與總計正確,且金額等於同口徑(Paid+Approved)的支出總額。
- 變更某事工
DefaultFunctionalClass或某筆FunctionalClass後,報表對應欄位即時反映。 - 未細映的子項目金額落在 line 24,且
UnmappedExpenseCount正確顯示。 - 類別樹中不再有「同父同名」歧義;
Training/Missions > Travel維持並正確合併至 line 17。 - 既有支出資料不因本次變更而遺失或錯置。