Skip to main content

datasynth_core/models/
expense_report.rs

1//! Expense report models for the Hire-to-Retire (H2R) process.
2//!
3//! These models represent employee expense reports and their line items,
4//! supporting the full expense lifecycle from draft submission through payment.
5
6use chrono::NaiveDate;
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9use smallvec::SmallVec;
10use std::collections::HashMap;
11
12use super::graph_properties::{GraphPropertyValue, ToNodeProperties};
13
14/// Status of an expense report through the approval and payment lifecycle.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
16#[serde(rename_all = "snake_case")]
17pub enum ExpenseStatus {
18    /// Initial draft, not yet submitted
19    #[default]
20    Draft,
21    /// Submitted for approval
22    Submitted,
23    /// Approved by manager
24    Approved,
25    /// Rejected by manager
26    Rejected,
27    /// Reimbursement paid to employee
28    Paid,
29}
30
31/// Category of an expense line item.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum ExpenseCategory {
35    /// Airfare, mileage, etc.
36    Travel,
37    /// Business meals and dining
38    Meals,
39    /// Hotel and accommodation
40    Lodging,
41    /// Taxi, rideshare, rental car, parking
42    Transportation,
43    /// Office supplies and equipment
44    Office,
45    /// Client entertainment
46    Entertainment,
47    /// Professional development and training
48    Training,
49    /// Miscellaneous expenses
50    Other,
51}
52
53/// An expense report submitted by an employee for reimbursement.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ExpenseReport {
56    /// Unique expense report identifier
57    pub report_id: String,
58    /// Employee who submitted the report
59    pub employee_id: String,
60    /// Date the report was submitted
61    pub submission_date: NaiveDate,
62    /// Overall description/purpose of the expense report
63    pub description: String,
64    /// Current status of the expense report
65    pub status: ExpenseStatus,
66    /// Total amount across all line items
67    #[serde(with = "rust_decimal::serde::str")]
68    pub total_amount: Decimal,
69    /// Currency code (e.g., USD, EUR)
70    pub currency: String,
71    /// Individual expense line items
72    pub line_items: SmallVec<[ExpenseLineItem; 4]>,
73    /// Manager who approved/rejected the report
74    pub approved_by: Option<String>,
75    /// Date the report was approved
76    pub approved_date: Option<NaiveDate>,
77    /// Date the reimbursement was paid
78    pub paid_date: Option<NaiveDate>,
79    /// Cost center to charge
80    pub cost_center: Option<String>,
81    /// Department to charge
82    pub department: Option<String>,
83    /// List of policy violations flagged on this report
84    pub policy_violations: Vec<String>,
85    /// Employee display name (denormalized, DS-011)
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub employee_name: Option<String>,
88}
89
90impl ToNodeProperties for ExpenseReport {
91    fn node_type_name(&self) -> &'static str {
92        "expense_report"
93    }
94    fn node_type_code(&self) -> u16 {
95        332
96    }
97    fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
98        let mut p = HashMap::new();
99        p.insert(
100            "reportId".into(),
101            GraphPropertyValue::String(self.report_id.clone()),
102        );
103        p.insert(
104            "employeeId".into(),
105            GraphPropertyValue::String(self.employee_id.clone()),
106        );
107        if let Some(ref name) = self.employee_name {
108            p.insert(
109                "employeeName".into(),
110                GraphPropertyValue::String(name.clone()),
111            );
112        }
113        p.insert(
114            "submissionDate".into(),
115            GraphPropertyValue::Date(self.submission_date),
116        );
117        p.insert(
118            "totalAmount".into(),
119            GraphPropertyValue::Decimal(self.total_amount),
120        );
121        p.insert(
122            "currency".into(),
123            GraphPropertyValue::String(self.currency.clone()),
124        );
125        p.insert(
126            "lineCount".into(),
127            GraphPropertyValue::Int(self.line_items.len() as i64),
128        );
129        p.insert(
130            "status".into(),
131            GraphPropertyValue::String(format!("{:?}", self.status)),
132        );
133        p.insert(
134            "isApproved".into(),
135            GraphPropertyValue::Bool(matches!(
136                self.status,
137                ExpenseStatus::Approved | ExpenseStatus::Paid
138            )),
139        );
140        if let Some(ref dept) = self.department {
141            p.insert(
142                "department".into(),
143                GraphPropertyValue::String(dept.clone()),
144            );
145        }
146        p
147    }
148}
149
150/// An individual line item within an expense report.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct ExpenseLineItem {
153    /// Unique line item identifier
154    pub item_id: String,
155    /// Expense category
156    pub category: ExpenseCategory,
157    /// Date the expense was incurred
158    pub date: NaiveDate,
159    /// Amount of the expense
160    #[serde(with = "rust_decimal::serde::str")]
161    pub amount: Decimal,
162    /// Currency code (e.g., USD, EUR)
163    pub currency: String,
164    /// Description of the expense
165    pub description: String,
166    /// Whether a receipt is attached
167    pub receipt_attached: bool,
168    /// Merchant or vendor name
169    pub merchant: Option<String>,
170}