Reimbursement Approval
Data Entity
Description
Tracks the approval lifecycle for a travel expense report, including auto-approved and manually attested decisions with complete audit trails. Records coordinator decisions, rejection reasons, and timestamps required for Bufdir grant compliance auditing.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Immutable primary key for the approval record, generated server-side on creation. | PKrequiredunique |
expense_report_id |
uuid |
Foreign key to travel_expense_reports. Each expense report has exactly one approval record. Enforced as unique to guarantee the one-to-one relationship. | requiredunique |
organization_id |
uuid |
Denormalized organization reference for RLS-scoped queries, allowing coordinators to list all pending approvals within their org without joining through expense reports. | required |
status |
enum |
Current state of the approval lifecycle. Transitions: pending → auto_approved | pending → approved | pending → rejected | pending → requires_correction. Terminal states (auto_approved, approved, rejected) cannot transition back to pending. | required |
auto_approved |
boolean |
True when the record was approved automatically by the Auto-Approval Service because the expense report fell below configured mileage and amount thresholds. False for all manual coordinator decisions. | required |
approved_by |
uuid |
User ID of the coordinator or administrator who manually attested or rejected the expense report. Null for auto-approved records and for records still in pending state. | - |
decision_at |
datetime |
Timestamp (UTC) when the final approval decision was recorded — either the automated threshold evaluation timestamp or the coordinator's manual attestation time. Null until a terminal status is reached. | - |
comment |
text |
Optional free-text comment from the coordinator accompanying an approval decision. Stored for audit purposes and displayed in the reimbursement detail screen. | - |
rejection_reason |
text |
Mandatory rejection reason captured when status transitions to 'rejected'. Must be non-empty and meaningful — used to inform the peer mentor of the correction needed and retained in the Bufdir audit trail. | - |
correction_requested_at |
datetime |
Timestamp when the coordinator set status to 'requires_correction', enabling calculation of how long a report has been awaiting peer mentor action. | - |
threshold_config_snapshot |
json |
Immutable snapshot of the organization's approval threshold configuration (mileage limit in km, expense amount limit in NOK, per-km rate) captured at submission time. Ensures the audit trail reflects the rules that were in effect when the decision was made, even if the organization later changes its thresholds. | required |
total_amount_nok |
decimal |
Denormalized total reimbursement amount (in NOK) calculated from the parent expense report's expense items at submission time. Used for threshold evaluation and stored for fast audit queries without joining expense_items. | required |
total_distance_km |
decimal |
Denormalized total mileage distance (in km) from all mileage-type expense items in the parent expense report. Used for auto-approval threshold evaluation. Zero when no mileage items are present. | required |
accounting_synced_at |
datetime |
Timestamp when this approved record was successfully synchronized to the organization's accounting system (Xledger or Dynamics). Null until sync completes. Used by the Accounting Sync Orchestrator to determine pending sync work. | - |
accounting_sync_reference |
string |
External reference ID returned by the accounting system (Xledger ledger entry ID or Dynamics record GUID) after successful sync. Used for cross-system reconciliation and displayed in audit logs. | - |
created_at |
datetime |
Server-side timestamp when the approval record was first created, typically at the moment the parent expense report was submitted. Used for aging calculations and Bufdir reporting period filters. | required |
updated_at |
datetime |
Server-side timestamp updated on every write to this record. Used by Riverpod real-time subscriptions and optimistic concurrency checks. | required |
Database Indexes
idx_reimbursement_approvals_expense_report_id
Columns: expense_report_id
idx_reimbursement_approvals_org_status
Columns: organization_id, status
idx_reimbursement_approvals_organization_id
Columns: organization_id
idx_reimbursement_approvals_status
Columns: status
idx_reimbursement_approvals_approved_by
Columns: approved_by
idx_reimbursement_approvals_created_at
Columns: created_at
idx_reimbursement_approvals_accounting_synced
Columns: organization_id, accounting_synced_at
Validation Rules
expense_report_id_exists
error
Validation failed
status_transition_validity
error
Validation failed
approved_by_is_valid_coordinator
error
Validation failed
rejection_reason_non_empty
error
Validation failed
total_amount_matches_expense_items
error
Validation failed
total_distance_non_negative
error
Validation failed
threshold_snapshot_schema_valid
error
Validation failed
comment_and_rejection_reason_length
error
Validation failed
organization_id_matches_expense_report
error
Validation failed
auto_approved_flag_consistency
error
Validation failed
Business Rules
one_approval_per_expense_report
Each travel_expense_reports record must have exactly one reimbursement_approvals record. The approval record is created atomically when the expense report is submitted, preventing any report from existing without an associated approval lifecycle entry. Enforced by the unique constraint on expense_report_id.
auto_approval_threshold_evaluation
When an expense report is submitted, the Auto-Approval Service evaluates the total mileage (km) and total expense amount (NOK) against the organization's configured thresholds. If both values are within the configured limits, the approval record is created with status 'auto_approved' and auto_approved=true. The threshold_config_snapshot is always written to capture the rules in effect at decision time.
terminal_status_immutability
Once an approval record reaches a terminal status ('auto_approved', 'approved', 'rejected'), its status field cannot be changed. This guarantees the integrity of the Bufdir audit trail. Any attempt to update status from a terminal state must be rejected with an error. The 'requires_correction' state is non-terminal: a coordinator may subsequently approve or reject after the peer mentor resubmits.
rejection_requires_reason
A status transition to 'rejected' is only permitted when a non-empty rejection_reason is provided. The rejection reason is surfaced to the peer mentor in the Approval Status Widget and stored permanently in the audit trail for Bufdir compliance.
manual_attestation_requires_coordinator_role
The approved_by field may only be set to the ID of a user who holds the coordinator or admin role within the same organization as the expense report. The Attestation Service validates the acting user's role via the Permission Guard before executing the approval RPC.
decision_timestamp_required_on_terminal_transition
When status transitions to any terminal state ('auto_approved', 'approved', 'rejected'), decision_at must be set to the current UTC timestamp. The server enforces this via the Supabase Edge Function runtime — the client cannot supply its own timestamp to prevent clock manipulation.
accounting_sync_on_approval
When status transitions to 'approved' or 'auto_approved', the Accounting Sync Orchestrator is triggered to route the record to the correct accounting connector (Xledger or Dynamics) based on the organization's integration_configs feature flags. The accounting_synced_at field is updated only after a confirmed successful sync.
org_scoped_rls_enforcement
Row-Level Security policies on this table restrict read access to users who belong to the same organization as the approval record (via user_organization_memberships). Coordinators may only view and act on approvals within their organizational scope. Peer mentors may only read their own approval records.
threshold_snapshot_immutability
The threshold_config_snapshot field is set once at record creation and must never be modified. It captures the exact threshold rules that governed the auto-approval decision, enabling accurate retrospective auditing even if the organization later adjusts its thresholds.
bufdir_compliance_retention
Approval records must be retained permanently (never deleted by automated processes) because they form part of the Bufdir grant compliance audit trail. Admin-initiated deletion is restricted to global admin role and must create an audit log entry before execution.
CRUD Operations
Storage Configuration
Entity Relationships
Each expense report has exactly one approval record tracking its auto-approval or manual attestation lifecycle