critical priority medium complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

CertificationBLoC constructor accepts a `CertificationManagementService` via dependency injection and registers handlers using `on<EventType>` for all three event types with `transformer: droppable()` or `transformer: sequential()` as appropriate per event (see implementation notes)
LoadCertification handler: emits `CertificationLoading`, calls `service.getCertification(certificationId)`, emits `CertificationLoaded` on success, emits `CertificationError` with a user-readable message on any exception
RecordRenewal handler: emits `CertificationLoading`, calls `service.recordRenewal(certificationId, renewalDate)`, emits `RenewalSuccess` carrying the updated `Certification` on success, emits `CertificationError` on failure
EnrolInCourse handler: emits `CertificationLoading`, calls `service.enrolInCourse(certificationId, courseId)`, emits `EnrolmentSuccess` on success, emits `CertificationError` on failure
All handlers use `emit.forEach` or `async*` pattern — never raw `await` followed by bare `emit` after an `await` gap without checking `isClosed`
Exceptions thrown by the service are caught with a typed try/catch; `ServiceException` maps to a user-facing message string; unexpected exceptions log to the error logger and emit a generic error message
No navigation logic, no BuildContext references, and no direct Supabase calls exist inside the BLoC class
The BLoC compiles with zero `dart analyze` warnings and passes all pre-existing tests in the module

Technical Requirements

frameworks
Flutter
flutter_bloc
BLoC
bloc_concurrency (for event transformers)
apis
CertificationManagementService.getCertification()
CertificationManagementService.recordRenewal()
CertificationManagementService.enrolInCourse()
data models
Certification
ServiceException (domain error model)
performance requirements
LoadCertification must not fire a new network call if a load is already in progress — use droppable() transformer
RecordRenewal and EnrolInCourse must queue and process sequentially to prevent race conditions on the same certification record — use sequential() transformer
BLoC must not hold strong references to BuildContext or UI widgets to prevent memory leaks
security requirements
Error messages emitted in CertificationError must never expose raw Supabase error bodies or stack traces to the UI — map to safe user-readable strings
certificationId and courseId values passed to service calls must be validated as non-empty strings before the call is made; emit CertificationError immediately if invalid

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Import `bloc_concurrency` and apply `transformer: droppable()` to the `LoadCertification` handler and `transformer: sequential()` to `RecordRenewal` and `EnrolInCourse`. Use the `on(handler, transformer: ...)` overload rather than registering transformers globally on the BLoC to keep each handler's concurrency contract explicit. The recommended safe async pattern inside handlers: `on((event, emit) async { emit(CertificationLoading()); try { final cert = await _service.getCertification(event.certificationId); emit(CertificationLoaded(certification: cert)); } on ServiceException catch (e) { emit(CertificationError(message: e.userMessage)); } catch (e, st) { _logger.error(e, st); emit(CertificationError(message: 'An unexpected error occurred')); } });`. Do not use `emit.forEach` for one-shot async calls — reserve it for stream subscriptions.

Ensure `CertificationManagementService` is defined as an abstract interface so it can be mocked cleanly in tests without hitting Supabase.

Testing Requirements

Unit tests using `bloc_test` package and `mocktail` (or Mockito) for mocking `CertificationManagementService`. Test 1 (LoadCertification success): `blocTest` expects states `[CertificationLoading, CertificationLoaded]` when service returns a valid Certification. Test 2 (LoadCertification error): expects `[CertificationLoading, CertificationError]` when service throws `ServiceException`. Test 3 (RecordRenewal success): expects `[CertificationLoading, RenewalSuccess]`.

Test 4 (RecordRenewal error): expects `[CertificationLoading, CertificationError]`. Test 5 (EnrolInCourse success): expects `[CertificationLoading, EnrolmentSuccess]`. Test 6 (concurrent LoadCertification with droppable): fire two `LoadCertification` events in rapid succession; assert service is called only once. Test 7 (unexpected exception): service throws a raw `Exception`; assert `CertificationError` is emitted with the generic message string.

Minimum 80% branch coverage.

Component
Certification BLoC
service medium
Epic Risks (2)
medium impact low prob technical

Supabase Edge Functions can have cold-start latency that causes the nightly cron to time out when processing large cohorts of expiring certifications, resulting in partial reminder dispatches.

Mitigation & Contingency

Mitigation: Batch the cron processing in chunks of 50 mentors per iteration. Use pagination with a cursor to resume processing if the function is re-invoked. Keep total invocation time well under the Edge Function timeout limit.

Contingency: If timeouts occur in production, split the cron into two separate functions: one for reminders and one for auto-pauses, each with its own schedule offset to reduce peak load.

low impact medium prob technical

Certification BLoC covers three distinct workflows (view, renew, enrol) which may lead to an overly complex state machine that is hard to test and maintain, particularly when error states from multiple concurrent operations need to be differentiated in the UI.

Mitigation & Contingency

Mitigation: Use separate sealed state classes per workflow (CertificationViewState, RenewalState, EnrolmentState) composed into a single BLoC state wrapper. Follow the existing BLoC patterns established in the codebase for consistency.

Contingency: If the BLoC grows too complex, split into two BLoCs: CertificationBLoC (view/load) and CertificationActionBLoC (mutations), connected via a shared stream.