Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 KiB
子專案 B — 1099 收款人追蹤(1099 Recipient Tracking)設計
日期: 2026-06-25 狀態: Approved(user 已核可,待轉 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-1Nonemployee compensation 非員工報酬;MISC-1Rents 租金。目錄可擴充。
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。
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 為日後若要更精準的替代基準。)
彙總邏輯:
- Join 已付支出(PaidAt 落在該年、
PayeeId非 null)→ExpenseLines→ SubCategory/Group → 有效 box。 - 只保留 有效 box ≠ null 且
payee.Is1099Tracked的行。 - 依
(PayeeId, BoxCode)加總。 - 每位收款人:各 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)
- 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。 - 科目 → box 映射 — 擴充現有
expense-categories-page,在既有 990-line 下拉旁加一個「1099 Box」下拉(大類/子項目皆可設Form1099BoxId)。[valuePrimitive]="true"(feedback-kendo-value-primitive)。 - 支出表單(
expense-form-dialog)— 新增可選「1099 收款人」payee 選擇器(DropdownList、valuePrimitive)。 - 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:seedForm1099Box目錄 + 子項目→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 旗標正確。
Is1099Trackedgate;員工薪資科目被排除;同工(member-linked)收款人正確加總。- TIN 加解密 round-trip + 末四碼 + 遮罩。
10. 不在此範圍(已知缺口)
- IRS 電子申報(IRIS/FIRE)整合。
- 官方 Copy A / 1096 表單(v1 僅 Copy B + 資料匯出)。
- payroll / W-2(員工)。
- 既有
VendorName→ master 自動回填。 - 可設定門檻(v1 以常數)。
11. 驗收標準
- 可在收款人維護頁建立
Payee1099(含 W-9/TIN,TIN 遮罩),並可連結 Member。 - 支出可選填 1099 收款人;科目可設 1099 box;映射採子項目優先、大類 fallback、否則不列入。
- 年度報表依已付金額按收款人 × box 加總,正確標示 $600 門檻與缺 W-9;可下鑽明細。
- 完整 TIN 僅在具 Write 權限時可揭示;其餘遮罩為末四碼。
- 可產出收款人聯 Copy B 1099-NEC PDF(無 DevExpress 浮水印)與申報資料 CSV。
- 員工薪資科目即使付給被追蹤收款人,也不出現在 1099 報表;既有支出資料不受影響。