Expense Item
Data Entity
Description
Individual expense line item within a travel expense report, representing a specific type such as mileage, toll, parking, or public transit. Items enforce mutual exclusivity rules preventing invalid combinations and may require receipt attachments when amounts exceed thresholds.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Unique identifier for the expense line item, generated server-side via Supabase gen_random_uuid() | PKrequiredunique |
report_id |
uuid |
Foreign key referencing the parent travel_expense_reports record. All expense items must belong to exactly one report. | required |
org_id |
uuid |
Organization the submitting user belongs to. Denormalized from the parent report for RLS policy evaluation and org-specific rule lookups without a join. | required |
user_id |
uuid |
User who owns this expense item (the peer mentor or coordinator submitting). Denormalized for RLS enforcement and audit traceability. | required |
expense_type |
enum |
Category of the expense item. Drives conditional field requirements, mutual exclusivity validation, and reimbursement calculation logic. Fixed enumeration prevents free-text errors and invalid combinations. | required |
amount |
decimal |
Monetary expense amount in NOK for toll, parking, and public_transit types. NULL for mileage items where reimbursement is calculated from distance and rate. Stored with 2 decimal precision. | - |
distance_km |
decimal |
Distance driven in kilometres for mileage expense type. NULL for all non-mileage types. Used with snapshotted rate_per_km to compute calculated_reimbursement. | - |
rate_per_km |
decimal |
Snapshotted per-kilometre reimbursement rate at the time the item was created, sourced from org-specific configuration. Stored to prevent retrospective rate changes from altering submitted amounts. | - |
calculated_reimbursement |
decimal |
Final reimbursement amount in NOK. For mileage: distance_km * rate_per_km. For other types: equals amount. Computed on create/update and stored to support threshold evaluation and report totals without re-computation. | required |
currency |
string |
ISO 4217 currency code. Defaults to NOK for all Norwegian organisations. Stored explicitly to support future internationalisation. | required |
description |
text |
Optional free-text notes provided by the peer mentor, for example route details for a mileage claim or purpose of a parking expense. Not used for expense type classification. | - |
requires_receipt |
boolean |
Computed flag indicating whether at least one receipt attachment is mandatory for this item. Set to true when amount (or calculated_reimbursement for mileage) exceeds the org-configured receipt threshold (typically 100 NOK for HLF). Stored for fast validation without re-evaluating threshold config on every read. | required |
receipt_threshold_applied |
decimal |
Snapshotted receipt-required threshold amount (in NOK) from org configuration at the time the item was saved. Stored alongside requires_receipt to make threshold decisions auditable and stable against future config changes. | - |
mutual_exclusion_group |
string |
Exclusion group identifier used to enforce that incompatible expense types cannot coexist within the same report. Mileage and public_transit belong to group 'transport'; toll and parking belong to group 'vehicle_ancillary'. Business logic prevents two items with conflicting group membership from appearing in the same report. | - |
sort_order |
integer |
Display ordering of items within a report, matching the sequence the peer mentor added them in the wizard. Used for consistent presentation in approval screens. | required |
created_at |
datetime |
UTC timestamp when the expense item record was first persisted. Set by the database server on insert; not client-controlled. | required |
updated_at |
datetime |
UTC timestamp of the most recent update to this record. Automatically maintained via Supabase trigger on row update. Used to detect stale cache entries. | required |
Database Indexes
idx_expense_items_report_id
Columns: report_id
idx_expense_items_user_id
Columns: user_id
idx_expense_items_org_id
Columns: org_id
idx_expense_items_expense_type
Columns: expense_type
idx_expense_items_report_type
Columns: report_id, expense_type
idx_expense_items_created_at
Columns: created_at
idx_expense_items_requires_receipt
Columns: requires_receipt, report_id
Validation Rules
amount_positive
error
Validation failed
distance_within_configured_bounds
error
Validation failed
expense_type_valid_for_org
error
Validation failed
no_duplicate_expense_type_in_report
error
Validation failed
receipt_count_sufficient_if_required
error
Validation failed
currency_matches_org
error
Validation failed
calculated_reimbursement_matches_inputs
error
Validation failed
report_must_be_in_draft_on_create
error
Validation failed
Business Rules
mutual_exclusivity_transport
An expense report must not contain both a mileage item and a public_transit item simultaneously. These represent contradictory transport methods for the same journey. If both types are attempted, the second addition must be rejected with a user-visible error.
receipt_required_above_threshold
When the calculated_reimbursement (or amount for non-mileage types) of an expense item exceeds the org-configured receipt threshold (default 100 NOK for HLF), the requires_receipt flag is set to true and the item cannot transition to submitted state until at least one receipt is attached via the receipts table.
mileage_requires_distance_not_amount
Expense items of type 'mileage' must have distance_km populated and must NOT have amount populated directly. The reimbursement is always derived from distance_km multiplied by the snapshotted rate_per_km. This enforces consistent calculation and prevents manual override of the per-km rate.
non_mileage_requires_amount_not_distance
Expense items of type 'toll', 'parking', or 'public_transit' must have amount populated and must NOT have distance_km populated. These are direct cost entries, not distance-based calculations.
snapshot_rate_on_creation
When a mileage expense item is created, the org-specific per-km reimbursement rate is fetched from configuration and stored in rate_per_km on the item. This snapshot ensures the reimbursement amount cannot change retroactively if the org later modifies its rate configuration.
snapshot_receipt_threshold_on_creation
The org-specific receipt threshold in NOK is fetched and stored in receipt_threshold_applied at the time the expense item is persisted. This ensures that threshold-based decisions remain stable and auditable even if the org later reconfigures its threshold.
immutable_after_report_submission
Expense items belonging to a report that has entered 'submitted', 'approved', or 'rejected' status must not be modified by the peer mentor. Only coordinators with correction authorization may update items via the correction workflow, which creates an immutable audit record of the change.
accounting_sync_on_approval
When the parent travel_expense_reports record transitions to 'approved', the accounting sync orchestrator is triggered to forward approved expense items to the configured ERP (Xledger for Blindeforbundet, Dynamics for HLF). Each item is mapped to the appropriate ledger account based on expense_type.
cascade_delete_with_report
When a parent travel_expense_report is deleted, all associated expense_items and their receipts are cascade-deleted. The database enforces ON DELETE CASCADE on report_id. This rule is triggered at the database level, not the application layer.
CRUD Operations
Storage Configuration
Entity Relationships
Expense items above configured thresholds require one or more photographic receipt attachments
Each expense report contains one or more individual expense line items of different types