high priority high complexity backend pending backend specialist Tier 4

Acceptance Criteria

MentorMapBloc is a Bloc<MentorMapEvent, MentorMapState> subclass with a single state that composes MapViewState and FilterPanelState as nested value objects
MapViewState has exactly four variants: MapViewLoading, MapViewLoaded(mentors), MapViewError(message), MapViewEmpty
FilterPanelState has exactly three variants: FilterPanelInactive, FilterPanelActive(criteria), FilterPanelApplying(criteria)
BoundingBoxChanged events are debounced by 400 ms using RxDart or EventTransformer.debounce before triggering a MentorLocationService fetch
FiltersApplied transitions FilterPanelState to FilterPanelApplying, triggers a fetch with the new criteria, then transitions to FilterPanelActive on success
FiltersReset clears FilterCriteria, transitions FilterPanelState to FilterPanelInactive, and triggers a fresh fetch
RefreshRequested bypasses debounce and immediately triggers a fetch with the current bounding box and active criteria
On network error, MapViewState transitions to MapViewError and FilterPanelState is not mutated
If the fetch returns an empty list, MapViewState transitions to MapViewEmpty (not MapViewError)
CacheHitState from MentorLocationService is propagated in MapViewLoaded with a lastUpdated timestamp field
BLoC is closed without errors when dispose is called during an in-flight fetch (StreamSubscription cancelled cleanly)
Unit tests cover all event→state transitions using bloc_test

Technical Requirements

frameworks
Flutter
flutter_bloc
BLoC
bloc_test
rxdart
apis
MentorLocationService.fetchMentorsInBoundingBox
MentorLocationService stream (if reactive)
data models
MentorLocation
FilterCriteria
BoundingBox
MapViewState
FilterPanelState
MentorMapState
MentorMapEvent
performance requirements
Debounce window of 400 ms on BoundingBoxChanged to prevent flooding Supabase during map pan/zoom
State objects must be immutable and implement Equatable to prevent unnecessary widget rebuilds
In-flight fetch must be cancelled (via CancelToken or StreamSubscription disposal) when a new BoundingBoxChanged event arrives after debounce
security requirements
BLoC must not cache raw mentor PII beyond the current loaded state — cleared on close()
Error messages emitted in MapViewError must not expose Supabase internals or stack traces to the UI layer

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Model MentorMapState as a single immutable class with two fields: mapView (MapViewState) and filterPanel (FilterPanelState), both sealed classes. Use copyWith to produce the next state, which keeps event handlers concise. For debounce, use EventTransformer from the bloc_concurrency package (restartable transformer) on the BoundingBoxChanged event handler — this also cancels in-flight fetches automatically. Do not use two separate BLoCs; keeping both state machines in one BLoC avoids cross-BLoC synchronisation complexity and ensures atomic state transitions.

Use equatable on all state and event classes. Keep MentorLocationService injection via constructor for testability. The BLoC should not know about the map widget library (e.g., flutter_map or google_maps) — all geographic types should be project-defined (BoundingBox, LatLng) so the BLoC remains UI-framework agnostic.

Testing Requirements

Use bloc_test package for all BLoC tests. Write an expect sequence test for each event type covering: the initial state, intermediate states (e.g., MapViewLoading emitted before MapViewLoaded), and the final steady state. Test debounce by emitting multiple rapid BoundingBoxChanged events and asserting that MentorLocationService.fetchMentorsInBoundingBox is called only once. Test concurrent event handling: emit FiltersApplied during an in-flight BoundingBoxChanged fetch and verify the BLoC does not emit inconsistent states.

Test error propagation: mock MentorLocationService to throw a NetworkException and verify MapViewError is emitted. Test cache hit: mock service to return CacheHitState and verify lastUpdated is present in MapViewLoaded. Minimum 95% branch coverage on event handlers.

Component
Mentor Location Service
service high
Epic Risks (2)
medium impact medium prob technical

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.

low impact medium prob scope

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.