Mileage Claim
Data Entity
Description
Stores kilometer distance and route details for a mileage-based expense claim line. Linked one-to-one to an expense claim, capturing origin, optional destination, distance in kilometers, per-km rate at time of submission, and calculated reimbursement amount. Auto-approval is applied when distance falls below the organization's configured threshold.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Primary key. Generated on the client before insert for optimistic UI updates. | PKrequiredunique |
expense_claim_id |
uuid |
Foreign key to the parent expense_claim record. Enforces the one-to-one relationship — each expense claim may have at most one mileage_claim row. | requiredunique |
organization_id |
uuid |
Foreign key to the organization. Injected at creation time for Supabase RLS tenant isolation. Must match the organization_id on the parent expense_claim. | required |
submitter_id |
uuid |
User ID of the peer mentor who submitted the claim. Used for ownership checks, history queries, and coordinator-scope validation. | required |
origin |
string |
Free-text description of the travel starting point (e.g. home address, town name). Required field; rendered with a privacy-aware placeholder in the UI to discourage overly precise personal addresses. | required |
destination |
string |
Free-text description of the travel end point. Optional — peer mentors may omit for privacy. Rendered with an explicit 'optional' label and privacy hint in the route input widget. | - |
distance_km |
decimal |
Distance driven in kilometers. Must be a positive value with up to two decimal places. Pre-filled from the last-used distance cache for the submitting user; always editable. | required |
rate_per_km |
decimal |
The organization's per-kilometer reimbursement rate in NOK at the exact moment of submission. Captured as a snapshot so historical claims remain accurate if the rate changes later. | required |
reimbursement_amount |
decimal |
Gross reimbursement amount in NOK, computed as distance_km × rate_per_km and rounded per Norwegian reimbursement standards. Stored after server-side calculation to prevent client tampering. | required |
approval_threshold_km |
decimal |
Snapshot of the organization's auto-approval distance threshold (default 50 km) at submission time. Stored so threshold changes do not retroactively alter already-submitted claims. | required |
status |
enum |
Approval lifecycle state of the mileage claim. Set to 'auto_approved' immediately on submission when distance_km < approval_threshold_km; otherwise 'pending_review' until coordinator action. | required |
auto_approved |
boolean |
True when the claim was approved automatically without coordinator intervention because distance_km was below approval_threshold_km. Used for analytics and audit reporting. | required |
submitted_at |
datetime |
UTC timestamp when the claim was first submitted. Set server-side on insert; immutable thereafter. | required |
reviewed_at |
datetime |
UTC timestamp when a coordinator approved or rejected the claim. Null for auto-approved and pending claims. | - |
reviewed_by |
uuid |
User ID of the coordinator who performed the manual approval or rejection. Null for auto-approved claims. | - |
review_comment |
text |
Optional coordinator comment attached to an approval or rejection decision. Required when status transitions to 'rejected'; optional for 'approved'. | - |
created_at |
datetime |
Row creation timestamp, set server-side. Equals submitted_at in practice but kept separate for database audit hygiene. | required |
updated_at |
datetime |
Row last-update timestamp, maintained by a Supabase database trigger on every write. | required |
Database Indexes
idx_mileage_claim_expense_claim_id
Columns: expense_claim_id
idx_mileage_claim_submitter_id
Columns: submitter_id
idx_mileage_claim_organization_id_status
Columns: organization_id, status
idx_mileage_claim_organization_id_submitted_at
Columns: organization_id, submitted_at
idx_mileage_claim_submitter_id_submitted_at
Columns: submitter_id, submitted_at
Validation Rules
distance_km_positive
error
Validation failed
distance_km_format
error
Validation failed
origin_non_empty
error
Validation failed
origin_max_length
error
Validation failed
destination_max_length
error
Validation failed
rate_per_km_positive
error
Validation failed
expense_claim_exists
error
Validation failed
no_duplicate_mileage_claim
error
Validation failed
reimbursement_amount_consistency
error
Validation failed
valid_status_transition
error
Validation failed
reviewer_must_be_coordinator
error
Validation failed
Business Rules
one_to_one_expense_claim
Each expense_claim may have at most one mileage_claim. The expense_claim_id column carries a UNIQUE constraint enforced at the database level. Attempts to insert a second mileage_claim for the same expense_claim_id must be rejected with a domain error.
auto_approval_below_threshold
When distance_km is strictly less than the organization's configured auto-approval threshold (fetched via org-rate-config-repository and snapshotted into approval_threshold_km), the claim status is set to 'auto_approved' and auto_approved is set to true without requiring coordinator action.
manual_review_above_threshold
When distance_km is greater than or equal to approval_threshold_km, status is set to 'pending_review' and the claim enters the coordinator attestation queue. The claim must not be counted as approved for reimbursement until a coordinator explicitly approves it.
rate_snapshot_at_submission
rate_per_km must be fetched from the organization configuration at the moment of submission and stored immutably on the row. Subsequent rate changes must not alter previously submitted claims. The calculated reimbursement_amount is derived from this snapshot.
reimbursement_amount_derived
reimbursement_amount must equal distance_km × rate_per_km, rounded per Norwegian rounding rules (two decimal places, half-up). Client-side display via realtime-reimbursement-display is for UX only; the authoritative value is computed server-side by mileage-calculation-service before persistence.
immutable_after_final_decision
Once status transitions to 'approved', 'auto_approved', or 'rejected', the distance_km, rate_per_km, reimbursement_amount, origin, and destination fields must not be modified. Only review metadata (reviewed_at, reviewed_by, review_comment) may be added during the review transition itself.
review_comment_required_on_rejection
When a coordinator transitions status to 'rejected', review_comment must be non-null and non-empty. This is enforced at the service layer before the repository write to ensure the submitter always receives an explanation.
organization_scope_isolation
All reads and writes must be scoped to the authenticated user's organization_id via Supabase RLS policies. A peer mentor may only read their own mileage claims; a coordinator may only read claims within their assigned chapters; org admins may read all claims for their organization.
last_used_distance_update
After a successful claim submission, distance_km must be persisted to the local distance cache via distance-prefill-service so the next mileage entry is pre-filled with this value. Cache must be cleared on logout.
CRUD Operations
Storage Configuration
Entity Relationships
When a claim includes a kilometers-driven expense type, exactly one mileage_claim record stores the distance and calculated reimbursement