mitgliederverwaltung/docs/membership-fee-overview.md

12 KiB
Raw Permalink Blame History

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.


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.

  • 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.