Skip to main content

backbone_payroll/domain/entity/
salary_slip.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5use rust_decimal::Decimal;
6use super::AuditMetadata;
7
8/// Strongly-typed ID for SalarySlip
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(transparent)]
11pub struct SalarySlipId(pub Uuid);
12
13impl SalarySlipId {
14    pub fn new(id: Uuid) -> Self { Self(id) }
15    pub fn generate() -> Self { Self(Uuid::new_v4()) }
16    pub fn into_inner(self) -> Uuid { self.0 }
17}
18
19impl std::fmt::Display for SalarySlipId {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        write!(f, "{}", self.0)
22    }
23}
24
25impl std::str::FromStr for SalarySlipId {
26    type Err = uuid::Error;
27    fn from_str(s: &str) -> Result<Self, Self::Err> {
28        Ok(Self(Uuid::parse_str(s)?))
29    }
30}
31
32impl From<Uuid> for SalarySlipId {
33    fn from(id: Uuid) -> Self { Self(id) }
34}
35
36impl From<SalarySlipId> for Uuid {
37    fn from(id: SalarySlipId) -> Self { id.0 }
38}
39
40impl AsRef<Uuid> for SalarySlipId {
41    fn as_ref(&self) -> &Uuid { &self.0 }
42}
43
44impl std::ops::Deref for SalarySlipId {
45    type Target = Uuid;
46    fn deref(&self) -> &Self::Target { &self.0 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
50pub struct SalarySlip {
51    pub id: Uuid,
52    pub payroll_entry_id: Uuid,
53    pub employee_id: Uuid,
54    pub structure_id: Option<Uuid>,
55    pub working_days: Decimal,
56    pub unpaid_days: Decimal,
57    pub gross_pay: Decimal,
58    pub total_deductions: Decimal,
59    pub net_pay: Decimal,
60    pub overtime_hours: Option<Decimal>,
61    pub tax_method: Option<String>,
62    #[serde(default)]
63    #[sqlx(json)]
64    pub metadata: AuditMetadata,
65}
66
67impl SalarySlip {
68    /// Create a builder for SalarySlip
69    pub fn builder() -> SalarySlipBuilder {
70        <SalarySlipBuilder as Default>::default()
71    }
72
73    /// Create a new SalarySlip with required fields
74    pub fn new(payroll_entry_id: Uuid, employee_id: Uuid, working_days: Decimal, unpaid_days: Decimal, gross_pay: Decimal, total_deductions: Decimal, net_pay: Decimal) -> Self {
75        Self {
76            id: Uuid::new_v4(),
77            payroll_entry_id,
78            employee_id,
79            structure_id: None,
80            working_days,
81            unpaid_days,
82            gross_pay,
83            total_deductions,
84            net_pay,
85            overtime_hours: None,
86            tax_method: None,
87            metadata: AuditMetadata::default(),
88        }
89    }
90
91    /// Get the entity's unique identifier
92    pub fn id(&self) -> &Uuid {
93        &self.id
94    }
95
96    /// Get a strongly-typed ID for this entity
97    pub fn typed_id(&self) -> SalarySlipId {
98        SalarySlipId(self.id)
99    }
100
101    /// Get when this entity was created
102    pub fn created_at(&self) -> Option<&DateTime<Utc>> {
103        self.metadata.created_at.as_ref()
104    }
105
106    /// Get when this entity was last updated
107    pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
108        self.metadata.updated_at.as_ref()
109    }
110
111    /// Check if this entity is soft deleted
112    pub fn is_deleted(&self) -> bool {
113        self.metadata.deleted_at.is_some()
114    }
115
116    /// Check if this entity is active (not deleted)
117    pub fn is_active(&self) -> bool {
118        self.metadata.deleted_at.is_none()
119    }
120
121    /// Get when this entity was deleted
122    pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
123        self.metadata.deleted_at.as_ref()
124    }
125
126    /// Get who created this entity
127    pub fn created_by(&self) -> Option<&Uuid> {
128        self.metadata.created_by.as_ref()
129    }
130
131    /// Get who last updated this entity
132    pub fn updated_by(&self) -> Option<&Uuid> {
133        self.metadata.updated_by.as_ref()
134    }
135
136    /// Get who deleted this entity
137    pub fn deleted_by(&self) -> Option<&Uuid> {
138        self.metadata.deleted_by.as_ref()
139    }
140
141
142    // ==========================================================
143    // Fluent Setters (with_* for optional fields)
144    // ==========================================================
145
146    /// Set the structure_id field (chainable)
147    pub fn with_structure_id(mut self, value: Uuid) -> Self {
148        self.structure_id = Some(value);
149        self
150    }
151
152    /// Set the overtime_hours field (chainable)
153    pub fn with_overtime_hours(mut self, value: Decimal) -> Self {
154        self.overtime_hours = Some(value);
155        self
156    }
157
158    /// Set the tax_method field (chainable)
159    pub fn with_tax_method(mut self, value: String) -> Self {
160        self.tax_method = Some(value);
161        self
162    }
163
164    // ==========================================================
165    // Partial Update
166    // ==========================================================
167
168    /// Apply partial updates from a map of field name to JSON value
169    pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
170        for (key, value) in fields {
171            match key.as_str() {
172                "payroll_entry_id" => {
173                    if let Ok(v) = serde_json::from_value(value) { self.payroll_entry_id = v; }
174                }
175                "employee_id" => {
176                    if let Ok(v) = serde_json::from_value(value) { self.employee_id = v; }
177                }
178                "structure_id" => {
179                    if let Ok(v) = serde_json::from_value(value) { self.structure_id = v; }
180                }
181                "working_days" => {
182                    if let Ok(v) = serde_json::from_value(value) { self.working_days = v; }
183                }
184                "unpaid_days" => {
185                    if let Ok(v) = serde_json::from_value(value) { self.unpaid_days = v; }
186                }
187                "gross_pay" => {
188                    if let Ok(v) = serde_json::from_value(value) { self.gross_pay = v; }
189                }
190                "total_deductions" => {
191                    if let Ok(v) = serde_json::from_value(value) { self.total_deductions = v; }
192                }
193                "net_pay" => {
194                    if let Ok(v) = serde_json::from_value(value) { self.net_pay = v; }
195                }
196                "overtime_hours" => {
197                    if let Ok(v) = serde_json::from_value(value) { self.overtime_hours = v; }
198                }
199                "tax_method" => {
200                    if let Ok(v) = serde_json::from_value(value) { self.tax_method = v; }
201                }
202                _ => {} // ignore unknown fields
203            }
204        }
205    }
206
207    // <<< CUSTOM METHODS START >>>
208    // <<< CUSTOM METHODS END >>>
209}
210
211impl super::Entity for SalarySlip {
212    type Id = Uuid;
213
214    fn entity_id(&self) -> &Self::Id {
215        &self.id
216    }
217
218    fn entity_type() -> &'static str {
219        "SalarySlip"
220    }
221}
222
223impl backbone_core::PersistentEntity for SalarySlip {
224    fn entity_id(&self) -> String {
225        self.id.to_string()
226    }
227    fn set_entity_id(&mut self, id: String) {
228        if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
229            self.id = uuid;
230        }
231    }
232    fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
233        self.metadata.created_at
234    }
235    fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
236        self.metadata.created_at = Some(ts);
237    }
238    fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
239        self.metadata.updated_at
240    }
241    fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
242        self.metadata.updated_at = Some(ts);
243    }
244    fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
245        self.metadata.deleted_at
246    }
247    fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
248        self.metadata.deleted_at = ts;
249    }
250}
251
252impl backbone_orm::EntityRepoMeta for SalarySlip {
253    fn column_types() -> std::collections::HashMap<String, String> {
254        let mut m = std::collections::HashMap::new();
255        m.insert("id".to_string(), "uuid".to_string());
256        m.insert("payroll_entry_id".to_string(), "uuid".to_string());
257        m.insert("employee_id".to_string(), "uuid".to_string());
258        m.insert("structure_id".to_string(), "uuid".to_string());
259        m
260    }
261    fn search_fields() -> &'static [&'static str] {
262        &[]
263    }
264}
265
266/// Builder for SalarySlip entity
267///
268/// Provides a fluent API for constructing SalarySlip instances.
269/// System fields (id, metadata, timestamps) are auto-initialized.
270#[derive(Debug, Clone, Default)]
271pub struct SalarySlipBuilder {
272    payroll_entry_id: Option<Uuid>,
273    employee_id: Option<Uuid>,
274    structure_id: Option<Uuid>,
275    working_days: Option<Decimal>,
276    unpaid_days: Option<Decimal>,
277    gross_pay: Option<Decimal>,
278    total_deductions: Option<Decimal>,
279    net_pay: Option<Decimal>,
280    overtime_hours: Option<Decimal>,
281    tax_method: Option<String>,
282}
283
284impl SalarySlipBuilder {
285    /// Set the payroll_entry_id field (required)
286    pub fn payroll_entry_id(mut self, value: Uuid) -> Self {
287        self.payroll_entry_id = Some(value);
288        self
289    }
290
291    /// Set the employee_id field (required)
292    pub fn employee_id(mut self, value: Uuid) -> Self {
293        self.employee_id = Some(value);
294        self
295    }
296
297    /// Set the structure_id field (optional)
298    pub fn structure_id(mut self, value: Uuid) -> Self {
299        self.structure_id = Some(value);
300        self
301    }
302
303    /// Set the working_days field (default: `Decimal::from(0)`)
304    pub fn working_days(mut self, value: Decimal) -> Self {
305        self.working_days = Some(value);
306        self
307    }
308
309    /// Set the unpaid_days field (default: `Decimal::from(0)`)
310    pub fn unpaid_days(mut self, value: Decimal) -> Self {
311        self.unpaid_days = Some(value);
312        self
313    }
314
315    /// Set the gross_pay field (default: `Decimal::from(0)`)
316    pub fn gross_pay(mut self, value: Decimal) -> Self {
317        self.gross_pay = Some(value);
318        self
319    }
320
321    /// Set the total_deductions field (default: `Decimal::from(0)`)
322    pub fn total_deductions(mut self, value: Decimal) -> Self {
323        self.total_deductions = Some(value);
324        self
325    }
326
327    /// Set the net_pay field (default: `Decimal::from(0)`)
328    pub fn net_pay(mut self, value: Decimal) -> Self {
329        self.net_pay = Some(value);
330        self
331    }
332
333    /// Set the overtime_hours field (optional)
334    pub fn overtime_hours(mut self, value: Decimal) -> Self {
335        self.overtime_hours = Some(value);
336        self
337    }
338
339    /// Set the tax_method field (optional)
340    pub fn tax_method(mut self, value: String) -> Self {
341        self.tax_method = Some(value);
342        self
343    }
344
345    /// Build the SalarySlip entity
346    ///
347    /// Returns Err if any required field without a default is missing.
348    pub fn build(self) -> Result<SalarySlip, String> {
349        let payroll_entry_id = self.payroll_entry_id.ok_or_else(|| "payroll_entry_id is required".to_string())?;
350        let employee_id = self.employee_id.ok_or_else(|| "employee_id is required".to_string())?;
351
352        Ok(SalarySlip {
353            id: Uuid::new_v4(),
354            payroll_entry_id,
355            employee_id,
356            structure_id: self.structure_id,
357            working_days: self.working_days.unwrap_or(Decimal::from(0)),
358            unpaid_days: self.unpaid_days.unwrap_or(Decimal::from(0)),
359            gross_pay: self.gross_pay.unwrap_or(Decimal::from(0)),
360            total_deductions: self.total_deductions.unwrap_or(Decimal::from(0)),
361            net_pay: self.net_pay.unwrap_or(Decimal::from(0)),
362            overtime_hours: self.overtime_hours,
363            tax_method: self.tax_method,
364            metadata: AuditMetadata::default(),
365        })
366    }
367}