mitgliederverwaltung/docs/membership-fee-overview.md
Moritz 3fd8483231
All checks were successful
continuous-integration/drone/push Build is passing
docs: small changes based on review
2025-12-11 15:52:32 +01:00

14 KiB

Membership Fees - Overview

Project: Mila - Membership Management System
Feature: Membership Fee Management
Version: 1.0
Last Updated: 2025-11-27
Status: Concept - Ready for Review


Purpose

This document provides a comprehensive overview of the Membership Fees system. It covers business logic, data model, UI/UX design, and technical architecture in a concise, bullet-point format.

For detailed implementation: See membership-fee-implementation-plan.md (created after concept iterations)


Table of Contents

  1. Core Principle
  2. Terminology
  3. Data Model
  4. Business Logic
  5. UI/UX Design
  6. Edge Cases
  7. Technical Integration
  8. Implementation Scope

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

Membership Fee Type (MembershipFeeType)

- id (UUID)
- name (String) - e.g., "Regular", "Reduced", "Student"
- amount (Decimal) - Membership fee amount in Euro
- interval (Enum) - :monthly, :quarterly, :half_yearly, :yearly
- description (Text, optional)

Important:

  • interval is IMMUTABLE after creation!
  • Admin can only change name, amount, description
  • On change: Future unpaid cycles regenerated with new amount

Membership Fee Cycle (MembershipFeeCycle)

- id (UUID)
- member_id (FK → members.id)
- membership_fee_type_id (FK → membership_fee_types.id)
- cycle_start (Date) - Calendar cycle start (01.01., 01.04., 01.07., 01.10., etc.)
- status (Enum) - :unpaid (default), :paid, :suspended
- amount (Decimal) - Membership fee amount at generation time (history when type changes)
- notes (Text, optional) - Admin notes

Important:

  • NO cycle_end - calculated from cycle_start + interval
  • NO interval_type - read from membership_fee_type.interval
  • Avoids redundancy and inconsistencies!

Calendar Cycle Logic:

  • Monthly: 01.01. - 31.01., 01.02. - 28./29.02., 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.

Member (Extensions)

- membership_fee_type_id (FK → membership_fee_types.id, NOT NULL, default from settings)
- membership_fee_start_date (Date, nullable) - When to start generating membership fees
- left_at (Date, nullable) - Exit date (existing)

Logic for membership_fee_start_date:

  • Auto-set based on global setting include_joining_cycle
  • If include_joining_cycle = true: First day of joining month/quarter/year
  • If include_joining_cycle = false: First day of NEXT cycle after joining
  • Can be manually overridden by admin

NO include_joining_cycle field on Member - unnecessary due to membership_fee_start_date!

Global Settings

key: "membership_fees.include_joining_cycle" 
value: Boolean (Default: true)

key: "membership_fees.default_membership_fee_type_id"
value: UUID (Required) - Default membership fee type for new members

Meaning include_joining_cycle:

  • true: Joining cycle is included (member pays from joining cycle)
  • false: Only from next full cycle after joining

Meaning of default membership fee type setting:

  • Every new member automatically gets this membership fee type
  • Must be configured in admin settings
  • Prevents: Members without membership fee type

Business Logic

Cycle Generation

Triggers:

  • Member gets membership fee type assigned (also during member creation)
  • New cycle begins (Cron job daily/weekly)
  • Admin requests manual regeneration

Algorithm:

Lock the whole cycle table for the duration of the algorithm

  1. Get member.membership_fee_start_date and member's membership fee type
  2. Generate cycles until today (or left_at if present):
    • If no cycle exists:
      • Generate all cycles from membership_fee_start_date
    • else:
      • Generate all cycles from last existing cycle
    • use the interval to generate the cycles
  3. Set amount to current membership fee type's amount

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

Permissions:

  • Admin + Treasurer (Kassenwart) can change status
  • Uses existing permission system

Membership Fee Type Change

MVP - Same Cycle Only:

  • Member can only choose membership fee type with same cycle
  • Example: From "Regular (yearly)" to "Reduced (yearly)" ✓
  • Example: From "Regular (yearly)" to "Reduced (monthly)" ✗

Logic on Change:

  1. Check: New membership fee type has same interval
  2. If yes: Set member.membership_fee_type_id
  3. Future unpaid cycles: Delete and regenerate with new amount
  4. Paid/suspended cycles: Remain unchanged (historical amount)

Future - Different Intervals:

  • Enable interval switching (e.g., yearly → monthly)
  • More complex logic for cycle overlaps
  • Needs additional validation

Member Exit

Logic:

  • Cycles only generated until member.left_at
  • Existing cycles remain visible
  • Unpaid exit cycle can be marked as "suspended"

Example:

Exit: 15.08.2024
Yearly cycle: 01.01.2024 - 31.12.2024

→ Cycle 2024 is shown (Status: unpaid)
→ Admin can set to "suspended"
→ No cycles for 2025+ generated

UI/UX Design

Member List View

New Column: "Membership Fee Status"

Default Display (Last Cycle):

  • Shows status of last completed cycle
  • Example in 2024: Shows membership fee for 2023
  • Color coding:
    • Green: paid ✓
    • Red: unpaid ✗
    • Gray: suspended ⊘

Optional: Show Current Cycle

  • Toggle: "Show current cycle" (2024)
  • Admin decides what to display

Filters:

  • "Unpaid membership fees in last cycle"
  • "Unpaid membership fees in current cycle"

Member Detail View

Section: "Membership Fees"

Membership 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 in each row for fast marking
  • Button: "Mark selected as paid/unpaid/suspended"
  • 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: ✓ editable
  • Amount: ✓ editable
  • 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. Membership Fee Type Change with Different Interval

MVP: Blocked (only same interval allowed)

UI:

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
  • Calculate overlaps
  • Generate new cycles without duplicates

2. Exit with Unpaid Membership Fees

Scenario:

Member exits: 15.08.2024
Yearly cycle 2024: unpaid

UI Notice on Exit: (Low Prio)

⚠ 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

Scenario: Member hasn't paid for 2 years

Display:

┌───────────────┬──────────┬────────┬──────────┬─────────┐
│ 2023          │ Yearly   │ 50 €   │ ☐ Open   │ [✓]     │
│ 2024          │ Yearly   │ 60 €   │ ☐ Open   │ [✓]     │
│ 2025          │ Yearly   │ 60 €   │ ☐ Open   │ [ ]     │
└───────────────┴──────────┴────────┴──────────┴─────────┘

[Mark selected as paid/unpaid/suspended] (2 selected)

4. Amount Changes

Scenario:

2023: Regular = 50 €
2024: Regular = 60 € (increase)

Result:

  • Cycle 2023: Saved with 50 € (history)
  • Cycle 2024: Generated with 60 € (current)
  • Both cycles show correct historical amount

5. Date Boundaries

Problem: What if today = 01.01.2025?

Solution:

  • Current cycle (2025) is generated
  • Status: unpaid (open)
  • Shown in overview

Implementation Scope

MVP (Phase 1)

Included:

  • ✓ Membership fee types (CRUD)
  • ✓ Automatic cycle generation
  • ✓ Status management (paid/unpaid/suspended)
  • ✓ Member overview with membership fee status
  • ✓ Cycle view per member
  • ✓ Quick checkbox marking
  • ✓ Bulk actions
  • ✓ Amount history
  • ✓ Same-interval type change
  • ✓ Default membership fee type
  • ✓ Joining cycle configuration

NOT Included:

  • ✗ Interval change (only same interval)
  • ✗ Payment details (date, method)
  • ✗ Automatic integration (vereinfacht.digital)
  • ✗ Prorata calculation
  • ✗ Reports/statistics
  • ✗ Reminders/dunning (manual via filters)

Future Enhancements

Phase 2:

  • Payment details (date, amount, method)
  • Interval change for future unpaid cycles
  • Manual vereinfacht.digital links per member
  • Extended filter options

Phase 3:

  • Automated vereinfacht.digital integration
  • Automatic payment matching
  • SEPA integration
  • Advanced reports