1use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14#[derive(Debug, Error, Clone, Serialize, Deserialize)]
16pub enum LaborCodeError {
17 #[error("Invalid employment contract: {0}")]
19 InvalidContract(String),
20
21 #[error("Invalid working hours: {0}")]
23 InvalidWorkingHours(String),
24
25 #[error("Invalid termination: {0}")]
27 InvalidTermination(String),
28
29 #[error("Validation failed: {0}")]
31 ValidationFailed(String),
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub enum EmploymentType {
37 Indefinite,
39 FixedTerm { end_date: chrono::NaiveDate },
41 Temporary { end_date: chrono::NaiveDate },
43 Seasonal { end_date: chrono::NaiveDate },
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct WorkingTimeRegime {
50 pub hours_per_week: u32,
52 pub days_per_week: u32,
54 pub start_time: chrono::NaiveTime,
56 pub end_time: chrono::NaiveTime,
58 pub lunch_break_minutes: u32,
60}
61
62impl WorkingTimeRegime {
63 pub fn standard() -> Self {
65 Self {
66 hours_per_week: 40,
67 days_per_week: 5,
68 start_time: chrono::NaiveTime::from_hms_opt(9, 0, 0).expect("Valid time"),
69 end_time: chrono::NaiveTime::from_hms_opt(18, 0, 0).expect("Valid time"),
70 lunch_break_minutes: 60,
71 }
72 }
73
74 pub fn reduced(hours_per_week: u32) -> Self {
76 Self {
77 hours_per_week,
78 days_per_week: 5,
79 start_time: chrono::NaiveTime::from_hms_opt(9, 0, 0).expect("Valid time"),
80 end_time: chrono::NaiveTime::from_hms_opt(18, 0, 0).expect("Valid time"),
81 lunch_break_minutes: 60,
82 }
83 }
84
85 pub fn validate(&self) -> Result<(), LaborCodeError> {
87 if self.hours_per_week > 40 {
88 return Err(LaborCodeError::InvalidWorkingHours(
89 "Normal working time cannot exceed 40 hours per week".to_string(),
90 ));
91 }
92
93 if self.days_per_week > 6 {
94 return Err(LaborCodeError::InvalidWorkingHours(
95 "Working days per week cannot exceed 6".to_string(),
96 ));
97 }
98
99 if self.lunch_break_minutes < 30 || self.lunch_break_minutes > 120 {
100 return Err(LaborCodeError::InvalidWorkingHours(
101 "Lunch break must be between 30 and 120 minutes".to_string(),
102 ));
103 }
104
105 Ok(())
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct EmploymentContract {
112 pub employer: String,
114 pub employee: String,
116 pub contract_date: chrono::NaiveDate,
118 pub start_date: chrono::NaiveDate,
120 pub employment_type: EmploymentType,
122 pub position: String,
124 pub monthly_salary: crate::common::Currency,
126 pub working_time: WorkingTimeRegime,
128 pub workplace: String,
130 pub written_form: bool,
132}
133
134impl EmploymentContract {
135 pub fn new(
137 employer: impl Into<String>,
138 employee: impl Into<String>,
139 contract_date: chrono::NaiveDate,
140 start_date: chrono::NaiveDate,
141 position: impl Into<String>,
142 monthly_salary: crate::common::Currency,
143 ) -> Self {
144 Self {
145 employer: employer.into(),
146 employee: employee.into(),
147 contract_date,
148 start_date,
149 employment_type: EmploymentType::Indefinite,
150 position: position.into(),
151 monthly_salary,
152 working_time: WorkingTimeRegime::standard(),
153 workplace: String::new(),
154 written_form: false,
155 }
156 }
157
158 pub fn with_employment_type(mut self, employment_type: EmploymentType) -> Self {
160 self.employment_type = employment_type;
161 self
162 }
163
164 pub fn with_working_time(mut self, working_time: WorkingTimeRegime) -> Self {
166 self.working_time = working_time;
167 self
168 }
169
170 pub fn with_workplace(mut self, workplace: impl Into<String>) -> Self {
172 self.workplace = workplace.into();
173 self
174 }
175
176 pub fn with_written_form(mut self, written: bool) -> Self {
178 self.written_form = written;
179 self
180 }
181
182 pub fn validate(&self) -> Result<(), LaborCodeError> {
184 if !self.written_form {
186 return Err(LaborCodeError::InvalidContract(
187 "Employment contract must be in written form".to_string(),
188 ));
189 }
190
191 if self.start_date < self.contract_date {
193 return Err(LaborCodeError::InvalidContract(
194 "Start date cannot be before contract date".to_string(),
195 ));
196 }
197
198 self.working_time.validate()?;
200
201 if !self.monthly_salary.is_positive() {
203 return Err(LaborCodeError::InvalidContract(
204 "Monthly salary must be positive".to_string(),
205 ));
206 }
207
208 Ok(())
209 }
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct LaborRights {
215 pub safe_working_conditions: bool,
217 pub timely_salary: bool,
219 pub rest_periods: bool,
221 pub professional_training: bool,
223 pub collective_bargaining: bool,
225}
226
227impl LaborRights {
228 pub fn full_rights() -> Self {
230 Self {
231 safe_working_conditions: true,
232 timely_salary: true,
233 rest_periods: true,
234 professional_training: true,
235 collective_bargaining: true,
236 }
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub enum TerminationGround {
243 MutualAgreement,
245 ContractExpiration,
247 EmployeeInitiative,
249 EmployerInitiative { reason: EmployerTerminationReason },
251 Transfer,
253 EmployeeRefusal,
255 ForceMajeure,
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
261pub enum EmployerTerminationReason {
262 Liquidation,
264 StaffReduction,
266 InadequateQualifications,
268 RepeatedFailure,
270 GrossViolation,
272 LossOfTrust,
274 ImmoralAct,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct TerminationNotice {
281 pub initiated_by: String,
283 pub ground: TerminationGround,
285 pub notice_date: chrono::NaiveDate,
287 pub termination_date: chrono::NaiveDate,
289 pub notice_period_days: u32,
291}
292
293impl TerminationNotice {
294 pub fn new(
296 initiated_by: impl Into<String>,
297 ground: TerminationGround,
298 notice_date: chrono::NaiveDate,
299 termination_date: chrono::NaiveDate,
300 ) -> Self {
301 let notice_period_days = (termination_date - notice_date).num_days() as u32;
302
303 Self {
304 initiated_by: initiated_by.into(),
305 ground,
306 notice_date,
307 termination_date,
308 notice_period_days,
309 }
310 }
311
312 pub fn validate(&self) -> Result<(), LaborCodeError> {
314 if matches!(self.ground, TerminationGround::EmployeeInitiative)
316 && self.notice_period_days < 14
317 {
318 return Err(LaborCodeError::InvalidTermination(
319 "Employee must give at least 14 days notice".to_string(),
320 ));
321 }
322
323 if matches!(
325 self.ground,
326 TerminationGround::EmployerInitiative {
327 reason: EmployerTerminationReason::StaffReduction
328 }
329 ) && self.notice_period_days < 60
330 {
331 return Err(LaborCodeError::InvalidTermination(
332 "Employer must give at least 60 days notice for staff reduction".to_string(),
333 ));
334 }
335
336 Ok(())
337 }
338}
339
340pub fn quick_validate_employment_contract(
342 contract: &EmploymentContract,
343) -> Result<(), LaborCodeError> {
344 contract.validate()
345}
346
347pub fn check_minimum_wage(salary: &crate::common::Currency) -> Result<(), LaborCodeError> {
349 let min_wage = crate::common::Currency::from_rubles(19242);
351
352 if salary.kopecks < min_wage.kopecks {
353 return Err(LaborCodeError::InvalidContract(format!(
354 "Salary {} is below minimum wage {}",
355 salary, min_wage
356 )));
357 }
358
359 Ok(())
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn test_working_time_regime() {
368 let standard = WorkingTimeRegime::standard();
369 assert_eq!(standard.hours_per_week, 40);
370 assert!(standard.validate().is_ok());
371
372 let excessive = WorkingTimeRegime {
373 hours_per_week: 50,
374 days_per_week: 5,
375 start_time: chrono::NaiveTime::from_hms_opt(9, 0, 0).expect("Valid time"),
376 end_time: chrono::NaiveTime::from_hms_opt(18, 0, 0).expect("Valid time"),
377 lunch_break_minutes: 60,
378 };
379 assert!(excessive.validate().is_err());
380 }
381
382 #[test]
383 fn test_employment_contract() {
384 let contract = EmploymentContract::new(
385 "ООО Компания",
386 "Иванов Иван",
387 chrono::NaiveDate::from_ymd_opt(2024, 1, 1).expect("Valid date"),
388 chrono::NaiveDate::from_ymd_opt(2024, 1, 15).expect("Valid date"),
389 "Software Engineer",
390 crate::common::Currency::from_rubles(100000),
391 )
392 .with_workplace("Moscow")
393 .with_written_form(true);
394
395 assert!(contract.validate().is_ok());
396 }
397
398 #[test]
399 fn test_termination_notice() {
400 let notice = TerminationNotice::new(
401 "Employee",
402 TerminationGround::EmployeeInitiative,
403 chrono::NaiveDate::from_ymd_opt(2024, 1, 1).expect("Valid date"),
404 chrono::NaiveDate::from_ymd_opt(2024, 1, 16).expect("Valid date"),
405 );
406
407 assert!(notice.validate().is_ok());
408
409 let short_notice = TerminationNotice::new(
410 "Employee",
411 TerminationGround::EmployeeInitiative,
412 chrono::NaiveDate::from_ymd_opt(2024, 1, 1).expect("Valid date"),
413 chrono::NaiveDate::from_ymd_opt(2024, 1, 8).expect("Valid date"),
414 );
415
416 assert!(short_notice.validate().is_err());
417 }
418
419 #[test]
420 fn test_minimum_wage() {
421 let adequate = crate::common::Currency::from_rubles(25000);
422 assert!(check_minimum_wage(&adequate).is_ok());
423
424 let inadequate = crate::common::Currency::from_rubles(10000);
425 assert!(check_minimum_wage(&inadequate).is_err());
426 }
427}