Implement search path selection and query normalization
epic-contact-list-management-business-logic-task-006 — Wire the threshold-based path selection logic in ContactSearchService so it automatically dispatches to either local or remote search based on the current dataset size. Extract query normalization into a shared private utility method used by both paths. Ensure the debounce duration is configurable and defaults to 300ms for the remote path.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
The `_normalise` method should be a static private method or a top-level function in a companion file — this makes it independently testable without instantiating ContactSearchService. Implementation: `query.trim().toLowerCase()`. NFC normalisation via the `characters` package is recommended for robustness but verify it handles all three Norwegian vowels correctly before adding the dependency. The path selection should read `contacts.length` passed in at call time, not store a cached count — this avoids stale routing after list updates.
Consider making `ContactSearchService` a `ChangeNotifier`-free plain Dart class: Riverpod's provider wraps it, and the service itself stays pure. Document the threshold constant with a comment explaining why 200 was chosen (e.g., empirical testing showed local filtering stays under 50ms for up to 200 contacts on a low-end device).
Testing Requirements
Unit tests (flutter_test) for path selection boundary conditions: size = threshold-1 (local), size = threshold (remote), size = threshold+1 (remote). Test normalisation method independently with Norwegian characters, leading/trailing whitespace, and empty input. Test that switching dataset size between calls routes correctly. No integration tests needed at this layer — repository interactions are covered in task-005 tests.
Use dependency injection to supply a mock for the remote path in path-selection tests to avoid real network calls.
For organizations with large contact lists (NHF has 1,400 local chapters and potentially thousands of contacts), local in-memory filtering may be too slow and Supabase ILIKE queries without supporting indexes may exceed acceptable response times or accumulate excessive read costs, degrading search usability for power users.
Mitigation & Contingency
Mitigation: Define and document the list-size threshold in ContactSearchService before implementation. Confirm that indexes on name and notes columns exist in the Supabase schema before enabling server-side search. Profile ContactSearchService against realistic data volumes in the staging environment using the largest expected org.
Contingency: If response times are unacceptable in staging, introduce result-count pagination in ContactListService and add a user-visible 'showing top N results — refine your search' indicator, deferring full pagination to a follow-up task.
In NHF's multi-chapter context, when a user switches organization, Riverpod providers may emit a brief window of stale contact data scoped to the previous organization before the invalidation cycle completes, transiently exposing contacts from the wrong chapter.
Mitigation & Contingency
Mitigation: Model organization context as a Riverpod provider dependency so that any context change immediately marks contact providers as stale. Render a loading skeleton instead of the stale list during the re-fetch transition. Cover this scenario in integration tests with explicit org-switch sequences.
Contingency: If race conditions are observed during QA, add an explicit organization_id equality check in ContactListService that compares each fetched record's scope to the active session org, discarding any mismatched batch before returning results to the provider.