Reimbursement
Data Entity
Description
A bundled reimbursement claim grouping one or more expenses submitted by a peer mentor for coordinator or automated approval. Tracks the overall claim status through the approval lifecycle: pending, auto-approved, manually approved, or rejected. Upon approval, forwards data to external accounting systems.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Globally unique identifier for the reimbursement claim, used as the primary key and in all cross-entity references. | PKrequiredunique |
user_id |
uuid |
Foreign key referencing the users table — the peer mentor who submitted this reimbursement claim. Used to scope reads and enforce ownership. | required |
organization_id |
uuid |
Foreign key referencing the organizations table. Used for multi-tenancy scoping of all queries — ensures data cannot cross organizational boundaries. | required |
local_association_id |
uuid |
Foreign key referencing the local_associations table. Used for coordinator-scoped approval queue queries — coordinators may only see claims from their own local association. | required |
status |
enum |
Current lifecycle state of the reimbursement claim. Drives UI badge rendering, approval queue visibility, and accounting forwarding logic. | required |
total_amount_nok |
decimal |
Computed sum of all linked expense amounts in Norwegian kroner. Denormalized for fast threshold evaluation and queue list display without requiring a JOIN-aggregate on each read. | required |
total_distance_km |
decimal |
Computed sum of all linked mileage expense distances in kilometres. Denormalized for threshold-based auto-approval evaluation without recomputing on each approval check. | - |
submitted_at |
datetime |
Timestamp when the peer mentor submitted the claim for review. Set once on submission and immutable thereafter. Used for queue sorting and time-window reporting. | required |
approved_at |
datetime |
Timestamp when the claim reached a terminal approved state (either auto_approved or manually_approved). Null while the claim is pending or under clarification. | - |
rejected_at |
datetime |
Timestamp when the claim was rejected by a coordinator. Null for all non-rejected states. | - |
approved_by |
uuid |
Foreign key referencing the users table — the coordinator who issued the manual approval or rejection decision. Null for auto-approved claims and pending claims. | - |
rejection_reason |
text |
Optional freeform text provided by the coordinator explaining why the claim was rejected or what clarification is needed. Required when status transitions to 'rejected'. | - |
accounting_forwarded_at |
datetime |
Timestamp when the approved claim was successfully forwarded to the external accounting system (Xledger for Blindeforbundet, Dynamics portal for HLF). Null until forwarding completes. Used for idempotency — prevents double-forwarding on retry. | - |
accounting_reference |
string |
External reference ID returned by the accounting system (Xledger transaction ID or Dynamics record ID) after successful forwarding. Used for reconciliation and audit trail. | - |
coordinator_notes |
text |
Optional notes added by the coordinator during the approval or clarification workflow. Stored on the reimbursement for display in the claim detail screen. | - |
auto_approval_rules_applied |
json |
JSONB snapshot of the threshold configuration values that were evaluated at auto-approval time (km threshold, amount threshold, organization_id). Preserved for audit trail even if thresholds change later. | - |
created_at |
datetime |
Record creation timestamp, set by the database on INSERT. Distinct from submitted_at to allow draft states if introduced in future versions. | required |
updated_at |
datetime |
Timestamp of the last status change or field update. Maintained by the application layer on every UPDATE operation. | required |
Database Indexes
idx_reimbursement_user_id
Columns: user_id
idx_reimbursement_organization_id
Columns: organization_id
idx_reimbursement_local_association_id
Columns: local_association_id
idx_reimbursement_status
Columns: status
idx_reimbursement_submitted_at
Columns: submitted_at
idx_reimbursement_org_status
Columns: organization_id, status
idx_reimbursement_assoc_status_submitted
Columns: local_association_id, status, submitted_at
idx_reimbursement_user_submitted
Columns: user_id, submitted_at
idx_reimbursement_accounting_forwarded
Columns: accounting_forwarded_at
Validation Rules
total_amount_non_negative
error
Validation failed
total_distance_non_negative
error
Validation failed
submitted_at_not_future
error
Validation failed
user_must_be_active_peer_mentor
error
Validation failed
organization_and_association_consistency
error
Validation failed
coordinator_notes_length
error
Validation failed
accounting_reference_format
error
Validation failed
auto_approval_rules_applied_schema
error
Validation failed
Business Rules
minimum_one_expense_required
A reimbursement claim must reference at least one linked expense record at the time of submission. An empty claim cannot be submitted and will be rejected at the service layer before persisting.
auto_approval_threshold_evaluation
On submission, the Reimbursement Approval Service evaluates total_distance_km against the organization-specific km threshold (default: 50 km) and total_amount_nok against the monetary threshold. Claims meeting both conditions (distance below threshold AND no individual expense over the amount threshold) are auto-approved immediately and status is set to 'auto_approved'. The threshold configuration at decision time is snapshotted into auto_approval_rules_applied for immutable audit trail.
immutable_after_terminal_state
Once a reimbursement claim reaches a terminal state (auto_approved, manually_approved, rejected), its status, total_amount_nok, total_distance_km, and expense linkages cannot be modified. Any attempt to update a terminal-state claim is rejected with a 409 Conflict response.
coordinator_association_scope
A coordinator may only read and act on reimbursement claims belonging to peer mentors within their own local_association_id. The reimbursement-repository and reimbursement-admin-repository enforce this at the SQL query level via JOIN to user_organization_roles, not at the application layer.
accounting_forwarding_on_approval
When a claim transitions to 'auto_approved' or 'manually_approved', the Accounting Integration Adapter is invoked to forward the claim to the correct external accounting system for the organization (Xledger for Blindeforbundet, Dynamics portal for HLF). The adapter uses exponential backoff retry. accounting_forwarded_at remains null until forwarding succeeds, and idempotency is enforced by checking this field before attempting forwarding.
rejection_reason_required
When a coordinator transitions a claim to 'rejected' or 'clarification_requested', a non-empty rejection_reason must be provided. This is enforced at the service layer before the status update is persisted.
audit_log_on_every_decision
Every status transition — whether automatic or manual — must produce an immutable audit log entry via the Approval Audit Logger, capturing the actor identity, decision, timestamp, and rule evaluation details (for auto-approvals). No decision may be persisted without a corresponding audit entry in the same transaction.
valid_status_transition
Status transitions must follow the defined lifecycle DAG: pending → auto_approved | manually_approved | rejected | clarification_requested. clarification_requested → manually_approved | rejected. Terminal states (auto_approved, manually_approved, rejected) have no outgoing transitions. Any attempt to set an invalid transition is rejected.
peer_mentor_ownership
Only the peer mentor identified by user_id may create a reimbursement claim, and only their own claims are visible in their personal expense history. Coordinators may view claims of peer mentors in their association; they cannot create claims on behalf of others.
CRUD Operations
Storage Configuration
Entity Relationships
A reimbursement claim bundles multiple individual expense line items for approval
A reimbursement claim can have multiple approval records (e.g., initial auto-approval followed by manual override)
A user (peer mentor) submits many reimbursement claims over their active period