Build CertificationExpiryBadge reusable widget
epic-certification-management-foundation-task-010 — Implement the CertificationExpiryBadge Flutter widget that renders inline certification status throughout the app. Badge states: active (green), expiring_soon (amber, shows days remaining), expired (red), not_certified (grey). Widget must accept a CertificationStatus model, be fully accessible (semantic labels, contrast-safe colours from design tokens), and sized for use inside list tiles and profile headers.
Acceptance Criteria
Technical Requirements
Execution Context
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.
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.
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.
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.