Skip to main content

stdbr_core/
cnpj.rs

1//! CNPJ (Cadastro Nacional da Pessoa Jurídica) - validation, formatting and generation.
2//!
3//! Supports both numeric (current) and alphanumeric (IN RFB 2.119/2022, July 2026) formats.
4//! Uses modulo-11 weighted sums as specified by Receita Federal do Brasil.
5
6use alloc::string::String;
7use alloc::vec::Vec;
8use core::fmt;
9
10use crate::rand::{simple_seed, xorshift64};
11use crate::util::{self, impl_document_traits};
12
13const CNPJ_LEN: usize = 14;
14const WEIGHTS_D1: [u32; 12] = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
15const WEIGHTS_D2: [u32; 13] = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
16const FORMATTED_CHAR_POS: [usize; 14] = [0, 1, 3, 4, 5, 7, 8, 9, 11, 12, 13, 14, 16, 17];
17
18/// Whether the CNPJ uses only digits or also contains letters.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[repr(u8)]
21pub enum CnpjKind {
22    Numeric = 0,
23    Alphanumeric = 1,
24}
25
26/// Whether the establishment is the main office (Matriz) or a branch (Filial).
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[repr(u8)]
29pub enum EstablishmentType {
30    Matriz = 0,
31    Filial = 1,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum CnpjError {
36    InvalidLength,
37    InvalidCharacter,
38    InvalidFormat,
39    AllCharsEqual,
40    InvalidCheckDigits,
41}
42
43impl fmt::Display for CnpjError {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        f.write_str(match self {
46            Self::InvalidLength => "CNPJ must contain exactly 14 characters",
47            Self::InvalidCharacter => "CNPJ contains invalid characters",
48            Self::InvalidFormat => "CNPJ format must be XX.XXX.XXX/XXXX-DD or 14 characters",
49            Self::AllCharsEqual => "CNPJ with all equal characters is invalid",
50            Self::InvalidCheckDigits => "CNPJ check digits are invalid",
51        })
52    }
53}
54
55/// A validated CNPJ stored as 14 ASCII bytes.
56///
57/// Positions 0-11 may contain `0-9` or `A-Z` (alphanumeric CNPJ).
58/// Positions 12-13 are always numeric digits (check digits).
59///
60/// ```
61/// use stdbr_core::cnpj::{Cnpj, CnpjKind, generate_cnpj};
62///
63/// let cnpj = generate_cnpj(CnpjKind::Numeric);
64/// assert_eq!(cnpj.as_str().len(), 14);
65/// assert_eq!(cnpj.to_string().len(), 18); // XX.XXX.XXX/XXXX-DD
66///
67/// let parsed: Cnpj = cnpj.to_string().parse().unwrap();
68/// assert_eq!(cnpj, parsed);
69/// ```
70#[derive(Clone, Copy, PartialEq, Eq, Hash)]
71pub struct Cnpj {
72    bytes: [u8; CNPJ_LEN],
73}
74
75impl Cnpj {
76    /// Unformatted 14-character `&str`.
77    pub fn as_str(&self) -> &str {
78        // SAFETY: constructors guarantee ASCII alphanumeric only.
79        unsafe { core::str::from_utf8_unchecked(&self.bytes) }
80    }
81
82    /// Whether the CNPJ is numeric or alphanumeric.
83    pub fn kind(&self) -> CnpjKind {
84        if self.bytes[..12].iter().all(u8::is_ascii_digit) {
85            CnpjKind::Numeric
86        } else {
87            CnpjKind::Alphanumeric
88        }
89    }
90
91    /// Root (positions 1-8): identifies the company.
92    pub fn raiz(&self) -> &str {
93        unsafe { core::str::from_utf8_unchecked(&self.bytes[..8]) }
94    }
95
96    /// Order (positions 9-12): identifies the establishment.
97    pub fn ordem(&self) -> &str {
98        unsafe { core::str::from_utf8_unchecked(&self.bytes[8..12]) }
99    }
100
101    /// Whether this is Matriz (ordem == "0001") or Filial.
102    pub fn establishment_type(&self) -> EstablishmentType {
103        if &self.bytes[8..12] == b"0001" {
104            EstablishmentType::Matriz
105        } else {
106            EstablishmentType::Filial
107        }
108    }
109
110    /// The two check digits `(d1, d2)` as numeric values.
111    pub fn check_digits(&self) -> (u8, u8) {
112        (self.bytes[12] - b'0', self.bytes[13] - b'0')
113    }
114
115    /// Masked: `XX.XXX.XXX/****-**`.
116    pub fn masked(&self) -> String {
117        let s = self.as_str();
118        alloc::format!("{}.{}.{}/****-**", &s[0..2], &s[2..5], &s[5..8])
119    }
120}
121
122impl fmt::Display for Cnpj {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        let s = self.as_str();
125        write!(
126            f,
127            "{}.{}.{}/{}-{}",
128            &s[0..2],
129            &s[2..5],
130            &s[5..8],
131            &s[8..12],
132            &s[12..14]
133        )
134    }
135}
136
137impl_document_traits!(Cnpj, CnpjError);
138
139/// Strips punctuation, preserves letters and digits, uppercases letters.
140pub fn remove_symbols(cnpj: &str) -> String {
141    cnpj.chars()
142        .filter(char::is_ascii_alphanumeric)
143        .map(|c| c.to_ascii_uppercase())
144        .collect()
145}
146
147/// Lenient validation: strips punctuation, uppercases, then validates.
148pub fn is_valid(cnpj: &str) -> bool {
149    let raw = remove_symbols(cnpj);
150    if raw.len() != CNPJ_LEN {
151        return false;
152    }
153    let bytes = raw.as_bytes();
154    // Positions 0-11 must be alphanumeric uppercase, 12-13 must be digits
155    if !bytes[..12]
156        .iter()
157        .all(|&b| b.is_ascii_uppercase() || b.is_ascii_digit())
158    {
159        return false;
160    }
161    if !bytes[12..14].iter().all(u8::is_ascii_digit) {
162        return false;
163    }
164    validate(bytes)
165}
166
167/// Strict validation - accepts `XX.XXX.XXX/XXXX-DD` or `XXXXXXXXXXXXXXDD` only.
168pub fn is_valid_strict(cnpj: &str) -> Result<(), CnpjError> {
169    parse_strict(cnpj).map(|_| ())
170}
171
172/// Formats as `XX.XXX.XXX/XXXX-DD`, or `None` if not valid 14 chars.
173pub fn format_cnpj(cnpj: &str) -> Option<String> {
174    let raw = remove_symbols(cnpj);
175    if raw.len() != CNPJ_LEN {
176        return None;
177    }
178    let bytes = raw.as_bytes();
179    if !bytes[..12]
180        .iter()
181        .all(|&b| b.is_ascii_uppercase() || b.is_ascii_digit())
182    {
183        return None;
184    }
185    if !bytes[12..14].iter().all(u8::is_ascii_digit) {
186        return None;
187    }
188    Some(alloc::format!(
189        "{}.{}.{}/{}-{}",
190        &raw[0..2],
191        &raw[2..5],
192        &raw[5..8],
193        &raw[8..12],
194        &raw[12..14]
195    ))
196}
197
198/// Generates a random valid CNPJ as a 14-character string.
199pub fn generate(kind: CnpjKind) -> String {
200    generate_cnpj(kind).as_str().into()
201}
202
203/// Generates a random valid [`Cnpj`].
204pub fn generate_cnpj(kind: CnpjKind) -> Cnpj {
205    generate_with_seed(simple_seed(), kind)
206}
207
208/// Generates a random valid [`Cnpj`] with ordem "0001" (Matriz).
209pub fn generate_matriz(kind: CnpjKind) -> Cnpj {
210    generate_with_ordem(simple_seed(), kind, *b"0001")
211}
212
213/// Computes check digits for a 12-character CNPJ base.
214/// Returns `None` if input is invalid.
215pub fn compute_check_digits(base: &str) -> Option<(u8, u8)> {
216    let raw = remove_symbols(base);
217    if raw.len() != 12 {
218        return None;
219    }
220    let bytes = raw.as_bytes();
221    if !bytes
222        .iter()
223        .all(|&b| b.is_ascii_uppercase() || b.is_ascii_digit())
224    {
225        return None;
226    }
227    if all_equal(bytes) {
228        return None;
229    }
230
231    let values: Vec<u32> = bytes.iter().map(|&b| char_value(b)).collect();
232    let d1 = calc_check_digit(&values, &WEIGHTS_D1);
233    let mut full = Vec::with_capacity(13);
234    full.extend_from_slice(&values);
235    full.push(u32::from(d1));
236    let d2 = calc_check_digit(&full, &WEIGHTS_D2);
237
238    Some((d1, d2))
239}
240
241// --- Private functions ---
242
243/// Converts ASCII byte to its numeric value for CNPJ calculation.
244/// '0'-'9' => 0-9, 'A'-'Z' => 17-42 (ASCII - 48).
245fn char_value(b: u8) -> u32 {
246    u32::from(b) - 48
247}
248
249fn all_equal(bytes: &[u8]) -> bool {
250    util::all_equal(bytes)
251}
252
253fn validate(bytes: &[u8]) -> bool {
254    if all_equal(bytes) {
255        return false;
256    }
257    let values: Vec<u32> = bytes[..12].iter().map(|&b| char_value(b)).collect();
258    let d1 = calc_check_digit(&values, &WEIGHTS_D1);
259    let mut full = Vec::with_capacity(13);
260    full.extend_from_slice(&values);
261    full.push(u32::from(d1));
262    let d2 = calc_check_digit(&full, &WEIGHTS_D2);
263    bytes[12] - b'0' == d1 && bytes[13] - b'0' == d2
264}
265
266fn calc_check_digit(values: &[u32], weights: &[u32]) -> u8 {
267    let sum: u32 = values.iter().zip(weights).map(|(&v, &w)| v * w).sum();
268    let rem = sum % 11;
269    #[allow(clippy::cast_possible_truncation)]
270    // rem is always 2..=10, so 11-rem fits in u8
271    if rem < 2 { 0 } else { (11 - rem) as u8 }
272}
273
274fn append_check_digits(bytes: &mut [u8; CNPJ_LEN]) {
275    let values: Vec<u32> = bytes[..12].iter().map(|&b| char_value(b)).collect();
276    let d1 = calc_check_digit(&values, &WEIGHTS_D1);
277    let mut full = Vec::with_capacity(13);
278    full.extend_from_slice(&values);
279    full.push(u32::from(d1));
280    let d2 = calc_check_digit(&full, &WEIGHTS_D2);
281    bytes[12] = b'0' + d1;
282    bytes[13] = b'0' + d2;
283}
284
285fn parse_strict(s: &str) -> Result<Cnpj, CnpjError> {
286    let raw = s.as_bytes();
287
288    let chars: Vec<u8> = match raw.len() {
289        14 => {
290            // Positions 0-11: alphanumeric uppercase, 12-13: digits
291            if !raw[..12]
292                .iter()
293                .all(|&b| b.is_ascii_uppercase() || b.is_ascii_digit())
294            {
295                return Err(CnpjError::InvalidCharacter);
296            }
297            if !raw[12..14].iter().all(u8::is_ascii_digit) {
298                return Err(CnpjError::InvalidCharacter);
299            }
300            raw.to_vec()
301        }
302        18 => {
303            // XX.XXX.XXX/XXXX-DD
304            if raw[2] != b'.' || raw[6] != b'.' || raw[10] != b'/' || raw[15] != b'-' {
305                return Err(CnpjError::InvalidFormat);
306            }
307            let extracted: Vec<u8> = FORMATTED_CHAR_POS.iter().map(|&i| raw[i]).collect();
308            if !extracted[..12]
309                .iter()
310                .all(|&b| b.is_ascii_uppercase() || b.is_ascii_digit())
311            {
312                return Err(CnpjError::InvalidCharacter);
313            }
314            if !extracted[12..14].iter().all(u8::is_ascii_digit) {
315                return Err(CnpjError::InvalidCharacter);
316            }
317            extracted
318        }
319        _ => return Err(CnpjError::InvalidLength),
320    };
321
322    if all_equal(&chars) {
323        return Err(CnpjError::AllCharsEqual);
324    }
325
326    let values: Vec<u32> = chars[..12].iter().map(|&b| char_value(b)).collect();
327    let d1 = calc_check_digit(&values, &WEIGHTS_D1);
328    let mut full = Vec::with_capacity(13);
329    full.extend_from_slice(&values);
330    full.push(u32::from(d1));
331    let d2 = calc_check_digit(&full, &WEIGHTS_D2);
332
333    if chars[12] - b'0' != d1 || chars[13] - b'0' != d2 {
334        return Err(CnpjError::InvalidCheckDigits);
335    }
336
337    let mut bytes = [0u8; CNPJ_LEN];
338    bytes.copy_from_slice(&chars);
339    Ok(Cnpj { bytes })
340}
341
342const ALPHANUMERIC_CHARS: &[u8; 36] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
343
344fn generate_with_seed(mut seed: u64, kind: CnpjKind) -> Cnpj {
345    let mut bytes = [0u8; CNPJ_LEN];
346
347    loop {
348        for b in &mut bytes[..12] {
349            seed = xorshift64(seed);
350            *b = match kind {
351                CnpjKind::Numeric => b'0' + (seed % 10) as u8,
352                CnpjKind::Alphanumeric => ALPHANUMERIC_CHARS[(seed % 36) as usize],
353            };
354        }
355        if !all_equal(&bytes[..12]) {
356            break;
357        }
358    }
359
360    append_check_digits(&mut bytes);
361    Cnpj { bytes }
362}
363
364fn generate_with_ordem(mut seed: u64, kind: CnpjKind, ordem: [u8; 4]) -> Cnpj {
365    let mut bytes = [0u8; CNPJ_LEN];
366    bytes[8..12].copy_from_slice(&ordem);
367
368    loop {
369        for b in &mut bytes[..8] {
370            seed = xorshift64(seed);
371            *b = match kind {
372                CnpjKind::Numeric => b'0' + (seed % 10) as u8,
373                CnpjKind::Alphanumeric => ALPHANUMERIC_CHARS[(seed % 36) as usize],
374            };
375        }
376        if !all_equal(&bytes[..12]) {
377            break;
378        }
379    }
380
381    append_check_digits(&mut bytes);
382    Cnpj { bytes }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    use alloc::string::ToString;
389
390    // Known valid numeric CNPJ: 11.222.333/0001-81
391    fn cnpj_numeric() -> Cnpj {
392        "11222333000181".parse().unwrap()
393    }
394
395    // Known valid numeric CNPJ: 11.444.777/0001-61
396    fn cnpj_numeric_b() -> Cnpj {
397        "11444777000161".parse().unwrap()
398    }
399
400    // Build a CNPJ from a 12-char base
401    fn make_cnpj(base: &[u8; 12]) -> Cnpj {
402        let mut bytes = [0u8; CNPJ_LEN];
403        bytes[..12].copy_from_slice(base);
404        append_check_digits(&mut bytes);
405        Cnpj { bytes }
406    }
407
408    fn cnpj_alpha() -> Cnpj {
409        // Alphanumeric CNPJ: base "12ABC34500DE", compute check digits
410        make_cnpj(b"12ABC34500DE")
411    }
412
413    #[test]
414    fn parse_known_numeric() {
415        let cnpj = cnpj_numeric();
416        assert_eq!(cnpj.as_str(), "11222333000181");
417        assert_eq!(cnpj.kind(), CnpjKind::Numeric);
418    }
419
420    #[test]
421    fn parse_known_numeric_b() {
422        let cnpj = cnpj_numeric_b();
423        assert_eq!(cnpj.as_str(), "11444777000161");
424        assert_eq!(cnpj.kind(), CnpjKind::Numeric);
425    }
426
427    #[test]
428    fn validate_known_numeric() {
429        assert!(is_valid("11.222.333/0001-81"));
430        assert!(is_valid("11222333000181"));
431        assert!(is_valid("11.444.777/0001-61"));
432        assert!(is_valid("11444777000161"));
433    }
434
435    #[test]
436    fn parse_alphanumeric() {
437        let cnpj = cnpj_alpha();
438        assert_eq!(cnpj.kind(), CnpjKind::Alphanumeric);
439        assert!(is_valid(cnpj.as_str()));
440    }
441
442    #[test]
443    fn validate_alphanumeric_formatted() {
444        let cnpj = cnpj_alpha();
445        let formatted = cnpj.to_string();
446        assert!(is_valid_strict(&formatted).is_ok());
447    }
448
449    #[test]
450    fn is_valid_lenient_strips_garbage() {
451        let cnpj = cnpj_numeric();
452        let s = cnpj.as_str();
453        let garbage = alloc::format!("{}${}#{}!{}", &s[0..2], &s[2..5], &s[5..8], &s[8..14]);
454        assert!(is_valid(&garbage));
455    }
456
457    #[test]
458    fn is_valid_lenient_lowercase_to_uppercase() {
459        let cnpj = cnpj_alpha();
460        let lower = cnpj.as_str().to_lowercase();
461        assert!(is_valid(&lower));
462    }
463
464    #[test]
465    fn is_valid_rejects_bad_check_digits() {
466        let mut bytes = *b"11222333000182"; // last digit wrong
467        assert!(!is_valid(core::str::from_utf8(&bytes).unwrap()));
468        bytes = *b"11222333000191"; // first check digit wrong
469        assert!(!is_valid(core::str::from_utf8(&bytes).unwrap()));
470    }
471
472    #[test]
473    fn is_valid_rejects_all_equal() {
474        assert!(!is_valid("11111111111111"));
475        assert!(!is_valid("00000000000000"));
476        assert!(!is_valid("AAAAAAAAAAAAAA"));
477    }
478
479    #[test]
480    fn is_valid_rejects_wrong_length() {
481        assert!(!is_valid(""));
482        assert!(!is_valid("1234567890123"));
483        assert!(!is_valid("123456789012345"));
484    }
485
486    #[test]
487    fn is_valid_rejects_invalid_chars() {
488        assert!(!is_valid("1122233300018!")); // special char in check digit pos
489    }
490
491    #[test]
492    fn strict_accepts_valid_unformatted() {
493        assert!(is_valid_strict("11222333000181").is_ok());
494        assert!(is_valid_strict("11444777000161").is_ok());
495    }
496
497    #[test]
498    fn strict_accepts_valid_formatted() {
499        assert!(is_valid_strict("11.222.333/0001-81").is_ok());
500        assert!(is_valid_strict("11.444.777/0001-61").is_ok());
501    }
502
503    #[test]
504    fn strict_rejects_garbage() {
505        assert!(is_valid_strict("11$222$333$0001$81").is_err());
506    }
507
508    #[test]
509    fn strict_rejects_whitespace() {
510        // 18 chars matches formatted-length path, but separators are wrong
511        assert_eq!(
512            is_valid_strict("  11222333000181  "),
513            Err(CnpjError::InvalidFormat)
514        );
515        // Other lengths
516        assert_eq!(
517            is_valid_strict(" 11222333000181"),
518            Err(CnpjError::InvalidLength)
519        );
520    }
521
522    #[test]
523    fn strict_rejects_misplaced_separators() {
524        assert!(is_valid_strict("112.223.330/0018-1").is_err());
525    }
526
527    #[test]
528    fn strict_rejects_lowercase() {
529        let cnpj = cnpj_alpha();
530        let lower = cnpj.as_str().to_lowercase();
531        assert_eq!(is_valid_strict(&lower), Err(CnpjError::InvalidCharacter));
532    }
533
534    #[test]
535    fn strict_rejects_all_equal() {
536        assert_eq!(
537            is_valid_strict("11111111111111"),
538            Err(CnpjError::AllCharsEqual)
539        );
540        assert_eq!(
541            is_valid_strict("00.000.000/0000-00"),
542            Err(CnpjError::AllCharsEqual)
543        );
544    }
545
546    #[test]
547    fn strict_rejects_invalid_check_digits() {
548        assert_eq!(
549            is_valid_strict("11222333000182"),
550            Err(CnpjError::InvalidCheckDigits)
551        );
552    }
553
554    #[test]
555    fn parse_roundtrip_formatted() {
556        let cnpj = cnpj_numeric();
557        let parsed: Cnpj = cnpj.to_string().parse().unwrap();
558        assert_eq!(cnpj, parsed);
559    }
560
561    #[test]
562    fn parse_roundtrip_raw() {
563        let cnpj = cnpj_numeric();
564        let parsed: Cnpj = cnpj.as_str().parse().unwrap();
565        assert_eq!(cnpj, parsed);
566    }
567
568    #[test]
569    fn parse_roundtrip_alphanumeric() {
570        let cnpj = cnpj_alpha();
571        let from_raw: Cnpj = cnpj.as_str().parse().unwrap();
572        let from_fmt: Cnpj = cnpj.to_string().parse().unwrap();
573        assert_eq!(cnpj, from_raw);
574        assert_eq!(cnpj, from_fmt);
575    }
576
577    #[test]
578    fn accessor_kind() {
579        assert_eq!(cnpj_numeric().kind(), CnpjKind::Numeric);
580        assert_eq!(cnpj_alpha().kind(), CnpjKind::Alphanumeric);
581    }
582
583    #[test]
584    fn accessor_raiz() {
585        assert_eq!(cnpj_numeric().raiz(), "11222333");
586    }
587
588    #[test]
589    fn accessor_ordem() {
590        assert_eq!(cnpj_numeric().ordem(), "0001");
591    }
592
593    #[test]
594    fn accessor_establishment_type() {
595        assert_eq!(
596            cnpj_numeric().establishment_type(),
597            EstablishmentType::Matriz
598        );
599        // Build one with ordem != "0001"
600        let filial = make_cnpj(b"112223330002");
601        assert_eq!(filial.establishment_type(), EstablishmentType::Filial);
602    }
603
604    #[test]
605    fn accessor_check_digits() {
606        let cnpj = cnpj_numeric();
607        let (d1, d2) = cnpj.check_digits();
608        assert_eq!(d1, 8);
609        assert_eq!(d2, 1);
610    }
611
612    #[test]
613    fn accessor_masked() {
614        let cnpj = cnpj_numeric();
615        assert_eq!(cnpj.masked(), "11.222.333/****-**");
616    }
617
618    #[test]
619    fn format_cnpj_produces_formatted_output() {
620        assert_eq!(
621            format_cnpj("11222333000181"),
622            Some("11.222.333/0001-81".to_string())
623        );
624    }
625
626    #[test]
627    fn format_cnpj_preserves_letters() {
628        let cnpj = cnpj_alpha();
629        let formatted = format_cnpj(cnpj.as_str()).unwrap();
630        assert!(formatted.contains('/'));
631        assert!(formatted.contains('-'));
632        let reparsed: Cnpj = formatted.parse().unwrap();
633        assert_eq!(cnpj, reparsed);
634    }
635
636    #[test]
637    fn format_cnpj_returns_none_on_bad_length() {
638        assert_eq!(format_cnpj("1234"), None);
639        assert_eq!(format_cnpj(""), None);
640    }
641
642    #[test]
643    fn remove_symbols_strips_formatting() {
644        assert_eq!(remove_symbols("11.222.333/0001-81"), "11222333000181");
645    }
646
647    #[test]
648    fn remove_symbols_preserves_letters_and_uppercases() {
649        assert_eq!(remove_symbols("12.abc.345/00de-XX"), "12ABC34500DEXX");
650    }
651
652    #[test]
653    fn generate_numeric_produces_valid() {
654        for _ in 0..100 {
655            let cnpj = generate(CnpjKind::Numeric);
656            assert_eq!(cnpj.len(), 14);
657            assert!(is_valid(&cnpj), "generated invalid CNPJ: {cnpj}");
658            let parsed: Cnpj = cnpj.parse().unwrap();
659            assert_eq!(parsed.kind(), CnpjKind::Numeric);
660        }
661    }
662
663    #[test]
664    fn generate_alphanumeric_produces_valid() {
665        for _ in 0..100 {
666            let cnpj = generate(CnpjKind::Alphanumeric);
667            assert_eq!(cnpj.len(), 14);
668            assert!(is_valid(&cnpj), "generated invalid CNPJ: {cnpj}");
669        }
670    }
671
672    #[test]
673    fn generate_cnpj_roundtrips() {
674        for _ in 0..100 {
675            let cnpj = generate_cnpj(CnpjKind::Numeric);
676            assert!(is_valid(cnpj.as_str()));
677            let parsed: Cnpj = cnpj.as_str().parse().unwrap();
678            assert_eq!(cnpj, parsed);
679        }
680    }
681
682    #[test]
683    fn generate_matriz_has_correct_ordem_and_type() {
684        for _ in 0..20 {
685            let cnpj = generate_matriz(CnpjKind::Numeric);
686            assert_eq!(cnpj.ordem(), "0001");
687            assert_eq!(cnpj.establishment_type(), EstablishmentType::Matriz);
688            assert!(is_valid(cnpj.as_str()));
689        }
690        for _ in 0..20 {
691            let cnpj = generate_matriz(CnpjKind::Alphanumeric);
692            assert_eq!(cnpj.ordem(), "0001");
693            assert_eq!(cnpj.establishment_type(), EstablishmentType::Matriz);
694            assert!(is_valid(cnpj.as_str()));
695        }
696    }
697
698    #[test]
699    fn compute_check_digits_known_base() {
700        let (d1, d2) = compute_check_digits("112223330001").unwrap();
701        assert_eq!(d1, 8);
702        assert_eq!(d2, 1);
703    }
704
705    #[test]
706    fn compute_check_digits_alphanumeric_base() {
707        let cnpj = cnpj_alpha();
708        let base = &cnpj.as_str()[..12];
709        let (d1, d2) = compute_check_digits(base).unwrap();
710        assert_eq!(d1, cnpj.check_digits().0);
711        assert_eq!(d2, cnpj.check_digits().1);
712    }
713
714    #[test]
715    fn compute_check_digits_rejects_bad_input() {
716        assert_eq!(compute_check_digits("12345678901"), None); // too short
717        assert_eq!(compute_check_digits("1234567890123"), None); // too long
718        assert_eq!(compute_check_digits("000000000000"), None); // all equal
719    }
720
721    #[test]
722    fn cnpj_is_copy() {
723        let a = cnpj_numeric();
724        let b = a;
725        assert_eq!(a, b);
726    }
727
728    #[test]
729    fn cnpj_as_ref_str() {
730        let cnpj = cnpj_numeric();
731        let r: &str = cnpj.as_ref();
732        assert_eq!(r, cnpj.as_str());
733    }
734
735    #[test]
736    fn debug_format() {
737        let cnpj = cnpj_numeric();
738        let dbg = alloc::format!("{cnpj:?}");
739        assert!(dbg.starts_with("Cnpj("));
740        assert!(dbg.ends_with(')'));
741        assert!(dbg.contains('.'));
742        assert!(dbg.contains('/'));
743        assert!(dbg.contains('-'));
744    }
745
746    #[test]
747    fn display_format() {
748        let cnpj = cnpj_numeric();
749        assert_eq!(cnpj.to_string(), "11.222.333/0001-81");
750    }
751
752    #[test]
753    fn from_str_trait() {
754        let cnpj: Cnpj = "11.222.333/0001-81".parse().unwrap();
755        assert_eq!(cnpj.as_str(), "11222333000181");
756    }
757
758    #[test]
759    fn all_zeros_rejected() {
760        assert!(!is_valid("00000000000000"));
761        assert_eq!(
762            is_valid_strict("00000000000000"),
763            Err(CnpjError::AllCharsEqual)
764        );
765    }
766
767    #[test]
768    fn leading_zeros() {
769        // 00.623.904/0001-73 is a valid CNPJ with leading zeros
770        let result = is_valid("00623904000173");
771        // It's valid only if check digits match - let's compute
772        if let Some((d1, d2)) = compute_check_digits("006239040001") {
773            let cnpj_str = alloc::format!("006239040001{d1}{d2}");
774            assert!(is_valid(&cnpj_str));
775            let cnpj: Cnpj = cnpj_str.parse().unwrap();
776            assert!(cnpj.as_str().starts_with("00"));
777        } else {
778            // base might be all-equal or invalid, skip
779            let _ = result;
780        }
781    }
782
783    #[test]
784    fn char_value_mapping() {
785        assert_eq!(char_value(b'0'), 0);
786        assert_eq!(char_value(b'9'), 9);
787        assert_eq!(char_value(b'A'), 17);
788        assert_eq!(char_value(b'Z'), 42);
789    }
790}