Receipt
Data Entity
Description
Photographic evidence attached to an expense item, captured via device camera or gallery. Stores compressed image storage path and thumbnail URL alongside the linked expense item ID; mandatory for HLF expense items exceeding 100 NOK.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Primary key, auto-generated UUID for each receipt record | PKrequiredunique |
expense_item_id |
uuid |
Foreign key referencing the parent expense item this receipt documents. An expense item may have multiple receipts. | required |
organization_id |
uuid |
Organization scope for RLS policy enforcement. Denormalized from the parent expense report to allow direct bucket-policy evaluation without joins. | required |
uploaded_by |
uuid |
User ID of the peer mentor or coordinator who uploaded the receipt. Used for ownership checks and audit trail. | required |
storage_path |
string |
Supabase Storage object path for the compressed full-resolution receipt image. Format: receipts/{organization_id}/{expense_item_id}/{id}. Never exposed directly to clients — signed URLs are generated on demand. | requiredunique |
thumbnail_url |
string |
Public or signed URL pointing to the compressed thumbnail variant generated by the Supabase Edge Function at upload time. Used by Receipt Thumbnail Widget for grid display without loading full-resolution images. | - |
file_size_bytes |
integer |
Size of the compressed image in bytes after client-side compression. Used for storage quota monitoring and upload progress reporting. | required |
mime_type |
enum |
MIME type of the uploaded image file. Constrained to supported image formats only. | required |
upload_status |
enum |
Lifecycle status of the receipt upload. Pending indicates client-side queued but not yet confirmed by storage backend; uploaded means Supabase Storage confirmed receipt; failed means upload was rejected or timed out. | required |
original_filename |
string |
Original filename as provided by the device camera or gallery picker before compression and renaming. Stored for audit and user reference only — not used for storage addressing. | - |
compression_ratio |
decimal |
Ratio of compressed size to original size (0.0–1.0) recorded at upload time by flutter_image_compress. Useful for storage cost analysis and quality audits. | - |
uploaded_at |
datetime |
UTC timestamp when the receipt was successfully confirmed as stored in Supabase Storage. Set server-side on status transition to 'uploaded'. | required |
deleted_at |
datetime |
Soft-delete timestamp. When set, the receipt is logically removed from active queries but retained for financial audit trail compliance. The corresponding Storage object is hard-deleted immediately; only the metadata row is soft-deleted. | - |
Database Indexes
idx_receipts_expense_item_id
Columns: expense_item_id
idx_receipts_uploaded_by
Columns: uploaded_by
idx_receipts_organization_id
Columns: organization_id
idx_receipts_upload_status
Columns: upload_status
idx_receipts_uploaded_at
Columns: uploaded_at
idx_receipts_expense_item_status
Columns: expense_item_id, upload_status
idx_receipts_storage_path
Columns: storage_path
idx_receipts_active
Columns: expense_item_id, deleted_at
Validation Rules
file_size_within_limit
error
Validation failed
mime_type_is_image
error
Validation failed
expense_item_exists_and_active
error
Validation failed
storage_path_unique
error
Validation failed
uploaded_by_matches_session_user
error
Validation failed
compression_ratio_plausible
warning
Validation failed
upload_status_transition_valid
error
Validation failed
Business Rules
mandatory_receipt_above_threshold
For HLF organizations, any expense item with an amount exceeding 100 NOK must have at least one receipt with upload_status = 'uploaded' before the parent travel expense report can be submitted. Expense items below the threshold may be submitted without a receipt.
receipt_belongs_to_accessible_expense_item
A receipt may only be created for an expense item that the authenticated user owns (peer mentor) or has coordinator-level access to (coordinator acting on behalf). Enforced via Supabase RLS policy joining receipts → expense_items → travel_expense_reports → users.
receipt_immutable_after_approval
Once the parent travel expense report enters 'approved' or 'rejected' status in reimbursement_approvals, no new receipts may be added and existing receipts may not be deleted. The financial record must remain intact for audit purposes.
soft_delete_with_storage_cleanup
When a receipt is deleted (deleted_at set), the corresponding Supabase Storage object at storage_path must be hard-deleted immediately by the Image Storage Backend to prevent orphaned billing. The metadata row is soft-deleted and retained for audit trail.
thumbnail_generated_on_upload_confirmation
Upon successful transition of upload_status to 'uploaded', a Supabase Edge Function is triggered to generate a compressed thumbnail and write the resulting URL back to thumbnail_url. The receipt is considered fully processed only after thumbnail_url is populated.
organization_scoped_storage_path
The storage_path must follow the prescribed format receipts/{organization_id}/{expense_item_id}/{id} to ensure bucket-level RLS policies can enforce organization isolation without additional join queries.
CRUD Operations
Storage Configuration
Entity Relationships
Expense items above configured thresholds require one or more photographic receipt attachments