core PK: id 11 required 2 unique

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.

18
Attributes
6
Indexes
9
Validation Rules
19
CRUD Operations

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
btree unique

Columns: activity_id

idx_expense_claim_org_status
btree

Columns: organization_id, status

idx_expense_claim_peer_mentor_submitted
btree

Columns: peer_mentor_id, submitted_at

idx_expense_claim_export_run_id
btree

Columns: export_run_id

idx_expense_claim_status
btree

Columns: status

idx_expense_claim_org_submitted_at
btree

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
on_create

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_create

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
on_create

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
on_create

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
on_update

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
on_update

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
on_update

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
on_update

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
always

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
on_create

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
on_create

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
on_update

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.

Storage Configuration

Storage Type
primary_table
Location
main_db
Partitioning
No Partitioning
Retention
Permanent Storage

Entity Relationships

activity
incoming one_to_one

A travel-eligible activity may have exactly one linked expense claim covering all expense lines for that activity

optional
claim_event
outgoing one_to_many

Every state transition of an expense claim generates an immutable audit event forming a complete approval history timeline

optional cascade delete
export_run
outgoing references

An approved expense claim is stamped with the export_run_id when it is first included in an accounting system export, preventing double-export

optional
mileage_claim
outgoing one_to_one

When a claim includes a kilometers-driven expense type, exactly one mileage_claim record stores the distance and calculated reimbursement

optional cascade delete
receipt
outgoing one_to_one

When a claim amount exceeds the configured threshold, exactly one receipt image record is linked to the expense claim for audit purposes

optional cascade delete