critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

The org-isolation filter is a dedicated class or mixin (`OrgIsolationFilter`) within the AggregationQueryBuilder that exposes a method `applyOrgFilter(Map<String, dynamic> params)` returning the params map with `organization_id` constraint appended
Single-org context: when the tenant session store provides exactly one active org, the filter appends `organization_id = <orgId>` to the query params
Multi-org context: when the tenant session store provides multiple active org IDs, the filter appends an `organization_id IN (<orgIds>)` constraint
No-org context: when no org context is active in the session store, calling `applyOrgFilter` throws `NoActiveOrgContextException` (a typed exception extending `Exception`), not a generic error
The filter is applied automatically inside every public method of AggregationQueryBuilder before any Supabase dispatch — verified by code review
Unit tests confirm that a query built without calling the filter never reaches Supabase (via mock verification)
The filter integrates with the existing tenant session store without introducing a circular dependency
The `NoActiveOrgContextException` includes a human-readable message and optionally a stack trace for debugging
The filter implementation is covered by unit tests for all three contexts (single-org, multi-org, no-org)
The filter does not modify any params keys other than `organization_id` — all other params pass through unchanged

Technical Requirements

frameworks
flutter_test
Riverpod (for accessing tenant session provider)
BLoC (if session state is managed via BLoC)
apis
Supabase RPC parameter injection API
Tenant session store / provider (existing project dependency)
data models
OrgIsolationFilter
NoActiveOrgContextException
TenantSession (existing model)
performance requirements
Filter application adds zero async overhead — it is a synchronous parameter transformation
Multi-org list encoding must not perform any database lookup — org IDs come exclusively from the session store
security requirements
The filter must be impossible to bypass: it must be called inside a private method of AggregationQueryBuilder, not left as an optional public call
organization_id values must be validated as non-empty UUIDs before appending to prevent injection via malformed session data
No org context must always result in an exception — silent fallback to unfiltered queries is not permitted under any circumstance

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement the filter as a private method `_buildOrgConstraint(Map params)` on `AggregationQueryBuilder` rather than a standalone class, unless the filter needs to be shared across multiple query builder implementations — in that case extract it to a mixin `OrgIsolationMixin`. Read the current org context synchronously from the Riverpod `ref.read()` inside the query builder's constructor or via a passed-in `OrgContextProvider` to avoid making the filter async. Define `NoActiveOrgContextException` in `lib/src/features/bufdir/data/exceptions.dart` alongside any other domain-specific exceptions for the aggregation feature. Use a UUID regex validator on the org ID before appending it to ensure malformed session data does not pass through.

Document clearly in code comments that this filter is the security boundary preventing cross-org data leakage — any future developer bypassing it must understand the consequences.

Testing Requirements

Unit tests using `flutter_test` and Mockito/mocktail must cover: (1) Single-org context: verify `applyOrgFilter` returns params with correct `organization_id` string value. (2) Multi-org context: verify params contain an `organization_id` list matching all session org IDs. (3) No-org context: verify `NoActiveOrgContextException` is thrown synchronously with a non-null message. (4) Passthrough test: verify that all pre-existing params keys are present and unchanged after filter application.

(5) Mock-injection test: stub AggregationQueryBuilder's Supabase client with a mock, call any public query method without an active org context, and assert the mock's RPC method was never called. Test coverage target: 100% of OrgIsolationFilter logic.

Component
Aggregation Query Builder
data high
Epic Risks (2)
high impact medium prob technical

Supabase RPC functions return JSON with PostgreSQL numeric types (bigint, numeric) that do not map cleanly to Dart int/double. Silent truncation or JSON parsing errors could corrupt participant counts in the final Bufdir submission without any runtime exception.

Mitigation & Contingency

Mitigation: Define explicit Dart fromJson factories for all RPC result models with type-safe parsing and assertion checks. Add a contract test that compares raw RPC JSON output against expected Dart model values using a known seed dataset.

Contingency: If type mismatches are found in production metrics, expose a validation endpoint in BufdirMetricsRepository that re-fetches and compares raw RPC output against the persisted snapshot, flagging any discrepancies before export proceeds.

medium impact high prob scope

Persisted metric snapshots can become stale if additional activities are registered after the snapshot is saved but before the export is finalized. Coordinators might unknowingly export data that does not reflect the latest activity registrations.

Mitigation & Contingency

Mitigation: Store a snapshot_generated_at timestamp and a record_count_at_generation field in the snapshot. When the coordinator views cached results, compare the current activity count for the period against the snapshot value and display a 'Data updated since last aggregation — re-run?' warning if counts differ.

Contingency: Add a mandatory staleness check before the export confirmation dialog can proceed: if the snapshot is more than 24 hours old or the record count has changed, require the coordinator to re-run aggregation before the export button is enabled.