high priority medium complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

CertificateExpiryNotificationState sealed class hierarchy defines: CertificateExpiryNotificationInitial, CertificateExpiryNotificationLoading, CertificateExpiryNotificationLoaded (contains List<ExpiryNotificationRecord>), CertificateExpiryNotificationError (contains String message), and CertificateExpiryNotificationEmpty
CertificateExpiryNotificationEvent sealed class hierarchy defines: LoadExpiryNotifications, RefreshExpiryNotifications, and AcknowledgeLapse (contains String notificationId)
ExpiryNotificationRecord data model includes: certificateName, expiryDate (DateTime), daysUntilExpiry (int, negative when expired), status (enum: expiringSoon | expired), and isAcknowledged (bool)
BLoC class CertificateExpiryNotificationBloc extends Bloc<CertificateExpiryNotificationEvent, CertificateExpiryNotificationState> and registers handlers for all three events
BLoC constructor accepts an ExpiryNotificationRepository interface (abstract class), not a concrete implementation, enabling testability
LoadExpiryNotifications and RefreshExpiryNotifications handlers emit Loading then Loaded/Empty/Error states; AcknowledgeLapse handler emits updated Loaded state with the target record's isAcknowledged set to true
AcknowledgeLapse event is only processed when the authenticated user has the coordinator role; if role check fails the BLoC emits CertificateExpiryNotificationError with an authorization error message
ExpiryNotificationRepository abstract class defines at minimum: Future<List<ExpiryNotificationRecord>> fetchExpiryNotifications() and Future<void> acknowledgeLapse(String notificationId)
All BLoC files are placed in the correct feature directory (e.g., lib/features/certificate_expiry_notifications/bloc/) and exported from a barrel file
Dart analysis (dart analyze) passes with zero errors or warnings on all new files

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc package)
apis
Supabase REST API — certificate/expiry notifications endpoint
Role resolution service (existing)
data models
ExpiryNotificationRecord
CertificateExpiryNotificationState
CertificateExpiryNotificationEvent
performance requirements
BLoC state transitions must complete within 50 ms (excluding async repository calls)
Repository fetch should be cancellable on BLoC close to prevent memory leaks (use emit.isDone guard or StreamSubscription)
security requirements
AcknowledgeLapse event must validate coordinator role before calling repository; never trust client-side role alone — repository layer must also enforce authorization via Supabase RLS
No sensitive certificate holder PII should be stored in BLoC state beyond what is required for display (certificate name and expiry date only)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use the sealed class pattern (Dart 3+) for both events and states — this gives exhaustive switch coverage in widgets with no default branch needed. Define ExpiryNotificationRecord as an immutable class with copyWith. The ExpiryNotificationRepository interface should be defined in a separate file from its Supabase implementation to keep the BLoC layer decoupled from Supabase. Follow the existing BLoC directory convention already in the project.

Use the project's existing role-resolution service rather than reading user metadata directly. The daysUntilExpiry field should be computed at parse time (DateTime.now() diff) and stored as a signed int — negative means already expired — so widgets only need to check the sign.

Testing Requirements

Unit tests using flutter_test and bloc_test package. Test each event-to-state transition: (1) LoadExpiryNotifications emits [Loading, Loaded] when repository returns records, (2) LoadExpiryNotifications emits [Loading, Empty] when repository returns empty list, (3) LoadExpiryNotifications emits [Loading, Error] when repository throws, (4) RefreshExpiryNotifications follows same pattern as Load, (5) AcknowledgeLapse on a valid notificationId emits updated Loaded state with isAcknowledged=true on matched record, (6) AcknowledgeLapse from a non-coordinator role emits Error. Use MockExpiryNotificationRepository (mockito or manual mock). Aim for 100% branch coverage on the BLoC class.

Epic Risks (2)
medium impact medium prob technical

The persistent banner must remain visible across app sessions and only disappear when a specific backend condition is met (renewal or coordinator acknowledgement). If the BLoC state is not properly sourced from the notification record repository on every app launch, the banner may disappear prematurely or fail to reappear after a session restart.

Mitigation & Contingency

Mitigation: Drive the banner's visibility exclusively from a Supabase real-time subscription on the notification records table filtered by mentor_id and acknowledged_at IS NULL. Never persist banner visibility state locally. Write an integration test that restarts the BLoC and verifies the banner reappears from the database source.

Contingency: If real-time subscriptions introduce latency or connection reliability issues in offline-first scenarios, add a local cache flag that is only cleared when the repository confirms the acknowledgement write succeeded, with a cache TTL of 24 hours as a fallback.

high impact low prob security

The notification detail view must conditionally render coordinator-specific actions based on the authenticated user's role. Incorrect role resolution could expose the 'Acknowledge Lapse' action to peer mentors or hide it from coordinators, breaking the workflow and potentially allowing unauthorised state changes.

Mitigation & Contingency

Mitigation: Source the role check from the existing role_state_manager BLoC that is already authenticated against Supabase role claims. Do not rely on a local flag. The coordinator acknowledgement service backend also validates role server-side, providing defence in depth. Add widget tests that render the detail view with mentor and coordinator role fixtures and assert the presence or absence of coordinator actions.

Contingency: If a role resolution bug is found in production, immediately disable the acknowledge action via a feature flag and patch the role check in a hotfix release. The server-side validation in the coordinator acknowledgement service ensures no actual state change can occur even if the button is incorrectly rendered.