Define and implement HLF Dynamics webhook payload contract
epic-certification-management-foundation-task-009 — Define the canonical webhook payload schema for outbound certification status updates to HLF Dynamics portal: mentorId, certificationStatus, expiryDate, renewalHistory snapshot. Implement serialisation, signature verification for inbound webhooks, and idempotency handling using the cert_notification_log table to reject duplicate webhook deliveries.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Define a `WebhookPayload` Dart class with `toJson()` / `fromJson()` using `json_serializable`. The HMAC signing should be encapsulated in a `WebhookSigner` utility class that takes the raw body bytes and the secret, returning the hex-encoded HMAC-SHA256 digest. For Deno Edge Functions, use the built-in `crypto.subtle.sign()` with HMAC-SHA256. The `cert_notification_log` table needs a unique index on `event_id` to enforce idempotency at the database level (not just application level) — this prevents race conditions on concurrent delivery.
Use `INSERT ... ON CONFLICT (event_id) DO NOTHING` and check `rowsAffected` to detect duplicates. The `renewal_history_snapshot` should be computed at serialisation time by sorting renewal records by `issued_at` descending and taking the first 5. Keep the webhook payload schema in a shared Dart model file so both the Edge Function and any Flutter-side preview code use the same definition.
Testing Requirements
Unit tests with flutter_test covering: correct JSON serialisation of all payload fields including ISO 8601 dates, HMAC signature generation produces consistent output for identical inputs, duplicate event_id lookup returns early without DB write, invalid signature → 401 response, missing signature header → 401 response, renewal_history_snapshot truncated to 5 entries when source has more. Integration test against sandbox: end-to-end outbound webhook delivery with valid signature accepted by Dynamics sandbox, inbound webhook from Dynamics sandbox processed and logged. Test the constant-time comparison to ensure it handles both valid and invalid signatures without timing variance.
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.