Expense Item
Data Entity
Description
A single line item within an expense claim representing one reimbursable cost — mileage, toll, parking, public transport, accommodation, meal, or chauffeur fee. Enforces mutually exclusive expense type combinations, receipt thresholds, and auto-approval eligibility rules per organization configuration.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Primary key, generated server-side (UUID v4). Stable offline ID assigned before sync. | PKrequiredunique |
claim_id |
uuid |
Foreign key to expense_claims. Every item belongs to exactly one claim. | required |
expense_type |
enum |
Structured expense type. Fixed vocabulary prevents invalid combinations (e.g. mileage + public_transport on the same item). HLF requirement: no free-text type. | required |
description |
text |
Optional free-text note for the item (e.g. route description for mileage, vendor name for a receipt). Not a substitute for expense_type — type is always the structured field. | - |
amount |
decimal |
Monetary value of the item in the stated currency. For mileage this is computed as distance_km × unit_rate_nok_per_km at save time and stored for auditability. | required |
currency |
string |
ISO 4217 currency code. Defaults to 'NOK'. Stored explicitly to support future multi-currency scenarios. | required |
distance_km |
decimal |
Kilometers driven. Required when expense_type = 'mileage'. Null for all other types. | - |
unit_rate_nok |
decimal |
Rate per km in NOK at the time of claim creation, snapshotted from expense-type-config-repository. Immutable after save so rate changes do not retroactively alter approved claims. | - |
quantity |
decimal |
Generic quantity for non-mileage countable items (e.g. number of toll passages, parking days). Defaults to 1. | - |
requires_receipt |
boolean |
Computed flag set at item creation: true when amount >= organization's receipt_threshold (default 100 NOK for HLF). Drives UI prompt to attach a receipt and blocks approval if no receipt is linked. | required |
is_auto_approvable |
boolean |
True when the item meets all auto-approval criteria: mileage-only type, distance_km <= org threshold (default 50 km), and no receipt items present on the same claim. Set by expense-validation-service at create/update. | required |
status |
enum |
Approval lifecycle state for this individual item. Parent claim status is a rollup of all item statuses. | required |
approved_by_user_id |
uuid |
User ID of the coordinator or admin who manually approved or rejected this item. Null for auto-approved items and pending items. | - |
approved_at |
datetime |
Timestamp of approval or rejection. Set at the same time as status transition. | - |
rejection_reason |
text |
Free-text reason entered by the approver when rejecting. Required when status transitions to 'rejected'. | - |
sort_order |
integer |
Display ordering of items within a claim. Client-assigned, 0-indexed. | - |
created_at |
datetime |
Server-side creation timestamp (UTC). Immutable after insert. | required |
updated_at |
datetime |
Last-modified timestamp (UTC). Updated on every write including status transitions. | required |
Database Indexes
idx_expense_items_claim_id
Columns: claim_id
idx_expense_items_claim_status
Columns: claim_id, status
idx_expense_items_expense_type
Columns: expense_type
idx_expense_items_status
Columns: status
idx_expense_items_approved_by
Columns: approved_by_user_id
Validation Rules
amount_positive
error
Validation failed
distance_km_required_for_mileage
error
Validation failed
distance_km_upper_bound
warning
Validation failed
rejection_reason_required
error
Validation failed
currency_iso_format
error
Validation failed
expense_type_org_allowed
error
Validation failed
description_length
error
Validation failed
chauffeur_fee_requires_declaration
error
Validation failed
Business Rules
mileage_public_transport_mutual_exclusivity
A single expense item cannot have expense_type = 'mileage' and expense_type = 'public_transport' simultaneously. Across items on the same claim, if a mileage item and a public_transport item cover the same trip leg, the submission is flagged for manual review. Prevents double-dipping on the same journey segment.
receipt_required_above_threshold
If amount >= organization's receipt_threshold_nok (default 100 NOK), requires_receipt is set to true and the claim cannot transition out of 'pending' until at least one receipt is linked in expense_receipts. Enforced at approval time, not at creation.
auto_approval_eligibility
Items are auto-approvable when: expense_type is 'mileage', distance_km <= org auto_approval_distance_threshold (default 50 km), and no item on the parent claim has requires_receipt = true. Eligible items transition to 'auto_approved' immediately on claim submission without coordinator action.
mileage_rate_snapshot
unit_rate_nok is snapshotted from the current organization rate configuration at item creation and stored immutably. Subsequent rate config changes do not affect existing items. This ensures approved claim amounts are auditable against the rate in effect at the time of the activity.
amount_computed_for_mileage
For expense_type = 'mileage', amount is computed server-side as distance_km × unit_rate_nok. The client submits distance_km; the server rejects any client-supplied amount that deviates to prevent manipulation.
no_delete_after_approval
Items with status 'approved', 'auto_approved', or 'rejected' cannot be deleted. Cancellation (status = 'cancelled') is the soft-delete path; hard delete is only permitted for 'pending' items while the parent claim is still in draft.
claim_status_rollup
After any item status transition, expense-service recomputes the parent expense_claim status: all items auto_approved → claim auto_approved; any item approved/auto_approved and none pending → claim approved; any item rejected → claim partially_rejected; all items cancelled → claim cancelled.
accounting_export_lock
Once a claim has been dispatched to the accounting system via expense-batch-dispatcher, its items become immutable. Any correction attempt must open a new supplementary claim.