critical priority medium complexity backend pending backend specialist Tier 4

Acceptance Criteria

MilestoneDetectionService.evaluate(AnnualStatsResult stats, List<MilestoneDefinition> definitions, AnnualStatsResult? previousStats) returns List<MilestoneResult> synchronously
MilestoneResult includes: definition (MilestoneDefinition), is_unlocked (bool), is_newly_unlocked (bool), description (String), actual_value (num)
A milestone is is_unlocked=true when stats value >= threshold_value for the corresponding threshold_type
A milestone is is_newly_unlocked=true when is_unlocked=true AND (previousStats is null OR previousStats value < threshold_value)
When previousStats is null (first-time Wrapped view), all unlocked milestones are marked newly_unlocked=true
description field is produced by replacing {{value}} and {{threshold}} tokens in description_template with actual stat values formatted with locale-appropriate number formatting (e.g., '100 timer' not '100.0')
Returned list contains only is_unlocked=true milestones, sorted by significance_rank descending
If AnnualStatsResult has zero total_hours and zero contacts_helped, the returned list is empty (no false positives)
Service is a pure function with no side effects — persistence is handled by task-009
All threshold_type enum cases are handled; an unknown type logs a warning and is skipped rather than throwing

Technical Requirements

frameworks
Flutter
BLoC
data models
annual_summary
activity
performance requirements
Evaluation of up to 50 milestone definitions against one AnnualStatsResult must complete in under 5ms
No async operations; synchronous pure function only
security requirements
No PII included in MilestoneResult descriptions — use aggregate stats only (hours, counts)
description_template substitution must escape any user-derived string values to prevent template injection in display

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Implement as a stateless class with a single public evaluate() method — no constructor injection of repos. Map threshold_type to the correct AnnualStatsResult field using a switch-case; avoid dynamic field access by name to keep type safety. For description_template substitution, use String.replaceAll with literal token strings ('{{value}}', '{{threshold}}') rather than regex to avoid complexity. Format numbers using NumberFormat from the intl package with the device locale.

Newly-unlocked detection: compare previousStats?.totalHours ?? -1 < definition.thresholdValue when threshold_type is total_hours. Keep the service free of BLoC/Riverpod imports — it is a plain Dart class testable without Flutter framework.

Testing Requirements

Unit tests using flutter_test: (1) evaluate with stats above multiple thresholds and assert correct is_unlocked=true set; (2) evaluate with previousStats below threshold and currentStats above — assert is_newly_unlocked=true; (3) evaluate with previousStats already above threshold — assert is_newly_unlocked=false; (4) evaluate with null previousStats — assert all unlocked milestones are newly_unlocked; (5) evaluate with zero stats — assert empty result list; (6) assert description tokens are correctly substituted; (7) assert result list is sorted by significance_rank descending; (8) assert unknown threshold_type is skipped without throwing. Coverage target: 100% of evaluate() method branches.

Component
Milestone Detection Service
service medium
Epic Risks (3)
high impact medium prob integration

Activity records may contain duplicate entries (as evidenced by the duplicate-detection feature dependency) or proxy-registered activities that should be attributed differently. Including duplicates or mis-attributed records would produce inflated stats, undermining trust in the summary.

Mitigation & Contingency

Mitigation: Implement the aggregation query to join against the deduplication-reviewed-flag on activity records and filter out unresolved duplicates. Coordinate with the duplicate-detection feature team to confirm the authoritative flag field before implementing the RPC. Include a data-quality warning in the summary when unresolved duplicates are detected.

Contingency: If deduplication state is unreliable at release time, add a prominent disclaimer in the summary UI noting that figures reflect all registered activities and may include duplicates pending review. Track a follow-up task to re-aggregate after deduplication runs.

medium impact high prob scope

Each organisation wants to define their own milestone thresholds (e.g., NHF's counting model differs from HLF's certification model). Implementing configurable thresholds may expand scope significantly if the configuration UI is expected in this epic.

Mitigation & Contingency

Mitigation: Scope this epic strictly to the evaluation engine and a hardcoded default threshold set. Define the MilestoneDefinition interface with an organisation_id discriminator so per-org configs can be loaded from the database in a later sprint. Build the admin configuration UI as a separate follow-on task outside this epic.

Contingency: If stakeholders require per-org milestone configuration before launch, deliver a JSON-based configuration file per org as an interim solution, loaded from Supabase storage, until a full admin UI is built.

medium impact medium prob technical

Android 13+ restricts access to media collections and requires READ_MEDIA_IMAGES permission for gallery saves, while older Android versions use WRITE_EXTERNAL_STORAGE. Handling both permission models correctly across the device matrix is error-prone.

Mitigation & Contingency

Mitigation: Use the permission_handler Flutter package with version-aware permission requests abstracted behind the summary-share-service interface. Write platform-specific unit tests for both Android API levels in the test harness. Test on a minimum of three Android versions (API 29, 32, 34) in CI.

Contingency: If gallery save is broken on specific Android versions at launch, disable the 'Save to gallery' option on affected API levels and surface only clipboard and system share sheet, which require no media permissions.