high priority low complexity backend pending backend specialist Tier 2

Acceptance Criteria

getUnresolvedCount() returns an integer >= 0 representing the number of unresolved pairs scoped to the coordinator's chapters
Uses a COUNT aggregate query — does NOT fetch full rows and count in Dart
Result is cached with a TTL of 30 seconds to prevent redundant calls on tab focus or widget rebuild
Cache is invalidated immediately when forceResolve() is called successfully (task-007)
Returns 0 (not null or error) when the coordinator has no unresolved pairs
Throws ChapterScopeException if the authenticated user has no assigned chapters
Count respects the same chapter scope as getUnresolvedPairs() — consistent filtering logic
Nav badge displays the count and disappears (or shows 0) when count is zero
Badge updates within 1 second of a successful resolution action

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Dart client
apis
Supabase PostgREST — COUNT query on duplicate_queue filtered by status and chapter_id
data models
CoordinatorChapterScope
UnresolvedCountCache
performance requirements
COUNT query must not fetch row data — use Supabase `.count()` method or head:true option
Cached result served within 1ms; cache miss query must complete within 200ms
Cache TTL: 30 seconds; invalidated eagerly on resolve action
security requirements
Same RLS chapter scope as getUnresolvedPairs() — chapter filter applied server-side
Cache key must be scoped to the authenticated user ID to prevent cross-user cache leakage
ui components
NavigationBadge widget consuming the count stream

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Supabase Dart supports `.count()` via `CountOption` — use `CountOption.exact` for precision. Example: `.from('duplicate_queue').select('', const FetchOptions(count: CountOption.exact)).eq('status', 'unresolved').in('chapter_id', chapterIds)` — the response `.count` field gives the integer without fetching rows. Implement the cache as a simple Riverpod `StateProvider<({int count, DateTime fetchedAt})?>` keyed to the user session. The provider is invalidated (set to null) by calling `ref.invalidate()` from forceResolve().

Use a Riverpod `FutureProvider` that checks the cache first and falls through to the repository only on miss or expiry. Ensure the nav badge widget uses `ref.watch` on this provider so it rebuilds reactively when the cache is invalidated.

Testing Requirements

Unit tests: verify COUNT query is issued (not a full-row fetch), cache returns stale value within TTL and fetches fresh value after TTL expires, cache invalidation triggers a fresh fetch. Test zero count returns 0 (not null). Test that cache is keyed per user (simulate two user sessions). Integration test: confirm count matches the actual number of unresolved rows in the test database.

Test that resolving a pair via forceResolve() reduces count by 1 on next call.

Component
Deduplication Queue Service
service medium
Epic Risks (2)
medium impact medium prob technical

If the duplicate check RPC fails due to a network error or Supabase outage, the service must decide whether to block submission entirely (safe but disruptive) or allow submission to proceed silently (functional but risks data duplication). An incorrect choice leads to either user frustration or data quality issues.

Mitigation & Contingency

Mitigation: Define an explicit error policy in the service: RPC failures result in a DuplicateCheckResult with status: 'check_failed' and no candidates. The caller treats this as 'allow submission, flag for async review'. Document this as the intended graceful degradation behaviour in the service interface contract.

Contingency: If stakeholders require blocking on RPC failure, expose a configurable `failMode` parameter in the service that can be toggled per organisation via the feature flag system without a code deployment.

medium impact medium prob scope

The DuplicateComparisonPanel must handle varying activity schemas across organisations (NHF, HLF, Blindeforbundet each have different activity fields). A rigid layout may not accommodate all field variations, causing truncation or missing data in the comparison view.

Mitigation & Contingency

Mitigation: Design the panel to render a dynamic list of key-value pairs rather than a fixed-column layout. Define a `ComparisonField` model that each service populates with only the fields relevant to the activity type and organisation, allowing the panel to adapt without schema knowledge.

Contingency: If dynamic rendering proves too complex within the timeline, ship a simplified panel showing only the five most critical fields (peer mentor, activity type, date, chapter, submitter) and log a follow-up ticket for full field rendering in a later sprint.