mitgliederverwaltung/docs/contributions-architecture.md
2025-12-03 14:32:09 +01:00

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:


Table of Contents

  1. Architecture Principles
  2. Domain Structure
  3. Data Architecture
  4. Business Logic Architecture
  5. Integration Points
  6. Acceptance Criteria
  7. Testing Strategy
  8. Security Considerations
  9. 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 and 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

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