Files
ROLAC/docs/superpowers/specs/2026-05-28-giving-donation-tracking-design.md
T
Chris Chen 82b9744024 docs: add design spec for giving/donation tracking module
Manual giving module (Phase 1): giving category config, single-entry
giving, and keyboard-first Sunday offering batch entry (OfferingSession)
with finance-gated reconciliation. Client-buffered bulk submit (decision
B), lock-after-submit, Member-or-anonymous givers with inline quick-add.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-28 15:47:09 -07:00

15 KiB
Raw Blame History

Design Spec: 奉獻追蹤(手動)Giving / Donation Tracking

日期: 2026-05-28 作者: Chris Chen 狀態: 待核准 對應規劃: docs/PLANNING.md §3.6 / §3.6cdocs/DB_SCHEMA.md §7


1. 範疇

本 spec 只規劃**奉獻追蹤(手動)**模組,三項核心功能:

  1. 奉獻類型設定GivingCategory CRUD(什一 / 一般 / 特別 / 建堂 / 宣教…)
  2. 單筆奉獻記錄 — 單筆 Giving(現金 / 支票 / Zelle / PayPal 手動)
  3. 主日奉獻袋批次輸入OfferingSession 鍵盤優先快速錄入 + 對帳

明確不在本次範圍(之後另開 spec):

  • 支出追蹤 & 報銷、月結對帳表(屬另一模組)
  • 年度 IRS 收據(GivingReceiptPLANNING §3.7
  • 個人奉獻歷史查詢、奉獻摘要報表(PLANNING §3.9
  • 線上金流 Stripe / PayPal Checkout、定期奉獻(GivingRecurringSchedulePhase 4
  • 收據照片上傳(奉獻無此需求;blob 儲存尚未建置)

2. 關鍵決策摘要

# 決策 選擇
D1 批次錄入儲存策略 B — 純前端緩衝、最後一次 bulk submit。前端記憶體累積整批,提交時一次 POST 建立 Session + 所有 Giving(單一 transaction)。
D2 Session 鎖定 Submitted 後鎖定。修改需 finance 角色 reopen(回 Draft、整批載回前端)→ 編輯 → 重新提交整批替換。所有變動進 Audit Log。
D3 奇款人識別 giver 永遠是 Member 或匿名(維持 DB_SCHEMA 不變,不加 GiverName 自由欄)。非匿名又查無此人時,批次介面可快速新增 Member
D4 快速新增教友欄位 快速表單要求英文姓名FirstName_en / LastName_en),與現有 Member schema 必填一致;Status 預設 Visitor
D5 授權 奉獻所有寫入操作限 finance + super_admin(對應 PLANNING §4 權限矩陣)。

3. 資料模型

DB_SCHEMA.md §7,新增 3 個 entity,全部繼承 AuditableEntity(非軟刪除 —— 奉獻記錄為金錢帳目,刪除走實刪 + Audit Log;與 Member 的軟刪除不同)。

討論點(待確認): DB_SCHEMA §7 的 Giving / OfferingSession 未列 IsDeleted。本 spec 採實刪 + Audit。若日後需保留刪除痕跡,可改繼承 SoftDeleteEntity,但會影響月結加總邏輯(需排除 IsDeleted)。本次維持實刪。

3.1 GivingCategory(奉獻類型)

欄位 型別 說明
Id int PK
Name_en varchar(200) NOT NULL e.g. 'Tithe'
Name_zh varchar(200)? e.g. '什一奉獻'
Description_en varchar(500)?
Description_zh varchar(500)?
IsActive bool NOT NULL DEFAULT true DELETE = 軟停用(IsActive=false),不實刪,以保留歷史 Giving 的 FK
SortOrder int NOT NULL DEFAULT 0
+ AuditableEntity 欄位 CreatedAt/By, UpdatedAt/By

3.2 OfferingSession(主日奉獻袋批次)

欄位 型別 說明
Id int PK
SessionDate date NOT NULL UNIQUE 一個主日只有一個 Session
Status varchar(20) NOT NULL DEFAULT 'Draft' 'Draft' | 'Submitted' | 'Reconciled'
CashTotal decimal(18,2) NOT NULL DEFAULT 0 財務人工清點現金總額
CheckTotal decimal(18,2) NOT NULL DEFAULT 0 支票清點總額
SystemTotal decimal(18,2) NOT NULL DEFAULT 0 伺服器計算的明細加總
Difference decimal(18,2) NOT NULL DEFAULT 0 = (CashTotal + CheckTotal) SystemTotal,目標 0
Notes text?
SubmittedAt / SubmittedBy timestamp? / varchar(450)?
ReconciledAt / ReconciledBy timestamp? / varchar(450)?
+ AuditableEntity 欄位

Reconciled 狀態用途: 本次仍實作 submitDraft→Submitted)。Reconciled 作為日後月結對帳模組對接的終態保留欄位,本次 UI 不主動設定(只在 schema 預留)。

3.3 Giving(奉獻記錄)

欄位 型別 說明
Id int PK
MemberId int? FK → Members.Id;null = 匿名
GivingCategoryId int NOT NULL FK → GivingCategories.Id
OfferingSessionId int? FK → OfferingSessions.Id;null = 非批次單筆
Amount decimal(18,2) NOT NULL IRS 收據用金額
PaymentMethod varchar(20) NOT NULL 'Cash' | 'Check' | 'Zelle' | 'PayPal' | 'Other'
CheckNumber varchar(50)? PaymentMethod=Check 時必填
ZelleReferenceCode varchar(100)? PaymentMethod=Zelle
PayPalTransactionId varchar(100)? PaymentMethod=PayPal
GivingDate date NOT NULL 批次時 = SessionDate
IsAnonymous bool NOT NULL DEFAULT false
Notes varchar(500)?
+ AuditableEntity 欄位

Phase 4 欄位(GrossAmount / FeeAmount / StripePaymentIntentId / GivingRecurringScheduleId)本次不建,待線上金流模組再加,屆時為純新增欄位,不影響本次結構。

3.4 EF Core 設定(AppDbContext.OnModelCreating

  • 新增 3 個 DbSet<GivingCategory> / <OfferingSession> / <Giving>
  • fluent 設定:各 Name_* / Description_* maxlength;decimal 欄位 decimal(18,2)(沿用既有全域 decimal 慣例,或逐欄設定)。
  • OfferingSession:HasIndex(SessionDate).IsUnique()
  • Giving 索引(對應 DB_SCHEMA §17):
    • idx_givings_member_date(MemberId, GivingDate)
    • idx_givings_session(OfferingSessionId) filter not null
    • idx_givings_date(GivingDate)
  • FK 行為:
    • Giving.GivingCategoryIdRestrict(類型有奉獻時不可實刪,只能軟停用)
    • Giving.MemberIdSetNull
    • Giving.OfferingSessionIdCascade(刪 Session 連帶刪明細;一般不會發生,僅資料一致性)

3.5 SeedDbSeeder

新增 SeedGivingCategoriesAsync(沿用既有 code-based 風格,非 HasData):

1. Tithe / 什一奉獻
2. General Offering / 一般奉獻
3. Special Offering / 特別奉獻
4. Building Fund / 建堂基金
5. Mission / 宣教奉獻

3.6 Migration

一支 EF migration(如 AddGivingModule),涵蓋 3 表 + 索引 + FK。


4. API 介面

全部 [Authorize(Roles = "finance,super_admin")]。Controller 維持 thin → 委派 service,沿用既有 PagedResult<> 與錯誤轉換慣例(KeyNotFoundException→404,鎖定衝突→409)。

4.1 奉獻類型 GivingCategoriesController/api/giving-categories

GET    /api/giving-categories?includeInactive=false   全部(預設僅 active
POST   /api/giving-categories
PUT    /api/giving-categories/{id}
DELETE /api/giving-categories/{id}                     軟停用 IsActive=false

4.2 單筆奉獻 GivingsController/api/givings

GET    /api/givings?page&pageSize&search&categoryId&from&to   分頁列表
GET    /api/givings/{id}
POST   /api/givings                                    單筆,OfferingSessionId=null
PUT    /api/givings/{id}
DELETE /api/givings/{id}
  • search 比對 Member 姓名(中英)/ CheckNumber / Notes。
  • 屬於某 SessionOfferingSessionId != null)的 Giving,若該 Session 已 Submitted,單筆 PUT/DELETE 一律拒絕(409)—— 必須走 Session reopen

4.3 批次 OfferingSessionsController/api/offering-sessions

GET    /api/offering-sessions?page&pageSize&from&to    歷次 Session 列表
GET    /api/offering-sessions/{id}                      含完整明細 + 小計(reopen/檢視用)
POST   /api/offering-sessions                           一次建立整批(見 §6
POST   /api/offering-sessions/{id}/reopen               Submitted→Draftfinance,進 Audit
PUT    /api/offering-sessions/{id}                       整批替換明細 + 總額,重新 Submitted
GET    /api/offering-sessions/check-date?date=...        檢查該日期是否已有 Session
  • 批次教友搜尋:復用 GET /api/members?search=(已支援中英文,見 MemberService)。
  • 批次內快速新增教友:復用 POST /api/members(現有 CreateMemberRequest)。

POST /api/offering-sessions 請求形狀

{
  "sessionDate": "2026-05-31",
  "cashTotal": 1250.00,
  "checkTotal": 800.00,
  "notes": "...",
  "givings": [
    { "memberId": 12, "givingCategoryId": 1, "amount": 100.00,
      "paymentMethod": "Cash", "isAnonymous": false, "notes": null },
    { "memberId": null, "givingCategoryId": 2, "amount": 50.00,
      "paymentMethod": "Cash", "isAnonymous": true },
    { "memberId": 8, "givingCategoryId": 1, "amount": 300.00,
      "paymentMethod": "Check", "checkNumber": "1043" }
  ]
}

回傳建立後的 Session(含 id、伺服器算的 systemTotaldifferencestatus="Submitted")。


5. 後端 Service 層

3 個 serviceinterface + impl,沿用 MemberService 風格:建構式注入 AppDbContext + IHttpContextAccessorCurrentUserId、手動 DTO mapping)。

  • IGivingCategoryService — CRUD;DELETE = 設 IsActive=false
  • IGivingService — 單筆 CRUD + 分頁查詢;寫入前檢查所屬 Session 鎖定狀態。
  • IOfferingSessionService — 核心:
    • CreateAsync(request):單一 transaction 內建立 OfferingSessionStatus=Submitted+ 所有 Giving(OfferingSessionId 回填、GivingDate=SessionDate);伺服器重算 SystemTotal = Σ amountDifference;SubmittedAt/By 設值。SessionDate 撞 unique → 409。
    • GetByIdAsync(id):含明細投影。
    • ReopenAsync(id):僅 Submitted 可 reopen → Draft,清 SubmittedAt/By
    • ReplaceAsync(id, request):僅 Draft 可改;刪舊明細、插新明細、重算、回 Submitted
    • 鎖定守門:對已 Submitted 的修改丟 InvalidOperationException → controller 轉 409。
    • 金額一律以伺服器加總為準(不信任前端 systemTotal)。

6. 批次錄入流程(前端,核心體驗)

頁面:features/giving/pages/offering-session-page。採決策 D1(純前端緩衝)。

1. 選日期(預設今天)→ 呼叫 check-date 確認當日尚無 Session
2. 逐筆快速錄入(全部存在前端記憶體 buffer)
   ┌─────────────────────────────────────────────┐
   │ 搜尋教友:[即時下拉] ← 復用 GET /api/members  │
   │ 類型:[GivingCategory ▼]                     │
   │ 付款:(●現金 ○支票 ○Zelle ○PayPal)           │
   │   現金→信封號(選填) / 支票→支票號(必填)        │
   │ 金額:[$____]   備註:[____]                  │
   │ [匿名] [快速新增教友] [+ 新增(Enter)]         │
   │ ─────────────────────────────────────────    │
   │ 已錄入 N 筆 | 前端小計:$X,XXX.XX            │
   └─────────────────────────────────────────────┘
   - Tab 跳欄、Enter 新增;新增後焦點回搜尋框
   - 樂觀 UI:新增即在右側清單顯示、即時更新前端小計
   - 清單每列可即時改 / 刪(純前端操作)
3. 對帳步驟:輸入實點 CashTotal / CheckTotal
   → 前端顯示 Difference = (Cash+Check)  前端小計(目標 0
4. 「提交」→ 一次 POST /api/offering-sessions(整批)
   → 伺服器 transaction 建立 + 重算 + 回傳 → 鎖定
5. 編輯已提交:finance 在歷次列表點 reopen
   → GET 載回整批進 buffer → 編輯 → PUT 整批替換 → 重新 Submitted

鍵盤優先要點: 復用既有 focus-navigator / init-focus directives 與 currency-input / drop-down-list 共用元件。


7. 快速新增教友(批次介面內)

  • 搜尋無結果時出現「快速新增教友」→ inline 小 dialog。
  • 收最少欄位:FirstName_enLastName_en(必填,決策 D4FirstName_zh/LastName_zh(選填)、PhoneCell(選填)。
  • Status 預設 Visitor
  • 呼叫現有 POST /api/membersCreateMemberRequest)→ 取回 id → 自動帶回當前錄入列、焦點移至金額。

8. 前端 — 其餘頁面

依既有 feature 結構 features/giving/{pages,components,services,models},標準元件:standalone component + Kendo Grid/Inputs/Buttons/Dropdowns + RxJS + ApiConfigService

8.1 奉獻類型管理頁 giving-categories-page

Kendo Grid 列表 + 新增/編輯 dialog(雙欄 EN/中名稱、排序、啟用)。沿用 users/members 頁模式。

8.2 單筆奉獻頁 givings-page

Kendo Grid 列表(搜尋 / 日期區間 / 類型篩選 / 分頁)+ 單筆新增/編輯 dialog。付款方式切換時動態顯示支票號 / Zelle ref / PayPal txn 欄位。教友選擇復用 GET /api/members?search=

8.3 API service / model

每頁一支 *-api.service.ts + models/*.model.tsDTO interface,對齊後端 DTO 命名)。


9. 授權 & 導覽

  • 後端:所有奉獻 controller [Authorize(Roles="finance,super_admin")]
  • 前端:沿用既有 role-gated sidebar nav(commit bc67146 的 Administration 機制),在 Finance/Administration 區塊新增「奉獻」分組,內含三頁,僅 finance / super_admin 可見。
  • route guard 沿用 core/guards 既有角色守衛。

10. 測試

後端 ROLAC.API.Tests(沿用既有測試風格,InMemory provider):

  • GivingCategoryService:CRUD、DELETE 軟停用、有奉獻時類型不可實刪。
  • GivingService:單筆 CRUD、分頁/篩選、屬已 Submitted Session 的單筆寫入被拒(409)。
  • OfferingSessionService(重點):
    • 整批建立 → SystemTotal / Difference 伺服器重算正確(不信前端值)。
    • SessionDate 撞 unique → 409。
    • Submitted 修改被拒(鎖定守門)。
    • reopenreplace → 重新 Submitted 流程,明細正確替換。
    • 匿名筆(MemberId=null / IsAnonymous=true)正確記錄。
  • 授權:非 finance/super_admin 角色被 403。

11. 風險 / 待確認

# 項目 說明
R1 實刪 vs 軟刪除(§3 討論點) 本次 Giving/Session 採實刪 + Audit。若財務合規要求保留刪除痕跡,需改軟刪除並調整加總邏輯。待 Chris 確認。
R2 前端緩衝資料遺失(決策 B 的取捨) 提交前瀏覽器崩潰 → 整批未存的資料遺失。可選緩解:localStorage 暫存草稿(本次列為可選,不強制)。
R3 reopen 整批替換的 Audit 粒度 整批替換會產生「刪舊 N 筆 + 插新 M 筆」的 Audit 記錄,而非逐筆 diff。對帳追溯時需理解此模型。
R4 Audit Log 基礎設施 現有 AuditSaveChangesInterceptor 是否已涵蓋這些新 entity 的 Create/Update/Delete,需在實作時確認;AuditLog 表本身屬另一模組,若尚未建置需補。

12. 交付順序建議

  1. Entities + EF 設定 + Seed + migration
  2. Service 層(3 個)+ 單元測試
  3. Controllers + 授權
  4. 前端 models + api services
  5. 奉獻類型管理頁 → 單筆奉獻頁 → 批次錄入頁
  6. 導覽整合 + 端到端手動驗證