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