From a5aeef3e2700e669c07b1709afeda1b3202f2760 Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 27 Nov 2025 21:30:06 +0100 Subject: [PATCH] docs: payment concept --- docs/contributions-architecture.md | 651 +++++++++++++++++++++++++++++ docs/contributions-overview.md | 527 +++++++++++++++++++++++ 2 files changed, 1178 insertions(+) create mode 100644 docs/contributions-architecture.md create mode 100644 docs/contributions-overview.md diff --git a/docs/contributions-architecture.md b/docs/contributions-architecture.md new file mode 100644 index 0000000..b01ad30 --- /dev/null +++ b/docs/contributions-architecture.md @@ -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** + diff --git a/docs/contributions-overview.md b/docs/contributions-overview.md new file mode 100644 index 0000000..03f347b --- /dev/null +++ b/docs/contributions-overview.md @@ -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