Skip to main content

ec_validator/
ruc.rs

1use crate::cedula;
2use crate::ValidationError;
3
4#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum RucType {
7    NaturalPerson,
8    JuridicalEntity,
9    PublicEntity,
10}
11
12/// Determines the RUC type from the 3rd digit.
13///
14/// # Arguments
15///
16/// * `input` - A 13-digit RUC string.
17///
18/// # Returns
19///
20/// Returns `Some` with the [`RucType`] if valid, `None` otherwise.
21///
22/// # Examples
23///
24/// ```
25/// use ec_validator::ruc::{ruc_type, RucType};
26///
27/// assert_eq!(ruc_type("1713175071001"), Some(RucType::NaturalPerson));
28/// assert_eq!(ruc_type("1790085783001"), Some(RucType::JuridicalEntity));
29/// ```
30pub fn ruc_type(input: &str) -> Option<RucType> {
31    let input = input.trim();
32    if input.len() != 13 || !input.chars().all(|c| c.is_ascii_digit()) {
33        return None;
34    }
35    match input.chars().nth(2).unwrap().to_digit(10).unwrap() {
36        0..=5 => Some(RucType::NaturalPerson),
37        6 => Some(RucType::PublicEntity),
38        9 => Some(RucType::JuridicalEntity),
39        _ => None,
40    }
41}
42
43/// Validates an Ecuadorian RUC (Registro Único de Contribuyentes).
44///
45/// # Arguments
46///
47/// * `input` - A string containing the RUC number (13 digits).
48///
49/// # Errors
50///
51/// Returns [`ValidationError`] on validation failure:
52/// - [`ValidationError::InvalidLength`] - Not exactly 13 digits
53/// - [`ValidationError::InvalidFormat`] - Non-numeric or invalid RUC type digit
54/// - [`ValidationError::InvalidProvinceCode`] - Province code not in 01-24
55/// - [`ValidationError::InvalidCheckDigit`] - Check digit invalid
56///
57/// # Examples
58///
59/// ```
60/// use ec_validator::ruc;
61///
62/// // Natural person RUC (3rd digit 0-5)
63/// let result = ruc::validate("1713175071001");
64/// assert!(result.is_ok());
65///
66/// // Juridical entity RUC (3rd digit 9)
67/// let result = ruc::validate("1790085783001");
68/// assert!(result.is_ok());
69///
70/// // Public entity RUC (3rd digit 6)
71/// let result = ruc::validate("1760001550001");
72/// assert!(result.is_ok());
73/// ```
74pub fn validate(input: &str) -> Result<(), ValidationError> {
75    let input = input.trim();
76
77    if input.len() != 13 {
78        return Err(ValidationError::InvalidLength);
79    }
80
81    if !input.chars().all(|c| c.is_ascii_digit()) {
82        return Err(ValidationError::InvalidFormat);
83    }
84
85    let third_digit = input.chars().nth(2).unwrap().to_digit(10).unwrap();
86
87    match third_digit {
88        0..=5 => validate_natural(input),
89        6 => validate_public(input),
90        7..=8 => Err(ValidationError::InvalidFormat),
91        9 => validate_juridical(input),
92        _ => Err(ValidationError::InvalidFormat),
93    }
94}
95
96fn validate_natural(input: &str) -> Result<(), ValidationError> {
97    let cedula = &input[..10];
98    cedula::validate(cedula)?;
99
100    let establishment: u32 = input[10..13].parse().unwrap();
101    if establishment == 0 || establishment > 999 {
102        return Err(ValidationError::InvalidFormat);
103    }
104
105    Ok(())
106}
107
108fn validate_juridical(input: &str) -> Result<(), ValidationError> {
109    let province: u32 = input[..2].parse().unwrap();
110    if province == 0 || province > 24 {
111        return Err(ValidationError::InvalidProvinceCode);
112    }
113
114    let check_digit_pos9 = input.chars().nth(9).unwrap().to_digit(10).unwrap();
115
116    let weights = [4, 3, 2, 7, 6, 5, 4, 3, 2];
117    let mut sum = 0u32;
118
119    for (i, digit) in input.chars().take(9).enumerate() {
120        let digit = digit.to_digit(10).unwrap();
121        sum += digit * weights[i];
122    }
123
124    let computed_check = if sum.is_multiple_of(11) {
125        0
126    } else {
127        11 - (sum % 11)
128    };
129
130    if computed_check == 10 {
131        return Err(ValidationError::InvalidCheckDigit);
132    }
133
134    if computed_check != check_digit_pos9 {
135        return Err(ValidationError::InvalidCheckDigit);
136    }
137
138    let establishment: u32 = input[9..13].parse().unwrap();
139    if establishment == 0 || establishment > 9999 {
140        return Err(ValidationError::InvalidFormat);
141    }
142
143    Ok(())
144}
145
146fn validate_public(input: &str) -> Result<(), ValidationError> {
147    let province: u32 = input[..2].parse().unwrap();
148    if province == 0 || province > 24 {
149        return Err(ValidationError::InvalidProvinceCode);
150    }
151
152    let check_digit = input.chars().nth(8).unwrap().to_digit(10).unwrap();
153
154    let weights = [3, 2, 7, 6, 5, 4, 3, 2];
155    let mut sum = 0u32;
156
157    for (i, digit) in input.chars().take(8).enumerate() {
158        let digit = digit.to_digit(10).unwrap();
159        sum += digit * weights[i];
160    }
161
162    let remainder = if sum.is_multiple_of(11) {
163        0
164    } else {
165        11 - (sum % 11)
166    };
167
168    if remainder != check_digit {
169        return Err(ValidationError::InvalidCheckDigit);
170    }
171
172    let establishment: u32 = input[9..13].parse().unwrap();
173    if establishment == 0 || establishment > 9999 {
174        return Err(ValidationError::InvalidFormat);
175    }
176
177    Ok(())
178}
179
180/// Convenience function that returns `true` if the RUC is valid, `false` otherwise.
181///
182/// # Arguments
183///
184/// * `input` - A string containing the RUC number.
185///
186/// # Examples
187///
188/// ```
189/// use ec_validator::ruc;
190///
191/// assert!(ruc::is_valid("1713175071001"));
192/// assert!(!ruc::is_valid("0000000000000"));
193/// ```
194pub fn is_valid(input: &str) -> bool {
195    validate(input).is_ok()
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn natural_ruc() {
204        assert!(validate("1713175071001").is_ok());
205    }
206
207    #[test]
208    fn juridical_ruc() {
209        assert!(validate("1790085783001").is_ok());
210    }
211
212    #[test]
213    fn public_ruc() {
214        assert!(validate("1760001550001").is_ok());
215    }
216
217    #[test]
218    fn invalid_establishment() {
219        assert_eq!(
220            validate("1713175071000"),
221            Err(ValidationError::InvalidFormat)
222        );
223    }
224
225    #[test]
226    fn bad_check_digit() {
227        assert_eq!(
228            validate("1790085782001"),
229            Err(ValidationError::InvalidCheckDigit)
230        );
231    }
232}