Skip to main content

backbone_payroll/domain/entity/
payroll_entry.rs

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