# 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**