# 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](./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) - timestamps ``` **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 - timestamps ``` **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:** 1. Get `member.membership_fee_start_date` and member's membership fee type 2. Calculate first cycle based on `membership_fee_start_date` 3. Generate all cycles from start to today (or `left_at` if present) 4. Skip existing cycles 5. 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