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

10 KiB
Raw Blame History

子專案 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

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-navproject-unified-system-headerfeedback-mobile-friendly-all-screensfeedback-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:新表 Payee1099sForm1099Boxes;新欄 Expenses.PayeeIdExpenseSubCategories.Form1099BoxIdExpenseCategoryGroups.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 報表;既有支出資料不受影響。