high priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

Receiving ChangePeriod emits WrappedSummaryLoading before any async work begins
The aggregation pipeline is invoked with the period carried in the ChangePeriod event, not the previously loaded period
On success, emits WrappedSummaryLoaded with `currentSlideIndex` reset to 0 (first slide of new period)
Milestone detection is re-run for the new period's data before the loaded state is emitted
Successful result is written to the offline cache keyed by the new period identifier
On network failure, attempts to read the cache entry for the requested period specifically
If the requested period has no cache entry and the network fails, emits WrappedSummaryError with code `PERIOD_UNAVAILABLE_OFFLINE`
Rapid ChangePeriod events (period selector tapped quickly) are handled with sequential or restartable transformer — no data from a stale period leaks into a newer period's state
The handler reuses the same internal fetch-annotate-cache-emit flow as `_onLoadSummary` without code duplication

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
bloc_concurrency
apis
Annual stats aggregation service (internal, period-parameterised)
Milestone detection service (internal)
Offline cache service (internal, keyed by period)
data models
SummaryPeriod
WrappedSummarySlide
WrappedMilestone
CachedSummary
performance requirements
Period switch must display loading indicator within 100ms of user interaction
Re-aggregation for a cached period should read from cache and skip the network call entirely
security requirements
Cache keys must be scoped per user ID + period to prevent data leakage between accounts

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Avoid duplicating the fetch-annotate-cache-emit sequence. Extract a private `_fetchAndEmit(Emitter emit, SummaryPeriod period)` method shared by both `_onLoadSummary` and `_onChangePeriod`. Cache lookups should use a compound key of `userId_year_quarter` to avoid collisions across users or periods. Always reset `currentSlideIndex` to 0 on a period change — the slide deck for a different year has a different length.

Use `sequential()` transformer if you want period changes to queue; use `restartable()` if you want the latest selection to cancel prior in-flight fetches. The restartable approach is recommended here to keep UI snappy.

Testing Requirements

Unit tests with bloc_test. Test cases: (1) emits [Loading, Loaded] with slide index 0 on period change, (2) loaded state reflects the requested period (not previous), (3) cache is written with new period key, (4) rapid period changes only emit final result (restartable), (5) emits [Loading, Offline] if cache for that period exists but network fails, (6) emits [Loading, Error(PERIOD_UNAVAILABLE_OFFLINE)] if network fails and no period cache. Verify `_fetchAndEmit` private helper is exercised by both `_onLoadSummary` and `_onChangePeriod` test suites.

Component
Wrapped Summary BLoC
service medium
Epic Risks (3)
medium impact medium prob technical

If the device transitions between online and offline states while the user is mid-session in the wrapped screen, the BLoC may emit conflicting state transitions (loaded → error → offline) that cause visual flickering or an inconsistent UI state such as showing the offline banner over an already-loaded summary.

Mitigation & Contingency

Mitigation: Implement a connectivity stream listener in the BLoC that only triggers a state re-evaluation when transitioning from online to offline, not on every connectivity event. Once a summary is in the Loaded state, the BLoC should not transition to error/offline unless the user explicitly requests a refresh. Store the last-loaded data in BLoC state so it survives connectivity changes.

Contingency: If state flickering is observed in testing, add a minimum 3-second debounce on connectivity state changes before the BLoC reacts, and display a non-blocking top banner rather than replacing the entire screen state.

high impact medium prob integration

The push notification deep-link to the wrapped-summary-screen must work correctly whether the app is in the foreground, background, or terminated state. Handling all three app launch states on both iOS and Android is a common source of edge-case bugs, particularly when authentication state must be restored before the deep link can be resolved.

Mitigation & Contingency

Mitigation: Implement deep-link handling through the existing notification-deep-link-handler component which already manages app-state-aware routing. Define the wrapped-summary route in the navigation config early in the epic so the router is ready before notification dispatch is wired. Test all three app states (foreground, background, terminated) explicitly in the QA checklist.

Contingency: If terminated-state deep-linking fails on specific platforms, fall back to launching the app to the home screen with an in-app notification banner prompting the user to open their summary, rather than direct deep-link navigation.

high impact low prob technical

The wrapped-summary-screen manages a large number of AnimationController instances (one or more per slide) via the wrapped-animation-controller. If disposal is not triggered correctly when the user exits mid-flow (e.g., via system back gesture or deep-link away), memory leaks will accumulate across session navigation.

Mitigation & Contingency

Mitigation: Implement screen disposal via Flutter's dispose() lifecycle method calling a single wrapped-animation-controller.disposeAll() method that iterates the named controller registry. Write a test that navigates to the screen, starts animations, then navigates away and verifies no active AnimationController listeners remain using Flutter's test binding.

Contingency: If disposal bugs are detected in production via memory profiling, patch by converting all AnimationControllers to use AutomaticKeepAliveClientMixin false and wrap each slide in a widget that disposes its own controller when removed from the widget tree.