mitgliederverwaltung/docs/membership-fee-overview.md

278 lines
12 KiB
Markdown
Raw Permalink 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.

# Membership Fees - Overview
**Feature:** Membership Fee Management — **Status:** Implemented
Coarse, business-oriented entry point for the Membership Fees system: terminology, worked examples, and UI/UX. For architecture (data model, FK behaviors, module map, generation algorithm, policies) see [membership-fee-architecture.md](./membership-fee-architecture.md).
---
## Core Principle
Maximum simplicity: minimal complexity, clear data model without redundancies, intuitive operation, calendar-cycle-based (Month / Quarter / Half-Year / Year).
---
## Terminology (German ↔ English)
**Core entities:**
- Beitragsart ↔ Membership Fee Type
- Beitragszyklus ↔ Membership Fee Cycle
- Mitgliedsbeitrag ↔ Membership Fee
**Status:**
- bezahlt ↔ paid
- unbezahlt ↔ unpaid
- ausgesetzt ↔ suspended / waived
**Intervals (Frequenz / payment frequency):**
- monatlich ↔ monthly
- quartalsweise ↔ quarterly
- halbjährlich ↔ half-yearly / semi-annually
- jährlich ↔ yearly / annually
**UI elements:**
- "Letzter Zyklus" ↔ "Last Cycle" (e.g., 2023 when in 2024)
- "Aktueller Zyklus" ↔ "Current Cycle" (e.g., 2024)
- "Als bezahlt markieren" ↔ "Mark as paid"
- "Aussetzen" ↔ "Suspend" / "Waive"
---
## Data Model (summary)
Three entities — full schema, FK on-delete behaviors, and design rationale are in [membership-fee-architecture.md](./membership-fee-architecture.md).
- **MembershipFeeType:** name, amount (€), `interval` (:monthly/:quarterly/:half_yearly/:yearly), optional description. `interval` is **IMMUTABLE** after creation; admin can change only name/amount/description; on amount change, future unpaid cycles regenerate with the new amount.
- **MembershipFeeCycle:** member_id, membership_fee_type_id, `cycle_start` (calendar start: 01.01., 01.04., 01.07., 01.10., …), status (:unpaid default / :paid / :suspended), `amount` (captured at generation time → history when type changes), optional notes. NO `cycle_end` (derived from `cycle_start` + interval), NO `interval_type` (read from the fee type).
- **Member extensions:** `membership_fee_type_id` (FK, nullable — default applied from settings at the app level), `membership_fee_start_date` (Date, nullable), plus the existing `exit_date`.
**Calendar cycle logic:** Monthly 01.01.31.01., etc. · Quarterly 01.01.31.03., 01.04.30.06., 01.07.30.09., 01.10.31.12. · Half-yearly 01.01.30.06., 01.07.31.12. · Yearly 01.01.31.12.
### `membership_fee_start_date` derivation
Auto-set from global setting `include_joining_cycle`:
- `include_joining_cycle = true` → first day of the joining month/quarter/year (member pays from the joining cycle).
- `include_joining_cycle = false` → first day of the NEXT cycle after joining.
Can be manually overridden by admin. There is intentionally **no** `include_joining_cycle` field on Member — `membership_fee_start_date` makes it unnecessary.
### Global settings
- `membership_fees.include_joining_cycle` — Boolean (default `true`): whether the joining cycle is billed.
- `membership_fees.default_membership_fee_type_id` — UUID (required): fee type auto-assigned to every new member; must be configured in admin settings (prevents members without a fee type).
---
## Business Logic
### Cycle generation
**Triggers:** fee type assigned (incl. at member creation), new cycle begins (cron daily/weekly), admin manual regeneration. Uses PostgreSQL advisory locks per member.
**Algorithm:** start from `membership_fee_start_date` if no cycles exist, else from the cycle AFTER the last existing one; generate to today (or `exit_date`); set each cycle's `amount` from the current fee type. **Deleted cycles (gaps) are NOT recreated** — generation always continues after the last existing cycle. (Full algorithm in architecture doc.)
**Example (Yearly):**
```
Joining date: 15.03.2023
include_joining_cycle: true
→ membership_fee_start_date: 01.01.2023
Generated cycles:
- 01.01.2023 - 31.12.2023 (joining cycle)
- 01.01.2024 - 31.12.2024
- 01.01.2025 - 31.12.2025 (current year)
```
**Example (Quarterly):**
```
Joining date: 15.03.2023
include_joining_cycle: false
→ membership_fee_start_date: 01.04.2023
Generated cycles:
- 01.04.2023 - 30.06.2023 (first full quarter)
- 01.07.2023 - 30.09.2023
- 01.10.2023 - 31.12.2023
- 01.01.2024 - 31.03.2024
- ...
```
### Status transitions
unpaid → paid · unpaid → suspended · paid → unpaid · suspended → paid · suspended → unpaid. Admin + Treasurer (Kassenwart) can change status, via the existing permission system.
### Membership fee type change
MVP allows changing only to a fee type with the **same interval** (e.g. "Regular (yearly)" → "Reduced (yearly)" ✓; → "Reduced (monthly)" ✗). On change: set `member.membership_fee_type_id`; future **unpaid** cycles deleted and regenerated with the new amount; paid/suspended cycles unchanged (historical amount). Future: enable interval switching (overlap handling, extra validation).
### Member exit
Cycles generated only up to `member.exit_date`; existing cycles remain visible; an unpaid exit cycle can be marked "suspended". E.g. exit 15.08.2024 with a yearly cycle 01.01.31.12.2024 → 2024 cycle shown (unpaid, admin may suspend); no 2025+ cycles generated.
---
## UI/UX Design
### Member List View — column "Membership Fee Status"
- **Default (last completed cycle):** in 2024, shows the 2023 status. Color: green = paid ✓, red = unpaid ✗, gray = suspended ⊘.
- **Optional toggle:** "Show current cycle" (2024).
- **Filters:** "Unpaid membership fees in last cycle", "Unpaid membership fees in current cycle".
### Member Detail View — section "Membership Fees"
**Fee type assignment:**
```
┌─────────────────────────────────────┐
│ Membership Fee Type: [Dropdown] │
│ ⚠ Only types with same interval │
│ can be selected │
└─────────────────────────────────────┘
```
**Cycle table:**
```
┌───────────────┬──────────┬────────┬──────────┬─────────┐
│ Cycle │ Interval │ Amount │ Status │ Action │
├───────────────┼──────────┼────────┼──────────┼─────────┤
│ 01.01.2023- │ Yearly │ 50 € │ ☑ Paid │ │
│ 31.12.2023 │ │ │ │ │
├───────────────┼──────────┼────────┼──────────┼─────────┤
│ 01.01.2024- │ Yearly │ 60 € │ ☐ Open │ [Mark │
│ 31.12.2024 │ │ │ │ as paid]│
├───────────────┼──────────┼────────┼──────────┼─────────┤
│ 01.01.2025- │ Yearly │ 60 € │ ☐ Open │ [Mark │
│ 31.12.2025 │ │ │ │ as paid]│
└───────────────┴──────────┴────────┴──────────┴─────────┘
Legend: ☑ = paid | ☐ = unpaid | ⊘ = suspended
```
**Quick marking:** checkbox per row; "Mark selected as paid/unpaid/suspended" button; bulk action for multiple cycles.
### Admin: Membership Fee Types Management
**List:**
```
┌────────────┬──────────┬──────────┬────────────┬─────────┐
│ Name │ Amount │ Interval │ Members │ Actions │
├────────────┼──────────┼──────────┼────────────┼─────────┤
│ Regular │ 60 € │ Yearly │ 45 │ [Edit] │
│ Reduced │ 30 € │ Yearly │ 12 │ [Edit] │
│ Student │ 20 € │ Monthly │ 8 │ [Edit] │
└────────────┴──────────┴──────────┴────────────┴─────────┘
```
**Edit:** Name ✓, Amount ✓, Description ✓ editable; Interval ✗ **NOT** editable (grayed out).
**Warning on amount change:**
```
⚠ Change amount to 65 €?
Impact:
- 45 members affected
- Future unpaid cycles will be generated with 65 €
- Already paid cycles remain with old amount
[Cancel] [Confirm]
```
### Admin: Settings — Membership Fee Configuration
```
Default Membership Fee Type: [Dropdown: Membership Fee Types]
Selected: "Regular (60 €, Yearly)"
This membership fee type is automatically assigned to all new members.
Can be changed individually per member.
---
☐ Include joining cycle
When active:
Members pay from the cycle of their joining.
Example (Yearly):
Joining: 15.03.2023
→ Pays from 2023
When inactive:
Members pay from the next full cycle.
Example (Yearly):
Joining: 15.03.2023
→ Pays from 2024
```
---
## Edge Cases
1. **Type change with different interval:** MVP blocks it. UI message:
```
Error: Interval change not possible
Current membership fee type: "Regular (Yearly)"
Selected membership fee type: "Student (Monthly)"
Changing the interval is currently not possible.
Please select a membership fee type with interval "Yearly".
[OK]
```
Future: allow interval switching with overlap calculation and no duplicate cycles.
2. **Exit with unpaid fees (low prio):** on exit, offer to mark unpaid cycles "suspended".
```
⚠ Unpaid membership fees present
This member has 1 unpaid cycle(s):
- 2024: 60 € (unpaid)
Do you want to continue?
[ ] Mark membership fee as "suspended"
[Cancel] [Confirm Exit]
```
3. **Multiple unpaid cycles:** all shown; select several and bulk-mark.
```
┌───────────────┬──────────┬────────┬──────────┬─────────┐
│ 2023 │ Yearly │ 50 € │ ☐ Open │ [✓] │
│ 2024 │ Yearly │ 60 € │ ☐ Open │ [✓] │
│ 2025 │ Yearly │ 60 € │ ☐ Open │ [ ] │
└───────────────┴──────────┴────────┴──────────┴─────────┘
[Mark selected as paid/unpaid/suspended] (2 selected)
```
4. **Amount changes:** 2023 Regular = 50 €, 2024 = 60 € → 2023 cycle keeps 50 € (history), 2024 generated with 60 €; each cycle shows its historical amount.
5. **Date boundaries:** today = 01.01.2025 → current 2025 cycle generated, status unpaid, shown in overview.
---
## Implementation Scope
**MVP (Phase 1) — included:** fee types CRUD; automatic cycle generation; status management (paid/unpaid/suspended); member overview with status; per-member cycle view; quick checkbox marking; bulk actions; amount history; same-interval type change; default fee type; joining-cycle configuration.
**NOT included:** interval change; payment details (date, method); automatic vereinfacht.digital integration; prorata; reports/statistics; reminders/dunning (manual via filters).
**Future:** Phase 2 — payment details, interval change for future unpaid cycles, manual vereinfacht.digital links per member, extended filters. Phase 3 — automated vereinfacht.digital integration, automatic payment matching, SEPA, advanced reports.