b29a268b1d
- 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
3.7 KiB
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:- If rolle == 'erziehungsberechtigter' -> return 0 (no fee)
- If status == 'inaktiv' and frozenFeeRate set -> return frozen rate
- Get all active children in family sorted by geburtsdatum ASC (oldest first)
- Find member's position (1st, 2nd, 3rd+ child)
- Apply family_rules_json for that position
- Members without family -> use base_rate
batchCalculate($year)- iterate all active+paying members, skip manuell_angepasstgetRulesForYear($year)- find applicable rulegetFeeRecordsForYear($year)- all records for a yeargetMemberFeeHistory($memberId)- all records for a membermarkAsPaid($recordId, $paymentDate)manualOverride($recordId, $amount, $notes)createRule($data)- create a new fee ruleupdateRule($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
- FeeRule entity exists with correct column mappings
- FeeRuleMapper can find rules by year
- FeeRecord entity exists with correct column mappings
- FeeRecordMapper supports finding by member and year
- calculateFee returns 0 for Erziehungsberechtigter
- calculateFee uses frozen rate for inactive members
- calculateFee determines child position by age and applies family discount
- calculateFee uses base_rate for members without family
- batchCalculate iterates all active members, skips manuell_angepasst, creates/updates records
- markAsPaid updates payment status and date
- manualOverride sets amount and notes, marks manuell_angepasst=true
- FeeController exposes all required REST endpoints
- Routes are registered in routes.php
- Code follows existing patterns (namespace, error handling, doc comments)