Add specialisation tag multi-select filter to MentorFilterService
epic-geographic-peer-mentor-map-core-services-task-010 — Implement multi-select specialisation tag filtering in MentorFilterService. Tags are organisation-defined strings (e.g. hearing-loss, mobility). Filter logic: if no tags selected, return all; if one or more tags selected, return mentors matching ANY selected tag (OR semantics). Provide a method to retrieve the available tag vocabulary for population of the filter panel UI. Add unit tests for empty, single-tag, and multi-tag scenarios.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
In FilterCriteria, initialise specialisationTags as const [] (not null) to avoid null checks throughout the codebase. Normalisation in validate() should use a LinkedHashSet to deduplicate while preserving insertion order, then convert to a sorted list for deterministic query generation. The .overlaps() Supabase operator maps to the Postgres && array operator — confirm the database column is a text[] or varchar[] type and not JSONB; if JSONB, use .contains() instead. For getAvailableTagVocabulary, add a simple in-memory cache using a Map
Expose a clearVocabularyCache() method for testing. The organisation-scoped vocabulary directly supports the multi-org architecture described in the app (NHF, HLF, Blindeforbundet each define their own tag sets — e.g., 'hearing-loss', 'mobility').
Testing Requirements
Pure unit tests in flutter_test for all filter logic. Mock the Supabase client for getAvailableTagVocabulary tests using mocktail — test the success path, empty-result path, and network error path (expecting a propagated exception). Write parameterised tests for the normalisation logic covering: duplicates, mixed case, leading/trailing whitespace, empty string removal. Verify OR semantics: given a mock list of MentorLocation with tags ['hearing-loss'], ['mobility'], ['hearing-loss','mobility'], and a filter of ['hearing-loss'], confirm all mentors with 'hearing-loss' are included and the mobility-only mentor is excluded (this is an integration-level assertion on the Supabase overlaps semantics — document as an integration test to run against the emulator).
Target 100% branch coverage on tag normalisation and clause generation.
The dual BLoC state machines (map view state + filter state) may introduce subtle synchronisation bugs where filter changes do not correctly re-trigger viewport queries, causing stale data to appear on the map.
Mitigation & Contingency
Mitigation: Define all BLoC state transitions in a state diagram before implementation. Use flutter_bloc's BlocObserver in development mode to log every state transition. Write explicit unit tests for filter-change → re-query transitions.
Contingency: If state synchronisation bugs appear in integration testing, refactor to a single unified BLoC that owns both map viewport state and filter state, eliminating cross-BLoC dependencies.
Cached mentor location data may become stale (mentors move, pause, or revoke consent) and coordinators in offline mode could be shown incorrect mentor information, leading to wasted outreach.
Mitigation & Contingency
Mitigation: Display a clear timestamp on cached data indicating when it was last synced. Set cache TTL to 24 hours and show an 'offline — data from [date]' banner. Revoked consent removes the mentor from the cache on next successful sync via contact-cache-sync-repository.
Contingency: If cache staleness causes user complaints, reduce TTL to 4 hours and implement background sync on app foreground. Accept that very-recently-revoked mentors may appear briefly in offline mode — document this as a known limitation in the privacy policy.