Skip to main content

agentis_pay_shared/types/
cpf.rs

1use serde::{Deserialize, Deserializer, Serialize};
2use std::{
3    ops::Deref,
4    str::{Chars, FromStr},
5};
6
7#[derive(Debug, Clone, Serialize)]
8pub struct Cpf(String);
9
10impl Cpf {
11    pub fn format_and_mask(&self) -> String {
12        Self::format_and_mask_cpf(&self.0)
13    }
14
15    pub fn format_and_mask_cpf(cpf: &str) -> String {
16        format!("•••.{}.{}-••", &cpf[3..6], &cpf[6..9])
17    }
18}
19
20#[derive(Debug, PartialEq, Eq)]
21pub struct Error(&'static str);
22
23impl Error {
24    const LENGTH: Self = Error("length != 11");
25    const SYMBOLS: Self = Error("non-digit symbols");
26    const DIGITS: Self = Error("invalid check digits");
27}
28
29impl std::fmt::Display for Error {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        write!(f, "cpf error {}", self.0)
32    }
33}
34
35impl std::error::Error for Error {}
36
37impl<'de> Deserialize<'de> for Cpf {
38    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Cpf, D::Error> {
39        let raw = String::deserialize(deserializer)?;
40        raw.parse().map_err(serde::de::Error::custom)
41    }
42}
43
44impl Deref for Cpf {
45    type Target = String;
46
47    fn deref(&self) -> &Self::Target {
48        &self.0
49    }
50}
51
52impl FromStr for Cpf {
53    type Err = Error;
54
55    #[expect(clippy::similar_names, reason = "because it fits so well")]
56    fn from_str(s: &str) -> Result<Self, Self::Err> {
57        if s.len() > 11 {
58            return Err(Error::LENGTH);
59        }
60
61        let mut chars = s.chars();
62        let digit2 = read_back_digit(&mut chars)?;
63        let digit1 = read_back_digit(&mut chars)?;
64
65        let mut digit_acc = Some(digit1).filter(|d| *d == digit2);
66        let mut digit1_acc = 0;
67        let mut digit2_acc = 0;
68
69        for i in 0..9 {
70            let digit = read_front_digit(&mut chars)?;
71
72            digit_acc = digit_acc.filter(|d| *d == digit);
73            digit1_acc += digit * (10 - i);
74            digit2_acc += digit * (11 - i);
75        }
76
77        let expected_digit1 = (digit1_acc * 10 % 11) % 10;
78        let expected_digit2 = ((digit2_acc + 2 * expected_digit1) * 10 % 11) % 10;
79
80        if expected_digit1 != digit1 || expected_digit2 != digit2 || digit_acc.is_some() {
81            return Err(Error::DIGITS);
82        }
83
84        Ok(Cpf(s.to_owned()))
85    }
86}
87
88fn read_front_digit(chars: &mut Chars) -> Result<u32, Error> {
89    let char = chars.next().ok_or(Error::LENGTH)?;
90    char.to_digit(10).ok_or(Error::SYMBOLS)
91}
92
93fn read_back_digit(chars: &mut Chars) -> Result<u32, Error> {
94    let char = chars.next_back().ok_or(Error::LENGTH)?;
95    char.to_digit(10).ok_or(Error::SYMBOLS)
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn cpf_parsing() {
104        assert!("64780788099".parse::<Cpf>().is_ok());
105        assert!("34790194043".parse::<Cpf>().is_ok());
106        assert!("04584475016".parse::<Cpf>().is_ok());
107        assert!("76073649002".parse::<Cpf>().is_ok());
108        assert!("01234567890".parse::<Cpf>().is_ok());
109        assert!("24254794061".parse::<Cpf>().is_ok());
110        assert!("40654704040".parse::<Cpf>().is_ok());
111        assert!("54350515097".parse::<Cpf>().is_ok());
112        assert!("46337130006".parse::<Cpf>().is_ok());
113        assert!("25422669053".parse::<Cpf>().is_ok());
114        assert!("53266606019".parse::<Cpf>().is_ok());
115        assert!("60595030041".parse::<Cpf>().is_ok());
116        assert!("49274939035".parse::<Cpf>().is_ok());
117        assert!("71482987090".parse::<Cpf>().is_ok());
118        assert!("66611386025".parse::<Cpf>().is_ok());
119        assert!("30355706040".parse::<Cpf>().is_ok());
120        assert!("58012477009".parse::<Cpf>().is_ok());
121
122        assert_eq!("01234567891".parse::<Cpf>().unwrap_err(), Error::DIGITS);
123        assert_eq!("0123456789".parse::<Cpf>().unwrap_err(), Error::LENGTH);
124        assert_eq!("012345678901".parse::<Cpf>().unwrap_err(), Error::LENGTH);
125        assert_eq!("0123456789a".parse::<Cpf>().unwrap_err(), Error::SYMBOLS);
126
127        assert_eq!("00000000000".parse::<Cpf>().unwrap_err(), Error::DIGITS);
128        assert_eq!("11111111111".parse::<Cpf>().unwrap_err(), Error::DIGITS);
129        assert_eq!("22222222222".parse::<Cpf>().unwrap_err(), Error::DIGITS);
130        assert_eq!("33333333333".parse::<Cpf>().unwrap_err(), Error::DIGITS);
131        assert_eq!("44444444444".parse::<Cpf>().unwrap_err(), Error::DIGITS);
132        assert_eq!("55555555555".parse::<Cpf>().unwrap_err(), Error::DIGITS);
133        assert_eq!("66666666666".parse::<Cpf>().unwrap_err(), Error::DIGITS);
134        assert_eq!("77777777777".parse::<Cpf>().unwrap_err(), Error::DIGITS);
135        assert_eq!("88888888888".parse::<Cpf>().unwrap_err(), Error::DIGITS);
136        assert_eq!("99999999999".parse::<Cpf>().unwrap_err(), Error::DIGITS);
137    }
138}