high priority low complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

Widget renders four distinct visual states: active (green background, 'Active' label), expiring_soon (amber background, 'Expires in X days' label), expired (red background, 'Expired' label), not_certified (grey background, 'Not certified' label)
expiring_soon state displays the exact number of days remaining (e.g., 'Expires in 14 days'), computed from the certification expiry date
All four badge states meet WCAG 2.2 AA colour contrast ratio of at least 4.5:1 for text on badge background — colours sourced exclusively from the design token system, not hardcoded hex values
Widget accepts a `CertificationStatus` model as its sole required parameter
Semantic label is set via `Semantics(label: ...)` for each state: 'Certification active', 'Certification expiring in X days', 'Certification expired', 'Not certified' — readable by VoiceOver and TalkBack
Widget is sized to fit inside a `ListTile` trailing slot (max height 28px, max width 120px) and a profile header context (max height 36px, max width 140px) without overflow
Widget is stateless — all display logic derived from the input `CertificationStatus` model with no internal state
Widget does not import or depend on BLoC or Riverpod directly — it is a pure display widget
Golden test: all 4 states produce stable pixel-accurate screenshots at 1x and 3x device pixel ratios
Widget handles null/missing expiry date gracefully — defaults to not_certified state without throwing

Technical Requirements

frameworks
Flutter
Dart
data models
certification
performance requirements
Widget build method must complete in under 1ms — no async operations in build()
No unnecessary rebuilds — widget should be const-constructable where possible
security requirements
Badge must not display raw certification IDs or personal identifiers — only status and days-remaining count
Colour tokens must be sourced from the design token system to ensure org-level theming compliance
ui components
CertificationExpiryBadge (new widget)
Design token colour values: tokenColorSuccess, tokenColorWarning, tokenColorError, tokenColorNeutral
Semantics widget wrapper for accessibility
Container with BoxDecoration (borderRadius, color from tokens)
Text widget with typography tokens

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Create the widget in `lib/widgets/certification/certification_expiry_badge.dart`. Define a `CertificationBadgeState` enum internally: `{ active, expiringSoon, expired, notCertified }`. Compute the badge state from `CertificationStatus` in a pure static helper method `_computeState(CertificationStatus status, DateTime now)` — inject `DateTime now` as a parameter for testability (avoids `DateTime.now()` calls inside build). Use a `switch` expression (Dart 3) to map state → `(color, label)` pair.

Reference design tokens via the existing token system (e.g., `AppColors.success`, `AppColors.warning`). Do NOT use `Colors.green` or similar Material constants — always use tokens. Add a `size` parameter with a `BadgeSize` enum (small for ListTile, large for profile header) to control padding and font size. Mark the constructor `const` to enable compile-time optimisation.

Add a widget test file at `test/widgets/certification/certification_expiry_badge_test.dart`.

Testing Requirements

Widget tests using flutter_test: render all 4 states and assert correct label text is present, verify Semantics node label for each state (using `tester.getSemantics()`), verify expiring_soon state shows correct day count for a given mock expiry date, verify null expiry date renders as not_certified without exception, verify widget fits within ListTile trailing slot bounds (height <= 28px). Golden tests for all 4 states at 1x and 3x DPR using `matchesGoldenFile`. Accessibility test: ensure `ExcludeSemantics` is NOT applied and all states have non-empty semantic labels. Test contrast ratio programmatically by reading token values.

Component
Certification Expiry Badge
ui low
Epic Risks (3)
high impact medium prob integration

HLF Dynamics portal webhook API contract may be undocumented, subject to change, or require a separate authentication flow not yet agreed upon with HLF. If the contract changes post-implementation, the sync service silently fails and expired peer mentors remain on public listings.

Mitigation & Contingency

Mitigation: Obtain the official Dynamics webhook specification and test credentials from HLF before starting HLFDynamicsSyncService implementation. Agree on a versioned webhook contract and request a staging endpoint for integration testing.

Contingency: If the contract is unavailable, stub the sync service behind a feature flag and ship without Dynamics sync initially. Queue sync events locally and replay once the contract is confirmed.

high impact medium prob security

Supabase RLS policies for certifications must correctly scope data to the coordinator's chapter without leaking cross-organisation data, particularly complex in multi-chapter membership scenarios. A misconfigured policy could expose peer mentor PII to wrong coordinators.

Mitigation & Contingency

Mitigation: Write RLS policies against the established org-hierarchy schema used by other tables. Peer review all policies before migration deployment. Add integration tests that assert cross-organisation data isolation using test accounts with different org scopes.

Contingency: If a policy gap is discovered post-merge, immediately disable the affected query endpoint and apply a hotfix migration. Audit access logs in Supabase for any cross-org data access events.

medium impact low prob technical

Storing renewal history as a JSONB field rather than a normalised table simplifies queries but makes retrospective schema changes (adding fields to history entries) harder and could cause issues if history grows very large for long-tenured mentors.

Mitigation & Contingency

Mitigation: Define a versioned JSONB entry schema (include a schema_version field in each entry) so future migrations can transform old entries. Add a size guard in the repository to warn if renewal_history exceeds 500 entries.

Contingency: If JSONB approach proves limiting, add a normalised certification_renewal_events table and migrate history entries in a background job, keeping the JSONB field as a read cache.