mitgliederverwaltung/docs/membership-fee-architecture.md
Moritz f5ef16ec20 docs: change wording
contribution -> membership fee
period -> cycle
2025-12-11 15:52:32 +01:00

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:


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
    • 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 and 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

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