# Membership Fees - Overview

**Project:** Mila - Membership Management System  
**Feature:** Membership Fee Management  
**Version:** 1.0  
**Last Updated:** 2026-01-13  
**Status:** ✅ Implemented

---

## 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](./membership-fee-implementation-plan.md) (created after concept iterations)

---

## Table of Contents

1. [Core Principle](#core-principle)
2. [Terminology](#terminology)
3. [Data Model](#data-model)
4. [Business Logic](#business-logic)
5. [UI/UX Design](#uiux-design)
6. [Edge Cases](#edge-cases)
7. [Technical Integration](#technical-integration)
8. [Implementation Scope](#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
