docs(1099): design spec for sub-project B — 1099 recipient tracking
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
# 子專案 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-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 報表;既有支出資料不受影響。
|
||||
Reference in New Issue
Block a user