1use super::tax_id::TaxId;
4use crate::types::{InvoiceBuildData, InvoiceModel, ReferenceDoc};
5use crate::xml_utils::{TagContent, tag};
6
7pub fn format_datetime_nfe(dt: &chrono::DateTime<chrono::FixedOffset>, state_code: &str) -> String {
11 let offset = match state_code {
12 "AC" => "-05:00",
13 "AM" | "RO" | "RR" | "MT" | "MS" => "-04:00",
14 _ => "-03:00",
15 };
16 format!("{}{offset}", dt.format("%Y-%m-%dT%H:%M:%S"))
17}
18
19pub(crate) fn build_ide(
21 data: &InvoiceBuildData,
22 state_ibge: &str,
23 numeric_code: &str,
24 access_key: &str,
25) -> String {
26 let ref_elements = build_references(data.references.as_deref());
27
28 let dh_emi = format_datetime_nfe(&data.issued_at, &data.issuer.state_code);
29 let series_str = data.series.to_string();
30 let number_str = data.number.to_string();
31 let tp_nf = data.operation_type.unwrap_or(1).to_string();
32 let fin_nfe = data.purpose_code.unwrap_or(1).to_string();
33
34 let mut children = vec![
35 tag("cUF", &[], TagContent::Text(state_ibge)),
36 tag("cNF", &[], TagContent::Text(numeric_code)),
37 tag("natOp", &[], TagContent::Text(&data.operation_nature)),
38 tag("mod", &[], TagContent::Text(data.model.as_str())),
39 tag("serie", &[], TagContent::Text(&series_str)),
40 tag("nNF", &[], TagContent::Text(&number_str)),
41 tag("dhEmi", &[], TagContent::Text(&dh_emi)),
42 ];
43
44 if data.model == InvoiceModel::Nfe {
46 if let Some(ref exit_dt) = data.exit_at {
47 let dh_sai_ent = format_datetime_nfe(exit_dt, &data.issuer.state_code);
48 children.push(tag("dhSaiEnt", &[], TagContent::Text(&dh_sai_ent)));
49 }
50 }
51
52 children.extend([
53 tag("tpNF", &[], TagContent::Text(&tp_nf)),
54 tag(
55 "idDest",
56 &[],
57 TagContent::Text(data.destination_indicator.as_deref().unwrap_or("1")),
58 ),
59 tag("cMunFG", &[], TagContent::Text(&data.issuer.city_code.0)),
60 tag(
61 "tpImp",
62 &[],
63 TagContent::Text(data.print_format.as_deref().unwrap_or("1")),
64 ),
65 tag("tpEmis", &[], TagContent::Text(data.emission_type.as_str())),
66 tag("cDV", &[], TagContent::Text(&access_key[43..44])),
67 tag("tpAmb", &[], TagContent::Text(data.environment.as_str())),
68 tag("finNFe", &[], TagContent::Text(&fin_nfe)),
69 tag(
70 "indFinal",
71 &[],
72 TagContent::Text(data.consumer_type.as_deref().unwrap_or("0")),
73 ),
74 tag(
75 "indPres",
76 &[],
77 TagContent::Text(data.buyer_presence.as_deref().unwrap_or("0")),
78 ),
79 ]);
80
81 if let Some(ref ind) = data.intermediary_indicator {
84 children.push(tag("indIntermed", &[], TagContent::Text(ind)));
85 }
86
87 children.extend([
88 tag(
89 "procEmi",
90 &[],
91 TagContent::Text(data.emission_process.as_deref().unwrap_or("0")),
92 ),
93 tag(
94 "verProc",
95 &[],
96 TagContent::Text(data.ver_proc.as_deref().unwrap_or("FinOpenPOS 1.0")),
97 ),
98 ]);
99
100 if let Some(ref cont) = data.contingency {
102 let dh_cont = format_datetime_nfe(&cont.at, &data.issuer.state_code);
103 children.push(tag("dhCont", &[], TagContent::Text(&dh_cont)));
104 children.push(tag("xJust", &[], TagContent::Text(&cont.reason)));
105 }
106
107 children.extend(ref_elements);
108
109 if data.schema_version.is_pl010() {
112 if let Some(ref cg) = data.compra_gov {
113 children.push(super::optional::build_compra_gov(cg));
114 }
115 if let Some(ref pa) = data.pag_antecipado {
116 children.push(super::optional::build_pag_antecipado(pa));
117 }
118 }
119
120 tag("ide", &[], TagContent::Children(children))
121}
122
123fn build_references(references: Option<&[ReferenceDoc]>) -> Vec<String> {
125 let Some(refs) = references else {
126 return vec![];
127 };
128
129 refs.iter()
130 .map(|r| match r {
131 ReferenceDoc::Nfe { access_key } => tag(
132 "NFref",
133 &[],
134 TagContent::Children(vec![tag("refNFe", &[], TagContent::Text(access_key))]),
135 ),
136 ReferenceDoc::NfeSig { access_key } => tag(
137 "NFref",
138 &[],
139 TagContent::Children(vec![tag("refNFeSig", &[], TagContent::Text(access_key))]),
140 ),
141 ReferenceDoc::Nf {
142 state_code,
143 year_month,
144 tax_id,
145 model,
146 series,
147 number,
148 } => tag(
149 "NFref",
150 &[],
151 TagContent::Children(vec![tag(
152 "refNF",
153 &[],
154 TagContent::Children(vec![
155 tag("cUF", &[], TagContent::Text(&state_code.0)),
156 tag("AAMM", &[], TagContent::Text(year_month)),
157 tag("CNPJ", &[], TagContent::Text(tax_id)),
158 tag("mod", &[], TagContent::Text(model)),
159 tag("serie", &[], TagContent::Text(series)),
160 tag("nNF", &[], TagContent::Text(number)),
161 ]),
162 )]),
163 ),
164 ReferenceDoc::Nfp {
165 state_code,
166 year_month,
167 tax_id,
168 ie,
169 model,
170 series,
171 number,
172 } => {
173 let tid = TaxId::new(tax_id);
174 tag(
175 "NFref",
176 &[],
177 TagContent::Children(vec![tag(
178 "refNFP",
179 &[],
180 TagContent::Children(vec![
181 tag("cUF", &[], TagContent::Text(&state_code.0)),
182 tag("AAMM", &[], TagContent::Text(year_month)),
183 tag(tid.tag_name(), &[], TagContent::Text(tax_id)),
184 tag("IE", &[], TagContent::Text(ie)),
185 tag("mod", &[], TagContent::Text(model)),
186 tag("serie", &[], TagContent::Text(series)),
187 tag("nNF", &[], TagContent::Text(number)),
188 ]),
189 )]),
190 )
191 }
192 ReferenceDoc::Cte { access_key } => tag(
193 "NFref",
194 &[],
195 TagContent::Children(vec![tag("refCTe", &[], TagContent::Text(access_key))]),
196 ),
197 ReferenceDoc::Ecf {
198 model,
199 ecf_number,
200 coo_number,
201 } => tag(
202 "NFref",
203 &[],
204 TagContent::Children(vec![tag(
205 "refECF",
206 &[],
207 TagContent::Children(vec![
208 tag("mod", &[], TagContent::Text(model)),
209 tag("nECF", &[], TagContent::Text(ecf_number)),
210 tag("nCOO", &[], TagContent::Text(coo_number)),
211 ]),
212 )]),
213 ),
214 })
215 .collect()
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn build_references_nfe_emits_ref_nfe() {
224 let refs = vec![ReferenceDoc::Nfe {
225 access_key: "41260304123456000190550010000001231123456780".to_string(),
226 }];
227 let result = build_references(Some(&refs));
228 assert_eq!(result.len(), 1);
229 assert_eq!(
230 result[0],
231 "<NFref><refNFe>41260304123456000190550010000001231123456780</refNFe></NFref>"
232 );
233 }
234
235 #[test]
236 fn build_references_nfe_sig_emits_ref_nfe_sig() {
237 let refs = vec![ReferenceDoc::NfeSig {
238 access_key: "41260304123456000190550010000001231123456780".to_string(),
239 }];
240 let result = build_references(Some(&refs));
241 assert_eq!(result.len(), 1);
242 assert_eq!(
243 result[0],
244 "<NFref><refNFeSig>41260304123456000190550010000001231123456780</refNFeSig></NFref>"
245 );
246 }
247
248 #[test]
249 fn build_references_none_returns_empty() {
250 let result = build_references(None);
251 assert!(result.is_empty());
252 }
253
254 #[test]
255 fn build_references_nfe_and_nfe_sig_are_independent() {
256 let refs = vec![
257 ReferenceDoc::Nfe {
258 access_key: "11111111111111111111111111111111111111111111".to_string(),
259 },
260 ReferenceDoc::NfeSig {
261 access_key: "22222222222222222222222222222222222222222222".to_string(),
262 },
263 ];
264 let result = build_references(Some(&refs));
265 assert_eq!(result.len(), 2);
266 assert!(result[0].contains("<refNFe>"));
267 assert!(!result[0].contains("<refNFeSig>"));
268 assert!(result[1].contains("<refNFeSig>"));
269 assert!(!result[1].contains("<refNFe>"));
270 }
271}