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 mut imposto_children: Vec<String> = Vec::new();
586 if let Some(ref v) = item.v_tot_trib {
588 if v.0 > 0 {
589 imposto_children.push(tag(
590 "vTotTrib",
591 &[],
592 TagContent::Text(&format_cents(v.0, 2)),
593 ));
594 }
595 }
596 if !icms_xml.is_empty() {
597 imposto_children.push(icms_xml);
598 }
599 if !ipi_xml.is_empty() {
600 imposto_children.push(ipi_xml);
601 }
602 if let Some(ref pis_st) = item.pis_st {
604 imposto_children.push(tax_pis_cofins_ipi::build_pis_st_xml(pis_st));
605 } else {
606 imposto_children.push(pis_xml);
607 }
608 if let Some(ref cofins_st) = item.cofins_st {
610 imposto_children.push(tax_pis_cofins_ipi::build_cofins_st_xml(cofins_st));
611 } else {
612 imposto_children.push(cofins_xml);
613 }
614 if !ii_xml.is_empty() {
615 imposto_children.push(ii_xml);
616 }
617 if !issqn_xml.is_empty() {
618 imposto_children.push(issqn_xml);
619 }
620
621 if data.schema_version.is_pl010() {
624 if let Some(ref is_data) = item.is_data {
625 imposto_children.push(tax_is::build_is_xml(is_data));
626 }
627
628 if let Some(ref ibs_cbs_data) = item.ibs_cbs {
630 imposto_children.push(tax_ibs_cbs::build_ibs_cbs_xml(ibs_cbs_data));
631 }
632 }
633
634 let fc2 = |c: i64| format_cents(c, 2);
636 let fc10 = |c: i64| format_cents(c, 10);
637 let fd4 = |v: f64| format_decimal(v, 4);
638
639 let mut prod_children = vec![
640 tag("cProd", &[], TagContent::Text(&item.product_code)),
641 tag(
642 "cEAN",
643 &[],
644 TagContent::Text(item.c_ean.as_deref().unwrap_or("SEM GTIN")),
645 ),
646 ];
647 if let Some(ref cb) = item.c_barra {
648 prod_children.push(tag("cBarra", &[], TagContent::Text(cb)));
649 }
650 prod_children.push(tag(
651 "xProd",
652 &[],
653 TagContent::Text(
654 if item.item_number == 1
656 && data.environment == SefazEnvironment::Homologation
657 && data.model == InvoiceModel::Nfce
658 {
659 HOMOLOGATION_XPROD
660 } else {
661 &item.description
662 },
663 ),
664 ));
665 prod_children.push(tag("NCM", &[], TagContent::Text(&item.ncm)));
666 for nve_code in &item.nve {
667 prod_children.push(tag("NVE", &[], TagContent::Text(nve_code)));
668 }
669 if let Some(ref cest) = item.cest {
670 prod_children.push(tag("CEST", &[], TagContent::Text(cest)));
671 if let Some(ref ind) = item.cest_ind_escala {
672 prod_children.push(tag("indEscala", &[], TagContent::Text(ind)));
673 }
674 if let Some(ref fab) = item.cest_cnpj_fab {
675 prod_children.push(tag("CNPJFab", &[], TagContent::Text(fab)));
676 }
677 }
678 if let Some(ref cb) = item.c_benef {
679 prod_children.push(tag("cBenef", &[], TagContent::Text(cb)));
680 }
681 if data.schema_version.is_pl010() {
683 if let Some(ref tp) = item.tp_cred_pres_ibs_zfm {
684 prod_children.push(tag("tpCredPresIBSZFM", &[], TagContent::Text(tp)));
685 }
686 }
687 for gc in item.g_cred.iter().take(4) {
689 let p_str = format_rate4(gc.p_cred_presumido.0);
690 let mut gc_children = vec![
691 tag(
692 "cCredPresumido",
693 &[],
694 TagContent::Text(&gc.c_cred_presumido),
695 ),
696 tag("pCredPresumido", &[], TagContent::Text(&p_str)),
697 ];
698 if let Some(v) = gc.v_cred_presumido {
699 let v_str = format_cents(v.0, 2);
700 gc_children.push(tag("vCredPresumido", &[], TagContent::Text(&v_str)));
701 }
702 prod_children.push(tag("gCred", &[], TagContent::Children(gc_children)));
703 }
704 if let Some(ref ex) = item.extipi {
705 prod_children.push(tag("EXTIPI", &[], TagContent::Text(ex)));
706 }
707 prod_children.extend([
708 tag("CFOP", &[], TagContent::Text(&item.cfop)),
709 tag("uCom", &[], TagContent::Text(&item.unit_of_measure)),
710 tag("qCom", &[], TagContent::Text(&fd4(item.quantity))),
711 tag("vUnCom", &[], TagContent::Text(&fc10(item.unit_price.0))),
712 tag("vProd", &[], TagContent::Text(&fc2(item.total_price.0))),
713 tag(
714 "cEANTrib",
715 &[],
716 TagContent::Text(item.c_ean_trib.as_deref().unwrap_or("SEM GTIN")),
717 ),
718 ]);
719 if let Some(ref cbt) = item.c_barra_trib {
720 prod_children.push(tag("cBarraTrib", &[], TagContent::Text(cbt)));
721 }
722 let u_trib = item
723 .taxable_unit
724 .as_deref()
725 .unwrap_or(&item.unit_of_measure);
726 let q_trib = item.taxable_quantity.unwrap_or(item.quantity);
727 let v_un_trib = item
728 .taxable_unit_price
729 .map(|c| c.0)
730 .unwrap_or(item.unit_price.0);
731 prod_children.extend([
732 tag("uTrib", &[], TagContent::Text(u_trib)),
733 tag("qTrib", &[], TagContent::Text(&fd4(q_trib))),
734 tag("vUnTrib", &[], TagContent::Text(&fc10(v_un_trib))),
735 ]);
736 if let Some(v) = item.v_frete {
737 prod_children.push(tag("vFrete", &[], TagContent::Text(&fc2(v.0))));
738 }
739 if let Some(v) = item.v_seg {
740 prod_children.push(tag("vSeg", &[], TagContent::Text(&fc2(v.0))));
741 }
742 if let Some(v) = item.v_desc {
743 prod_children.push(tag("vDesc", &[], TagContent::Text(&fc2(v.0))));
744 }
745 if let Some(v) = item.v_outro {
746 prod_children.push(tag("vOutro", &[], TagContent::Text(&fc2(v.0))));
747 }
748 let ind_tot_str = match item.ind_tot {
749 Some(v) => v.to_string(),
750 None => "1".to_string(),
751 };
752 prod_children.push(tag("indTot", &[], TagContent::Text(&ind_tot_str)));
753 if item.ind_bem_movel_usado == Some(true) {
754 prod_children.push(tag("indBemMovelUsado", &[], TagContent::Text("1")));
755 }
756 if let Some(ref dis) = item.di {
758 for di in dis.iter().take(100) {
759 prod_children.push(build_di_xml(di));
760 }
761 }
762 if let Some(ref exports) = item.det_export {
764 for dex in exports.iter().take(500) {
765 prod_children.push(build_det_export_xml(dex));
766 }
767 }
768 if let Some(ref xped) = item.x_ped {
769 prod_children.push(tag("xPed", &[], TagContent::Text(xped)));
770 }
771 if let Some(ref nip) = item.n_item_ped {
772 prod_children.push(tag("nItemPed", &[], TagContent::Text(nip)));
773 }
774 if let Some(ref nfci) = item.n_fci {
775 prod_children.push(tag("nFCI", &[], TagContent::Text(nfci)));
776 }
777 prod_children.extend(prod_options);
778
779 let mut v_ipi_devol = 0i64;
781 let imposto_devol_xml = if let Some(ref devol) = item.imposto_devol {
782 v_ipi_devol = devol.v_ipi_devol.0;
783 let p_devol_str = format_decimal(devol.p_devol.0 as f64 / 100.0, 2);
784 let v_ipi_devol_str = format_cents(devol.v_ipi_devol.0, 2);
785 tag(
786 "impostoDevol",
787 &[],
788 TagContent::Children(vec![
789 tag("pDevol", &[], TagContent::Text(&p_devol_str)),
790 tag(
791 "IPI",
792 &[],
793 TagContent::Children(vec![tag(
794 "vIPIDevol",
795 &[],
796 TagContent::Text(&v_ipi_devol_str),
797 )]),
798 ),
799 ]),
800 )
801 } else {
802 String::new()
803 };
804
805 let v_item_xml =
808 if data.schema_version.is_pl010() && data.items.iter().any(|i| i.ibs_cbs.is_some()) {
809 let v_item_cents = if let Some(ref explicit) = item.v_item {
810 explicit.0
812 } else {
813 let v_prod = item.total_price.0;
815 let v_desc = item.v_desc.map(|c| c.0).unwrap_or(0);
816 let v_icms_deson = if item_ind_deduz_deson {
817 item.icms_v_icms_deson.map(|c| c.0).unwrap_or(0)
818 } else {
819 0
820 };
821 let v_icms_st = icms_totals.v_st.0;
822 let v_icms_mono_reten = icms_totals.v_icms_mono_reten.0;
823 let v_fcp_st = icms_totals.v_fcp_st.0;
824 let v_frete = item.v_frete.map(|c| c.0).unwrap_or(0);
825 let v_seg = item.v_seg.map(|c| c.0).unwrap_or(0);
826 let v_outro = item.v_outro.map(|c| c.0).unwrap_or(0);
827
828 v_prod - v_desc - v_icms_deson
829 + v_icms_st
830 + v_icms_mono_reten
831 + v_fcp_st
832 + v_frete
833 + v_seg
834 + v_outro
835 + v_ii
836 + v_ipi
837 + v_ipi_devol
838 + v_pis_st
839 + v_cofins_st
840 };
841 let v_item_str = format_cents(v_item_cents, 2);
842 tag("vItem", &[], TagContent::Text(&v_item_str))
843 } else {
844 String::new()
845 };
846
847 let det_extras = build_det_extras(item, &v_item_xml);
849
850 let nitem = item.item_number.to_string();
852 let mut det_children = vec![
853 tag("prod", &[], TagContent::Children(prod_children)),
854 tag("imposto", &[], TagContent::Children(imposto_children)),
855 ];
856 if !imposto_devol_xml.is_empty() {
857 det_children.push(imposto_devol_xml);
858 }
859 det_children.extend(det_extras);
860
861 let xml = tag(
862 "det",
863 &[("nItem", &nitem)],
864 TagContent::Children(det_children),
865 );
866
867 Ok(DetResult {
868 xml,
869 icms_totals,
870 v_ipi,
871 v_pis: item.pis_v_pis.map(|c| c.0).unwrap_or(0),
872 v_cofins: item.cofins_v_cofins.map(|c| c.0).unwrap_or(0),
873 v_ii,
874 v_frete: item.v_frete.map(|c| c.0).unwrap_or(0),
875 v_seg: item.v_seg.map(|c| c.0).unwrap_or(0),
876 v_desc: item.v_desc.map(|c| c.0).unwrap_or(0),
877 v_outro: item.v_outro.map(|c| c.0).unwrap_or(0),
878 ind_tot: item.ind_tot.unwrap_or(1),
879 v_tot_trib: item.v_tot_trib.map(|c| c.0).unwrap_or(0),
880 v_ipi_devol,
881 v_pis_st,
882 v_cofins_st,
883 ind_deduz_deson: item_ind_deduz_deson,
884 has_issqn,
885 })
886}
887
888fn build_prod_options(item: &InvoiceItemData) -> Vec<String> {
889 let mut opts = Vec::new();
890
891 if let Some(ref rastros) = item.rastro {
893 for r in rastros.iter().take(500) {
894 let mut rastro_children = vec![
895 tag("nLote", &[], TagContent::Text(&r.n_lote)),
896 tag("qLote", &[], TagContent::Text(&format_decimal(r.q_lote, 3))),
897 tag("dFab", &[], TagContent::Text(&r.d_fab)),
898 tag("dVal", &[], TagContent::Text(&r.d_val)),
899 ];
900 if let Some(ref agreg) = r.c_agreg {
901 rastro_children.push(tag("cAgreg", &[], TagContent::Text(agreg)));
902 }
903 opts.push(tag("rastro", &[], TagContent::Children(rastro_children)));
904 }
905 }
906
907 if let Some(ref v) = item.veic_prod {
909 opts.push(tag(
910 "veicProd",
911 &[],
912 TagContent::Children(vec![
913 tag("tpOp", &[], TagContent::Text(&v.tp_op)),
914 tag("chassi", &[], TagContent::Text(&v.chassi)),
915 tag("cCor", &[], TagContent::Text(&v.c_cor)),
916 tag("xCor", &[], TagContent::Text(&v.x_cor)),
917 tag("pot", &[], TagContent::Text(&v.pot)),
918 tag("cilin", &[], TagContent::Text(&v.cilin)),
919 tag("pesoL", &[], TagContent::Text(&v.peso_l)),
920 tag("pesoB", &[], TagContent::Text(&v.peso_b)),
921 tag("nSerie", &[], TagContent::Text(&v.n_serie)),
922 tag("tpComb", &[], TagContent::Text(&v.tp_comb)),
923 tag("nMotor", &[], TagContent::Text(&v.n_motor)),
924 tag("CMT", &[], TagContent::Text(&v.cmt)),
925 tag("dist", &[], TagContent::Text(&v.dist)),
926 tag("anoMod", &[], TagContent::Text(&v.ano_mod)),
927 tag("anoFab", &[], TagContent::Text(&v.ano_fab)),
928 tag("tpPint", &[], TagContent::Text(&v.tp_pint)),
929 tag("tpVeic", &[], TagContent::Text(&v.tp_veic)),
930 tag("espVeic", &[], TagContent::Text(&v.esp_veic)),
931 tag("VIN", &[], TagContent::Text(&v.vin)),
932 tag("condVeic", &[], TagContent::Text(&v.cond_veic)),
933 tag("cMod", &[], TagContent::Text(&v.c_mod)),
934 tag("cCorDENATRAN", &[], TagContent::Text(&v.c_cor_denatran)),
935 tag("lota", &[], TagContent::Text(&v.lota)),
936 tag("tpRest", &[], TagContent::Text(&v.tp_rest)),
937 ]),
938 ));
939 } else if let Some(ref m) = item.med {
940 let mut med_children = Vec::new();
941 if let Some(ref code) = m.c_prod_anvisa {
942 med_children.push(tag("cProdANVISA", &[], TagContent::Text(code)));
943 }
944 if let Some(ref reason) = m.x_motivo_isencao {
945 med_children.push(tag("xMotivoIsencao", &[], TagContent::Text(reason)));
946 }
947 med_children.push(tag(
948 "vPMC",
949 &[],
950 TagContent::Text(&format_cents(m.v_pmc.0, 2)),
951 ));
952 opts.push(tag("med", &[], TagContent::Children(med_children)));
953 } else if let Some(ref arms) = item.arma {
954 for a in arms.iter().take(500) {
955 opts.push(tag(
956 "arma",
957 &[],
958 TagContent::Children(vec![
959 tag("tpArma", &[], TagContent::Text(&a.tp_arma)),
960 tag("nSerie", &[], TagContent::Text(&a.n_serie)),
961 tag("nCano", &[], TagContent::Text(&a.n_cano)),
962 tag("descr", &[], TagContent::Text(&a.descr)),
963 ]),
964 ));
965 }
966 } else if let Some(ref recopi) = item.n_recopi {
967 if !recopi.is_empty() {
968 opts.push(tag("nRECOPI", &[], TagContent::Text(recopi)));
969 }
970 }
971
972 if let Some(ref comb) = item.comb {
974 opts.push(build_comb_xml(comb));
975 }
976
977 opts
978}
979
980fn build_di_xml(di: &crate::types::DiData) -> String {
982 let mut children = vec![
983 tag("nDI", &[], TagContent::Text(&di.n_di)),
984 tag("dDI", &[], TagContent::Text(&di.d_di)),
985 tag("xLocDesemb", &[], TagContent::Text(&di.x_loc_desemb)),
986 tag("UFDesemb", &[], TagContent::Text(&di.uf_desemb)),
987 tag("dDesemb", &[], TagContent::Text(&di.d_desemb)),
988 tag("tpViaTransp", &[], TagContent::Text(&di.tp_via_transp)),
989 ];
990 if let Some(ref v) = di.v_afrmm {
991 children.push(tag("vAFRMM", &[], TagContent::Text(&format_cents(v.0, 2))));
992 }
993 children.push(tag(
994 "tpIntermedio",
995 &[],
996 TagContent::Text(&di.tp_intermedio),
997 ));
998 if let Some(ref cnpj) = di.cnpj {
999 children.push(tag("CNPJ", &[], TagContent::Text(cnpj)));
1000 } else if let Some(ref cpf) = di.cpf {
1001 children.push(tag("CPF", &[], TagContent::Text(cpf)));
1002 }
1003 if let Some(ref uf) = di.uf_terceiro {
1004 children.push(tag("UFTerceiro", &[], TagContent::Text(uf)));
1005 }
1006 children.push(tag("cExportador", &[], TagContent::Text(&di.c_exportador)));
1007 for adi in di.adi.iter().take(999) {
1009 let mut adi_children = Vec::new();
1010 if let Some(ref n) = adi.n_adicao {
1011 adi_children.push(tag("nAdicao", &[], TagContent::Text(n)));
1012 }
1013 adi_children.push(tag("nSeqAdic", &[], TagContent::Text(&adi.n_seq_adic)));
1014 adi_children.push(tag("cFabricante", &[], TagContent::Text(&adi.c_fabricante)));
1015 if let Some(ref v) = adi.v_desc_di {
1016 adi_children.push(tag("vDescDI", &[], TagContent::Text(&format_cents(v.0, 2))));
1017 }
1018 if let Some(ref n) = adi.n_draw {
1019 adi_children.push(tag("nDraw", &[], TagContent::Text(n)));
1020 }
1021 children.push(tag("adi", &[], TagContent::Children(adi_children)));
1022 }
1023 tag("DI", &[], TagContent::Children(children))
1024}
1025
1026fn build_det_export_xml(dex: &crate::types::DetExportData) -> String {
1028 let mut children = Vec::new();
1029 if let Some(ref n) = dex.n_draw {
1030 children.push(tag("nDraw", &[], TagContent::Text(n)));
1031 }
1032 if dex.n_re.is_some() || dex.ch_nfe.is_some() || dex.q_export.is_some() {
1033 let mut exp_ind_children = Vec::new();
1034 if let Some(ref n) = dex.n_re {
1035 exp_ind_children.push(tag("nRE", &[], TagContent::Text(n)));
1036 }
1037 if let Some(ref ch) = dex.ch_nfe {
1038 exp_ind_children.push(tag("chNFe", &[], TagContent::Text(ch)));
1039 }
1040 if let Some(q) = dex.q_export {
1041 exp_ind_children.push(tag("qExport", &[], TagContent::Text(&format_decimal(q, 4))));
1042 }
1043 children.push(tag(
1044 "exportInd",
1045 &[],
1046 TagContent::Children(exp_ind_children),
1047 ));
1048 }
1049 tag("detExport", &[], TagContent::Children(children))
1050}
1051
1052fn build_comb_xml(comb: &CombData) -> String {
1058 let mut children = vec![
1059 tag("cProdANP", &[], TagContent::Text(&comb.c_prod_anp)),
1060 tag("descANP", &[], TagContent::Text(&comb.desc_anp)),
1061 ];
1062
1063 if let Some(ref v) = comb.p_glp {
1064 children.push(tag("pGLP", &[], TagContent::Text(v)));
1065 }
1066 if let Some(ref v) = comb.p_gn_n {
1067 children.push(tag("pGNn", &[], TagContent::Text(v)));
1068 }
1069 if let Some(ref v) = comb.p_gn_i {
1070 children.push(tag("pGNi", &[], TagContent::Text(v)));
1071 }
1072 if let Some(ref v) = comb.v_part {
1073 children.push(tag("vPart", &[], TagContent::Text(v)));
1074 }
1075 if let Some(ref v) = comb.codif {
1076 children.push(tag("CODIF", &[], TagContent::Text(v)));
1077 }
1078 if let Some(ref v) = comb.q_temp {
1079 children.push(tag("qTemp", &[], TagContent::Text(v)));
1080 }
1081
1082 children.push(tag("UFCons", &[], TagContent::Text(&comb.uf_cons)));
1083
1084 if let Some(ref cide) = comb.cide {
1086 let cide_children = vec![
1087 tag("qBCProd", &[], TagContent::Text(&cide.q_bc_prod)),
1088 tag("vAliqProd", &[], TagContent::Text(&cide.v_aliq_prod)),
1089 tag("vCIDE", &[], TagContent::Text(&cide.v_cide)),
1090 ];
1091 children.push(tag("CIDE", &[], TagContent::Children(cide_children)));
1092 }
1093
1094 if let Some(ref enc) = comb.encerrante {
1096 let mut enc_children = vec![tag("nBico", &[], TagContent::Text(&enc.n_bico))];
1097 if let Some(ref bomba) = enc.n_bomba {
1098 enc_children.push(tag("nBomba", &[], TagContent::Text(bomba)));
1099 }
1100 enc_children.push(tag("nTanque", &[], TagContent::Text(&enc.n_tanque)));
1101 enc_children.push(tag("vEncIni", &[], TagContent::Text(&enc.v_enc_ini)));
1102 enc_children.push(tag("vEncFin", &[], TagContent::Text(&enc.v_enc_fin)));
1103 children.push(tag("encerrante", &[], TagContent::Children(enc_children)));
1104 }
1105
1106 if let Some(ref v) = comb.p_bio {
1108 children.push(tag("pBio", &[], TagContent::Text(v)));
1109 }
1110
1111 if let Some(ref origins) = comb.orig_comb {
1113 for orig in origins {
1114 let orig_children = vec![
1115 tag("indImport", &[], TagContent::Text(&orig.ind_import)),
1116 tag("cUFOrig", &[], TagContent::Text(&orig.c_uf_orig)),
1117 tag("pOrig", &[], TagContent::Text(&orig.p_orig)),
1118 ];
1119 children.push(tag("origComb", &[], TagContent::Children(orig_children)));
1120 }
1121 }
1122
1123 tag("comb", &[], TagContent::Children(children))
1124}
1125
1126fn build_det_extras(item: &InvoiceItemData, v_item_xml: &str) -> Vec<String> {
1127 let mut extras = Vec::new();
1128
1129 if let Some(ref info) = item.inf_ad_prod {
1130 extras.push(tag("infAdProd", &[], TagContent::Text(info)));
1131 }
1132
1133 if let Some(ref obs) = item.obs_item {
1134 let mut obs_children = Vec::new();
1135 if let Some(ref cont) = obs.obs_cont {
1136 obs_children.push(tag(
1137 "obsCont",
1138 &[("xCampo", &cont.x_campo)],
1139 TagContent::Children(vec![tag("xTexto", &[], TagContent::Text(&cont.x_texto))]),
1140 ));
1141 }
1142 if let Some(ref fisco) = obs.obs_fisco {
1143 obs_children.push(tag(
1144 "obsFisco",
1145 &[("xCampo", &fisco.x_campo)],
1146 TagContent::Children(vec![tag("xTexto", &[], TagContent::Text(&fisco.x_texto))]),
1147 ));
1148 }
1149 extras.push(tag("obsItem", &[], TagContent::Children(obs_children)));
1150 }
1151
1152 if !v_item_xml.is_empty() {
1154 extras.push(v_item_xml.to_string());
1155 }
1156
1157 if let Some(ref dfe) = item.dfe_referenciado {
1158 let mut dfe_children = vec![tag("chaveAcesso", &[], TagContent::Text(&dfe.chave_acesso))];
1159 if let Some(ref n) = dfe.n_item {
1160 dfe_children.push(tag("nItem", &[], TagContent::Text(n)));
1161 }
1162 extras.push(tag(
1163 "DFeReferenciado",
1164 &[],
1165 TagContent::Children(dfe_children),
1166 ));
1167 }
1168
1169 extras
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174 use super::*;
1175 use crate::newtypes::{Cents, IbgeCode, Rate, Rate4};
1176 use crate::tax_issqn::IssqnData as TaxIssqnData;
1177 use crate::types::{
1178 ArmaData, CideData, CombData, EncerranteData, GCredData, InvoiceItemData, InvoiceModel,
1179 IssuerData, MedData, OrigCombData, RastroData, SefazEnvironment, TaxRegime, VeicProdData,
1180 };
1181
1182 fn sample_build_data() -> InvoiceBuildData {
1183 let issuer = IssuerData::new(
1184 "12345678000199",
1185 "123456789",
1186 "Test Company",
1187 TaxRegime::SimplesNacional,
1188 "SP",
1189 IbgeCode("3550308".to_string()),
1190 "Sao Paulo",
1191 "Av Paulista",
1192 "1000",
1193 "Bela Vista",
1194 "01310100",
1195 );
1196
1197 InvoiceBuildData {
1198 schema_version: crate::types::SchemaVersion::PL009,
1199 model: InvoiceModel::Nfe,
1200 series: 1,
1201 number: 1,
1202 emission_type: crate::types::EmissionType::Normal,
1203 environment: SefazEnvironment::Homologation,
1204 issued_at: chrono::Utc::now()
1205 .with_timezone(&chrono::FixedOffset::west_opt(3 * 3600).expect("valid offset")),
1206 operation_nature: "VENDA".to_string(),
1207 issuer,
1208 recipient: None,
1209 items: Vec::new(),
1210 payments: Vec::new(),
1211 change_amount: None,
1212 payment_card_details: None,
1213 contingency: None,
1214 exit_at: None,
1215 operation_type: None,
1216 purpose_code: None,
1217 intermediary_indicator: None,
1218 emission_process: None,
1219 consumer_type: None,
1220 buyer_presence: None,
1221 print_format: None,
1222 references: None,
1223 transport: None,
1224 billing: None,
1225 withdrawal: None,
1226 delivery: None,
1227 authorized_xml: None,
1228 additional_info: None,
1229 intermediary: None,
1230 ret_trib: None,
1231 tech_responsible: None,
1232 purchase: None,
1233 export: None,
1234 issqn_tot: None,
1235 cana: None,
1236 agropecuario: None,
1237 compra_gov: None,
1238 pag_antecipado: None,
1239 is_tot: None,
1240 ibs_cbs_tot: None,
1241 v_nf_tot_override: None,
1242 destination_indicator: None,
1243 ver_proc: None,
1244 only_ascii: false,
1245 calculation_method: crate::types::CalculationMethod::V2,
1246 }
1247 }
1248
1249 fn sample_item() -> InvoiceItemData {
1250 InvoiceItemData::new(
1251 1,
1252 "001",
1253 "Gasolina Comum",
1254 "27101259",
1255 "5102",
1256 "LT",
1257 50.0,
1258 Cents(599),
1259 Cents(29950),
1260 "102",
1261 Rate(0),
1262 Cents(0),
1263 "99",
1264 "99",
1265 )
1266 }
1267
1268 #[test]
1271 fn comb_minimal_produces_correct_xml() {
1272 let comb = CombData::new("210203001", "GLP", "SP");
1273 let xml = build_comb_xml(&comb);
1274
1275 assert_eq!(
1276 xml,
1277 "<comb>\
1278 <cProdANP>210203001</cProdANP>\
1279 <descANP>GLP</descANP>\
1280 <UFCons>SP</UFCons>\
1281 </comb>"
1282 );
1283 }
1284
1285 #[test]
1286 fn comb_with_glp_percentages() {
1287 let comb = CombData::new("210203001", "GLP", "SP")
1288 .p_glp("60.0000")
1289 .p_gn_n("25.0000")
1290 .p_gn_i("15.0000")
1291 .v_part("3.50");
1292
1293 let xml = build_comb_xml(&comb);
1294
1295 assert_eq!(
1296 xml,
1297 "<comb>\
1298 <cProdANP>210203001</cProdANP>\
1299 <descANP>GLP</descANP>\
1300 <pGLP>60.0000</pGLP>\
1301 <pGNn>25.0000</pGNn>\
1302 <pGNi>15.0000</pGNi>\
1303 <vPart>3.50</vPart>\
1304 <UFCons>SP</UFCons>\
1305 </comb>"
1306 );
1307 }
1308
1309 #[test]
1310 fn comb_with_codif_and_qtemp() {
1311 let comb = CombData::new("320102001", "GASOLINA COMUM", "PR")
1312 .codif("123456789")
1313 .q_temp("1000.0000");
1314
1315 let xml = build_comb_xml(&comb);
1316
1317 assert_eq!(
1318 xml,
1319 "<comb>\
1320 <cProdANP>320102001</cProdANP>\
1321 <descANP>GASOLINA COMUM</descANP>\
1322 <CODIF>123456789</CODIF>\
1323 <qTemp>1000.0000</qTemp>\
1324 <UFCons>PR</UFCons>\
1325 </comb>"
1326 );
1327 }
1328
1329 #[test]
1330 fn comb_with_cide() {
1331 let cide = CideData::new("1000.0000", "0.0700", "70.00");
1332 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").cide(cide);
1333
1334 let xml = build_comb_xml(&comb);
1335
1336 assert_eq!(
1337 xml,
1338 "<comb>\
1339 <cProdANP>320102001</cProdANP>\
1340 <descANP>GASOLINA COMUM</descANP>\
1341 <UFCons>SP</UFCons>\
1342 <CIDE>\
1343 <qBCProd>1000.0000</qBCProd>\
1344 <vAliqProd>0.0700</vAliqProd>\
1345 <vCIDE>70.00</vCIDE>\
1346 </CIDE>\
1347 </comb>"
1348 );
1349 }
1350
1351 #[test]
1352 fn comb_with_encerrante() {
1353 let enc = EncerranteData::new("1", "1", "1234.567", "1284.567").n_bomba("2");
1354 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").encerrante(enc);
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 <encerrante>\
1365 <nBico>1</nBico>\
1366 <nBomba>2</nBomba>\
1367 <nTanque>1</nTanque>\
1368 <vEncIni>1234.567</vEncIni>\
1369 <vEncFin>1284.567</vEncFin>\
1370 </encerrante>\
1371 </comb>"
1372 );
1373 }
1374
1375 #[test]
1376 fn comb_encerrante_without_bomba() {
1377 let enc = EncerranteData::new("3", "2", "5000.000", "5050.000");
1378 let comb = CombData::new("320102001", "GASOLINA COMUM", "RJ").encerrante(enc);
1379
1380 let xml = build_comb_xml(&comb);
1381
1382 assert_eq!(
1383 xml,
1384 "<comb>\
1385 <cProdANP>320102001</cProdANP>\
1386 <descANP>GASOLINA COMUM</descANP>\
1387 <UFCons>RJ</UFCons>\
1388 <encerrante>\
1389 <nBico>3</nBico>\
1390 <nTanque>2</nTanque>\
1391 <vEncIni>5000.000</vEncIni>\
1392 <vEncFin>5050.000</vEncFin>\
1393 </encerrante>\
1394 </comb>"
1395 );
1396 }
1397
1398 #[test]
1399 fn comb_with_pbio() {
1400 let comb = CombData::new("810102001", "OLEO DIESEL B S10", "SP").p_bio("15.0000");
1401
1402 let xml = build_comb_xml(&comb);
1403
1404 assert_eq!(
1405 xml,
1406 "<comb>\
1407 <cProdANP>810102001</cProdANP>\
1408 <descANP>OLEO DIESEL B S10</descANP>\
1409 <UFCons>SP</UFCons>\
1410 <pBio>15.0000</pBio>\
1411 </comb>"
1412 );
1413 }
1414
1415 #[test]
1416 fn comb_with_orig_comb_single() {
1417 let orig = OrigCombData::new("0", "35", "100.0000");
1418 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").orig_comb(vec![orig]);
1419
1420 let xml = build_comb_xml(&comb);
1421
1422 assert_eq!(
1423 xml,
1424 "<comb>\
1425 <cProdANP>320102001</cProdANP>\
1426 <descANP>GASOLINA COMUM</descANP>\
1427 <UFCons>SP</UFCons>\
1428 <origComb>\
1429 <indImport>0</indImport>\
1430 <cUFOrig>35</cUFOrig>\
1431 <pOrig>100.0000</pOrig>\
1432 </origComb>\
1433 </comb>"
1434 );
1435 }
1436
1437 #[test]
1438 fn comb_with_orig_comb_multiple() {
1439 let orig1 = OrigCombData::new("0", "35", "70.0000");
1440 let orig2 = OrigCombData::new("1", "99", "30.0000");
1441 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").orig_comb(vec![orig1, orig2]);
1442
1443 let xml = build_comb_xml(&comb);
1444
1445 assert_eq!(
1446 xml,
1447 "<comb>\
1448 <cProdANP>320102001</cProdANP>\
1449 <descANP>GASOLINA COMUM</descANP>\
1450 <UFCons>SP</UFCons>\
1451 <origComb>\
1452 <indImport>0</indImport>\
1453 <cUFOrig>35</cUFOrig>\
1454 <pOrig>70.0000</pOrig>\
1455 </origComb>\
1456 <origComb>\
1457 <indImport>1</indImport>\
1458 <cUFOrig>99</cUFOrig>\
1459 <pOrig>30.0000</pOrig>\
1460 </origComb>\
1461 </comb>"
1462 );
1463 }
1464
1465 #[test]
1466 fn comb_full_with_all_fields() {
1467 let cide = CideData::new("500.0000", "0.0700", "35.00");
1468 let enc = EncerranteData::new("1", "1", "10000.000", "10050.000").n_bomba("1");
1469 let orig = OrigCombData::new("0", "35", "100.0000");
1470
1471 let comb = CombData::new("210203001", "GLP", "SP")
1472 .p_glp("60.0000")
1473 .p_gn_n("25.0000")
1474 .p_gn_i("15.0000")
1475 .v_part("3.50")
1476 .codif("999888777")
1477 .q_temp("500.0000")
1478 .cide(cide)
1479 .encerrante(enc)
1480 .p_bio("12.0000")
1481 .orig_comb(vec![orig]);
1482
1483 let xml = build_comb_xml(&comb);
1484
1485 assert_eq!(
1486 xml,
1487 "<comb>\
1488 <cProdANP>210203001</cProdANP>\
1489 <descANP>GLP</descANP>\
1490 <pGLP>60.0000</pGLP>\
1491 <pGNn>25.0000</pGNn>\
1492 <pGNi>15.0000</pGNi>\
1493 <vPart>3.50</vPart>\
1494 <CODIF>999888777</CODIF>\
1495 <qTemp>500.0000</qTemp>\
1496 <UFCons>SP</UFCons>\
1497 <CIDE>\
1498 <qBCProd>500.0000</qBCProd>\
1499 <vAliqProd>0.0700</vAliqProd>\
1500 <vCIDE>35.00</vCIDE>\
1501 </CIDE>\
1502 <encerrante>\
1503 <nBico>1</nBico>\
1504 <nBomba>1</nBomba>\
1505 <nTanque>1</nTanque>\
1506 <vEncIni>10000.000</vEncIni>\
1507 <vEncFin>10050.000</vEncFin>\
1508 </encerrante>\
1509 <pBio>12.0000</pBio>\
1510 <origComb>\
1511 <indImport>0</indImport>\
1512 <cUFOrig>35</cUFOrig>\
1513 <pOrig>100.0000</pOrig>\
1514 </origComb>\
1515 </comb>"
1516 );
1517 }
1518
1519 #[test]
1520 fn comb_in_det_xml() {
1521 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP");
1522 let item = sample_item().comb(comb);
1523 let data = sample_build_data();
1524 let result = build_det(&item, &data).expect("build_det should succeed");
1525
1526 let prod_start = result.xml.find("<prod>").expect("<prod> must exist");
1528 let prod_end = result.xml.find("</prod>").expect("</prod> must exist");
1529 let prod_section = &result.xml[prod_start..prod_end];
1530
1531 assert!(prod_section.contains("<comb>"));
1532 assert!(prod_section.contains("<cProdANP>320102001</cProdANP>"));
1533 assert!(prod_section.contains("<descANP>GASOLINA COMUM</descANP>"));
1534 assert!(prod_section.contains("<UFCons>SP</UFCons>"));
1535 assert!(prod_section.contains("</comb>"));
1536 }
1537
1538 #[test]
1541 fn issqn_item_produces_issqn_tag_not_icms() {
1542 let issqn_data = TaxIssqnData::new(10000, 500, 500, "3550308", "14.01")
1543 .ind_iss("1")
1544 .ind_incentivo("2");
1545 let item = sample_item().issqn(issqn_data);
1546 let data = sample_build_data();
1547 let result = build_det(&item, &data).expect("build_det should succeed");
1548
1549 assert!(result.xml.contains("<ISSQN>"));
1551 assert!(result.xml.contains("<vBC>100.00</vBC>"));
1552 assert!(result.xml.contains("<vAliq>5.0000</vAliq>"));
1553 assert!(result.xml.contains("<vISSQN>5.00</vISSQN>"));
1554 assert!(result.xml.contains("<cMunFG>3550308</cMunFG>"));
1555 assert!(result.xml.contains("<cListServ>14.01</cListServ>"));
1556 assert!(result.xml.contains("<indISS>1</indISS>"));
1557 assert!(result.xml.contains("<indIncentivo>2</indIncentivo>"));
1558 assert!(result.xml.contains("</ISSQN>"));
1559
1560 assert!(!result.xml.contains("<ICMS>"));
1562 assert!(!result.xml.contains("</ICMS>"));
1563 assert!(result.has_issqn);
1564 }
1565
1566 #[test]
1567 fn issqn_item_with_all_optional_fields() {
1568 let issqn_data = TaxIssqnData::new(20000, 300, 600, "3304557", "07.02")
1569 .v_deducao(1000)
1570 .v_outro(500)
1571 .v_desc_incond(200)
1572 .v_desc_cond(100)
1573 .v_iss_ret(300)
1574 .ind_iss("1")
1575 .c_servico("1234")
1576 .c_mun("3304557")
1577 .c_pais("1058")
1578 .n_processo("ABC123")
1579 .ind_incentivo("1");
1580
1581 let item = sample_item().issqn(issqn_data);
1582 let data = sample_build_data();
1583 let result = build_det(&item, &data).expect("build_det should succeed");
1584
1585 assert!(result.xml.contains("<vBC>200.00</vBC>"));
1586 assert!(result.xml.contains("<vAliq>3.0000</vAliq>"));
1587 assert!(result.xml.contains("<vISSQN>6.00</vISSQN>"));
1588 assert!(result.xml.contains("<vDeducao>10.00</vDeducao>"));
1589 assert!(result.xml.contains("<vOutro>5.00</vOutro>"));
1590 assert!(result.xml.contains("<vDescIncond>2.00</vDescIncond>"));
1591 assert!(result.xml.contains("<vDescCond>1.00</vDescCond>"));
1592 assert!(result.xml.contains("<vISSRet>3.00</vISSRet>"));
1593 assert!(result.xml.contains("<cServico>1234</cServico>"));
1594 assert!(result.xml.contains("<cMun>3304557</cMun>"));
1595 assert!(result.xml.contains("<cPais>1058</cPais>"));
1596 assert!(result.xml.contains("<nProcesso>ABC123</nProcesso>"));
1597 assert!(result.xml.contains("<indIncentivo>1</indIncentivo>"));
1598 assert!(result.has_issqn);
1599 }
1600
1601 #[test]
1602 fn non_issqn_item_has_icms_and_no_issqn() {
1603 let item = sample_item();
1604 let data = sample_build_data();
1605 let result = build_det(&item, &data).expect("build_det should succeed");
1606
1607 assert!(result.xml.contains("<ICMS"));
1608 assert!(!result.xml.contains("<ISSQN>"));
1609 assert!(!result.has_issqn);
1610 }
1611
1612 #[test]
1615 fn di_minimal_with_one_adi() {
1616 use crate::types::{AdiData, DiData};
1617
1618 let adi = AdiData::new("1", "FABRICANTE_X").n_adicao("001");
1619 let di = DiData::new(
1620 "1234567890",
1621 "2025-01-15",
1622 "Santos",
1623 "SP",
1624 "2025-01-20",
1625 "1",
1626 "1",
1627 "EXP001",
1628 vec![adi],
1629 );
1630 let xml = build_di_xml(&di);
1631
1632 assert_eq!(
1633 xml,
1634 "<DI>\
1635 <nDI>1234567890</nDI>\
1636 <dDI>2025-01-15</dDI>\
1637 <xLocDesemb>Santos</xLocDesemb>\
1638 <UFDesemb>SP</UFDesemb>\
1639 <dDesemb>2025-01-20</dDesemb>\
1640 <tpViaTransp>1</tpViaTransp>\
1641 <tpIntermedio>1</tpIntermedio>\
1642 <cExportador>EXP001</cExportador>\
1643 <adi>\
1644 <nAdicao>001</nAdicao>\
1645 <nSeqAdic>1</nSeqAdic>\
1646 <cFabricante>FABRICANTE_X</cFabricante>\
1647 </adi>\
1648 </DI>"
1649 );
1650 }
1651
1652 #[test]
1653 fn di_with_all_optional_fields() {
1654 use crate::types::{AdiData, DiData};
1655
1656 let adi = AdiData::new("1", "FAB_Y")
1657 .n_adicao("002")
1658 .v_desc_di(Cents(15000))
1659 .n_draw("20259999999");
1660 let di = DiData::new(
1661 "DI-2025-001",
1662 "2025-03-01",
1663 "Paranagua",
1664 "PR",
1665 "2025-03-05",
1666 "1",
1667 "2",
1668 "EXP002",
1669 vec![adi],
1670 )
1671 .v_afrmm(Cents(5000))
1672 .cnpj("12345678000199")
1673 .uf_terceiro("RJ");
1674
1675 let xml = build_di_xml(&di);
1676
1677 assert_eq!(
1678 xml,
1679 "<DI>\
1680 <nDI>DI-2025-001</nDI>\
1681 <dDI>2025-03-01</dDI>\
1682 <xLocDesemb>Paranagua</xLocDesemb>\
1683 <UFDesemb>PR</UFDesemb>\
1684 <dDesemb>2025-03-05</dDesemb>\
1685 <tpViaTransp>1</tpViaTransp>\
1686 <vAFRMM>50.00</vAFRMM>\
1687 <tpIntermedio>2</tpIntermedio>\
1688 <CNPJ>12345678000199</CNPJ>\
1689 <UFTerceiro>RJ</UFTerceiro>\
1690 <cExportador>EXP002</cExportador>\
1691 <adi>\
1692 <nAdicao>002</nAdicao>\
1693 <nSeqAdic>1</nSeqAdic>\
1694 <cFabricante>FAB_Y</cFabricante>\
1695 <vDescDI>150.00</vDescDI>\
1696 <nDraw>20259999999</nDraw>\
1697 </adi>\
1698 </DI>"
1699 );
1700 }
1701
1702 #[test]
1703 fn di_with_cpf_instead_of_cnpj() {
1704 use crate::types::{AdiData, DiData};
1705
1706 let adi = AdiData::new("1", "FAB_Z");
1707 let di = DiData::new(
1708 "DI-CPF",
1709 "2025-06-01",
1710 "Recife",
1711 "PE",
1712 "2025-06-03",
1713 "7",
1714 "3",
1715 "EXP003",
1716 vec![adi],
1717 )
1718 .cpf("12345678901");
1719
1720 let xml = build_di_xml(&di);
1721 assert!(xml.contains("<CPF>12345678901</CPF>"));
1722 assert!(!xml.contains("<CNPJ>"));
1723 }
1724
1725 #[test]
1726 fn di_with_multiple_adi() {
1727 use crate::types::{AdiData, DiData};
1728
1729 let adi1 = AdiData::new("1", "FAB_A").n_adicao("001");
1730 let adi2 = AdiData::new("2", "FAB_B").n_adicao("001");
1731 let di = DiData::new(
1732 "DI-MULTI",
1733 "2025-01-01",
1734 "Santos",
1735 "SP",
1736 "2025-01-05",
1737 "1",
1738 "1",
1739 "EXP-M",
1740 vec![adi1, adi2],
1741 );
1742 let xml = build_di_xml(&di);
1743
1744 let count = xml.matches("<adi>").count();
1746 assert_eq!(count, 2, "expected 2 <adi> elements, got {count}");
1747 assert!(xml.contains("<nSeqAdic>1</nSeqAdic>"));
1748 assert!(xml.contains("<nSeqAdic>2</nSeqAdic>"));
1749 assert!(xml.contains("<cFabricante>FAB_A</cFabricante>"));
1750 assert!(xml.contains("<cFabricante>FAB_B</cFabricante>"));
1751 }
1752
1753 #[test]
1754 fn di_in_det_xml_between_ind_tot_and_xped() {
1755 use crate::types::{AdiData, DiData};
1756
1757 let adi = AdiData::new("1", "FAB").n_adicao("001");
1758 let di = DiData::new(
1759 "DI-001",
1760 "2025-01-15",
1761 "Santos",
1762 "SP",
1763 "2025-01-20",
1764 "1",
1765 "1",
1766 "EXP",
1767 vec![adi],
1768 );
1769 let item = sample_item().di(vec![di]).x_ped("PO-123");
1770 let data = sample_build_data();
1771 let result = build_det(&item, &data).expect("build_det should succeed");
1772
1773 let xml = &result.xml;
1774 let ind_tot_pos = xml.find("</indTot>").expect("</indTot> must exist");
1775 let di_pos = xml.find("<DI>").expect("<DI> must exist");
1776 let xped_pos = xml.find("<xPed>").expect("<xPed> must exist");
1777
1778 assert!(di_pos > ind_tot_pos, "DI must come after indTot");
1779 assert!(xped_pos > di_pos, "xPed must come after DI");
1780 }
1781
1782 #[test]
1785 fn det_export_with_n_draw_only() {
1786 use crate::types::DetExportData;
1787
1788 let dex = DetExportData::new().n_draw("20250000001");
1789 let xml = build_det_export_xml(&dex);
1790
1791 assert_eq!(
1792 xml,
1793 "<detExport>\
1794 <nDraw>20250000001</nDraw>\
1795 </detExport>"
1796 );
1797 }
1798
1799 #[test]
1800 fn det_export_with_export_ind() {
1801 use crate::types::DetExportData;
1802
1803 let dex = DetExportData::new()
1804 .n_draw("20250000002")
1805 .n_re("123456789012")
1806 .ch_nfe("12345678901234567890123456789012345678901234")
1807 .q_export(100.5);
1808 let xml = build_det_export_xml(&dex);
1809
1810 assert_eq!(
1811 xml,
1812 "<detExport>\
1813 <nDraw>20250000002</nDraw>\
1814 <exportInd>\
1815 <nRE>123456789012</nRE>\
1816 <chNFe>12345678901234567890123456789012345678901234</chNFe>\
1817 <qExport>100.5000</qExport>\
1818 </exportInd>\
1819 </detExport>"
1820 );
1821 }
1822
1823 #[test]
1824 fn det_export_empty() {
1825 use crate::types::DetExportData;
1826
1827 let dex = DetExportData::new();
1828 let xml = build_det_export_xml(&dex);
1829
1830 assert_eq!(xml, "<detExport></detExport>");
1831 }
1832
1833 #[test]
1834 fn det_export_in_det_xml_between_ind_tot_and_xped() {
1835 use crate::types::DetExportData;
1836
1837 let dex = DetExportData::new().n_draw("20250000001");
1838 let item = sample_item().det_export(vec![dex]).x_ped("PO-456");
1839 let data = sample_build_data();
1840 let result = build_det(&item, &data).expect("build_det should succeed");
1841
1842 let xml = &result.xml;
1843 let ind_tot_pos = xml.find("</indTot>").expect("</indTot> must exist");
1844 let det_exp_pos = xml.find("<detExport>").expect("<detExport> must exist");
1845 let xped_pos = xml.find("<xPed>").expect("<xPed> must exist");
1846
1847 assert!(
1848 det_exp_pos > ind_tot_pos,
1849 "detExport must come after indTot"
1850 );
1851 assert!(xped_pos > det_exp_pos, "xPed must come after detExport");
1852 }
1853
1854 #[test]
1857 fn imposto_devol_produces_correct_xml() {
1858 use crate::types::ImpostoDevolData;
1859
1860 let devol = ImpostoDevolData::new(Rate(10000), Cents(5000));
1861 let item = sample_item().imposto_devol(devol);
1862 let data = sample_build_data();
1863 let result = build_det(&item, &data).expect("build_det should succeed");
1864
1865 assert!(result.xml.contains(
1866 "<impostoDevol>\
1867 <pDevol>100.00</pDevol>\
1868 <IPI>\
1869 <vIPIDevol>50.00</vIPIDevol>\
1870 </IPI>\
1871 </impostoDevol>"
1872 ));
1873 assert_eq!(result.v_ipi_devol, 5000);
1874 }
1875
1876 #[test]
1877 fn imposto_devol_50_percent() {
1878 use crate::types::ImpostoDevolData;
1879
1880 let devol = ImpostoDevolData::new(Rate(5000), Cents(2500));
1881 let item = sample_item().imposto_devol(devol);
1882 let data = sample_build_data();
1883 let result = build_det(&item, &data).expect("build_det should succeed");
1884
1885 assert!(result.xml.contains("<pDevol>50.00</pDevol>"));
1886 assert!(result.xml.contains("<vIPIDevol>25.00</vIPIDevol>"));
1887 assert_eq!(result.v_ipi_devol, 2500);
1888 }
1889
1890 #[test]
1891 fn imposto_devol_after_imposto_before_inf_ad_prod() {
1892 use crate::types::ImpostoDevolData;
1893
1894 let devol = ImpostoDevolData::new(Rate(10000), Cents(1000));
1895 let item = sample_item().imposto_devol(devol).inf_ad_prod("test info");
1896 let data = sample_build_data();
1897 let result = build_det(&item, &data).expect("build_det should succeed");
1898
1899 let imposto_end = result
1900 .xml
1901 .find("</imposto>")
1902 .expect("</imposto> must exist");
1903 let devol_pos = result
1904 .xml
1905 .find("<impostoDevol>")
1906 .expect("<impostoDevol> must exist");
1907 let inf_ad_pos = result
1908 .xml
1909 .find("<infAdProd>")
1910 .expect("<infAdProd> must exist");
1911
1912 assert!(
1913 devol_pos > imposto_end,
1914 "impostoDevol must come after </imposto>"
1915 );
1916 assert!(
1917 inf_ad_pos > devol_pos,
1918 "infAdProd must come after impostoDevol"
1919 );
1920 }
1921
1922 #[test]
1923 fn no_imposto_devol_when_none() {
1924 let item = sample_item();
1925 let data = sample_build_data();
1926 let result = build_det(&item, &data).expect("build_det should succeed");
1927
1928 assert!(!result.xml.contains("<impostoDevol>"));
1929 assert_eq!(result.v_ipi_devol, 0);
1930 }
1931
1932 #[test]
1935 fn nve_single_code_produces_correct_xml() {
1936 let item = sample_item().nve("AA0001");
1937 let data = sample_build_data();
1938 let result = build_det(&item, &data).expect("build_det should succeed");
1939
1940 assert!(result.xml.contains("<NVE>AA0001</NVE>"));
1941 let ncm_pos = result.xml.find("<NCM>").expect("<NCM> must exist");
1943 let nve_pos = result
1944 .xml
1945 .find("<NVE>AA0001</NVE>")
1946 .expect("<NVE> must exist");
1947 assert!(nve_pos > ncm_pos, "NVE must come after NCM");
1948 }
1949
1950 #[test]
1951 fn nve_multiple_codes_produces_correct_xml() {
1952 let item = sample_item().nve("AA0001").nve("BB0002").nve("CC0003");
1953 let data = sample_build_data();
1954 let result = build_det(&item, &data).expect("build_det should succeed");
1955
1956 assert!(result.xml.contains("<NVE>AA0001</NVE>"));
1957 assert!(result.xml.contains("<NVE>BB0002</NVE>"));
1958 assert!(result.xml.contains("<NVE>CC0003</NVE>"));
1959 let pos_a = result.xml.find("<NVE>AA0001</NVE>").expect("AA0001");
1961 let pos_b = result.xml.find("<NVE>BB0002</NVE>").expect("BB0002");
1962 let pos_c = result.xml.find("<NVE>CC0003</NVE>").expect("CC0003");
1963 assert!(pos_a < pos_b, "NVE codes must preserve insertion order");
1964 assert!(pos_b < pos_c, "NVE codes must preserve insertion order");
1965 }
1966
1967 #[test]
1968 fn nve_eight_codes_is_valid() {
1969 let item = sample_item()
1970 .nve("AA0001")
1971 .nve("AA0002")
1972 .nve("AA0003")
1973 .nve("AA0004")
1974 .nve("AA0005")
1975 .nve("AA0006")
1976 .nve("AA0007")
1977 .nve("AA0008");
1978 let data = sample_build_data();
1979 let result = build_det(&item, &data);
1980 assert!(result.is_ok(), "8 NVE codes should be valid");
1981 let xml = result.expect("valid").xml;
1982 assert_eq!(xml.matches("<NVE>").count(), 8);
1983 }
1984
1985 #[test]
1986 fn nve_nine_codes_returns_error() {
1987 let item = sample_item()
1988 .nve("AA0001")
1989 .nve("AA0002")
1990 .nve("AA0003")
1991 .nve("AA0004")
1992 .nve("AA0005")
1993 .nve("AA0006")
1994 .nve("AA0007")
1995 .nve("AA0008")
1996 .nve("AA0009");
1997 let data = sample_build_data();
1998 let result = build_det(&item, &data);
1999 assert!(result.is_err(), "9 NVE codes should be rejected");
2000 let err = result.unwrap_err();
2001 assert_eq!(
2002 err,
2003 FiscalError::InvalidTaxData("Item 1: NVE limited to 8 entries, got 9".to_string())
2004 );
2005 }
2006
2007 #[test]
2008 fn nve_empty_vec_produces_no_nve_tags() {
2009 let item = sample_item();
2010 let data = sample_build_data();
2011 let result = build_det(&item, &data).expect("build_det should succeed");
2012
2013 assert!(!result.xml.contains("<NVE>"));
2014 }
2015
2016 #[test]
2017 fn nve_appears_before_cest() {
2018 let item = sample_item().nve("AA0001").cest("1234567");
2019 let data = sample_build_data();
2020 let result = build_det(&item, &data).expect("build_det should succeed");
2021
2022 let nve_pos = result
2023 .xml
2024 .find("<NVE>AA0001</NVE>")
2025 .expect("<NVE> must exist");
2026 let cest_pos = result.xml.find("<CEST>").expect("<CEST> must exist");
2027 assert!(nve_pos < cest_pos, "NVE must come before CEST");
2028 }
2029
2030 #[test]
2033 fn gcred_single_with_value_produces_correct_xml() {
2034 let gc = GCredData::new("SP000001", Rate4(50000)).v_cred_presumido(Cents(1500));
2035 let item = sample_item().g_cred(vec![gc]);
2036 let data = sample_build_data();
2037 let result = build_det(&item, &data).expect("build_det should succeed");
2038
2039 assert!(result.xml.contains(
2040 "<gCred><cCredPresumido>SP000001</cCredPresumido>\
2041 <pCredPresumido>5.0000</pCredPresumido>\
2042 <vCredPresumido>15.00</vCredPresumido></gCred>"
2043 ));
2044 }
2045
2046 #[test]
2047 fn gcred_without_value_omits_v_cred_presumido() {
2048 let gc = GCredData::new("RJ000002", Rate4(120000));
2049 let item = sample_item().g_cred(vec![gc]);
2050 let data = sample_build_data();
2051 let result = build_det(&item, &data).expect("build_det should succeed");
2052
2053 assert!(result.xml.contains(
2054 "<gCred>\
2055 <cCredPresumido>RJ000002</cCredPresumido>\
2056 <pCredPresumido>12.0000</pCredPresumido>\
2057 </gCred>"
2058 ));
2059 assert!(!result.xml.contains("<vCredPresumido>"));
2060 }
2061
2062 #[test]
2063 fn gcred_multiple_entries_up_to_four() {
2064 let entries = vec![
2065 GCredData::new("SP000001", Rate4(10000)).v_cred_presumido(Cents(100)),
2066 GCredData::new("SP000002", Rate4(20000)).v_cred_presumido(Cents(200)),
2067 GCredData::new("SP000003", Rate4(30000)).v_cred_presumido(Cents(300)),
2068 GCredData::new("SP000004", Rate4(40000)).v_cred_presumido(Cents(400)),
2069 ];
2070 let item = sample_item().g_cred(entries);
2071 let data = sample_build_data();
2072 let result = build_det(&item, &data).expect("build_det should succeed");
2073
2074 assert!(
2075 result
2076 .xml
2077 .contains("<cCredPresumido>SP000001</cCredPresumido>")
2078 );
2079 assert!(
2080 result
2081 .xml
2082 .contains("<cCredPresumido>SP000002</cCredPresumido>")
2083 );
2084 assert!(
2085 result
2086 .xml
2087 .contains("<cCredPresumido>SP000003</cCredPresumido>")
2088 );
2089 assert!(
2090 result
2091 .xml
2092 .contains("<cCredPresumido>SP000004</cCredPresumido>")
2093 );
2094 }
2095
2096 #[test]
2097 fn gcred_truncates_at_four_entries() {
2098 let entries = vec![
2099 GCredData::new("SP000001", Rate4(10000)),
2100 GCredData::new("SP000002", Rate4(20000)),
2101 GCredData::new("SP000003", Rate4(30000)),
2102 GCredData::new("SP000004", Rate4(40000)),
2103 GCredData::new("SP000005", Rate4(50000)),
2104 ];
2105 let item = sample_item().g_cred(entries);
2106 let data = sample_build_data();
2107 let result = build_det(&item, &data).expect("build_det should succeed");
2108
2109 assert!(
2110 result
2111 .xml
2112 .contains("<cCredPresumido>SP000004</cCredPresumido>")
2113 );
2114 assert!(
2115 !result
2116 .xml
2117 .contains("<cCredPresumido>SP000005</cCredPresumido>")
2118 );
2119 }
2120
2121 #[test]
2122 fn gcred_positioned_after_cbenef_before_cfop() {
2123 let gc = GCredData::new("MG000001", Rate4(50000)).v_cred_presumido(Cents(1000));
2124 let item = sample_item().c_benef("SEM CBENEF").g_cred(vec![gc]);
2125 let data = sample_build_data();
2126 let result = build_det(&item, &data).expect("build_det should succeed");
2127
2128 let cbenef_pos = result.xml.find("<cBenef>").expect("cBenef should exist");
2129 let gcred_pos = result.xml.find("<gCred>").expect("gCred should exist");
2130 let cfop_pos = result.xml.find("<CFOP>").expect("CFOP should exist");
2131
2132 assert!(gcred_pos > cbenef_pos, "gCred must come after cBenef");
2133 assert!(gcred_pos < cfop_pos, "gCred must come before CFOP");
2134 }
2135
2136 #[test]
2137 fn gcred_empty_vec_produces_no_gcred_tags() {
2138 let item = sample_item();
2139 let data = sample_build_data();
2140 let result = build_det(&item, &data).expect("build_det should succeed");
2141
2142 assert!(!result.xml.contains("<gCred>"));
2143 }
2144
2145 fn normal_build_data() -> InvoiceBuildData {
2148 let mut data = sample_build_data();
2149 data.issuer.tax_regime = TaxRegime::Normal;
2150 data
2151 }
2152
2153 fn pl010_build_data() -> InvoiceBuildData {
2154 let mut data = sample_build_data();
2155 data.schema_version = crate::types::SchemaVersion::PL010;
2156 data
2157 }
2158
2159 #[test]
2162 fn csosn_101_produces_correct_xml() {
2163 let item = InvoiceItemData::new(
2164 1,
2165 "001",
2166 "Produto",
2167 "27101259",
2168 "5102",
2169 "UN",
2170 1.0,
2171 Cents(1000),
2172 Cents(1000),
2173 "101",
2174 Rate(1800),
2175 Cents(180),
2176 "99",
2177 "99",
2178 )
2179 .icms_p_cred_sn(Rate(500))
2180 .icms_v_cred_icms_sn(Cents(50));
2181 let data = sample_build_data();
2182 let result = build_det(&item, &data).expect("build_det should succeed");
2183
2184 assert!(result.xml.contains("<ICMSSN101>"));
2185 assert!(result.xml.contains("<CSOSN>101</CSOSN>"));
2186 assert!(result.xml.contains("<pCredSN>"));
2187 assert!(result.xml.contains("<vCredICMSSN>"));
2188 }
2189
2190 #[test]
2191 fn csosn_101_missing_p_cred_sn_returns_error() {
2192 let item = InvoiceItemData::new(
2193 1,
2194 "001",
2195 "Produto",
2196 "27101259",
2197 "5102",
2198 "UN",
2199 1.0,
2200 Cents(1000),
2201 Cents(1000),
2202 "101",
2203 Rate(0),
2204 Cents(0),
2205 "99",
2206 "99",
2207 )
2208 .icms_v_cred_icms_sn(Cents(50));
2209 let data = sample_build_data();
2210 let result = build_det(&item, &data);
2211 assert!(result.is_err());
2212 }
2213
2214 #[test]
2215 fn csosn_101_missing_v_cred_icms_sn_returns_error() {
2216 let item = InvoiceItemData::new(
2217 1,
2218 "001",
2219 "Produto",
2220 "27101259",
2221 "5102",
2222 "UN",
2223 1.0,
2224 Cents(1000),
2225 Cents(1000),
2226 "101",
2227 Rate(0),
2228 Cents(0),
2229 "99",
2230 "99",
2231 )
2232 .icms_p_cred_sn(Rate(500));
2233 let data = sample_build_data();
2234 let result = build_det(&item, &data);
2235 assert!(result.is_err());
2236 }
2237
2238 #[test]
2239 fn csosn_empty_defaults_to_102() {
2240 let item = InvoiceItemData::new(
2242 1,
2243 "001",
2244 "Produto",
2245 "27101259",
2246 "5102",
2247 "UN",
2248 1.0,
2249 Cents(1000),
2250 Cents(1000),
2251 "",
2252 Rate(0),
2253 Cents(0),
2254 "99",
2255 "99",
2256 );
2257 let data = sample_build_data();
2258 let result = build_det(&item, &data).expect("build_det should succeed");
2259
2260 assert!(result.xml.contains("<ICMSSN102>"));
2261 assert!(result.xml.contains("<CSOSN>102</CSOSN>"));
2262 }
2263
2264 #[test]
2265 fn csosn_103_produces_correct_xml() {
2266 let item = InvoiceItemData::new(
2267 1,
2268 "001",
2269 "Produto",
2270 "27101259",
2271 "5102",
2272 "UN",
2273 1.0,
2274 Cents(1000),
2275 Cents(1000),
2276 "103",
2277 Rate(0),
2278 Cents(0),
2279 "99",
2280 "99",
2281 );
2282 let data = sample_build_data();
2283 let result = build_det(&item, &data).expect("build_det should succeed");
2284 assert!(result.xml.contains("<ICMSSN102>"));
2286 assert!(result.xml.contains("<CSOSN>103</CSOSN>"));
2287 }
2288
2289 #[test]
2290 fn csosn_300_produces_correct_xml() {
2291 let item = InvoiceItemData::new(
2292 1,
2293 "001",
2294 "Produto",
2295 "27101259",
2296 "5102",
2297 "UN",
2298 1.0,
2299 Cents(1000),
2300 Cents(1000),
2301 "300",
2302 Rate(0),
2303 Cents(0),
2304 "99",
2305 "99",
2306 );
2307 let data = sample_build_data();
2308 let result = build_det(&item, &data).expect("build_det should succeed");
2309 assert!(result.xml.contains("<ICMSSN102>"));
2310 assert!(result.xml.contains("<CSOSN>300</CSOSN>"));
2311 }
2312
2313 #[test]
2314 fn csosn_400_produces_correct_xml() {
2315 let item = InvoiceItemData::new(
2316 1,
2317 "001",
2318 "Produto",
2319 "27101259",
2320 "5102",
2321 "UN",
2322 1.0,
2323 Cents(1000),
2324 Cents(1000),
2325 "400",
2326 Rate(0),
2327 Cents(0),
2328 "99",
2329 "99",
2330 );
2331 let data = sample_build_data();
2332 let result = build_det(&item, &data).expect("build_det should succeed");
2333 assert!(result.xml.contains("<ICMSSN102>"));
2334 assert!(result.xml.contains("<CSOSN>400</CSOSN>"));
2335 }
2336
2337 #[test]
2338 fn csosn_201_produces_correct_xml() {
2339 let item = InvoiceItemData::new(
2340 1,
2341 "001",
2342 "Produto",
2343 "27101259",
2344 "5102",
2345 "UN",
2346 1.0,
2347 Cents(1000),
2348 Cents(1000),
2349 "201",
2350 Rate(1800),
2351 Cents(180),
2352 "99",
2353 "99",
2354 )
2355 .icms_mod_bc_st(4)
2356 .icms_v_bc_st(Cents(1200))
2357 .icms_p_icms_st(Rate(1200))
2358 .icms_v_icms_st(Cents(144))
2359 .icms_p_mva_st(Rate(5000))
2360 .icms_red_bc_st(Rate(1000))
2361 .icms_v_bc_fcp_st(Cents(1200))
2362 .icms_p_fcp_st(Rate(200))
2363 .icms_v_fcp_st(Cents(24))
2364 .icms_p_cred_sn(Rate(500))
2365 .icms_v_cred_icms_sn(Cents(50));
2366 let data = sample_build_data();
2367 let result = build_det(&item, &data).expect("build_det should succeed");
2368
2369 assert!(result.xml.contains("<ICMSSN201>"));
2370 assert!(result.xml.contains("<CSOSN>201</CSOSN>"));
2371 assert!(result.xml.contains("<modBCST>4</modBCST>"));
2372 assert!(result.xml.contains("<vBCST>12.00</vBCST>"));
2373 assert!(result.xml.contains("<pICMSST>"));
2374 assert!(result.xml.contains("<vICMSST>"));
2375 assert!(result.xml.contains("<pMVAST>"));
2376 assert!(result.xml.contains("<pRedBCST>"));
2377 }
2378
2379 #[test]
2380 fn csosn_202_produces_correct_xml() {
2381 let item = InvoiceItemData::new(
2382 1,
2383 "001",
2384 "Produto",
2385 "27101259",
2386 "5102",
2387 "UN",
2388 1.0,
2389 Cents(1000),
2390 Cents(1000),
2391 "202",
2392 Rate(0),
2393 Cents(0),
2394 "99",
2395 "99",
2396 )
2397 .icms_mod_bc_st(4)
2398 .icms_v_bc_st(Cents(1200))
2399 .icms_p_icms_st(Rate(1200))
2400 .icms_v_icms_st(Cents(144))
2401 .icms_p_mva_st(Rate(5000))
2402 .icms_red_bc_st(Rate(1000))
2403 .icms_v_bc_fcp_st(Cents(1200))
2404 .icms_p_fcp_st(Rate(200))
2405 .icms_v_fcp_st(Cents(24));
2406 let data = sample_build_data();
2407 let result = build_det(&item, &data).expect("build_det should succeed");
2408
2409 assert!(result.xml.contains("<ICMSSN202>"));
2410 assert!(result.xml.contains("<CSOSN>202</CSOSN>"));
2411 assert!(result.xml.contains("<modBCST>4</modBCST>"));
2412 }
2413
2414 #[test]
2415 fn csosn_203_produces_correct_xml() {
2416 let item = InvoiceItemData::new(
2417 1,
2418 "001",
2419 "Produto",
2420 "27101259",
2421 "5102",
2422 "UN",
2423 1.0,
2424 Cents(1000),
2425 Cents(1000),
2426 "203",
2427 Rate(0),
2428 Cents(0),
2429 "99",
2430 "99",
2431 )
2432 .icms_mod_bc_st(4)
2433 .icms_v_bc_st(Cents(1200))
2434 .icms_p_icms_st(Rate(1200))
2435 .icms_v_icms_st(Cents(144))
2436 .icms_p_mva_st(Rate(5000))
2437 .icms_red_bc_st(Rate(1000))
2438 .icms_v_bc_fcp_st(Cents(1200))
2439 .icms_p_fcp_st(Rate(200))
2440 .icms_v_fcp_st(Cents(24));
2441 let data = sample_build_data();
2442 let result = build_det(&item, &data).expect("build_det should succeed");
2443
2444 assert!(result.xml.contains("<ICMSSN202>"));
2446 assert!(result.xml.contains("<CSOSN>203</CSOSN>"));
2447 assert!(result.xml.contains("<modBCST>4</modBCST>"));
2448 }
2449
2450 #[test]
2451 fn csosn_500_produces_correct_xml() {
2452 let item = InvoiceItemData::new(
2453 1,
2454 "001",
2455 "Produto",
2456 "27101259",
2457 "5102",
2458 "UN",
2459 1.0,
2460 Cents(1000),
2461 Cents(1000),
2462 "500",
2463 Rate(0),
2464 Cents(0),
2465 "99",
2466 "99",
2467 )
2468 .icms_v_icms_substituto(Cents(200));
2469 let data = sample_build_data();
2470 let result = build_det(&item, &data).expect("build_det should succeed");
2471
2472 assert!(result.xml.contains("<ICMSSN500>"));
2473 assert!(result.xml.contains("<CSOSN>500</CSOSN>"));
2474 assert!(result.xml.contains("<vICMSSubstituto>"));
2475 }
2476
2477 #[test]
2478 fn csosn_900_produces_correct_xml() {
2479 let item = InvoiceItemData::new(
2480 1,
2481 "001",
2482 "Produto",
2483 "27101259",
2484 "5102",
2485 "UN",
2486 1.0,
2487 Cents(1000),
2488 Cents(1000),
2489 "900",
2490 Rate(1800),
2491 Cents(180),
2492 "99",
2493 "99",
2494 )
2495 .icms_mod_bc(3)
2496 .icms_red_bc(Rate(1000))
2497 .icms_mod_bc_st(4)
2498 .icms_p_mva_st(Rate(5000))
2499 .icms_red_bc_st(Rate(1000))
2500 .icms_v_bc_st(Cents(1200))
2501 .icms_p_icms_st(Rate(1200))
2502 .icms_v_icms_st(Cents(144))
2503 .icms_v_bc_fcp_st(Cents(1200))
2504 .icms_p_fcp_st(Rate(200))
2505 .icms_v_fcp_st(Cents(24))
2506 .icms_p_cred_sn(Rate(500))
2507 .icms_v_cred_icms_sn(Cents(50));
2508 let data = sample_build_data();
2509 let result = build_det(&item, &data).expect("build_det should succeed");
2510
2511 assert!(result.xml.contains("<ICMSSN900>"));
2512 assert!(result.xml.contains("<CSOSN>900</CSOSN>"));
2513 assert!(result.xml.contains("<modBC>3</modBC>"));
2514 }
2515
2516 #[test]
2517 fn csosn_unsupported_returns_error() {
2518 let item = InvoiceItemData::new(
2519 1,
2520 "001",
2521 "Produto",
2522 "27101259",
2523 "5102",
2524 "UN",
2525 1.0,
2526 Cents(1000),
2527 Cents(1000),
2528 "999",
2529 Rate(0),
2530 Cents(0),
2531 "99",
2532 "99",
2533 );
2534 let data = sample_build_data();
2535 let result = build_det(&item, &data);
2536 assert!(result.is_err());
2537 let err = result.unwrap_err();
2538 assert!(
2539 matches!(err, FiscalError::UnsupportedIcmsCsosn(ref c) if c == "999"),
2540 "expected UnsupportedIcmsCsosn, got {:?}",
2541 err
2542 );
2543 }
2544
2545 #[test]
2548 fn cst_10_produces_correct_xml() {
2549 let item = InvoiceItemData::new(
2550 1,
2551 "001",
2552 "Produto",
2553 "27101259",
2554 "5102",
2555 "UN",
2556 1.0,
2557 Cents(1000),
2558 Cents(1000),
2559 "10",
2560 Rate(1800),
2561 Cents(180),
2562 "99",
2563 "99",
2564 )
2565 .icms_mod_bc_st(4)
2566 .icms_v_bc_st(Cents(1200))
2567 .icms_p_icms_st(Rate(1200))
2568 .icms_v_icms_st(Cents(144))
2569 .icms_v_bc_fcp(Cents(1000))
2570 .icms_p_fcp(Rate(200))
2571 .icms_v_fcp(Cents(20))
2572 .icms_p_mva_st(Rate(5000))
2573 .icms_red_bc_st(Rate(1000))
2574 .icms_v_bc_fcp_st(Cents(1200))
2575 .icms_p_fcp_st(Rate(200))
2576 .icms_v_fcp_st(Cents(24));
2577 let data = normal_build_data();
2578 let result = build_det(&item, &data).expect("build_det should succeed");
2579
2580 assert!(result.xml.contains("<ICMS10>"));
2581 assert!(result.xml.contains("<CST>10</CST>"));
2582 assert!(result.xml.contains("<modBCST>4</modBCST>"));
2583 assert!(result.xml.contains("<vBCST>12.00</vBCST>"));
2584 assert!(result.xml.contains("<pICMSST>"));
2585 assert!(result.xml.contains("<vICMSST>"));
2586 }
2587
2588 #[test]
2589 fn cst_20_produces_correct_xml() {
2590 let item = InvoiceItemData::new(
2591 1,
2592 "001",
2593 "Produto",
2594 "27101259",
2595 "5102",
2596 "UN",
2597 1.0,
2598 Cents(1000),
2599 Cents(1000),
2600 "20",
2601 Rate(1800),
2602 Cents(180),
2603 "99",
2604 "99",
2605 )
2606 .icms_mod_bc(3)
2607 .icms_red_bc(Rate(2000))
2608 .icms_v_bc_fcp(Cents(1000))
2609 .icms_p_fcp(Rate(200))
2610 .icms_v_fcp(Cents(20))
2611 .icms_v_icms_deson(Cents(50))
2612 .icms_mot_des_icms(9)
2613 .icms_ind_deduz_deson("1");
2614 let data = normal_build_data();
2615 let result = build_det(&item, &data).expect("build_det should succeed");
2616
2617 assert!(result.xml.contains("<ICMS20>"));
2618 assert!(result.xml.contains("<CST>20</CST>"));
2619 assert!(result.xml.contains("<pRedBC>"));
2620 assert!(result.xml.contains("<vICMSDeson>"));
2621 assert!(result.xml.contains("<motDesICMS>9</motDesICMS>"));
2622 assert!(result.xml.contains("<indDeduzDeson>1</indDeduzDeson>"));
2623 assert!(result.ind_deduz_deson);
2624 }
2625
2626 #[test]
2627 fn cst_30_produces_correct_xml() {
2628 let item = InvoiceItemData::new(
2629 1,
2630 "001",
2631 "Produto",
2632 "27101259",
2633 "5102",
2634 "UN",
2635 1.0,
2636 Cents(1000),
2637 Cents(1000),
2638 "30",
2639 Rate(0),
2640 Cents(0),
2641 "99",
2642 "99",
2643 )
2644 .icms_mod_bc_st(4)
2645 .icms_v_bc_st(Cents(1200))
2646 .icms_p_icms_st(Rate(1200))
2647 .icms_v_icms_st(Cents(144))
2648 .icms_p_mva_st(Rate(5000))
2649 .icms_red_bc_st(Rate(1000))
2650 .icms_v_bc_fcp_st(Cents(1200))
2651 .icms_p_fcp_st(Rate(200))
2652 .icms_v_fcp_st(Cents(24))
2653 .icms_v_icms_deson(Cents(50))
2654 .icms_mot_des_icms(9)
2655 .icms_ind_deduz_deson("1");
2656 let data = normal_build_data();
2657 let result = build_det(&item, &data).expect("build_det should succeed");
2658
2659 assert!(result.xml.contains("<ICMS30>"));
2660 assert!(result.xml.contains("<CST>30</CST>"));
2661 assert!(result.xml.contains("<modBCST>4</modBCST>"));
2662 assert!(result.xml.contains("<vICMSDeson>"));
2663 assert!(result.xml.contains("<motDesICMS>9</motDesICMS>"));
2664 }
2665
2666 #[test]
2667 fn cst_40_produces_correct_xml() {
2668 let item = InvoiceItemData::new(
2669 1,
2670 "001",
2671 "Produto",
2672 "27101259",
2673 "5102",
2674 "UN",
2675 1.0,
2676 Cents(1000),
2677 Cents(1000),
2678 "40",
2679 Rate(0),
2680 Cents(0),
2681 "99",
2682 "99",
2683 )
2684 .icms_v_icms_deson(Cents(100))
2685 .icms_mot_des_icms(1)
2686 .icms_ind_deduz_deson("1");
2687 let data = normal_build_data();
2688 let result = build_det(&item, &data).expect("build_det should succeed");
2689
2690 assert!(result.xml.contains("<ICMS40>"));
2691 assert!(result.xml.contains("<CST>40</CST>"));
2692 assert!(result.xml.contains("<vICMSDeson>"));
2693 assert!(result.xml.contains("<motDesICMS>1</motDesICMS>"));
2694 }
2695
2696 #[test]
2697 fn cst_41_produces_correct_xml() {
2698 let item = InvoiceItemData::new(
2699 1,
2700 "001",
2701 "Produto",
2702 "27101259",
2703 "5102",
2704 "UN",
2705 1.0,
2706 Cents(1000),
2707 Cents(1000),
2708 "41",
2709 Rate(0),
2710 Cents(0),
2711 "99",
2712 "99",
2713 )
2714 .icms_v_icms_deson(Cents(100))
2715 .icms_mot_des_icms(1)
2716 .icms_ind_deduz_deson("1");
2717 let data = normal_build_data();
2718 let result = build_det(&item, &data).expect("build_det should succeed");
2719
2720 assert!(result.xml.contains("<ICMS40>"));
2722 assert!(result.xml.contains("<CST>41</CST>"));
2723 assert!(result.xml.contains("<vICMSDeson>"));
2724 }
2725
2726 #[test]
2727 fn cst_50_produces_correct_xml() {
2728 let item = InvoiceItemData::new(
2729 1,
2730 "001",
2731 "Produto",
2732 "27101259",
2733 "5102",
2734 "UN",
2735 1.0,
2736 Cents(1000),
2737 Cents(1000),
2738 "50",
2739 Rate(0),
2740 Cents(0),
2741 "99",
2742 "99",
2743 )
2744 .icms_v_icms_deson(Cents(100))
2745 .icms_mot_des_icms(1)
2746 .icms_ind_deduz_deson("1");
2747 let data = normal_build_data();
2748 let result = build_det(&item, &data).expect("build_det should succeed");
2749
2750 assert!(result.xml.contains("<ICMS40>"));
2752 assert!(result.xml.contains("<CST>50</CST>"));
2753 assert!(result.xml.contains("<vICMSDeson>"));
2754 }
2755
2756 #[test]
2757 fn cst_51_produces_correct_xml() {
2758 let item = InvoiceItemData::new(
2759 1,
2760 "001",
2761 "Produto",
2762 "27101259",
2763 "5102",
2764 "UN",
2765 1.0,
2766 Cents(1000),
2767 Cents(1000),
2768 "51",
2769 Rate(1800),
2770 Cents(180),
2771 "99",
2772 "99",
2773 )
2774 .icms_mod_bc(3)
2775 .icms_red_bc(Rate(1000))
2776 .icms_v_bc_fcp(Cents(1000))
2777 .icms_p_fcp(Rate(200))
2778 .icms_v_fcp(Cents(20));
2779 let data = normal_build_data();
2780 let result = build_det(&item, &data).expect("build_det should succeed");
2781
2782 assert!(result.xml.contains("<ICMS51>"));
2783 assert!(result.xml.contains("<CST>51</CST>"));
2784 assert!(result.xml.contains("<modBC>3</modBC>"));
2785 assert!(result.xml.contains("<pRedBC>"));
2786 assert!(result.xml.contains("<vICMS>"));
2787 }
2788
2789 #[test]
2790 fn cst_60_produces_correct_xml() {
2791 let item = InvoiceItemData::new(
2792 1,
2793 "001",
2794 "Produto",
2795 "27101259",
2796 "5102",
2797 "UN",
2798 1.0,
2799 Cents(1000),
2800 Cents(1000),
2801 "60",
2802 Rate(0),
2803 Cents(0),
2804 "99",
2805 "99",
2806 )
2807 .icms_v_icms_substituto(Cents(200));
2808 let data = normal_build_data();
2809 let result = build_det(&item, &data).expect("build_det should succeed");
2810
2811 assert!(result.xml.contains("<ICMS60>"));
2812 assert!(result.xml.contains("<CST>60</CST>"));
2813 assert!(result.xml.contains("<vICMSSubstituto>"));
2814 }
2815
2816 #[test]
2817 fn cst_70_produces_correct_xml() {
2818 let item = InvoiceItemData::new(
2819 1,
2820 "001",
2821 "Produto",
2822 "27101259",
2823 "5102",
2824 "UN",
2825 1.0,
2826 Cents(1000),
2827 Cents(1000),
2828 "70",
2829 Rate(1800),
2830 Cents(180),
2831 "99",
2832 "99",
2833 )
2834 .icms_mod_bc(3)
2835 .icms_red_bc(Rate(2000))
2836 .icms_mod_bc_st(4)
2837 .icms_v_bc_st(Cents(1200))
2838 .icms_p_icms_st(Rate(1200))
2839 .icms_v_icms_st(Cents(144))
2840 .icms_v_bc_fcp(Cents(1000))
2841 .icms_p_fcp(Rate(200))
2842 .icms_v_fcp(Cents(20))
2843 .icms_p_mva_st(Rate(5000))
2844 .icms_red_bc_st(Rate(1000))
2845 .icms_v_bc_fcp_st(Cents(1200))
2846 .icms_p_fcp_st(Rate(200))
2847 .icms_v_fcp_st(Cents(24))
2848 .icms_v_icms_deson(Cents(50))
2849 .icms_mot_des_icms(9)
2850 .icms_ind_deduz_deson("1");
2851 let data = normal_build_data();
2852 let result = build_det(&item, &data).expect("build_det should succeed");
2853
2854 assert!(result.xml.contains("<ICMS70>"));
2855 assert!(result.xml.contains("<CST>70</CST>"));
2856 assert!(result.xml.contains("<pRedBC>"));
2857 assert!(result.xml.contains("<modBCST>4</modBCST>"));
2858 assert!(result.xml.contains("<vICMSDeson>"));
2859 }
2860
2861 #[test]
2862 fn cst_90_produces_correct_xml() {
2863 let item = InvoiceItemData::new(
2864 1,
2865 "001",
2866 "Produto",
2867 "27101259",
2868 "5102",
2869 "UN",
2870 1.0,
2871 Cents(1000),
2872 Cents(1000),
2873 "90",
2874 Rate(1800),
2875 Cents(180),
2876 "99",
2877 "99",
2878 )
2879 .icms_mod_bc(3)
2880 .icms_red_bc(Rate(1000))
2881 .icms_v_bc_fcp(Cents(1000))
2882 .icms_p_fcp(Rate(200))
2883 .icms_v_fcp(Cents(20))
2884 .icms_mod_bc_st(4)
2885 .icms_p_mva_st(Rate(5000))
2886 .icms_red_bc_st(Rate(1000))
2887 .icms_v_bc_st(Cents(1200))
2888 .icms_p_icms_st(Rate(1200))
2889 .icms_v_icms_st(Cents(144))
2890 .icms_v_bc_fcp_st(Cents(1200))
2891 .icms_p_fcp_st(Rate(200))
2892 .icms_v_fcp_st(Cents(24))
2893 .icms_v_icms_deson(Cents(50))
2894 .icms_mot_des_icms(9)
2895 .icms_ind_deduz_deson("1");
2896 let data = normal_build_data();
2897 let result = build_det(&item, &data).expect("build_det should succeed");
2898
2899 assert!(result.xml.contains("<ICMS90>"));
2900 assert!(result.xml.contains("<CST>90</CST>"));
2901 assert!(result.xml.contains("<modBC>3</modBC>"));
2902 assert!(result.xml.contains("<modBCST>4</modBCST>"));
2903 assert!(result.xml.contains("<vICMSDeson>"));
2904 }
2905
2906 #[test]
2907 fn cst_unsupported_returns_error() {
2908 let item = InvoiceItemData::new(
2909 1,
2910 "001",
2911 "Produto",
2912 "27101259",
2913 "5102",
2914 "UN",
2915 1.0,
2916 Cents(1000),
2917 Cents(1000),
2918 "99",
2919 Rate(0),
2920 Cents(0),
2921 "99",
2922 "99",
2923 );
2924 let data = normal_build_data();
2925 let result = build_det(&item, &data);
2926 assert!(result.is_err());
2927 let err = result.unwrap_err();
2928 assert!(
2929 matches!(err, FiscalError::UnsupportedIcmsCst(ref c) if c == "99"),
2930 "expected UnsupportedIcmsCst, got {:?}",
2931 err
2932 );
2933 }
2934
2935 #[test]
2938 fn ipi_produces_correct_xml() {
2939 let item = sample_item()
2940 .ipi_cst("50")
2941 .ipi_c_enq("999")
2942 .ipi_v_bc(Cents(10000))
2943 .ipi_p_ipi(Rate(500))
2944 .ipi_v_ipi(Cents(500));
2945 let data = sample_build_data();
2946 let result = build_det(&item, &data).expect("build_det should succeed");
2947
2948 assert!(result.xml.contains("<IPI>"));
2949 assert!(result.xml.contains("<CST>50</CST>"));
2950 assert!(result.xml.contains("<cEnq>999</cEnq>"));
2951 assert!(result.xml.contains("<vIPI>5.00</vIPI>"));
2952 assert_eq!(result.v_ipi, 500);
2953 }
2954
2955 #[test]
2956 fn ipi_default_c_enq_when_missing() {
2957 let item = sample_item()
2958 .ipi_cst("50")
2959 .ipi_v_bc(Cents(10000))
2960 .ipi_p_ipi(Rate(500))
2961 .ipi_v_ipi(Cents(500));
2962 let data = sample_build_data();
2963 let result = build_det(&item, &data).expect("build_det should succeed");
2964
2965 assert!(result.xml.contains("<cEnq>999</cEnq>"));
2966 }
2967
2968 #[test]
2969 fn ipi_with_quantity_based() {
2970 let item = sample_item()
2971 .ipi_cst("50")
2972 .ipi_q_unid(100)
2973 .ipi_v_unid(50)
2974 .ipi_v_ipi(Cents(5000));
2975 let data = sample_build_data();
2976 let result = build_det(&item, &data).expect("build_det should succeed");
2977
2978 assert!(result.xml.contains("<IPI>"));
2979 assert_eq!(result.v_ipi, 5000);
2980 }
2981
2982 #[test]
2983 fn no_ipi_when_cst_absent() {
2984 let item = sample_item();
2985 let data = sample_build_data();
2986 let result = build_det(&item, &data).expect("build_det should succeed");
2987
2988 assert!(!result.xml.contains("<IPI>"));
2989 assert_eq!(result.v_ipi, 0);
2990 }
2991
2992 #[test]
2995 fn ii_produces_correct_xml() {
2996 let item = sample_item()
2997 .ii_v_bc(Cents(50000))
2998 .ii_v_desp_adu(Cents(5000))
2999 .ii_v_ii(Cents(10000))
3000 .ii_v_iof(Cents(2000));
3001 let data = sample_build_data();
3002 let result = build_det(&item, &data).expect("build_det should succeed");
3003
3004 assert!(result.xml.contains("<II>"));
3005 assert!(result.xml.contains("<vBC>500.00</vBC>"));
3006 assert!(result.xml.contains("<vDespAdu>50.00</vDespAdu>"));
3007 assert!(result.xml.contains("<vII>100.00</vII>"));
3008 assert!(result.xml.contains("<vIOF>20.00</vIOF>"));
3009 assert_eq!(result.v_ii, 10000);
3010 }
3011
3012 #[test]
3013 fn no_ii_when_absent() {
3014 let item = sample_item();
3015 let data = sample_build_data();
3016 let result = build_det(&item, &data).expect("build_det should succeed");
3017
3018 assert!(!result.xml.contains("<II>"));
3019 assert_eq!(result.v_ii, 0);
3020 }
3021
3022 #[test]
3025 fn pis_st_replaces_pis_and_accumulates_when_ind_soma_1() {
3026 use crate::tax_pis_cofins_ipi::PisStData;
3027 let pis_st = PisStData::new(Cents(500))
3028 .v_bc(Cents(10000))
3029 .p_pis(Rate4(16500))
3030 .ind_soma_pis_st(1);
3031 let item = sample_item().pis_st(pis_st);
3032 let data = sample_build_data();
3033 let result = build_det(&item, &data).expect("build_det should succeed");
3034
3035 assert!(result.xml.contains("<PISST>"));
3036 assert!(!result.xml.contains("<PISAliq>"));
3037 assert_eq!(result.v_pis_st, 500);
3038 }
3039
3040 #[test]
3041 fn pis_st_does_not_accumulate_when_ind_soma_0() {
3042 use crate::tax_pis_cofins_ipi::PisStData;
3043 let pis_st = PisStData::new(Cents(500))
3044 .v_bc(Cents(10000))
3045 .p_pis(Rate4(16500))
3046 .ind_soma_pis_st(0);
3047 let item = sample_item().pis_st(pis_st);
3048 let data = sample_build_data();
3049 let result = build_det(&item, &data).expect("build_det should succeed");
3050
3051 assert!(result.xml.contains("<PISST>"));
3052 assert_eq!(result.v_pis_st, 0);
3053 }
3054
3055 #[test]
3056 fn cofins_st_replaces_cofins_and_accumulates_when_ind_soma_1() {
3057 use crate::tax_pis_cofins_ipi::CofinsStData;
3058 let cofins_st = CofinsStData::new(Cents(750))
3059 .v_bc(Cents(10000))
3060 .p_cofins(Rate4(76000))
3061 .ind_soma_cofins_st(1);
3062 let item = sample_item().cofins_st(cofins_st);
3063 let data = sample_build_data();
3064 let result = build_det(&item, &data).expect("build_det should succeed");
3065
3066 assert!(result.xml.contains("<COFINSST>"));
3067 assert!(!result.xml.contains("<COFINSAliq>"));
3068 assert_eq!(result.v_cofins_st, 750);
3069 }
3070
3071 #[test]
3072 fn cofins_st_does_not_accumulate_when_ind_soma_0() {
3073 use crate::tax_pis_cofins_ipi::CofinsStData;
3074 let cofins_st = CofinsStData::new(Cents(750))
3075 .v_bc(Cents(10000))
3076 .p_cofins(Rate4(76000))
3077 .ind_soma_cofins_st(0);
3078 let item = sample_item().cofins_st(cofins_st);
3079 let data = sample_build_data();
3080 let result = build_det(&item, &data).expect("build_det should succeed");
3081
3082 assert!(result.xml.contains("<COFINSST>"));
3083 assert_eq!(result.v_cofins_st, 0);
3084 }
3085
3086 #[test]
3089 fn is_data_emitted_with_pl010_schema() {
3090 use crate::tax_is::IsData;
3091 let is = IsData::new("00", "001", "5.00");
3092 let item = sample_item().is_data(is);
3093 let data = pl010_build_data();
3094 let result = build_det(&item, &data).expect("build_det should succeed");
3095
3096 assert!(result.xml.contains("<IS>"));
3097 }
3098
3099 #[test]
3100 fn is_data_not_emitted_with_pl009_schema() {
3101 use crate::tax_is::IsData;
3102 let is = IsData::new("00", "001", "5.00");
3103 let item = sample_item().is_data(is);
3104 let data = sample_build_data(); let result = build_det(&item, &data).expect("build_det should succeed");
3106
3107 assert!(!result.xml.contains("<IS>"));
3108 }
3109
3110 #[test]
3111 fn ibs_cbs_data_emitted_with_pl010_schema() {
3112 use crate::tax_ibs_cbs::IbsCbsData;
3113 let ibs_cbs = IbsCbsData::new("00", "001");
3114 let item = sample_item().ibs_cbs(ibs_cbs);
3115 let data = pl010_build_data();
3116 let result = build_det(&item, &data).expect("build_det should succeed");
3117
3118 assert!(result.xml.contains("<IBSCBS>"));
3119 }
3120
3121 #[test]
3124 fn cest_with_ind_escala_and_cnpj_fab() {
3125 let item = sample_item()
3126 .cest("1234567")
3127 .cest_ind_escala("S")
3128 .cest_cnpj_fab("12345678000199");
3129 let data = sample_build_data();
3130 let result = build_det(&item, &data).expect("build_det should succeed");
3131
3132 assert!(result.xml.contains("<CEST>1234567</CEST>"));
3133 assert!(result.xml.contains("<indEscala>S</indEscala>"));
3134 assert!(result.xml.contains("<CNPJFab>12345678000199</CNPJFab>"));
3135 }
3136
3137 #[test]
3140 fn extipi_produces_correct_xml() {
3141 let item = sample_item().extipi("01");
3142 let data = sample_build_data();
3143 let result = build_det(&item, &data).expect("build_det should succeed");
3144
3145 assert!(result.xml.contains("<EXTIPI>01</EXTIPI>"));
3146 let ncm_pos = result.xml.find("<NCM>").unwrap();
3148 let extipi_pos = result.xml.find("<EXTIPI>").unwrap();
3149 let cfop_pos = result.xml.find("<CFOP>").unwrap();
3150 assert!(extipi_pos > ncm_pos);
3151 assert!(extipi_pos < cfop_pos);
3152 }
3153
3154 #[test]
3157 fn n_item_ped_produces_correct_xml() {
3158 let item = sample_item().n_item_ped("5");
3159 let data = sample_build_data();
3160 let result = build_det(&item, &data).expect("build_det should succeed");
3161
3162 assert!(result.xml.contains("<nItemPed>5</nItemPed>"));
3163 }
3164
3165 #[test]
3166 fn n_fci_produces_correct_xml() {
3167 let item = sample_item().n_fci("B01F70AF-10BF-4B1F-848C-65FF57F616FE");
3168 let data = sample_build_data();
3169 let result = build_det(&item, &data).expect("build_det should succeed");
3170
3171 assert!(
3172 result
3173 .xml
3174 .contains("<nFCI>B01F70AF-10BF-4B1F-848C-65FF57F616FE</nFCI>")
3175 );
3176 }
3177
3178 #[test]
3181 fn veic_prod_produces_correct_xml() {
3182 let veic = VeicProdData::new(
3183 "1",
3184 "9BWZZZ377VT004251",
3185 "1",
3186 "PRATA",
3187 "100",
3188 "1600",
3189 "1050",
3190 "1250",
3191 "ABC123",
3192 "1",
3193 "MOT123",
3194 "1500",
3195 "2600",
3196 "2025",
3197 "2025",
3198 "M",
3199 "06",
3200 "1",
3201 "R",
3202 "1",
3203 "MOD001",
3204 "02",
3205 "5",
3206 "0",
3207 );
3208 let item = sample_item().veic_prod(veic);
3209 let data = sample_build_data();
3210 let result = build_det(&item, &data).expect("build_det should succeed");
3211
3212 assert!(result.xml.contains("<veicProd>"));
3213 assert!(result.xml.contains("<tpOp>1</tpOp>"));
3214 assert!(result.xml.contains("<chassi>9BWZZZ377VT004251</chassi>"));
3215 assert!(result.xml.contains("<cCor>1</cCor>"));
3216 assert!(result.xml.contains("<xCor>PRATA</xCor>"));
3217 assert!(result.xml.contains("<pot>100</pot>"));
3218 assert!(result.xml.contains("<cilin>1600</cilin>"));
3219 assert!(result.xml.contains("<pesoL>1050</pesoL>"));
3220 assert!(result.xml.contains("<pesoB>1250</pesoB>"));
3221 assert!(result.xml.contains("<nSerie>ABC123</nSerie>"));
3222 assert!(result.xml.contains("<tpComb>1</tpComb>"));
3223 assert!(result.xml.contains("<nMotor>MOT123</nMotor>"));
3224 assert!(result.xml.contains("<CMT>1500</CMT>"));
3225 assert!(result.xml.contains("<dist>2600</dist>"));
3226 assert!(result.xml.contains("<anoMod>2025</anoMod>"));
3227 assert!(result.xml.contains("<anoFab>2025</anoFab>"));
3228 assert!(result.xml.contains("<tpPint>M</tpPint>"));
3229 assert!(result.xml.contains("<tpVeic>06</tpVeic>"));
3230 assert!(result.xml.contains("<espVeic>1</espVeic>"));
3231 assert!(result.xml.contains("<VIN>R</VIN>"));
3232 assert!(result.xml.contains("<condVeic>1</condVeic>"));
3233 assert!(result.xml.contains("<cMod>MOD001</cMod>"));
3234 assert!(result.xml.contains("<cCorDENATRAN>02</cCorDENATRAN>"));
3235 assert!(result.xml.contains("<lota>5</lota>"));
3236 assert!(result.xml.contains("<tpRest>0</tpRest>"));
3237 assert!(result.xml.contains("</veicProd>"));
3238 }
3239
3240 #[test]
3243 fn med_with_anvisa_code() {
3244 let med = MedData::new(Cents(5000)).c_prod_anvisa("1234567890123");
3245 let item = sample_item().med(med);
3246 let data = sample_build_data();
3247 let result = build_det(&item, &data).expect("build_det should succeed");
3248
3249 assert!(result.xml.contains("<med>"));
3250 assert!(
3251 result
3252 .xml
3253 .contains("<cProdANVISA>1234567890123</cProdANVISA>")
3254 );
3255 assert!(result.xml.contains("<vPMC>50.00</vPMC>"));
3256 assert!(result.xml.contains("</med>"));
3257 }
3258
3259 #[test]
3260 fn med_with_exemption_reason() {
3261 let med = MedData::new(Cents(3000)).x_motivo_isencao("Medicamento isento de registro");
3262 let item = sample_item().med(med);
3263 let data = sample_build_data();
3264 let result = build_det(&item, &data).expect("build_det should succeed");
3265
3266 assert!(result.xml.contains("<med>"));
3267 assert!(
3268 result
3269 .xml
3270 .contains("<xMotivoIsencao>Medicamento isento de registro</xMotivoIsencao>")
3271 );
3272 assert!(result.xml.contains("<vPMC>30.00</vPMC>"));
3273 assert!(!result.xml.contains("<cProdANVISA>"));
3274 }
3275
3276 #[test]
3279 fn arma_single_produces_correct_xml() {
3280 let arma = ArmaData::new("0", "SN12345", "CN6789", "Pistola Taurus");
3281 let item = sample_item().arma(vec![arma]);
3282 let data = sample_build_data();
3283 let result = build_det(&item, &data).expect("build_det should succeed");
3284
3285 assert!(result.xml.contains("<arma>"));
3286 assert!(result.xml.contains("<tpArma>0</tpArma>"));
3287 assert!(result.xml.contains("<nSerie>SN12345</nSerie>"));
3288 assert!(result.xml.contains("<nCano>CN6789</nCano>"));
3289 assert!(result.xml.contains("<descr>Pistola Taurus</descr>"));
3290 assert!(result.xml.contains("</arma>"));
3291 }
3292
3293 #[test]
3294 fn arma_multiple_produces_multiple_elements() {
3295 let a1 = ArmaData::new("0", "SN001", "CN001", "Arma 1");
3296 let a2 = ArmaData::new("1", "SN002", "CN002", "Arma 2");
3297 let item = sample_item().arma(vec![a1, a2]);
3298 let data = sample_build_data();
3299 let result = build_det(&item, &data).expect("build_det should succeed");
3300
3301 assert_eq!(result.xml.matches("<arma>").count(), 2);
3302 assert!(result.xml.contains("<nSerie>SN001</nSerie>"));
3303 assert!(result.xml.contains("<nSerie>SN002</nSerie>"));
3304 }
3305
3306 #[test]
3309 fn n_recopi_produces_correct_xml() {
3310 let item = sample_item().n_recopi("20250000001234567890");
3311 let data = sample_build_data();
3312 let result = build_det(&item, &data).expect("build_det should succeed");
3313
3314 assert!(
3315 result
3316 .xml
3317 .contains("<nRECOPI>20250000001234567890</nRECOPI>")
3318 );
3319 }
3320
3321 #[test]
3322 fn n_recopi_empty_not_emitted() {
3323 let item = sample_item().n_recopi("");
3324 let data = sample_build_data();
3325 let result = build_det(&item, &data).expect("build_det should succeed");
3326
3327 assert!(!result.xml.contains("<nRECOPI>"));
3328 }
3329
3330 #[test]
3333 fn rastro_single_produces_correct_xml() {
3334 let r = RastroData::new("LOTE001", 10.5, "2025-01-01", "2026-01-01");
3335 let item = sample_item().rastro(vec![r]);
3336 let data = sample_build_data();
3337 let result = build_det(&item, &data).expect("build_det should succeed");
3338
3339 assert!(result.xml.contains("<rastro>"));
3340 assert!(result.xml.contains("<nLote>LOTE001</nLote>"));
3341 assert!(result.xml.contains("<qLote>10.500</qLote>"));
3342 assert!(result.xml.contains("<dFab>2025-01-01</dFab>"));
3343 assert!(result.xml.contains("<dVal>2026-01-01</dVal>"));
3344 assert!(result.xml.contains("</rastro>"));
3345 }
3346
3347 #[test]
3348 fn rastro_with_c_agreg() {
3349 let r = RastroData::new("LOTE002", 5.0, "2025-06-01", "2026-06-01").c_agreg("AGREG001");
3350 let item = sample_item().rastro(vec![r]);
3351 let data = sample_build_data();
3352 let result = build_det(&item, &data).expect("build_det should succeed");
3353
3354 assert!(result.xml.contains("<cAgreg>AGREG001</cAgreg>"));
3355 }
3356
3357 #[test]
3360 fn obs_item_with_obs_cont_only() {
3361 use crate::types::{ObsField, ObsItemData};
3362 let obs = ObsItemData::new().obs_cont(ObsField::new("campo1", "texto1"));
3363 let item = sample_item().obs_item(obs);
3364 let data = sample_build_data();
3365 let result = build_det(&item, &data).expect("build_det should succeed");
3366
3367 assert!(result.xml.contains("<obsItem>"));
3368 assert!(result.xml.contains("<obsCont xCampo=\"campo1\">"));
3369 assert!(result.xml.contains("<xTexto>texto1</xTexto>"));
3370 assert!(!result.xml.contains("<obsFisco"));
3371 }
3372
3373 #[test]
3374 fn obs_item_with_obs_fisco() {
3375 use crate::types::{ObsField, ObsItemData};
3376 let obs = ObsItemData::new().obs_fisco(ObsField::new("campo_fisco", "texto_fisco"));
3377 let item = sample_item().obs_item(obs);
3378 let data = sample_build_data();
3379 let result = build_det(&item, &data).expect("build_det should succeed");
3380
3381 assert!(result.xml.contains("<obsItem>"));
3382 assert!(result.xml.contains("<obsFisco xCampo=\"campo_fisco\">"));
3383 assert!(result.xml.contains("<xTexto>texto_fisco</xTexto>"));
3384 }
3385
3386 #[test]
3387 fn obs_item_with_both_obs_cont_and_obs_fisco() {
3388 use crate::types::{ObsField, ObsItemData};
3389 let obs = ObsItemData::new()
3390 .obs_cont(ObsField::new("campo_cont", "texto_cont"))
3391 .obs_fisco(ObsField::new("campo_fisco", "texto_fisco"));
3392 let item = sample_item().obs_item(obs);
3393 let data = sample_build_data();
3394 let result = build_det(&item, &data).expect("build_det should succeed");
3395
3396 assert!(result.xml.contains("<obsCont xCampo=\"campo_cont\">"));
3397 assert!(result.xml.contains("<obsFisco xCampo=\"campo_fisco\">"));
3398 }
3399
3400 #[test]
3403 fn dfe_referenciado_without_n_item() {
3404 use crate::types::DFeReferenciadoData;
3405 let dfe = DFeReferenciadoData::new("12345678901234567890123456789012345678901234");
3406 let item = sample_item().dfe_referenciado(dfe);
3407 let data = sample_build_data();
3408 let result = build_det(&item, &data).expect("build_det should succeed");
3409
3410 assert!(result.xml.contains("<DFeReferenciado>"));
3411 assert!(
3412 result.xml.contains(
3413 "<chaveAcesso>12345678901234567890123456789012345678901234</chaveAcesso>"
3414 )
3415 );
3416 assert!(!result.xml.contains("<nItem>"));
3417 }
3418
3419 #[test]
3420 fn dfe_referenciado_with_n_item() {
3421 use crate::types::DFeReferenciadoData;
3422 let dfe =
3423 DFeReferenciadoData::new("12345678901234567890123456789012345678901234").n_item("3");
3424 let item = sample_item().dfe_referenciado(dfe);
3425 let data = sample_build_data();
3426 let result = build_det(&item, &data).expect("build_det should succeed");
3427
3428 assert!(result.xml.contains("<DFeReferenciado>"));
3429 assert!(result.xml.contains("<nItem>3</nItem>"));
3430 }
3431
3432 #[test]
3435 fn nfce_homologation_substitutes_xprod_for_item_1() {
3436 let item = sample_item();
3437 let mut data = sample_build_data();
3438 data.model = InvoiceModel::Nfce;
3439 data.environment = SefazEnvironment::Homologation;
3440 let result = build_det(&item, &data).expect("build_det should succeed");
3441
3442 assert!(result.xml.contains(HOMOLOGATION_XPROD));
3443 }
3444
3445 #[test]
3446 fn nfce_homologation_does_not_substitute_for_item_2() {
3447 let mut item = sample_item();
3448 item.item_number = 2;
3449 let mut data = sample_build_data();
3450 data.model = InvoiceModel::Nfce;
3451 data.environment = SefazEnvironment::Homologation;
3452 let result = build_det(&item, &data).expect("build_det should succeed");
3453
3454 assert!(!result.xml.contains(HOMOLOGATION_XPROD));
3455 assert!(result.xml.contains("<xProd>Gasolina Comum</xProd>"));
3456 }
3457
3458 #[test]
3461 fn optional_value_fields_in_det_result() {
3462 let item = sample_item()
3463 .v_frete(Cents(1000))
3464 .v_seg(Cents(500))
3465 .v_desc(Cents(200))
3466 .v_outro(Cents(300));
3467 let data = sample_build_data();
3468 let result = build_det(&item, &data).expect("build_det should succeed");
3469
3470 assert!(result.xml.contains("<vFrete>10.00</vFrete>"));
3471 assert!(result.xml.contains("<vSeg>5.00</vSeg>"));
3472 assert!(result.xml.contains("<vDesc>2.00</vDesc>"));
3473 assert!(result.xml.contains("<vOutro>3.00</vOutro>"));
3474 assert_eq!(result.v_frete, 1000);
3475 assert_eq!(result.v_seg, 500);
3476 assert_eq!(result.v_desc, 200);
3477 assert_eq!(result.v_outro, 300);
3478 }
3479
3480 #[test]
3483 fn ind_tot_zero_excludes_from_total() {
3484 let item = sample_item().ind_tot(0);
3485 let data = sample_build_data();
3486 let result = build_det(&item, &data).expect("build_det should succeed");
3487
3488 assert!(result.xml.contains("<indTot>0</indTot>"));
3489 assert_eq!(result.ind_tot, 0);
3490 }
3491
3492 #[test]
3495 fn v_tot_trib_propagated_to_result() {
3496 let item = sample_item().v_tot_trib(Cents(1234));
3497 let data = sample_build_data();
3498 let result = build_det(&item, &data).expect("build_det should succeed");
3499
3500 assert_eq!(result.v_tot_trib, 1234);
3501 }
3502
3503 #[test]
3506 fn x_ped_produces_correct_xml() {
3507 let item = sample_item().x_ped("PEDIDO-001");
3508 let data = sample_build_data();
3509 let result = build_det(&item, &data).expect("build_det should succeed");
3510
3511 assert!(result.xml.contains("<xPed>PEDIDO-001</xPed>"));
3512 }
3513
3514 #[test]
3517 fn inf_ad_prod_produces_correct_xml() {
3518 let item = sample_item().inf_ad_prod("informacao adicional do produto");
3519 let data = sample_build_data();
3520 let result = build_det(&item, &data).expect("build_det should succeed");
3521
3522 assert!(
3523 result
3524 .xml
3525 .contains("<infAdProd>informacao adicional do produto</infAdProd>")
3526 );
3527 }
3528
3529 #[test]
3532 fn ind_deduz_deson_true_when_set_to_1() {
3533 let item = InvoiceItemData::new(
3534 1,
3535 "001",
3536 "Produto",
3537 "27101259",
3538 "5102",
3539 "UN",
3540 1.0,
3541 Cents(1000),
3542 Cents(1000),
3543 "40",
3544 Rate(0),
3545 Cents(0),
3546 "99",
3547 "99",
3548 )
3549 .icms_v_icms_deson(Cents(100))
3550 .icms_mot_des_icms(1)
3551 .icms_ind_deduz_deson("1");
3552 let data = normal_build_data();
3553 let result = build_det(&item, &data).expect("build_det should succeed");
3554 assert!(result.ind_deduz_deson);
3555 }
3556
3557 #[test]
3558 fn ind_deduz_deson_false_when_not_set() {
3559 let item = sample_item();
3560 let data = sample_build_data();
3561 let result = build_det(&item, &data).expect("build_det should succeed");
3562 assert!(!result.ind_deduz_deson);
3563 }
3564
3565 #[test]
3568 fn custom_orig_used_in_icms() {
3569 let item = InvoiceItemData::new(
3570 1,
3571 "001",
3572 "Produto",
3573 "27101259",
3574 "5102",
3575 "UN",
3576 1.0,
3577 Cents(1000),
3578 Cents(1000),
3579 "00",
3580 Rate(1800),
3581 Cents(180),
3582 "99",
3583 "99",
3584 )
3585 .orig("1");
3586 let data = normal_build_data();
3587 let result = build_det(&item, &data).expect("build_det should succeed");
3588
3589 assert!(result.xml.contains("<orig>1</orig>"));
3590 }
3591
3592 #[test]
3595 fn tp_cred_pres_ibs_zfm_emitted_with_pl010_schema() {
3596 let item = sample_item()
3597 .tp_cred_pres_ibs_zfm("1")
3598 .c_benef("SEM CBENEF");
3599 let data = pl010_build_data();
3600 let result = build_det(&item, &data).expect("build_det should succeed");
3601
3602 assert!(
3603 result
3604 .xml
3605 .contains("<tpCredPresIBSZFM>1</tpCredPresIBSZFM>")
3606 );
3607 let cbenef_pos = result.xml.find("<cBenef>").expect("cBenef should exist");
3609 let tp_pos = result
3610 .xml
3611 .find("<tpCredPresIBSZFM>")
3612 .expect("tpCredPresIBSZFM should exist");
3613 assert!(
3614 tp_pos > cbenef_pos,
3615 "tpCredPresIBSZFM must come after cBenef"
3616 );
3617 }
3618
3619 #[test]
3620 fn tp_cred_pres_ibs_zfm_not_emitted_with_pl009_schema() {
3621 let item = sample_item().tp_cred_pres_ibs_zfm("1");
3622 let data = sample_build_data(); let result = build_det(&item, &data).expect("build_det should succeed");
3624
3625 assert!(
3626 !result.xml.contains("<tpCredPresIBSZFM>"),
3627 "tpCredPresIBSZFM must not be emitted with PL009"
3628 );
3629 }
3630
3631 #[test]
3632 fn tp_cred_pres_ibs_zfm_not_emitted_when_not_set() {
3633 let item = sample_item();
3634 let data = pl010_build_data();
3635 let result = build_det(&item, &data).expect("build_det should succeed");
3636
3637 assert!(
3638 !result.xml.contains("<tpCredPresIBSZFM>"),
3639 "tpCredPresIBSZFM must not be emitted when not set"
3640 );
3641 }
3642
3643 #[test]
3644 fn tp_cred_pres_ibs_zfm_position_after_cbenef_before_gcred() {
3645 let gc = GCredData::new("ABC1234567", Rate4(2500)).v_cred_presumido(Cents(500));
3646 let item = sample_item()
3647 .c_benef("SEM CBENEF")
3648 .tp_cred_pres_ibs_zfm("2")
3649 .g_cred(vec![gc]);
3650 let data = pl010_build_data();
3651 let result = build_det(&item, &data).expect("build_det should succeed");
3652
3653 let cbenef_pos = result.xml.find("<cBenef>").expect("cBenef should exist");
3654 let tp_pos = result
3655 .xml
3656 .find("<tpCredPresIBSZFM>")
3657 .expect("tpCredPresIBSZFM should exist");
3658 let gcred_pos = result.xml.find("<gCred>").expect("gCred should exist");
3659 assert!(
3660 tp_pos > cbenef_pos && tp_pos < gcred_pos,
3661 "tpCredPresIBSZFM must be between cBenef and gCred"
3662 );
3663 }
3664
3665 #[test]
3668 fn v_item_emitted_with_pl010_and_ibs_cbs() {
3669 use crate::tax_ibs_cbs::IbsCbsData;
3670 let ibs_cbs = IbsCbsData::new("00", "001");
3671 let item = sample_item().ibs_cbs(ibs_cbs);
3672 let mut data = pl010_build_data();
3673 data.items = vec![item.clone()];
3675 let result = build_det(&item, &data).expect("build_det should succeed");
3676
3677 assert!(
3678 result.xml.contains("<vItem>"),
3679 "vItem must be emitted with PL010 and IBS/CBS data"
3680 );
3681 }
3682
3683 #[test]
3684 fn v_item_not_emitted_with_pl009() {
3685 use crate::tax_ibs_cbs::IbsCbsData;
3686 let ibs_cbs = IbsCbsData::new("00", "001");
3687 let item = sample_item().ibs_cbs(ibs_cbs);
3688 let mut data = sample_build_data(); data.items = vec![item.clone()];
3690 let result = build_det(&item, &data).expect("build_det should succeed");
3691
3692 assert!(
3693 !result.xml.contains("<vItem>"),
3694 "vItem must not be emitted with PL009"
3695 );
3696 }
3697
3698 #[test]
3699 fn v_item_not_emitted_without_ibs_cbs() {
3700 let item = sample_item();
3701 let mut data = pl010_build_data();
3702 data.items = vec![item.clone()];
3703 let result = build_det(&item, &data).expect("build_det should succeed");
3704
3705 assert!(
3706 !result.xml.contains("<vItem>"),
3707 "vItem must not be emitted without any IBS/CBS data"
3708 );
3709 }
3710
3711 #[test]
3712 fn v_item_auto_calculated_from_values() {
3713 use crate::tax_ibs_cbs::IbsCbsData;
3714 let ibs_cbs = IbsCbsData::new("00", "001");
3717 let item = InvoiceItemData::new(
3718 1,
3719 "001",
3720 "Produto",
3721 "27101259",
3722 "5102",
3723 "UN",
3724 1.0,
3725 Cents(1000),
3726 Cents(1000), "102", Rate(1800),
3729 Cents(180),
3730 "99",
3731 "99",
3732 )
3733 .v_desc(Cents(100))
3734 .v_frete(Cents(50))
3735 .v_seg(Cents(30))
3736 .v_outro(Cents(20))
3737 .ibs_cbs(ibs_cbs);
3738 let mut data = pl010_build_data();
3739 data.items = vec![item.clone()];
3740 let result = build_det(&item, &data).expect("build_det should succeed");
3741
3742 assert!(
3743 result.xml.contains("<vItem>10.00</vItem>"),
3744 "vItem should be auto-calculated as 10.00, got xml: {}",
3745 result.xml
3746 );
3747 }
3748
3749 #[test]
3750 fn v_item_user_supplied_takes_precedence() {
3751 use crate::tax_ibs_cbs::IbsCbsData;
3752 let ibs_cbs = IbsCbsData::new("00", "001");
3753 let item = sample_item().ibs_cbs(ibs_cbs).v_item(Cents(9999)); let mut data = pl010_build_data();
3755 data.items = vec![item.clone()];
3756 let result = build_det(&item, &data).expect("build_det should succeed");
3757
3758 assert!(
3759 result.xml.contains("<vItem>99.99</vItem>"),
3760 "vItem should use user-supplied value 99.99"
3761 );
3762 }
3763
3764 #[test]
3765 fn v_item_position_after_obs_item_before_dfe_referenciado() {
3766 use crate::tax_ibs_cbs::IbsCbsData;
3767 use crate::types::{DFeReferenciadoData, ObsField, ObsItemData};
3768 let ibs_cbs = IbsCbsData::new("00", "001");
3769 let obs = ObsItemData::new().obs_cont(ObsField::new("campo1", "texto1"));
3770 let dfe = DFeReferenciadoData::new("12345678901234567890123456789012345678901234");
3771 let item = sample_item()
3772 .ibs_cbs(ibs_cbs)
3773 .obs_item(obs)
3774 .dfe_referenciado(dfe)
3775 .v_item(Cents(5000));
3776 let mut data = pl010_build_data();
3777 data.items = vec![item.clone()];
3778 let result = build_det(&item, &data).expect("build_det should succeed");
3779
3780 let obs_pos = result.xml.find("<obsItem>").expect("obsItem should exist");
3781 let v_item_pos = result.xml.find("<vItem>").expect("vItem should exist");
3782 let dfe_pos = result
3783 .xml
3784 .find("<DFeReferenciado>")
3785 .expect("DFeReferenciado should exist");
3786 assert!(
3787 v_item_pos > obs_pos && v_item_pos < dfe_pos,
3788 "vItem must be between obsItem and DFeReferenciado"
3789 );
3790 }
3791
3792 #[test]
3793 fn v_item_emitted_for_item_without_ibs_cbs_when_another_item_has_it() {
3794 use crate::tax_ibs_cbs::IbsCbsData;
3795 let item1 = sample_item();
3797 let ibs_cbs = IbsCbsData::new("00", "001");
3798 let item2 = InvoiceItemData::new(
3799 2,
3800 "002",
3801 "Produto 2",
3802 "27101259",
3803 "5102",
3804 "UN",
3805 1.0,
3806 Cents(500),
3807 Cents(500),
3808 "00",
3809 Rate(1800),
3810 Cents(90),
3811 "99",
3812 "99",
3813 )
3814 .ibs_cbs(ibs_cbs);
3815 let mut data = pl010_build_data();
3816 data.items = vec![item1.clone(), item2.clone()];
3817 let result = build_det(&item1, &data).expect("build_det should succeed");
3819
3820 assert!(
3821 result.xml.contains("<vItem>"),
3822 "vItem must be emitted even for items without IBS/CBS when another item has it"
3823 );
3824 }
3825}