medium priority low complexity backend pending backend specialist Tier 3

Acceptance Criteria

ContributionData model contains a Map<String, Map<ActivityType, int>> field named monthlyActivityCounts keyed by ISO year-month string (e.g. '2025-03') with inner map keyed by ActivityType enum
Model includes cumulativeTotals field of type Map<ActivityType, int> summing all months
Model includes currentStreak (int), longestStreak (int), and lastActivityDate (DateTime?) fields for streak tracking
Model includes milestoneFlags field of type Map<String, bool> for extensible Phase 4 milestone tracking (e.g. 'first_activity', 'ten_activities', 'yearly_goal_met')
fromJson() and toJson() methods correctly round-trip all fields including nested maps and DateTime values as ISO 8601 strings
toWrappedInput() adapter method returns a WrappedInputDto (or Map<String, dynamic> stub) containing only the fields Phase 4 requires, documented with inline TODO comments referencing Phase 4 epic
copyWith() method preserves all unmodified fields when partially updating the model
Model has == and hashCode overrides for value equality
Dart doc comments on all public fields and methods, including a note on forward-compatibility design intent
No Supabase or BLoC imports in the model file — pure domain layer

Technical Requirements

frameworks
Flutter
Dart
data models
ContributionData
ActivityType
WrappedInputDto
performance requirements
toWrappedInput() must complete in O(n) time relative to number of months
JSON serialisation must not allocate excessive intermediate maps — use direct field assignment
security requirements
Model must not expose raw user identifiers; all fields are aggregate counts or flags only
No PII stored in ContributionData — coordinator and peer mentor IDs are held only in enclosing service context

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Place ContributionData in lib/features/stats/domain/models/contribution_data.dart to keep it in the domain layer, independent of Supabase or BLoC. Use a sealed ActivityType enum (or import the existing one from the activity domain) so the map keys are type-safe. For the toWrappedInput() stub, define a WrappedInputDto class in the same file or a sibling file with a TODO linking to Phase 4 epic, to prevent schema drift when Phase 4 is implemented. Prefer explicit Map.fromEntries() or map() transforms over imperative loops in toJson()/fromJson() for readability.

The milestoneFlags map should be String-keyed to allow Phase 4 to add new flags without a Dart model change — document this extensibility decision explicitly.

Testing Requirements

Unit tests using flutter_test. Test fromJson(toJson(model)) round-trip for fully populated, partially populated, and empty models. Test toWrappedInput() returns expected keys and values. Test copyWith() changes only the targeted field.

Test == returns true for identical instances and false for differing instances. Test streak fields handle null lastActivityDate gracefully. Minimum 95% line coverage on ContributionData model file.

Component
Personal Statistics Service
service medium
Epic Risks (3)
medium impact medium prob technical

fl_chart's default colour palette may not meet WCAG 2.2 AA contrast requirements when rendered on the app's dark or light backgrounds. If segment colours are insufficient, the donut chart will fail accessibility audits, which is a compliance blocker for all three organisations.

Mitigation & Contingency

Mitigation: Define all chart colours in the design token system with pre-validated contrast ratios. Run the contrast-ratio-validator against every chart colour during the adapter's unit tests. Use the contrast-safe-color-palette as the source palette.

Contingency: If a colour fails validation, replace with the nearest compliant token. If activity types exceed the available token set, implement a deterministic hashing algorithm that maps activity type IDs to compliant colours.

medium impact medium prob technical

StatsBloc subscribing to the activity registration stream creates a long-lived subscription. If the subscription is not disposed correctly when the dashboard is closed, it will cause a stream leak and potentially trigger re-fetches on a disposed BLoC, resulting in uncaught errors in production.

Mitigation & Contingency

Mitigation: Implement subscription disposal in the BLoC's close() override. Write a widget test that navigates away from the dashboard and asserts no BLoC events are emitted after disposal.

Contingency: If leaks are detected in QA, add a mounted check guard before emitting states from async callbacks, and audit all other BLoC stream subscriptions in the codebase for the same pattern.

low impact low prob scope

PersonalStatsService's Phase 4 gamification data structure is designed against an assumed future schema. If the Phase 4 Spotify Wrapped feature defines a different data contract when it is developed, the structure built now will require a breaking change and migration.

Mitigation & Contingency

Mitigation: Document the contribution data structure with explicit field semantics and versioning comments. Keep the Phase 4 fields as optional/nullable so they do not break existing consumers if the schema evolves.

Contingency: If the Phase 4 schema diverges significantly, the personal stats data can be re-mapped in a thin adapter layer without changing PersonalStatsService's core implementation.