Expense
Data Entity
Description
Represents a reimbursable expense claim submitted by a peer mentor or coordinator, covering travel costs (km reimbursement, tolls, parking, public transit) incurred during activities. Supports configurable auto-approval thresholds, receipt attachment, confidentiality declarations, and integration with external accounting systems.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Unique identifier for the expense record | PKrequiredunique |
activity_id |
uuid |
Foreign key referencing the activity this expense is associated with | required |
expense_type_id |
uuid |
Foreign key referencing the configured expense type (e.g. km reimbursement, toll, parking, public transit) | required |
user_id |
uuid |
Foreign key referencing the peer mentor or coordinator who owns this expense claim | required |
organization_id |
uuid |
Tenant scoping — organization this expense belongs to, enforced for all queries | required |
submitted_by_user_id |
uuid |
User who submitted this expense — differs from user_id when a coordinator registers on behalf of a peer mentor (proxy registration) | required |
is_proxy_submission |
boolean |
True when the expense was submitted by a coordinator on behalf of a peer mentor | required |
expense_date |
datetime |
Date the expense was incurred (not the submission date) | required |
amount |
decimal |
Monetary amount in NOK for fixed-cost expenses (tolls, parking, public transit). Null for pure km-based claims where amount is computed from distance × rate. | - |
distance_km |
decimal |
Distance in kilometres for km-reimbursement expense types. Null for non-km expense types. | - |
reimbursement_rate_nok_per_km |
decimal |
Rate per km in NOK at the time of submission, snapshot from the organization's expense type configuration to preserve historical accuracy | - |
computed_amount |
decimal |
Final computed reimbursement amount in NOK. For km types: distance_km × reimbursement_rate_nok_per_km. For fixed types: equals amount. Stored for audit and reporting. | required |
currency |
string |
ISO 4217 currency code, always NOK for this platform | required |
status |
enum |
Lifecycle status of the expense claim | required |
description |
text |
Optional free-text description or notes from the submitter about this expense | - |
requires_receipt |
boolean |
Computed flag: true when computed_amount exceeds the organization's receipt threshold (default 100 NOK). Stored for efficient querying and validation. | required |
receipt_verified |
boolean |
Set to true by an approver or auto-approval rule once at least one valid receipt is confirmed for claims where requires_receipt is true | required |
requires_confidentiality_declaration |
boolean |
True when the expense type is configured to mandate a confidentiality declaration (e.g. driver honorarium for Blindeforbundet) | required |
declaration_id |
uuid |
Foreign key to the associated confidentiality declaration when requires_confidentiality_declaration is true | - |
auto_approval_eligible |
boolean |
Computed at submission time: true when the expense meets all auto-approval criteria (e.g. distance ≤ 50 km and no receipt required). Stored for auditability. | required |
submitted_at |
datetime |
Timestamp when the expense was submitted for approval (status transitioned from draft to submitted/auto_approved) | - |
approved_at |
datetime |
Timestamp when the expense was approved (manually or automatically) | - |
approved_by_user_id |
uuid |
User who manually approved the expense. Null for auto-approved expenses. | - |
rejected_at |
datetime |
Timestamp when the expense was rejected | - |
rejected_by_user_id |
uuid |
User who rejected the expense | - |
rejection_reason |
text |
Mandatory reason text when an approver rejects an expense claim | - |
accounting_export_reference |
string |
External reference ID returned by the accounting system (Xledger / Dynamics) after successful export. Null until exported. | - |
accounting_exported_at |
datetime |
Timestamp of successful export to the external accounting system | - |
created_at |
datetime |
Record creation timestamp, set by the database | required |
updated_at |
datetime |
Last modification timestamp, updated on every write | required |
deleted_at |
datetime |
Soft-delete timestamp. Null means record is active. Set when a draft expense is withdrawn before submission. | - |
Database Indexes
idx_expenses_user_id
Columns: user_id
idx_expenses_activity_id
Columns: activity_id
idx_expenses_organization_status
Columns: organization_id, status
idx_expenses_organization_expense_date
Columns: organization_id, expense_date
idx_expenses_expense_type_id
Columns: expense_type_id
idx_expenses_submitted_at
Columns: submitted_at
idx_expenses_accounting_export
Columns: accounting_exported_at, organization_id
idx_expenses_deleted_at
Columns: deleted_at
Validation Rules
amount_or_distance_required
error
Validation failed
amount_positive
error
Validation failed
distance_positive
error
Validation failed
expense_date_not_in_future
error
Validation failed
expense_type_active
error
Validation failed
activity_belongs_to_submitter_org
error
Validation failed
rejection_reason_required_on_reject
error
Validation failed
km_type_requires_distance
error
Validation failed
receipt_verified_before_approval
error
Validation failed
proxy_requires_coordinator_role
error
Validation failed
Business Rules
receipt_required_above_threshold
When computed_amount exceeds the organization's configured receipt threshold (default 100 NOK), the expense cannot be submitted without at least one attached receipt. Sets requires_receipt = true and blocks submission until receipt_verified = true.
auto_approval_below_threshold
Expenses meeting all auto-approval criteria — distance ≤ 50 km for km-type OR computed_amount below the manual-approval threshold AND no receipt required — are automatically transitioned to auto_approved status on submission without coordinator action.
expense_type_exclusivity
Certain expense types are mutually exclusive within a single expense record. Specifically, km-based reimbursement and public transit cannot both be selected simultaneously, as this represents an invalid combination. The expense_type configuration carries the exclusivity group; the service enforces it at submission.
status_forward_only_transitions
The status field may only advance forward through the lifecycle: draft → submitted → (auto_approved | pending_approval) → (approved | rejected). Reverting a submitted or approved expense to draft is not permitted. A rejected expense may only be corrected by creating a new expense record.
coordinator_approval_required_above_threshold
Expenses that are not auto_approval_eligible must be reviewed and actioned by a coordinator or organization admin with the appropriate permission. The expense remains in pending_approval status until an authorized approver acts.
confidentiality_declaration_required_for_driver_expenses
When the selected expense type is configured as requiring a confidentiality declaration (e.g. driver honorarium for Blindeforbundet), a linked declaration record must exist and be signed before the expense can be submitted.
org_tenant_isolation
All expense reads and writes are scoped to the authenticated user's organization_id. Cross-organization expense access is forbidden regardless of user role, except for global admins performing system-level audits.
soft_delete_only_drafts
Only expense records in draft status may be soft-deleted (deleted_at set). Submitted, approved, or rejected expenses are immutable financial records and cannot be deleted — they must be rejected or superseded.
accounting_export_immutability
Once accounting_exported_at is set, the expense record is locked — no further updates to financial fields (amount, distance_km, expense_type_id) are permitted to maintain accounting system integrity.