Implement mutual exclusion rule engine for expense validation
epic-travel-expense-registration-core-services-task-001 — Build the core mutual exclusion validation logic within the expense validation service. Implement exclusive_groups configuration parsing so that expense types sharing a group (e.g., km-allowance and bus-ticket) cannot be selected simultaneously. This is a hard HLF contractual requirement — the rule must be enforced at the service layer, not merely hinted in the UI.
Acceptance Criteria
Technical Requirements
Implementation Notes
This is a contractual HLF requirement — treat it as a financial compliance rule, not a UX nicety. The rule engine should be a pure function: ExpenseValidationResult validateMutualExclusion(List
Algorithm: for each exclusive group, count how many selected expense types belong to it; if count > 1, add a violation. Store violations as a list to support multiple violations per submission. Do NOT place this logic in the UI layer (BLoC or widget) — it belongs in the service layer so that coordinator bulk-registration and proxy submission paths also enforce it. Given that HLF's workshop notes explicitly state 'faste valg for utleggstype for å hindre feilkombinasjon' (fixed choices to prevent wrong combinations), the UI should also disable conflicting options after selection — but the service validation is the authoritative enforcement layer.
Consider adding an ExpenseValidationService.validateAll() method that runs mutual exclusion plus any future validators (amount limits, required receipts above 100 NOK) to keep the validation pipeline extensible.
Testing Requirements
Unit tests with flutter_test covering: (1) single exclusive group violation detected correctly, (2) multiple violations in one submission all reported, (3) non-conflicting types pass validation, (4) empty expense list passes, (5) expense types outside any group unaffected, (6) config load failure returns config-error validation result (not success), (7) org-A config does not apply to org-B validation. Property-based tests (if dart_test_framework supports it) for: given any list of expense types with no group overlap, validation always returns success. Integration test (separate file): submit expenses with km-allowance + bus-ticket via the full BLoC → service → repository flow and assert the submission is rejected before any Supabase insert is attempted. Target 95%+ line coverage given the critical nature of this rule.
Mutual exclusion rules are stored in the expense type catalogue's exclusive_groups field. If the catalogue schema or group definitions differ between HLF and Blindeforbundet, the validation service must handle multiple group configurations without hardcoding organisation-specific logic.
Mitigation & Contingency
Mitigation: Design the validation service to be purely data-driven: it reads exclusive_groups from the cached catalogue and enforces whichever groups are defined, with no hardcoded organisation names. Write parameterised unit tests covering at least 4 different catalogue configurations to verify generality.
Contingency: If an organisation requires non-standard exclusion semantics (e.g. partial exclusion within a group), introduce an exclusion_type field to the catalogue schema and extend the service, treating it as a catalogue configuration change rather than a code fork.
The attestation service subscribes to Supabase Realtime for live queue updates. On mobile, Realtime WebSocket connections can be dropped during network transitions, causing the coordinator queue to become stale without the user being aware.
Mitigation & Contingency
Mitigation: Implement connection lifecycle management: reconnect on network-change events, show a 'reconnecting' indicator when the subscription is broken, and perform a full queue refresh on reconnect rather than relying solely on delta events.
Contingency: Add a manual pull-to-refresh gesture on the attestation queue screen as a guaranteed fallback. If Realtime proves unreliable in production, switch to periodic polling (30-second interval) as a degraded but functional mode.
If a peer mentor submits a draft while offline and then submits the same claim again after connectivity is restored (thinking the first attempt failed), duplicate claims may be persisted in Supabase.
Mitigation & Contingency
Mitigation: Assign a client-generated idempotency key (UUID) to each draft at creation time. The submission service sends this key as an upsert key to Supabase, preventing duplicate inserts. The draft is marked 'submitted' locally after first successful upload.
Contingency: Implement a server-side duplicate detection trigger on the expense_claims table checking (activity_id, claimant_id, created_date) within a 24-hour window and returning the existing record ID rather than inserting a duplicate.