Expense Approval
Data Entity
Description
Records the approval or rejection decision for a submitted expense claim. Each expense has exactly one approval record that tracks the decision lifecycle from pending review through auto-approval or manual coordinator/admin action, capturing the reviewer identity, timestamp, decision comment, and rejection reason. Triggers reimbursement creation on approval and feeds into KPI dashboards and Bufdir financial reporting.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Primary key. Unique identifier for the approval record. | PKrequiredunique |
expense_id |
uuid |
Foreign key to expenses. One-to-one relationship — each expense has at most one approval record. Used to look up the full expense context during review. | requiredunique |
organization_id |
uuid |
Foreign key to organizations. Denormalized for tenant-scoped queries without requiring a join through expenses. All queries are scoped to this field. | required |
status |
enum |
Current decision state of the approval. 'pending' is the initial state on expense submission. 'auto_approved' is set by the auto-approval rule engine without human review. 'approved' and 'rejected' are set by a coordinator or org admin. | required |
reviewed_by |
uuid |
Foreign key to users. The coordinator or org admin who made the decision. Null when status is 'pending' or 'auto_approved'. | - |
reviewed_at |
datetime |
Timestamp when the approval decision was made. Null when status is 'pending'. Set to the system clock at the moment of the approve/reject action. | - |
decision_comment |
text |
Optional free-text comment from the reviewer. Can be used for both approvals (e.g., 'Approved with minor note') and rejections (supplementary to rejection_reason). | - |
rejection_reason |
text |
Required when status is 'rejected'. Structured explanation for why the expense was rejected, displayed to the peer mentor. Not applicable for approvals or auto-approvals. | - |
auto_approved |
boolean |
True when the expense was approved automatically by the auto-approval rule engine without human review. False for all manually reviewed decisions. | required |
auto_approval_rule_id |
uuid |
Foreign key to auto_approval_rules (if that table exists) or stored as a reference string. Records which auto-approval rule triggered the automatic decision. Null for manually reviewed approvals. | - |
version |
integer |
Optimistic lock version counter. Incremented on every status update. Concurrent approval attempts compare client-held version against this field and fail if they do not match, preventing double-approval race conditions. | required |
created_at |
datetime |
Timestamp when the approval record was created. Set automatically on expense submission. Serves as the start of the approval lifecycle for reporting. | required |
updated_at |
datetime |
Timestamp of the last status change. Updated on every approval or rejection action. Used to compute how long expenses have been in the pending queue. | required |
Database Indexes
idx_expense_approval_expense_id
Columns: expense_id
idx_expense_approval_organization_status
Columns: organization_id, status
idx_expense_approval_organization_created_at
Columns: organization_id, created_at
idx_expense_approval_reviewed_by
Columns: reviewed_by
idx_expense_approval_status
Columns: status
Validation Rules
expense_id_must_exist
error
Validation failed
rejection_reason_required_on_reject
error
Validation failed
reviewed_by_must_be_valid_user
error
Validation failed
reviewed_at_required_with_reviewer
error
Validation failed
status_transition_validity
error
Validation failed
version_must_match_on_update
error
Validation failed
organization_id_matches_expense
error
Validation failed
Business Rules
one_approval_per_expense
Each expense may have exactly one approval record. A second INSERT for the same expense_id must be rejected at the database level via a unique constraint. The expense-approval-service must check for an existing record before creating a new one.
only_authorized_roles_can_review
Only users with the Coordinator or Organization Admin role may manually approve or reject an expense. Peer Mentors cannot act on expense_approvals. Global Admins may view but not approve org-level expenses by default.
tenant_isolation
All reads and writes must be scoped to the organization_id of the authenticated user. A coordinator from org A must never be able to read or modify approvals belonging to org B.
approval_triggers_reimbursement
When status transitions to 'approved' or 'auto_approved', the expense-approval-service must create a linked reimbursement record in the reimbursements table within the same database transaction.
finalized_decision_is_immutable
Once status is 'approved', 'auto_approved', or 'rejected', no further status transitions are permitted. Only system administrators performing data corrections may override this rule, and all such overrides must be audit-logged.
audit_all_decisions
Every status transition (pending→approved, pending→rejected, pending→auto_approved) must produce an immutable audit log entry via the audit-log-service within the same transaction, capturing the actor, action, expense_id, and timestamp.
auto_approval_sets_no_reviewer
When the auto-approval rule engine approves an expense, reviewed_by must remain null and auto_approved must be true. The auto_approval_rule_id must be populated to record which rule triggered the decision.
optimistic_locking_on_concurrent_review
If two reviewers attempt to approve or reject the same expense simultaneously, the second write must fail with a conflict error. The client must re-fetch the current state and present the already-resolved decision to the user.