high priority medium complexity integration pending integration specialist Tier 5

Acceptance Criteria

After submitForApproval() succeeds, ApprovalNotificationService.notifyCoordinatorOfSubmission() is called with the claim ID and submitting peer mentor's details
After approveClaim() succeeds, ApprovalNotificationService.notifyPeerMentorOfApproval() is called with the claim ID and approving coordinator's details
After rejectClaim() succeeds, ApprovalNotificationService.notifyPeerMentorOfRejection() is called with the claim ID, rejecting coordinator's details, and optional rejection reason
After markAsExported() succeeds, ApprovalNotificationService.notifyCoordinatorOfExport() is called with the claim ID
A failure in any notification call does NOT throw an exception to the caller of the state transition method — the transition is already committed
Notification errors are logged (non-blocking) with sufficient context to debug — claim ID, transition type, error message
Notifications are dispatched after the audit event is persisted — never before
The notification call is async and does not block the state transition response from being returned to the caller
Device tokens for target users are resolved by ApprovalNotificationService, not by ApprovalWorkflowService — concerns are separated
FCM dispatch occurs server-side via Supabase Edge Function — the mobile client only triggers the notification service, never dispatches FCM directly

Technical Requirements

frameworks
Flutter
BLoC
Dart
apis
Firebase Cloud Messaging (FCM) API v1 via Supabase Edge Function
Supabase Edge Functions (Deno) for server-side FCM dispatch
Supabase PostgreSQL 15 for device_token lookup
data models
device_token
claim_event
performance requirements
Notification dispatch must not add latency to the state transition response — fully async fire-and-forget
Notification Edge Function must complete FCM dispatch within 2 seconds
security requirements
FCM server key and service account credentials must never be present in Flutter app binary — all FCM dispatch via Edge Function only
Notification payloads must contain only claim ID and status — no PII in the push payload; full data fetched by recipient app on open
Device token lookups scoped by organisation_id to prevent cross-organisation notification leakage
Edge Function validates JWT before dispatching any notification

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Implement the fire-and-forget pattern using `unawaited()` from dart:async — wrap the notification call in `unawaited(approvalNotificationService.notify(...).catchError((e) => _log.error(...)))`. This ensures the future is not abandoned (avoiding unhandled future warnings) while also not blocking. ApprovalWorkflowService should depend on an abstract `IApprovalNotificationService` interface — the concrete implementation calls the Supabase Edge Function via HTTP. The Edge Function then performs the device_token lookup and FCM dispatch.

Keep the notification trigger in ApprovalWorkflowService thin: only pass the minimum data (claim ID, transition type, actor ID) — let the notification service resolve recipient details independently to maintain separation of concerns.

Testing Requirements

Unit tests with flutter_test and mocked ApprovalNotificationService. Test cases: (1) notification called with correct parameters after each of the 4 transition types, (2) notification failure (mock throws) does not propagate exception to state transition caller, (3) notification not called if state transition itself fails, (4) notification called after audit event write, not before, (5) correct recipient targeted per transition type (coordinator vs peer mentor). Use verify() from mocktail to assert notification method invocations without actually calling FCM.

Component
Approval Workflow Service
service high
Epic Risks (3)
high impact high prob technical

The ThresholdEvaluationService is described as shared Dart logic used both client-side and in the Edge Function. Supabase Edge Functions run Deno/TypeScript, not Dart, meaning the threshold logic must be maintained in two languages and can diverge, causing the server to reject legitimate client submissions.

Mitigation & Contingency

Mitigation: Implement the threshold logic as a single TypeScript module in the Edge Function and call it via a thin Dart HTTP client wrapper for client-side preview feedback only. The server is always authoritative; the client version is purely for UX (showing the user whether their claim will auto-approve before they submit).

Contingency: If dual-language maintenance is unavoidable, create a shared golden test file (JSON fixtures with inputs and expected outputs) that is run against both implementations in CI to detect divergence immediately.

medium impact medium prob technical

A peer mentor could double-tap the submit button or a network retry could trigger a duplicate submission, causing the ApprovalWorkflowService to attempt two concurrent state transitions from draft→submitted for the same claim, potentially resulting in two audit events or conflicting statuses.

Mitigation & Contingency

Mitigation: Implement idempotency in the ApprovalWorkflowService using a database-level unique constraint on (claim_id, from_status, to_status) per transition, combined with a UI-level submission lock (disable button after first tap until response returns).

Contingency: Add a deduplication check at the start of every state transition method that returns the existing state if an identical transition is already in progress or completed within the last 10 seconds.

high impact medium prob scope

Claims with multiple expense lines (e.g., mileage + parking) must have their combined total evaluated against the threshold. If individual lines are added asynchronously or the evaluation runs before all lines are persisted, the auto-approval decision may be computed on an incomplete set of expense lines.

Mitigation & Contingency

Mitigation: The Edge Function always fetches all expense lines from the database (not from the client payload) before computing the threshold decision. Define a clear claim submission contract that requires all expense lines to be persisted before the submit action is called.

Contingency: Add a validation step in ApprovalWorkflowService that counts expected vs. persisted expense lines before allowing the transition, returning a validation error if lines are missing.