Skip to main content

fiscal_core/complement/
mod.rs

1//! Functions for attaching SEFAZ authorization protocols to signed XML documents.
2//!
3//! Each submodule handles one type of protocol attachment:
4//! - [`protocol`] — NFe authorization (`<nfeProc>`)
5//! - [`inutilizacao`] — Number voiding (`<ProcInutNFe>`)
6//! - [`event`] — Event protocol (`<procEventoNFe>`)
7//! - [`b2b`] — B2B financial wrapper (`<nfeProcB2B>`)
8//! - [`cancellation`] — Cancellation event attachment
9//! - [`helpers`] — Internal XML parsing utilities
10
11mod b2b;
12mod cancellation;
13mod event;
14mod helpers;
15mod inutilizacao;
16mod protocol;
17
18pub use b2b::attach_b2b;
19pub use cancellation::attach_cancellation;
20pub use event::attach_event_protocol;
21pub use inutilizacao::attach_inutilizacao;
22pub use protocol::attach_protocol;
23
24use crate::FiscalError;
25use helpers::contains_xml_tag;
26
27// ── Unified routing (mirrors PHP Complements::toAuthorize) ──────────────────
28
29/// Detect the document type from raw XML and dispatch to the correct
30/// protocol-attachment function.
31///
32/// This mirrors the PHP `Complements::toAuthorize()` method, which uses
33/// `Standardize::whichIs()` internally. The detection logic checks for
34/// the same root tags in the same priority order as the PHP implementation:
35///
36/// | Detected tag    | Dispatches to                  |
37/// |-----------------|-------------------------------|
38/// | `NFe`           | [`attach_protocol`]           |
39/// | `envEvento`     | [`attach_event_protocol`]     |
40/// | `inutNFe`       | [`attach_inutilizacao`]       |
41///
42/// # Errors
43///
44/// Returns [`FiscalError::XmlParsing`] if:
45/// - Either input is empty
46/// - The request XML does not match any of the known document types
47/// - The delegated function returns an error
48pub fn to_authorize(request_xml: &str, response_xml: &str) -> Result<String, FiscalError> {
49    if request_xml.is_empty() {
50        return Err(FiscalError::XmlParsing(
51            "Erro ao protocolar: o XML a protocolar está vazio.".into(),
52        ));
53    }
54    if response_xml.is_empty() {
55        return Err(FiscalError::XmlParsing(
56            "Erro ao protocolar: o retorno da SEFAZ está vazio.".into(),
57        ));
58    }
59
60    // Detect using the same tag order as PHP Standardize::whichIs() + the
61    // ucfirst() / if-check in toAuthorize().
62    // PHP checks: whichIs() returns the root tag name from rootTagList,
63    // then toAuthorize() accepts only "NFe", "EnvEvento", "InutNFe".
64    // We search for these tags in the XML content:
65    if contains_xml_tag(request_xml, "NFe") {
66        attach_protocol(request_xml, response_xml)
67    } else if contains_xml_tag(request_xml, "envEvento") {
68        attach_event_protocol(request_xml, response_xml)
69    } else if contains_xml_tag(request_xml, "inutNFe") {
70        attach_inutilizacao(request_xml, response_xml)
71    } else {
72        Err(FiscalError::XmlParsing(
73            "Tipo de documento não reconhecido para protocolação".into(),
74        ))
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn to_authorize_empty_request_returns_error() {
84        let err = to_authorize("", "<retEnviNFe/>").unwrap_err();
85        assert!(matches!(err, FiscalError::XmlParsing(_)));
86    }
87
88    #[test]
89    fn to_authorize_empty_response_returns_error() {
90        let err = to_authorize("<NFe/>", "").unwrap_err();
91        assert!(matches!(err, FiscalError::XmlParsing(_)));
92    }
93
94    #[test]
95    fn to_authorize_unrecognized_document_returns_error() {
96        let err = to_authorize("<other>data</other>", "<response/>").unwrap_err();
97        let msg = format!("{err}");
98        assert!(
99            msg.contains("não reconhecido"),
100            "should mention unrecognized type: {msg}"
101        );
102    }
103
104    // ── to_authorize: NFe path (line 428) ───────────────────────────────
105
106    #[test]
107    fn to_authorize_dispatches_nfe() {
108        let request = concat!(
109            r#"<NFe><infNFe versao="4.00" Id="NFe35260112345678000199550010000000011123456780">"#,
110            r#"<DigestValue>abc</DigestValue>"#,
111            r#"</infNFe></NFe>"#
112        );
113        let response = concat!(
114            r#"<protNFe versao="4.00"><infProt>"#,
115            r#"<cStat>100</cStat><xMotivo>OK</xMotivo>"#,
116            r#"<digVal>abc</digVal>"#,
117            r#"<chNFe>35260112345678000199550010000000011123456780</chNFe>"#,
118            r#"</infProt></protNFe>"#
119        );
120        let result = to_authorize(request, response).unwrap();
121        assert!(result.contains("<nfeProc"));
122    }
123
124    // ── to_authorize: envEvento path (line 430) ─────────────────────────
125
126    #[test]
127    fn to_authorize_dispatches_env_evento() {
128        let request = concat!(
129            r#"<envEvento><idLote>1</idLote>"#,
130            r#"<evento versao="1.00"><infEvento>"#,
131            r#"<tpEvento>110110</tpEvento>"#,
132            r#"</infEvento></evento></envEvento>"#
133        );
134        let response = concat!(
135            r#"<retEnvEvento><idLote>1</idLote>"#,
136            r#"<retEvento versao="1.00"><infEvento>"#,
137            r#"<cStat>135</cStat><xMotivo>OK</xMotivo>"#,
138            r#"<tpEvento>110110</tpEvento>"#,
139            r#"</infEvento></retEvento></retEnvEvento>"#
140        );
141        let result = to_authorize(request, response).unwrap();
142        assert!(result.contains("<procEventoNFe"));
143    }
144
145    // ── to_authorize: inutNFe path (line 432) ───────────────────────────
146
147    #[test]
148    fn to_authorize_dispatches_inut_nfe() {
149        let request = concat!(
150            r#"<inutNFe versao="4.00"><infInut>"#,
151            r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
152            r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>1</serie>"#,
153            r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
154            r#"</infInut></inutNFe>"#
155        );
156        let response = concat!(
157            r#"<retInutNFe versao="4.00"><infInut>"#,
158            r#"<cStat>102</cStat><xMotivo>Inutilizacao homologada</xMotivo>"#,
159            r#"<tpAmb>2</tpAmb><cUF>35</cUF><ano>26</ano>"#,
160            r#"<CNPJ>12345678000199</CNPJ><mod>55</mod><serie>1</serie>"#,
161            r#"<nNFIni>1</nNFIni><nNFFin>10</nNFFin>"#,
162            r#"</infInut></retInutNFe>"#
163        );
164        let result = to_authorize(request, response).unwrap();
165        assert!(result.contains("<ProcInutNFe"));
166    }
167}