mitgliederverwaltung/docs/membership-fee-overview.md
Moritz 82897d5cd3
All checks were successful
continuous-integration/drone/push Build is passing
refactor: improve cycle generation code quality and documentation
- Remove Process.sleep calls from integration tests (tests run synchronously in SQL sandbox)
- Improve error handling: membership_fee_type_not_found now returns changeset error instead of just logging
- Clarify partial_failure documentation: successful_cycles are not persisted on rollback
- Update documentation: joined_at → join_date, left_at → exit_date
- Document PostgreSQL advisory locks per member (not whole table lock)
- Document gap handling: explicitly deleted cycles are not recreated
2025-12-12 17:41:22 +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
- exit_date (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:

Use PostgreSQL advisory locks per member to prevent race conditions

  1. Get member.membership_fee_start_date and member's membership fee type
  2. Determine generation start point:
    • If NO cycles exist: Start from membership_fee_start_date
    • If cycles exist: Start from the cycle AFTER the last existing one
  3. Generate cycles until today (or exit_date if present):
    • Use the interval to generate the cycles
    • Note: If cycles were explicitly deleted (gaps exist), they are NOT recreated. The generator always continues from the cycle AFTER the last existing cycle.
  4. 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.exit_date
  • 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