docs: payment concept

This commit is contained in:
Moritz 2025-11-27 21:30:06 +01:00 committed by Rafael Epplée
parent 422cf37a1e
commit a5aeef3e27
2 changed files with 1178 additions and 0 deletions

View file

@ -0,0 +1,651 @@
# Membership Contributions - Technical Architecture
**Project:** Mila - Membership Management System
**Feature:** Membership Contribution Management
**Version:** 1.0
**Last Updated:** 2025-11-27
**Status:** Architecture Design - Ready for Implementation
---
## Purpose
This document defines the technical architecture for the Membership Contributions system. It focuses on architectural decisions, patterns, module structure, and integration points **without** concrete implementation details.
**Related Documents:**
- [contributions-overview.md](./contributions-overview.md) - Business logic and requirements
- [database-schema-readme.md](./database-schema-readme.md) - Database documentation
- [database_schema.dbml](./database_schema.dbml) - Database schema definition
---
## Table of Contents
1. [Architecture Principles](#architecture-principles)
2. [Domain Structure](#domain-structure)
3. [Data Architecture](#data-architecture)
4. [Business Logic Architecture](#business-logic-architecture)
5. [Integration Points](#integration-points)
6. [Acceptance Criteria](#acceptance-criteria)
7. [Testing Strategy](#testing-strategy)
8. [Security Considerations](#security-considerations)
9. [Performance Considerations](#performance-considerations)
---
## Architecture Principles
### Core Design Decisions
1. **Single Responsibility:**
- Each module has one clear responsibility
- Period generation separated from status management
- Calendar logic isolated in dedicated module
2. **No Redundancy:**
- No `period_end` field (calculated from `period_start` + `interval`)
- No `interval_type` field (read from `contribution_type.interval`)
- Eliminates data inconsistencies
3. **Immutability Where Important:**
- `contribution_type.interval` cannot be changed after creation
- Prevents complex migration scenarios
- Enforced via Ash change validation
4. **Historical Accuracy:**
- `amount` stored per period for audit trail
- Enables tracking of contribution changes over time
- Old periods retain original amounts
5. **Calendar-Based Periods:**
- All periods aligned to calendar boundaries
- Simplifies date calculations
- Predictable period generation
---
## Domain Structure
### Ash Domain: `Mv.Contributions`
**Purpose:** Encapsulates all contribution-related resources and logic
**Resources:**
- `ContributionType` - Contribution type definitions (admin-managed)
- `ContributionPeriod` - Individual contribution periods per member
**Extensions:**
- Member resource extended with contribution fields
### Module Organization
```
lib/
├── contributions/
│ ├── contributions.ex # Ash domain definition
│ ├── contribution_type.ex # ContributionType resource
│ ├── contribution_period.ex # ContributionPeriod resource
│ └── changes/
│ ├── prevent_interval_change.ex # Validates interval immutability
│ ├── set_contribution_start_date.ex # Auto-sets start date
│ └── validate_same_interval.ex # Validates interval match on type change
├── mv/
│ └── contributions/
│ ├── period_generator.ex # Period generation algorithm
│ └── calendar_periods.ex # Calendar period calculations
└── membership/
└── member.ex # Extended with contribution relationships
```
### Separation of Concerns
**Domain Layer (Ash Resources):**
- Data validation
- Relationship management
- Policy enforcement
- Action definitions
**Business Logic Layer (`Mv.Contributions`):**
- Period generation algorithm
- Calendar calculations
- Date boundary handling
- Status transitions
**UI Layer (LiveView):**
- User interaction
- Display logic
- Authorization checks
- Form handling
---
## Data Architecture
### Database Schema Extensions
**See:** [database-schema-readme.md](./database-schema-readme.md) and [database_schema.dbml](./database_schema.dbml) for complete schema documentation.
### New Tables
1. **`contribution_types`**
- Purpose: Define contribution types with fixed intervals
- Key Constraint: `interval` field immutable after creation
- Relationships: has_many members, has_many contribution_periods
2. **`contribution_periods`**
- Purpose: Individual contribution periods for members
- Key Design: NO `period_end` or `interval_type` fields (calculated)
- Relationships: belongs_to member, belongs_to contribution_type
- Composite uniqueness: One period per member per period_start
### Member Table Extensions
**Fields Added:**
- `contribution_type_id` (FK, NOT NULL with default from settings)
- `contribution_start_date` (Date, nullable)
**Existing Fields Used:**
- `joined_at` - For calculating contribution start
- `left_at` - For limiting period generation
### Settings Integration
**Global Settings:**
- `contributions.include_joining_period` (Boolean)
- `contributions.default_contribution_type_id` (UUID)
**Storage:** Existing settings mechanism (TBD: dedicated table or configuration resource)
### Foreign Key Behaviors
| Relationship | On Delete | Rationale |
|--------------|-----------|-----------|
| `contribution_periods.member_id → members.id` | CASCADE | Remove periods when member deleted |
| `contribution_periods.contribution_type_id → contribution_types.id` | RESTRICT | Prevent type deletion if periods exist |
| `members.contribution_type_id → contribution_types.id` | RESTRICT | Prevent type deletion if assigned to members |
---
## Business Logic Architecture
### Period Generation System
**Component:** `Mv.Contributions.PeriodGenerator`
**Responsibilities:**
- Calculate which periods should exist for a member
- Generate missing periods
- Respect contribution_start_date and left_at boundaries
- Skip existing periods (idempotent)
**Triggers:**
1. Member contribution type assigned (via Ash change)
2. Member created with contribution type (via Ash change)
3. Scheduled job runs (daily/weekly cron)
4. Admin manual regeneration (UI action)
**Algorithm Steps:**
1. Retrieve member with contribution_type and dates
2. Determine first period start (based on contribution_start_date)
3. Calculate all period starts from first to today (or left_at)
4. Query existing periods for member
5. Generate missing periods with current contribution_type.amount
6. Insert new periods (batch operation)
**Edge Case Handling:**
- If contribution_start_date is NULL: Calculate from joined_at + global setting
- If left_at is set: Stop generation at left_at
- If contribution_type changes: Handled separately by regeneration logic
### Calendar Period Calculations
**Component:** `Mv.Contributions.CalendarPeriods`
**Responsibilities:**
- Calculate period boundaries based on interval type
- Determine current period
- Determine last completed period
- Calculate period_end from period_start + interval
**Functions (high-level):**
- `calculate_period_start/3` - Given date and interval, find period start
- `calculate_period_end/2` - Given period_start and interval, calculate end
- `next_period_start/2` - Given period_start and interval, find next
- `is_current_period?/2` - Check if period contains today
- `is_last_completed_period?/2` - Check if period just ended
**Interval Logic:**
- **Monthly:** Start = 1st of month, End = last day of month
- **Quarterly:** Start = 1st of quarter (Jan/Apr/Jul/Oct), End = last day of quarter
- **Half-yearly:** Start = 1st of half (Jan/Jul), End = last day of half
- **Yearly:** Start = Jan 1st, End = Dec 31st
### Status Management
**Component:** Ash actions on `ContributionPeriod`
**Status Transitions:**
- Simple state machine: unpaid ↔ paid ↔ suspended
- No complex validation (all transitions allowed)
- Permissions checked via Ash policies
**Actions Required:**
- `mark_as_paid` - Set status to :paid
- `mark_as_suspended` - Set status to :suspended
- `mark_as_unpaid` - Set status to :unpaid (error correction)
**Bulk Operations:**
- `bulk_mark_as_paid` - Mark multiple periods as paid (efficiency)
### Contribution Type Change Handling
**Component:** Ash change on `Member.contribution_type_id`
**Validation:**
- Check if new type has same interval as old type
- If different: Reject change (MVP constraint)
- If same: Allow change
**Side Effects on Allowed Change:**
1. Keep all existing periods unchanged
2. Find future unpaid periods
3. Delete future unpaid periods
4. Regenerate periods with new contribution_type_id and amount
**Implementation Pattern:**
- Use Ash change module to validate
- Use after_action hook to trigger regeneration
- Use transaction to ensure atomicity
---
## Integration Points
### Member Resource Integration
**Extension Points:**
1. Add fields via migration
2. Add relationships (belongs_to, has_many)
3. Add calculations (current_period_status, overdue_count)
4. Add changes (auto-set contribution_start_date, validate interval)
**Backward Compatibility:**
- New fields nullable or with defaults
- Existing members get default contribution type from settings
- No breaking changes to existing member functionality
### Settings System Integration
**Requirements:**
- Store two global settings
- Provide UI for admin to modify
- Default values if not set
- Validation (e.g., default_contribution_type_id must exist)
**Access Pattern:**
- Read settings during period generation
- Read settings during member creation
- Write settings only via admin UI
### Permission System Integration
**See:** [roles-and-permissions-architecture.md](./roles-and-permissions-architecture.md)
**Required Permissions:**
- `ContributionType.create/update/destroy` - Admin only
- `ContributionType.read` - Admin, Treasurer, Board
- `ContributionPeriod.update` (status changes) - Admin, Treasurer
- `ContributionPeriod.read` - Admin, Treasurer, Board, Own member
**Policy Patterns:**
- Use existing HasPermission check
- Leverage existing roles (Admin, Kassenwart)
- Member can read own periods (linked via member_id)
### LiveView Integration
**New LiveViews Required:**
1. ContributionType index/form (admin)
2. ContributionPeriod table component (member detail view)
3. Settings form section (admin)
4. Member list column (contribution status)
**Existing LiveViews to Extend:**
- Member detail view: Add contributions section
- Member list view: Add status column
- Settings page: Add contributions section
**Authorization Helpers:**
- Use existing `can?/3` helper for UI conditionals
- Check permissions before showing actions
---
## Acceptance Criteria
### ContributionType Resource
**AC-CT-1:** Admin can create contribution type with name, amount, interval, description
**AC-CT-2:** Interval field is immutable after creation (validation error on change attempt)
**AC-CT-3:** Admin can update name, amount, description (but not interval)
**AC-CT-4:** Cannot delete contribution type if assigned to members
**AC-CT-5:** Cannot delete contribution type if periods exist referencing it
**AC-CT-6:** Interval must be one of: monthly, quarterly, half_yearly, yearly
### ContributionPeriod Resource
**AC-CP-1:** Period has period_start, status, amount, notes, member_id, contribution_type_id
**AC-CP-2:** Period_end is calculated, not stored
**AC-CP-3:** Status defaults to :unpaid
**AC-CP-4:** One period per member per period_start (uniqueness constraint)
**AC-CP-5:** Amount is set at generation time from contribution_type.amount
**AC-CP-6:** Periods cascade delete when member deleted
**AC-CP-7:** Admin/Treasurer can change status
**AC-CP-8:** Member can read own periods
### Member Extensions
**AC-M-1:** Member has contribution_type_id field (NOT NULL with default)
**AC-M-2:** Member has contribution_start_date field (nullable)
**AC-M-3:** New members get default contribution type from global setting
**AC-M-4:** contribution_start_date auto-set based on joined_at and global setting
**AC-M-5:** Admin can manually override contribution_start_date
**AC-M-6:** Cannot change to contribution type with different interval (MVP)
### Period Generation
**AC-PG-1:** Periods generated when member gets contribution type
**AC-PG-2:** Periods generated when member created (via change hook)
**AC-PG-3:** Scheduled job generates missing periods daily
**AC-PG-4:** Generation respects contribution_start_date
**AC-PG-5:** Generation stops at left_at if member exited
**AC-PG-6:** Generation is idempotent (skips existing periods)
**AC-PG-7:** Periods align to calendar boundaries (1st of month/quarter/half/year)
**AC-PG-8:** Amount comes from contribution_type at generation time
### Calendar Logic
**AC-CL-1:** Monthly periods: 1st to last day of month
**AC-CL-2:** Quarterly periods: 1st of Jan/Apr/Jul/Oct to last day of quarter
**AC-CL-3:** Half-yearly periods: 1st of Jan/Jul to last day of half
**AC-CL-4:** Yearly periods: Jan 1 to Dec 31
**AC-CL-5:** Period_end calculated correctly for all interval types
**AC-CL-6:** Current period determined correctly based on today's date
**AC-CL-7:** Last completed period determined correctly
### Contribution Type Change
**AC-TC-1:** Can change to type with same interval
**AC-TC-2:** Cannot change to type with different interval (error message)
**AC-TC-3:** On allowed change: future unpaid periods regenerated
**AC-TC-4:** On allowed change: paid/suspended periods unchanged
**AC-TC-5:** On allowed change: amount updated to new type's amount
**AC-TC-6:** Change is atomic (transaction)
### Settings
**AC-S-1:** Global setting: include_joining_period (boolean, default true)
**AC-S-2:** Global setting: default_contribution_type_id (UUID, required)
**AC-S-3:** Admin can modify settings via UI
**AC-S-4:** Settings validated (e.g., default type must exist)
**AC-S-5:** Settings applied to new members immediately
### UI - Member List
**AC-UI-ML-1:** New column shows contribution status
**AC-UI-ML-2:** Default: Shows last completed period status
**AC-UI-ML-3:** Optional: Toggle to show current period status
**AC-UI-ML-4:** Color coding: green (paid), red (unpaid), gray (suspended)
**AC-UI-ML-5:** Filter: Unpaid in last period
**AC-UI-ML-6:** Filter: Unpaid in current period
### UI - Member Detail
**AC-UI-MD-1:** Contributions section shows all periods
**AC-UI-MD-2:** Table columns: Period, Interval, Amount, Status, Actions
**AC-UI-MD-3:** Checkbox per period for bulk marking
**AC-UI-MD-4:** "Mark selected as paid" button
**AC-UI-MD-5:** Dropdown to change contribution type (same interval only)
**AC-UI-MD-6:** Warning if different interval selected
**AC-UI-MD-7:** Only show actions if user has permission
### UI - Contribution Types Admin
**AC-UI-CTA-1:** List all contribution types
**AC-UI-CTA-2:** Show: Name, Amount, Interval, Member count
**AC-UI-CTA-3:** Create new contribution type form
**AC-UI-CTA-4:** Edit form: Name, Amount, Description editable
**AC-UI-CTA-5:** Edit form: Interval grayed out (not editable)
**AC-UI-CTA-6:** Warning on amount change (explain impact)
**AC-UI-CTA-7:** Cannot delete if members assigned
**AC-UI-CTA-8:** Only admin can access
### UI - Settings Admin
**AC-UI-SA-1:** Contributions section in settings
**AC-UI-SA-2:** Dropdown to select default contribution type
**AC-UI-SA-3:** Checkbox: Include joining period
**AC-UI-SA-4:** Explanatory text with examples
**AC-UI-SA-5:** Save button with validation
---
## Testing Strategy
### Unit Testing
**Period Generator Tests:**
- Correct period_start calculation for all interval types
- Correct period count from start to end date
- Respects contribution_start_date boundary
- Respects left_at boundary
- Skips existing periods (idempotent)
- Handles edge dates (year boundaries, leap years)
**Calendar Periods Tests:**
- Period boundaries correct for all intervals
- Period_end calculation correct
- Current period detection
- Last completed period detection
- Next period calculation
**Validation Tests:**
- Interval immutability enforced
- Same interval validation on type change
- Status transitions allowed
- Uniqueness constraints enforced
### Integration Testing
**Period Generation Flow:**
- Member creation triggers generation
- Type assignment triggers generation
- Type change regenerates future periods
- Scheduled job generates missing periods
- Left member stops generation
**Status Management Flow:**
- Mark single period as paid
- Bulk mark multiple periods
- Status transitions work
- Permissions enforced
**Contribution Type Management:**
- Create type
- Update amount (regeneration triggered)
- Cannot update interval
- Cannot delete if in use
### LiveView Testing
**Member List:**
- Status column displays correctly
- Toggle between last/current works
- Filters work correctly
- Color coding applied
**Member Detail:**
- Periods table displays all periods
- Checkboxes work
- Bulk marking works
- Type change validation works
- Actions only shown with permission
**Admin UI:**
- Type CRUD works
- Settings save correctly
- Validations display errors
- Only authorized users can access
### Edge Case Testing
**Interval Change Attempt:**
- Error message displayed
- No data modified
- User can cancel/choose different type
**Exit with Unpaid:**
- Warning shown
- Option to suspend offered
- Exit completes correctly
**Amount Change:**
- Warning displayed
- Only future unpaid regenerated
- Historical periods unchanged
**Date Boundaries:**
- Today = period start handled
- Today = period end handled
- Leap year handled
### Performance Testing
**Period Generation:**
- Generate 10 years of monthly periods: < 100ms
- Generate for 1000 members: < 5 seconds
- Idempotent check efficient (no full scan)
**Member List Query:**
- With status column: < 200ms for 1000 members
- Filters applied efficiently
- No N+1 queries
---
## Security Considerations
### Authorization
**Permissions Required:**
- ContributionType management: Admin only
- ContributionPeriod status changes: Admin + Treasurer
- View all periods: Admin + Treasurer + Board
- View own periods: All authenticated users
**Policy Enforcement:**
- All actions protected by Ash policies
- UI shows/hides based on permissions
- Backend validates permissions (never trust UI alone)
### Data Integrity
**Validation Layers:**
1. Database constraints (NOT NULL, UNIQUE, CHECK)
2. Ash validations (business rules)
3. UI validations (user experience)
**Immutability Protection:**
- Interval change prevented at multiple layers
- Period amounts immutable (audit trail)
- Settings changes logged (future)
### Audit Trail
**Tracked Information:**
- Period status changes (who, when) - future enhancement
- Type amount changes (implicit via period amounts)
- Member type assignments (via timestamps)
---
## Performance Considerations
### Database Indexes
**Required Indexes:**
- `contribution_periods(member_id)` - For member period lookups
- `contribution_periods(contribution_type_id)` - For type queries
- `contribution_periods(status)` - For unpaid filters
- `contribution_periods(period_start)` - For date range queries
- `contribution_periods(member_id, period_start)` - Composite unique index
- `members(contribution_type_id)` - For type membership count
### Query Optimization
**Preloading:**
- Load contribution_type with periods (avoid N+1)
- Load periods when displaying member detail
- Use Ash's load for efficient preloading
**Calculated Fields:**
- period_end calculated on-demand (not stored)
- current_period_status calculated when needed
- Use Ash calculations for lazy evaluation
**Pagination:**
- Period list paginated if > 50 periods
- Member list already paginated
### Caching Strategy
**No caching needed in MVP:**
- Contribution types rarely change
- Period queries are fast
- Settings read infrequently
**Future caching if needed:**
- Cache settings in application memory
- Cache contribution types list
- Invalidate on change
### Scheduled Job Performance
**Period Generation Job:**
- Run daily or weekly (not hourly)
- Batch members (process 100 at a time)
- Skip members with no changes
- Log failures for retry
---
## Future Enhancements
### Phase 2: Interval Change Support
**Architecture Changes:**
- Add logic to handle period overlaps
- Calculate prorata amounts if needed
- More complex validation
- Migration path for existing periods
### Phase 3: Payment Details
**Architecture Changes:**
- Add PaymentTransaction resource
- Link transactions to periods
- Support multiple payments per period
- Reconciliation logic
### Phase 4: vereinfacht.digital Integration
**Architecture Changes:**
- External API client module
- Webhook handling for transactions
- Automatic matching logic
- Manual review interface
---
**End of Architecture Document**

View file

@ -0,0 +1,527 @@
# 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