critical priority medium complexity backend pending backend specialist Tier 5

Acceptance Criteria

recordAcknowledgement(declarationId, scrollCompleted) creates an acknowledgement record in Supabase with: driver_id (from JWT), declaration_id, acknowledged_at (UTC, server-generated via now()), scroll_completed (boolean), and document_hash (copied from the declaration record's content_hash)
acknowledged_at is generated by the Supabase database server using DEFAULT now() — the client never sends a timestamp to prevent clock manipulation
document_hash in the acknowledgement record is copied from the corresponding declaration.content_hash at insert time — not recomputed by the client
After insert, the record is immutable: Supabase RLS policy denies UPDATE and DELETE on declaration_acknowledgements for all roles including the authenticated driver
If declarationId does not exist in the declarations table, ForeignKeyViolationException is surfaced and no acknowledgement record is written
If the driver has already acknowledged this declarationId, DuplicateAcknowledgementException is thrown — the unique constraint (driver_id, declaration_id) is enforced at the database level
If scrollCompleted is false, the record is still persisted (scroll completion is informational for audit, not a gate) — but the calling UI layer is responsible for warning the driver
The declaration status is updated to ACKNOWLEDGED in the same database transaction as the acknowledgement insert — not as a separate call
The method returns an AcknowledgementReceipt containing: acknowledgement_id, acknowledged_at (UTC ISO-8601 string), and document_hash
The entire operation completes within 500 ms under normal network conditions

Technical Requirements

frameworks
Flutter
BLoC
Riverpod
flutter_test
apis
Supabase PostgREST REST API (declaration_acknowledgements table insert, declarations table update status)
Supabase Edge Function or Postgres function for atomic insert + status update transaction
data models
DeclarationAcknowledgement (id, driver_id, declaration_id, acknowledged_at, scroll_completed, document_hash)
AcknowledgementReceipt (acknowledgement_id, acknowledged_at, document_hash)
Declaration (status updated to ACKNOWLEDGED)
performance requirements
Atomic insert + declaration status update must be a single Supabase RPC call to a Postgres function — not two sequential PostgREST requests
Unique index on (driver_id, declaration_id) must exist — enforces duplicate prevention at DB level with O(log n) lookup
security requirements
driver_id must be extracted from the authenticated Supabase JWT (auth.uid()) inside the Postgres function — never passed as a client parameter
RLS on declaration_acknowledgements: INSERT allowed only if auth.uid() = the driver_id resolved from the declaration record; UPDATE and DELETE denied for all roles
document_hash must be fetched from the declarations table inside the Postgres function — the client cannot supply or override it
acknowledged_at uses DEFAULT now() (server UTC time) — the client's clock is not trusted
The Postgres function must validate that the declaration's status is SENT before allowing acknowledgement — cannot acknowledge an already-acknowledged or non-existent declaration

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Implement the atomic insert + declaration status update as a Postgres function (Supabase Edge Function alternative): CREATE OR REPLACE FUNCTION record_declaration_acknowledgement(p_declaration_id UUID, p_scroll_completed BOOLEAN) RETURNS JSON AS $$ ... $$ — this function resolves driver_id from auth.uid(), copies document_hash from declarations, inserts into declaration_acknowledgements with DEFAULT timestamps, and updates declarations.status = 'ACKNOWLEDGED' in a single transaction. Call it via supabase.rpc('record_declaration_acknowledgement', {...}). The immutability requirement (no UPDATE/DELETE) is enforced by two separate RLS policies.

Document in a code comment that this record constitutes the legally defensible e-signature under Norwegian electronic signature law (eSignatur-loven) — future developers must not remove the immutability policies without legal review. The AcknowledgementReceipt returned to the Flutter layer should be persisted locally (Hive or shared_preferences) as a client-side confirmation copy in case the driver later disputes the acknowledgement.

Testing Requirements

Unit tests (flutter_test) with mocked AcknowledgementRepository: happy path returns correct receipt with document_hash and acknowledged_at, declarationId not found (ForeignKeyViolationException), duplicate acknowledgement (DuplicateAcknowledgementException), scroll_completed false (record still persisted). Integration tests against Supabase test project: insert acknowledgement and verify all fields in the returned row, verify declaration.status is ACKNOWLEDGED after the call, attempt duplicate acknowledgement and verify DB constraint fires, attempt to UPDATE the acknowledgement row (verify RLS blocks it), attempt acknowledgement by a different driver (verify RLS blocks it). Legal audit test: verify document_hash in acknowledgement matches the content_hash on the declaration record exactly.

Component
Declaration Acknowledgement Service
service medium
Epic Risks (4)
high impact medium prob security

Org-scoped encryption key management is complex. If keys are not correctly isolated per organization, a breach in one org's key could expose another org's declarations. Additionally, key rotation is not specified but may be needed for compliance, and the current implementation may not support it.

Mitigation & Contingency

Mitigation: Use Supabase Vault or a dedicated secrets management approach for org-scoped key storage. Define the key derivation strategy (per-org master key) in a security design document reviewed before implementation begins. Include key isolation tests in the test suite.

Contingency: If a full per-org key management system cannot be safely implemented within the sprint, fall back to a single platform-level encryption key with strict RLS isolation as a temporary measure, flagging the key rotation gap as a security debt item with a defined resolution milestone.

medium impact medium prob integration

Push notification delivery to drivers depends on FCM token availability and device connectivity. If a driver has not granted notification permissions or has an expired FCM token, the declaration delivery notification will silently fail, leaving the coordinator unaware and the declaration unacknowledged.

Mitigation & Contingency

Mitigation: Implement delivery status tracking in declaration-notification-service. Fall back to in-app notification and SMS (if configured) when push delivery fails. Expose delivery failure status in the declaration status badge so coordinators can identify and manually follow up.

Contingency: If push delivery proves unreliable, implement a polling-based in-app notification fallback where drivers see pending declarations on next app open, ensuring the workflow can complete even without push notifications.

medium impact medium prob technical

The acknowledgement service is meant to validate that the driver has fully scrolled through the declaration before confirming. Implementing reliable scroll completion detection in Flutter across different screen sizes and font sizes is technically non-trivial and could be bypassed.

Mitigation & Contingency

Mitigation: Implement scroll position tracking using ScrollController with a threshold (e.g., 95% of content height reached) and record the validated state server-side before allowing acknowledgement submission. Document the approach in the legal sign-off checkpoint noted in the feature documentation.

Contingency: If reliable scroll detection cannot be implemented within the sprint, add a mandatory reading delay timer (e.g., estimated reading time based on word count) as an alternative validation mechanism, pending legal review of the approach.

medium impact low prob dependency

The driver assignment service must coordinate with the threshold-based expense approval workflow for fees above configured thresholds. If the expense approval workflow interface changes or is not yet stable, the integration point could break or produce incorrect routing behavior.

Mitigation & Contingency

Mitigation: Define a clear interface contract between driver-assignment-service and the expense approval workflow before implementation. Use dependency injection so the expense workflow client can be mocked in tests. Monitor the expense approval feature for interface changes.

Contingency: If the expense approval workflow interface is not stable, implement a direct database insert to the expense records table as a temporary bypass, with a flag indicating manual review is needed, until the stable interface is available.