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