Skip to main content

datasynth_core/models/
payroll.rs

1//! Payroll models for the Hire-to-Retire (H2R) process.
2//!
3//! These models represent payroll runs and individual employee pay line items,
4//! supporting the full payroll cycle from draft calculation through posting.
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 a payroll run through the processing lifecycle.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
15#[serde(rename_all = "snake_case")]
16pub enum PayrollRunStatus {
17    /// Initial draft state before calculation
18    #[default]
19    Draft,
20    /// Payroll has been calculated but not yet approved
21    Calculated,
22    /// Payroll approved for posting
23    Approved,
24    /// Payroll posted to GL
25    Posted,
26    /// Payroll run has been reversed
27    Reversed,
28}
29
30/// A payroll run representing a complete pay cycle for a company.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct PayrollRun {
33    /// Company code
34    pub company_code: String,
35    /// Unique payroll run identifier
36    pub payroll_id: String,
37    /// Start of the pay period
38    pub pay_period_start: NaiveDate,
39    /// End of the pay period
40    pub pay_period_end: NaiveDate,
41    /// Date the payroll was run/processed
42    pub run_date: NaiveDate,
43    /// Current status of the payroll run
44    pub status: PayrollRunStatus,
45    /// Total gross pay across all employees
46    #[serde(with = "rust_decimal::serde::str")]
47    pub total_gross: Decimal,
48    /// Total deductions across all employees
49    #[serde(with = "rust_decimal::serde::str")]
50    pub total_deductions: Decimal,
51    /// Total net pay across all employees
52    #[serde(with = "rust_decimal::serde::str")]
53    pub total_net: Decimal,
54    /// Total employer cost (gross + employer-side taxes/benefits)
55    #[serde(with = "rust_decimal::serde::str")]
56    pub total_employer_cost: Decimal,
57    /// Number of employees included in this run
58    pub employee_count: u32,
59    /// Currency code (e.g., USD, EUR)
60    pub currency: String,
61    /// User who posted the payroll
62    pub posted_by: Option<String>,
63    /// User who approved the payroll
64    pub approved_by: Option<String>,
65}
66
67/// An individual employee's payroll line item within a payroll run.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct PayrollLineItem {
70    /// Reference to the parent payroll run
71    pub payroll_id: String,
72    /// Employee identifier
73    pub employee_id: String,
74    /// Unique line item identifier
75    pub line_id: String,
76    /// Total gross pay for this employee
77    #[serde(with = "rust_decimal::serde::str")]
78    pub gross_pay: Decimal,
79    /// Base salary component
80    #[serde(with = "rust_decimal::serde::str")]
81    pub base_salary: Decimal,
82    /// Overtime pay component
83    #[serde(with = "rust_decimal::serde::str")]
84    pub overtime_pay: Decimal,
85    /// Bonus component
86    #[serde(with = "rust_decimal::serde::str")]
87    pub bonus: Decimal,
88    /// Federal/state tax withholding
89    #[serde(with = "rust_decimal::serde::str")]
90    pub tax_withholding: Decimal,
91    /// Social security / FICA deduction
92    #[serde(with = "rust_decimal::serde::str")]
93    pub social_security: Decimal,
94    /// Health insurance deduction
95    #[serde(with = "rust_decimal::serde::str")]
96    pub health_insurance: Decimal,
97    /// Retirement plan contribution (employee side)
98    #[serde(with = "rust_decimal::serde::str")]
99    pub retirement_contribution: Decimal,
100    /// Other deductions (garnishments, voluntary deductions, etc.)
101    #[serde(with = "rust_decimal::serde::str")]
102    pub other_deductions: Decimal,
103    /// Net pay after all deductions
104    #[serde(with = "rust_decimal::serde::str")]
105    pub net_pay: Decimal,
106    /// Regular hours worked in the period
107    pub hours_worked: f64,
108    /// Overtime hours worked in the period
109    pub overtime_hours: f64,
110    /// Date payment is issued
111    pub pay_date: NaiveDate,
112    /// Cost center allocation
113    pub cost_center: Option<String>,
114    /// Department allocation
115    pub department: Option<String>,
116
117    // -- Country-pack deduction labels ----------------------------------------
118    // When a country pack is available these carry the localized deduction names
119    // (e.g. "Lohnsteuer" instead of "Federal Income Tax"). When no pack is set
120    // the fields are `None` and the implicit US-centric names apply.
121    /// Localized label for the tax withholding deduction.
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub tax_withholding_label: Option<String>,
124    /// Localized label for the social security / FICA deduction.
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub social_security_label: Option<String>,
127    /// Localized label for the health insurance deduction.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub health_insurance_label: Option<String>,
130    /// Localized label for the retirement / pension contribution.
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub retirement_contribution_label: Option<String>,
133    /// Localized label(s) for employer contributions (semicolon-separated).
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub employer_contribution_label: Option<String>,
136}
137
138impl ToNodeProperties for PayrollRun {
139    fn node_type_name(&self) -> &'static str {
140        "payroll_run"
141    }
142    fn node_type_code(&self) -> u16 {
143        330
144    }
145    fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
146        let mut p = HashMap::new();
147        p.insert(
148            "entityCode".into(),
149            GraphPropertyValue::String(self.company_code.clone()),
150        );
151        p.insert(
152            "payrollId".into(),
153            GraphPropertyValue::String(self.payroll_id.clone()),
154        );
155        p.insert(
156            "periodStart".into(),
157            GraphPropertyValue::Date(self.pay_period_start),
158        );
159        p.insert(
160            "periodEnd".into(),
161            GraphPropertyValue::Date(self.pay_period_end),
162        );
163        p.insert("runDate".into(), GraphPropertyValue::Date(self.run_date));
164        p.insert(
165            "status".into(),
166            GraphPropertyValue::String(format!("{:?}", self.status)),
167        );
168        p.insert(
169            "employeeCount".into(),
170            GraphPropertyValue::Int(self.employee_count as i64),
171        );
172        p.insert(
173            "grossPay".into(),
174            GraphPropertyValue::Decimal(self.total_gross),
175        );
176        p.insert("netPay".into(), GraphPropertyValue::Decimal(self.total_net));
177        p.insert(
178            "taxWithheld".into(),
179            GraphPropertyValue::Decimal(self.total_deductions),
180        );
181        p.insert(
182            "currency".into(),
183            GraphPropertyValue::String(self.currency.clone()),
184        );
185        p.insert(
186            "isApproved".into(),
187            GraphPropertyValue::Bool(matches!(
188                self.status,
189                PayrollRunStatus::Approved | PayrollRunStatus::Posted
190            )),
191        );
192        p
193    }
194}
195
196// ---------------------------------------------------------------------------
197// Benefit enrollment models
198// ---------------------------------------------------------------------------
199
200/// Type of benefit plan.
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
202#[serde(rename_all = "snake_case")]
203pub enum BenefitPlanType {
204    #[default]
205    Health,
206    Dental,
207    Vision,
208    Retirement401k,
209    StockPurchase,
210    LifeInsurance,
211    Disability,
212}
213
214/// Status of a benefit enrollment.
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
216#[serde(rename_all = "snake_case")]
217pub enum BenefitStatus {
218    #[default]
219    Active,
220    Pending,
221    Terminated,
222    OnLeave,
223}
224
225/// An employee's enrollment in a benefit plan.
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct BenefitEnrollment {
228    /// Unique enrollment identifier
229    pub id: String,
230    /// Company / entity code
231    pub entity_code: String,
232    /// Employee ID
233    pub employee_id: String,
234    /// Employee display name (denormalized)
235    pub employee_name: String,
236    /// Benefit plan type
237    pub plan_type: BenefitPlanType,
238    /// Plan name (e.g. "Blue Cross PPO")
239    pub plan_name: String,
240    /// Date enrollment was submitted
241    pub enrollment_date: NaiveDate,
242    /// Coverage effective date
243    pub effective_date: NaiveDate,
244    /// Fiscal period (e.g. "2024-06")
245    pub period: String,
246    /// Employee contribution amount per period
247    #[serde(with = "rust_decimal::serde::str")]
248    pub employee_contribution: Decimal,
249    /// Employer contribution amount per period
250    #[serde(with = "rust_decimal::serde::str")]
251    pub employer_contribution: Decimal,
252    /// Currency code
253    pub currency: String,
254    /// Current enrollment status
255    pub status: BenefitStatus,
256    /// Whether enrollment is currently active
257    pub is_active: bool,
258}
259
260impl BenefitEnrollment {
261    /// Create a new benefit enrollment.
262    #[allow(clippy::too_many_arguments)]
263    pub fn new(
264        id: impl Into<String>,
265        entity_code: impl Into<String>,
266        employee_id: impl Into<String>,
267        employee_name: impl Into<String>,
268        plan_type: BenefitPlanType,
269        plan_name: impl Into<String>,
270        enrollment_date: NaiveDate,
271        effective_date: NaiveDate,
272        period: impl Into<String>,
273        employee_contribution: Decimal,
274        employer_contribution: Decimal,
275        currency: impl Into<String>,
276        status: BenefitStatus,
277        is_active: bool,
278    ) -> Self {
279        Self {
280            id: id.into(),
281            entity_code: entity_code.into(),
282            employee_id: employee_id.into(),
283            employee_name: employee_name.into(),
284            plan_type,
285            plan_name: plan_name.into(),
286            enrollment_date,
287            effective_date,
288            period: period.into(),
289            employee_contribution,
290            employer_contribution,
291            currency: currency.into(),
292            status,
293            is_active,
294        }
295    }
296}
297
298impl ToNodeProperties for BenefitEnrollment {
299    fn node_type_name(&self) -> &'static str {
300        "benefit_enrollment"
301    }
302    fn node_type_code(&self) -> u16 {
303        333
304    }
305    fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
306        let mut p = HashMap::new();
307        p.insert(
308            "entityCode".into(),
309            GraphPropertyValue::String(self.entity_code.clone()),
310        );
311        p.insert(
312            "employeeId".into(),
313            GraphPropertyValue::String(self.employee_id.clone()),
314        );
315        p.insert(
316            "employeeName".into(),
317            GraphPropertyValue::String(self.employee_name.clone()),
318        );
319        p.insert(
320            "planType".into(),
321            GraphPropertyValue::String(format!("{:?}", self.plan_type)),
322        );
323        p.insert(
324            "planName".into(),
325            GraphPropertyValue::String(self.plan_name.clone()),
326        );
327        p.insert(
328            "enrollmentDate".into(),
329            GraphPropertyValue::Date(self.enrollment_date),
330        );
331        p.insert(
332            "effectiveDate".into(),
333            GraphPropertyValue::Date(self.effective_date),
334        );
335        p.insert(
336            "period".into(),
337            GraphPropertyValue::String(self.period.clone()),
338        );
339        p.insert(
340            "employeeContribution".into(),
341            GraphPropertyValue::Decimal(self.employee_contribution),
342        );
343        p.insert(
344            "employerContribution".into(),
345            GraphPropertyValue::Decimal(self.employer_contribution),
346        );
347        p.insert(
348            "currency".into(),
349            GraphPropertyValue::String(self.currency.clone()),
350        );
351        p.insert(
352            "status".into(),
353            GraphPropertyValue::String(format!("{:?}", self.status)),
354        );
355        p.insert("isActive".into(), GraphPropertyValue::Bool(self.is_active));
356        p
357    }
358}