fiscal_core/convert/
mod.rs1mod build_det;
6mod build_header;
7mod build_optional;
8mod build_parties;
9mod build_tax;
10mod build_totals;
11mod build_xml;
12mod helpers;
13mod parser;
14mod structures;
15mod types;
16
17use std::collections::HashMap;
18
19use crate::FiscalError;
20
21use helpers::validate_txt_lines;
22use parser::NFeParser;
23use structures::*;
24
25pub(super) fn get_structure(
34 version: &str,
35 layout: &str,
36) -> Result<HashMap<&'static str, &'static str>, FiscalError> {
37 let ver: u32 = version.replace('.', "").parse().unwrap_or(0);
38 let lay = layout.to_uppercase();
39
40 if ver == 310 {
41 return Ok(structure_310());
42 }
43 if ver == 400 {
44 if lay == "SEBRAE" {
45 return Ok(structure_400_sebrae());
46 }
47 if lay == "LOCAL_V12" {
48 return Ok(structure_400_v12());
49 }
50 if lay == "LOCAL_V13" {
51 return Ok(structure_400_v13());
52 }
53 return Ok(structure_400());
54 }
55
56 Err(FiscalError::InvalidTxt(format!(
57 "Structure definition for TXT layout version {version} ({layout}) was not found."
58 )))
59}
60
61pub fn txt_to_xml(txt: &str, layout: &str) -> Result<String, FiscalError> {
75 let mut xmls = txt_to_xml_all(txt, layout)?;
76 Ok(xmls.swap_remove(0))
78}
79
80pub fn txt_to_xml_all(txt: &str, layout: &str) -> Result<Vec<String>, FiscalError> {
97 let txt = txt.trim();
98 if txt.is_empty() {
99 return Err(FiscalError::WrongDocument("Empty document".into()));
100 }
101
102 let lines: Vec<&str> = txt.lines().collect();
103 let first_fields: Vec<&str> = lines[0].split('|').collect();
104 if first_fields[0] != "NOTAFISCAL" {
105 return Err(FiscalError::WrongDocument(
106 "Wrong document: not a valid NFe TXT".into(),
107 ));
108 }
109
110 let declared_count: usize = first_fields
111 .get(1)
112 .and_then(|s| s.parse().ok())
113 .unwrap_or(0);
114 let rest: Vec<&str> = lines[1..].to_vec();
115
116 let invoices = slice_invoices(&rest, declared_count);
118 if invoices.len() != declared_count {
119 return Err(FiscalError::WrongDocument(format!(
120 "Number of NFe declared ({declared_count}) does not match found ({})",
121 invoices.len()
122 )));
123 }
124
125 let norm_layout = normalize_layout(layout);
126 let mut xmls = Vec::with_capacity(declared_count);
127
128 for invoice in &invoices {
129 let version = extract_layout_version(invoice)?;
130
131 let errors = validate_txt_lines(invoice, &norm_layout);
133 if !errors.is_empty() {
134 return Err(FiscalError::InvalidTxt(errors.join("\n")));
135 }
136
137 let structure = get_structure(&version, &norm_layout)?;
139 let mut parser = NFeParser::new(&version, &norm_layout, &structure);
140 parser.parse(invoice);
141
142 if !parser.inf_nfe_id.is_empty() {
144 let key = parser
145 .inf_nfe_id
146 .strip_prefix("NFe")
147 .unwrap_or(&parser.inf_nfe_id);
148 if !key.is_empty() && key.len() != 44 {
149 return Err(FiscalError::InvalidTxt(format!(
150 "A chave informada est\u{e1} incorreta [{}]",
151 parser.inf_nfe_id
152 )));
153 }
154 }
155
156 xmls.push(parser.build_xml());
157 }
158
159 Ok(xmls)
160}
161
162pub fn validate_txt(txt: &str, layout: &str) -> Result<bool, FiscalError> {
172 let txt = txt.replace(['\r', '\t'], "");
173 let txt = txt.trim();
174 if txt.is_empty() {
175 return Err(FiscalError::WrongDocument("Empty document".into()));
176 }
177
178 let lines: Vec<&str> = txt.lines().collect();
179 let first_fields: Vec<&str> = lines[0].split('|').collect();
180 if first_fields[0] != "NOTAFISCAL" {
181 return Err(FiscalError::WrongDocument(
182 "Wrong document: not a valid NFe TXT".into(),
183 ));
184 }
185
186 let rest: Vec<&str> = lines[1..].to_vec();
187 let norm_layout = normalize_layout(layout);
188 let errors = validate_txt_lines(&rest, &norm_layout);
189
190 if errors.is_empty() {
191 Ok(true)
192 } else {
193 Ok(false)
194 }
195}
196
197fn normalize_layout(layout: &str) -> String {
200 let up = layout.to_uppercase();
201 match up.as_str() {
202 "LOCAL" | "LOCAL_V12" | "LOCAL_V13" | "SEBRAE" => up,
203 _ => "LOCAL_V12".to_string(),
204 }
205}
206
207fn slice_invoices<'a>(rest: &[&'a str], declared: usize) -> Vec<Vec<&'a str>> {
208 if declared <= 1 {
209 return vec![rest.to_vec()];
210 }
211
212 let mut markers: Vec<(usize, usize)> = Vec::new();
213 for (i, line) in rest.iter().enumerate() {
214 if line.starts_with("A|") {
215 if let Some(last) = markers.last_mut() {
216 last.1 = i;
217 }
218 markers.push((i, 0));
219 }
220 }
221 if let Some(last) = markers.last_mut() {
222 last.1 = rest.len();
223 }
224
225 markers.iter().map(|(s, e)| rest[*s..*e].to_vec()).collect()
226}
227
228fn extract_layout_version(invoice: &[&str]) -> Result<String, FiscalError> {
229 for line in invoice {
230 let fields: Vec<&str> = line.split('|').collect();
231 if fields[0] == "A" {
232 return Ok(fields.get(1).unwrap_or(&"4.00").to_string());
233 }
234 }
235 Err(FiscalError::InvalidTxt(
236 "No 'A' entity found in invoice".into(),
237 ))
238}