Files
ROLAC/docs/DB_SCHEMA.md
T
Chris Chen ad276c01f3 docs(1099): document Payee1099s/Form1099Boxes schema and seed Form1099 permissions
- DB_SCHEMA.md §8: add Form1099Box catalog table, Payee1099 recipient master
  (with TIN at-rest encryption note), and new FK columns on Expenses /
  ExpenseSubCategories / ExpenseCategoryGroups; update TOC and Seed Data section
- DbSeeder.cs: grant Modules.Form1099 to finance (R/W/D), pastor (R), and
  board_member (R), mirroring the Form990Report + Disbursements pattern;
  idempotent (only inserts if row absent, never clobbers admin edits)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 17:56:09 -07:00

1269 lines
47 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.
# ROLAC — Database Schema Design
**版本:** v1.0 (2026-05-24)
**資料庫:** PostgreSQL 15+
**ORM:** Entity Framework Core 8 (Code-First Migrations)
**命名慣例:** PascalCase 表名 / 欄位名(EF Core 預設映射到 snake_case PostgreSQL 欄位)
---
## 目錄
1. [設計原則](#1-設計原則)
2. [BaseEntity 模式](#2-baseentity-模式)
3. [Phase 1 — Authentication & Identity](#3-authentication--identity)
4. [Phase 1 — Member Management](#4-member-management)
5. [Phase 1 — Ministry(事工部門)](#5-ministry-事工部門)
6. [Phase 1 — CMS](#6-cms)
7. [Phase 1 — Giving & Donations(奉獻)](#7-giving--donations-奉獻)
8. [Phase 1 — Expense Tracking(支出)](#8-expense-tracking-支出)
- [Form1099Box1099 欄位目錄)](#form1099box-irs-1099-報告欄位目錄)
- [Payee1099(收款人主檔)](#payee1099-1099-申報收款人主檔)
- [現有表新增欄位(1099 歸屬)](#現有表新增欄位1099-歸屬)
9. [Phase 1 — Prayer Requests(代禱)](#9-prayer-requests-代禱)
10. [Phase 1 — Audit Log](#10-audit-log)
11. [Phase 1 — Notifications](#11-notifications)
12. [Phase 2 — Service Roster(服事表)](#12-service-roster-服事表)
13. [Phase 2 — Sunday Attendance(主日出席)](#13-sunday-attendance-主日出席)
14. [Phase 2 — Cell Groups(小組)](#14-cell-groups-小組)
15. [Phase 3 — Ministry Budget(事工預算)](#15-ministry-budget-事工預算)
16. [Seed Data](#16-seed-data)
17. [Indexes](#17-indexes)
18. [EF Core 設定摘要](#18-ef-core-設定摘要)
---
## 1. 設計原則
| 原則 | 說明 |
|------|------|
| **Code-First** | 全部透過 EF Core Migration 建立,不手寫 DDL |
| **Soft Delete** | 重要實體用 `IsDeleted` 標記刪除,不實際刪除 DB 記錄 |
| **Audit Trail** | 每個可修改實體均記錄 `CreatedAt / CreatedBy / UpdatedAt / UpdatedBy` |
| **Bilingual Fields** | 需要雙語的欄位加 `_en` / `_zh` 後綴(如 `Name_en` / `Name_zh`|
| **Money** | 所有金額欄位使用 `decimal(18,2)` |
| **Phone** | `varchar(30)`(支援國際格式)|
| **EIN** | 不存入資料庫,由環境變數 `CHURCH_EIN` 提供 |
| **Azure Blob** | 圖片/PDF 只存 Blob 路徑(`varchar(500)`),不存 Base64 |
| **jsonb** | AuditLog 的 Before/After 用 PostgreSQL `jsonb` 儲存 |
| **IDs** | 一般實體用 `int` (SERIAL)User 繼承 ASP.NET Identity 使用 `string (Guid)` |
---
## 2. BaseEntity 模式
大部分實體繼承以下基礎類別(透過 EF Core TPH 或 owned type 實現):
```csharp
// 帶 Audit 的實體(可建立/修改)
public abstract class AuditableEntity
{
public DateTime CreatedAt { get; set; }
public string CreatedBy { get; set; } = null!; // FK → AppUser.Id
public DateTime UpdatedAt { get; set; }
public string UpdatedBy { get; set; } = null!; // FK → AppUser.Id
}
// 帶軟刪除的實體
public abstract class SoftDeleteEntity : AuditableEntity
{
public bool IsDeleted { get; set; } = false;
public DateTime? DeletedAt { get; set; }
public string? DeletedBy { get; set; } // FK → AppUser.Id
}
```
---
## 3. Authentication & Identity
### AppUser(繼承 IdentityUser
```
Table: AspNetUsers (ASP.NET Identity 預設名稱)
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | varchar(450) PK | Guid,由 Identity 管理 |
| UserName | varchar(256) | 使用者名稱(通常等同 Email)|
| NormalizedUserName | varchar(256) | 大寫版本,用於查詢 |
| Email | varchar(256) | 電子郵件 |
| NormalizedEmail | varchar(256) | |
| EmailConfirmed | bool | |
| PasswordHash | varchar(MAX) | |
| SecurityStamp | varchar(MAX) | |
| ConcurrencyStamp | varchar(MAX) | |
| PhoneNumber | varchar(256)? | |
| PhoneNumberConfirmed | bool | |
| TwoFactorEnabled | bool | |
| LockoutEnd | datetimeoffset? | |
| LockoutEnabled | bool | |
| AccessFailedCount | int | |
| **MemberId** | int? | FK → Member.Id(可為 null|
| **LanguagePreference** | varchar(10) | 'en' \| 'zh-TW'DEFAULT 'en' |
| **IsActive** | bool | DEFAULT true,停用帳號用 |
| **LastLoginAt** | timestamp? | 最後登入時間 |
| **CreatedAt** | timestamp | 帳號建立時間 |
> Identity 角色表(AspNetRoles, AspNetUserRoles)由 ASP.NET Identity 自動管理。
### AppRole(繼承 IdentityRole
```
Table: AspNetRoles
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | varchar(450) PK | Guid |
| Name | varchar(256) | 角色名稱(見下方列表)|
| NormalizedName | varchar(256) | |
| ConcurrencyStamp | varchar(MAX) | |
| **Description** | varchar(500)? | 角色說明 |
**預設角色(Seed):**
> ROLAC 為靈糧堂體制,無長老制。
| Name | 中文 | 說明 |
|------|------|------|
| super_admin | 系統管理員 | 所有權限 |
| pastor | 牧師 | 全覽教友與財務摘要 |
| board_member | 理事 | 教會治理委員,可查看財務摘要與教友概覽 |
| coworker_chair | 同工會主席 | 統籌各事工領袖,可管理服事與小組 |
| ministry_leader | 事工領袖 | 受 Ministry Scope 限制 |
| district_leader | 區長 | 管理轄下多個小組 |
| cell_leader | 小組長 | 僅限自身小組 |
| coworker | 同工 | 參與指定事工,可申請報銷 |
| finance | 財務同工 | 管理奉獻與支出 |
| secretary | 行政秘書 | 管理教友資料與排班 |
| worship_leader | 敬拜領袖 | 管理歌曲庫與歌單(Phase 暫緩)|
| member | 一般教友 | 查看個人資料與服事表 |
| visitor | 訪客 | 僅限公開頁面 |
### UserMinistryMinistry Scope — 用戶管理哪個事工)
```
Table: UserMinistries
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| UserId | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| MinistryId | int NOT NULL | FK → Ministries.Id |
| AssignedAt | timestamp NOT NULL | |
| AssignedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| **UNIQUE** | (UserId, MinistryId) | 一個用戶不重複指派同一事工 |
### UserDeviceFCM 推播 Token
```
Table: UserDevices
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| UserId | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| FcmToken | varchar(500) NOT NULL | Firebase Cloud Messaging token |
| Platform | varchar(20) NOT NULL | 'ios' \| 'android' \| 'web' |
| DeviceName | varchar(100)? | 裝置名稱(選填)|
| LastSeenAt | timestamp NOT NULL | |
| IsActive | bool NOT NULL DEFAULT true | |
| CreatedAt | timestamp NOT NULL | |
---
## 4. Member Management
### Member(教友資料)
```
Table: Members
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| FirstName_en | varchar(100) NOT NULL | 英文名 |
| LastName_en | varchar(100) NOT NULL | 英文姓 |
| FirstName_zh | varchar(100)? | 中文名 |
| LastName_zh | varchar(100)? | 中文姓 |
| Gender | varchar(10)? | 'M' \| 'F' \| 'Other' |
| DateOfBirth | date? | 生日 |
| BaptismDate | date? | 受洗日期 |
| BaptismChurch | varchar(200)? | 受洗教會 |
| Email | varchar(200)? | |
| PhoneCell | varchar(30)? | 手機 |
| PhoneHome | varchar(30)? | 家電 |
| Address | varchar(500)? | 地址 |
| City | varchar(100)? | |
| State | varchar(50)? | |
| ZipCode | varchar(20)? | |
| Country | varchar(100) NOT NULL DEFAULT 'USA' | |
| PhotoBlobPath | varchar(500)? | Azure Blob 路徑 |
| Status | varchar(20) NOT NULL DEFAULT 'Member' | 'Member' \| 'Visitor' \| 'Inactive' \| 'Former' |
| LanguagePreference | varchar(10) NOT NULL DEFAULT 'en' | 'en' \| 'zh-TW' |
| JoinDate | date? | 加入教會日期 |
| Notes | text? | 內部備注 |
| FamilyUnitId | int? | FK → FamilyUnits.Id |
| IsDeleted | bool NOT NULL DEFAULT false | 軟刪除 |
| DeletedAt | timestamp? | |
| DeletedBy | varchar(450)? | FK → AspNetUsers.Id |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| UpdatedAt | timestamp NOT NULL | |
| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
### FamilyUnit(家庭單元)
```
Table: FamilyUnits
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| FamilyName_en | varchar(200)? | 家庭英文名(如 "Chang Family"|
| FamilyName_zh | varchar(200)? | 家庭中文名(如「張家」)|
| Notes | text? | |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| UpdatedAt | timestamp NOT NULL | |
| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
> 一個 FamilyUnit 可有多個 MembersOne-to-Many via `Member.FamilyUnitId`
### MemberMinistry(教友服事事工關聯 — M:N)
```
Table: MemberMinistries
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| MemberId | int NOT NULL | FK → Members.Id |
| MinistryId | int NOT NULL | FK → Ministries.Id |
| MinistryRole | varchar(50)? | 在事工中的角色(Leader/Member/Coordinator|
| JoinedAt | date? | |
| Notes | varchar(200)? | |
| **UNIQUE** | (MemberId, MinistryId) | |
### MemberTag(教友標籤)
```
Table: MemberTags
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| MemberId | int NOT NULL | FK → Members.Id |
| Tag | varchar(100) NOT NULL | e.g., 'NewBeliever', 'Volunteer', 'Youth' |
| **UNIQUE** | (MemberId, Tag) | |
---
## 5. Ministry(事工部門)
```
Table: Ministries
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| Name_en | varchar(200) NOT NULL | |
| Name_zh | varchar(200)? | |
| Description_en | text? | |
| Description_zh | text? | |
| SortOrder | int NOT NULL DEFAULT 0 | 顯示排序 |
| IsActive | bool NOT NULL DEFAULT true | |
| **DefaultFunctionalClass** | varchar(20) NOT NULL DEFAULT 'Program' | IRS Form 990 功能性費用分類:'Program' \| 'ManagementGeneral' \| 'Fundraising'。SeedAdministration → 'ManagementGeneral',其餘 → 'Program' |
---
## 6. CMS
### CmsPage(靜態頁面:關於我們、異象等)
```
Table: CmsPages
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| Slug | varchar(100) UNIQUE NOT NULL | URL 識別碼,如 'about', 'vision' |
| Title_en | varchar(300) NOT NULL | |
| Title_zh | varchar(300)? | |
| Body_en | text? | Markdown 或 HTML |
| Body_zh | text? | |
| IsPublished | bool NOT NULL DEFAULT false | |
| PublishedAt | timestamp? | |
| UpdatedAt | timestamp NOT NULL | |
| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
### Announcement(消息公告)
```
Table: Announcements
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| Title_en | varchar(300) NOT NULL | |
| Title_zh | varchar(300)? | |
| Body_en | text NOT NULL | |
| Body_zh | text? | |
| PinnedUntil | date? | null = 不置頂 |
| ScheduledAt | timestamp? | null = 立即發佈 |
| IsPublished | bool NOT NULL DEFAULT false | |
| PublishedAt | timestamp? | |
| ImageBlobPath | varchar(500)? | 封面圖 Azure Blob 路徑 |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| UpdatedAt | timestamp NOT NULL | |
| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
### SermonVideo(講道影片 — YouTube 嵌入)
```
Table: SermonVideos
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| Title_en | varchar(300) NOT NULL | |
| Title_zh | varchar(300)? | |
| YouTubeVideoId | varchar(50) NOT NULL | YouTube 影片 ID(非完整 URL|
| PreacherName | varchar(200)? | 講員姓名 |
| SermonDate | date NOT NULL | 講道日期 |
| Description_en | text? | |
| Description_zh | text? | |
| IsPublished | bool NOT NULL DEFAULT true | |
| SortOrder | int NOT NULL DEFAULT 0 | 顯示排序(新到舊)|
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
### ContactInquiry(聯絡表單)
```
Table: ContactInquiries
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| Name | varchar(200) NOT NULL | |
| Email | varchar(200) NOT NULL | |
| Phone | varchar(30)? | |
| Subject | varchar(300)? | |
| Message | text NOT NULL | |
| Language | varchar(10) NOT NULL DEFAULT 'en' | 表單填寫語言 |
| IsRead | bool NOT NULL DEFAULT false | |
| ReadAt | timestamp? | |
| ReadBy | varchar(450)? | FK → AspNetUsers.Id |
| AssignedTo | varchar(450)? | FK → AspNetUsers.Id |
| InternalNotes | text? | 後台同工備注 |
| CreatedAt | timestamp NOT NULL DEFAULT now() | |
---
## 7. Giving & Donations(奉獻)
### GivingCategory(奉獻類型)
```
Table: GivingCategories
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| Name_en | varchar(200) NOT NULL | e.g., 'Tithe', 'General Offering', 'Building Fund' |
| Name_zh | varchar(200)? | e.g., '什一奉獻', '一般奉獻', '建堂基金' |
| Description_en | varchar(500)? | |
| Description_zh | varchar(500)? | |
| IsActive | bool NOT NULL DEFAULT true | |
| SortOrder | int NOT NULL DEFAULT 0 | |
### OfferingSession(主日奉獻袋批次作業)
```
Table: OfferingSessions
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| SessionDate | date NOT NULL | 主日日期 |
| 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 |
| Notes | text? | |
| SubmittedAt | timestamp? | |
| SubmittedBy | varchar(450)? | FK → AspNetUsers.Id |
| ReconciledAt | timestamp? | |
| ReconciledBy | varchar(450)? | FK → AspNetUsers.Id |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| UpdatedAt | timestamp NOT NULL | |
| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| **UNIQUE** | (SessionDate) | 一個主日只有一個 Session |
> **鍵盤優先 UI 設計:** 財務同工 Tab/Enter 跳欄逐筆輸入,每筆加入後右側即時更新加總;最後輸入 CashTotal / CheckTotal 人工清點金額,系統計算 Difference,目標為零後點「提交」。
### Giving(奉獻記錄)
```
Table: Givings
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| MemberId | int? | FK → Members.Idnull = 匿名 |
| GivingCategoryId | int NOT NULL | FK → GivingCategories.Id |
| OfferingSessionId | int? | FK → OfferingSessions.Idnull = 非批次單筆 |
| Amount | decimal(18,2) NOT NULL | 用於 IRS 收據的金額(PayPal 用 NetAmount|
| GrossAmount | decimal(18,2)? | PayPal 手續費前總額 |
| FeeAmount | decimal(18,2)? | PayPal/Stripe 手續費 |
| PaymentMethod | varchar(20) NOT NULL | 'Cash' \| 'Check' \| 'Zelle' \| 'PayPal' \| 'Stripe' \| 'Other' |
| CheckNumber | varchar(50)? | 支票號碼 |
| ZelleReferenceCode | varchar(100)? | Zelle 參考碼(手動輸入)|
| PayPalTransactionId | varchar(100)? | PayPal 交易 ID |
| StripePaymentIntentId | varchar(200)? | Stripe PaymentIntent IDPhase 4|
| GivingDate | date NOT NULL | 奉獻日期 |
| IsAnonymous | bool NOT NULL DEFAULT false | |
| Notes | varchar(500)? | |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| UpdatedAt | timestamp NOT NULL | |
| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
> **IRS 規則:** 匿名奉獻(`IsAnonymous = true` 或 `MemberId = null`)不計入個人年度收據。
> **PayPal** `Amount` = `GrossAmount` - `FeeAmount`Net Amount 才用於 IRS)。
### GivingReceipt(年度 IRS 奉獻收據)
```
Table: GivingReceipts
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| MemberId | int NOT NULL | FK → Members.Id |
| FiscalYear | int NOT NULL | 年份(如 2026|
| TotalAmount | decimal(18,2) NOT NULL | 該年度奉獻總額 |
| GivingCount | int NOT NULL | 奉獻筆數 |
| PdfBlobPath | varchar(500)? | Azure Blob 路徑(receipts/{year}/{memberId}.pdf|
| GeneratedAt | timestamp? | PDF 產生時間 |
| SentAt | timestamp? | Email 寄出時間 |
| SentToEmail | varchar(200)? | 寄送 Email 位址 |
| IsVoided | bool NOT NULL DEFAULT false | 作廢(保留 PDF,不再有效)|
| VoidReason | varchar(500)? | |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| **UNIQUE** | (MemberId, FiscalYear) | 每人每年只一份正式收據 |
### GivingRecurringSchedule(定期奉獻排程 — Phase 4
```
Table: GivingRecurringSchedules
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| MemberId | int NOT NULL | FK → Members.Id |
| GivingCategoryId | int NOT NULL | FK → GivingCategories.Id |
| Amount | decimal(18,2) NOT NULL | |
| Frequency | varchar(20) NOT NULL | 'Weekly' \| 'BiWeekly' \| 'Monthly' |
| StripeSubscriptionId | varchar(200)? | Stripe Subscription ID |
| StartDate | date NOT NULL | |
| EndDate | date? | null = 持續進行 |
| IsActive | bool NOT NULL DEFAULT true | |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
---
## 8. Expense Tracking(支出)
### Form990ExpenseLinesIRS Form 990 Part IX 自然費用科目)
```
Table: Form990ExpenseLines
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| LineCode | varchar(10) NOT NULL UNIQUE | Part IX 行號,如 "7"、"11b"、"16"、"24" |
| Name_en | varchar(200) NOT NULL | 英文科目名稱 |
| Name_zh | varchar(200)? | 中文科目名稱 |
| SortOrder | int NOT NULL | 排序 |
| IsActive | bool NOT NULL DEFAULT true | |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| UpdatedAt | timestamp NOT NULL | |
| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
> **IRS Form 990 Part IX 說明:** Part IX「Statement of Functional Expenses」要求將每筆支出依 IRS 自然費用科目(LineCode)分類,同時按功能性費用分類(Program / Management & General / Fundraising)橫向彙總,生成 Form 990 Schedule Part IX 報表。
### ExpenseCategoryGroup(支出大類)
```
Table: ExpenseCategoryGroups
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| Name_en | varchar(200) NOT NULL | |
| Name_zh | varchar(200)? | |
| SortOrder | int NOT NULL DEFAULT 0 | |
| IsActive | bool NOT NULL DEFAULT true | |
| **Form990LineId** | int? | FK → Form990ExpenseLines.IdON DELETE SET NULL。大類層級預設 990 行號;Seed 預設為行 "24"(其他費用)|
**Seed 大類(14 個):**
| Id | Name_en | Name_zh |
|----|---------|---------|
| 1 | Equipment | 設備 |
| 2 | Consumables | 消耗品 |
| 3 | Food & Beverage | 餐飲 |
| 4 | Training | 培訓 |
| 5 | Materials | 教材 |
| 6 | Facility | 場地 |
| 7 | Printing | 印刷 |
| 8 | Missions | 宣教 |
| 9 | Benevolence | 關懷救助 |
| 10 | Other | 其他 |
| 11 | Personnel | 人事 |
| 12 | Professional Services | 專業服務 |
| 13 | Information Technology | 資訊科技 |
| 14 | Finance & Banking | 財務與銀行 |
### ExpenseSubCategory(支出子項目)
```
Table: ExpenseSubCategories
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| GroupId | int NOT NULL | FK → ExpenseCategoryGroups.Id |
| Name_en | varchar(200) NOT NULL | |
| Name_zh | varchar(200)? | |
| SortOrder | int NOT NULL DEFAULT 0 | |
| IsActive | bool NOT NULL DEFAULT true | |
| **Form990LineId** | int? | FK → Form990ExpenseLines.IdON DELETE SET NULL。子項目層級 990 行號(優先於大類值)|
> **有效 990 行號解析順序:** `SubCategory.Form990LineId ?? Group.Form990LineId ?? "24"`(先取子項目的行號;若為 null 則取大類的行號;仍為 null 則視為行 "24" — Other Expenses)。
**Seed 子項目(完整種子):**
| GroupId | Name_en | Name_zh |
|---------|---------|---------|
| 1 Equipment | Purchase | 購置 |
| 1 Equipment | Rental | 租借 |
| 1 Equipment | Maintenance & Repair | 維修 |
| 2 Consumables | Batteries | 電池 |
| 2 Consumables | Accessories | 配件 |
| 2 Consumables | Cleaning Supplies | 清潔用品 |
| 2 Consumables | Office Supplies | 文具 |
| 3 Food & Beverage | Catering | 出餐費用 |
| 3 Food & Beverage | Food Ingredients | 食材採購 |
| 3 Food & Beverage | Utensils | 器具 |
| 3 Food & Beverage | Disposable Tableware | 一次性餐具 |
| 4 Training | Course Fees | 課程費用 |
| 4 Training | Books | 書籍 |
| 4 Training | Conference | 研討會 |
| 4 Training | Travel | 差旅 |
| 5 Materials | Curriculum Printing | 教材印刷 |
| 5 Materials | Craft Supplies | 手工材料 |
| 5 Materials | Copyright & Licensing | 版權購買 |
| 6 Facility | Rent | 場地租金 |
| 6 Facility | Utilities | 水電 |
| 6 Facility | Property Insurance | 財產保險 |
| 6 Facility | Decoration | 裝飾 |
| 6 Facility | Repairs & Maintenance | 修繕維護 |
| 7 Printing | Bulletins | 週報 |
| 7 Printing | Order of Service | 程序單 |
| 7 Printing | Posters | 海報 |
| 7 Printing | Advertising & Promotion | 廣告與推廣 |
| 8 Missions | Offering Transfer | 奉獻轉帳 |
| 8 Missions | Missionary Support | 宣教士支援 |
| 8 Missions | Foreign Missions Support | 海外宣教支援 |
| 8 Missions | Travel | 差旅 |
| 9 Benevolence | Emergency Aid | 急難救助 |
| 9 Benevolence | Condolence Gifts | 慰問禮品 |
| 9 Benevolence | Visit Expenses | 探訪費用 |
| 10 Other | Miscellaneous | 雜支 |
| 10 Other | Gifts | 禮品 |
| 11 Personnel | Salary & Wages | 薪資 |
| 11 Personnel | Officer / Key Employee Compensation | 主任/關鍵員工薪酬 |
| 11 Personnel | Payroll Taxes | 薪資稅費 |
| 11 Personnel | Employee Benefits | 員工福利 |
| 11 Personnel | Retirement / Pension | 退休/養老金 |
| 11 Personnel | Workers Compensation | 勞工保險 |
| 11 Personnel | Honorarium | 酬庸 |
| 11 Personnel | Staff Training | 同工進修 |
| 11 Personnel | Contract Labor | 外包勞務 |
> **備注:** `Facility > 財產保險` 指建築物/場地責任險;員工健保、團體保險等歸 `Personnel > 員工福利`。同工代墊報銷依**實際購買物**選大類,不歸人事。
> **修繕歸類:** 建物日常修繕(水電、油漆等)歸 `Facility > 修繕維護`990 第16行 Occupancy);**設備**維修歸 `Equipment > Maintenance & Repair`990 第13行)。重大資本改良(整修屋頂、大規模裝修)應資本化、走折舊(第22行,目前未實作)。
> **子項目更名說明:** `Food & Beverage > Consumables`(消耗品)更名為 `Disposable Tableware`(一次性餐具)以消除與大類同名的歧義;`Materials > Printing`(印刷費用)更名為 `Curriculum Printing`(教材印刷)以與 Printing 大類區分。
### Expense(支出記錄)
```
Table: Expenses
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| MinistryId | int NOT NULL | FK → Ministries.Id |
| CategoryGroupId | int NOT NULL | FK → ExpenseCategoryGroups.Id |
| SubCategoryId | int NOT NULL | FK → ExpenseSubCategories.Id |
| Type | varchar(30) NOT NULL | 'VendorPayment' \| 'StaffReimbursement' |
| Status | varchar(30) NOT NULL DEFAULT 'Draft' | 見下方說明 |
| Amount | decimal(18,2) NOT NULL | |
| Description | varchar(500) NOT NULL | 費用說明 |
| VendorName | varchar(200)? | 廠商名稱(VendorPayment 用)|
| CheckNumber | varchar(50)? | 付款支票號碼 |
| ExpenseDate | date NOT NULL | 費用日期 |
| ReceiptBlobPath | varchar(500)? | 收據照片 Azure Blob 路徑 |
| Notes | text? | |
| SubmittedBy | varchar(450)? | FK → AspNetUsers.Id(申請報銷的同工)|
| SubmittedAt | timestamp? | |
| ReviewedBy | varchar(450)? | FK → AspNetUsers.Id(財務審核人)|
| ReviewedAt | timestamp? | |
| ReviewNotes | varchar(500)? | 審核備注 |
| PaidAt | timestamp? | 標記已付款時間 |
| PaidBy | varchar(450)? | FK → AspNetUsers.Id |
| **FunctionalClass** | varchar(20)? | IRS Form 990 功能性費用分類個別覆寫:'Program' \| 'ManagementGeneral' \| 'Fundraising'null = 繼承 Ministry.DefaultFunctionalClass |
| IsDeleted | bool NOT NULL DEFAULT false | |
| DeletedAt | timestamp? | |
| DeletedBy | varchar(450)? | FK → AspNetUsers.Id |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| UpdatedAt | timestamp NOT NULL | |
| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
> **有效功能性費用分類解析順序:** `Expense.FunctionalClass ?? Ministry.DefaultFunctionalClass ?? 'Program'`(先取費用記錄的個別覆寫值;若為 null 則取所屬事工的預設值;仍為 null 則視為 'Program')。
**Status 工作流程:**
```
VendorPayment:
財務直接建立 → Status = 'Paid'(無需審核)
StaffReimbursement:
同工提交 → 'Draft'
提交審核 → 'PendingApproval'
財務審核通過 → 'Approved'
標記已還款 → 'Paid'
財務拒絕 → 'Rejected'
```
### MonthlyStatement(月底對帳表)
```
Table: MonthlyStatements
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| Year | int NOT NULL | |
| Month | int NOT NULL | 112 |
| OpeningBalance | decimal(18,2) NOT NULL | 期初餘額(手輸)|
| TotalGiving | decimal(18,2) NOT NULL | 系統計算:本月奉獻合計 |
| TotalOtherIncome | decimal(18,2) NOT NULL DEFAULT 0 | 其他收入(手輸)|
| TotalExpenses | decimal(18,2) NOT NULL | 系統計算:本月已付支出合計 |
| CalculatedClosingBalance | decimal(18,2) NOT NULL | = OpeningBalance + TotalGiving + TotalOtherIncome TotalExpenses |
| BankStatementBalance | decimal(18,2) NOT NULL | 銀行對帳單期末餘額(手輸)|
| Difference | decimal(18,2) NOT NULL | = CalculatedClosingBalance BankStatementBalance(目標 = 0|
| Notes | text? | |
| IsFinalized | bool NOT NULL DEFAULT false | 定稿後鎖定,不允許修改 |
| FinalizedAt | timestamp? | |
| FinalizedBy | varchar(450)? | FK → AspNetUsers.Id |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| UpdatedAt | timestamp NOT NULL | |
| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| **UNIQUE** | (Year, Month) | 每個月只有一份月結報表 |
### Form1099BoxIRS 1099 報告欄位目錄)
```
Table: Form1099Boxes
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| BoxCode | varchar(20) NOT NULL UNIQUE | 欄位代碼,如 "NEC-1"、"MISC-1" |
| Name_en | varchar(200) NOT NULL | 英文欄位名稱 |
| Name_zh | varchar(200)? | 中文欄位名稱 |
| FormType | varchar(20) NOT NULL | '1099-NEC' \| '1099-MISC' |
| SortOrder | int NOT NULL DEFAULT 0 | 顯示排序 |
| IsActive | bool NOT NULL DEFAULT true | |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| UpdatedAt | timestamp NOT NULL | |
| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
> **說明:** IRS 1099 申報欄位目錄(catalog)。Seed 預設兩個欄位:`NEC-1`Nonemployee compensation — 非員工報酬,1099-NEC 第 1 欄)與 `MISC-1`Rents — 租金,1099-MISC 第 1 欄)。此表為唯讀參考資料,僅透過 seed 管理;新增欄位須更新 seed 並重新執行 migration。
### Payee10991099 申報收款人主檔)
```
Table: Payee1099s
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| LegalName | varchar(200) NOT NULL | IRS 法定全名(個人或公司)|
| DisplayName | varchar(200)? | 顯示用簡稱(選填)|
| MemberId | int? | FK → Members.IdON DELETE SET NULL。收款人同時為教友時可選填關聯 |
| TaxClassification | varchar(50) NOT NULL | 稅務分類,如 'Individual'、'SoleProprietor'、'Corporation'、'Partnership' 等 |
| Is1099Tracked | bool NOT NULL DEFAULT true | 是否需要申報 1099 |
| TinType | varchar(10)? | 'SSN' \| 'EIN'null = 尚未收到 W-9 |
| **TinEncrypted** | varchar(MAX)? | **TIN 加密密文(使用 ASP.NET Data Protection API 加密靜態儲存,明文永不入庫)** |
| **TinLast4** | varchar(4)? | **TIN 末四碼明文(僅供遮罩顯示用,如 \*\*\*-\*\*-1234** |
| AddressLine1 | varchar(200)? | |
| AddressLine2 | varchar(200)? | |
| City | varchar(100)? | |
| State | varchar(50)? | |
| Zip | varchar(20)? | |
| Email | varchar(200)? | |
| Phone | varchar(30)? | |
| W9Status | varchar(20) NOT NULL DEFAULT 'Missing' | 'Missing' \| 'Requested' \| 'OnFile' \| 'Expired' |
| W9ReceivedDate | date? | W-9 文件收到日期 |
| W9BlobPath | varchar(500)? | 上傳的 W-9 文件 Azure Blob 路徑 |
| IsActive | bool NOT NULL DEFAULT true | |
| Notes | text? | 內部備注 |
| IsDeleted | bool NOT NULL DEFAULT false | 軟刪除 |
| DeletedAt | timestamp? | |
| DeletedBy | varchar(450)? | FK → AspNetUsers.Id |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| UpdatedAt | timestamp NOT NULL | |
| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
> **TIN 靜態加密(Encryption at Rest):** 納稅識別碼(SSN / EIN)屬高敏感個人資料。`TinEncrypted` 欄位儲存使用 ASP.NET Data Protection API`IDataProtector`)加密後的密文;`TinLast4` 僅儲存末四碼明文供前端遮罩顯示(\*\*\*-\*\*-XXXX)。明文 TIN 永遠不寫入資料庫,也不出現在 Audit Log 快照中。
### 現有表新增欄位(1099 歸屬)
以下欄位由 1099 功能新增至現有表,透過 EF Core Migration 套用:
**`Expenses`(新增欄位)**
| 欄位 | 型別 | 說明 |
|------|------|------|
| **PayeeId** | int? | FK → Payee1099s.IdON DELETE SET NULL。費用標題層級 1099 收款人歸屬;null = 不申報 1099 |
**`ExpenseSubCategories`(新增欄位)**
| 欄位 | 型別 | 說明 |
|------|------|------|
| **Form1099BoxId** | int? | FK → Form1099Boxes.IdON DELETE SET NULL。子項目層級 1099 申報欄位映射(優先於大類值)|
**`ExpenseCategoryGroups`(新增欄位)**
| 欄位 | 型別 | 說明 |
|------|------|------|
| **Form1099BoxId** | int? | FK → Form1099Boxes.IdON DELETE SET NULL。大類層級 1099 申報欄位備援映射 |
> **有效 1099 欄位解析順序:** `SubCategory.Form1099BoxId ?? Group.Form1099BoxId ?? null`(先取子項目欄位;若為 null 則取大類欄位;仍為 null = 該費用不需申報 1099)。此解析邏輯與 Form 990 行號解析(`SubCategory.Form990LineId ?? Group.Form990LineId ?? "24"`)平行,但語意不同:1099 的 null 代表「不申報」,而 990 的 null 會回退至行 "24"(其他費用)。
---
## 9. Prayer Requests(代禱)
### PrayerRequest(代禱事項)
```
Table: PrayerRequests
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| RequestedByMemberId | int NOT NULL | FK → Members.Id(提交人)|
| Title | varchar(300) NOT NULL | |
| Content | text NOT NULL | |
| Visibility | varchar(20) NOT NULL | 'Private' \| 'CellGroup' \| 'AllLeaders' \| 'Public' |
| IsAnswered | bool NOT NULL DEFAULT false | 已應允 |
| AnsweredAt | timestamp? | |
| ExpiresAt | date? | 可選到期日(過期不顯示)|
| IsDeleted | bool NOT NULL DEFAULT false | |
| DeletedAt | timestamp? | |
| DeletedBy | varchar(450)? | FK → AspNetUsers.Id |
| CreatedAt | timestamp NOT NULL | |
| UpdatedAt | timestamp NOT NULL | |
**Visibility 規則:**
| Visibility | 可見對象 |
|------------|---------|
| Private | 僅本人 + pastor + super_admin |
| CellGroup | 本人所在小組成員 + cell_leader + district_leader + pastor |
| AllLeaders | ministry_leader / district_leader / coworker_chair / board_member / pastor 及以上 |
| Public | 全體教友(登入後可見)|
### PrayerFollow(代禱跟進記錄)
```
Table: PrayerFollows
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| PrayerRequestId | int NOT NULL | FK → PrayerRequests.Id |
| FollowedByUserId | varchar(450) NOT NULL | FK → AspNetUsers.Id(牧者/組長)|
| Note | text? | 跟進備注 |
| CreatedAt | timestamp NOT NULL | |
---
## 10. Audit Log
### AuditLog(稽核記錄 — 不可修改/刪除)
```
Table: AuditLogs
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | bigint PK | 使用 bigint(記錄量大)|
| UserId | varchar(450)? | FK → AspNetUsers.Idnull = 系統自動操作 |
| Action | varchar(50) NOT NULL | 'Create' \| 'Update' \| 'Delete' \| 'Login' \| 'Logout' \| 'Export' \| 'ViewSensitive' |
| EntityType | varchar(100) NOT NULL | 實體名稱(如 'Member', 'Giving', 'Expense'|
| EntityId | varchar(100)? | 受影響實體的主鍵值 |
| OldValues | jsonb? | 修改前的 JSON 快照 |
| NewValues | jsonb? | 修改後的 JSON 快照 |
| IpAddress | varchar(45)? | IPv4 或 IPv6 |
| UserAgent | varchar(500)? | 瀏覽器/App 資訊 |
| CreatedAt | timestamp NOT NULL DEFAULT now() | |
> **重要:** AuditLog 永遠只有 INSERT,永遠不 UPDATE 或 DELETE。
---
## 11. Notifications
### NotificationLog(通知發送記錄)
```
Table: NotificationLogs
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| UserId | varchar(450)? | FK → AspNetUsers.Id |
| MemberId | int? | FK → Members.Id |
| Channel | varchar(20) NOT NULL | 'Email' \| 'Push' \| 'SMS' \| 'InApp' |
| TemplateKey | varchar(100) NOT NULL | 通知模板 Key(如 'giving.receipt.sent'|
| Subject | varchar(300)? | Email 主旨 |
| SentToAddress | varchar(200)? | Email / 手機號 / FCM Token |
| Status | varchar(20) NOT NULL | 'Sent' \| 'Failed' \| 'Queued' |
| ErrorMessage | varchar(500)? | 失敗原因 |
| RelatedEntityType | varchar(100)? | 關聯實體類型 |
| RelatedEntityId | varchar(100)? | 關聯實體 ID |
| CreatedAt | timestamp NOT NULL DEFAULT now() | |
---
## 12. Service Roster(服事表 — Phase 2
### ServiceSlot(服事項目定義)
```
Table: ServiceSlots
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| MinistryId | int NOT NULL | FK → Ministries.Id |
| Name_en | varchar(200) NOT NULL | e.g., 'Worship Leader' |
| Name_zh | varchar(200)? | e.g., '敬拜帶領' |
| RequiredCount | int NOT NULL DEFAULT 1 | 每次需要幾人 |
| IsActive | bool NOT NULL DEFAULT true | |
| SortOrder | int NOT NULL DEFAULT 0 | |
**Seed 服事項目(16 個主日服事槽,參見 PLANNING.md §3.4):**
| MinistryId | Name_en | Name_zh |
|------------|---------|---------|
| 2 Preaching | Preacher | 講員 |
| 3 Emcee | Emcee | 司會 |
| 4 Worship | Worship Leader | 敬拜帶領 |
| 4 Worship | Keyboard / Piano | 鍵盤/鋼琴 |
| 4 Worship | Guitar | 吉他 |
| 4 Worship | Bass | 貝斯 |
| 4 Worship | Drums | 爵士鼓 |
| 4 Worship | Vocalist | 詩班 |
| 5 PPT/Media | PPT Operator | PPT 操作 |
| 5 PPT/Media | Livestream | 直播 |
| 6 Sound | Sound Engineer | 音控 |
| 7 Facility | Setup Lead | 場地佈置組長 |
| 7 Facility | Setup Team | 場地佈置組員 |
| 8 Hospitality | Greeter | 招待接待 |
| 9 Children | Children's Teacher | 兒童老師 |
| 10 Catering | Agape Meal Coord. | 愛宴負責人 |
### ServiceAssignment(服事排班記錄)
```
Table: ServiceAssignments
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| ServiceSlotId | int NOT NULL | FK → ServiceSlots.Id |
| MemberId | int NOT NULL | FK → Members.Id |
| ServiceDate | date NOT NULL | 服事日期(通常為主日)|
| Status | varchar(20) NOT NULL DEFAULT 'Scheduled' | 'Scheduled' \| 'Confirmed' \| 'Absent' \| 'Replaced' |
| ReplacedByMemberId | int? | FK → Members.Id(替補人選)|
| Notes | varchar(200)? | |
| ReminderSentAt | timestamp? | 提醒通知發送時間 |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| UpdatedAt | timestamp NOT NULL | |
| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
---
## 13. Sunday Attendance(主日出席 — Phase 2
```
Table: SundayAttendances
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| AttendanceDate | date NOT NULL UNIQUE | 主日日期 |
| AdultCount | int NOT NULL DEFAULT 0 | 大人 |
| TeenagerCount | int NOT NULL DEFAULT 0 | 青少年 |
| ChildrenCount | int NOT NULL DEFAULT 0 | 兒童 |
| TotalCount | int NOT NULL | 計算欄位(= Adult + Teenager + Children|
| Notes | varchar(500)? | |
| RecordedByUserId | varchar(450) NOT NULL | FK → AspNetUsers.Id(行政秘書)|
| CreatedAt | timestamp NOT NULL | |
| UpdatedAt | timestamp NOT NULL | |
---
## 14. Cell Groups(小組 — Phase 2
### CellGroup(小組)
```
Table: CellGroups
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| Name_en | varchar(200) NOT NULL | |
| Name_zh | varchar(200)? | |
| ParentGroupId | int? | FK → CellGroups.Id(巢狀小組)|
| LeaderMemberId | int? | FK → Members.Id(組長)|
| CoLeaderMemberId | int? | FK → Members.Id(副組長)|
| MeetingDay | varchar(20)? | 'Monday' \| 'Tuesday' \| ... |
| MeetingTime | time? | |
| MeetingLocation | varchar(200)? | |
| Description_en | text? | |
| Description_zh | text? | |
| IsActive | bool NOT NULL DEFAULT true | |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| UpdatedAt | timestamp NOT NULL | |
| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
### CellGroupMembership(小組成員 — M:N
```
Table: CellGroupMemberships
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| CellGroupId | int NOT NULL | FK → CellGroups.Id |
| MemberId | int NOT NULL | FK → Members.Id |
| JoinedAt | date? | |
| LeftAt | date? | null = 仍在小組中 |
| IsActive | bool NOT NULL DEFAULT true | |
| **UNIQUE** | (CellGroupId, MemberId) WHERE IsActive = true | |
### CellGroupMeeting(小組聚會記錄)
```
Table: CellGroupMeetings
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| CellGroupId | int NOT NULL | FK → CellGroups.Id |
| MeetingDate | date NOT NULL | |
| Topic | varchar(300)? | 聚會主題 |
| Notes | text? | |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
### CellGroupAttendance(小組出席 — 個人層級)
```
Table: CellGroupAttendances
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| MeetingId | int NOT NULL | FK → CellGroupMeetings.Id |
| MemberId | int NOT NULL | FK → Members.Id |
| IsPresent | bool NOT NULL DEFAULT true | 出席 / 缺席 |
| Notes | varchar(200)? | |
| **UNIQUE** | (MeetingId, MemberId) | |
---
## 15. Ministry Budget(事工預算 — Phase 3
```
Table: MinistryBudgets
```
| 欄位 | 型別 | 說明 |
|------|------|------|
| Id | int PK | |
| MinistryId | int NOT NULL | FK → Ministries.Id |
| CategoryGroupId | int NOT NULL | FK → ExpenseCategoryGroups.Id |
| SubCategoryId | int? | FK → ExpenseSubCategories.Idnull = 整個大類的預算 |
| FiscalYear | int NOT NULL | 財政年度(如 2027|
| BudgetAmount | decimal(18,2) NOT NULL | |
| Notes | varchar(500)? | |
| CreatedAt | timestamp NOT NULL | |
| CreatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| UpdatedAt | timestamp NOT NULL | |
| UpdatedBy | varchar(450) NOT NULL | FK → AspNetUsers.Id |
| **UNIQUE** | (MinistryId, CategoryGroupId, SubCategoryId, FiscalYear) | |
> **備注:** 此表 Phase 3 才建立,但 `ExpenseCategoryGroups`、`ExpenseSubCategories`、`Ministries` 表已在 Phase 1 建好,無需 Schema 改動。
---
## 16. Seed Data
以下資料在 `DbInitializer`(或 `HasData` Migration)中植入:
### Roles(角色)
```
super_admin, pastor, board_member, coworker_chair, ministry_leader, district_leader, cell_leader, coworker, finance, secretary, worship_leader, member, visitor
```
### Ministries12 個事工部門)
```
1. Administration / 行政 DefaultFunctionalClass = 'ManagementGeneral'
2. Preaching / 講道 DefaultFunctionalClass = 'Program'
3. Emcee / 司會 DefaultFunctionalClass = 'Program'
4. Worship / 敬拜 DefaultFunctionalClass = 'Program'
5. PPT/Media / PPT/影音 DefaultFunctionalClass = 'Program'
6. Sound / 音控 DefaultFunctionalClass = 'Program'
7. Facility / 場地組 DefaultFunctionalClass = 'Program'
8. Hospitality / 招待 DefaultFunctionalClass = 'Program'
9. Children / 兒牧 DefaultFunctionalClass = 'Program'
10. Catering / 餐飲 DefaultFunctionalClass = 'Program'
11. Cell Groups / 小組牧養 DefaultFunctionalClass = 'Program'
12. Special Events / 特別活動 DefaultFunctionalClass = 'Program'
```
### GivingCategories(奉獻類型)
```
1. Tithe / 什一奉獻
2. General Offering / 一般奉獻
3. Special Offering / 特別奉獻
4. Building Fund / 建堂基金
5. Mission / 宣教奉獻
```
### ExpenseCategoryGroups14 個大類)
```
見 §8 Seed 大類列表(含新增 Professional Services、Information Technology、Finance & Banking
```
### Form990 權限模組
```
Form990Report — 唯讀報表權限,授予角色:finance、pastor、board_member
```
### Form1099 權限模組
```
Form1099 — 1099 收款人管理與申報,授予角色:
finance — Read / Write / Delete(完整管理)
pastor — Read(唯讀總覽)
board_member — Read(唯讀總覽)
```
### CmsPages(靜態頁面 Slug
```
about, vision, service-times, contact
```
---
## 17. Indexes
```sql
-- Members
CREATE INDEX idx_members_status ON "Members" ("Status") WHERE "IsDeleted" = false;
CREATE INDEX idx_members_family_unit ON "Members" ("FamilyUnitId");
CREATE INDEX idx_members_email ON "Members" ("Email") WHERE "Email" IS NOT NULL;
-- Giving
CREATE INDEX idx_givings_member_date ON "Givings" ("MemberId", "GivingDate");
CREATE INDEX idx_givings_session ON "Givings" ("OfferingSessionId") WHERE "OfferingSessionId" IS NOT NULL;
CREATE INDEX idx_givings_date ON "Givings" ("GivingDate");
-- Expense
CREATE INDEX idx_expenses_ministry ON "Expenses" ("MinistryId");
CREATE INDEX idx_expenses_status ON "Expenses" ("Status") WHERE "IsDeleted" = false;
CREATE INDEX idx_expenses_date ON "Expenses" ("ExpenseDate");
-- AuditLog
CREATE INDEX idx_auditlog_user ON "AuditLogs" ("UserId");
CREATE INDEX idx_auditlog_entity ON "AuditLogs" ("EntityType", "EntityId");
CREATE INDEX idx_auditlog_created ON "AuditLogs" ("CreatedAt");
-- PrayerRequest
CREATE INDEX idx_prayer_visibility ON "PrayerRequests" ("Visibility") WHERE "IsDeleted" = false;
CREATE INDEX idx_prayer_member ON "PrayerRequests" ("RequestedByMemberId");
-- Phase 2: ServiceAssignment
CREATE INDEX idx_service_assign_date ON "ServiceAssignments" ("ServiceDate");
CREATE INDEX idx_service_assign_member ON "ServiceAssignments" ("MemberId");
-- Phase 2: SundayAttendance
-- UNIQUE constraint on AttendanceDate already creates an index
```
---
## 18. EF Core 設定摘要
### DbContext 範例結構
```csharp
public class RolacDbContext : IdentityDbContext<AppUser, AppRole, string>
{
// Phase 1
public DbSet<Member> Members => Set<Member>();
public DbSet<FamilyUnit> FamilyUnits => Set<FamilyUnit>();
public DbSet<MemberMinistry> MemberMinistries => Set<MemberMinistry>();
public DbSet<MemberTag> MemberTags => Set<MemberTag>();
public DbSet<Ministry> Ministries => Set<Ministry>();
public DbSet<UserMinistry> UserMinistries => Set<UserMinistry>();
public DbSet<UserDevice> UserDevices => Set<UserDevice>();
public DbSet<Announcement> Announcements => Set<Announcement>();
public DbSet<SermonVideo> SermonVideos => Set<SermonVideo>();
public DbSet<CmsPage> CmsPages => Set<CmsPage>();
public DbSet<ContactInquiry> ContactInquiries => Set<ContactInquiry>();
public DbSet<GivingCategory> GivingCategories => Set<GivingCategory>();
public DbSet<OfferingSession> OfferingSessions => Set<OfferingSession>();
public DbSet<Giving> Givings => Set<Giving>();
public DbSet<GivingReceipt> GivingReceipts => Set<GivingReceipt>();
public DbSet<GivingRecurringSchedule> GivingRecurringSchedules => Set<GivingRecurringSchedule>();
public DbSet<Form990ExpenseLine> Form990ExpenseLines => Set<Form990ExpenseLine>();
public DbSet<ExpenseCategoryGroup> ExpenseCategoryGroups => Set<ExpenseCategoryGroup>();
public DbSet<ExpenseSubCategory> ExpenseSubCategories => Set<ExpenseSubCategory>();
public DbSet<Expense> Expenses => Set<Expense>();
public DbSet<MonthlyStatement> MonthlyStatements => Set<MonthlyStatement>();
public DbSet<PrayerRequest> PrayerRequests => Set<PrayerRequest>();
public DbSet<PrayerFollow> PrayerFollows => Set<PrayerFollow>();
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
public DbSet<NotificationLog> NotificationLogs => Set<NotificationLog>();
// Phase 2 (schema defined now, implemented later)
public DbSet<ServiceSlot> ServiceSlots => Set<ServiceSlot>();
public DbSet<ServiceAssignment> ServiceAssignments => Set<ServiceAssignment>();
public DbSet<SundayAttendance> SundayAttendances => Set<SundayAttendance>();
public DbSet<CellGroup> CellGroups => Set<CellGroup>();
public DbSet<CellGroupMembership> CellGroupMemberships => Set<CellGroupMembership>();
public DbSet<CellGroupMeeting> CellGroupMeetings => Set<CellGroupMeeting>();
public DbSet<CellGroupAttendance> CellGroupAttendances => Set<CellGroupAttendance>();
// Phase 3
public DbSet<MinistryBudget> MinistryBudgets => Set<MinistryBudget>();
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Apply all entity configurations from assembly
builder.ApplyConfigurationsFromAssembly(typeof(RolacDbContext).Assembly);
// Global soft-delete filter
builder.Entity<Member>().HasQueryFilter(m => !m.IsDeleted);
builder.Entity<Expense>().HasQueryFilter(e => !e.IsDeleted);
builder.Entity<PrayerRequest>().HasQueryFilter(p => !p.IsDeleted);
// Decimal precision
foreach (var property in builder.Model.GetEntityTypes()
.SelectMany(t => t.GetProperties())
.Where(p => p.ClrType == typeof(decimal) || p.ClrType == typeof(decimal?)))
{
property.SetColumnType("decimal(18,2)");
}
}
}
```
### 重要 EF Core 慣例
```csharp
// 軟刪除攔截 (SaveChangesInterceptor)
// 在 SaveChanges 前,如果 EntityState = Deleted 且實體繼承 SoftDeleteEntity
// 改成 Modified 並設定 IsDeleted = true, DeletedAt = now(), DeletedBy = currentUser
// Audit 自動填充 (SaveChangesInterceptor)
// 在 SaveChanges 前,自動填充 CreatedAt/CreatedBy(新增)和 UpdatedAt/UpdatedBy(修改)
// CurrentUser 透過 IHttpContextAccessor 取得
// AuditLog 攔截 (SaveChangesInterceptor)
// 記錄所有 Create/Update/Delete 動作到 AuditLogs 表
// OldValues / NewValues 使用 JsonSerializer.Serialize(entry.OriginalValues.ToObject())
```
---
*文件由 ROLAC 開發團隊維護。如需更新 Schema,必須同步更新此文件和 EF Core Migration。*