Files
ROLAC/docs/superpowers/specs/2026-06-25-1099-recipient-tracking-design.md
T
2026-06-25 16:16:04 -07:00

155 lines
10 KiB
Markdown
Raw 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.
# 子專案 B — 1099 收款人追蹤(1099 Recipient Tracking)設計
**日期:** 2026-06-25
**狀態:** Approveduser 已核可,待轉 implementation plan
**範圍:** 僅子專案 B。支出 Part IX(A)已上線;收入端 Part VIII(C)為獨立 spec,不在此。
---
## 1. 目標與背景
教會依 IRC §6033(a)(3)(A) 免於申報 990,但對**獨立承攬人/廠商**的付款仍須在年底產出 **1099-NEC**(非員工報酬)記錄。本子專案讓系統能:辨識收款人身分、保存 W-9/TIN、依**已付**金額按年彙總、標示 $600 門檻與缺漏 W-9,並產出可交付的收款人聯(Copy B)PDF 與申報用資料檔。
**現況缺口:** 系統沒有收款人身分。廠商付款只存自由文字 `Expense.VendorName`(nullable, max 200);出納工作清單以該**字串**分組。沒有任何 W-9/TIN 資料,也無法把一整年付款依收款人加總。
**實際驅動案例:** 一位**兼職同工同時也是 Member**,以獨立承攬人身分受款,需開立 1099-NEC。
### 設計原則
- **資料驅動、疊在現有分類軸之上**,沿用子專案 A 的「映射欄位 + 參考表」風格,不重寫分類樹。
- 收款人身分以**獨立 master** 表達,與 Member **可選關聯**(不強耦合)。
- 1099 應報與否需**兩個條件同時成立**:收款人被追蹤 + 該筆科目映射到 1099 box。
- 員工(W-2/薪資)**不在範圍**(本系統無 payroll 模組)。
- 向後相容:新增欄位皆 nullable,既有資料不破。
---
## 2. 資料模型變更
### 2.1 新表 `Payee1099`(收款人 master)— 繼承 `SoftDeleteEntity, IAuditable`
檔案:`API/ROLAC.API/Entities/Payee1099.cs`
| 欄位 | 型別 | 說明 |
|---|---|---|
| Id | int PK | |
| LegalName | varchar(200) NOT NULL | W-9 上的法定名稱 |
| DisplayName | varchar(200)? | 友善 / DBA 名稱 |
| MemberId | int? FK→Member (SetNull) | 收款人同時是 Member 時連結(兼職同工案例) |
| TaxClassification | varchar(40) | Individual/SoleProprietor、Partnership、CCorp、SCorp、LLC、Other — 決定 `Is1099Tracked` 預設 |
| Is1099Tracked | bool NOT NULL DEFAULT true | 可覆寫;公司(C/S Corp)預設 false |
| TinType | varchar(10)? | "SSN" \| "EIN" |
| TinEncrypted | text? | 經 Data Protection API 加密的 TIN |
| TinLast4 | varchar(4)? | 遮罩顯示 / 搜尋用,免解密 |
| AddressLine1/2, City, State, Zip | varchar | 1099 表單用地址 |
| Email, Phone | varchar? | W-9 催收用 |
| W9Status | varchar(20) DEFAULT 'Missing' | Missing \| Requested \| OnFile \| Expired |
| W9ReceivedDate | DateOnly? | |
| W9BlobPath | text? | 上傳的 W-9 PDF/影像(比照 `Expense.ReceiptBlobPath`) |
| IsActive | bool DEFAULT true | |
| Notes | text? | |
| + audit + soft-delete | | 由 `SoftDeleteEntity` 提供 |
### 2.2 `Expense` 新增 `PayeeId int?` FK → Payee1099 (SetNull)
檔案:`API/ROLAC.API/Entities/Expense.cs`。**表頭層**(一筆支出/一張支票 = 一位收款人,與 `Check.PayeeName` 一致)。與 `Type` 無關 — 外部廠商與「同工承攬人」皆適用。`VendorName` 仍保留為自由文字 fallback/snapshot。
### 2.3 新參考表 `Form1099Box` — 繼承 `AuditableEntity, IAuditable`(比照 `Form990ExpenseLine`)
檔案:`API/ROLAC.API/Entities/Form1099Box.cs`
- Id、BoxCode(unique,如 `"NEC-1"``"MISC-1"`)、Name_en、Name_zh?、FormType(`"1099-NEC"` | `"1099-MISC"`)、SortOrder、IsActive。
- **seed 子集:** `NEC-1` Nonemployee compensation 非員工報酬;`MISC-1` Rents 租金。目錄可擴充。
### 2.4 映射欄位(完全比照 990-line 模式)
- `ExpenseSubCategory.Form1099BoxId int?` FK → Form1099Box (SetNull)— **主要映射**
- `ExpenseCategoryGroup.Form1099BoxId int?` FK — 大類 fallback
**有效 box = `sub ?? group ?? null`。** 與 990 不同(990 fallback 為 line 24,人人有歸屬);此處 **null = 不列入 1099** 才是預設 — 只有勞務性科目才給 box。
**預設 seed 映射(子項目 → box),僅列可報者:**
- Personnel ▸ Honorarium → NEC-1
- Personnel ▸ Contract Labor → NEC-1
- Professional Services ▸ Legal / Accounting & Audit / Other Professional → NEC-1
- Facility ▸ Rent → MISC-1
- **其餘一律 unmapped(排除)。** Salary & Wages / Officer Compensation 維持 unmapped(那是 W-2 薪資,永不入 1099)— 即使被追蹤的收款人記在這些科目,box gate 也會擋下。
---
## 3. 報表層
新服務 `Form1099ReportService`(讀取為主,與 `Form990ReportService` 並列)。
檔案:`API/ROLAC.API/Services/{IForm1099ReportService,Form1099ReportService}.cs`、DTOs `API/ROLAC.API/DTOs/Finance/Form1099ReportDtos.cs`、controller `API/ROLAC.API/Controllers/Form1099ReportController.cs`
```csharp
Task<Form1099SummaryDto> GetAnnualSummaryAsync(int taxYear);
Task<Form1099RecipientDetailDto> GetRecipientDetailAsync(int payeeId, int taxYear);
Task<List<Form1099BoxDto>> GetBoxesAsync();
```
**現金基礎查詢(與 990 報表不同):** `Status == "Paid"``PaidAt` 年份 == taxYear。(1099 報的是**該曆年實際支付**的金額,而非 990 報表採用的 Approved/ExpenseDate。`Expense.PaidAt` 為支付日;`Check.CheckDate` 為日後若要更精準的替代基準。)
**彙總邏輯:**
1. Join 已付支出(PaidAt 落在該年、`PayeeId` 非 null)→ `ExpenseLines` → SubCategory/Group → 有效 box。
2. 只保留 **有效 box ≠ null 且 `payee.Is1099Tracked`** 的行。
3.`(PayeeId, BoxCode)` 加總。
4. 每位收款人:各 box 小計;`MeetsThreshold`(每 box ≥ **$600**,常數 `Form1099.ReportingThreshold`);`W9Missing`(`W9Status != "OnFile"`)。
**DTOs:**
- `Form1099SummaryDto { TaxYear, Rows:[Form1099RecipientRowDto], TotalReportable, RecipientsAtThreshold, RecipientsMissingW9 }`
- `Form1099RecipientRowDto { PayeeId, LegalName, TinLast4, W9Status, NecTotal, RentsTotal, GrandTotal, MeetsThreshold, W9Missing }`
- `Form1099RecipientDetailDto { 收款人表頭 + 構成付款明細: [date, description, categoryName, boxCode, amount] }`
---
## 4. TIN 加密
採用 ASP.NET Core **Data Protection API**(`IDataProtectionProvider.CreateProtector("Payee1099.Tin")`)— 可逆、由框架管理金鑰、不引入新加密相依。寫入時加密;另存 `TinLast4` 供顯示/搜尋。完整 TIN 解密僅透過專屬 endpoint,並以本模組 **Write** action 把關;其餘一律遮罩(`***-**-1234`)。
## 5. 1099-NEC Copy B PDF + 申報資料匯出
新服務 `I1099FormService`,沿用 DevExpress 管道(`ICheckPrintService` / `DevExpress.Document.Processor`;授權檔已設定,見 [[project-devexpress-check-printing]])。產出**收款人聯 Copy B** 1099-NEC(payer = `ChurchProfile`、recipient = `Payee1099`、box 1 = NEC 合計),純白紙列印。另產出供 IRIS/會計師用的**申報資料 CSV/試算表**。不含 IRS 傳輸。
## 6. 權限
`API/ROLAC.API/Authorization/Modules.cs`(+ `Modules.All`)新增模組 `Form1099`,並同步前端 `PermissionModules`(`APP/src/app/core/models/permission.model.ts`)。Actions:Read(收款人 + 報表)、Write(編輯收款人、連結 payee、顯示完整 TIN)、Delete。seed 財務角色之 RolePermission;super_admin 自動 bypass。
## 7. 前端(Angular,admin)
慣例:`UserPortalComponent` 財務導覽群組 + `app.routes.ts` 路由 data(`title/titleZh/section` + `PermissionGuard`)、unified header(`appPageHeaderActions`)、Kendo UI、Tailwind 表單版面、行動裝置 `hidden md:block` + `md:hidden` 卡片。([[project-real-sidebar-nav]]、[[project-unified-system-header]]、[[feedback-mobile-friendly-all-screens]]、[[feedback-form-layout-tailwind]])
1. **1099 收款人維護頁**(`features/payee1099/pages/payee-1099-page`)— Kendo Grid(LegalName、member 連結、分類、TIN 末四碼遮罩、W-9 狀態徽章、Tracked 開關、Active);右鍵 context menu Edit/Deactivate;編輯對話框含 W-9 欄位 + Member 選擇器 + 遮罩 TIN 輸入 + W-9 上傳;行動卡片。比照 `expense-categories-page`
2. **科目 → box 映射** — 擴充現有 `expense-categories-page`,在既有 990-line 下拉旁加一個「1099 Box」下拉(大類/子項目皆可設 `Form1099BoxId`)。`[valuePrimitive]="true"`([[feedback-kendo-value-primitive]])。
3. **支出表單**(`expense-form-dialog`)— 新增可選「1099 收款人」payee 選擇器(DropdownList、`valuePrimitive`)。
4. **1099 年度報表頁**(`features/finance-report/pages/form1099-report-page`)— 年度選擇器;收款人 grid(NEC/Rents 合計、門檻旗標、缺 W-9 旗標);下鑽收款人明細(構成付款,[[feedback-kendo-table-select-via-row-click]]);header actions「匯出申報資料」+「產生 Copy B PDF」。行動卡片。比照 `form990-report-page`
## 8. Migration / 落地
- EF migration:新表 `Payee1099s``Form1099Boxes`;新欄 `Expenses.PayeeId``ExpenseSubCategories.Form1099BoxId``ExpenseCategoryGroups.Form1099BoxId`(FK、SetNull)。
- `DbSeeder`:seed `Form1099Box` 目錄 + 子項目→box 映射(**只填 NULL**,比照 `SeedForm990ExpenseLinesAsync` 的冪等性;不得覆蓋 admin 編輯)。無 catch-all fallback(unmapped = 不列入)。
- 同步更新 `docs/DB_SCHEMA.md`(新表 + 新欄)。
- v1 **不**自動把既有自由文字 `VendorName` 回填成 master(教會規模小,手動連結即可)。列為已知後續。
---
## 9. 測試
沿用既有測試模式(`ExpenseServiceTests` 等;Release build,見 [[project-build-run-env]]):
- `EffectiveBox` 解析:子項目 ?? 大類 ?? null。
- 報表現金基礎:Paid + PaidAt 年份;每收款人/每 box 加總正確。
- 門檻:恰 $600 觸發旗標;缺 W-9 旗標正確。
- `Is1099Tracked` gate;員工薪資科目被排除;同工(member-linked)收款人正確加總。
- TIN 加解密 round-trip + 末四碼 + 遮罩。
## 10. 不在此範圍(已知缺口)
- IRS 電子申報(IRIS/FIRE)整合。
- 官方 Copy A / 1096 表單(v1 僅 Copy B + 資料匯出)。
- payroll / W-2(員工)。
- 既有 `VendorName` → master 自動回填。
- 可設定門檻(v1 以常數)。
## 11. 驗收標準
1. 可在收款人維護頁建立 `Payee1099`(含 W-9/TIN,TIN 遮罩),並可連結 Member。
2. 支出可選填 1099 收款人;科目可設 1099 box;映射採子項目優先、大類 fallback、否則不列入。
3. 年度報表依**已付**金額按收款人 × box 加總,正確標示 $600 門檻與缺 W-9;可下鑽明細。
4. 完整 TIN 僅在具 Write 權限時可揭示;其餘遮罩為末四碼。
5. 可產出收款人聯 Copy B 1099-NEC PDF(無 DevExpress 浮水印)與申報資料 CSV。
6. 員工薪資科目即使付給被追蹤收款人,也不出現在 1099 報表;既有支出資料不受影響。