# Membership Fees - Technical Architecture **Project:** Mila - Membership Management System **Feature:** Membership Fee 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 Fees system. It focuses on architectural decisions, patterns, module structure, and integration points **without** concrete implementation details. **Related Documents:** - [membership-fee-overview.md](./membership-fee-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 - Cycle generation separated from status management - Calendar logic isolated in dedicated module 2. **No Redundancy:** - No `cycle_end` field (calculated from `cycle_start` + `interval`) - No `interval_type` field (read from `membership_fee_type.interval`) - Eliminates data inconsistencies 3. **Immutability Where Important:** - `membership_fee_type.interval` cannot be changed after creation - Prevents complex migration scenarios - Enforced via Ash change validation 4. **Historical Accuracy:** - `amount` stored per cycle for audit trail - Enables tracking of membership fee changes over time - Old cycles retain original amounts 5. **Calendar-Based Cycles:** - All cycles aligned to calendar boundaries - Simplifies date calculations - Predictable cycle generation --- ## Domain Structure ### Ash Domain: `Mv.MembershipFees` **Purpose:** Encapsulates all membership fee-related resources and logic **Resources:** - `MembershipFeeType` - Membership fee type definitions (admin-managed) - `MembershipFeeCycle` - Individual membership fee cycles per member **Extensions:** - Member resource extended with membership fee fields ### Module Organization ``` lib/ ├── membership_fees/ │ ├── membership_fees.ex # Ash domain definition │ ├── membership_fee_type.ex # MembershipFeeType resource │ ├── membership_fee_cycle.ex # MembershipFeeCycle resource │ └── changes/ │ ├── prevent_interval_change.ex # Validates interval immutability │ ├── set_membership_fee_start_date.ex # Auto-sets start date │ └── validate_same_interval.ex # Validates interval match on type change ├── mv/ │ └── membership_fees/ │ ├── cycle_generator.ex # Cycle generation algorithm │ └── calendar_cycles.ex # Calendar cycle calculations └── membership/ └── member.ex # Extended with membership fee relationships ``` ### Separation of Concerns **Domain Layer (Ash Resources):** - Data validation - Relationship management - Policy enforcement - Action definitions **Business Logic Layer (`Mv.MembershipFees`):** - Cycle 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. **`membership_fee_types`** - Purpose: Define membership fee types with fixed intervals - Key Constraint: `interval` field immutable after creation - Relationships: has_many members, has_many membership_fee_cycles 2. **`membership_fee_cycles`** - Purpose: Individual membership fee cycles for members - Key Design: NO `cycle_end` or `interval_type` fields (calculated) - Relationships: belongs_to member, belongs_to membership_fee_type - Composite uniqueness: One cycle per member per cycle_start ### Member Table Extensions **Fields Added:** - `membership_fee_type_id` (FK, NOT NULL with default from settings) - `membership_fee_start_date` (Date, nullable) **Existing Fields Used:** - `joined_at` - For calculating membership fee start - `left_at` - For limiting cycle generation - These fields must remain member fields and should not be replaced by custom fields in the future ### Settings Integration **Global Settings:** - `membership_fees.include_joining_cycle` (Boolean) - `membership_fees.default_membership_fee_type_id` (UUID) **Storage:** Existing settings mechanism (TBD: dedicated table or configuration resource) ### Foreign Key Behaviors | Relationship | On Delete | Rationale | |--------------|-----------|-----------| | `membership_fee_cycles.member_id → members.id` | CASCADE | Remove membership fee cycles when member deleted | | `membership_fee_cycles.membership_fee_type_id → membership_fee_types.id` | RESTRICT | Prevent membership fee type deletion if cycles exist | | `members.membership_fee_type_id → membership_fee_types.id` | RESTRICT | Prevent membership fee type deletion if assigned to members | --- ## Business Logic Architecture ### Cycle Generation System **Component:** `Mv.MembershipFees.CycleGenerator` **Responsibilities:** - Calculate which cycles should exist for a member - Generate missing cycles - Respect membership_fee_start_date and left_at boundaries - Skip existing cycles (idempotent) **Triggers:** 1. Member membership fee type assigned (via Ash change) 2. Member created with membership fee type (via Ash change) 3. Scheduled job runs (daily/weekly cron) 4. Admin manual regeneration (UI action) **Algorithm Steps:** 1. Retrieve member with membership fee type and dates 2. Determine first cycle start (based on membership_fee_start_date) 3. Calculate all cycle starts from first to today (or left_at) 4. Query existing cycles for member 5. Generate missing cycles with current membership fee type's amount 6. Insert new cycles (batch operation) **Edge Case Handling:** - If membership_fee_start_date is NULL: Calculate from joined_at + global setting - If left_at is set: Stop generation at left_at - If membership fee type changes: Handled separately by regeneration logic ### Calendar Cycle Calculations **Component:** `Mv.MembershipFees.CalendarCycles` **Responsibilities:** - Calculate cycle boundaries based on interval type - Determine current cycle - Determine last completed cycle - Calculate cycle_end from cycle_start + interval **Functions (high-level):** - `calculate_cycle_start/3` - Given date and interval, find cycle start - `calculate_cycle_end/2` - Given cycle_start and interval, calculate end - `next_cycle_start/2` - Given cycle_start and interval, find next - `is_current_cycle?/2` - Check if cycle contains today - `is_last_completed_cycle?/2` - Check if cycle 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 `MembershipFeeCycle` **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 cycles as paid (efficiency) - low priority, can be a future issue ### Membership Fee Type Change Handling **Component:** Ash change on `Member.membership_fee_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 cycles unchanged 2. Find future unpaid cycles 3. Delete future unpaid cycles 4. Regenerate cycles with new membership_fee_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_cycle_status, overdue_count) 4. Add changes (auto-set membership_fee_start_date, validate interval) **Backward Compatibility:** - New fields nullable or with defaults - Existing members get default membership fee 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 membership fee type must exist) **Access Pattern:** - Read settings during cycle 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:** - `MembershipFeeType.create/update/destroy` - Admin only - `MembershipFeeType.read` - Admin, Treasurer, Board - `MembershipFeeCycle.update` (status changes) - Admin, Treasurer - `MembershipFeeCycle.read` - Admin, Treasurer, Board, Own member **Policy Patterns:** - Use existing HasPermission check - Leverage existing roles (Admin, Kassenwart) - Member can read own cycles (linked via member_id) ### LiveView Integration **New LiveViews Required:** 1. MembershipFeeType index/form (admin) 2. MembershipFeeCycle table component (member detail view) 3. Settings form section (admin) 4. Member list column (membership fee status) **Existing LiveViews to Extend:** - Member detail view: Add membership fees section - Member list view: Add status column - Settings page: Add membership fees section **Authorization Helpers:** - Use existing `can?/3` helper for UI conditionals - Check permissions before showing actions --- ## Acceptance Criteria ### MembershipFeeType Resource **AC-MFT-1:** Admin can create membership fee type with name, amount, interval, description **AC-MFT-2:** Interval field is immutable after creation (validation error on change attempt) **AC-MFT-3:** Admin can update name, amount, description (but not interval) **AC-MFT-4:** Cannot delete membership fee type if assigned to members **AC-MFT-5:** Cannot delete membership fee type if cycles exist referencing it **AC-MFT-6:** Interval must be one of: monthly, quarterly, half_yearly, yearly ### MembershipFeeCycle Resource **AC-MFC-1:** Cycle has cycle_start, status, amount, notes, member_id, membership_fee_type_id **AC-MFC-2:** cycle_end is calculated, not stored **AC-MFC-3:** Status defaults to :unpaid **AC-MFC-4:** One cycle per member per cycle_start (uniqueness constraint) **AC-MFC-5:** Amount is set at generation time from membership_fee_type.amount **AC-MFC-6:** Cycles cascade delete when member deleted **AC-MFC-7:** Admin/Treasurer can change status **AC-MFC-8:** Member can read own cycles ### Member Extensions **AC-M-1:** Member has membership_fee_type_id field (NOT NULL with default) **AC-M-2:** Member has membership_fee_start_date field (nullable) **AC-M-3:** New members get default membership fee type from global setting **AC-M-4:** membership_fee_start_date auto-set based on joined_at and global setting **AC-M-5:** Admin can manually override membership_fee_start_date **AC-M-6:** Cannot change to membership fee type with different interval (MVP) ### Cycle Generation **AC-CG-1:** Cycles generated when member gets membership fee type **AC-CG-2:** Cycles generated when member created (via change hook) **AC-CG-3:** Scheduled job generates missing cycles daily **AC-CG-4:** Generation respects membership_fee_start_date **AC-CG-5:** Generation stops at left_at if member exited **AC-CG-6:** Generation is idempotent (skips existing cycles) **AC-CG-7:** Cycles align to calendar boundaries (1st of month/quarter/half/year) **AC-CG-8:** Amount comes from membership_fee_type at generation time ### Calendar Logic **AC-CL-1:** Monthly cycles: 1st to last day of month **AC-CL-2:** Quarterly cycles: 1st of Jan/Apr/Jul/Oct to last day of quarter **AC-CL-3:** Half-yearly cycles: 1st of Jan/Jul to last day of half **AC-CL-4:** Yearly cycles: Jan 1 to Dec 31 **AC-CL-5:** cycle_end calculated correctly for all interval types **AC-CL-6:** Current cycle determined correctly based on today's date **AC-CL-7:** Last completed cycle determined correctly ### Membership Fee 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 cycles regenerated **AC-TC-4:** On allowed change: paid/suspended cycles 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_cycle (boolean, default true) **AC-S-2:** Global setting: default_membership_fee_type_id (UUID, required) **AC-S-3:** Admin can modify settings via UI **AC-S-4:** Settings validated (e.g., default membership fee type must exist) **AC-S-5:** Settings applied to new members immediately ### UI - Member List **AC-UI-ML-1:** New column shows membership fee status **AC-UI-ML-2:** Default: Shows last completed cycle status **AC-UI-ML-3:** Optional: Toggle to show current cycle status **AC-UI-ML-4:** Color coding: green (paid), red (unpaid), gray (suspended) **AC-UI-ML-5:** Filter: Unpaid in last cycle **AC-UI-ML-6:** Filter: Unpaid in current cycle ### UI - Member Detail **AC-UI-MD-1:** Membership fees section shows all cycles **AC-UI-MD-2:** Table columns: Cycle, Interval, Amount, Status, Actions **AC-UI-MD-3:** Checkbox per cycle for bulk marking (low prio) **AC-UI-MD-4:** "Mark selected as paid" button **AC-UI-MD-5:** Dropdown to change membership fee 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 - Membership Fee Types Admin **AC-UI-CTA-1:** List all membership fee types **AC-UI-CTA-2:** Show: Name, Amount, Interval, Member count **AC-UI-CTA-3:** Create new membership fee 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:** Membership fees section in settings **AC-UI-SA-2:** Dropdown to select default membership fee type **AC-UI-SA-3:** Checkbox: Include joining cycle **AC-UI-SA-4:** Explanatory text with examples **AC-UI-SA-5:** Save button with validation --- ## Testing Strategy ### Unit Testing **Cycle Generator Tests:** - Correct cycle_start calculation for all interval types - Correct cycle count from start to end date - Respects membership_fee_start_date boundary - Respects left_at boundary - Skips existing cycles (idempotent) - Handles edge dates (year boundaries, leap years) **Calendar Cycles Tests:** - Cycle boundaries correct for all intervals - cycle_end calculation correct - Current cycle detection - Last completed cycle detection - Next cycle calculation **Validation Tests:** - Interval immutability enforced - Same interval validation on type change - Status transitions allowed - Uniqueness constraints enforced ### Integration Testing **Cycle Generation Flow:** - Member creation triggers generation - Type assignment triggers generation - Type change regenerates future cycles - Scheduled job generates missing cycles - Left member stops generation **Status Management Flow:** - Mark single cycle as paid - Bulk mark multiple cycles (low prio) - Status transitions work - Permissions enforced **Membership Fee 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:** - Cycles table displays all cycles - Checkboxes work - Bulk marking works (low prio) - Membership fee 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 cycles unchanged **Date Boundaries:** - Today = cycle start handled - Today = cycle end handled - Leap year handled ### Performance Testing **Cycle Generation:** - Generate 10 years of monthly cycles: < 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:** - Membership fee type management: Admin only - Membership fee cycle status changes: Admin + Treasurer - View all cycles: Admin + Treasurer + Board - View own cycles: 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 - Cycle amounts immutable (audit trail) - Settings changes logged (future) ### Audit Trail **Tracked Information:** - Cycle status changes (who, when) - future enhancement - Membership fee type amount changes (implicit via cycle amounts) --- ## Performance Considerations ### Database Indexes **Required Indexes:** - `membership_fee_cycles(member_id)` - For member cycle lookups - `membership_fee_cycles(membership_fee_type_id)` - For type queries - `membership_fee_cycles(status)` - For unpaid filters - `membership_fee_cycles(cycle_start)` - For date range queries - `membership_fee_cycles(member_id, cycle_start)` - Composite unique index - `members(membership_fee_type_id)` - For type membership count ### Query Optimization **Preloading:** - Load membership_fee_type with cycles (avoid N+1) - Load cycles when displaying member detail - Use Ash's load for efficient preloading **Calculated Fields:** - cycle_end calculated on-demand (not stored) - current_cycle_status calculated when needed - Use Ash calculations for lazy evaluation **Pagination:** - Cycle list paginated if > 50 cycles - Member list already paginated ### Caching Strategy **No caching needed in MVP:** - Membership fee types rarely change - Cycle queries are fast - Settings read infrequently **Future caching if needed:** - Cache settings in application memory - Cache membership fee types list - Invalidate on change ### Scheduled Job Performance **Cycle 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 cycle overlaps - Calculate prorata amounts if needed - More complex validation - Migration path for existing cycles ### Phase 3: Payment Details **Architecture Changes:** - Add PaymentTransaction resource - Link transactions to cycles - Support multiple payments per cycle - 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**