1use crate::FiscalError;
4use crate::format_utils::{format_cents, format_decimal, format_rate4};
5use crate::newtypes::{Cents, Rate, Rate4};
6use crate::tax_ibs_cbs;
7use crate::tax_icms::{self, IcmsCsosn, IcmsCst, IcmsTotals, IcmsVariant};
8use crate::tax_is;
9use crate::tax_issqn;
10use crate::tax_pis_cofins_ipi::{self, CofinsData, IiData, IpiData, PisData};
11use crate::types::{
12 CombData, InvoiceBuildData, InvoiceItemData, InvoiceModel, SefazEnvironment, TaxRegime,
13};
14use crate::xml_utils::{TagContent, tag};
15
16const HOMOLOGATION_XPROD: &str =
18 "NOTA FISCAL EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL";
19
20#[derive(Debug, Clone)]
22pub struct DetResult {
23 pub xml: String,
25 pub icms_totals: IcmsTotals,
27 pub v_ipi: i64,
29 pub v_pis: i64,
31 pub v_cofins: i64,
33 pub v_ii: i64,
35 pub v_frete: i64,
37 pub v_seg: i64,
39 pub v_desc: i64,
41 pub v_outro: i64,
43 pub ind_tot: u8,
45 pub v_tot_trib: i64,
47 pub v_ipi_devol: i64,
49 pub v_pis_st: i64,
51 pub v_cofins_st: i64,
53 pub ind_deduz_deson: bool,
55 pub has_issqn: bool,
57}
58
59fn build_icms_variant(
61 item: &InvoiceItemData,
62 is_simples: bool,
63) -> Result<IcmsVariant, FiscalError> {
64 let orig = item.orig.clone().unwrap_or_else(|| "0".to_string());
65
66 if is_simples {
67 let csosn_code = if item.icms_cst.is_empty() {
68 "102"
69 } else {
70 item.icms_cst.as_str()
71 };
72
73 let csosn = match csosn_code {
74 "101" => IcmsCsosn::Csosn101 {
75 orig,
76 csosn: csosn_code.to_string(),
77 p_cred_sn: item.icms_p_cred_sn.ok_or_else(|| {
78 FiscalError::MissingRequiredField {
79 field: "pCredSN".to_string(),
80 }
81 })?,
82 v_cred_icms_sn: item.icms_v_cred_icms_sn.ok_or_else(|| {
83 FiscalError::MissingRequiredField {
84 field: "vCredICMSSN".to_string(),
85 }
86 })?,
87 },
88 "102" => IcmsCsosn::Csosn102 {
89 orig,
90 csosn: csosn_code.to_string(),
91 },
92 "103" => IcmsCsosn::Csosn103 {
93 orig,
94 csosn: csosn_code.to_string(),
95 },
96 "300" => IcmsCsosn::Csosn300 {
97 orig,
98 csosn: csosn_code.to_string(),
99 },
100 "400" => IcmsCsosn::Csosn400 {
101 orig,
102 csosn: csosn_code.to_string(),
103 },
104 "201" => IcmsCsosn::Csosn201 {
105 orig,
106 csosn: csosn_code.to_string(),
107 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
108 FiscalError::MissingRequiredField {
109 field: "modBCST".to_string(),
110 }
111 })?,
112 p_mva_st: item.icms_p_mva_st,
113 p_red_bc_st: item.icms_red_bc_st,
114 v_bc_st: item
115 .icms_v_bc_st
116 .ok_or_else(|| FiscalError::MissingRequiredField {
117 field: "vBCST".to_string(),
118 })?,
119 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
120 FiscalError::MissingRequiredField {
121 field: "pICMSST".to_string(),
122 }
123 })?,
124 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
125 FiscalError::MissingRequiredField {
126 field: "vICMSST".to_string(),
127 }
128 })?,
129 v_bc_fcp_st: item.icms_v_bc_fcp_st,
130 p_fcp_st: item.icms_p_fcp_st,
131 v_fcp_st: item.icms_v_fcp_st,
132 p_cred_sn: item.icms_p_cred_sn,
133 v_cred_icms_sn: item.icms_v_cred_icms_sn,
134 },
135 "202" => IcmsCsosn::Csosn202 {
136 orig,
137 csosn: csosn_code.to_string(),
138 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
139 FiscalError::MissingRequiredField {
140 field: "modBCST".to_string(),
141 }
142 })?,
143 p_mva_st: item.icms_p_mva_st,
144 p_red_bc_st: item.icms_red_bc_st,
145 v_bc_st: item
146 .icms_v_bc_st
147 .ok_or_else(|| FiscalError::MissingRequiredField {
148 field: "vBCST".to_string(),
149 })?,
150 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
151 FiscalError::MissingRequiredField {
152 field: "pICMSST".to_string(),
153 }
154 })?,
155 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
156 FiscalError::MissingRequiredField {
157 field: "vICMSST".to_string(),
158 }
159 })?,
160 v_bc_fcp_st: item.icms_v_bc_fcp_st,
161 p_fcp_st: item.icms_p_fcp_st,
162 v_fcp_st: item.icms_v_fcp_st,
163 },
164 "203" => IcmsCsosn::Csosn203 {
165 orig,
166 csosn: csosn_code.to_string(),
167 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
168 FiscalError::MissingRequiredField {
169 field: "modBCST".to_string(),
170 }
171 })?,
172 p_mva_st: item.icms_p_mva_st,
173 p_red_bc_st: item.icms_red_bc_st,
174 v_bc_st: item
175 .icms_v_bc_st
176 .ok_or_else(|| FiscalError::MissingRequiredField {
177 field: "vBCST".to_string(),
178 })?,
179 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
180 FiscalError::MissingRequiredField {
181 field: "pICMSST".to_string(),
182 }
183 })?,
184 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
185 FiscalError::MissingRequiredField {
186 field: "vICMSST".to_string(),
187 }
188 })?,
189 v_bc_fcp_st: item.icms_v_bc_fcp_st,
190 p_fcp_st: item.icms_p_fcp_st,
191 v_fcp_st: item.icms_v_fcp_st,
192 },
193 "500" => IcmsCsosn::Csosn500 {
194 orig,
195 csosn: csosn_code.to_string(),
196 v_bc_st_ret: None,
197 p_st: None,
198 v_icms_substituto: item.icms_v_icms_substituto,
199 v_icms_st_ret: None,
200 v_bc_fcp_st_ret: None,
201 p_fcp_st_ret: None,
202 v_fcp_st_ret: None,
203 p_red_bc_efet: None,
204 v_bc_efet: None,
205 p_icms_efet: None,
206 v_icms_efet: None,
207 },
208 "900" => IcmsCsosn::Csosn900 {
209 orig,
210 csosn: csosn_code.to_string(),
211 mod_bc: item.icms_mod_bc.map(|v| v.to_string()),
212 v_bc: Some(item.total_price),
213 p_red_bc: item.icms_red_bc,
214 p_icms: Some(item.icms_rate),
215 v_icms: Some(item.icms_amount),
216 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()),
217 p_mva_st: item.icms_p_mva_st,
218 p_red_bc_st: item.icms_red_bc_st,
219 v_bc_st: item.icms_v_bc_st,
220 p_icms_st: item.icms_p_icms_st,
221 v_icms_st: item.icms_v_icms_st,
222 v_bc_fcp_st: item.icms_v_bc_fcp_st,
223 p_fcp_st: item.icms_p_fcp_st,
224 v_fcp_st: item.icms_v_fcp_st,
225 p_cred_sn: item.icms_p_cred_sn,
226 v_cred_icms_sn: item.icms_v_cred_icms_sn,
227 },
228 other => return Err(FiscalError::UnsupportedIcmsCsosn(other.to_string())),
229 };
230 Ok(csosn.into())
231 } else {
232 let cst_code = item.icms_cst.as_str();
233 let cst = match cst_code {
234 "00" => IcmsCst::Cst00 {
235 orig,
236 mod_bc: item
237 .icms_mod_bc
238 .map(|v| v.to_string())
239 .unwrap_or_else(|| "3".to_string()),
240 v_bc: item.total_price,
241 p_icms: item.icms_rate,
242 v_icms: item.icms_amount,
243 p_fcp: item.icms_p_fcp,
244 v_fcp: item.icms_v_fcp,
245 },
246 "10" => IcmsCst::Cst10 {
247 orig,
248 mod_bc: item
249 .icms_mod_bc
250 .map(|v| v.to_string())
251 .unwrap_or_else(|| "3".to_string()),
252 v_bc: item.total_price,
253 p_icms: item.icms_rate,
254 v_icms: item.icms_amount,
255 v_bc_fcp: item.icms_v_bc_fcp,
256 p_fcp: item.icms_p_fcp,
257 v_fcp: item.icms_v_fcp,
258 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
259 FiscalError::MissingRequiredField {
260 field: "modBCST".to_string(),
261 }
262 })?,
263 p_mva_st: item.icms_p_mva_st,
264 p_red_bc_st: item.icms_red_bc_st,
265 v_bc_st: item
266 .icms_v_bc_st
267 .ok_or_else(|| FiscalError::MissingRequiredField {
268 field: "vBCST".to_string(),
269 })?,
270 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
271 FiscalError::MissingRequiredField {
272 field: "pICMSST".to_string(),
273 }
274 })?,
275 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
276 FiscalError::MissingRequiredField {
277 field: "vICMSST".to_string(),
278 }
279 })?,
280 v_bc_fcp_st: item.icms_v_bc_fcp_st,
281 p_fcp_st: item.icms_p_fcp_st,
282 v_fcp_st: item.icms_v_fcp_st,
283 v_icms_st_deson: None,
284 mot_des_icms_st: None,
285 },
286 "20" => IcmsCst::Cst20 {
287 orig,
288 mod_bc: item
289 .icms_mod_bc
290 .map(|v| v.to_string())
291 .unwrap_or_else(|| "3".to_string()),
292 p_red_bc: item.icms_red_bc.unwrap_or(Rate(0)),
293 v_bc: item.total_price,
294 p_icms: item.icms_rate,
295 v_icms: item.icms_amount,
296 v_bc_fcp: item.icms_v_bc_fcp,
297 p_fcp: item.icms_p_fcp,
298 v_fcp: item.icms_v_fcp,
299 v_icms_deson: item.icms_v_icms_deson,
300 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
301 ind_deduz_deson: item.icms_ind_deduz_deson.clone(),
302 },
303 "30" => IcmsCst::Cst30 {
304 orig,
305 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
306 FiscalError::MissingRequiredField {
307 field: "modBCST".to_string(),
308 }
309 })?,
310 p_mva_st: item.icms_p_mva_st,
311 p_red_bc_st: item.icms_red_bc_st,
312 v_bc_st: item
313 .icms_v_bc_st
314 .ok_or_else(|| FiscalError::MissingRequiredField {
315 field: "vBCST".to_string(),
316 })?,
317 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
318 FiscalError::MissingRequiredField {
319 field: "pICMSST".to_string(),
320 }
321 })?,
322 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
323 FiscalError::MissingRequiredField {
324 field: "vICMSST".to_string(),
325 }
326 })?,
327 v_bc_fcp_st: item.icms_v_bc_fcp_st,
328 p_fcp_st: item.icms_p_fcp_st,
329 v_fcp_st: item.icms_v_fcp_st,
330 v_icms_deson: item.icms_v_icms_deson,
331 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
332 ind_deduz_deson: item.icms_ind_deduz_deson.clone(),
333 },
334 "40" => IcmsCst::Cst40 {
335 orig,
336 v_icms_deson: item.icms_v_icms_deson,
337 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
338 ind_deduz_deson: item.icms_ind_deduz_deson.clone(),
339 },
340 "41" => IcmsCst::Cst41 {
341 orig,
342 v_icms_deson: item.icms_v_icms_deson,
343 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
344 ind_deduz_deson: item.icms_ind_deduz_deson.clone(),
345 },
346 "50" => IcmsCst::Cst50 {
347 orig,
348 v_icms_deson: item.icms_v_icms_deson,
349 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
350 ind_deduz_deson: item.icms_ind_deduz_deson.clone(),
351 },
352 "51" => IcmsCst::Cst51 {
353 orig,
354 mod_bc: item.icms_mod_bc.map(|v| v.to_string()),
355 p_red_bc: item.icms_red_bc,
356 c_benef_rbc: None,
357 v_bc: Some(item.total_price),
358 p_icms: Some(item.icms_rate),
359 v_icms_op: None,
360 p_dif: None,
361 v_icms_dif: None,
362 v_icms: Some(item.icms_amount),
363 v_bc_fcp: item.icms_v_bc_fcp,
364 p_fcp: item.icms_p_fcp,
365 v_fcp: item.icms_v_fcp,
366 p_fcp_dif: None,
367 v_fcp_dif: None,
368 v_fcp_efet: None,
369 },
370 "60" => IcmsCst::Cst60 {
371 orig,
372 v_bc_st_ret: None,
373 p_st: None,
374 v_icms_substituto: item.icms_v_icms_substituto,
375 v_icms_st_ret: None,
376 v_bc_fcp_st_ret: None,
377 p_fcp_st_ret: None,
378 v_fcp_st_ret: None,
379 p_red_bc_efet: None,
380 v_bc_efet: None,
381 p_icms_efet: None,
382 v_icms_efet: None,
383 },
384 "70" => IcmsCst::Cst70 {
385 orig,
386 mod_bc: item
387 .icms_mod_bc
388 .map(|v| v.to_string())
389 .unwrap_or_else(|| "3".to_string()),
390 p_red_bc: item.icms_red_bc.unwrap_or(Rate(0)),
391 v_bc: item.total_price,
392 p_icms: item.icms_rate,
393 v_icms: item.icms_amount,
394 v_bc_fcp: item.icms_v_bc_fcp,
395 p_fcp: item.icms_p_fcp,
396 v_fcp: item.icms_v_fcp,
397 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
398 FiscalError::MissingRequiredField {
399 field: "modBCST".to_string(),
400 }
401 })?,
402 p_mva_st: item.icms_p_mva_st,
403 p_red_bc_st: item.icms_red_bc_st,
404 v_bc_st: item
405 .icms_v_bc_st
406 .ok_or_else(|| FiscalError::MissingRequiredField {
407 field: "vBCST".to_string(),
408 })?,
409 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
410 FiscalError::MissingRequiredField {
411 field: "pICMSST".to_string(),
412 }
413 })?,
414 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
415 FiscalError::MissingRequiredField {
416 field: "vICMSST".to_string(),
417 }
418 })?,
419 v_bc_fcp_st: item.icms_v_bc_fcp_st,
420 p_fcp_st: item.icms_p_fcp_st,
421 v_fcp_st: item.icms_v_fcp_st,
422 v_icms_deson: item.icms_v_icms_deson,
423 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
424 ind_deduz_deson: item.icms_ind_deduz_deson.clone(),
425 v_icms_st_deson: None,
426 mot_des_icms_st: None,
427 },
428 "90" => IcmsCst::Cst90 {
429 orig,
430 mod_bc: item.icms_mod_bc.map(|v| v.to_string()),
431 v_bc: Some(item.total_price),
432 p_red_bc: item.icms_red_bc,
433 c_benef_rbc: None,
434 p_icms: Some(item.icms_rate),
435 v_icms_op: None,
436 p_dif: None,
437 v_icms_dif: None,
438 v_icms: Some(item.icms_amount),
439 v_bc_fcp: item.icms_v_bc_fcp,
440 p_fcp: item.icms_p_fcp,
441 v_fcp: item.icms_v_fcp,
442 p_fcp_dif: None,
443 v_fcp_dif: None,
444 v_fcp_efet: None,
445 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()),
446 p_mva_st: item.icms_p_mva_st,
447 p_red_bc_st: item.icms_red_bc_st,
448 v_bc_st: item.icms_v_bc_st,
449 p_icms_st: item.icms_p_icms_st,
450 v_icms_st: item.icms_v_icms_st,
451 v_bc_fcp_st: item.icms_v_bc_fcp_st,
452 p_fcp_st: item.icms_p_fcp_st,
453 v_fcp_st: item.icms_v_fcp_st,
454 v_icms_deson: item.icms_v_icms_deson,
455 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
456 ind_deduz_deson: item.icms_ind_deduz_deson.clone(),
457 v_icms_st_deson: None,
458 mot_des_icms_st: None,
459 },
460 other => return Err(FiscalError::UnsupportedIcmsCst(other.to_string())),
461 };
462 Ok(cst.into())
463 }
464}
465
466pub(crate) fn build_det(
468 item: &InvoiceItemData,
469 data: &InvoiceBuildData,
470) -> Result<DetResult, FiscalError> {
471 if item.nve.len() > 8 {
473 return Err(FiscalError::InvalidTaxData(format!(
474 "Item {}: NVE limited to 8 entries, got {}",
475 item.item_number,
476 item.nve.len()
477 )));
478 }
479
480 let is_simples = matches!(
481 data.issuer.tax_regime,
482 TaxRegime::SimplesNacional | TaxRegime::SimplesExcess
483 );
484
485 let has_issqn = item.issqn.is_some();
486
487 let mut icms_totals = IcmsTotals::default();
489 let icms_xml = if has_issqn {
490 String::new()
491 } else {
492 let icms_variant = build_icms_variant(item, is_simples)?;
493 tax_icms::build_icms_xml(&icms_variant, &mut icms_totals)?
494 };
495
496 let issqn_xml = if let Some(ref issqn_data) = item.issqn {
498 tax_issqn::build_issqn_xml(issqn_data)
499 } else {
500 String::new()
501 };
502
503 let pis_xml = tax_pis_cofins_ipi::build_pis_xml(&PisData {
505 cst: item.pis_cst.clone(),
506 v_bc: item.pis_v_bc.or(Some(Cents(0))),
507 p_pis: item.pis_p_pis.or(Some(Rate4(0))),
508 v_pis: item.pis_v_pis.or(Some(Cents(0))),
509 q_bc_prod: item.pis_q_bc_prod,
510 v_aliq_prod: item.pis_v_aliq_prod,
511 });
512
513 let cofins_xml = tax_pis_cofins_ipi::build_cofins_xml(&CofinsData {
515 cst: item.cofins_cst.clone(),
516 v_bc: item.cofins_v_bc.or(Some(Cents(0))),
517 p_cofins: item.cofins_p_cofins.or(Some(Rate4(0))),
518 v_cofins: item.cofins_v_cofins.or(Some(Cents(0))),
519 q_bc_prod: item.cofins_q_bc_prod,
520 v_aliq_prod: item.cofins_v_aliq_prod,
521 });
522
523 let mut ipi_xml = String::new();
525 let mut v_ipi = 0i64;
526 if let Some(ref ipi_cst) = item.ipi_cst {
527 ipi_xml = tax_pis_cofins_ipi::build_ipi_xml(&IpiData {
528 cst: ipi_cst.clone(),
529 c_enq: item.ipi_c_enq.clone().unwrap_or_else(|| "999".to_string()),
530 v_bc: item.ipi_v_bc,
531 p_ipi: item.ipi_p_ipi,
532 v_ipi: item.ipi_v_ipi,
533 q_unid: item.ipi_q_unid,
534 v_unid: item.ipi_v_unid,
535 ..IpiData::default()
536 });
537 v_ipi = item.ipi_v_ipi.map(|c| c.0).unwrap_or(0);
538 }
539
540 let mut ii_xml = String::new();
542 let mut v_ii = 0i64;
543 if let Some(ii_vbc) = item.ii_v_bc {
544 ii_xml = tax_pis_cofins_ipi::build_ii_xml(&IiData {
545 v_bc: ii_vbc,
546 v_desp_adu: item.ii_v_desp_adu.unwrap_or(Cents(0)),
547 v_ii: item.ii_v_ii.unwrap_or(Cents(0)),
548 v_iof: item.ii_v_iof.unwrap_or(Cents(0)),
549 });
550 v_ii = item.ii_v_ii.map(|c| c.0).unwrap_or(0);
551 }
552
553 let prod_options = build_prod_options(item);
555
556 let mut v_pis_st = 0i64;
558 if let Some(ref pis_st_data) = item.pis_st {
559 if pis_st_data.ind_soma_pis_st == Some(1) {
561 v_pis_st = pis_st_data.v_pis.0;
562 }
563 }
564
565 let mut v_cofins_st = 0i64;
567 if let Some(ref cofins_st_data) = item.cofins_st {
568 if cofins_st_data.ind_soma_cofins_st == Some(1) {
570 v_cofins_st = cofins_st_data.v_cofins.0;
571 }
572 }
573
574 let item_ind_deduz_deson = item
576 .icms_ind_deduz_deson
577 .as_deref()
578 .map(|v| v == "1")
579 .unwrap_or(false);
580
581 let det_extras = build_det_extras(item);
583
584 let mut imposto_children: Vec<String> = Vec::new();
586 if !icms_xml.is_empty() {
587 imposto_children.push(icms_xml);
588 }
589 if !ipi_xml.is_empty() {
590 imposto_children.push(ipi_xml);
591 }
592 if let Some(ref pis_st) = item.pis_st {
594 imposto_children.push(tax_pis_cofins_ipi::build_pis_st_xml(pis_st));
595 } else {
596 imposto_children.push(pis_xml);
597 }
598 if let Some(ref cofins_st) = item.cofins_st {
600 imposto_children.push(tax_pis_cofins_ipi::build_cofins_st_xml(cofins_st));
601 } else {
602 imposto_children.push(cofins_xml);
603 }
604 if !ii_xml.is_empty() {
605 imposto_children.push(ii_xml);
606 }
607 if !issqn_xml.is_empty() {
608 imposto_children.push(issqn_xml);
609 }
610
611 if data.schema_version.is_pl010() {
614 if let Some(ref is_data) = item.is_data {
615 imposto_children.push(tax_is::build_is_xml(is_data));
616 }
617
618 if let Some(ref ibs_cbs_data) = item.ibs_cbs {
620 imposto_children.push(tax_ibs_cbs::build_ibs_cbs_xml(ibs_cbs_data));
621 }
622 }
623
624 let fc2 = |c: i64| format_cents(c, 2);
626 let fc10 = |c: i64| format_cents(c, 10);
627 let fd4 = |v: f64| format_decimal(v, 4);
628
629 let mut prod_children = vec![
630 tag("cProd", &[], TagContent::Text(&item.product_code)),
631 tag(
632 "cEAN",
633 &[],
634 TagContent::Text(item.c_ean.as_deref().unwrap_or("SEM GTIN")),
635 ),
636 tag(
637 "xProd",
638 &[],
639 TagContent::Text(
640 if item.item_number == 1
642 && data.environment == SefazEnvironment::Homologation
643 && data.model == InvoiceModel::Nfce
644 {
645 HOMOLOGATION_XPROD
646 } else {
647 &item.description
648 },
649 ),
650 ),
651 tag("NCM", &[], TagContent::Text(&item.ncm)),
652 ];
653 for nve_code in &item.nve {
654 prod_children.push(tag("NVE", &[], TagContent::Text(nve_code)));
655 }
656 if let Some(ref cest) = item.cest {
657 prod_children.push(tag("CEST", &[], TagContent::Text(cest)));
658 if let Some(ref ind) = item.cest_ind_escala {
659 prod_children.push(tag("indEscala", &[], TagContent::Text(ind)));
660 }
661 if let Some(ref fab) = item.cest_cnpj_fab {
662 prod_children.push(tag("CNPJFab", &[], TagContent::Text(fab)));
663 }
664 }
665 if let Some(ref cb) = item.c_benef {
666 prod_children.push(tag("cBenef", &[], TagContent::Text(cb)));
667 }
668 for gc in item.g_cred.iter().take(4) {
670 let p_str = format_rate4(gc.p_cred_presumido.0);
671 let mut gc_children = vec![
672 tag(
673 "cCredPresumido",
674 &[],
675 TagContent::Text(&gc.c_cred_presumido),
676 ),
677 tag("pCredPresumido", &[], TagContent::Text(&p_str)),
678 ];
679 if let Some(v) = gc.v_cred_presumido {
680 let v_str = format_cents(v.0, 2);
681 gc_children.push(tag("vCredPresumido", &[], TagContent::Text(&v_str)));
682 }
683 prod_children.push(tag("gCred", &[], TagContent::Children(gc_children)));
684 }
685 if let Some(ref ex) = item.extipi {
686 prod_children.push(tag("EXTIPI", &[], TagContent::Text(ex)));
687 }
688 prod_children.extend([
689 tag("CFOP", &[], TagContent::Text(&item.cfop)),
690 tag("uCom", &[], TagContent::Text(&item.unit_of_measure)),
691 tag("qCom", &[], TagContent::Text(&fd4(item.quantity))),
692 tag("vUnCom", &[], TagContent::Text(&fc10(item.unit_price.0))),
693 tag("vProd", &[], TagContent::Text(&fc2(item.total_price.0))),
694 tag(
695 "cEANTrib",
696 &[],
697 TagContent::Text(item.c_ean_trib.as_deref().unwrap_or("SEM GTIN")),
698 ),
699 tag("uTrib", &[], TagContent::Text(&item.unit_of_measure)),
700 tag("qTrib", &[], TagContent::Text(&fd4(item.quantity))),
701 tag("vUnTrib", &[], TagContent::Text(&fc10(item.unit_price.0))),
702 ]);
703 if let Some(v) = item.v_frete {
704 prod_children.push(tag("vFrete", &[], TagContent::Text(&fc2(v.0))));
705 }
706 if let Some(v) = item.v_seg {
707 prod_children.push(tag("vSeg", &[], TagContent::Text(&fc2(v.0))));
708 }
709 if let Some(v) = item.v_desc {
710 prod_children.push(tag("vDesc", &[], TagContent::Text(&fc2(v.0))));
711 }
712 if let Some(v) = item.v_outro {
713 prod_children.push(tag("vOutro", &[], TagContent::Text(&fc2(v.0))));
714 }
715 let ind_tot_str = match item.ind_tot {
716 Some(v) => v.to_string(),
717 None => "1".to_string(),
718 };
719 prod_children.push(tag("indTot", &[], TagContent::Text(&ind_tot_str)));
720 if let Some(ref dis) = item.di {
722 for di in dis.iter().take(100) {
723 prod_children.push(build_di_xml(di));
724 }
725 }
726 if let Some(ref exports) = item.det_export {
728 for dex in exports.iter().take(500) {
729 prod_children.push(build_det_export_xml(dex));
730 }
731 }
732 if let Some(ref xped) = item.x_ped {
733 prod_children.push(tag("xPed", &[], TagContent::Text(xped)));
734 }
735 if let Some(ref nip) = item.n_item_ped {
736 prod_children.push(tag("nItemPed", &[], TagContent::Text(nip)));
737 }
738 if let Some(ref nfci) = item.n_fci {
739 prod_children.push(tag("nFCI", &[], TagContent::Text(nfci)));
740 }
741 prod_children.extend(prod_options);
742
743 let mut v_ipi_devol = 0i64;
745 let imposto_devol_xml = if let Some(ref devol) = item.imposto_devol {
746 v_ipi_devol = devol.v_ipi_devol.0;
747 let p_devol_str = format_decimal(devol.p_devol.0 as f64 / 100.0, 2);
748 let v_ipi_devol_str = format_cents(devol.v_ipi_devol.0, 2);
749 tag(
750 "impostoDevol",
751 &[],
752 TagContent::Children(vec![
753 tag("pDevol", &[], TagContent::Text(&p_devol_str)),
754 tag(
755 "IPI",
756 &[],
757 TagContent::Children(vec![tag(
758 "vIPIDevol",
759 &[],
760 TagContent::Text(&v_ipi_devol_str),
761 )]),
762 ),
763 ]),
764 )
765 } else {
766 String::new()
767 };
768
769 let nitem = item.item_number.to_string();
771 let mut det_children = vec![
772 tag("prod", &[], TagContent::Children(prod_children)),
773 tag("imposto", &[], TagContent::Children(imposto_children)),
774 ];
775 if !imposto_devol_xml.is_empty() {
776 det_children.push(imposto_devol_xml);
777 }
778 det_children.extend(det_extras);
779
780 let xml = tag(
781 "det",
782 &[("nItem", &nitem)],
783 TagContent::Children(det_children),
784 );
785
786 Ok(DetResult {
787 xml,
788 icms_totals,
789 v_ipi,
790 v_pis: item.pis_v_pis.map(|c| c.0).unwrap_or(0),
791 v_cofins: item.cofins_v_cofins.map(|c| c.0).unwrap_or(0),
792 v_ii,
793 v_frete: item.v_frete.map(|c| c.0).unwrap_or(0),
794 v_seg: item.v_seg.map(|c| c.0).unwrap_or(0),
795 v_desc: item.v_desc.map(|c| c.0).unwrap_or(0),
796 v_outro: item.v_outro.map(|c| c.0).unwrap_or(0),
797 ind_tot: item.ind_tot.unwrap_or(1),
798 v_tot_trib: item.v_tot_trib.map(|c| c.0).unwrap_or(0),
799 v_ipi_devol,
800 v_pis_st,
801 v_cofins_st,
802 ind_deduz_deson: item_ind_deduz_deson,
803 has_issqn,
804 })
805}
806
807fn build_prod_options(item: &InvoiceItemData) -> Vec<String> {
808 let mut opts = Vec::new();
809
810 if let Some(ref rastros) = item.rastro {
812 for r in rastros.iter().take(500) {
813 let mut rastro_children = vec![
814 tag("nLote", &[], TagContent::Text(&r.n_lote)),
815 tag("qLote", &[], TagContent::Text(&format_decimal(r.q_lote, 3))),
816 tag("dFab", &[], TagContent::Text(&r.d_fab)),
817 tag("dVal", &[], TagContent::Text(&r.d_val)),
818 ];
819 if let Some(ref agreg) = r.c_agreg {
820 rastro_children.push(tag("cAgreg", &[], TagContent::Text(agreg)));
821 }
822 opts.push(tag("rastro", &[], TagContent::Children(rastro_children)));
823 }
824 }
825
826 if let Some(ref v) = item.veic_prod {
828 opts.push(tag(
829 "veicProd",
830 &[],
831 TagContent::Children(vec![
832 tag("tpOp", &[], TagContent::Text(&v.tp_op)),
833 tag("chassi", &[], TagContent::Text(&v.chassi)),
834 tag("cCor", &[], TagContent::Text(&v.c_cor)),
835 tag("xCor", &[], TagContent::Text(&v.x_cor)),
836 tag("pot", &[], TagContent::Text(&v.pot)),
837 tag("cilin", &[], TagContent::Text(&v.cilin)),
838 tag("pesoL", &[], TagContent::Text(&v.peso_l)),
839 tag("pesoB", &[], TagContent::Text(&v.peso_b)),
840 tag("nSerie", &[], TagContent::Text(&v.n_serie)),
841 tag("tpComb", &[], TagContent::Text(&v.tp_comb)),
842 tag("nMotor", &[], TagContent::Text(&v.n_motor)),
843 tag("CMT", &[], TagContent::Text(&v.cmt)),
844 tag("dist", &[], TagContent::Text(&v.dist)),
845 tag("anoMod", &[], TagContent::Text(&v.ano_mod)),
846 tag("anoFab", &[], TagContent::Text(&v.ano_fab)),
847 tag("tpPint", &[], TagContent::Text(&v.tp_pint)),
848 tag("tpVeic", &[], TagContent::Text(&v.tp_veic)),
849 tag("espVeic", &[], TagContent::Text(&v.esp_veic)),
850 tag("VIN", &[], TagContent::Text(&v.vin)),
851 tag("condVeic", &[], TagContent::Text(&v.cond_veic)),
852 tag("cMod", &[], TagContent::Text(&v.c_mod)),
853 tag("cCorDENATRAN", &[], TagContent::Text(&v.c_cor_denatran)),
854 tag("lota", &[], TagContent::Text(&v.lota)),
855 tag("tpRest", &[], TagContent::Text(&v.tp_rest)),
856 ]),
857 ));
858 } else if let Some(ref m) = item.med {
859 let mut med_children = Vec::new();
860 if let Some(ref code) = m.c_prod_anvisa {
861 med_children.push(tag("cProdANVISA", &[], TagContent::Text(code)));
862 }
863 if let Some(ref reason) = m.x_motivo_isencao {
864 med_children.push(tag("xMotivoIsencao", &[], TagContent::Text(reason)));
865 }
866 med_children.push(tag(
867 "vPMC",
868 &[],
869 TagContent::Text(&format_cents(m.v_pmc.0, 2)),
870 ));
871 opts.push(tag("med", &[], TagContent::Children(med_children)));
872 } else if let Some(ref arms) = item.arma {
873 for a in arms.iter().take(500) {
874 opts.push(tag(
875 "arma",
876 &[],
877 TagContent::Children(vec![
878 tag("tpArma", &[], TagContent::Text(&a.tp_arma)),
879 tag("nSerie", &[], TagContent::Text(&a.n_serie)),
880 tag("nCano", &[], TagContent::Text(&a.n_cano)),
881 tag("descr", &[], TagContent::Text(&a.descr)),
882 ]),
883 ));
884 }
885 } else if let Some(ref recopi) = item.n_recopi {
886 if !recopi.is_empty() {
887 opts.push(tag("nRECOPI", &[], TagContent::Text(recopi)));
888 }
889 }
890
891 if let Some(ref comb) = item.comb {
893 opts.push(build_comb_xml(comb));
894 }
895
896 opts
897}
898
899fn build_di_xml(di: &crate::types::DiData) -> String {
901 let mut children = vec![
902 tag("nDI", &[], TagContent::Text(&di.n_di)),
903 tag("dDI", &[], TagContent::Text(&di.d_di)),
904 tag("xLocDesemb", &[], TagContent::Text(&di.x_loc_desemb)),
905 tag("UFDesemb", &[], TagContent::Text(&di.uf_desemb)),
906 tag("dDesemb", &[], TagContent::Text(&di.d_desemb)),
907 tag("tpViaTransp", &[], TagContent::Text(&di.tp_via_transp)),
908 ];
909 if let Some(ref v) = di.v_afrmm {
910 children.push(tag("vAFRMM", &[], TagContent::Text(&format_cents(v.0, 2))));
911 }
912 children.push(tag(
913 "tpIntermedio",
914 &[],
915 TagContent::Text(&di.tp_intermedio),
916 ));
917 if let Some(ref cnpj) = di.cnpj {
918 children.push(tag("CNPJ", &[], TagContent::Text(cnpj)));
919 } else if let Some(ref cpf) = di.cpf {
920 children.push(tag("CPF", &[], TagContent::Text(cpf)));
921 }
922 if let Some(ref uf) = di.uf_terceiro {
923 children.push(tag("UFTerceiro", &[], TagContent::Text(uf)));
924 }
925 children.push(tag("cExportador", &[], TagContent::Text(&di.c_exportador)));
926 for adi in di.adi.iter().take(999) {
928 let mut adi_children = Vec::new();
929 if let Some(ref n) = adi.n_adicao {
930 adi_children.push(tag("nAdicao", &[], TagContent::Text(n)));
931 }
932 adi_children.push(tag("nSeqAdic", &[], TagContent::Text(&adi.n_seq_adic)));
933 adi_children.push(tag("cFabricante", &[], TagContent::Text(&adi.c_fabricante)));
934 if let Some(ref v) = adi.v_desc_di {
935 adi_children.push(tag("vDescDI", &[], TagContent::Text(&format_cents(v.0, 2))));
936 }
937 if let Some(ref n) = adi.n_draw {
938 adi_children.push(tag("nDraw", &[], TagContent::Text(n)));
939 }
940 children.push(tag("adi", &[], TagContent::Children(adi_children)));
941 }
942 tag("DI", &[], TagContent::Children(children))
943}
944
945fn build_det_export_xml(dex: &crate::types::DetExportData) -> String {
947 let mut children = Vec::new();
948 if let Some(ref n) = dex.n_draw {
949 children.push(tag("nDraw", &[], TagContent::Text(n)));
950 }
951 if dex.n_re.is_some() || dex.ch_nfe.is_some() || dex.q_export.is_some() {
952 let mut exp_ind_children = Vec::new();
953 if let Some(ref n) = dex.n_re {
954 exp_ind_children.push(tag("nRE", &[], TagContent::Text(n)));
955 }
956 if let Some(ref ch) = dex.ch_nfe {
957 exp_ind_children.push(tag("chNFe", &[], TagContent::Text(ch)));
958 }
959 if let Some(q) = dex.q_export {
960 exp_ind_children.push(tag("qExport", &[], TagContent::Text(&format_decimal(q, 4))));
961 }
962 children.push(tag(
963 "exportInd",
964 &[],
965 TagContent::Children(exp_ind_children),
966 ));
967 }
968 tag("detExport", &[], TagContent::Children(children))
969}
970
971fn build_comb_xml(comb: &CombData) -> String {
977 let mut children = vec![
978 tag("cProdANP", &[], TagContent::Text(&comb.c_prod_anp)),
979 tag("descANP", &[], TagContent::Text(&comb.desc_anp)),
980 ];
981
982 if let Some(ref v) = comb.p_glp {
983 children.push(tag("pGLP", &[], TagContent::Text(v)));
984 }
985 if let Some(ref v) = comb.p_gn_n {
986 children.push(tag("pGNn", &[], TagContent::Text(v)));
987 }
988 if let Some(ref v) = comb.p_gn_i {
989 children.push(tag("pGNi", &[], TagContent::Text(v)));
990 }
991 if let Some(ref v) = comb.v_part {
992 children.push(tag("vPart", &[], TagContent::Text(v)));
993 }
994 if let Some(ref v) = comb.codif {
995 children.push(tag("CODIF", &[], TagContent::Text(v)));
996 }
997 if let Some(ref v) = comb.q_temp {
998 children.push(tag("qTemp", &[], TagContent::Text(v)));
999 }
1000
1001 children.push(tag("UFCons", &[], TagContent::Text(&comb.uf_cons)));
1002
1003 if let Some(ref cide) = comb.cide {
1005 let cide_children = vec![
1006 tag("qBCProd", &[], TagContent::Text(&cide.q_bc_prod)),
1007 tag("vAliqProd", &[], TagContent::Text(&cide.v_aliq_prod)),
1008 tag("vCIDE", &[], TagContent::Text(&cide.v_cide)),
1009 ];
1010 children.push(tag("CIDE", &[], TagContent::Children(cide_children)));
1011 }
1012
1013 if let Some(ref enc) = comb.encerrante {
1015 let mut enc_children = vec![tag("nBico", &[], TagContent::Text(&enc.n_bico))];
1016 if let Some(ref bomba) = enc.n_bomba {
1017 enc_children.push(tag("nBomba", &[], TagContent::Text(bomba)));
1018 }
1019 enc_children.push(tag("nTanque", &[], TagContent::Text(&enc.n_tanque)));
1020 enc_children.push(tag("vEncIni", &[], TagContent::Text(&enc.v_enc_ini)));
1021 enc_children.push(tag("vEncFin", &[], TagContent::Text(&enc.v_enc_fin)));
1022 children.push(tag("encerrante", &[], TagContent::Children(enc_children)));
1023 }
1024
1025 if let Some(ref v) = comb.p_bio {
1027 children.push(tag("pBio", &[], TagContent::Text(v)));
1028 }
1029
1030 if let Some(ref origins) = comb.orig_comb {
1032 for orig in origins {
1033 let orig_children = vec![
1034 tag("indImport", &[], TagContent::Text(&orig.ind_import)),
1035 tag("cUFOrig", &[], TagContent::Text(&orig.c_uf_orig)),
1036 tag("pOrig", &[], TagContent::Text(&orig.p_orig)),
1037 ];
1038 children.push(tag("origComb", &[], TagContent::Children(orig_children)));
1039 }
1040 }
1041
1042 tag("comb", &[], TagContent::Children(children))
1043}
1044
1045fn build_det_extras(item: &InvoiceItemData) -> Vec<String> {
1046 let mut extras = Vec::new();
1047
1048 if let Some(ref info) = item.inf_ad_prod {
1049 extras.push(tag("infAdProd", &[], TagContent::Text(info)));
1050 }
1051
1052 if let Some(ref obs) = item.obs_item {
1053 let mut obs_children = Vec::new();
1054 if let Some(ref cont) = obs.obs_cont {
1055 obs_children.push(tag(
1056 "obsCont",
1057 &[("xCampo", &cont.x_campo)],
1058 TagContent::Children(vec![tag("xTexto", &[], TagContent::Text(&cont.x_texto))]),
1059 ));
1060 }
1061 if let Some(ref fisco) = obs.obs_fisco {
1062 obs_children.push(tag(
1063 "obsFisco",
1064 &[("xCampo", &fisco.x_campo)],
1065 TagContent::Children(vec![tag("xTexto", &[], TagContent::Text(&fisco.x_texto))]),
1066 ));
1067 }
1068 extras.push(tag("obsItem", &[], TagContent::Children(obs_children)));
1069 }
1070
1071 if let Some(ref dfe) = item.dfe_referenciado {
1072 let mut dfe_children = vec![tag("chaveAcesso", &[], TagContent::Text(&dfe.chave_acesso))];
1073 if let Some(ref n) = dfe.n_item {
1074 dfe_children.push(tag("nItem", &[], TagContent::Text(n)));
1075 }
1076 extras.push(tag(
1077 "DFeReferenciado",
1078 &[],
1079 TagContent::Children(dfe_children),
1080 ));
1081 }
1082
1083 extras
1084}
1085
1086#[cfg(test)]
1087mod tests {
1088 use super::*;
1089 use crate::newtypes::{Cents, IbgeCode, Rate, Rate4};
1090 use crate::tax_issqn::IssqnData as TaxIssqnData;
1091 use crate::types::{
1092 CideData, CombData, EncerranteData, GCredData, InvoiceItemData, InvoiceModel, IssuerData,
1093 OrigCombData, SefazEnvironment, TaxRegime,
1094 };
1095
1096 fn sample_build_data() -> InvoiceBuildData {
1097 let issuer = IssuerData::new(
1098 "12345678000199",
1099 "123456789",
1100 "Test Company",
1101 TaxRegime::SimplesNacional,
1102 "SP",
1103 IbgeCode("3550308".to_string()),
1104 "Sao Paulo",
1105 "Av Paulista",
1106 "1000",
1107 "Bela Vista",
1108 "01310100",
1109 );
1110
1111 InvoiceBuildData {
1112 schema_version: crate::types::SchemaVersion::PL009,
1113 model: InvoiceModel::Nfe,
1114 series: 1,
1115 number: 1,
1116 emission_type: crate::types::EmissionType::Normal,
1117 environment: SefazEnvironment::Homologation,
1118 issued_at: chrono::Utc::now()
1119 .with_timezone(&chrono::FixedOffset::west_opt(3 * 3600).expect("valid offset")),
1120 operation_nature: "VENDA".to_string(),
1121 issuer,
1122 recipient: None,
1123 items: Vec::new(),
1124 payments: Vec::new(),
1125 change_amount: None,
1126 payment_card_details: None,
1127 contingency: None,
1128 exit_at: None,
1129 operation_type: None,
1130 purpose_code: None,
1131 intermediary_indicator: None,
1132 emission_process: None,
1133 consumer_type: None,
1134 buyer_presence: None,
1135 print_format: None,
1136 references: None,
1137 transport: None,
1138 billing: None,
1139 withdrawal: None,
1140 delivery: None,
1141 authorized_xml: None,
1142 additional_info: None,
1143 intermediary: None,
1144 ret_trib: None,
1145 tech_responsible: None,
1146 purchase: None,
1147 export: None,
1148 issqn_tot: None,
1149 cana: None,
1150 agropecuario: None,
1151 compra_gov: None,
1152 pag_antecipado: None,
1153 is_tot: None,
1154 ibs_cbs_tot: None,
1155 destination_indicator: None,
1156 ver_proc: None,
1157 only_ascii: false,
1158 calculation_method: crate::types::CalculationMethod::V2,
1159 }
1160 }
1161
1162 fn sample_item() -> InvoiceItemData {
1163 InvoiceItemData::new(
1164 1,
1165 "001",
1166 "Gasolina Comum",
1167 "27101259",
1168 "5102",
1169 "LT",
1170 50.0,
1171 Cents(599),
1172 Cents(29950),
1173 "102",
1174 Rate(0),
1175 Cents(0),
1176 "99",
1177 "99",
1178 )
1179 }
1180
1181 #[test]
1184 fn comb_minimal_produces_correct_xml() {
1185 let comb = CombData::new("210203001", "GLP", "SP");
1186 let xml = build_comb_xml(&comb);
1187
1188 assert_eq!(
1189 xml,
1190 "<comb>\
1191 <cProdANP>210203001</cProdANP>\
1192 <descANP>GLP</descANP>\
1193 <UFCons>SP</UFCons>\
1194 </comb>"
1195 );
1196 }
1197
1198 #[test]
1199 fn comb_with_glp_percentages() {
1200 let comb = CombData::new("210203001", "GLP", "SP")
1201 .p_glp("60.0000")
1202 .p_gn_n("25.0000")
1203 .p_gn_i("15.0000")
1204 .v_part("3.50");
1205
1206 let xml = build_comb_xml(&comb);
1207
1208 assert_eq!(
1209 xml,
1210 "<comb>\
1211 <cProdANP>210203001</cProdANP>\
1212 <descANP>GLP</descANP>\
1213 <pGLP>60.0000</pGLP>\
1214 <pGNn>25.0000</pGNn>\
1215 <pGNi>15.0000</pGNi>\
1216 <vPart>3.50</vPart>\
1217 <UFCons>SP</UFCons>\
1218 </comb>"
1219 );
1220 }
1221
1222 #[test]
1223 fn comb_with_codif_and_qtemp() {
1224 let comb = CombData::new("320102001", "GASOLINA COMUM", "PR")
1225 .codif("123456789")
1226 .q_temp("1000.0000");
1227
1228 let xml = build_comb_xml(&comb);
1229
1230 assert_eq!(
1231 xml,
1232 "<comb>\
1233 <cProdANP>320102001</cProdANP>\
1234 <descANP>GASOLINA COMUM</descANP>\
1235 <CODIF>123456789</CODIF>\
1236 <qTemp>1000.0000</qTemp>\
1237 <UFCons>PR</UFCons>\
1238 </comb>"
1239 );
1240 }
1241
1242 #[test]
1243 fn comb_with_cide() {
1244 let cide = CideData::new("1000.0000", "0.0700", "70.00");
1245 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").cide(cide);
1246
1247 let xml = build_comb_xml(&comb);
1248
1249 assert_eq!(
1250 xml,
1251 "<comb>\
1252 <cProdANP>320102001</cProdANP>\
1253 <descANP>GASOLINA COMUM</descANP>\
1254 <UFCons>SP</UFCons>\
1255 <CIDE>\
1256 <qBCProd>1000.0000</qBCProd>\
1257 <vAliqProd>0.0700</vAliqProd>\
1258 <vCIDE>70.00</vCIDE>\
1259 </CIDE>\
1260 </comb>"
1261 );
1262 }
1263
1264 #[test]
1265 fn comb_with_encerrante() {
1266 let enc = EncerranteData::new("1", "1", "1234.567", "1284.567").n_bomba("2");
1267 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").encerrante(enc);
1268
1269 let xml = build_comb_xml(&comb);
1270
1271 assert_eq!(
1272 xml,
1273 "<comb>\
1274 <cProdANP>320102001</cProdANP>\
1275 <descANP>GASOLINA COMUM</descANP>\
1276 <UFCons>SP</UFCons>\
1277 <encerrante>\
1278 <nBico>1</nBico>\
1279 <nBomba>2</nBomba>\
1280 <nTanque>1</nTanque>\
1281 <vEncIni>1234.567</vEncIni>\
1282 <vEncFin>1284.567</vEncFin>\
1283 </encerrante>\
1284 </comb>"
1285 );
1286 }
1287
1288 #[test]
1289 fn comb_encerrante_without_bomba() {
1290 let enc = EncerranteData::new("3", "2", "5000.000", "5050.000");
1291 let comb = CombData::new("320102001", "GASOLINA COMUM", "RJ").encerrante(enc);
1292
1293 let xml = build_comb_xml(&comb);
1294
1295 assert_eq!(
1296 xml,
1297 "<comb>\
1298 <cProdANP>320102001</cProdANP>\
1299 <descANP>GASOLINA COMUM</descANP>\
1300 <UFCons>RJ</UFCons>\
1301 <encerrante>\
1302 <nBico>3</nBico>\
1303 <nTanque>2</nTanque>\
1304 <vEncIni>5000.000</vEncIni>\
1305 <vEncFin>5050.000</vEncFin>\
1306 </encerrante>\
1307 </comb>"
1308 );
1309 }
1310
1311 #[test]
1312 fn comb_with_pbio() {
1313 let comb = CombData::new("810102001", "OLEO DIESEL B S10", "SP").p_bio("15.0000");
1314
1315 let xml = build_comb_xml(&comb);
1316
1317 assert_eq!(
1318 xml,
1319 "<comb>\
1320 <cProdANP>810102001</cProdANP>\
1321 <descANP>OLEO DIESEL B S10</descANP>\
1322 <UFCons>SP</UFCons>\
1323 <pBio>15.0000</pBio>\
1324 </comb>"
1325 );
1326 }
1327
1328 #[test]
1329 fn comb_with_orig_comb_single() {
1330 let orig = OrigCombData::new("0", "35", "100.0000");
1331 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").orig_comb(vec![orig]);
1332
1333 let xml = build_comb_xml(&comb);
1334
1335 assert_eq!(
1336 xml,
1337 "<comb>\
1338 <cProdANP>320102001</cProdANP>\
1339 <descANP>GASOLINA COMUM</descANP>\
1340 <UFCons>SP</UFCons>\
1341 <origComb>\
1342 <indImport>0</indImport>\
1343 <cUFOrig>35</cUFOrig>\
1344 <pOrig>100.0000</pOrig>\
1345 </origComb>\
1346 </comb>"
1347 );
1348 }
1349
1350 #[test]
1351 fn comb_with_orig_comb_multiple() {
1352 let orig1 = OrigCombData::new("0", "35", "70.0000");
1353 let orig2 = OrigCombData::new("1", "99", "30.0000");
1354 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").orig_comb(vec![orig1, orig2]);
1355
1356 let xml = build_comb_xml(&comb);
1357
1358 assert_eq!(
1359 xml,
1360 "<comb>\
1361 <cProdANP>320102001</cProdANP>\
1362 <descANP>GASOLINA COMUM</descANP>\
1363 <UFCons>SP</UFCons>\
1364 <origComb>\
1365 <indImport>0</indImport>\
1366 <cUFOrig>35</cUFOrig>\
1367 <pOrig>70.0000</pOrig>\
1368 </origComb>\
1369 <origComb>\
1370 <indImport>1</indImport>\
1371 <cUFOrig>99</cUFOrig>\
1372 <pOrig>30.0000</pOrig>\
1373 </origComb>\
1374 </comb>"
1375 );
1376 }
1377
1378 #[test]
1379 fn comb_full_with_all_fields() {
1380 let cide = CideData::new("500.0000", "0.0700", "35.00");
1381 let enc = EncerranteData::new("1", "1", "10000.000", "10050.000").n_bomba("1");
1382 let orig = OrigCombData::new("0", "35", "100.0000");
1383
1384 let comb = CombData::new("210203001", "GLP", "SP")
1385 .p_glp("60.0000")
1386 .p_gn_n("25.0000")
1387 .p_gn_i("15.0000")
1388 .v_part("3.50")
1389 .codif("999888777")
1390 .q_temp("500.0000")
1391 .cide(cide)
1392 .encerrante(enc)
1393 .p_bio("12.0000")
1394 .orig_comb(vec![orig]);
1395
1396 let xml = build_comb_xml(&comb);
1397
1398 assert_eq!(
1399 xml,
1400 "<comb>\
1401 <cProdANP>210203001</cProdANP>\
1402 <descANP>GLP</descANP>\
1403 <pGLP>60.0000</pGLP>\
1404 <pGNn>25.0000</pGNn>\
1405 <pGNi>15.0000</pGNi>\
1406 <vPart>3.50</vPart>\
1407 <CODIF>999888777</CODIF>\
1408 <qTemp>500.0000</qTemp>\
1409 <UFCons>SP</UFCons>\
1410 <CIDE>\
1411 <qBCProd>500.0000</qBCProd>\
1412 <vAliqProd>0.0700</vAliqProd>\
1413 <vCIDE>35.00</vCIDE>\
1414 </CIDE>\
1415 <encerrante>\
1416 <nBico>1</nBico>\
1417 <nBomba>1</nBomba>\
1418 <nTanque>1</nTanque>\
1419 <vEncIni>10000.000</vEncIni>\
1420 <vEncFin>10050.000</vEncFin>\
1421 </encerrante>\
1422 <pBio>12.0000</pBio>\
1423 <origComb>\
1424 <indImport>0</indImport>\
1425 <cUFOrig>35</cUFOrig>\
1426 <pOrig>100.0000</pOrig>\
1427 </origComb>\
1428 </comb>"
1429 );
1430 }
1431
1432 #[test]
1433 fn comb_in_det_xml() {
1434 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP");
1435 let item = sample_item().comb(comb);
1436 let data = sample_build_data();
1437 let result = build_det(&item, &data).expect("build_det should succeed");
1438
1439 let prod_start = result.xml.find("<prod>").expect("<prod> must exist");
1441 let prod_end = result.xml.find("</prod>").expect("</prod> must exist");
1442 let prod_section = &result.xml[prod_start..prod_end];
1443
1444 assert!(prod_section.contains("<comb>"));
1445 assert!(prod_section.contains("<cProdANP>320102001</cProdANP>"));
1446 assert!(prod_section.contains("<descANP>GASOLINA COMUM</descANP>"));
1447 assert!(prod_section.contains("<UFCons>SP</UFCons>"));
1448 assert!(prod_section.contains("</comb>"));
1449 }
1450
1451 #[test]
1454 fn issqn_item_produces_issqn_tag_not_icms() {
1455 let issqn_data = TaxIssqnData::new(10000, 500, 500, "3550308", "14.01")
1456 .ind_iss("1")
1457 .ind_incentivo("2");
1458 let item = sample_item().issqn(issqn_data);
1459 let data = sample_build_data();
1460 let result = build_det(&item, &data).expect("build_det should succeed");
1461
1462 assert!(result.xml.contains("<ISSQN>"));
1464 assert!(result.xml.contains("<vBC>100.00</vBC>"));
1465 assert!(result.xml.contains("<vAliq>5.0000</vAliq>"));
1466 assert!(result.xml.contains("<vISSQN>5.00</vISSQN>"));
1467 assert!(result.xml.contains("<cMunFG>3550308</cMunFG>"));
1468 assert!(result.xml.contains("<cListServ>14.01</cListServ>"));
1469 assert!(result.xml.contains("<indISS>1</indISS>"));
1470 assert!(result.xml.contains("<indIncentivo>2</indIncentivo>"));
1471 assert!(result.xml.contains("</ISSQN>"));
1472
1473 assert!(!result.xml.contains("<ICMS>"));
1475 assert!(!result.xml.contains("</ICMS>"));
1476 assert!(result.has_issqn);
1477 }
1478
1479 #[test]
1480 fn issqn_item_with_all_optional_fields() {
1481 let issqn_data = TaxIssqnData::new(20000, 300, 600, "3304557", "07.02")
1482 .v_deducao(1000)
1483 .v_outro(500)
1484 .v_desc_incond(200)
1485 .v_desc_cond(100)
1486 .v_iss_ret(300)
1487 .ind_iss("1")
1488 .c_servico("1234")
1489 .c_mun("3304557")
1490 .c_pais("1058")
1491 .n_processo("ABC123")
1492 .ind_incentivo("1");
1493
1494 let item = sample_item().issqn(issqn_data);
1495 let data = sample_build_data();
1496 let result = build_det(&item, &data).expect("build_det should succeed");
1497
1498 assert!(result.xml.contains("<vBC>200.00</vBC>"));
1499 assert!(result.xml.contains("<vAliq>3.0000</vAliq>"));
1500 assert!(result.xml.contains("<vISSQN>6.00</vISSQN>"));
1501 assert!(result.xml.contains("<vDeducao>10.00</vDeducao>"));
1502 assert!(result.xml.contains("<vOutro>5.00</vOutro>"));
1503 assert!(result.xml.contains("<vDescIncond>2.00</vDescIncond>"));
1504 assert!(result.xml.contains("<vDescCond>1.00</vDescCond>"));
1505 assert!(result.xml.contains("<vISSRet>3.00</vISSRet>"));
1506 assert!(result.xml.contains("<cServico>1234</cServico>"));
1507 assert!(result.xml.contains("<cMun>3304557</cMun>"));
1508 assert!(result.xml.contains("<cPais>1058</cPais>"));
1509 assert!(result.xml.contains("<nProcesso>ABC123</nProcesso>"));
1510 assert!(result.xml.contains("<indIncentivo>1</indIncentivo>"));
1511 assert!(result.has_issqn);
1512 }
1513
1514 #[test]
1515 fn non_issqn_item_has_icms_and_no_issqn() {
1516 let item = sample_item();
1517 let data = sample_build_data();
1518 let result = build_det(&item, &data).expect("build_det should succeed");
1519
1520 assert!(result.xml.contains("<ICMS"));
1521 assert!(!result.xml.contains("<ISSQN>"));
1522 assert!(!result.has_issqn);
1523 }
1524
1525 #[test]
1528 fn di_minimal_with_one_adi() {
1529 use crate::types::{AdiData, DiData};
1530
1531 let adi = AdiData::new("1", "FABRICANTE_X").n_adicao("001");
1532 let di = DiData::new(
1533 "1234567890",
1534 "2025-01-15",
1535 "Santos",
1536 "SP",
1537 "2025-01-20",
1538 "1",
1539 "1",
1540 "EXP001",
1541 vec![adi],
1542 );
1543 let xml = build_di_xml(&di);
1544
1545 assert_eq!(
1546 xml,
1547 "<DI>\
1548 <nDI>1234567890</nDI>\
1549 <dDI>2025-01-15</dDI>\
1550 <xLocDesemb>Santos</xLocDesemb>\
1551 <UFDesemb>SP</UFDesemb>\
1552 <dDesemb>2025-01-20</dDesemb>\
1553 <tpViaTransp>1</tpViaTransp>\
1554 <tpIntermedio>1</tpIntermedio>\
1555 <cExportador>EXP001</cExportador>\
1556 <adi>\
1557 <nAdicao>001</nAdicao>\
1558 <nSeqAdic>1</nSeqAdic>\
1559 <cFabricante>FABRICANTE_X</cFabricante>\
1560 </adi>\
1561 </DI>"
1562 );
1563 }
1564
1565 #[test]
1566 fn di_with_all_optional_fields() {
1567 use crate::types::{AdiData, DiData};
1568
1569 let adi = AdiData::new("1", "FAB_Y")
1570 .n_adicao("002")
1571 .v_desc_di(Cents(15000))
1572 .n_draw("20259999999");
1573 let di = DiData::new(
1574 "DI-2025-001",
1575 "2025-03-01",
1576 "Paranagua",
1577 "PR",
1578 "2025-03-05",
1579 "1",
1580 "2",
1581 "EXP002",
1582 vec![adi],
1583 )
1584 .v_afrmm(Cents(5000))
1585 .cnpj("12345678000199")
1586 .uf_terceiro("RJ");
1587
1588 let xml = build_di_xml(&di);
1589
1590 assert_eq!(
1591 xml,
1592 "<DI>\
1593 <nDI>DI-2025-001</nDI>\
1594 <dDI>2025-03-01</dDI>\
1595 <xLocDesemb>Paranagua</xLocDesemb>\
1596 <UFDesemb>PR</UFDesemb>\
1597 <dDesemb>2025-03-05</dDesemb>\
1598 <tpViaTransp>1</tpViaTransp>\
1599 <vAFRMM>50.00</vAFRMM>\
1600 <tpIntermedio>2</tpIntermedio>\
1601 <CNPJ>12345678000199</CNPJ>\
1602 <UFTerceiro>RJ</UFTerceiro>\
1603 <cExportador>EXP002</cExportador>\
1604 <adi>\
1605 <nAdicao>002</nAdicao>\
1606 <nSeqAdic>1</nSeqAdic>\
1607 <cFabricante>FAB_Y</cFabricante>\
1608 <vDescDI>150.00</vDescDI>\
1609 <nDraw>20259999999</nDraw>\
1610 </adi>\
1611 </DI>"
1612 );
1613 }
1614
1615 #[test]
1616 fn di_with_cpf_instead_of_cnpj() {
1617 use crate::types::{AdiData, DiData};
1618
1619 let adi = AdiData::new("1", "FAB_Z");
1620 let di = DiData::new(
1621 "DI-CPF",
1622 "2025-06-01",
1623 "Recife",
1624 "PE",
1625 "2025-06-03",
1626 "7",
1627 "3",
1628 "EXP003",
1629 vec![adi],
1630 )
1631 .cpf("12345678901");
1632
1633 let xml = build_di_xml(&di);
1634 assert!(xml.contains("<CPF>12345678901</CPF>"));
1635 assert!(!xml.contains("<CNPJ>"));
1636 }
1637
1638 #[test]
1639 fn di_with_multiple_adi() {
1640 use crate::types::{AdiData, DiData};
1641
1642 let adi1 = AdiData::new("1", "FAB_A").n_adicao("001");
1643 let adi2 = AdiData::new("2", "FAB_B").n_adicao("001");
1644 let di = DiData::new(
1645 "DI-MULTI",
1646 "2025-01-01",
1647 "Santos",
1648 "SP",
1649 "2025-01-05",
1650 "1",
1651 "1",
1652 "EXP-M",
1653 vec![adi1, adi2],
1654 );
1655 let xml = build_di_xml(&di);
1656
1657 let count = xml.matches("<adi>").count();
1659 assert_eq!(count, 2, "expected 2 <adi> elements, got {count}");
1660 assert!(xml.contains("<nSeqAdic>1</nSeqAdic>"));
1661 assert!(xml.contains("<nSeqAdic>2</nSeqAdic>"));
1662 assert!(xml.contains("<cFabricante>FAB_A</cFabricante>"));
1663 assert!(xml.contains("<cFabricante>FAB_B</cFabricante>"));
1664 }
1665
1666 #[test]
1667 fn di_in_det_xml_between_ind_tot_and_xped() {
1668 use crate::types::{AdiData, DiData};
1669
1670 let adi = AdiData::new("1", "FAB").n_adicao("001");
1671 let di = DiData::new(
1672 "DI-001",
1673 "2025-01-15",
1674 "Santos",
1675 "SP",
1676 "2025-01-20",
1677 "1",
1678 "1",
1679 "EXP",
1680 vec![adi],
1681 );
1682 let item = sample_item().di(vec![di]).x_ped("PO-123");
1683 let data = sample_build_data();
1684 let result = build_det(&item, &data).expect("build_det should succeed");
1685
1686 let xml = &result.xml;
1687 let ind_tot_pos = xml.find("</indTot>").expect("</indTot> must exist");
1688 let di_pos = xml.find("<DI>").expect("<DI> must exist");
1689 let xped_pos = xml.find("<xPed>").expect("<xPed> must exist");
1690
1691 assert!(di_pos > ind_tot_pos, "DI must come after indTot");
1692 assert!(xped_pos > di_pos, "xPed must come after DI");
1693 }
1694
1695 #[test]
1698 fn det_export_with_n_draw_only() {
1699 use crate::types::DetExportData;
1700
1701 let dex = DetExportData::new().n_draw("20250000001");
1702 let xml = build_det_export_xml(&dex);
1703
1704 assert_eq!(
1705 xml,
1706 "<detExport>\
1707 <nDraw>20250000001</nDraw>\
1708 </detExport>"
1709 );
1710 }
1711
1712 #[test]
1713 fn det_export_with_export_ind() {
1714 use crate::types::DetExportData;
1715
1716 let dex = DetExportData::new()
1717 .n_draw("20250000002")
1718 .n_re("123456789012")
1719 .ch_nfe("12345678901234567890123456789012345678901234")
1720 .q_export(100.5);
1721 let xml = build_det_export_xml(&dex);
1722
1723 assert_eq!(
1724 xml,
1725 "<detExport>\
1726 <nDraw>20250000002</nDraw>\
1727 <exportInd>\
1728 <nRE>123456789012</nRE>\
1729 <chNFe>12345678901234567890123456789012345678901234</chNFe>\
1730 <qExport>100.5000</qExport>\
1731 </exportInd>\
1732 </detExport>"
1733 );
1734 }
1735
1736 #[test]
1737 fn det_export_empty() {
1738 use crate::types::DetExportData;
1739
1740 let dex = DetExportData::new();
1741 let xml = build_det_export_xml(&dex);
1742
1743 assert_eq!(xml, "<detExport></detExport>");
1744 }
1745
1746 #[test]
1747 fn det_export_in_det_xml_between_ind_tot_and_xped() {
1748 use crate::types::DetExportData;
1749
1750 let dex = DetExportData::new().n_draw("20250000001");
1751 let item = sample_item().det_export(vec![dex]).x_ped("PO-456");
1752 let data = sample_build_data();
1753 let result = build_det(&item, &data).expect("build_det should succeed");
1754
1755 let xml = &result.xml;
1756 let ind_tot_pos = xml.find("</indTot>").expect("</indTot> must exist");
1757 let det_exp_pos = xml.find("<detExport>").expect("<detExport> must exist");
1758 let xped_pos = xml.find("<xPed>").expect("<xPed> must exist");
1759
1760 assert!(
1761 det_exp_pos > ind_tot_pos,
1762 "detExport must come after indTot"
1763 );
1764 assert!(xped_pos > det_exp_pos, "xPed must come after detExport");
1765 }
1766
1767 #[test]
1770 fn imposto_devol_produces_correct_xml() {
1771 use crate::types::ImpostoDevolData;
1772
1773 let devol = ImpostoDevolData::new(Rate(10000), Cents(5000));
1774 let item = sample_item().imposto_devol(devol);
1775 let data = sample_build_data();
1776 let result = build_det(&item, &data).expect("build_det should succeed");
1777
1778 assert!(result.xml.contains(
1779 "<impostoDevol>\
1780 <pDevol>100.00</pDevol>\
1781 <IPI>\
1782 <vIPIDevol>50.00</vIPIDevol>\
1783 </IPI>\
1784 </impostoDevol>"
1785 ));
1786 assert_eq!(result.v_ipi_devol, 5000);
1787 }
1788
1789 #[test]
1790 fn imposto_devol_50_percent() {
1791 use crate::types::ImpostoDevolData;
1792
1793 let devol = ImpostoDevolData::new(Rate(5000), Cents(2500));
1794 let item = sample_item().imposto_devol(devol);
1795 let data = sample_build_data();
1796 let result = build_det(&item, &data).expect("build_det should succeed");
1797
1798 assert!(result.xml.contains("<pDevol>50.00</pDevol>"));
1799 assert!(result.xml.contains("<vIPIDevol>25.00</vIPIDevol>"));
1800 assert_eq!(result.v_ipi_devol, 2500);
1801 }
1802
1803 #[test]
1804 fn imposto_devol_after_imposto_before_inf_ad_prod() {
1805 use crate::types::ImpostoDevolData;
1806
1807 let devol = ImpostoDevolData::new(Rate(10000), Cents(1000));
1808 let item = sample_item().imposto_devol(devol).inf_ad_prod("test info");
1809 let data = sample_build_data();
1810 let result = build_det(&item, &data).expect("build_det should succeed");
1811
1812 let imposto_end = result
1813 .xml
1814 .find("</imposto>")
1815 .expect("</imposto> must exist");
1816 let devol_pos = result
1817 .xml
1818 .find("<impostoDevol>")
1819 .expect("<impostoDevol> must exist");
1820 let inf_ad_pos = result
1821 .xml
1822 .find("<infAdProd>")
1823 .expect("<infAdProd> must exist");
1824
1825 assert!(
1826 devol_pos > imposto_end,
1827 "impostoDevol must come after </imposto>"
1828 );
1829 assert!(
1830 inf_ad_pos > devol_pos,
1831 "infAdProd must come after impostoDevol"
1832 );
1833 }
1834
1835 #[test]
1836 fn no_imposto_devol_when_none() {
1837 let item = sample_item();
1838 let data = sample_build_data();
1839 let result = build_det(&item, &data).expect("build_det should succeed");
1840
1841 assert!(!result.xml.contains("<impostoDevol>"));
1842 assert_eq!(result.v_ipi_devol, 0);
1843 }
1844
1845 #[test]
1848 fn nve_single_code_produces_correct_xml() {
1849 let item = sample_item().nve("AA0001");
1850 let data = sample_build_data();
1851 let result = build_det(&item, &data).expect("build_det should succeed");
1852
1853 assert!(result.xml.contains("<NVE>AA0001</NVE>"));
1854 let ncm_pos = result.xml.find("<NCM>").expect("<NCM> must exist");
1856 let nve_pos = result
1857 .xml
1858 .find("<NVE>AA0001</NVE>")
1859 .expect("<NVE> must exist");
1860 assert!(nve_pos > ncm_pos, "NVE must come after NCM");
1861 }
1862
1863 #[test]
1864 fn nve_multiple_codes_produces_correct_xml() {
1865 let item = sample_item().nve("AA0001").nve("BB0002").nve("CC0003");
1866 let data = sample_build_data();
1867 let result = build_det(&item, &data).expect("build_det should succeed");
1868
1869 assert!(result.xml.contains("<NVE>AA0001</NVE>"));
1870 assert!(result.xml.contains("<NVE>BB0002</NVE>"));
1871 assert!(result.xml.contains("<NVE>CC0003</NVE>"));
1872 let pos_a = result.xml.find("<NVE>AA0001</NVE>").expect("AA0001");
1874 let pos_b = result.xml.find("<NVE>BB0002</NVE>").expect("BB0002");
1875 let pos_c = result.xml.find("<NVE>CC0003</NVE>").expect("CC0003");
1876 assert!(pos_a < pos_b, "NVE codes must preserve insertion order");
1877 assert!(pos_b < pos_c, "NVE codes must preserve insertion order");
1878 }
1879
1880 #[test]
1881 fn nve_eight_codes_is_valid() {
1882 let item = sample_item()
1883 .nve("AA0001")
1884 .nve("AA0002")
1885 .nve("AA0003")
1886 .nve("AA0004")
1887 .nve("AA0005")
1888 .nve("AA0006")
1889 .nve("AA0007")
1890 .nve("AA0008");
1891 let data = sample_build_data();
1892 let result = build_det(&item, &data);
1893 assert!(result.is_ok(), "8 NVE codes should be valid");
1894 let xml = result.expect("valid").xml;
1895 assert_eq!(xml.matches("<NVE>").count(), 8);
1896 }
1897
1898 #[test]
1899 fn nve_nine_codes_returns_error() {
1900 let item = sample_item()
1901 .nve("AA0001")
1902 .nve("AA0002")
1903 .nve("AA0003")
1904 .nve("AA0004")
1905 .nve("AA0005")
1906 .nve("AA0006")
1907 .nve("AA0007")
1908 .nve("AA0008")
1909 .nve("AA0009");
1910 let data = sample_build_data();
1911 let result = build_det(&item, &data);
1912 assert!(result.is_err(), "9 NVE codes should be rejected");
1913 let err = result.unwrap_err();
1914 assert_eq!(
1915 err,
1916 FiscalError::InvalidTaxData("Item 1: NVE limited to 8 entries, got 9".to_string())
1917 );
1918 }
1919
1920 #[test]
1921 fn nve_empty_vec_produces_no_nve_tags() {
1922 let item = sample_item();
1923 let data = sample_build_data();
1924 let result = build_det(&item, &data).expect("build_det should succeed");
1925
1926 assert!(!result.xml.contains("<NVE>"));
1927 }
1928
1929 #[test]
1930 fn nve_appears_before_cest() {
1931 let item = sample_item().nve("AA0001").cest("1234567");
1932 let data = sample_build_data();
1933 let result = build_det(&item, &data).expect("build_det should succeed");
1934
1935 let nve_pos = result
1936 .xml
1937 .find("<NVE>AA0001</NVE>")
1938 .expect("<NVE> must exist");
1939 let cest_pos = result.xml.find("<CEST>").expect("<CEST> must exist");
1940 assert!(nve_pos < cest_pos, "NVE must come before CEST");
1941 }
1942
1943 #[test]
1946 fn gcred_single_with_value_produces_correct_xml() {
1947 let gc = GCredData::new("SP000001", Rate4(50000)).v_cred_presumido(Cents(1500));
1948 let item = sample_item().g_cred(vec![gc]);
1949 let data = sample_build_data();
1950 let result = build_det(&item, &data).expect("build_det should succeed");
1951
1952 assert!(result.xml.contains(
1953 "<gCred><cCredPresumido>SP000001</cCredPresumido>\
1954 <pCredPresumido>5.0000</pCredPresumido>\
1955 <vCredPresumido>15.00</vCredPresumido></gCred>"
1956 ));
1957 }
1958
1959 #[test]
1960 fn gcred_without_value_omits_v_cred_presumido() {
1961 let gc = GCredData::new("RJ000002", Rate4(120000));
1962 let item = sample_item().g_cred(vec![gc]);
1963 let data = sample_build_data();
1964 let result = build_det(&item, &data).expect("build_det should succeed");
1965
1966 assert!(result.xml.contains(
1967 "<gCred>\
1968 <cCredPresumido>RJ000002</cCredPresumido>\
1969 <pCredPresumido>12.0000</pCredPresumido>\
1970 </gCred>"
1971 ));
1972 assert!(!result.xml.contains("<vCredPresumido>"));
1973 }
1974
1975 #[test]
1976 fn gcred_multiple_entries_up_to_four() {
1977 let entries = vec![
1978 GCredData::new("SP000001", Rate4(10000)).v_cred_presumido(Cents(100)),
1979 GCredData::new("SP000002", Rate4(20000)).v_cred_presumido(Cents(200)),
1980 GCredData::new("SP000003", Rate4(30000)).v_cred_presumido(Cents(300)),
1981 GCredData::new("SP000004", Rate4(40000)).v_cred_presumido(Cents(400)),
1982 ];
1983 let item = sample_item().g_cred(entries);
1984 let data = sample_build_data();
1985 let result = build_det(&item, &data).expect("build_det should succeed");
1986
1987 assert!(
1988 result
1989 .xml
1990 .contains("<cCredPresumido>SP000001</cCredPresumido>")
1991 );
1992 assert!(
1993 result
1994 .xml
1995 .contains("<cCredPresumido>SP000002</cCredPresumido>")
1996 );
1997 assert!(
1998 result
1999 .xml
2000 .contains("<cCredPresumido>SP000003</cCredPresumido>")
2001 );
2002 assert!(
2003 result
2004 .xml
2005 .contains("<cCredPresumido>SP000004</cCredPresumido>")
2006 );
2007 }
2008
2009 #[test]
2010 fn gcred_truncates_at_four_entries() {
2011 let entries = vec![
2012 GCredData::new("SP000001", Rate4(10000)),
2013 GCredData::new("SP000002", Rate4(20000)),
2014 GCredData::new("SP000003", Rate4(30000)),
2015 GCredData::new("SP000004", Rate4(40000)),
2016 GCredData::new("SP000005", Rate4(50000)),
2017 ];
2018 let item = sample_item().g_cred(entries);
2019 let data = sample_build_data();
2020 let result = build_det(&item, &data).expect("build_det should succeed");
2021
2022 assert!(
2023 result
2024 .xml
2025 .contains("<cCredPresumido>SP000004</cCredPresumido>")
2026 );
2027 assert!(
2028 !result
2029 .xml
2030 .contains("<cCredPresumido>SP000005</cCredPresumido>")
2031 );
2032 }
2033
2034 #[test]
2035 fn gcred_positioned_after_cbenef_before_cfop() {
2036 let gc = GCredData::new("MG000001", Rate4(50000)).v_cred_presumido(Cents(1000));
2037 let item = sample_item().c_benef("SEM CBENEF").g_cred(vec![gc]);
2038 let data = sample_build_data();
2039 let result = build_det(&item, &data).expect("build_det should succeed");
2040
2041 let cbenef_pos = result.xml.find("<cBenef>").expect("cBenef should exist");
2042 let gcred_pos = result.xml.find("<gCred>").expect("gCred should exist");
2043 let cfop_pos = result.xml.find("<CFOP>").expect("CFOP should exist");
2044
2045 assert!(gcred_pos > cbenef_pos, "gCred must come after cBenef");
2046 assert!(gcred_pos < cfop_pos, "gCred must come before CFOP");
2047 }
2048
2049 #[test]
2050 fn gcred_empty_vec_produces_no_gcred_tags() {
2051 let item = sample_item();
2052 let data = sample_build_data();
2053 let result = build_det(&item, &data).expect("build_det should succeed");
2054
2055 assert!(!result.xml.contains("<gCred>"));
2056 }
2057}