Skip to main content

legalis_ru/
labor_code.rs

1//! Labor Code of the Russian Federation (Трудовой кодекс РФ).
2//!
3//! Federal Law No. 197-FZ of December 30, 2001
4//!
5//! This module provides:
6//! - Employment contract regulations
7//! - Working time and rest periods (40-hour work week)
8//! - Labor rights and obligations
9//! - Termination procedures
10
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14/// Errors related to Labor Code operations
15#[derive(Debug, Error, Clone, Serialize, Deserialize)]
16pub enum LaborCodeError {
17    /// Invalid employment contract
18    #[error("Invalid employment contract: {0}")]
19    InvalidContract(String),
20
21    /// Invalid working hours
22    #[error("Invalid working hours: {0}")]
23    InvalidWorkingHours(String),
24
25    /// Invalid termination
26    #[error("Invalid termination: {0}")]
27    InvalidTermination(String),
28
29    /// Validation failed
30    #[error("Validation failed: {0}")]
31    ValidationFailed(String),
32}
33
34/// Types of employment
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub enum EmploymentType {
37    /// Indefinite term (бессрочный)
38    Indefinite,
39    /// Fixed term (срочный) - up to 5 years
40    FixedTerm { end_date: chrono::NaiveDate },
41    /// Temporary (временный) - up to 2 months
42    Temporary { end_date: chrono::NaiveDate },
43    /// Seasonal (сезонный)
44    Seasonal { end_date: chrono::NaiveDate },
45}
46
47/// Working time regime (Article 100)
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct WorkingTimeRegime {
50    /// Normal working hours per week (default 40)
51    pub hours_per_week: u32,
52    /// Working days per week
53    pub days_per_week: u32,
54    /// Start time
55    pub start_time: chrono::NaiveTime,
56    /// End time
57    pub end_time: chrono::NaiveTime,
58    /// Lunch break duration in minutes
59    pub lunch_break_minutes: u32,
60}
61
62impl WorkingTimeRegime {
63    /// Creates standard 40-hour work week regime
64    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    /// Creates reduced working time (Article 92)
75    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    /// Validates working time regime
86    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/// Employment contract (Article 56)
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct EmploymentContract {
112    /// Employer name
113    pub employer: String,
114    /// Employee name
115    pub employee: String,
116    /// Contract date
117    pub contract_date: chrono::NaiveDate,
118    /// Start date of work
119    pub start_date: chrono::NaiveDate,
120    /// Employment type
121    pub employment_type: EmploymentType,
122    /// Position
123    pub position: String,
124    /// Salary (monthly)
125    pub monthly_salary: crate::common::Currency,
126    /// Working time regime
127    pub working_time: WorkingTimeRegime,
128    /// Workplace location
129    pub workplace: String,
130    /// Is in written form
131    pub written_form: bool,
132}
133
134impl EmploymentContract {
135    /// Creates a new employment contract
136    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    /// Sets employment type
159    pub fn with_employment_type(mut self, employment_type: EmploymentType) -> Self {
160        self.employment_type = employment_type;
161        self
162    }
163
164    /// Sets working time regime
165    pub fn with_working_time(mut self, working_time: WorkingTimeRegime) -> Self {
166        self.working_time = working_time;
167        self
168    }
169
170    /// Sets workplace location
171    pub fn with_workplace(mut self, workplace: impl Into<String>) -> Self {
172        self.workplace = workplace.into();
173        self
174    }
175
176    /// Sets written form
177    pub fn with_written_form(mut self, written: bool) -> Self {
178        self.written_form = written;
179        self
180    }
181
182    /// Validates the employment contract
183    pub fn validate(&self) -> Result<(), LaborCodeError> {
184        // Contract must be in writing (Article 67)
185        if !self.written_form {
186            return Err(LaborCodeError::InvalidContract(
187                "Employment contract must be in written form".to_string(),
188            ));
189        }
190
191        // Start date should not be before contract date
192        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        // Validate working time
199        self.working_time.validate()?;
200
201        // Salary must be positive
202        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/// Labor rights (Article 21)
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct LaborRights {
215    /// Right to appropriate working conditions
216    pub safe_working_conditions: bool,
217    /// Right to timely payment of salary
218    pub timely_salary: bool,
219    /// Right to rest
220    pub rest_periods: bool,
221    /// Right to professional training
222    pub professional_training: bool,
223    /// Right to collective bargaining
224    pub collective_bargaining: bool,
225}
226
227impl LaborRights {
228    /// Creates full labor rights
229    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/// Grounds for termination (Article 77, 81)
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub enum TerminationGround {
243    /// Mutual agreement (соглашение сторон)
244    MutualAgreement,
245    /// Expiration of contract term (истечение срока договора)
246    ContractExpiration,
247    /// Employee's initiative (инициатива работника)
248    EmployeeInitiative,
249    /// Employer's initiative (инициатива работодателя)
250    EmployerInitiative { reason: EmployerTerminationReason },
251    /// Transfer to another employer
252    Transfer,
253    /// Employee's refusal to continue work
254    EmployeeRefusal,
255    /// Circumstances beyond control (форс-мажор)
256    ForceMajeure,
257}
258
259/// Reasons for employer-initiated termination (Article 81)
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
261pub enum EmployerTerminationReason {
262    /// Liquidation of organization
263    Liquidation,
264    /// Staff reduction (сокращение штата)
265    StaffReduction,
266    /// Inadequate qualifications
267    InadequateQualifications,
268    /// Repeated failure to perform duties
269    RepeatedFailure,
270    /// Gross violation of labor duties
271    GrossViolation,
272    /// Loss of trust
273    LossOfTrust,
274    /// Immoral act (for educators)
275    ImmoralAct,
276}
277
278/// Termination notice representation
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct TerminationNotice {
281    /// Party initiating termination
282    pub initiated_by: String,
283    /// Termination ground
284    pub ground: TerminationGround,
285    /// Notice date
286    pub notice_date: chrono::NaiveDate,
287    /// Termination date
288    pub termination_date: chrono::NaiveDate,
289    /// Notice period in days
290    pub notice_period_days: u32,
291}
292
293impl TerminationNotice {
294    /// Creates a new termination notice
295    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    /// Validates the termination notice
313    pub fn validate(&self) -> Result<(), LaborCodeError> {
314        // Employee must give at least 14 days notice (Article 80)
315        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        // Employer must give notice for staff reduction (Article 180)
324        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
340/// Quick validation for employment contract
341pub fn quick_validate_employment_contract(
342    contract: &EmploymentContract,
343) -> Result<(), LaborCodeError> {
344    contract.validate()
345}
346
347/// Article 133: Minimum wage check
348pub fn check_minimum_wage(salary: &crate::common::Currency) -> Result<(), LaborCodeError> {
349    // As of 2024, federal minimum wage is approximately 19,242 RUB
350    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}