21 KiB
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 - Business logic and requirements
- database-schema-readme.md - Database documentation
- database_schema.dbml - Database schema definition
Table of Contents
- Architecture Principles
- Domain Structure
- Data Architecture
- Business Logic Architecture
- Integration Points
- Acceptance Criteria
- Testing Strategy
- Security Considerations
- Performance Considerations
Architecture Principles
Core Design Decisions
-
Single Responsibility:
- Each module has one clear responsibility
- Cycle generation separated from status management
- Calendar logic isolated in dedicated module
-
No Redundancy:
- No
cycle_endfield (calculated fromcycle_start+interval) - No
interval_typefield (read frommembership_fee_type.interval) - Eliminates data inconsistencies
- No
-
Immutability Where Important:
membership_fee_type.intervalcannot be changed after creation- Prevents complex migration scenarios
- Enforced via Ash change validation
-
Historical Accuracy:
amountstored per cycle for audit trail- Enables tracking of membership fee changes over time
- Old cycles retain original amounts
-
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 and database_schema.dbml for complete schema documentation.
New Tables
-
membership_fee_types- Purpose: Define membership fee types with fixed intervals
- Key Constraint:
intervalfield immutable after creation - Relationships: has_many members, has_many membership_fee_cycles
-
membership_fee_cycles- Purpose: Individual membership fee cycles for members
- Key Design: NO
cycle_endorinterval_typefields (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 startleft_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:
- Member membership fee type assigned (via Ash change)
- Member created with membership fee type (via Ash change)
- Scheduled job runs (daily/weekly cron)
- Admin manual regeneration (UI action)
Algorithm Steps:
- Retrieve member with membership fee type and dates
- Determine first cycle start (based on membership_fee_start_date)
- Calculate all cycle starts from first to today (or left_at)
- Query existing cycles for member
- Generate missing cycles with current membership fee type's amount
- 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 startcalculate_cycle_end/2- Given cycle_start and interval, calculate endnext_cycle_start/2- Given cycle_start and interval, find nextis_current_cycle?/2- Check if cycle contains todayis_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 :paidmark_as_suspended- Set status to :suspendedmark_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:
- Keep all existing cycles unchanged
- Find future unpaid cycles
- Delete future unpaid cycles
- 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:
- Add fields via migration
- Add relationships (belongs_to, has_many)
- Add calculations (current_cycle_status, overdue_count)
- 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
Required Permissions:
MembershipFeeType.create/update/destroy- Admin onlyMembershipFeeType.read- Admin, Treasurer, BoardMembershipFeeCycle.update(status changes) - Admin, TreasurerMembershipFeeCycle.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:
- MembershipFeeType index/form (admin)
- MembershipFeeCycle table component (member detail view)
- Settings form section (admin)
- 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?/3helper 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:
- Database constraints (NOT NULL, UNIQUE, CHECK)
- Ash validations (business rules)
- 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 lookupsmembership_fee_cycles(membership_fee_type_id)- For type queriesmembership_fee_cycles(status)- For unpaid filtersmembership_fee_cycles(cycle_start)- For date range queriesmembership_fee_cycles(member_id, cycle_start)- Composite unique indexmembers(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