Expense Claim
Data Entity
Description
Represents a reimbursement claim submitted by a peer mentor for travel and other expenses incurred during an activity. Claims go through a two-path approval workflow: auto-approved when below configured distance and amount thresholds, otherwise requiring coordinator attestation. Each claim is linked to an activity and may include mileage, toll, parking, or transit expense lines.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Primary key. Stable identifier for the expense claim, generated server-side on insert. | PKrequiredunique |
activity_id |
uuid |
Foreign key referencing the parent activity this claim is attached to. Enforces the one-to-one relationship: each travel-eligible activity may have at most one expense claim. | requiredunique |
peer_mentor_id |
uuid |
Foreign key referencing the peer mentor (user) who submitted the claim. Must match the peer_mentor_id on the linked activity record. | required |
organization_id |
uuid |
Foreign key referencing the organization the claim belongs to. Used for RLS scoping, threshold configuration lookup, and accounting export routing. | required |
status |
enum |
Lifecycle state of the claim through the two-path approval workflow. Drives coordinator queue visibility, export eligibility, and mutation permissions. | required |
expense_types |
json |
JSONB array of selected expense type identifiers (e.g., ['kilometers', 'tolls']). Validated against the mutual-exclusion compatibility matrix at submission. Valid values: kilometers, tolls, parking, public_transit. | required |
total_amount |
decimal |
Total reimbursement amount in NOK (sum of all expense lines). Stored as NUMERIC(10,2). Used for threshold evaluation and accounting export. | required |
distance_km |
decimal |
Total kilometers driven. Present only when expense_types includes 'kilometers'. Used for auto-approval threshold comparison. Null for non-mileage claims. | - |
receipt_required |
boolean |
Snapshot flag indicating whether a receipt was required at submission time based on the organization's configured amount threshold. Stored at claim creation to preserve the requirement even if the threshold configuration changes later. | required |
receipt_path |
text |
Supabase Storage object path (org-scoped bucket) for the attached receipt image. Null when no receipt is required or attached. Signed URLs are generated on-demand, not stored. | - |
submitted_at |
datetime |
UTC timestamp when the peer mentor submitted the claim. Set once at creation by the server, never updated. Used for coordinator queue ordering and Bufdir reporting period filtering. | required |
approved_at |
datetime |
UTC timestamp when the claim reached an approved state (auto_approved or coordinator_approved). Null until approval occurs. Used for accounting export date filtering. | - |
rejected_at |
datetime |
UTC timestamp when the claim was rejected by a coordinator. Null unless status = rejected. Retained for audit reporting. | - |
coordinator_id |
uuid |
Foreign key referencing the coordinator user who made the attestation decision (approve or reject). Null for auto-approved claims and claims still pending review. | - |
coordinator_comment |
text |
Optional free-text comment provided by the coordinator on approval or rejection. Required by application-layer validation when transitioning to rejected. Shown in the claim status audit timeline. | - |
export_run_id |
uuid |
Foreign key referencing the export_run that first included this claim in an accounting system export. Stamped atomically by the double-export guard. Once set, prevents re-export in future runs. | - |
created_at |
datetime |
UTC timestamp of record creation. Set by the database on insert, never updated. | required |
updated_at |
datetime |
UTC timestamp of the most recent row modification. Updated by a database trigger on every UPDATE. Used for optimistic concurrency checks during status transitions. | required |
Database Indexes
idx_expense_claim_activity_id
Columns: activity_id
idx_expense_claim_org_status
Columns: organization_id, status
idx_expense_claim_peer_mentor_submitted
Columns: peer_mentor_id, submitted_at
idx_expense_claim_export_run_id
Columns: export_run_id
idx_expense_claim_status
Columns: status
idx_expense_claim_org_submitted_at
Columns: organization_id, submitted_at
Validation Rules
total_amount_positive
error
Validation failed
activity_exists_and_eligible
error
Validation failed
distance_km_required_for_mileage
error
Validation failed
receipt_present_when_required
error
Validation failed
valid_expense_type_values
error
Validation failed
expense_types_non_empty
error
Validation failed
submitted_at_set_by_server
error
Validation failed
coordinator_belongs_to_same_organization
error
Validation failed
export_run_id_immutable_once_set
error
Validation failed
Business Rules
single_claim_per_activity
A travel-eligible activity may have at most one expense claim. The UNIQUE constraint on activity_id enforces this at the database level. The expense-submission-service checks for an existing claim before creating a new one and returns a conflict error to the UI.
auto_approval_threshold_evaluation
On claim submission, the threshold-evaluation-service computes combined distance_km and total_amount and compares against the organization's configured thresholds. If both are below their respective thresholds, status is set to 'auto_approved' and approved_at is stamped immediately. Otherwise status is set to 'pending_review'.
receipt_required_above_threshold
If total_amount exceeds the organization's configured receipt threshold (e.g., 100 NOK), receipt_required is set to true and a receipt_path must be provided before the submission is accepted. Validated by receipt-threshold-validator before the expense-submission-service persists the record.
expense_type_mutual_exclusion
Certain expense type combinations are prohibited to prevent reimbursement errors (e.g., 'kilometers' and 'public_transit' cannot be selected simultaneously). The mutual-exclusion-rule-engine validates the selected set before submission; expense-validation-service re-validates server-side before persistence.
valid_status_transition
Status must progress through the allowed state machine: submitted → (auto_approved | pending_review) → (coordinator_approved | rejected) → exported. Backward transitions and skipped states are rejected. The approval-workflow-service enforces valid transitions and expense-claim-status-repository applies an optimistic concurrency check on the expected current status.
coordinator_comment_required_on_rejection
When transitioning to 'rejected' status, coordinator_comment must be non-empty. Enforced at the service layer in approval-workflow-service. The approval-action-sheet UI also enforces this constraint before allowing submission.
no_double_export
A claim may only be included in one accounting export run. The double-export-guard atomically filters claims where export_run_id IS NOT NULL from the candidate set and stamps the export_run_id in the same transaction as the export run creation.
immutable_after_export
Once a claim reaches 'exported' status, no further status transitions or field mutations are permitted. The approval-workflow-service and expense-claim-status-repository reject any update attempt on an exported claim.
only_approved_claims_exported
The accounting export pipeline only processes claims with status 'coordinator_approved' or 'auto_approved'. The approved-claims-query excludes all other status values, and the accounting-exporter-service validates this precondition before mapping claim fields.
claim_organization_matches_activity
The organization_id on the expense claim must match the organization_id on the referenced activity. Validated by expense-submission-service at creation time to prevent cross-organization data leakage.
peer_mentor_matches_activity
The peer_mentor_id on the claim must match the peer_mentor_id on the referenced activity record. Prevents coordinators from creating claims attributed to a different mentor than the activity.
notify_on_status_change
Whenever the claim status changes (to auto_approved, coordinator_approved, or rejected), the approval-notification-service dispatches a push notification and in-app notification to the submitting peer mentor, including the new status and coordinator_comment if present.
CRUD Operations
Storage Configuration
Entity Relationships
A travel-eligible activity may have exactly one linked expense claim covering all expense lines for that activity
Every state transition of an expense claim generates an immutable audit event forming a complete approval history timeline
An approved expense claim is stamped with the export_run_id when it is first included in an accounting system export, preventing double-export
When a claim includes a kilometers-driven expense type, exactly one mileage_claim record stores the distance and calculated reimbursement
When a claim amount exceeds the configured threshold, exactly one receipt image record is linked to the expense claim for audit purposes