Expense Receipt
Data Entity
Description
Stores metadata and storage references for receipt images and documents attached to expense claims. Supports mandatory audit documentation requirements for Bufdir-funded organisations, enabling peer mentors to photograph receipts immediately after incurring costs and attach them to travel expense registrations.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Primary key — unique identifier for the receipt record | PKrequiredunique |
expense_id |
uuid |
Foreign key to the expense this receipt is attached to | required |
storage_key |
string |
Object storage path/key used to retrieve the file from S3-compatible storage (e.g. receipts/{org_id}/{expense_id}/{uuid}.jpg) | requiredunique |
original_filename |
string |
Original filename as provided by the device image picker or camera, preserved for display purposes | - |
mime_type |
string |
MIME type of the uploaded file (e.g. image/jpeg, image/png, application/pdf) | required |
file_size_bytes |
integer |
Size of the stored (compressed) file in bytes, used for storage quota monitoring | required |
original_size_bytes |
integer |
Size of the original uncompressed file before client-side compression, used for compression ratio reporting | - |
upload_status |
enum |
Current upload lifecycle state of the receipt | required |
virus_scan_status |
enum |
Result of the server-side virus scan performed after upload to object storage | - |
virus_scan_completed_at |
datetime |
Timestamp when the virus scan completed, used for audit trail | - |
upload_started_at |
datetime |
Timestamp when the chunked upload was initiated, used for resumable upload tracking | - |
upload_completed_at |
datetime |
Timestamp when all chunks were received and the file was confirmed stored in object storage | - |
upload_retry_count |
integer |
Number of times the upload was retried after a failure, used for diagnostics | required |
chunk_upload_id |
string |
External identifier for a resumable multipart upload session (e.g. S3 multipart upload ID), null when upload is not in progress | - |
is_required |
boolean |
Whether this receipt was required by the organisation's expense type rules at time of submission (denormalised for audit clarity) | required |
sort_order |
integer |
Display order of the receipt within the expense claim, set by the peer mentor when multiple receipts are attached | required |
deleted_at |
datetime |
Soft-delete timestamp — set when the peer mentor removes a receipt before expense submission. NULL means active. | - |
deleted_by_user_id |
uuid |
User who soft-deleted this receipt, for audit traceability | - |
created_at |
datetime |
Timestamp when the receipt record was created (upload initiated) | required |
updated_at |
datetime |
Timestamp of the last status update to this record | required |
Database Indexes
idx_expense_receipt_expense_id
Columns: expense_id
idx_expense_receipt_storage_key
Columns: storage_key
idx_expense_receipt_upload_status
Columns: upload_status
idx_expense_receipt_expense_id_deleted_at
Columns: expense_id, deleted_at
idx_expense_receipt_created_at
Columns: created_at
Validation Rules
file_size_within_limit
error
Validation failed
storage_key_format
error
Validation failed
expense_id_exists
error
Validation failed
upload_status_transition
error
Validation failed
mime_type_consistency
error
Validation failed
deleted_receipt_not_counted_as_required
warning
Validation failed
Business Rules
receipt_required_by_expense_type
Certain expense types (e.g. toll, parking, public transport, expenses over 100 NOK for HLF) require at least one receipt. The expense cannot be submitted without a receipt in an 'available' upload status when the requirement applies.
virus_scan_required_before_availability
A receipt must pass a clean virus scan result before its upload_status transitions to 'available'. Infected files are quarantined and the expense submission is blocked.
max_receipts_per_expense
A single expense claim may have at most 10 active (non-soft-deleted) receipts attached to prevent storage abuse.
receipt_immutable_after_approval
Once the parent expense has been approved (expense_approvals.status = 'approved'), receipts cannot be added, updated, or soft-deleted. The audit trail must remain intact.
supported_mime_types_only
Only image/jpeg, image/png, and application/pdf are accepted. Other MIME types are rejected at the receipt-storage infrastructure layer before the record is persisted.
org_scoped_storage_path
The storage_key must be prefixed with the organisation ID of the submitting user to enforce tenant isolation in object storage. Cross-org path access must be rejected.