20 KiB
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 - 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
- Period generation separated from status management
- Calendar logic isolated in dedicated module
-
No Redundancy:
- No
period_endfield (calculated fromperiod_start+interval) - No
interval_typefield (read fromcontribution_type.interval) - Eliminates data inconsistencies
- No
-
Immutability Where Important:
contribution_type.intervalcannot be changed after creation- Prevents complex migration scenarios
- Enforced via Ash change validation
-
Historical Accuracy:
amountstored per period for audit trail- Enables tracking of contribution changes over time
- Old periods retain original amounts
-
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 and database_schema.dbml for complete schema documentation.
New Tables
-
contribution_types- Purpose: Define contribution types with fixed intervals
- Key Constraint:
intervalfield immutable after creation - Relationships: has_many members, has_many contribution_periods
-
contribution_periods- Purpose: Individual contribution periods for members
- Key Design: NO
period_endorinterval_typefields (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 startleft_at- For limiting period generation- These fields must remain member fields and should not be replaced by custom fields in the future
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:
- Member contribution type assigned (via Ash change)
- Member created with contribution type (via Ash change)
- Scheduled job runs (daily/weekly cron)
- Admin manual regeneration (UI action)
Algorithm Steps:
- Retrieve member with contribution_type and dates
- Determine first period start (based on contribution_start_date)
- Calculate all period starts from first to today (or left_at)
- Query existing periods for member
- Generate missing periods with current contribution_type.amount
- 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 startcalculate_period_end/2- Given period_start and interval, calculate endnext_period_start/2- Given period_start and interval, find nextis_current_period?/2- Check if period contains todayis_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 :paidmark_as_suspended- Set status to :suspendedmark_as_unpaid- Set status to :unpaid (error correction)
Bulk Operations:
bulk_mark_as_paid- Mark multiple periods as paid (efficiency)- low priority, can be a future issue
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:
- Keep all existing periods unchanged
- Find future unpaid periods
- Delete future unpaid periods
- 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:
- Add fields via migration
- Add relationships (belongs_to, has_many)
- Add calculations (current_period_status, overdue_count)
- 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
Required Permissions:
ContributionType.create/update/destroy- Admin onlyContributionType.read- Admin, Treasurer, BoardContributionPeriod.update(status changes) - Admin, TreasurerContributionPeriod.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:
- ContributionType index/form (admin)
- ContributionPeriod table component (member detail view)
- Settings form section (admin)
- 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?/3helper 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 (low prio)
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 (low prio)
- 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 (low prio)
- 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:
- Database constraints (NOT NULL, UNIQUE, CHECK)
- Ash validations (business rules)
- 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 lookupscontribution_periods(contribution_type_id)- For type queriescontribution_periods(status)- For unpaid filterscontribution_periods(period_start)- For date range queriescontribution_periods(member_id, period_start)- Composite unique indexmembers(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