Expense Receipt
Data Entity
Description
Stores metadata and cloud storage references for receipt images attached to individual expense items. Supports offline capture, compressed upload, and integrity verification for reimbursement workflows requiring receipt documentation (required for items above the 100 kr threshold per HLF policy).
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Primary key, generated client-side to support offline-first creation via mutation outbox | PKrequiredunique |
expense_item_id |
uuid |
Foreign key to expense_items; each receipt belongs to exactly one expense line item | required |
uploaded_by_user_id |
uuid |
FK to users; the peer mentor or coordinator who captured and uploaded the receipt | required |
storage_provider |
enum |
Cloud object storage provider where the file is persisted | required |
storage_bucket |
string |
Bucket or container name in the object storage provider | required |
storage_key |
string |
Full object key / path within the bucket for the compressed receipt file | requiredunique |
original_storage_key |
string |
Object key for the original uncompressed file; retained for potential re-compression or audit. Null if original was not preserved. | - |
original_file_name |
string |
File name as provided by the device camera or file picker, stored for display and audit | required |
mime_type |
enum |
MIME type of the stored (compressed) file | required |
file_size_bytes |
integer |
Size of the compressed file in bytes after upload; used for storage monitoring and display | required |
original_file_size_bytes |
integer |
Size of the original file before compression; null if original was not measured or retained | - |
checksum_sha256 |
string |
SHA-256 hash of the compressed file content; used for integrity verification on download and duplicate detection | required |
upload_status |
enum |
Lifecycle state of the receipt upload; pending while in offline queue, completed once confirmed in cloud storage | required |
upload_attempts |
integer |
Number of upload attempts made; used by retry-backoff-service to decide when to surface a user error | required |
last_upload_error |
text |
Human-readable error message from the most recent failed upload attempt; cleared on success | - |
compression_ratio |
decimal |
Ratio of compressed to original file size (e.g. 0.42 = 42% of original); null if no compression applied | - |
capture_method |
enum |
How the receipt image was captured; informs UX and audit trail | required |
uploaded_at |
datetime |
UTC timestamp when the upload to cloud storage was confirmed; null until upload_status = completed | - |
created_at |
datetime |
UTC timestamp when the receipt record was first created (may precede upload while in offline queue) | required |
updated_at |
datetime |
UTC timestamp of the last update to this record | required |
deleted_at |
datetime |
Soft-delete timestamp; non-null means the receipt was retracted. Physical file is scheduled for deletion by a cleanup job. | - |
Database Indexes
idx_expense_receipts_expense_item_id
Columns: expense_item_id
idx_expense_receipts_uploaded_by_user_id
Columns: uploaded_by_user_id
idx_expense_receipts_upload_status
Columns: upload_status
idx_expense_receipts_storage_key
Columns: storage_key
idx_expense_receipts_deleted_at
Columns: deleted_at
Validation Rules
allowed_mime_types
error
Validation failed
max_file_size_10mb
error
Validation failed
checksum_integrity_on_download
error
Validation failed
expense_item_id_must_exist
error
Validation failed
storage_key_uniqueness
error
Validation failed
upload_status_transition
error
Validation failed
Business Rules
receipt_required_above_threshold
Expense items with an amount exceeding 100 kr must have at least one receipt in upload_status = completed before the parent expense claim can be submitted for approval
no_receipt_deletion_after_submission
A receipt may not be soft-deleted once the parent expense claim has left draft status (pending/submitted/approved). Deletion is only permitted while the claim is in draft.
uploader_must_own_claim
The uploaded_by_user_id must match the user who owns the parent expense claim, unless the acting user is a coordinator submitting via proxy registration
offline_queue_retry_limit
If upload_attempts reaches 5 and upload_status is still not completed, the system surfaces a hard error to the user and stops retrying automatically. Manual retry is still allowed.
physical_file_cleanup_on_soft_delete
When deleted_at is set, the associated cloud storage object must be scheduled for deletion. The storage_key remains in the row for audit until a cleanup job removes both.
single_receipt_per_item_recommended
Multiple receipts per expense item are technically allowed (e.g. front and back of paper receipt), but the expense approval UI highlights items with more than 3 receipts for coordinator review.