mod build_det;
mod build_header;
mod build_optional;
mod build_parties;
mod build_tax;
mod build_totals;
mod build_xml;
mod helpers;
mod parser;
mod structures;
mod types;
use std::collections::HashMap;
use crate::FiscalError;
use helpers::validate_txt_lines;
use parser::NFeParser;
use structures::*;
pub(super) fn get_structure(
version: &str,
layout: &str,
) -> Result<HashMap<&'static str, &'static str>, FiscalError> {
let ver: u32 = version.replace('.', "").parse().unwrap_or(0);
let lay = layout.to_uppercase();
if ver == 310 {
return Ok(structure_310());
}
if ver == 400 {
if lay == "SEBRAE" {
return Ok(structure_400_sebrae());
}
if lay == "LOCAL_V12" {
return Ok(structure_400_v12());
}
if lay == "LOCAL_V13" {
return Ok(structure_400_v13());
}
return Ok(structure_400());
}
Err(FiscalError::InvalidTxt(format!(
"Structure definition for TXT layout version {version} ({layout}) was not found."
)))
}
pub fn txt_to_xml(txt: &str, layout: &str) -> Result<String, FiscalError> {
let mut xmls = txt_to_xml_all(txt, layout)?;
Ok(xmls.swap_remove(0))
}
pub fn txt_to_xml_all(txt: &str, layout: &str) -> Result<Vec<String>, FiscalError> {
let txt = txt.trim();
if txt.is_empty() {
return Err(FiscalError::WrongDocument("Empty document".into()));
}
let lines: Vec<&str> = txt.lines().collect();
let first_fields: Vec<&str> = lines[0].split('|').collect();
if first_fields[0] != "NOTAFISCAL" {
return Err(FiscalError::WrongDocument(
"Wrong document: not a valid NFe TXT".into(),
));
}
let declared_count: usize = first_fields
.get(1)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let rest: Vec<&str> = lines[1..].to_vec();
let invoices = slice_invoices(&rest, declared_count);
if invoices.len() != declared_count {
return Err(FiscalError::WrongDocument(format!(
"Number of NFe declared ({declared_count}) does not match found ({})",
invoices.len()
)));
}
let norm_layout = normalize_layout(layout);
let mut xmls = Vec::with_capacity(declared_count);
for invoice in &invoices {
let version = extract_layout_version(invoice)?;
let errors = validate_txt_lines(invoice, &norm_layout);
if !errors.is_empty() {
return Err(FiscalError::InvalidTxt(errors.join("\n")));
}
let structure = get_structure(&version, &norm_layout)?;
let mut parser = NFeParser::new(&version, &norm_layout, &structure);
parser.parse(invoice);
if !parser.inf_nfe_id.is_empty() {
let key = parser
.inf_nfe_id
.strip_prefix("NFe")
.unwrap_or(&parser.inf_nfe_id);
if !key.is_empty() && key.len() != 44 {
return Err(FiscalError::InvalidTxt(format!(
"A chave informada est\u{e1} incorreta [{}]",
parser.inf_nfe_id
)));
}
}
xmls.push(parser.build_xml());
}
Ok(xmls)
}
pub fn validate_txt(txt: &str, layout: &str) -> Result<bool, FiscalError> {
let txt = txt.replace(['\r', '\t'], "");
let txt = txt.trim();
if txt.is_empty() {
return Err(FiscalError::WrongDocument("Empty document".into()));
}
let lines: Vec<&str> = txt.lines().collect();
let first_fields: Vec<&str> = lines[0].split('|').collect();
if first_fields[0] != "NOTAFISCAL" {
return Err(FiscalError::WrongDocument(
"Wrong document: not a valid NFe TXT".into(),
));
}
let rest: Vec<&str> = lines[1..].to_vec();
let norm_layout = normalize_layout(layout);
let errors = validate_txt_lines(&rest, &norm_layout);
if errors.is_empty() {
Ok(true)
} else {
Ok(false)
}
}
fn normalize_layout(layout: &str) -> String {
let up = layout.to_uppercase();
match up.as_str() {
"LOCAL" | "LOCAL_V12" | "LOCAL_V13" | "SEBRAE" => up,
_ => "LOCAL_V12".to_string(),
}
}
fn slice_invoices<'a>(rest: &[&'a str], declared: usize) -> Vec<Vec<&'a str>> {
if declared <= 1 {
return vec![rest.to_vec()];
}
let mut markers: Vec<(usize, usize)> = Vec::new();
for (i, line) in rest.iter().enumerate() {
if line.starts_with("A|") {
if let Some(last) = markers.last_mut() {
last.1 = i;
}
markers.push((i, 0));
}
}
if let Some(last) = markers.last_mut() {
last.1 = rest.len();
}
markers.iter().map(|(s, e)| rest[*s..*e].to_vec()).collect()
}
fn extract_layout_version(invoice: &[&str]) -> Result<String, FiscalError> {
for line in invoice {
let fields: Vec<&str> = line.split('|').collect();
if fields[0] == "A" {
return Ok(fields.get(1).unwrap_or(&"4.00").to_string());
}
}
Err(FiscalError::InvalidTxt(
"No 'A' entity found in invoice".into(),
))
}