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