critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

SupabaseOrgRateConfigRepository implements OrgRateConfigRepository and is placed in lib/data/mileage/
watchConfig() returns a Stream backed by a BehaviorSubject that immediately emits the most recent cached value (or fetches from Supabase on first subscription)
On first call to watchConfig() for a given orgId, the implementation fetches from Supabase and emits OrgRateConfigLoaded with the result
In-memory cache stores the last fetched OrgRateConfig with a TTL (configurable, default 5 minutes); if a new subscriber calls watchConfig() within TTL, the cached value is emitted immediately without a Supabase round-trip
When the TTL expires, the next subscription triggers a background refresh; the previous cached value remains emitted until the new value arrives
Supabase real-time subscription (or polling fallback) is used to push admin rate config changes to all active subscribers without requiring a page reload
If Supabase returns an error (network failure, 4xx/5xx), an OrgRateConfigError is emitted on the stream — the stream does not close, allowing recovery on reconnect
The BehaviorSubject is per-orgId: switching organisation context creates a new subject and disposes the old one
A `dispose()` method closes all active BehaviorSubjects and cancels Supabase subscriptions to prevent memory leaks
Implementation compiles and passes `dart analyze lib/data/mileage/`

Technical Requirements

frameworks
Flutter
Dart
Supabase Dart client
rxdart (BehaviorSubject)
apis
Supabase REST API: SELECT per_km_rate, auto_approval_threshold_km FROM org_rate_config WHERE org_id = :orgId
Supabase Realtime: SUBSCRIBE to org_rate_config table changes for orgId
data models
OrgRateConfig
OrgRateConfigResult
performance requirements
Cached response must be emitted within 1ms (no I/O for cache hit)
Initial Supabase fetch must complete within 3 seconds on a standard mobile connection
Memory footprint for the BehaviorSubject cache must not grow unboundedly — cap at one entry per active org
security requirements
Supabase Row Level Security (RLS) must enforce that users can only read org_rate_config for their own organisation — the repository must not bypass RLS
The per_km_rate and threshold values must not be logged in plain text in production builds

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use `rxdart` package's `BehaviorSubject` for the internal stream. Maintain a `Map` where key is orgId and value holds `{subject: BehaviorSubject, cachedAt: DateTime?, subscription: RealtimeChannel?}`. On watchConfig(orgId): if no entry exists, create one and trigger an initial fetch; if entry exists, return the existing subject's stream. TTL check: in the fetch method, compare `DateTime.now().difference(cachedAt)` against the TTL constant.

For Supabase Realtime: use `supabase.channel('org_rate_config:$orgId').onPostgresChanges(...)` to receive INSERT/UPDATE events and update the BehaviorSubject. If Realtime is unavailable, implement a polling fallback with a configurable interval. Wrap all Supabase calls in try-catch and add the error to the subject via `subject.addError()` (which emits OrgRateConfigError downstream via map). Ensure `dispose()` is called by the Riverpod provider's `onDispose` callback (wired in task-003).

Testing Requirements

Write unit tests with a mock Supabase adapter: (1) first call fetches and emits OrgRateConfigLoaded, (2) second call within TTL emits cached value without calling Supabase, (3) call after TTL expiry re-fetches from Supabase, (4) Supabase error propagates as OrgRateConfigError on stream, (5) real-time push update emits new OrgRateConfigLoaded to all active subscribers. Use `fake_async` for TTL tests to avoid real time delays. Write one integration test that connects to the Supabase test instance and verifies a full fetch-and-stream cycle. Tag integration tests with `@Tags(['integration'])`.

Epic Risks (2)
medium impact medium prob integration

If OrgRateConfigRepository caches the per-km rate aggressively and an admin updates the rate mid-session, ongoing form interactions will show the old rate until the Stream emits. This could result in the UI showing a rate that differs from what is stored when the claim is submitted, causing confusion or disputes.

Mitigation & Contingency

Mitigation: Subscribe to a Supabase Realtime channel for the org_configuration table so config changes propagate within seconds. Document the eventual-consistency window in code comments.

Contingency: If Realtime subscription proves unreliable in test, add a polling fallback with a configurable interval (default 5 minutes) and display a 'rate updated' toast when the stream emits a changed value.

medium impact medium prob scope

The correction window within which a claim can be deleted or voided is not explicitly specified in the feature documentation. Implementing the wrong window (e.g. 24 hours vs 7 days) could lock users out of corrections or allow inappropriate post-approval modifications.

Mitigation & Contingency

Mitigation: Raise the correction window definition as a blocking question to the HLF product owner before implementing the delete/void path in MileageClaimRepository. Implement the window duration as an org-level configuration value rather than a hardcoded constant.

Contingency: If the question cannot be resolved before implementation, default to 24 hours as the most conservative option and flag the value for review in the first user-acceptance test.