1use crate::FiscalError;
4use crate::xml_utils::extract_xml_tag_value;
5
6use super::helpers::{DEFAULT_VERSION, extract_attribute, extract_tag, join_xml};
7
8const EVT_CANCELA: &str = "110111";
10
11pub fn attach_event_protocol(request_xml: &str, response_xml: &str) -> Result<String, FiscalError> {
30 if request_xml.is_empty() {
31 return Err(FiscalError::XmlParsing("Event request XML is empty".into()));
32 }
33 if response_xml.is_empty() {
34 return Err(FiscalError::XmlParsing(
35 "Event response XML is empty".into(),
36 ));
37 }
38
39 let evento_content = extract_tag(request_xml, "evento").ok_or_else(|| {
40 FiscalError::XmlParsing("Could not find <evento> tag in request XML".into())
41 })?;
42
43 let ret_evento_content = extract_tag(response_xml, "retEvento").ok_or_else(|| {
44 FiscalError::XmlParsing("Could not find <retEvento> tag in response XML".into())
45 })?;
46
47 let version = extract_attribute(&evento_content, "evento", "versao")
49 .unwrap_or_else(|| DEFAULT_VERSION.to_string());
50
51 let c_stat = extract_xml_tag_value(&ret_evento_content, "cStat").unwrap_or_default();
53 let tp_evento = extract_xml_tag_value(&ret_evento_content, "tpEvento").unwrap_or_default();
54
55 let mut valid_statuses: Vec<&str> = vec!["135", "136"];
57 if tp_evento == EVT_CANCELA {
58 valid_statuses.push("155");
59 }
60
61 if !valid_statuses.contains(&c_stat.as_str()) {
62 let x_motivo = extract_xml_tag_value(&ret_evento_content, "xMotivo").unwrap_or_default();
63 return Err(FiscalError::SefazRejection {
64 code: c_stat,
65 message: x_motivo,
66 });
67 }
68
69 let req_id_lote = extract_xml_tag_value(request_xml, "idLote")
73 .ok_or_else(|| FiscalError::XmlParsing("idLote not found in request XML".into()))?;
74 let ret_id_lote = extract_xml_tag_value(response_xml, "idLote")
75 .ok_or_else(|| FiscalError::XmlParsing("idLote not found in response XML".into()))?;
76 if req_id_lote != ret_id_lote {
77 return Err(FiscalError::XmlParsing(
78 "Os números de lote dos documentos são diferentes".into(),
79 ));
80 }
81
82 Ok(join_xml(
83 &evento_content,
84 &ret_evento_content,
85 "procEventoNFe",
86 &version,
87 ))
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn attach_event_protocol_empty_request() {
96 let err = attach_event_protocol("", "<retEvento/>").unwrap_err();
97 assert!(matches!(err, FiscalError::XmlParsing(_)));
98 }
99
100 #[test]
101 fn attach_event_protocol_empty_response() {
102 let err = attach_event_protocol("<evento/>", "").unwrap_err();
103 assert!(matches!(err, FiscalError::XmlParsing(_)));
104 }
105
106 #[test]
107 fn attach_event_protocol_missing_evento() {
108 let err = attach_event_protocol(
109 "<other/>",
110 "<retEvento><infEvento><cStat>135</cStat></infEvento></retEvento>",
111 )
112 .unwrap_err();
113 assert!(matches!(err, FiscalError::XmlParsing(_)));
114 }
115
116 #[test]
117 fn attach_event_protocol_missing_ret_evento() {
118 let err =
119 attach_event_protocol(r#"<evento versao="1.00"><infEvento/></evento>"#, "<other/>")
120 .unwrap_err();
121 assert!(matches!(err, FiscalError::XmlParsing(_)));
122 }
123
124 #[test]
125 fn attach_event_protocol_rejected_status() {
126 let err = attach_event_protocol(
127 r#"<evento versao="1.00"><infEvento/></evento>"#,
128 r#"<retEvento><infEvento><cStat>999</cStat><xMotivo>Rejeitado</xMotivo></infEvento></retEvento>"#,
129 )
130 .unwrap_err();
131 assert!(matches!(err, FiscalError::SefazRejection { .. }));
132 }
133
134 #[test]
135 fn attach_event_protocol_success() {
136 let request = concat!(
137 r#"<envEvento><idLote>100</idLote>"#,
138 r#"<evento versao="1.00"><infEvento Id="ID1234"/></evento>"#,
139 r#"</envEvento>"#
140 );
141 let response = concat!(
142 r#"<retEnvEvento><idLote>100</idLote>"#,
143 r#"<retEvento><infEvento><cStat>135</cStat>"#,
144 r#"<xMotivo>Evento registrado</xMotivo>"#,
145 r#"</infEvento></retEvento></retEnvEvento>"#
146 );
147 let result = attach_event_protocol(request, response).unwrap();
148 assert!(result.contains("<procEventoNFe"));
149 assert!(result.contains("<evento"));
150 assert!(result.contains("<retEvento>"));
151 }
152
153 #[test]
156 fn attach_event_protocol_id_lote_mismatch() {
157 let request = concat!(
158 r#"<envEvento><idLote>100</idLote>"#,
159 r#"<evento versao="1.00"><infEvento>"#,
160 r#"<tpEvento>110110</tpEvento>"#,
161 r#"</infEvento></evento></envEvento>"#
162 );
163 let response = concat!(
164 r#"<retEnvEvento><idLote>999</idLote>"#,
165 r#"<retEvento versao="1.00"><infEvento>"#,
166 r#"<cStat>135</cStat><xMotivo>OK</xMotivo>"#,
167 r#"<tpEvento>110110</tpEvento>"#,
168 r#"</infEvento></retEvento></retEnvEvento>"#
169 );
170 let err = attach_event_protocol(request, response).unwrap_err();
171 match err {
172 FiscalError::XmlParsing(msg) => {
173 assert!(
174 msg.contains("lote"),
175 "Expected lote mismatch error, got: {msg}"
176 );
177 }
178 other => panic!("Expected XmlParsing, got {:?}", other),
179 }
180 }
181
182 #[test]
185 fn attach_event_protocol_both_invalid_reports_cstat_first() {
186 let request = concat!(
190 r#"<envEvento><idLote>100</idLote>"#,
191 r#"<evento versao="1.00"><infEvento>"#,
192 r#"<tpEvento>110110</tpEvento>"#,
193 r#"</infEvento></evento></envEvento>"#
194 );
195 let response = concat!(
196 r#"<retEnvEvento><idLote>999</idLote>"#,
197 r#"<retEvento versao="1.00"><infEvento>"#,
198 r#"<cStat>573</cStat><xMotivo>Duplicidade de evento</xMotivo>"#,
199 r#"<tpEvento>110110</tpEvento>"#,
200 r#"</infEvento></retEvento></retEnvEvento>"#
201 );
202 let err = attach_event_protocol(request, response).unwrap_err();
203 match err {
204 FiscalError::SefazRejection { code, message } => {
205 assert_eq!(code, "573");
206 assert_eq!(message, "Duplicidade de evento");
207 }
208 other => panic!("Expected SefazRejection (cStat first), got {:?}", other),
209 }
210 }
211
212 #[test]
215 fn attach_event_protocol_missing_id_lote_in_request() {
216 let request = concat!(
217 r#"<envEvento>"#,
218 r#"<evento versao="1.00"><infEvento>"#,
219 r#"<tpEvento>110110</tpEvento>"#,
220 r#"</infEvento></evento></envEvento>"#
221 );
222 let response = concat!(
223 r#"<retEnvEvento><idLote>100</idLote>"#,
224 r#"<retEvento versao="1.00"><infEvento>"#,
225 r#"<cStat>135</cStat><xMotivo>OK</xMotivo>"#,
226 r#"<tpEvento>110110</tpEvento>"#,
227 r#"</infEvento></retEvento></retEnvEvento>"#
228 );
229 let err = attach_event_protocol(request, response).unwrap_err();
230 match err {
231 FiscalError::XmlParsing(msg) => {
232 assert_eq!(msg, "idLote not found in request XML");
233 }
234 other => panic!("Expected XmlParsing, got {:?}", other),
235 }
236 }
237
238 #[test]
239 fn attach_event_protocol_missing_id_lote_in_response() {
240 let request = concat!(
241 r#"<envEvento><idLote>100</idLote>"#,
242 r#"<evento versao="1.00"><infEvento>"#,
243 r#"<tpEvento>110110</tpEvento>"#,
244 r#"</infEvento></evento></envEvento>"#
245 );
246 let response = concat!(
247 r#"<retEnvEvento>"#,
248 r#"<retEvento versao="1.00"><infEvento>"#,
249 r#"<cStat>135</cStat><xMotivo>OK</xMotivo>"#,
250 r#"<tpEvento>110110</tpEvento>"#,
251 r#"</infEvento></retEvento></retEnvEvento>"#
252 );
253 let err = attach_event_protocol(request, response).unwrap_err();
254 match err {
255 FiscalError::XmlParsing(msg) => {
256 assert_eq!(msg, "idLote not found in response XML");
257 }
258 other => panic!("Expected XmlParsing, got {:?}", other),
259 }
260 }
261
262 #[test]
263 fn attach_event_protocol_missing_id_lote_in_both() {
264 let request = concat!(
265 r#"<envEvento>"#,
266 r#"<evento versao="1.00"><infEvento>"#,
267 r#"<tpEvento>110110</tpEvento>"#,
268 r#"</infEvento></evento></envEvento>"#
269 );
270 let response = concat!(
271 r#"<retEnvEvento>"#,
272 r#"<retEvento versao="1.00"><infEvento>"#,
273 r#"<cStat>135</cStat><xMotivo>OK</xMotivo>"#,
274 r#"<tpEvento>110110</tpEvento>"#,
275 r#"</infEvento></retEvento></retEnvEvento>"#
276 );
277 let err = attach_event_protocol(request, response).unwrap_err();
278 match err {
279 FiscalError::XmlParsing(msg) => {
280 assert_eq!(msg, "idLote not found in request XML");
281 }
282 other => panic!("Expected XmlParsing, got {:?}", other),
283 }
284 }
285}