Skip to main content

fiscal_core/xml_builder/
access_key.rs

1//! Access key (chave de acesso) generation for NF-e/NFC-e.
2//!
3//! An access key is a 44-digit numeric string that uniquely identifies
4//! a Brazilian fiscal document. Layout:
5//!
6//! ```text
7//! cUF(2) + AAMM(4) + CNPJ(14) + mod(2) + serie(3) + nNF(9)
8//! + tpEmis(1) + cNF(8) + cDV(1) = 44 digits
9//! ```
10
11use crate::FiscalError;
12use crate::types::AccessKeyParams;
13
14/// Build the 44-digit access key from its component parts.
15///
16/// Concatenates all parts with proper padding, computes the mod-11 check
17/// digit, and returns the complete 44-digit key.
18///
19/// # Errors
20///
21/// Returns [`FiscalError::XmlGeneration`] if the resulting base does not
22/// have exactly 43 digits (indicating malformed input parameters).
23pub fn build_access_key(params: &AccessKeyParams) -> Result<String, FiscalError> {
24    let base = format!(
25        "{cuf:0>2}{aamm}{cnpj:0>14}{model:0>2}{serie:0>3}{nnf:0>9}{tp_emis}{cnf:0>8}",
26        cuf = params.state_code,
27        aamm = params.year_month,
28        cnpj = params.tax_id,
29        model = params.model.as_str(),
30        serie = params.series,
31        nnf = params.number,
32        tp_emis = params.emission_type.as_str(),
33        cnf = params.numeric_code,
34    );
35
36    if base.len() != 43 {
37        return Err(FiscalError::XmlGeneration(format!(
38            "Access key base must be 43 digits, got {} (\"{}\")",
39            base.len(),
40            base
41        )));
42    }
43
44    let check_digit = calculate_mod11(&base);
45    Ok(format!("{base}{check_digit}"))
46}
47
48/// Calculate the mod-11 check digit used in Brazilian fiscal documents.
49///
50/// Weights cycle 2→9 from right to left. If the remainder after `% 11`
51/// is less than 2 the digit is 0; otherwise it is `11 - remainder`.
52pub fn calculate_mod11(digits: &str) -> u8 {
53    let mut sum: u32 = 0;
54    let mut weight: u32 = 2;
55
56    for ch in digits.bytes().rev() {
57        let val = (ch - b'0') as u32;
58        sum += val * weight;
59        weight = if weight >= 9 { 2 } else { weight + 1 };
60    }
61
62    let remainder = sum % 11;
63    if remainder < 2 {
64        0
65    } else {
66        (11 - remainder) as u8
67    }
68}
69
70/// Generate an 8-digit random numeric code for the access key.
71pub fn generate_numeric_code() -> String {
72    use std::time::{SystemTime, UNIX_EPOCH};
73    let nanos = SystemTime::now()
74        .duration_since(UNIX_EPOCH)
75        .unwrap_or_default()
76        .subsec_nanos();
77    let code = (nanos ^ (nanos >> 16)) % 100_000_000;
78    format!("{code:08}")
79}
80
81/// Format a `DateTime<FixedOffset>` as YYMM for the access key.
82pub fn format_year_month(dt: &chrono::DateTime<chrono::FixedOffset>) -> String {
83    format!("{}{:02}", &dt.format("%y"), dt.format("%m"))
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn mod11_known_values() {
92        let base = "4325030412345678901255001000000001100000001";
93        let dv = calculate_mod11(base);
94        let full = format!("{base}{dv}");
95        assert_eq!(full.len(), 44);
96    }
97
98    #[test]
99    fn mod11_all_zeros() {
100        let dv = calculate_mod11("0000000000000000000000000000000000000000000");
101        assert_eq!(dv, 0);
102    }
103}