critical priority high complexity backend pending backend specialist Tier 0

Acceptance Criteria

exclusive_groups configuration is parsed from org config (Supabase or local config) into a typed ExclusiveGroupConfig model with at least: group_id, group_name, mutually_exclusive_type_ids[]
Given expense selection [km-allowance, bus-ticket] where both share exclusive_group='transport-mode', validation returns ExpenseValidationResult.failure with error code MUTUAL_EXCLUSION_VIOLATION
Given expense selection [km-allowance, parking] where they do NOT share an exclusive group, validation returns ExpenseValidationResult.success
Multiple violations in a single submission are all reported: if two separate exclusive groups are violated, the result contains two violation entries
Empty expense list passes validation (no mutual exclusion violations possible with 0 or 1 item)
Expense types not present in any exclusive_group are not affected by mutual exclusion rules
Validation is stateless and pure: same input always produces same output regardless of call order
Validation enforced at service layer: validation result is checked before any Supabase insert — submission is rejected if violations exist
Validation result is strongly typed (sealed class or enum-based), not a raw boolean or string
Rule engine handles graceful degradation: if exclusive_groups config fails to load, submission is blocked with a config-error result (fail-safe, never silently permits invalid combos)
UI may call validateExpenses() pre-submission to show inline errors, but server-side enforcement remains the authoritative check

Technical Requirements

frameworks
Flutter
Riverpod
BLoC
apis
Supabase (to fetch org-specific exclusive_groups config)
expense-type-repository (internal)
data models
ExpenseType
ExclusiveGroupConfig
ExpenseValidationResult
ExpenseValidationViolation
performance requirements
Validation must complete synchronously (no async) once config is loaded — O(n * g) where n = selected expense types, g = number of exclusive groups
Config must be cached in memory after first load — do not re-fetch from Supabase on every validation call
Validation must add less than 1ms overhead to the submission flow
security requirements
Mutual exclusion rules are org-specific — ensure config loaded matches the authenticated user's org, not a cached config from a previous org session
Validation result must be evaluated server-side (in Supabase Edge Function or RLS policy) as a second enforcement layer — the Flutter service is the primary but not sole enforcement point
Validation bypass via API calls directly to Supabase must be blocked by RLS or Edge Function validation

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

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 selected, ExclusiveGroupConfig config). This makes it trivially testable and composable with other validators. Model ExclusiveGroupConfig as a Map> for O(1) group lookup.

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.

Component
Expense Validation Service
service high
Epic Risks (3)
high impact medium prob scope

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.

medium impact high prob technical

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.

medium impact medium prob integration

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.