Files
ROLAC/docs/superpowers/specs/2026-06-24-expense-990-functional-expenses-design.md
Chris Chen b51f22cfba docs: expand 990 expense-line catalog and add categories to cover gaps
Add 990 lines 5/8/11b/11c/20 to the catalog and new natural categories
(Personnel officer comp + pension, Missions foreign support, Printing
advertising, plus Professional Services / Information Technology /
Finance & Banking groups) so the category tree covers the common Part IX
lines instead of dumping uncovered lines into 24.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:30:49 -07:00

278 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 子專案 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 | 對國外之捐贈 |
| 5 | Compensation of current officers / key employees | 主要職員/負責人薪酬 |
| 7 | Other salaries and wages | 薪資 |
| 8 | Pension plan accruals and contributions | 退休金提撥 |
| 9 | Other employee benefits | 員工福利 |
| 10 | Payroll taxes | 薪資稅 |
| 11b| Legal fees | 法律服務費 |
| 11c| Accounting fees | 會計與審計費 |
| 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 | 會議與研習 |
| 20 | Interest | 利息 |
| 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` 不受影響。
### 2.4 新增類別(補齊 990 行覆蓋)
現有 11 大類對某些 990 行無對應(會讓那些行恆為 0 或被硬塞 line 24)。補上以下類別讓自然樹真正覆蓋常用 990 行。所有新增皆走 `DbSeeder`,並更新 DB_SCHEMA.md。
**加進現有大類**
| 大類 | 新子項目 | 990 行 |
|---|---|---|
| Personnel 人事 | Officer / Key Employee Compensation 主要職員薪酬 | **5** |
| Personnel 人事 | Retirement / Pension 退休金 | **8** |
| Missions 宣教 | Foreign Missions Support 國外宣教支援 | **3** |
| Printing 印刷 | Advertising & Promotion 廣告推廣 | **12** |
> Personnel 既有 `Salary & Wages` 維持 line 7(一般員工);牧師等 officer/key employee 薪酬改記新子項目 → line 5。
> Missions 既有 `Missionary Support` / `Offering Transfer` 視為國內/未分 → line 1;國外走新子項目 → line 3。
**新增大類**
| 新大類 | 子項目 | 990 行 |
|---|---|---|
| **Professional Services 專業服務** | Legal 法律服務 | **11b** |
| | Accounting & Audit 會計與審計 | **11c** |
| | Other Professional 其他專業服務 | 11g |
| **Information Technology 資訊科技** | Software & Subscriptions 軟體與訂閱 | **14** |
| | Website & Hosting 網站與主機 | **14** |
| | Internet & Telecom 網路與電信 | **14** |
| **Finance & Banking 財務與銀行** | Interest 利息支出 | **20** |
| | Bank & Processing Fees 銀行/金流手續費 | 24 |
大類數由 11 → **14**(新增 Professional Services、Information Technology、Finance & Banking)。新大類 `Form990LineId` 預設仍 seed 為 24(保底),實際映射在子項目層。
---
## 3. 報表層
**新服務 `Form990ReportService`**(與 `FinanceDashboardService` 並列,讀取為主)。
```csharp
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. 前端(Angularadmin)
沿用既有 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. 既有支出資料不因本次變更而遺失或錯置。