Expenses
Data Entity
Description
Records travel expense and reimbursement claims submitted by peer mentors and coordinators, capturing amounts, distances, expense types, and approval status. Central to the financial compliance workflow connecting activity registration to reimbursement processing and Bufdir reporting.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Unique identifier for the expense record | PKrequiredunique |
user_id |
uuid |
Reference to the user who submitted the expense claim (peer mentor or coordinator) | required |
activity_id |
uuid |
Reference to the activity this expense is associated with; every expense must be linked to a registered activity | required |
expense_type_id |
uuid |
Reference to the organisation-specific expense type from the expense type catalogue | required |
organization_id |
uuid |
Organisation scope for multi-tenant isolation; derived from the submitting user's organisation | required |
status |
enum |
Current lifecycle status of the expense claim | required |
total_amount |
decimal |
Total calculated expense amount in NOK, sum of all cost components (distance cost + tolls + parking + public transport + other) | required |
currency |
string |
ISO 4217 currency code, defaults to NOK for Norwegian organisations | required |
distance_km |
decimal |
Kilometres driven for the activity; null if not a driving expense | - |
km_rate |
decimal |
Kilometre reimbursement rate in NOK applied at time of submission, captured for audit trail | - |
toll_amount |
decimal |
Toll fees in NOK; null or 0 if no tolls claimed | - |
parking_amount |
decimal |
Parking costs in NOK; null or 0 if no parking claimed | - |
public_transport_amount |
decimal |
Public transport costs in NOK (bus, train, taxi); mutually exclusive with distance_km per expense type rules | - |
other_amount |
decimal |
Other reimbursable costs not covered by specific fields (e.g. driver honorarium for Blindeforbundet) | - |
description |
text |
Free-text description or notes about the expense claim | - |
expense_date |
datetime |
Date when the expense was incurred, typically matches the linked activity date | required |
receipt_required |
boolean |
Whether a receipt attachment is mandatory for this expense, determined by expense type rules and amount thresholds | required |
has_receipt |
boolean |
Whether at least one receipt image has been attached to this expense | required |
declaration_accepted |
boolean |
Whether the user has accepted the required confidentiality or expense declaration for this claim | required |
declaration_version_id |
string |
Version identifier of the declaration text accepted by the user, for audit compliance | - |
auto_approved |
boolean |
Whether the expense was automatically approved by the auto-approval rule engine (under threshold) | required |
auto_approval_rule_id |
uuid |
Reference to the auto-approval rule that approved this expense, null if manually reviewed | - |
submitted_by |
uuid |
User who actually submitted the expense; differs from user_id when a coordinator submits on behalf of a peer mentor (proxy) | - |
rejection_reason |
text |
Reason provided by the reviewer when rejecting the expense claim | - |
reviewed_by |
uuid |
User who approved or rejected the expense, null if pending or auto-approved | - |
reviewed_at |
datetime |
Timestamp when the expense was approved or rejected | - |
external_reference_id |
string |
Reference ID from external accounting system after export (Xledger, Dynamics), used for idempotent re-export | - |
exported_at |
datetime |
Timestamp when the expense was exported to the external accounting system | - |
version |
integer |
Optimistic locking version counter to prevent concurrent modification conflicts during approval | required |
created_at |
datetime |
Timestamp when the expense record was created | required |
updated_at |
datetime |
Timestamp when the expense record was last modified | required |
Database Indexes
idx_expenses_user_id
Columns: user_id
idx_expenses_activity_id
Columns: activity_id
idx_expenses_expense_type_id
Columns: expense_type_id
idx_expenses_organization_id
Columns: organization_id
idx_expenses_status
Columns: status
idx_expenses_org_status
Columns: organization_id, status
idx_expenses_expense_date
Columns: expense_date
idx_expenses_org_date
Columns: organization_id, expense_date
idx_expenses_user_status
Columns: user_id, status
idx_expenses_created_at
Columns: created_at
idx_expenses_external_ref
Columns: external_reference_id
Validation Rules
positive_amounts
error
Validation failed
positive_distance
error
Validation failed
valid_expense_type_reference
error
Validation failed
valid_activity_reference
error
Validation failed
valid_user_reference
error
Validation failed
expense_date_not_future
error
Validation failed
expense_date_not_too_old
warning
Validation failed
receipt_attachment_when_required
error
Validation failed
currency_code_format
error
Validation failed
description_max_length
error
Validation failed
rejection_reason_required
error
Validation failed
version_monotonic_increment
error
Validation failed
Business Rules
expense_must_link_to_activity
Every expense claim must be linked to an existing registered activity. Expenses cannot exist independently of activities.
mutual_exclusivity_enforcement
Expense type mutual-exclusivity rules must be enforced: for example, kilometre reimbursement and public transport costs cannot be claimed simultaneously if the expense type rules prohibit it. Rules are organisation-specific.
receipt_required_above_threshold
Receipt photo attachment is mandatory when the total expense amount exceeds the organisation-configured threshold (e.g., HLF requires receipts for expenses over 100 NOK). Submission is blocked until receipt is attached.
auto_approval_under_threshold
Expenses under the organisation-configured auto-approval threshold (e.g., under 50 km with no additional costs at HLF) are automatically approved without manual review. The auto_approved flag and rule reference are recorded.
status_transition_validity
Expense status transitions must follow the valid lifecycle: draft → pending_review → approved/rejected, approved → reimbursed. Cancelled can be set from draft or pending_review only. No backward transitions except rejection followed by resubmission as a new record.
optimistic_locking_on_approval
Approval and rejection operations must use optimistic locking via the version field to prevent concurrent modification conflicts when multiple reviewers access the same expense.
organisation_scoping
All expense queries and operations must be scoped to the requesting user's organisation. Coordinators see expenses from their local association only; Org Admins see all expenses within their organisation.
declaration_required_for_submission
Certain expense types (e.g., driver expenses at Blindeforbundet) require an accepted confidentiality declaration before the expense can be submitted. declaration_accepted must be true.
proxy_submission_attribution
When a coordinator submits an expense on behalf of a peer mentor, user_id must reflect the peer mentor (expense owner) and submitted_by must record the coordinator. Both must belong to the same local association.
immutable_after_approval
Once an expense reaches approved or reimbursed status, its financial fields (amounts, distance, expense type) cannot be modified. Only status transitions and external reference updates are permitted.
audit_trail_on_status_change
Every status change (submission, approval, rejection, reimbursement, cancellation) must generate an audit log entry recording the actor, timestamp, and reason.
total_amount_calculation
The total_amount field must equal the sum of (distance_km × km_rate) + toll_amount + parking_amount + public_transport_amount + other_amount. Recalculated on every save.