# Membership Contributions - Overview **Project:** Mila - Membership Management System **Feature:** Membership Contribution Management **Version:** 1.0 **Last Updated:** 2025-11-27 **Status:** Concept - Ready for Review --- ## Purpose This document provides a comprehensive overview of the Membership Contributions system. It covers business logic, data model, UI/UX design, and technical architecture in a concise, bullet-point format. **For detailed implementation:** See [contributions-implementation-plan.md](./contributions-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 period-based (Month/Quarter/Half-Year/Year) --- ## Terminology ### German ↔ English **Core Entities:** - Beitragsart ↔ Contribution Type / Membership Fee Type - Beitragsintervall ↔ Contribution Period - Mitgliedsbeitrag ↔ Membership Fee / Contribution **Status:** - bezahlt ↔ paid - unbezahlt ↔ unpaid - ausgesetzt ↔ suspended / waived **Intervals:** - monatlich ↔ monthly - quartalsweise ↔ quarterly - halbjährlich ↔ half-yearly / semi-annually - jährlich ↔ yearly / annually **UI Elements:** - "Letztes Intervall" ↔ "Last Period" (e.g., 2023 when in 2024) - "Aktuelles Intervall" ↔ "Current Period" (e.g., 2024) - "Als bezahlt markieren" ↔ "Mark as paid" - "Aussetzen" ↔ "Suspend" / "Waive" --- ## Data Model ### Contribution Type (ContributionType) ``` - id (UUID) - name (String) - e.g., "Regular", "Reduced", "Student" - amount (Decimal) - Contribution 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 periods regenerated with new amount ### Contribution Period (ContributionPeriod) ``` - id (UUID) - member_id (FK → members.id) - contribution_type_id (FK → contribution_types.id) - period_start (Date) - Calendar period start (01.01., 01.04., 01.07., 01.10., etc.) - status (Enum) - :unpaid (default), :paid, :suspended - amount (Decimal) - Amount at generation time (history when type changes) - notes (Text, optional) - Admin notes - timestamps ``` **Important:** - **NO** `period_end` - calculated from `period_start` + `interval` - **NO** `interval_type` - read from `contribution_type.interval` - Avoids redundancy and inconsistencies! **Calendar Period 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) ``` - contribution_type_id (FK → contribution_types.id, NOT NULL, default from settings) - contribution_start_date (Date, nullable) - When to start generating contributions - left_at (Date, nullable) - Exit date (existing) ``` **Logic for contribution_start_date:** - Auto-set based on global setting `include_joining_period` - If `include_joining_period = true`: First day of joining month/quarter/year - If `include_joining_period = false`: First day of NEXT period after joining - Can be manually overridden by admin **NO** `include_joining_period` field on Member - unnecessary due to `contribution_start_date`! ### Global Settings ``` key: "contributions.include_joining_period" value: Boolean (Default: true) key: "contributions.default_contribution_type_id" value: UUID (Required) - Default contribution type for new members ``` **Meaning include_joining_period:** - `true`: Joining period is included (member pays from joining period) - `false`: Only from next full period after joining **Meaning default_contribution_type_id:** - Every new member automatically gets this contribution type - Must be configured in admin settings - Prevents: Members without contribution type --- ## Business Logic ### Period Generation **Triggers:** - Member gets contribution type assigned (also during member creation) - New period begins (Cron job daily/weekly) - Admin requests manual regeneration **Algorithm:** 1. Get `member.contribution_start_date` and `member.contribution_type` 2. Calculate first period based on `contribution_start_date` 3. Generate all periods from start to today (or `left_at` if present) 4. Skip existing periods 5. Set `amount` to current `contribution_type.amount` **Example (Yearly):** ``` Joining date: 15.03.2023 include_joining_period: true → contribution_start_date: 01.01.2023 Generated periods: - 01.01.2023 - 31.12.2023 (joining period) - 01.01.2024 - 31.12.2024 - 01.01.2025 - 31.12.2025 (current year) ``` **Example (Quarterly):** ``` Joining date: 15.03.2023 include_joining_period: false → contribution_start_date: 01.04.2023 Generated periods: - 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 ### Contribution Type Change **MVP - Same Interval Only:** - Member can only choose contribution type with **same interval** - Example: From "Regular (yearly)" to "Reduced (yearly)" ✓ - Example: From "Regular (yearly)" to "Reduced (monthly)" ✗ **Logic on Change:** 1. Check: New contribution type has same interval 2. If yes: Set `member.contribution_type_id` 3. Future **unpaid** periods: Delete and regenerate with new amount 4. Paid/suspended periods: Remain unchanged (historical amount) **Future - Different Intervals:** - Enable interval switching (e.g., yearly → monthly) - More complex logic for period overlaps - Needs additional validation ### Member Exit **Logic:** - Periods only generated until `member.left_at` - Existing periods remain visible - Unpaid exit period can be marked as "suspended" **Example:** ``` Exit: 15.08.2024 Yearly period: 01.01.2024 - 31.12.2024 → Period 2024 is shown (Status: unpaid) → Admin can set to "suspended" → No periods for 2025+ generated ``` --- ## UI/UX Design ### Member List View **New Column: "Contribution Status"** **Default Display (Last Period):** - Shows status of **last completed** period - Example in 2024: Shows contribution for 2023 - Color coding: - Green: paid ✓ - Red: unpaid ✗ - Gray: suspended ⊘ **Optional: Show Current Period** - Toggle: "Show current period" (2024) - Admin decides what to display **Filters:** - "Unpaid contributions in last period" - "Unpaid contributions in current period" ### Member Detail View **Section: "Contributions"** **Contribution Type Assignment:** ``` ┌─────────────────────────────────────┐ │ Contribution Type: [Dropdown] │ │ ⚠ Only types with same interval │ │ can be selected │ └─────────────────────────────────────┘ ``` **Period Table:** ``` ┌───────────────┬──────────┬────────┬──────────┬─────────┐ │ Period │ 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" - Bulk action for multiple periods ### Admin: Contribution 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 periods will be generated with 65 € - Already paid periods remain with old amount [Cancel] [Confirm] ``` ### Admin: Settings **Contribution Configuration:** ``` Default Contribution Type: [Dropdown: Contribution Types] Selected: "Regular (60 €, Yearly)" This contribution type is automatically assigned to all new members. Can be changed individually per member. --- ☐ Include joining period When active: Members pay from the period of their joining. Example (Yearly): Joining: 15.03.2023 → Pays from 2023 When inactive: Members pay from the next full period. Example (Yearly): Joining: 15.03.2023 → Pays from 2024 ``` --- ## Edge Cases ### 1. Contribution Type Change with Different Interval **MVP:** Blocked (only same interval allowed) **UI:** ``` Error: Interval change not possible Current contribution type: "Regular (Yearly)" Selected contribution type: "Student (Monthly)" Changing the interval is currently not possible. Please select a contribution type with interval "Yearly". [OK] ``` **Future:** - Allow interval switching - Calculate overlaps - Generate new periods without duplicates ### 2. Exit with Unpaid Contributions **Scenario:** ``` Member exits: 15.08.2024 Yearly period 2024: unpaid ``` **UI Notice on Exit:** ``` ⚠ Unpaid contributions present This member has 1 unpaid period(s): - 2024: 60 € (unpaid) Do you want to continue? [ ] Mark contribution as "suspended" [Cancel] [Confirm Exit] ``` ### 3. Multiple Unpaid Periods **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] (2 selected) ``` ### 4. Amount Changes **Scenario:** ``` 2023: Regular = 50 € 2024: Regular = 60 € (increase) ``` **Result:** - Period 2023: Saved with 50 € (history) - Period 2024: Generated with 60 € (current) - Both periods show correct historical amount ### 5. Date Boundaries **Problem:** What if today = 01.01.2025? **Solution:** - Current period (2025) is generated - Status: unpaid (open) - Shown in overview --- ## Implementation Scope ### MVP (Phase 1) **Included:** - ✓ Contribution types (CRUD) - ✓ Automatic period generation - ✓ Status management (paid/unpaid/suspended) - ✓ Member overview with contribution status - ✓ Period view per member - ✓ Quick checkbox marking - ✓ Bulk actions - ✓ Amount history - ✓ Same-interval type change - ✓ Default contribution type - ✓ Joining period 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 periods - Manual vereinfacht.digital links per member - Extended filter options **Phase 3:** - Automated vereinfacht.digital integration - Automatic payment matching - SEPA integration - Advanced reports