core PK: id 11 required 1 unique

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.

17
Attributes
7
Indexes
8
Validation Rules
22
CRUD Operations

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
btree

Columns: report_id

idx_expense_items_user_id
btree

Columns: user_id

idx_expense_items_org_id
btree

Columns: org_id

idx_expense_items_expense_type
btree

Columns: expense_type

idx_expense_items_report_type
btree

Columns: report_id, expense_type

idx_expense_items_created_at
btree

Columns: created_at

idx_expense_items_requires_receipt
btree

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
on_create

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
on_create

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
on_create

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
on_create

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
on_create

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
on_create

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
on_update

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
on_update

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
on_delete

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.

Storage Configuration

Storage Type
primary_table
Location
main_db
Partitioning
No Partitioning
Retention
Permanent Storage

Entity Relationships

receipts
outgoing one_to_many

Expense items above configured thresholds require one or more photographic receipt attachments

optional cascade delete
travel_expense_reports
incoming one_to_many

Each expense report contains one or more individual expense line items of different types

optional cascade delete