critical priority low complexity database pending database specialist Tier 0

Acceptance Criteria

claim_events table exists in Supabase with columns: id (UUID, PK, default gen_random_uuid()), claim_id (UUID, NOT NULL, FK to expense_claims), event_type (TEXT, NOT NULL), actor_id (UUID, NOT NULL, FK to auth.users), metadata (JSONB, nullable), created_at (TIMESTAMPTZ, NOT NULL, DEFAULT now())
No UPDATE or DELETE permissions exist on claim_events for any role — enforced via RLS policies that only define INSERT and SELECT
A database-level CHECK constraint or trigger prevents direct UPDATE/DELETE even for service_role
Authenticated users can INSERT into claim_events only for claims they are authorized to act on (enforced via RLS joining to their chapter/org scope)
Authenticated users can SELECT claim_events only for claims within their scope
ClaimEvent Dart model has all fields typed correctly: id (String), claimId (String), eventType (ClaimEventType enum), actorId (String), metadata (Map<String, dynamic>?), createdAt (DateTime)
ClaimEventType is a sealed class or enum covering: submitted, approved, rejected, escalated, exported, commented, reopened
fromJson factory correctly parses ISO 8601 UTC timestamps to DateTime
toJson correctly serializes DateTime to ISO 8601 UTC string
Unit tests pass for fromJson/toJson round-trip with null metadata and with populated metadata
Supabase migration file exists and is version-controlled

Technical Requirements

frameworks
Flutter
Supabase
apis
Supabase REST API
Supabase Auth (RLS context via auth.uid())
data models
ClaimEvent
ExpenseClaim
performance requirements
Index on claim_id column for fast event log retrieval per claim
Index on actor_id for audit queries
created_at index for chronological ordering
security requirements
RLS policy: INSERT allowed only when auth.uid() matches actor_id or has coordinator role in the claim's chapter
RLS policy: SELECT allowed only for claims within the authenticated user's chapter scope
No UPDATE policy defined — any attempt returns permission denied
No DELETE policy defined — any attempt returns permission denied
metadata JSONB must not store PII directly; reference IDs only
All timestamps stored as UTC TIMESTAMPTZ

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use Supabase migrations (supabase/migrations/) for schema versioning — never apply schema changes manually in production. The immutability guarantee must be dual-layered: RLS has no UPDATE/DELETE policies (application layer), AND a BEFORE UPDATE/DELETE trigger raises an exception (database layer) as a belt-and-suspenders approach. For ClaimEventType, prefer a Dart enum with a fromString() factory that throws on unknown values rather than returning null — fail loudly for schema drift. The metadata JSONB column is intentionally flexible but document expected keys per event_type in a code comment or companion constants file.

Use const constructors on the Dart model where possible. Avoid using dynamic for metadata in typed contexts — wrap in a ClaimEventMetadata sealed class hierarchy if the metadata shapes are known.

Testing Requirements

Unit tests for ClaimEvent.fromJson() and toJson() covering: full object round-trip, null metadata field, all ClaimEventType enum values, malformed date string throws FormatException. Integration test (flutter_test with Supabase test instance): verify INSERT succeeds for authenticated user, verify UPDATE attempt throws PostgrestException with code 42501, verify DELETE attempt throws PostgrestException with code 42501, verify SELECT returns only events for claims in user's scope. Migration SQL must be tested by running it against a clean Supabase schema and verifying table structure with DESCRIBE or information_schema.

Component
Claim Events Repository
data low
Epic Risks (3)
medium impact medium prob technical

Optimistic locking in ExpenseClaimStatusRepository may produce excessive concurrency exceptions in high-volume coordinator sessions where multiple coordinators process the same queue simultaneously, causing confusing UI errors and coordinator frustration.

Mitigation & Contingency

Mitigation: Design the locking strategy with a short retry window (1-2 automatic retries with 200ms back-off) before surfacing the error to the UI. Document the concurrency model clearly so the UI layer can display a contextual 'claim was already actioned' message rather than a generic error.

Contingency: If contention remains high under load testing, switch to a last-writer-wins update with a conflict notification rather than a hard block, and log all concurrent edits for audit purposes.

medium impact medium prob integration

FCM device tokens stored for peer mentors may be stale (app reinstalled, token rotated) causing push notifications for claim status changes to silently fail, leaving submitters unaware their claim was approved or rejected.

Mitigation & Contingency

Mitigation: Implement token refresh on every app launch and store updated tokens in Supabase. ApprovalNotificationService should fall back to in-app Realtime delivery when FCM returns an invalid-token error and should queue a token refresh request.

Contingency: If FCM delivery rates fall below acceptable thresholds in production monitoring, add a polling fallback in the peer mentor claim list screen that checks status on foreground resume.

high impact low prob dependency

Supabase Realtime has per-project channel and connection limits. If many coordinators and peer mentors are simultaneously subscribed across multiple screens, the project may hit quota limits causing subscription failures.

Mitigation & Contingency

Mitigation: Design RealtimeApprovalSubscription to use a single shared channel per user session rather than per-screen subscriptions. Implement subscription reference counting so channels are only opened once and reused across screens.

Contingency: Upgrade the Supabase plan tier if limits are reached, and implement graceful degradation to polling with a 30-second interval when Realtime is unavailable.