Files
Mitgliederverwaltung/.plans/done/issue-42-feecalculationservice.md
shahondin1624 b29a268b1d Restructure .plans/ into done/ and open/ subdirectories
- Move completed plan files to .plans/done/
- Move 18 open plan files to .plans/open/
- Update .gitignore to exclude .verified_plans temp file
- Verified all 18 open plans still describe unimplemented issues
2026-04-28 20:30:55 +02:00

3.7 KiB

Plan: FeeCalculationService (Issue #42)

Summary

Implement the FeeCalculationService PHP class that provides fee calculation logic including family discount DSL, frozen rates for inactive members, and batch calculation. Also create the required FeeRule/FeeRecord entities, mappers, and a FeeController for API access. The service must integrate with existing MemberMapper and FamilyMapper.

Implementation Steps

Step 1: Create FeeRule entity (lib/Db/FeeRule.php)

  • Entity mapping oc_mv_fee_rules table columns: id, year_from, base_rate, family_rules_json, inactive_rule, created_at, created_by
  • jsonSerialize with parsed familyRules from JSON
  • addType for numeric fields

Step 2: Create FeeRuleMapper (lib/Db/FeeRuleMapper.php)

  • findById, findAll, findByYear (find rule where year_from <= $year, ordered DESC, first result)
  • Standard QBMapper pattern matching existing mappers

Step 3: Create FeeRecord entity (lib/Db/FeeRecord.php)

  • Entity mapping oc_mv_fee_records columns: id, member_id, year, amount, rule_snapshot_json, manuell_angepasst, paid, payment_date, notes, created_at, updated_at
  • jsonSerialize

Step 4: Create FeeRecordMapper (lib/Db/FeeRecordMapper.php)

  • findByMemberId, findByYear, findByMemberAndYear, countByYear
  • Standard QBMapper pattern

Step 5: Create FeeCalculationService (lib/Service/FeeCalculationService.php)

Core methods:

  • calculateFee($member, $family, $ruleForYear) - pure function:
    1. If rolle == 'erziehungsberechtigter' -> return 0 (no fee)
    2. If status == 'inaktiv' and frozenFeeRate set -> return frozen rate
    3. Get all active children in family sorted by geburtsdatum ASC (oldest first)
    4. Find member's position (1st, 2nd, 3rd+ child)
    5. Apply family_rules_json for that position
    6. Members without family -> use base_rate
  • batchCalculate($year) - iterate all active+paying members, skip manuell_angepasst
  • getRulesForYear($year) - find applicable rule
  • getFeeRecordsForYear($year) - all records for a year
  • getMemberFeeHistory($memberId) - all records for a member
  • markAsPaid($recordId, $paymentDate)
  • manualOverride($recordId, $amount, $notes)
  • createRule($data) - create a new fee rule
  • updateRule($id, $data) - update existing rule

Step 6: Create FeeController (lib/Controller/FeeController.php)

REST endpoints following existing controller patterns:

  • GET /api/v1/fees/rules - list fee rules
  • POST /api/v1/fees/rules - create rule
  • PUT /api/v1/fees/rules/{id} - update rule
  • GET /api/v1/fees/records?year=2026 - get records for year
  • GET /api/v1/fees/members/{memberId}/records - member history
  • POST /api/v1/fees/batch-calculate - trigger batch calculation
  • PUT /api/v1/fees/records/{id}/paid - mark as paid
  • PUT /api/v1/fees/records/{id}/override - manual override

Step 7: Register routes in appinfo/routes.php

Add fee-related routes following existing patterns.

AC Verification Checklist

  1. FeeRule entity exists with correct column mappings
  2. FeeRuleMapper can find rules by year
  3. FeeRecord entity exists with correct column mappings
  4. FeeRecordMapper supports finding by member and year
  5. calculateFee returns 0 for Erziehungsberechtigter
  6. calculateFee uses frozen rate for inactive members
  7. calculateFee determines child position by age and applies family discount
  8. calculateFee uses base_rate for members without family
  9. batchCalculate iterates all active members, skips manuell_angepasst, creates/updates records
  10. markAsPaid updates payment status and date
  11. manualOverride sets amount and notes, marks manuell_angepasst=true
  12. FeeController exposes all required REST endpoints
  13. Routes are registered in routes.php
  14. Code follows existing patterns (namespace, error handling, doc comments)