diff --git a/docs/superpowers/specs/2026-06-24-expense-990-functional-expenses-design.md b/docs/superpowers/specs/2026-06-24-expense-990-functional-expenses-design.md new file mode 100644 index 0000000..91dd07a --- /dev/null +++ b/docs/superpowers/specs/2026-06-24-expense-990-functional-expenses-design.md @@ -0,0 +1,243 @@ +# 子專案 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 + ?? // 保底,確保無漏列 +``` + +> 映射**必須能下到子項目層**: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` 並列,讀取為主)。 + +```csharp +Task 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` 卡片)。 + +1. **Part IX 報表頁**:Kendo Grid 矩陣(列=990 行,欄=三功能別+Total),年度/區間篩選,雙語,行動裝置卡片版;`UnmappedExpenseCount` 以提示列顯示;匯出鈕。 +2. **支出表單**(`expense-form-dialog`):新增 `FunctionalClass` 下拉(可空=繼承事工);Kendo DropdownList 設 `[valuePrimitive]="true"`。 +3. **類別維護頁**(`expense-categories-page`):每個大類/子項目可設 `Form990LineId`(990 行下拉)。 +4. **事工維護**:`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: +1. 新表 `Form990ExpenseLines`。 +2. 新欄 `Ministries.DefaultFunctionalClass`、`Expenses.FunctionalClass`、`ExpenseSubCategories.Form990LineId`、`ExpenseCategoryGroups.Form990LineId`(後二者建 FK)。 +3. 資料 migration:類別改名(§2.3)。 +4. `DbSeeder`:seed `Form990ExpenseLine`、子項目→行的預設映射、Ministry 預設功能別、更新改名後的 seed 字串。 + +DB_SCHEMA.md 同步更新(新表 + 新欄 + §8 備注)。 + +--- + +## 7. 不在此範圍(已知缺口) + +- **資本化 / 折舊(line 22)**:line 已 seed 但不映射;Equipment 購置暫入 line 24。需要時另開。 +- **1099 收款人追蹤**:子專案 B。 +- **收入端 Part VIII**:子專案 C。 +- **跨功能比例分攤**:本系統維持單筆單一功能別,不支援拆分。 + +--- + +## 8. 驗收標準 + +1. 可在報表頁選定年度,產出 Part IX 矩陣(三功能別 × 990 行),欄合計與總計正確,且金額等於同口徑(Paid+Approved)的支出總額。 +2. 變更某事工 `DefaultFunctionalClass` 或某筆 `FunctionalClass` 後,報表對應欄位即時反映。 +3. 未細映的子項目金額落在 line 24,且 `UnmappedExpenseCount` 正確顯示。 +4. 類別樹中不再有「同父同名」歧義;`Training/Missions > Travel` 維持並正確合併至 line 17。 +5. 既有支出資料不因本次變更而遺失或錯置。