Expense
Data Entity
Description
A travel expense or reimbursement claim submitted by a peer mentor or coordinator, linked to an activity. Captures kilometre reimbursement, toll fees, parking, public transport, and other allowable expense types with configurable mutual-exclusivity rules, threshold-based auto-approval, and full lifecycle tracking from draft through reimbursement.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Primary key. Globally unique identifier for the expense record. | PKrequiredunique |
user_id |
uuid |
FK to users. The peer mentor or coordinator who incurred the expense. | required |
organization_id |
uuid |
FK to organizations. Tenant isolation key. Set from the submitting user's org context at creation time. | required |
activity_id |
uuid |
FK to activities. The activity this expense is associated with. May be null for declarations or standalone cost claims not tied to a specific activity record. | - |
expense_type_id |
uuid |
FK to expense_types. The primary expense type selected from the organisation-specific catalogue. | required |
status |
enum |
Lifecycle status of the expense claim. Drives approval routing and reimbursement processing. | required |
distance_km |
decimal |
Kilometres driven, required when expense type is kilometre reimbursement. Precision (10,2). | - |
amount |
decimal |
Total monetary amount of the expense claim in the stated currency. Computed server-side for km-based types; entered directly for fixed costs. Precision (12,2). | - |
currency |
string |
ISO 4217 currency code. Defaults to NOK for all Norwegian organisations. | required |
expense_date |
datetime |
The date the expense was actually incurred (not the submission date). Used for reporting period allocation. | required |
description |
text |
Optional free-text note added by the submitter. Not used for expense type selection; expense types are structured choices. | - |
submission_date |
datetime |
Timestamp when the expense was formally submitted (status changed from draft to submitted). | - |
auto_approved |
boolean |
True if the expense was approved automatically by the rule engine without human review. | required |
auto_approval_rule_id |
uuid |
FK to auto_approval_rules (if applicable). Records which rule triggered auto-approval for audit traceability. | - |
rejection_reason |
text |
Human-readable reason provided by the approver when an expense is rejected. Required when status transitions to rejected. | - |
confidentiality_declaration_id |
uuid |
FK to declaration records. Required for driver expense types (e.g., Blindeforbundet chauffeur honorarium). Links to the signed confidentiality declaration. | - |
requires_receipt |
boolean |
Computed/cached flag indicating whether at least one receipt is required for this expense based on its type and amount. Populated by receipt requirement service at submission time. | required |
proxy_submitted_by |
uuid |
FK to users. Populated when a coordinator submits the expense on behalf of a peer mentor. Null for self-submitted expenses. | - |
version |
integer |
Optimistic locking counter. Incremented on every update. Prevents lost updates during concurrent approval operations. | required |
synced_to_accounting |
boolean |
True once the expense has been successfully exported to the organisation's accounting system (Xledger, Dynamics/Visma). | required |
accounting_reference_id |
string |
External reference ID returned by the accounting system after successful export. Used for idempotency on re-export attempts. | - |
created_at |
datetime |
Record creation timestamp. Set once at insert, never updated. | required |
updated_at |
datetime |
Last modification timestamp. Updated on every write operation. | required |
Database Indexes
idx_expense_user_created
Columns: user_id, created_at
idx_expense_org_status
Columns: organization_id, status
idx_expense_activity
Columns: activity_id
idx_expense_expense_type
Columns: expense_type_id
idx_expense_org_date
Columns: organization_id, expense_date
idx_expense_status_org_submitted
Columns: status, organization_id, submission_date
Validation Rules
user_id_required
error
Validation failed
expense_type_id_required
error
Validation failed
expense_date_required_and_not_future
error
Validation failed
distance_km_positive_when_set
error
Validation failed
amount_non_negative
error
Validation failed
km_or_amount_required_on_submit
error
Validation failed
currency_iso_format
error
Validation failed
version_matches_on_update
error
Validation failed
Business Rules
expense_type_mutual_exclusivity
Selected expense types must not violate the organisation's mutual-exclusivity rules. For example, kilometre reimbursement and a public-transport ticket cannot be claimed for the same journey. The expense type catalogue defines exclusivity groups per organisation.
receipt_required_above_threshold
If the expense amount exceeds the organisation-configured receipt threshold (default 100 NOK for HLF), at least one receipt must be attached before the expense can be submitted. The receipt requirement service evaluates this per expense type, amount, and organisation.
auto_approval_threshold_evaluation
On submission, the auto-approval rule engine evaluates all enabled auto-approval rules for the organisation. Expenses below the configured kilometre threshold (e.g., < 50 km) or with no receipts required are automatically approved without human review. The auto_approved flag and auto_approval_rule_id are set accordingly.
confidentiality_declaration_required_for_driver_types
For expense types classified as driver or chauffeur (Blindeforbundet requirement), a valid confidentiality declaration must exist for the submitting user before the expense can be submitted.
status_transition_guard
Expense status may only advance through the defined lifecycle: draft → submitted → pending_approval|auto_approved → approved|rejected → reimbursed. Reverse transitions are forbidden except for admin corrections with audit log entry.
rejection_reason_required
When an expense is rejected, a non-empty rejection_reason must be provided by the approving coordinator or org admin.
proxy_scope_validation
When proxy_submitted_by is set, the submitting coordinator must belong to the same local association as the peer mentor (user_id). Cross-association proxy expense submission is not permitted.
tenant_isolation
All expense queries must be scoped by organization_id. No cross-tenant expense data may be read or written except by Global Admins operating in explicit audit contexts.
immutable_after_reimbursement
Once an expense reaches 'reimbursed' status, its core financial fields (amount, distance_km, expense_type_id) become immutable. Only administrative metadata fields may be updated.
accounting_export_idempotency
Before exporting an approved expense to an external accounting system, the accounting_reference_id field is checked. If already populated, the export is skipped and the existing reference is returned to prevent duplicate journal entries.