1use crate::FiscalError;
4use crate::format_utils::{format_cents, format_decimal, format_rate4};
5use crate::newtypes::{Cents, Rate, Rate4};
6use crate::tax_ibs_cbs;
7use crate::tax_icms::{self, IcmsCsosn, IcmsCst, IcmsTotals, IcmsVariant};
8use crate::tax_is;
9use crate::tax_issqn;
10use crate::tax_pis_cofins_ipi::{self, CofinsData, IiData, IpiData, PisData};
11use crate::types::{
12 CombData, InvoiceBuildData, InvoiceItemData, InvoiceModel, SefazEnvironment, TaxRegime,
13};
14use crate::xml_utils::{TagContent, tag};
15
16const HOMOLOGATION_XPROD: &str =
18 "NOTA FISCAL EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL";
19
20#[derive(Debug, Clone)]
22pub struct DetResult {
23 pub xml: String,
25 pub icms_totals: IcmsTotals,
27 pub v_ipi: i64,
29 pub v_pis: i64,
31 pub v_cofins: i64,
33 pub v_ii: i64,
35 pub v_frete: i64,
37 pub v_seg: i64,
39 pub v_desc: i64,
41 pub v_outro: i64,
43 pub ind_tot: u8,
45 pub v_tot_trib: i64,
47 pub v_ipi_devol: i64,
49 pub v_pis_st: i64,
51 pub v_cofins_st: i64,
53 pub ind_deduz_deson: bool,
55 pub has_issqn: bool,
57}
58
59fn build_icms_variant(
61 item: &InvoiceItemData,
62 is_simples: bool,
63) -> Result<IcmsVariant, FiscalError> {
64 let orig = item.orig.clone().unwrap_or_else(|| "0".to_string());
65
66 if is_simples {
67 let csosn_code = if item.icms_cst.is_empty() {
68 "102"
69 } else {
70 item.icms_cst.as_str()
71 };
72
73 let csosn = match csosn_code {
74 "101" => IcmsCsosn::Csosn101 {
75 orig,
76 csosn: csosn_code.to_string(),
77 p_cred_sn: item.icms_p_cred_sn.ok_or_else(|| {
78 FiscalError::MissingRequiredField {
79 field: "pCredSN".to_string(),
80 }
81 })?,
82 v_cred_icms_sn: item.icms_v_cred_icms_sn.ok_or_else(|| {
83 FiscalError::MissingRequiredField {
84 field: "vCredICMSSN".to_string(),
85 }
86 })?,
87 },
88 "102" => IcmsCsosn::Csosn102 {
89 orig,
90 csosn: csosn_code.to_string(),
91 },
92 "103" => IcmsCsosn::Csosn103 {
93 orig,
94 csosn: csosn_code.to_string(),
95 },
96 "300" => IcmsCsosn::Csosn300 {
97 orig,
98 csosn: csosn_code.to_string(),
99 },
100 "400" => IcmsCsosn::Csosn400 {
101 orig,
102 csosn: csosn_code.to_string(),
103 },
104 "201" => IcmsCsosn::Csosn201 {
105 orig,
106 csosn: csosn_code.to_string(),
107 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
108 FiscalError::MissingRequiredField {
109 field: "modBCST".to_string(),
110 }
111 })?,
112 p_mva_st: item.icms_p_mva_st,
113 p_red_bc_st: item.icms_red_bc_st,
114 v_bc_st: item
115 .icms_v_bc_st
116 .ok_or_else(|| FiscalError::MissingRequiredField {
117 field: "vBCST".to_string(),
118 })?,
119 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
120 FiscalError::MissingRequiredField {
121 field: "pICMSST".to_string(),
122 }
123 })?,
124 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
125 FiscalError::MissingRequiredField {
126 field: "vICMSST".to_string(),
127 }
128 })?,
129 v_bc_fcp_st: item.icms_v_bc_fcp_st,
130 p_fcp_st: item.icms_p_fcp_st,
131 v_fcp_st: item.icms_v_fcp_st,
132 p_cred_sn: item.icms_p_cred_sn,
133 v_cred_icms_sn: item.icms_v_cred_icms_sn,
134 },
135 "202" => IcmsCsosn::Csosn202 {
136 orig,
137 csosn: csosn_code.to_string(),
138 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
139 FiscalError::MissingRequiredField {
140 field: "modBCST".to_string(),
141 }
142 })?,
143 p_mva_st: item.icms_p_mva_st,
144 p_red_bc_st: item.icms_red_bc_st,
145 v_bc_st: item
146 .icms_v_bc_st
147 .ok_or_else(|| FiscalError::MissingRequiredField {
148 field: "vBCST".to_string(),
149 })?,
150 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
151 FiscalError::MissingRequiredField {
152 field: "pICMSST".to_string(),
153 }
154 })?,
155 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
156 FiscalError::MissingRequiredField {
157 field: "vICMSST".to_string(),
158 }
159 })?,
160 v_bc_fcp_st: item.icms_v_bc_fcp_st,
161 p_fcp_st: item.icms_p_fcp_st,
162 v_fcp_st: item.icms_v_fcp_st,
163 },
164 "203" => IcmsCsosn::Csosn203 {
165 orig,
166 csosn: csosn_code.to_string(),
167 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
168 FiscalError::MissingRequiredField {
169 field: "modBCST".to_string(),
170 }
171 })?,
172 p_mva_st: item.icms_p_mva_st,
173 p_red_bc_st: item.icms_red_bc_st,
174 v_bc_st: item
175 .icms_v_bc_st
176 .ok_or_else(|| FiscalError::MissingRequiredField {
177 field: "vBCST".to_string(),
178 })?,
179 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
180 FiscalError::MissingRequiredField {
181 field: "pICMSST".to_string(),
182 }
183 })?,
184 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
185 FiscalError::MissingRequiredField {
186 field: "vICMSST".to_string(),
187 }
188 })?,
189 v_bc_fcp_st: item.icms_v_bc_fcp_st,
190 p_fcp_st: item.icms_p_fcp_st,
191 v_fcp_st: item.icms_v_fcp_st,
192 },
193 "500" => IcmsCsosn::Csosn500 {
194 orig,
195 csosn: csosn_code.to_string(),
196 v_bc_st_ret: None,
197 p_st: None,
198 v_icms_substituto: item.icms_v_icms_substituto,
199 v_icms_st_ret: None,
200 v_bc_fcp_st_ret: None,
201 p_fcp_st_ret: None,
202 v_fcp_st_ret: None,
203 p_red_bc_efet: None,
204 v_bc_efet: None,
205 p_icms_efet: None,
206 v_icms_efet: None,
207 },
208 "900" => IcmsCsosn::Csosn900 {
209 orig,
210 csosn: csosn_code.to_string(),
211 mod_bc: item.icms_mod_bc.map(|v| v.to_string()),
212 v_bc: Some(item.total_price),
213 p_red_bc: item.icms_red_bc,
214 p_icms: Some(item.icms_rate),
215 v_icms: Some(item.icms_amount),
216 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()),
217 p_mva_st: item.icms_p_mva_st,
218 p_red_bc_st: item.icms_red_bc_st,
219 v_bc_st: item.icms_v_bc_st,
220 p_icms_st: item.icms_p_icms_st,
221 v_icms_st: item.icms_v_icms_st,
222 v_bc_fcp_st: item.icms_v_bc_fcp_st,
223 p_fcp_st: item.icms_p_fcp_st,
224 v_fcp_st: item.icms_v_fcp_st,
225 p_cred_sn: item.icms_p_cred_sn,
226 v_cred_icms_sn: item.icms_v_cred_icms_sn,
227 },
228 other => return Err(FiscalError::UnsupportedIcmsCsosn(other.to_string())),
229 };
230 Ok(csosn.into())
231 } else {
232 let cst_code = item.icms_cst.as_str();
233 let cst = match cst_code {
234 "00" => IcmsCst::Cst00 {
235 orig,
236 mod_bc: item
237 .icms_mod_bc
238 .map(|v| v.to_string())
239 .unwrap_or_else(|| "3".to_string()),
240 v_bc: item.total_price,
241 p_icms: item.icms_rate,
242 v_icms: item.icms_amount,
243 p_fcp: item.icms_p_fcp,
244 v_fcp: item.icms_v_fcp,
245 },
246 "10" => IcmsCst::Cst10 {
247 orig,
248 mod_bc: item
249 .icms_mod_bc
250 .map(|v| v.to_string())
251 .unwrap_or_else(|| "3".to_string()),
252 v_bc: item.total_price,
253 p_icms: item.icms_rate,
254 v_icms: item.icms_amount,
255 v_bc_fcp: item.icms_v_bc_fcp,
256 p_fcp: item.icms_p_fcp,
257 v_fcp: item.icms_v_fcp,
258 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
259 FiscalError::MissingRequiredField {
260 field: "modBCST".to_string(),
261 }
262 })?,
263 p_mva_st: item.icms_p_mva_st,
264 p_red_bc_st: item.icms_red_bc_st,
265 v_bc_st: item
266 .icms_v_bc_st
267 .ok_or_else(|| FiscalError::MissingRequiredField {
268 field: "vBCST".to_string(),
269 })?,
270 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
271 FiscalError::MissingRequiredField {
272 field: "pICMSST".to_string(),
273 }
274 })?,
275 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
276 FiscalError::MissingRequiredField {
277 field: "vICMSST".to_string(),
278 }
279 })?,
280 v_bc_fcp_st: item.icms_v_bc_fcp_st,
281 p_fcp_st: item.icms_p_fcp_st,
282 v_fcp_st: item.icms_v_fcp_st,
283 v_icms_st_deson: None,
284 mot_des_icms_st: None,
285 },
286 "20" => IcmsCst::Cst20 {
287 orig,
288 mod_bc: item
289 .icms_mod_bc
290 .map(|v| v.to_string())
291 .unwrap_or_else(|| "3".to_string()),
292 p_red_bc: item.icms_red_bc.unwrap_or(Rate(0)),
293 v_bc: item.total_price,
294 p_icms: item.icms_rate,
295 v_icms: item.icms_amount,
296 v_bc_fcp: item.icms_v_bc_fcp,
297 p_fcp: item.icms_p_fcp,
298 v_fcp: item.icms_v_fcp,
299 v_icms_deson: item.icms_v_icms_deson,
300 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
301 ind_deduz_deson: item.icms_ind_deduz_deson.clone(),
302 },
303 "30" => IcmsCst::Cst30 {
304 orig,
305 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
306 FiscalError::MissingRequiredField {
307 field: "modBCST".to_string(),
308 }
309 })?,
310 p_mva_st: item.icms_p_mva_st,
311 p_red_bc_st: item.icms_red_bc_st,
312 v_bc_st: item
313 .icms_v_bc_st
314 .ok_or_else(|| FiscalError::MissingRequiredField {
315 field: "vBCST".to_string(),
316 })?,
317 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
318 FiscalError::MissingRequiredField {
319 field: "pICMSST".to_string(),
320 }
321 })?,
322 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
323 FiscalError::MissingRequiredField {
324 field: "vICMSST".to_string(),
325 }
326 })?,
327 v_bc_fcp_st: item.icms_v_bc_fcp_st,
328 p_fcp_st: item.icms_p_fcp_st,
329 v_fcp_st: item.icms_v_fcp_st,
330 v_icms_deson: item.icms_v_icms_deson,
331 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
332 ind_deduz_deson: item.icms_ind_deduz_deson.clone(),
333 },
334 "40" => IcmsCst::Cst40 {
335 orig,
336 v_icms_deson: item.icms_v_icms_deson,
337 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
338 ind_deduz_deson: item.icms_ind_deduz_deson.clone(),
339 },
340 "41" => IcmsCst::Cst41 {
341 orig,
342 v_icms_deson: item.icms_v_icms_deson,
343 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
344 ind_deduz_deson: item.icms_ind_deduz_deson.clone(),
345 },
346 "50" => IcmsCst::Cst50 {
347 orig,
348 v_icms_deson: item.icms_v_icms_deson,
349 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
350 ind_deduz_deson: item.icms_ind_deduz_deson.clone(),
351 },
352 "51" => IcmsCst::Cst51 {
353 orig,
354 mod_bc: item.icms_mod_bc.map(|v| v.to_string()),
355 p_red_bc: item.icms_red_bc,
356 c_benef_rbc: None,
357 v_bc: Some(item.total_price),
358 p_icms: Some(item.icms_rate),
359 v_icms_op: None,
360 p_dif: None,
361 v_icms_dif: None,
362 v_icms: Some(item.icms_amount),
363 v_bc_fcp: item.icms_v_bc_fcp,
364 p_fcp: item.icms_p_fcp,
365 v_fcp: item.icms_v_fcp,
366 p_fcp_dif: None,
367 v_fcp_dif: None,
368 v_fcp_efet: None,
369 },
370 "60" => IcmsCst::Cst60 {
371 orig,
372 v_bc_st_ret: None,
373 p_st: None,
374 v_icms_substituto: item.icms_v_icms_substituto,
375 v_icms_st_ret: None,
376 v_bc_fcp_st_ret: None,
377 p_fcp_st_ret: None,
378 v_fcp_st_ret: None,
379 p_red_bc_efet: None,
380 v_bc_efet: None,
381 p_icms_efet: None,
382 v_icms_efet: None,
383 },
384 "70" => IcmsCst::Cst70 {
385 orig,
386 mod_bc: item
387 .icms_mod_bc
388 .map(|v| v.to_string())
389 .unwrap_or_else(|| "3".to_string()),
390 p_red_bc: item.icms_red_bc.unwrap_or(Rate(0)),
391 v_bc: item.total_price,
392 p_icms: item.icms_rate,
393 v_icms: item.icms_amount,
394 v_bc_fcp: item.icms_v_bc_fcp,
395 p_fcp: item.icms_p_fcp,
396 v_fcp: item.icms_v_fcp,
397 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
398 FiscalError::MissingRequiredField {
399 field: "modBCST".to_string(),
400 }
401 })?,
402 p_mva_st: item.icms_p_mva_st,
403 p_red_bc_st: item.icms_red_bc_st,
404 v_bc_st: item
405 .icms_v_bc_st
406 .ok_or_else(|| FiscalError::MissingRequiredField {
407 field: "vBCST".to_string(),
408 })?,
409 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
410 FiscalError::MissingRequiredField {
411 field: "pICMSST".to_string(),
412 }
413 })?,
414 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
415 FiscalError::MissingRequiredField {
416 field: "vICMSST".to_string(),
417 }
418 })?,
419 v_bc_fcp_st: item.icms_v_bc_fcp_st,
420 p_fcp_st: item.icms_p_fcp_st,
421 v_fcp_st: item.icms_v_fcp_st,
422 v_icms_deson: item.icms_v_icms_deson,
423 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
424 ind_deduz_deson: item.icms_ind_deduz_deson.clone(),
425 v_icms_st_deson: None,
426 mot_des_icms_st: None,
427 },
428 "90" => IcmsCst::Cst90 {
429 orig,
430 mod_bc: item.icms_mod_bc.map(|v| v.to_string()),
431 v_bc: Some(item.total_price),
432 p_red_bc: item.icms_red_bc,
433 c_benef_rbc: None,
434 p_icms: Some(item.icms_rate),
435 v_icms_op: None,
436 p_dif: None,
437 v_icms_dif: None,
438 v_icms: Some(item.icms_amount),
439 v_bc_fcp: item.icms_v_bc_fcp,
440 p_fcp: item.icms_p_fcp,
441 v_fcp: item.icms_v_fcp,
442 p_fcp_dif: None,
443 v_fcp_dif: None,
444 v_fcp_efet: None,
445 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()),
446 p_mva_st: item.icms_p_mva_st,
447 p_red_bc_st: item.icms_red_bc_st,
448 v_bc_st: item.icms_v_bc_st,
449 p_icms_st: item.icms_p_icms_st,
450 v_icms_st: item.icms_v_icms_st,
451 v_bc_fcp_st: item.icms_v_bc_fcp_st,
452 p_fcp_st: item.icms_p_fcp_st,
453 v_fcp_st: item.icms_v_fcp_st,
454 v_icms_deson: item.icms_v_icms_deson,
455 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
456 ind_deduz_deson: item.icms_ind_deduz_deson.clone(),
457 v_icms_st_deson: None,
458 mot_des_icms_st: None,
459 },
460 other => return Err(FiscalError::UnsupportedIcmsCst(other.to_string())),
461 };
462 Ok(cst.into())
463 }
464}
465
466pub(crate) fn build_det(
468 item: &InvoiceItemData,
469 data: &InvoiceBuildData,
470) -> Result<DetResult, FiscalError> {
471 if item.nve.len() > 8 {
473 return Err(FiscalError::InvalidTaxData(format!(
474 "Item {}: NVE limited to 8 entries, got {}",
475 item.item_number,
476 item.nve.len()
477 )));
478 }
479
480 let is_simples = matches!(
481 data.issuer.tax_regime,
482 TaxRegime::SimplesNacional | TaxRegime::SimplesExcess
483 );
484
485 let has_issqn = item.issqn.is_some();
486
487 let mut icms_totals = IcmsTotals::default();
489 let icms_xml = if has_issqn {
490 String::new()
491 } else {
492 let icms_variant = build_icms_variant(item, is_simples)?;
493 tax_icms::build_icms_xml(&icms_variant, &mut icms_totals)?
494 };
495
496 let issqn_xml = if let Some(ref issqn_data) = item.issqn {
498 tax_issqn::build_issqn_xml(issqn_data)
499 } else {
500 String::new()
501 };
502
503 let pis_xml = tax_pis_cofins_ipi::build_pis_xml(&PisData {
505 cst: item.pis_cst.clone(),
506 v_bc: item.pis_v_bc.or(Some(Cents(0))),
507 p_pis: item.pis_p_pis.or(Some(Rate4(0))),
508 v_pis: item.pis_v_pis.or(Some(Cents(0))),
509 q_bc_prod: item.pis_q_bc_prod,
510 v_aliq_prod: item.pis_v_aliq_prod,
511 });
512
513 let cofins_xml = tax_pis_cofins_ipi::build_cofins_xml(&CofinsData {
515 cst: item.cofins_cst.clone(),
516 v_bc: item.cofins_v_bc.or(Some(Cents(0))),
517 p_cofins: item.cofins_p_cofins.or(Some(Rate4(0))),
518 v_cofins: item.cofins_v_cofins.or(Some(Cents(0))),
519 q_bc_prod: item.cofins_q_bc_prod,
520 v_aliq_prod: item.cofins_v_aliq_prod,
521 });
522
523 let mut ipi_xml = String::new();
525 let mut v_ipi = 0i64;
526 if let Some(ref ipi_cst) = item.ipi_cst {
527 ipi_xml = tax_pis_cofins_ipi::build_ipi_xml(&IpiData {
528 cst: ipi_cst.clone(),
529 c_enq: item.ipi_c_enq.clone().unwrap_or_else(|| "999".to_string()),
530 v_bc: item.ipi_v_bc,
531 p_ipi: item.ipi_p_ipi,
532 v_ipi: item.ipi_v_ipi,
533 q_unid: item.ipi_q_unid,
534 v_unid: item.ipi_v_unid,
535 ..IpiData::default()
536 });
537 v_ipi = item.ipi_v_ipi.map(|c| c.0).unwrap_or(0);
538 }
539
540 let mut ii_xml = String::new();
542 let mut v_ii = 0i64;
543 if let Some(ii_vbc) = item.ii_v_bc {
544 ii_xml = tax_pis_cofins_ipi::build_ii_xml(&IiData {
545 v_bc: ii_vbc,
546 v_desp_adu: item.ii_v_desp_adu.unwrap_or(Cents(0)),
547 v_ii: item.ii_v_ii.unwrap_or(Cents(0)),
548 v_iof: item.ii_v_iof.unwrap_or(Cents(0)),
549 });
550 v_ii = item.ii_v_ii.map(|c| c.0).unwrap_or(0);
551 }
552
553 let prod_options = build_prod_options(item);
555
556 let mut v_pis_st = 0i64;
558 if let Some(ref pis_st_data) = item.pis_st {
559 if pis_st_data.ind_soma_pis_st == Some(1) {
561 v_pis_st = pis_st_data.v_pis.0;
562 }
563 }
564
565 let mut v_cofins_st = 0i64;
567 if let Some(ref cofins_st_data) = item.cofins_st {
568 if cofins_st_data.ind_soma_cofins_st == Some(1) {
570 v_cofins_st = cofins_st_data.v_cofins.0;
571 }
572 }
573
574 let item_ind_deduz_deson = item
576 .icms_ind_deduz_deson
577 .as_deref()
578 .map(|v| v == "1")
579 .unwrap_or(false);
580
581 let det_extras = build_det_extras(item);
583
584 let mut imposto_children: Vec<String> = Vec::new();
586 if !icms_xml.is_empty() {
587 imposto_children.push(icms_xml);
588 }
589 if !ipi_xml.is_empty() {
590 imposto_children.push(ipi_xml);
591 }
592 if let Some(ref pis_st) = item.pis_st {
594 imposto_children.push(tax_pis_cofins_ipi::build_pis_st_xml(pis_st));
595 } else {
596 imposto_children.push(pis_xml);
597 }
598 if let Some(ref cofins_st) = item.cofins_st {
600 imposto_children.push(tax_pis_cofins_ipi::build_cofins_st_xml(cofins_st));
601 } else {
602 imposto_children.push(cofins_xml);
603 }
604 if !ii_xml.is_empty() {
605 imposto_children.push(ii_xml);
606 }
607 if !issqn_xml.is_empty() {
608 imposto_children.push(issqn_xml);
609 }
610
611 if data.schema_version.is_pl010() {
614 if let Some(ref is_data) = item.is_data {
615 imposto_children.push(tax_is::build_is_xml(is_data));
616 }
617
618 if let Some(ref ibs_cbs_data) = item.ibs_cbs {
620 imposto_children.push(tax_ibs_cbs::build_ibs_cbs_xml(ibs_cbs_data));
621 }
622 }
623
624 let fc2 = |c: i64| format_cents(c, 2);
626 let fc10 = |c: i64| format_cents(c, 10);
627 let fd4 = |v: f64| format_decimal(v, 4);
628
629 let mut prod_children = vec![
630 tag("cProd", &[], TagContent::Text(&item.product_code)),
631 tag(
632 "cEAN",
633 &[],
634 TagContent::Text(item.c_ean.as_deref().unwrap_or("SEM GTIN")),
635 ),
636 ];
637 if let Some(ref cb) = item.c_barra {
638 prod_children.push(tag("cBarra", &[], TagContent::Text(cb)));
639 }
640 prod_children.push(tag(
641 "xProd",
642 &[],
643 TagContent::Text(
644 if item.item_number == 1
646 && data.environment == SefazEnvironment::Homologation
647 && data.model == InvoiceModel::Nfce
648 {
649 HOMOLOGATION_XPROD
650 } else {
651 &item.description
652 },
653 ),
654 ));
655 prod_children.push(tag("NCM", &[], TagContent::Text(&item.ncm)));
656 for nve_code in &item.nve {
657 prod_children.push(tag("NVE", &[], TagContent::Text(nve_code)));
658 }
659 if let Some(ref cest) = item.cest {
660 prod_children.push(tag("CEST", &[], TagContent::Text(cest)));
661 if let Some(ref ind) = item.cest_ind_escala {
662 prod_children.push(tag("indEscala", &[], TagContent::Text(ind)));
663 }
664 if let Some(ref fab) = item.cest_cnpj_fab {
665 prod_children.push(tag("CNPJFab", &[], TagContent::Text(fab)));
666 }
667 }
668 if let Some(ref cb) = item.c_benef {
669 prod_children.push(tag("cBenef", &[], TagContent::Text(cb)));
670 }
671 for gc in item.g_cred.iter().take(4) {
673 let p_str = format_rate4(gc.p_cred_presumido.0);
674 let mut gc_children = vec![
675 tag(
676 "cCredPresumido",
677 &[],
678 TagContent::Text(&gc.c_cred_presumido),
679 ),
680 tag("pCredPresumido", &[], TagContent::Text(&p_str)),
681 ];
682 if let Some(v) = gc.v_cred_presumido {
683 let v_str = format_cents(v.0, 2);
684 gc_children.push(tag("vCredPresumido", &[], TagContent::Text(&v_str)));
685 }
686 prod_children.push(tag("gCred", &[], TagContent::Children(gc_children)));
687 }
688 if let Some(ref ex) = item.extipi {
689 prod_children.push(tag("EXTIPI", &[], TagContent::Text(ex)));
690 }
691 prod_children.extend([
692 tag("CFOP", &[], TagContent::Text(&item.cfop)),
693 tag("uCom", &[], TagContent::Text(&item.unit_of_measure)),
694 tag("qCom", &[], TagContent::Text(&fd4(item.quantity))),
695 tag("vUnCom", &[], TagContent::Text(&fc10(item.unit_price.0))),
696 tag("vProd", &[], TagContent::Text(&fc2(item.total_price.0))),
697 tag(
698 "cEANTrib",
699 &[],
700 TagContent::Text(item.c_ean_trib.as_deref().unwrap_or("SEM GTIN")),
701 ),
702 ]);
703 if let Some(ref cbt) = item.c_barra_trib {
704 prod_children.push(tag("cBarraTrib", &[], TagContent::Text(cbt)));
705 }
706 let u_trib = item
707 .taxable_unit
708 .as_deref()
709 .unwrap_or(&item.unit_of_measure);
710 let q_trib = item.taxable_quantity.unwrap_or(item.quantity);
711 let v_un_trib = item
712 .taxable_unit_price
713 .map(|c| c.0)
714 .unwrap_or(item.unit_price.0);
715 prod_children.extend([
716 tag("uTrib", &[], TagContent::Text(u_trib)),
717 tag("qTrib", &[], TagContent::Text(&fd4(q_trib))),
718 tag("vUnTrib", &[], TagContent::Text(&fc10(v_un_trib))),
719 ]);
720 if let Some(v) = item.v_frete {
721 prod_children.push(tag("vFrete", &[], TagContent::Text(&fc2(v.0))));
722 }
723 if let Some(v) = item.v_seg {
724 prod_children.push(tag("vSeg", &[], TagContent::Text(&fc2(v.0))));
725 }
726 if let Some(v) = item.v_desc {
727 prod_children.push(tag("vDesc", &[], TagContent::Text(&fc2(v.0))));
728 }
729 if let Some(v) = item.v_outro {
730 prod_children.push(tag("vOutro", &[], TagContent::Text(&fc2(v.0))));
731 }
732 let ind_tot_str = match item.ind_tot {
733 Some(v) => v.to_string(),
734 None => "1".to_string(),
735 };
736 prod_children.push(tag("indTot", &[], TagContent::Text(&ind_tot_str)));
737 if item.ind_bem_movel_usado == Some(true) {
738 prod_children.push(tag("indBemMovelUsado", &[], TagContent::Text("1")));
739 }
740 if let Some(ref dis) = item.di {
742 for di in dis.iter().take(100) {
743 prod_children.push(build_di_xml(di));
744 }
745 }
746 if let Some(ref exports) = item.det_export {
748 for dex in exports.iter().take(500) {
749 prod_children.push(build_det_export_xml(dex));
750 }
751 }
752 if let Some(ref xped) = item.x_ped {
753 prod_children.push(tag("xPed", &[], TagContent::Text(xped)));
754 }
755 if let Some(ref nip) = item.n_item_ped {
756 prod_children.push(tag("nItemPed", &[], TagContent::Text(nip)));
757 }
758 if let Some(ref nfci) = item.n_fci {
759 prod_children.push(tag("nFCI", &[], TagContent::Text(nfci)));
760 }
761 prod_children.extend(prod_options);
762
763 let mut v_ipi_devol = 0i64;
765 let imposto_devol_xml = if let Some(ref devol) = item.imposto_devol {
766 v_ipi_devol = devol.v_ipi_devol.0;
767 let p_devol_str = format_decimal(devol.p_devol.0 as f64 / 100.0, 2);
768 let v_ipi_devol_str = format_cents(devol.v_ipi_devol.0, 2);
769 tag(
770 "impostoDevol",
771 &[],
772 TagContent::Children(vec![
773 tag("pDevol", &[], TagContent::Text(&p_devol_str)),
774 tag(
775 "IPI",
776 &[],
777 TagContent::Children(vec![tag(
778 "vIPIDevol",
779 &[],
780 TagContent::Text(&v_ipi_devol_str),
781 )]),
782 ),
783 ]),
784 )
785 } else {
786 String::new()
787 };
788
789 let nitem = item.item_number.to_string();
791 let mut det_children = vec![
792 tag("prod", &[], TagContent::Children(prod_children)),
793 tag("imposto", &[], TagContent::Children(imposto_children)),
794 ];
795 if !imposto_devol_xml.is_empty() {
796 det_children.push(imposto_devol_xml);
797 }
798 det_children.extend(det_extras);
799
800 let xml = tag(
801 "det",
802 &[("nItem", &nitem)],
803 TagContent::Children(det_children),
804 );
805
806 Ok(DetResult {
807 xml,
808 icms_totals,
809 v_ipi,
810 v_pis: item.pis_v_pis.map(|c| c.0).unwrap_or(0),
811 v_cofins: item.cofins_v_cofins.map(|c| c.0).unwrap_or(0),
812 v_ii,
813 v_frete: item.v_frete.map(|c| c.0).unwrap_or(0),
814 v_seg: item.v_seg.map(|c| c.0).unwrap_or(0),
815 v_desc: item.v_desc.map(|c| c.0).unwrap_or(0),
816 v_outro: item.v_outro.map(|c| c.0).unwrap_or(0),
817 ind_tot: item.ind_tot.unwrap_or(1),
818 v_tot_trib: item.v_tot_trib.map(|c| c.0).unwrap_or(0),
819 v_ipi_devol,
820 v_pis_st,
821 v_cofins_st,
822 ind_deduz_deson: item_ind_deduz_deson,
823 has_issqn,
824 })
825}
826
827fn build_prod_options(item: &InvoiceItemData) -> Vec<String> {
828 let mut opts = Vec::new();
829
830 if let Some(ref rastros) = item.rastro {
832 for r in rastros.iter().take(500) {
833 let mut rastro_children = vec![
834 tag("nLote", &[], TagContent::Text(&r.n_lote)),
835 tag("qLote", &[], TagContent::Text(&format_decimal(r.q_lote, 3))),
836 tag("dFab", &[], TagContent::Text(&r.d_fab)),
837 tag("dVal", &[], TagContent::Text(&r.d_val)),
838 ];
839 if let Some(ref agreg) = r.c_agreg {
840 rastro_children.push(tag("cAgreg", &[], TagContent::Text(agreg)));
841 }
842 opts.push(tag("rastro", &[], TagContent::Children(rastro_children)));
843 }
844 }
845
846 if let Some(ref v) = item.veic_prod {
848 opts.push(tag(
849 "veicProd",
850 &[],
851 TagContent::Children(vec![
852 tag("tpOp", &[], TagContent::Text(&v.tp_op)),
853 tag("chassi", &[], TagContent::Text(&v.chassi)),
854 tag("cCor", &[], TagContent::Text(&v.c_cor)),
855 tag("xCor", &[], TagContent::Text(&v.x_cor)),
856 tag("pot", &[], TagContent::Text(&v.pot)),
857 tag("cilin", &[], TagContent::Text(&v.cilin)),
858 tag("pesoL", &[], TagContent::Text(&v.peso_l)),
859 tag("pesoB", &[], TagContent::Text(&v.peso_b)),
860 tag("nSerie", &[], TagContent::Text(&v.n_serie)),
861 tag("tpComb", &[], TagContent::Text(&v.tp_comb)),
862 tag("nMotor", &[], TagContent::Text(&v.n_motor)),
863 tag("CMT", &[], TagContent::Text(&v.cmt)),
864 tag("dist", &[], TagContent::Text(&v.dist)),
865 tag("anoMod", &[], TagContent::Text(&v.ano_mod)),
866 tag("anoFab", &[], TagContent::Text(&v.ano_fab)),
867 tag("tpPint", &[], TagContent::Text(&v.tp_pint)),
868 tag("tpVeic", &[], TagContent::Text(&v.tp_veic)),
869 tag("espVeic", &[], TagContent::Text(&v.esp_veic)),
870 tag("VIN", &[], TagContent::Text(&v.vin)),
871 tag("condVeic", &[], TagContent::Text(&v.cond_veic)),
872 tag("cMod", &[], TagContent::Text(&v.c_mod)),
873 tag("cCorDENATRAN", &[], TagContent::Text(&v.c_cor_denatran)),
874 tag("lota", &[], TagContent::Text(&v.lota)),
875 tag("tpRest", &[], TagContent::Text(&v.tp_rest)),
876 ]),
877 ));
878 } else if let Some(ref m) = item.med {
879 let mut med_children = Vec::new();
880 if let Some(ref code) = m.c_prod_anvisa {
881 med_children.push(tag("cProdANVISA", &[], TagContent::Text(code)));
882 }
883 if let Some(ref reason) = m.x_motivo_isencao {
884 med_children.push(tag("xMotivoIsencao", &[], TagContent::Text(reason)));
885 }
886 med_children.push(tag(
887 "vPMC",
888 &[],
889 TagContent::Text(&format_cents(m.v_pmc.0, 2)),
890 ));
891 opts.push(tag("med", &[], TagContent::Children(med_children)));
892 } else if let Some(ref arms) = item.arma {
893 for a in arms.iter().take(500) {
894 opts.push(tag(
895 "arma",
896 &[],
897 TagContent::Children(vec![
898 tag("tpArma", &[], TagContent::Text(&a.tp_arma)),
899 tag("nSerie", &[], TagContent::Text(&a.n_serie)),
900 tag("nCano", &[], TagContent::Text(&a.n_cano)),
901 tag("descr", &[], TagContent::Text(&a.descr)),
902 ]),
903 ));
904 }
905 } else if let Some(ref recopi) = item.n_recopi {
906 if !recopi.is_empty() {
907 opts.push(tag("nRECOPI", &[], TagContent::Text(recopi)));
908 }
909 }
910
911 if let Some(ref comb) = item.comb {
913 opts.push(build_comb_xml(comb));
914 }
915
916 opts
917}
918
919fn build_di_xml(di: &crate::types::DiData) -> String {
921 let mut children = vec![
922 tag("nDI", &[], TagContent::Text(&di.n_di)),
923 tag("dDI", &[], TagContent::Text(&di.d_di)),
924 tag("xLocDesemb", &[], TagContent::Text(&di.x_loc_desemb)),
925 tag("UFDesemb", &[], TagContent::Text(&di.uf_desemb)),
926 tag("dDesemb", &[], TagContent::Text(&di.d_desemb)),
927 tag("tpViaTransp", &[], TagContent::Text(&di.tp_via_transp)),
928 ];
929 if let Some(ref v) = di.v_afrmm {
930 children.push(tag("vAFRMM", &[], TagContent::Text(&format_cents(v.0, 2))));
931 }
932 children.push(tag(
933 "tpIntermedio",
934 &[],
935 TagContent::Text(&di.tp_intermedio),
936 ));
937 if let Some(ref cnpj) = di.cnpj {
938 children.push(tag("CNPJ", &[], TagContent::Text(cnpj)));
939 } else if let Some(ref cpf) = di.cpf {
940 children.push(tag("CPF", &[], TagContent::Text(cpf)));
941 }
942 if let Some(ref uf) = di.uf_terceiro {
943 children.push(tag("UFTerceiro", &[], TagContent::Text(uf)));
944 }
945 children.push(tag("cExportador", &[], TagContent::Text(&di.c_exportador)));
946 for adi in di.adi.iter().take(999) {
948 let mut adi_children = Vec::new();
949 if let Some(ref n) = adi.n_adicao {
950 adi_children.push(tag("nAdicao", &[], TagContent::Text(n)));
951 }
952 adi_children.push(tag("nSeqAdic", &[], TagContent::Text(&adi.n_seq_adic)));
953 adi_children.push(tag("cFabricante", &[], TagContent::Text(&adi.c_fabricante)));
954 if let Some(ref v) = adi.v_desc_di {
955 adi_children.push(tag("vDescDI", &[], TagContent::Text(&format_cents(v.0, 2))));
956 }
957 if let Some(ref n) = adi.n_draw {
958 adi_children.push(tag("nDraw", &[], TagContent::Text(n)));
959 }
960 children.push(tag("adi", &[], TagContent::Children(adi_children)));
961 }
962 tag("DI", &[], TagContent::Children(children))
963}
964
965fn build_det_export_xml(dex: &crate::types::DetExportData) -> String {
967 let mut children = Vec::new();
968 if let Some(ref n) = dex.n_draw {
969 children.push(tag("nDraw", &[], TagContent::Text(n)));
970 }
971 if dex.n_re.is_some() || dex.ch_nfe.is_some() || dex.q_export.is_some() {
972 let mut exp_ind_children = Vec::new();
973 if let Some(ref n) = dex.n_re {
974 exp_ind_children.push(tag("nRE", &[], TagContent::Text(n)));
975 }
976 if let Some(ref ch) = dex.ch_nfe {
977 exp_ind_children.push(tag("chNFe", &[], TagContent::Text(ch)));
978 }
979 if let Some(q) = dex.q_export {
980 exp_ind_children.push(tag("qExport", &[], TagContent::Text(&format_decimal(q, 4))));
981 }
982 children.push(tag(
983 "exportInd",
984 &[],
985 TagContent::Children(exp_ind_children),
986 ));
987 }
988 tag("detExport", &[], TagContent::Children(children))
989}
990
991fn build_comb_xml(comb: &CombData) -> String {
997 let mut children = vec![
998 tag("cProdANP", &[], TagContent::Text(&comb.c_prod_anp)),
999 tag("descANP", &[], TagContent::Text(&comb.desc_anp)),
1000 ];
1001
1002 if let Some(ref v) = comb.p_glp {
1003 children.push(tag("pGLP", &[], TagContent::Text(v)));
1004 }
1005 if let Some(ref v) = comb.p_gn_n {
1006 children.push(tag("pGNn", &[], TagContent::Text(v)));
1007 }
1008 if let Some(ref v) = comb.p_gn_i {
1009 children.push(tag("pGNi", &[], TagContent::Text(v)));
1010 }
1011 if let Some(ref v) = comb.v_part {
1012 children.push(tag("vPart", &[], TagContent::Text(v)));
1013 }
1014 if let Some(ref v) = comb.codif {
1015 children.push(tag("CODIF", &[], TagContent::Text(v)));
1016 }
1017 if let Some(ref v) = comb.q_temp {
1018 children.push(tag("qTemp", &[], TagContent::Text(v)));
1019 }
1020
1021 children.push(tag("UFCons", &[], TagContent::Text(&comb.uf_cons)));
1022
1023 if let Some(ref cide) = comb.cide {
1025 let cide_children = vec![
1026 tag("qBCProd", &[], TagContent::Text(&cide.q_bc_prod)),
1027 tag("vAliqProd", &[], TagContent::Text(&cide.v_aliq_prod)),
1028 tag("vCIDE", &[], TagContent::Text(&cide.v_cide)),
1029 ];
1030 children.push(tag("CIDE", &[], TagContent::Children(cide_children)));
1031 }
1032
1033 if let Some(ref enc) = comb.encerrante {
1035 let mut enc_children = vec![tag("nBico", &[], TagContent::Text(&enc.n_bico))];
1036 if let Some(ref bomba) = enc.n_bomba {
1037 enc_children.push(tag("nBomba", &[], TagContent::Text(bomba)));
1038 }
1039 enc_children.push(tag("nTanque", &[], TagContent::Text(&enc.n_tanque)));
1040 enc_children.push(tag("vEncIni", &[], TagContent::Text(&enc.v_enc_ini)));
1041 enc_children.push(tag("vEncFin", &[], TagContent::Text(&enc.v_enc_fin)));
1042 children.push(tag("encerrante", &[], TagContent::Children(enc_children)));
1043 }
1044
1045 if let Some(ref v) = comb.p_bio {
1047 children.push(tag("pBio", &[], TagContent::Text(v)));
1048 }
1049
1050 if let Some(ref origins) = comb.orig_comb {
1052 for orig in origins {
1053 let orig_children = vec![
1054 tag("indImport", &[], TagContent::Text(&orig.ind_import)),
1055 tag("cUFOrig", &[], TagContent::Text(&orig.c_uf_orig)),
1056 tag("pOrig", &[], TagContent::Text(&orig.p_orig)),
1057 ];
1058 children.push(tag("origComb", &[], TagContent::Children(orig_children)));
1059 }
1060 }
1061
1062 tag("comb", &[], TagContent::Children(children))
1063}
1064
1065fn build_det_extras(item: &InvoiceItemData) -> Vec<String> {
1066 let mut extras = Vec::new();
1067
1068 if let Some(ref info) = item.inf_ad_prod {
1069 extras.push(tag("infAdProd", &[], TagContent::Text(info)));
1070 }
1071
1072 if let Some(ref obs) = item.obs_item {
1073 let mut obs_children = Vec::new();
1074 if let Some(ref cont) = obs.obs_cont {
1075 obs_children.push(tag(
1076 "obsCont",
1077 &[("xCampo", &cont.x_campo)],
1078 TagContent::Children(vec![tag("xTexto", &[], TagContent::Text(&cont.x_texto))]),
1079 ));
1080 }
1081 if let Some(ref fisco) = obs.obs_fisco {
1082 obs_children.push(tag(
1083 "obsFisco",
1084 &[("xCampo", &fisco.x_campo)],
1085 TagContent::Children(vec![tag("xTexto", &[], TagContent::Text(&fisco.x_texto))]),
1086 ));
1087 }
1088 extras.push(tag("obsItem", &[], TagContent::Children(obs_children)));
1089 }
1090
1091 if let Some(ref dfe) = item.dfe_referenciado {
1092 let mut dfe_children = vec![tag("chaveAcesso", &[], TagContent::Text(&dfe.chave_acesso))];
1093 if let Some(ref n) = dfe.n_item {
1094 dfe_children.push(tag("nItem", &[], TagContent::Text(n)));
1095 }
1096 extras.push(tag(
1097 "DFeReferenciado",
1098 &[],
1099 TagContent::Children(dfe_children),
1100 ));
1101 }
1102
1103 extras
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108 use super::*;
1109 use crate::newtypes::{Cents, IbgeCode, Rate, Rate4};
1110 use crate::tax_issqn::IssqnData as TaxIssqnData;
1111 use crate::types::{
1112 ArmaData, CideData, CombData, EncerranteData, GCredData, InvoiceItemData, InvoiceModel,
1113 IssuerData, MedData, OrigCombData, RastroData, SefazEnvironment, TaxRegime, VeicProdData,
1114 };
1115
1116 fn sample_build_data() -> InvoiceBuildData {
1117 let issuer = IssuerData::new(
1118 "12345678000199",
1119 "123456789",
1120 "Test Company",
1121 TaxRegime::SimplesNacional,
1122 "SP",
1123 IbgeCode("3550308".to_string()),
1124 "Sao Paulo",
1125 "Av Paulista",
1126 "1000",
1127 "Bela Vista",
1128 "01310100",
1129 );
1130
1131 InvoiceBuildData {
1132 schema_version: crate::types::SchemaVersion::PL009,
1133 model: InvoiceModel::Nfe,
1134 series: 1,
1135 number: 1,
1136 emission_type: crate::types::EmissionType::Normal,
1137 environment: SefazEnvironment::Homologation,
1138 issued_at: chrono::Utc::now()
1139 .with_timezone(&chrono::FixedOffset::west_opt(3 * 3600).expect("valid offset")),
1140 operation_nature: "VENDA".to_string(),
1141 issuer,
1142 recipient: None,
1143 items: Vec::new(),
1144 payments: Vec::new(),
1145 change_amount: None,
1146 payment_card_details: None,
1147 contingency: None,
1148 exit_at: None,
1149 operation_type: None,
1150 purpose_code: None,
1151 intermediary_indicator: None,
1152 emission_process: None,
1153 consumer_type: None,
1154 buyer_presence: None,
1155 print_format: None,
1156 references: None,
1157 transport: None,
1158 billing: None,
1159 withdrawal: None,
1160 delivery: None,
1161 authorized_xml: None,
1162 additional_info: None,
1163 intermediary: None,
1164 ret_trib: None,
1165 tech_responsible: None,
1166 purchase: None,
1167 export: None,
1168 issqn_tot: None,
1169 cana: None,
1170 agropecuario: None,
1171 compra_gov: None,
1172 pag_antecipado: None,
1173 is_tot: None,
1174 ibs_cbs_tot: None,
1175 destination_indicator: None,
1176 ver_proc: None,
1177 only_ascii: false,
1178 calculation_method: crate::types::CalculationMethod::V2,
1179 }
1180 }
1181
1182 fn sample_item() -> InvoiceItemData {
1183 InvoiceItemData::new(
1184 1,
1185 "001",
1186 "Gasolina Comum",
1187 "27101259",
1188 "5102",
1189 "LT",
1190 50.0,
1191 Cents(599),
1192 Cents(29950),
1193 "102",
1194 Rate(0),
1195 Cents(0),
1196 "99",
1197 "99",
1198 )
1199 }
1200
1201 #[test]
1204 fn comb_minimal_produces_correct_xml() {
1205 let comb = CombData::new("210203001", "GLP", "SP");
1206 let xml = build_comb_xml(&comb);
1207
1208 assert_eq!(
1209 xml,
1210 "<comb>\
1211 <cProdANP>210203001</cProdANP>\
1212 <descANP>GLP</descANP>\
1213 <UFCons>SP</UFCons>\
1214 </comb>"
1215 );
1216 }
1217
1218 #[test]
1219 fn comb_with_glp_percentages() {
1220 let comb = CombData::new("210203001", "GLP", "SP")
1221 .p_glp("60.0000")
1222 .p_gn_n("25.0000")
1223 .p_gn_i("15.0000")
1224 .v_part("3.50");
1225
1226 let xml = build_comb_xml(&comb);
1227
1228 assert_eq!(
1229 xml,
1230 "<comb>\
1231 <cProdANP>210203001</cProdANP>\
1232 <descANP>GLP</descANP>\
1233 <pGLP>60.0000</pGLP>\
1234 <pGNn>25.0000</pGNn>\
1235 <pGNi>15.0000</pGNi>\
1236 <vPart>3.50</vPart>\
1237 <UFCons>SP</UFCons>\
1238 </comb>"
1239 );
1240 }
1241
1242 #[test]
1243 fn comb_with_codif_and_qtemp() {
1244 let comb = CombData::new("320102001", "GASOLINA COMUM", "PR")
1245 .codif("123456789")
1246 .q_temp("1000.0000");
1247
1248 let xml = build_comb_xml(&comb);
1249
1250 assert_eq!(
1251 xml,
1252 "<comb>\
1253 <cProdANP>320102001</cProdANP>\
1254 <descANP>GASOLINA COMUM</descANP>\
1255 <CODIF>123456789</CODIF>\
1256 <qTemp>1000.0000</qTemp>\
1257 <UFCons>PR</UFCons>\
1258 </comb>"
1259 );
1260 }
1261
1262 #[test]
1263 fn comb_with_cide() {
1264 let cide = CideData::new("1000.0000", "0.0700", "70.00");
1265 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").cide(cide);
1266
1267 let xml = build_comb_xml(&comb);
1268
1269 assert_eq!(
1270 xml,
1271 "<comb>\
1272 <cProdANP>320102001</cProdANP>\
1273 <descANP>GASOLINA COMUM</descANP>\
1274 <UFCons>SP</UFCons>\
1275 <CIDE>\
1276 <qBCProd>1000.0000</qBCProd>\
1277 <vAliqProd>0.0700</vAliqProd>\
1278 <vCIDE>70.00</vCIDE>\
1279 </CIDE>\
1280 </comb>"
1281 );
1282 }
1283
1284 #[test]
1285 fn comb_with_encerrante() {
1286 let enc = EncerranteData::new("1", "1", "1234.567", "1284.567").n_bomba("2");
1287 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").encerrante(enc);
1288
1289 let xml = build_comb_xml(&comb);
1290
1291 assert_eq!(
1292 xml,
1293 "<comb>\
1294 <cProdANP>320102001</cProdANP>\
1295 <descANP>GASOLINA COMUM</descANP>\
1296 <UFCons>SP</UFCons>\
1297 <encerrante>\
1298 <nBico>1</nBico>\
1299 <nBomba>2</nBomba>\
1300 <nTanque>1</nTanque>\
1301 <vEncIni>1234.567</vEncIni>\
1302 <vEncFin>1284.567</vEncFin>\
1303 </encerrante>\
1304 </comb>"
1305 );
1306 }
1307
1308 #[test]
1309 fn comb_encerrante_without_bomba() {
1310 let enc = EncerranteData::new("3", "2", "5000.000", "5050.000");
1311 let comb = CombData::new("320102001", "GASOLINA COMUM", "RJ").encerrante(enc);
1312
1313 let xml = build_comb_xml(&comb);
1314
1315 assert_eq!(
1316 xml,
1317 "<comb>\
1318 <cProdANP>320102001</cProdANP>\
1319 <descANP>GASOLINA COMUM</descANP>\
1320 <UFCons>RJ</UFCons>\
1321 <encerrante>\
1322 <nBico>3</nBico>\
1323 <nTanque>2</nTanque>\
1324 <vEncIni>5000.000</vEncIni>\
1325 <vEncFin>5050.000</vEncFin>\
1326 </encerrante>\
1327 </comb>"
1328 );
1329 }
1330
1331 #[test]
1332 fn comb_with_pbio() {
1333 let comb = CombData::new("810102001", "OLEO DIESEL B S10", "SP").p_bio("15.0000");
1334
1335 let xml = build_comb_xml(&comb);
1336
1337 assert_eq!(
1338 xml,
1339 "<comb>\
1340 <cProdANP>810102001</cProdANP>\
1341 <descANP>OLEO DIESEL B S10</descANP>\
1342 <UFCons>SP</UFCons>\
1343 <pBio>15.0000</pBio>\
1344 </comb>"
1345 );
1346 }
1347
1348 #[test]
1349 fn comb_with_orig_comb_single() {
1350 let orig = OrigCombData::new("0", "35", "100.0000");
1351 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").orig_comb(vec![orig]);
1352
1353 let xml = build_comb_xml(&comb);
1354
1355 assert_eq!(
1356 xml,
1357 "<comb>\
1358 <cProdANP>320102001</cProdANP>\
1359 <descANP>GASOLINA COMUM</descANP>\
1360 <UFCons>SP</UFCons>\
1361 <origComb>\
1362 <indImport>0</indImport>\
1363 <cUFOrig>35</cUFOrig>\
1364 <pOrig>100.0000</pOrig>\
1365 </origComb>\
1366 </comb>"
1367 );
1368 }
1369
1370 #[test]
1371 fn comb_with_orig_comb_multiple() {
1372 let orig1 = OrigCombData::new("0", "35", "70.0000");
1373 let orig2 = OrigCombData::new("1", "99", "30.0000");
1374 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").orig_comb(vec![orig1, orig2]);
1375
1376 let xml = build_comb_xml(&comb);
1377
1378 assert_eq!(
1379 xml,
1380 "<comb>\
1381 <cProdANP>320102001</cProdANP>\
1382 <descANP>GASOLINA COMUM</descANP>\
1383 <UFCons>SP</UFCons>\
1384 <origComb>\
1385 <indImport>0</indImport>\
1386 <cUFOrig>35</cUFOrig>\
1387 <pOrig>70.0000</pOrig>\
1388 </origComb>\
1389 <origComb>\
1390 <indImport>1</indImport>\
1391 <cUFOrig>99</cUFOrig>\
1392 <pOrig>30.0000</pOrig>\
1393 </origComb>\
1394 </comb>"
1395 );
1396 }
1397
1398 #[test]
1399 fn comb_full_with_all_fields() {
1400 let cide = CideData::new("500.0000", "0.0700", "35.00");
1401 let enc = EncerranteData::new("1", "1", "10000.000", "10050.000").n_bomba("1");
1402 let orig = OrigCombData::new("0", "35", "100.0000");
1403
1404 let comb = CombData::new("210203001", "GLP", "SP")
1405 .p_glp("60.0000")
1406 .p_gn_n("25.0000")
1407 .p_gn_i("15.0000")
1408 .v_part("3.50")
1409 .codif("999888777")
1410 .q_temp("500.0000")
1411 .cide(cide)
1412 .encerrante(enc)
1413 .p_bio("12.0000")
1414 .orig_comb(vec![orig]);
1415
1416 let xml = build_comb_xml(&comb);
1417
1418 assert_eq!(
1419 xml,
1420 "<comb>\
1421 <cProdANP>210203001</cProdANP>\
1422 <descANP>GLP</descANP>\
1423 <pGLP>60.0000</pGLP>\
1424 <pGNn>25.0000</pGNn>\
1425 <pGNi>15.0000</pGNi>\
1426 <vPart>3.50</vPart>\
1427 <CODIF>999888777</CODIF>\
1428 <qTemp>500.0000</qTemp>\
1429 <UFCons>SP</UFCons>\
1430 <CIDE>\
1431 <qBCProd>500.0000</qBCProd>\
1432 <vAliqProd>0.0700</vAliqProd>\
1433 <vCIDE>35.00</vCIDE>\
1434 </CIDE>\
1435 <encerrante>\
1436 <nBico>1</nBico>\
1437 <nBomba>1</nBomba>\
1438 <nTanque>1</nTanque>\
1439 <vEncIni>10000.000</vEncIni>\
1440 <vEncFin>10050.000</vEncFin>\
1441 </encerrante>\
1442 <pBio>12.0000</pBio>\
1443 <origComb>\
1444 <indImport>0</indImport>\
1445 <cUFOrig>35</cUFOrig>\
1446 <pOrig>100.0000</pOrig>\
1447 </origComb>\
1448 </comb>"
1449 );
1450 }
1451
1452 #[test]
1453 fn comb_in_det_xml() {
1454 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP");
1455 let item = sample_item().comb(comb);
1456 let data = sample_build_data();
1457 let result = build_det(&item, &data).expect("build_det should succeed");
1458
1459 let prod_start = result.xml.find("<prod>").expect("<prod> must exist");
1461 let prod_end = result.xml.find("</prod>").expect("</prod> must exist");
1462 let prod_section = &result.xml[prod_start..prod_end];
1463
1464 assert!(prod_section.contains("<comb>"));
1465 assert!(prod_section.contains("<cProdANP>320102001</cProdANP>"));
1466 assert!(prod_section.contains("<descANP>GASOLINA COMUM</descANP>"));
1467 assert!(prod_section.contains("<UFCons>SP</UFCons>"));
1468 assert!(prod_section.contains("</comb>"));
1469 }
1470
1471 #[test]
1474 fn issqn_item_produces_issqn_tag_not_icms() {
1475 let issqn_data = TaxIssqnData::new(10000, 500, 500, "3550308", "14.01")
1476 .ind_iss("1")
1477 .ind_incentivo("2");
1478 let item = sample_item().issqn(issqn_data);
1479 let data = sample_build_data();
1480 let result = build_det(&item, &data).expect("build_det should succeed");
1481
1482 assert!(result.xml.contains("<ISSQN>"));
1484 assert!(result.xml.contains("<vBC>100.00</vBC>"));
1485 assert!(result.xml.contains("<vAliq>5.0000</vAliq>"));
1486 assert!(result.xml.contains("<vISSQN>5.00</vISSQN>"));
1487 assert!(result.xml.contains("<cMunFG>3550308</cMunFG>"));
1488 assert!(result.xml.contains("<cListServ>14.01</cListServ>"));
1489 assert!(result.xml.contains("<indISS>1</indISS>"));
1490 assert!(result.xml.contains("<indIncentivo>2</indIncentivo>"));
1491 assert!(result.xml.contains("</ISSQN>"));
1492
1493 assert!(!result.xml.contains("<ICMS>"));
1495 assert!(!result.xml.contains("</ICMS>"));
1496 assert!(result.has_issqn);
1497 }
1498
1499 #[test]
1500 fn issqn_item_with_all_optional_fields() {
1501 let issqn_data = TaxIssqnData::new(20000, 300, 600, "3304557", "07.02")
1502 .v_deducao(1000)
1503 .v_outro(500)
1504 .v_desc_incond(200)
1505 .v_desc_cond(100)
1506 .v_iss_ret(300)
1507 .ind_iss("1")
1508 .c_servico("1234")
1509 .c_mun("3304557")
1510 .c_pais("1058")
1511 .n_processo("ABC123")
1512 .ind_incentivo("1");
1513
1514 let item = sample_item().issqn(issqn_data);
1515 let data = sample_build_data();
1516 let result = build_det(&item, &data).expect("build_det should succeed");
1517
1518 assert!(result.xml.contains("<vBC>200.00</vBC>"));
1519 assert!(result.xml.contains("<vAliq>3.0000</vAliq>"));
1520 assert!(result.xml.contains("<vISSQN>6.00</vISSQN>"));
1521 assert!(result.xml.contains("<vDeducao>10.00</vDeducao>"));
1522 assert!(result.xml.contains("<vOutro>5.00</vOutro>"));
1523 assert!(result.xml.contains("<vDescIncond>2.00</vDescIncond>"));
1524 assert!(result.xml.contains("<vDescCond>1.00</vDescCond>"));
1525 assert!(result.xml.contains("<vISSRet>3.00</vISSRet>"));
1526 assert!(result.xml.contains("<cServico>1234</cServico>"));
1527 assert!(result.xml.contains("<cMun>3304557</cMun>"));
1528 assert!(result.xml.contains("<cPais>1058</cPais>"));
1529 assert!(result.xml.contains("<nProcesso>ABC123</nProcesso>"));
1530 assert!(result.xml.contains("<indIncentivo>1</indIncentivo>"));
1531 assert!(result.has_issqn);
1532 }
1533
1534 #[test]
1535 fn non_issqn_item_has_icms_and_no_issqn() {
1536 let item = sample_item();
1537 let data = sample_build_data();
1538 let result = build_det(&item, &data).expect("build_det should succeed");
1539
1540 assert!(result.xml.contains("<ICMS"));
1541 assert!(!result.xml.contains("<ISSQN>"));
1542 assert!(!result.has_issqn);
1543 }
1544
1545 #[test]
1548 fn di_minimal_with_one_adi() {
1549 use crate::types::{AdiData, DiData};
1550
1551 let adi = AdiData::new("1", "FABRICANTE_X").n_adicao("001");
1552 let di = DiData::new(
1553 "1234567890",
1554 "2025-01-15",
1555 "Santos",
1556 "SP",
1557 "2025-01-20",
1558 "1",
1559 "1",
1560 "EXP001",
1561 vec![adi],
1562 );
1563 let xml = build_di_xml(&di);
1564
1565 assert_eq!(
1566 xml,
1567 "<DI>\
1568 <nDI>1234567890</nDI>\
1569 <dDI>2025-01-15</dDI>\
1570 <xLocDesemb>Santos</xLocDesemb>\
1571 <UFDesemb>SP</UFDesemb>\
1572 <dDesemb>2025-01-20</dDesemb>\
1573 <tpViaTransp>1</tpViaTransp>\
1574 <tpIntermedio>1</tpIntermedio>\
1575 <cExportador>EXP001</cExportador>\
1576 <adi>\
1577 <nAdicao>001</nAdicao>\
1578 <nSeqAdic>1</nSeqAdic>\
1579 <cFabricante>FABRICANTE_X</cFabricante>\
1580 </adi>\
1581 </DI>"
1582 );
1583 }
1584
1585 #[test]
1586 fn di_with_all_optional_fields() {
1587 use crate::types::{AdiData, DiData};
1588
1589 let adi = AdiData::new("1", "FAB_Y")
1590 .n_adicao("002")
1591 .v_desc_di(Cents(15000))
1592 .n_draw("20259999999");
1593 let di = DiData::new(
1594 "DI-2025-001",
1595 "2025-03-01",
1596 "Paranagua",
1597 "PR",
1598 "2025-03-05",
1599 "1",
1600 "2",
1601 "EXP002",
1602 vec![adi],
1603 )
1604 .v_afrmm(Cents(5000))
1605 .cnpj("12345678000199")
1606 .uf_terceiro("RJ");
1607
1608 let xml = build_di_xml(&di);
1609
1610 assert_eq!(
1611 xml,
1612 "<DI>\
1613 <nDI>DI-2025-001</nDI>\
1614 <dDI>2025-03-01</dDI>\
1615 <xLocDesemb>Paranagua</xLocDesemb>\
1616 <UFDesemb>PR</UFDesemb>\
1617 <dDesemb>2025-03-05</dDesemb>\
1618 <tpViaTransp>1</tpViaTransp>\
1619 <vAFRMM>50.00</vAFRMM>\
1620 <tpIntermedio>2</tpIntermedio>\
1621 <CNPJ>12345678000199</CNPJ>\
1622 <UFTerceiro>RJ</UFTerceiro>\
1623 <cExportador>EXP002</cExportador>\
1624 <adi>\
1625 <nAdicao>002</nAdicao>\
1626 <nSeqAdic>1</nSeqAdic>\
1627 <cFabricante>FAB_Y</cFabricante>\
1628 <vDescDI>150.00</vDescDI>\
1629 <nDraw>20259999999</nDraw>\
1630 </adi>\
1631 </DI>"
1632 );
1633 }
1634
1635 #[test]
1636 fn di_with_cpf_instead_of_cnpj() {
1637 use crate::types::{AdiData, DiData};
1638
1639 let adi = AdiData::new("1", "FAB_Z");
1640 let di = DiData::new(
1641 "DI-CPF",
1642 "2025-06-01",
1643 "Recife",
1644 "PE",
1645 "2025-06-03",
1646 "7",
1647 "3",
1648 "EXP003",
1649 vec![adi],
1650 )
1651 .cpf("12345678901");
1652
1653 let xml = build_di_xml(&di);
1654 assert!(xml.contains("<CPF>12345678901</CPF>"));
1655 assert!(!xml.contains("<CNPJ>"));
1656 }
1657
1658 #[test]
1659 fn di_with_multiple_adi() {
1660 use crate::types::{AdiData, DiData};
1661
1662 let adi1 = AdiData::new("1", "FAB_A").n_adicao("001");
1663 let adi2 = AdiData::new("2", "FAB_B").n_adicao("001");
1664 let di = DiData::new(
1665 "DI-MULTI",
1666 "2025-01-01",
1667 "Santos",
1668 "SP",
1669 "2025-01-05",
1670 "1",
1671 "1",
1672 "EXP-M",
1673 vec![adi1, adi2],
1674 );
1675 let xml = build_di_xml(&di);
1676
1677 let count = xml.matches("<adi>").count();
1679 assert_eq!(count, 2, "expected 2 <adi> elements, got {count}");
1680 assert!(xml.contains("<nSeqAdic>1</nSeqAdic>"));
1681 assert!(xml.contains("<nSeqAdic>2</nSeqAdic>"));
1682 assert!(xml.contains("<cFabricante>FAB_A</cFabricante>"));
1683 assert!(xml.contains("<cFabricante>FAB_B</cFabricante>"));
1684 }
1685
1686 #[test]
1687 fn di_in_det_xml_between_ind_tot_and_xped() {
1688 use crate::types::{AdiData, DiData};
1689
1690 let adi = AdiData::new("1", "FAB").n_adicao("001");
1691 let di = DiData::new(
1692 "DI-001",
1693 "2025-01-15",
1694 "Santos",
1695 "SP",
1696 "2025-01-20",
1697 "1",
1698 "1",
1699 "EXP",
1700 vec![adi],
1701 );
1702 let item = sample_item().di(vec![di]).x_ped("PO-123");
1703 let data = sample_build_data();
1704 let result = build_det(&item, &data).expect("build_det should succeed");
1705
1706 let xml = &result.xml;
1707 let ind_tot_pos = xml.find("</indTot>").expect("</indTot> must exist");
1708 let di_pos = xml.find("<DI>").expect("<DI> must exist");
1709 let xped_pos = xml.find("<xPed>").expect("<xPed> must exist");
1710
1711 assert!(di_pos > ind_tot_pos, "DI must come after indTot");
1712 assert!(xped_pos > di_pos, "xPed must come after DI");
1713 }
1714
1715 #[test]
1718 fn det_export_with_n_draw_only() {
1719 use crate::types::DetExportData;
1720
1721 let dex = DetExportData::new().n_draw("20250000001");
1722 let xml = build_det_export_xml(&dex);
1723
1724 assert_eq!(
1725 xml,
1726 "<detExport>\
1727 <nDraw>20250000001</nDraw>\
1728 </detExport>"
1729 );
1730 }
1731
1732 #[test]
1733 fn det_export_with_export_ind() {
1734 use crate::types::DetExportData;
1735
1736 let dex = DetExportData::new()
1737 .n_draw("20250000002")
1738 .n_re("123456789012")
1739 .ch_nfe("12345678901234567890123456789012345678901234")
1740 .q_export(100.5);
1741 let xml = build_det_export_xml(&dex);
1742
1743 assert_eq!(
1744 xml,
1745 "<detExport>\
1746 <nDraw>20250000002</nDraw>\
1747 <exportInd>\
1748 <nRE>123456789012</nRE>\
1749 <chNFe>12345678901234567890123456789012345678901234</chNFe>\
1750 <qExport>100.5000</qExport>\
1751 </exportInd>\
1752 </detExport>"
1753 );
1754 }
1755
1756 #[test]
1757 fn det_export_empty() {
1758 use crate::types::DetExportData;
1759
1760 let dex = DetExportData::new();
1761 let xml = build_det_export_xml(&dex);
1762
1763 assert_eq!(xml, "<detExport></detExport>");
1764 }
1765
1766 #[test]
1767 fn det_export_in_det_xml_between_ind_tot_and_xped() {
1768 use crate::types::DetExportData;
1769
1770 let dex = DetExportData::new().n_draw("20250000001");
1771 let item = sample_item().det_export(vec![dex]).x_ped("PO-456");
1772 let data = sample_build_data();
1773 let result = build_det(&item, &data).expect("build_det should succeed");
1774
1775 let xml = &result.xml;
1776 let ind_tot_pos = xml.find("</indTot>").expect("</indTot> must exist");
1777 let det_exp_pos = xml.find("<detExport>").expect("<detExport> must exist");
1778 let xped_pos = xml.find("<xPed>").expect("<xPed> must exist");
1779
1780 assert!(
1781 det_exp_pos > ind_tot_pos,
1782 "detExport must come after indTot"
1783 );
1784 assert!(xped_pos > det_exp_pos, "xPed must come after detExport");
1785 }
1786
1787 #[test]
1790 fn imposto_devol_produces_correct_xml() {
1791 use crate::types::ImpostoDevolData;
1792
1793 let devol = ImpostoDevolData::new(Rate(10000), Cents(5000));
1794 let item = sample_item().imposto_devol(devol);
1795 let data = sample_build_data();
1796 let result = build_det(&item, &data).expect("build_det should succeed");
1797
1798 assert!(result.xml.contains(
1799 "<impostoDevol>\
1800 <pDevol>100.00</pDevol>\
1801 <IPI>\
1802 <vIPIDevol>50.00</vIPIDevol>\
1803 </IPI>\
1804 </impostoDevol>"
1805 ));
1806 assert_eq!(result.v_ipi_devol, 5000);
1807 }
1808
1809 #[test]
1810 fn imposto_devol_50_percent() {
1811 use crate::types::ImpostoDevolData;
1812
1813 let devol = ImpostoDevolData::new(Rate(5000), Cents(2500));
1814 let item = sample_item().imposto_devol(devol);
1815 let data = sample_build_data();
1816 let result = build_det(&item, &data).expect("build_det should succeed");
1817
1818 assert!(result.xml.contains("<pDevol>50.00</pDevol>"));
1819 assert!(result.xml.contains("<vIPIDevol>25.00</vIPIDevol>"));
1820 assert_eq!(result.v_ipi_devol, 2500);
1821 }
1822
1823 #[test]
1824 fn imposto_devol_after_imposto_before_inf_ad_prod() {
1825 use crate::types::ImpostoDevolData;
1826
1827 let devol = ImpostoDevolData::new(Rate(10000), Cents(1000));
1828 let item = sample_item().imposto_devol(devol).inf_ad_prod("test info");
1829 let data = sample_build_data();
1830 let result = build_det(&item, &data).expect("build_det should succeed");
1831
1832 let imposto_end = result
1833 .xml
1834 .find("</imposto>")
1835 .expect("</imposto> must exist");
1836 let devol_pos = result
1837 .xml
1838 .find("<impostoDevol>")
1839 .expect("<impostoDevol> must exist");
1840 let inf_ad_pos = result
1841 .xml
1842 .find("<infAdProd>")
1843 .expect("<infAdProd> must exist");
1844
1845 assert!(
1846 devol_pos > imposto_end,
1847 "impostoDevol must come after </imposto>"
1848 );
1849 assert!(
1850 inf_ad_pos > devol_pos,
1851 "infAdProd must come after impostoDevol"
1852 );
1853 }
1854
1855 #[test]
1856 fn no_imposto_devol_when_none() {
1857 let item = sample_item();
1858 let data = sample_build_data();
1859 let result = build_det(&item, &data).expect("build_det should succeed");
1860
1861 assert!(!result.xml.contains("<impostoDevol>"));
1862 assert_eq!(result.v_ipi_devol, 0);
1863 }
1864
1865 #[test]
1868 fn nve_single_code_produces_correct_xml() {
1869 let item = sample_item().nve("AA0001");
1870 let data = sample_build_data();
1871 let result = build_det(&item, &data).expect("build_det should succeed");
1872
1873 assert!(result.xml.contains("<NVE>AA0001</NVE>"));
1874 let ncm_pos = result.xml.find("<NCM>").expect("<NCM> must exist");
1876 let nve_pos = result
1877 .xml
1878 .find("<NVE>AA0001</NVE>")
1879 .expect("<NVE> must exist");
1880 assert!(nve_pos > ncm_pos, "NVE must come after NCM");
1881 }
1882
1883 #[test]
1884 fn nve_multiple_codes_produces_correct_xml() {
1885 let item = sample_item().nve("AA0001").nve("BB0002").nve("CC0003");
1886 let data = sample_build_data();
1887 let result = build_det(&item, &data).expect("build_det should succeed");
1888
1889 assert!(result.xml.contains("<NVE>AA0001</NVE>"));
1890 assert!(result.xml.contains("<NVE>BB0002</NVE>"));
1891 assert!(result.xml.contains("<NVE>CC0003</NVE>"));
1892 let pos_a = result.xml.find("<NVE>AA0001</NVE>").expect("AA0001");
1894 let pos_b = result.xml.find("<NVE>BB0002</NVE>").expect("BB0002");
1895 let pos_c = result.xml.find("<NVE>CC0003</NVE>").expect("CC0003");
1896 assert!(pos_a < pos_b, "NVE codes must preserve insertion order");
1897 assert!(pos_b < pos_c, "NVE codes must preserve insertion order");
1898 }
1899
1900 #[test]
1901 fn nve_eight_codes_is_valid() {
1902 let item = sample_item()
1903 .nve("AA0001")
1904 .nve("AA0002")
1905 .nve("AA0003")
1906 .nve("AA0004")
1907 .nve("AA0005")
1908 .nve("AA0006")
1909 .nve("AA0007")
1910 .nve("AA0008");
1911 let data = sample_build_data();
1912 let result = build_det(&item, &data);
1913 assert!(result.is_ok(), "8 NVE codes should be valid");
1914 let xml = result.expect("valid").xml;
1915 assert_eq!(xml.matches("<NVE>").count(), 8);
1916 }
1917
1918 #[test]
1919 fn nve_nine_codes_returns_error() {
1920 let item = sample_item()
1921 .nve("AA0001")
1922 .nve("AA0002")
1923 .nve("AA0003")
1924 .nve("AA0004")
1925 .nve("AA0005")
1926 .nve("AA0006")
1927 .nve("AA0007")
1928 .nve("AA0008")
1929 .nve("AA0009");
1930 let data = sample_build_data();
1931 let result = build_det(&item, &data);
1932 assert!(result.is_err(), "9 NVE codes should be rejected");
1933 let err = result.unwrap_err();
1934 assert_eq!(
1935 err,
1936 FiscalError::InvalidTaxData("Item 1: NVE limited to 8 entries, got 9".to_string())
1937 );
1938 }
1939
1940 #[test]
1941 fn nve_empty_vec_produces_no_nve_tags() {
1942 let item = sample_item();
1943 let data = sample_build_data();
1944 let result = build_det(&item, &data).expect("build_det should succeed");
1945
1946 assert!(!result.xml.contains("<NVE>"));
1947 }
1948
1949 #[test]
1950 fn nve_appears_before_cest() {
1951 let item = sample_item().nve("AA0001").cest("1234567");
1952 let data = sample_build_data();
1953 let result = build_det(&item, &data).expect("build_det should succeed");
1954
1955 let nve_pos = result
1956 .xml
1957 .find("<NVE>AA0001</NVE>")
1958 .expect("<NVE> must exist");
1959 let cest_pos = result.xml.find("<CEST>").expect("<CEST> must exist");
1960 assert!(nve_pos < cest_pos, "NVE must come before CEST");
1961 }
1962
1963 #[test]
1966 fn gcred_single_with_value_produces_correct_xml() {
1967 let gc = GCredData::new("SP000001", Rate4(50000)).v_cred_presumido(Cents(1500));
1968 let item = sample_item().g_cred(vec![gc]);
1969 let data = sample_build_data();
1970 let result = build_det(&item, &data).expect("build_det should succeed");
1971
1972 assert!(result.xml.contains(
1973 "<gCred><cCredPresumido>SP000001</cCredPresumido>\
1974 <pCredPresumido>5.0000</pCredPresumido>\
1975 <vCredPresumido>15.00</vCredPresumido></gCred>"
1976 ));
1977 }
1978
1979 #[test]
1980 fn gcred_without_value_omits_v_cred_presumido() {
1981 let gc = GCredData::new("RJ000002", Rate4(120000));
1982 let item = sample_item().g_cred(vec![gc]);
1983 let data = sample_build_data();
1984 let result = build_det(&item, &data).expect("build_det should succeed");
1985
1986 assert!(result.xml.contains(
1987 "<gCred>\
1988 <cCredPresumido>RJ000002</cCredPresumido>\
1989 <pCredPresumido>12.0000</pCredPresumido>\
1990 </gCred>"
1991 ));
1992 assert!(!result.xml.contains("<vCredPresumido>"));
1993 }
1994
1995 #[test]
1996 fn gcred_multiple_entries_up_to_four() {
1997 let entries = vec![
1998 GCredData::new("SP000001", Rate4(10000)).v_cred_presumido(Cents(100)),
1999 GCredData::new("SP000002", Rate4(20000)).v_cred_presumido(Cents(200)),
2000 GCredData::new("SP000003", Rate4(30000)).v_cred_presumido(Cents(300)),
2001 GCredData::new("SP000004", Rate4(40000)).v_cred_presumido(Cents(400)),
2002 ];
2003 let item = sample_item().g_cred(entries);
2004 let data = sample_build_data();
2005 let result = build_det(&item, &data).expect("build_det should succeed");
2006
2007 assert!(
2008 result
2009 .xml
2010 .contains("<cCredPresumido>SP000001</cCredPresumido>")
2011 );
2012 assert!(
2013 result
2014 .xml
2015 .contains("<cCredPresumido>SP000002</cCredPresumido>")
2016 );
2017 assert!(
2018 result
2019 .xml
2020 .contains("<cCredPresumido>SP000003</cCredPresumido>")
2021 );
2022 assert!(
2023 result
2024 .xml
2025 .contains("<cCredPresumido>SP000004</cCredPresumido>")
2026 );
2027 }
2028
2029 #[test]
2030 fn gcred_truncates_at_four_entries() {
2031 let entries = vec![
2032 GCredData::new("SP000001", Rate4(10000)),
2033 GCredData::new("SP000002", Rate4(20000)),
2034 GCredData::new("SP000003", Rate4(30000)),
2035 GCredData::new("SP000004", Rate4(40000)),
2036 GCredData::new("SP000005", Rate4(50000)),
2037 ];
2038 let item = sample_item().g_cred(entries);
2039 let data = sample_build_data();
2040 let result = build_det(&item, &data).expect("build_det should succeed");
2041
2042 assert!(
2043 result
2044 .xml
2045 .contains("<cCredPresumido>SP000004</cCredPresumido>")
2046 );
2047 assert!(
2048 !result
2049 .xml
2050 .contains("<cCredPresumido>SP000005</cCredPresumido>")
2051 );
2052 }
2053
2054 #[test]
2055 fn gcred_positioned_after_cbenef_before_cfop() {
2056 let gc = GCredData::new("MG000001", Rate4(50000)).v_cred_presumido(Cents(1000));
2057 let item = sample_item().c_benef("SEM CBENEF").g_cred(vec![gc]);
2058 let data = sample_build_data();
2059 let result = build_det(&item, &data).expect("build_det should succeed");
2060
2061 let cbenef_pos = result.xml.find("<cBenef>").expect("cBenef should exist");
2062 let gcred_pos = result.xml.find("<gCred>").expect("gCred should exist");
2063 let cfop_pos = result.xml.find("<CFOP>").expect("CFOP should exist");
2064
2065 assert!(gcred_pos > cbenef_pos, "gCred must come after cBenef");
2066 assert!(gcred_pos < cfop_pos, "gCred must come before CFOP");
2067 }
2068
2069 #[test]
2070 fn gcred_empty_vec_produces_no_gcred_tags() {
2071 let item = sample_item();
2072 let data = sample_build_data();
2073 let result = build_det(&item, &data).expect("build_det should succeed");
2074
2075 assert!(!result.xml.contains("<gCred>"));
2076 }
2077
2078 fn normal_build_data() -> InvoiceBuildData {
2081 let mut data = sample_build_data();
2082 data.issuer.tax_regime = TaxRegime::Normal;
2083 data
2084 }
2085
2086 fn pl010_build_data() -> InvoiceBuildData {
2087 let mut data = sample_build_data();
2088 data.schema_version = crate::types::SchemaVersion::PL010;
2089 data
2090 }
2091
2092 #[test]
2095 fn csosn_101_produces_correct_xml() {
2096 let item = InvoiceItemData::new(
2097 1,
2098 "001",
2099 "Produto",
2100 "27101259",
2101 "5102",
2102 "UN",
2103 1.0,
2104 Cents(1000),
2105 Cents(1000),
2106 "101",
2107 Rate(1800),
2108 Cents(180),
2109 "99",
2110 "99",
2111 )
2112 .icms_p_cred_sn(Rate(500))
2113 .icms_v_cred_icms_sn(Cents(50));
2114 let data = sample_build_data();
2115 let result = build_det(&item, &data).expect("build_det should succeed");
2116
2117 assert!(result.xml.contains("<ICMSSN101>"));
2118 assert!(result.xml.contains("<CSOSN>101</CSOSN>"));
2119 assert!(result.xml.contains("<pCredSN>"));
2120 assert!(result.xml.contains("<vCredICMSSN>"));
2121 }
2122
2123 #[test]
2124 fn csosn_101_missing_p_cred_sn_returns_error() {
2125 let item = InvoiceItemData::new(
2126 1,
2127 "001",
2128 "Produto",
2129 "27101259",
2130 "5102",
2131 "UN",
2132 1.0,
2133 Cents(1000),
2134 Cents(1000),
2135 "101",
2136 Rate(0),
2137 Cents(0),
2138 "99",
2139 "99",
2140 )
2141 .icms_v_cred_icms_sn(Cents(50));
2142 let data = sample_build_data();
2143 let result = build_det(&item, &data);
2144 assert!(result.is_err());
2145 }
2146
2147 #[test]
2148 fn csosn_101_missing_v_cred_icms_sn_returns_error() {
2149 let item = InvoiceItemData::new(
2150 1,
2151 "001",
2152 "Produto",
2153 "27101259",
2154 "5102",
2155 "UN",
2156 1.0,
2157 Cents(1000),
2158 Cents(1000),
2159 "101",
2160 Rate(0),
2161 Cents(0),
2162 "99",
2163 "99",
2164 )
2165 .icms_p_cred_sn(Rate(500));
2166 let data = sample_build_data();
2167 let result = build_det(&item, &data);
2168 assert!(result.is_err());
2169 }
2170
2171 #[test]
2172 fn csosn_empty_defaults_to_102() {
2173 let item = InvoiceItemData::new(
2175 1,
2176 "001",
2177 "Produto",
2178 "27101259",
2179 "5102",
2180 "UN",
2181 1.0,
2182 Cents(1000),
2183 Cents(1000),
2184 "",
2185 Rate(0),
2186 Cents(0),
2187 "99",
2188 "99",
2189 );
2190 let data = sample_build_data();
2191 let result = build_det(&item, &data).expect("build_det should succeed");
2192
2193 assert!(result.xml.contains("<ICMSSN102>"));
2194 assert!(result.xml.contains("<CSOSN>102</CSOSN>"));
2195 }
2196
2197 #[test]
2198 fn csosn_103_produces_correct_xml() {
2199 let item = InvoiceItemData::new(
2200 1,
2201 "001",
2202 "Produto",
2203 "27101259",
2204 "5102",
2205 "UN",
2206 1.0,
2207 Cents(1000),
2208 Cents(1000),
2209 "103",
2210 Rate(0),
2211 Cents(0),
2212 "99",
2213 "99",
2214 );
2215 let data = sample_build_data();
2216 let result = build_det(&item, &data).expect("build_det should succeed");
2217 assert!(result.xml.contains("<ICMSSN102>"));
2219 assert!(result.xml.contains("<CSOSN>103</CSOSN>"));
2220 }
2221
2222 #[test]
2223 fn csosn_300_produces_correct_xml() {
2224 let item = InvoiceItemData::new(
2225 1,
2226 "001",
2227 "Produto",
2228 "27101259",
2229 "5102",
2230 "UN",
2231 1.0,
2232 Cents(1000),
2233 Cents(1000),
2234 "300",
2235 Rate(0),
2236 Cents(0),
2237 "99",
2238 "99",
2239 );
2240 let data = sample_build_data();
2241 let result = build_det(&item, &data).expect("build_det should succeed");
2242 assert!(result.xml.contains("<ICMSSN102>"));
2243 assert!(result.xml.contains("<CSOSN>300</CSOSN>"));
2244 }
2245
2246 #[test]
2247 fn csosn_400_produces_correct_xml() {
2248 let item = InvoiceItemData::new(
2249 1,
2250 "001",
2251 "Produto",
2252 "27101259",
2253 "5102",
2254 "UN",
2255 1.0,
2256 Cents(1000),
2257 Cents(1000),
2258 "400",
2259 Rate(0),
2260 Cents(0),
2261 "99",
2262 "99",
2263 );
2264 let data = sample_build_data();
2265 let result = build_det(&item, &data).expect("build_det should succeed");
2266 assert!(result.xml.contains("<ICMSSN102>"));
2267 assert!(result.xml.contains("<CSOSN>400</CSOSN>"));
2268 }
2269
2270 #[test]
2271 fn csosn_201_produces_correct_xml() {
2272 let item = InvoiceItemData::new(
2273 1,
2274 "001",
2275 "Produto",
2276 "27101259",
2277 "5102",
2278 "UN",
2279 1.0,
2280 Cents(1000),
2281 Cents(1000),
2282 "201",
2283 Rate(1800),
2284 Cents(180),
2285 "99",
2286 "99",
2287 )
2288 .icms_mod_bc_st(4)
2289 .icms_v_bc_st(Cents(1200))
2290 .icms_p_icms_st(Rate(1200))
2291 .icms_v_icms_st(Cents(144))
2292 .icms_p_mva_st(Rate(5000))
2293 .icms_red_bc_st(Rate(1000))
2294 .icms_v_bc_fcp_st(Cents(1200))
2295 .icms_p_fcp_st(Rate(200))
2296 .icms_v_fcp_st(Cents(24))
2297 .icms_p_cred_sn(Rate(500))
2298 .icms_v_cred_icms_sn(Cents(50));
2299 let data = sample_build_data();
2300 let result = build_det(&item, &data).expect("build_det should succeed");
2301
2302 assert!(result.xml.contains("<ICMSSN201>"));
2303 assert!(result.xml.contains("<CSOSN>201</CSOSN>"));
2304 assert!(result.xml.contains("<modBCST>4</modBCST>"));
2305 assert!(result.xml.contains("<vBCST>12.00</vBCST>"));
2306 assert!(result.xml.contains("<pICMSST>"));
2307 assert!(result.xml.contains("<vICMSST>"));
2308 assert!(result.xml.contains("<pMVAST>"));
2309 assert!(result.xml.contains("<pRedBCST>"));
2310 }
2311
2312 #[test]
2313 fn csosn_202_produces_correct_xml() {
2314 let item = InvoiceItemData::new(
2315 1,
2316 "001",
2317 "Produto",
2318 "27101259",
2319 "5102",
2320 "UN",
2321 1.0,
2322 Cents(1000),
2323 Cents(1000),
2324 "202",
2325 Rate(0),
2326 Cents(0),
2327 "99",
2328 "99",
2329 )
2330 .icms_mod_bc_st(4)
2331 .icms_v_bc_st(Cents(1200))
2332 .icms_p_icms_st(Rate(1200))
2333 .icms_v_icms_st(Cents(144))
2334 .icms_p_mva_st(Rate(5000))
2335 .icms_red_bc_st(Rate(1000))
2336 .icms_v_bc_fcp_st(Cents(1200))
2337 .icms_p_fcp_st(Rate(200))
2338 .icms_v_fcp_st(Cents(24));
2339 let data = sample_build_data();
2340 let result = build_det(&item, &data).expect("build_det should succeed");
2341
2342 assert!(result.xml.contains("<ICMSSN202>"));
2343 assert!(result.xml.contains("<CSOSN>202</CSOSN>"));
2344 assert!(result.xml.contains("<modBCST>4</modBCST>"));
2345 }
2346
2347 #[test]
2348 fn csosn_203_produces_correct_xml() {
2349 let item = InvoiceItemData::new(
2350 1,
2351 "001",
2352 "Produto",
2353 "27101259",
2354 "5102",
2355 "UN",
2356 1.0,
2357 Cents(1000),
2358 Cents(1000),
2359 "203",
2360 Rate(0),
2361 Cents(0),
2362 "99",
2363 "99",
2364 )
2365 .icms_mod_bc_st(4)
2366 .icms_v_bc_st(Cents(1200))
2367 .icms_p_icms_st(Rate(1200))
2368 .icms_v_icms_st(Cents(144))
2369 .icms_p_mva_st(Rate(5000))
2370 .icms_red_bc_st(Rate(1000))
2371 .icms_v_bc_fcp_st(Cents(1200))
2372 .icms_p_fcp_st(Rate(200))
2373 .icms_v_fcp_st(Cents(24));
2374 let data = sample_build_data();
2375 let result = build_det(&item, &data).expect("build_det should succeed");
2376
2377 assert!(result.xml.contains("<ICMSSN202>"));
2379 assert!(result.xml.contains("<CSOSN>203</CSOSN>"));
2380 assert!(result.xml.contains("<modBCST>4</modBCST>"));
2381 }
2382
2383 #[test]
2384 fn csosn_500_produces_correct_xml() {
2385 let item = InvoiceItemData::new(
2386 1,
2387 "001",
2388 "Produto",
2389 "27101259",
2390 "5102",
2391 "UN",
2392 1.0,
2393 Cents(1000),
2394 Cents(1000),
2395 "500",
2396 Rate(0),
2397 Cents(0),
2398 "99",
2399 "99",
2400 )
2401 .icms_v_icms_substituto(Cents(200));
2402 let data = sample_build_data();
2403 let result = build_det(&item, &data).expect("build_det should succeed");
2404
2405 assert!(result.xml.contains("<ICMSSN500>"));
2406 assert!(result.xml.contains("<CSOSN>500</CSOSN>"));
2407 assert!(result.xml.contains("<vICMSSubstituto>"));
2408 }
2409
2410 #[test]
2411 fn csosn_900_produces_correct_xml() {
2412 let item = InvoiceItemData::new(
2413 1,
2414 "001",
2415 "Produto",
2416 "27101259",
2417 "5102",
2418 "UN",
2419 1.0,
2420 Cents(1000),
2421 Cents(1000),
2422 "900",
2423 Rate(1800),
2424 Cents(180),
2425 "99",
2426 "99",
2427 )
2428 .icms_mod_bc(3)
2429 .icms_red_bc(Rate(1000))
2430 .icms_mod_bc_st(4)
2431 .icms_p_mva_st(Rate(5000))
2432 .icms_red_bc_st(Rate(1000))
2433 .icms_v_bc_st(Cents(1200))
2434 .icms_p_icms_st(Rate(1200))
2435 .icms_v_icms_st(Cents(144))
2436 .icms_v_bc_fcp_st(Cents(1200))
2437 .icms_p_fcp_st(Rate(200))
2438 .icms_v_fcp_st(Cents(24))
2439 .icms_p_cred_sn(Rate(500))
2440 .icms_v_cred_icms_sn(Cents(50));
2441 let data = sample_build_data();
2442 let result = build_det(&item, &data).expect("build_det should succeed");
2443
2444 assert!(result.xml.contains("<ICMSSN900>"));
2445 assert!(result.xml.contains("<CSOSN>900</CSOSN>"));
2446 assert!(result.xml.contains("<modBC>3</modBC>"));
2447 }
2448
2449 #[test]
2450 fn csosn_unsupported_returns_error() {
2451 let item = InvoiceItemData::new(
2452 1,
2453 "001",
2454 "Produto",
2455 "27101259",
2456 "5102",
2457 "UN",
2458 1.0,
2459 Cents(1000),
2460 Cents(1000),
2461 "999",
2462 Rate(0),
2463 Cents(0),
2464 "99",
2465 "99",
2466 );
2467 let data = sample_build_data();
2468 let result = build_det(&item, &data);
2469 assert!(result.is_err());
2470 let err = result.unwrap_err();
2471 assert!(
2472 matches!(err, FiscalError::UnsupportedIcmsCsosn(ref c) if c == "999"),
2473 "expected UnsupportedIcmsCsosn, got {:?}",
2474 err
2475 );
2476 }
2477
2478 #[test]
2481 fn cst_10_produces_correct_xml() {
2482 let item = InvoiceItemData::new(
2483 1,
2484 "001",
2485 "Produto",
2486 "27101259",
2487 "5102",
2488 "UN",
2489 1.0,
2490 Cents(1000),
2491 Cents(1000),
2492 "10",
2493 Rate(1800),
2494 Cents(180),
2495 "99",
2496 "99",
2497 )
2498 .icms_mod_bc_st(4)
2499 .icms_v_bc_st(Cents(1200))
2500 .icms_p_icms_st(Rate(1200))
2501 .icms_v_icms_st(Cents(144))
2502 .icms_v_bc_fcp(Cents(1000))
2503 .icms_p_fcp(Rate(200))
2504 .icms_v_fcp(Cents(20))
2505 .icms_p_mva_st(Rate(5000))
2506 .icms_red_bc_st(Rate(1000))
2507 .icms_v_bc_fcp_st(Cents(1200))
2508 .icms_p_fcp_st(Rate(200))
2509 .icms_v_fcp_st(Cents(24));
2510 let data = normal_build_data();
2511 let result = build_det(&item, &data).expect("build_det should succeed");
2512
2513 assert!(result.xml.contains("<ICMS10>"));
2514 assert!(result.xml.contains("<CST>10</CST>"));
2515 assert!(result.xml.contains("<modBCST>4</modBCST>"));
2516 assert!(result.xml.contains("<vBCST>12.00</vBCST>"));
2517 assert!(result.xml.contains("<pICMSST>"));
2518 assert!(result.xml.contains("<vICMSST>"));
2519 }
2520
2521 #[test]
2522 fn cst_20_produces_correct_xml() {
2523 let item = InvoiceItemData::new(
2524 1,
2525 "001",
2526 "Produto",
2527 "27101259",
2528 "5102",
2529 "UN",
2530 1.0,
2531 Cents(1000),
2532 Cents(1000),
2533 "20",
2534 Rate(1800),
2535 Cents(180),
2536 "99",
2537 "99",
2538 )
2539 .icms_mod_bc(3)
2540 .icms_red_bc(Rate(2000))
2541 .icms_v_bc_fcp(Cents(1000))
2542 .icms_p_fcp(Rate(200))
2543 .icms_v_fcp(Cents(20))
2544 .icms_v_icms_deson(Cents(50))
2545 .icms_mot_des_icms(9)
2546 .icms_ind_deduz_deson("1");
2547 let data = normal_build_data();
2548 let result = build_det(&item, &data).expect("build_det should succeed");
2549
2550 assert!(result.xml.contains("<ICMS20>"));
2551 assert!(result.xml.contains("<CST>20</CST>"));
2552 assert!(result.xml.contains("<pRedBC>"));
2553 assert!(result.xml.contains("<vICMSDeson>"));
2554 assert!(result.xml.contains("<motDesICMS>9</motDesICMS>"));
2555 assert!(result.xml.contains("<indDeduzDeson>1</indDeduzDeson>"));
2556 assert!(result.ind_deduz_deson);
2557 }
2558
2559 #[test]
2560 fn cst_30_produces_correct_xml() {
2561 let item = InvoiceItemData::new(
2562 1,
2563 "001",
2564 "Produto",
2565 "27101259",
2566 "5102",
2567 "UN",
2568 1.0,
2569 Cents(1000),
2570 Cents(1000),
2571 "30",
2572 Rate(0),
2573 Cents(0),
2574 "99",
2575 "99",
2576 )
2577 .icms_mod_bc_st(4)
2578 .icms_v_bc_st(Cents(1200))
2579 .icms_p_icms_st(Rate(1200))
2580 .icms_v_icms_st(Cents(144))
2581 .icms_p_mva_st(Rate(5000))
2582 .icms_red_bc_st(Rate(1000))
2583 .icms_v_bc_fcp_st(Cents(1200))
2584 .icms_p_fcp_st(Rate(200))
2585 .icms_v_fcp_st(Cents(24))
2586 .icms_v_icms_deson(Cents(50))
2587 .icms_mot_des_icms(9)
2588 .icms_ind_deduz_deson("1");
2589 let data = normal_build_data();
2590 let result = build_det(&item, &data).expect("build_det should succeed");
2591
2592 assert!(result.xml.contains("<ICMS30>"));
2593 assert!(result.xml.contains("<CST>30</CST>"));
2594 assert!(result.xml.contains("<modBCST>4</modBCST>"));
2595 assert!(result.xml.contains("<vICMSDeson>"));
2596 assert!(result.xml.contains("<motDesICMS>9</motDesICMS>"));
2597 }
2598
2599 #[test]
2600 fn cst_40_produces_correct_xml() {
2601 let item = InvoiceItemData::new(
2602 1,
2603 "001",
2604 "Produto",
2605 "27101259",
2606 "5102",
2607 "UN",
2608 1.0,
2609 Cents(1000),
2610 Cents(1000),
2611 "40",
2612 Rate(0),
2613 Cents(0),
2614 "99",
2615 "99",
2616 )
2617 .icms_v_icms_deson(Cents(100))
2618 .icms_mot_des_icms(1)
2619 .icms_ind_deduz_deson("1");
2620 let data = normal_build_data();
2621 let result = build_det(&item, &data).expect("build_det should succeed");
2622
2623 assert!(result.xml.contains("<ICMS40>"));
2624 assert!(result.xml.contains("<CST>40</CST>"));
2625 assert!(result.xml.contains("<vICMSDeson>"));
2626 assert!(result.xml.contains("<motDesICMS>1</motDesICMS>"));
2627 }
2628
2629 #[test]
2630 fn cst_41_produces_correct_xml() {
2631 let item = InvoiceItemData::new(
2632 1,
2633 "001",
2634 "Produto",
2635 "27101259",
2636 "5102",
2637 "UN",
2638 1.0,
2639 Cents(1000),
2640 Cents(1000),
2641 "41",
2642 Rate(0),
2643 Cents(0),
2644 "99",
2645 "99",
2646 )
2647 .icms_v_icms_deson(Cents(100))
2648 .icms_mot_des_icms(1)
2649 .icms_ind_deduz_deson("1");
2650 let data = normal_build_data();
2651 let result = build_det(&item, &data).expect("build_det should succeed");
2652
2653 assert!(result.xml.contains("<ICMS40>"));
2655 assert!(result.xml.contains("<CST>41</CST>"));
2656 assert!(result.xml.contains("<vICMSDeson>"));
2657 }
2658
2659 #[test]
2660 fn cst_50_produces_correct_xml() {
2661 let item = InvoiceItemData::new(
2662 1,
2663 "001",
2664 "Produto",
2665 "27101259",
2666 "5102",
2667 "UN",
2668 1.0,
2669 Cents(1000),
2670 Cents(1000),
2671 "50",
2672 Rate(0),
2673 Cents(0),
2674 "99",
2675 "99",
2676 )
2677 .icms_v_icms_deson(Cents(100))
2678 .icms_mot_des_icms(1)
2679 .icms_ind_deduz_deson("1");
2680 let data = normal_build_data();
2681 let result = build_det(&item, &data).expect("build_det should succeed");
2682
2683 assert!(result.xml.contains("<ICMS40>"));
2685 assert!(result.xml.contains("<CST>50</CST>"));
2686 assert!(result.xml.contains("<vICMSDeson>"));
2687 }
2688
2689 #[test]
2690 fn cst_51_produces_correct_xml() {
2691 let item = InvoiceItemData::new(
2692 1,
2693 "001",
2694 "Produto",
2695 "27101259",
2696 "5102",
2697 "UN",
2698 1.0,
2699 Cents(1000),
2700 Cents(1000),
2701 "51",
2702 Rate(1800),
2703 Cents(180),
2704 "99",
2705 "99",
2706 )
2707 .icms_mod_bc(3)
2708 .icms_red_bc(Rate(1000))
2709 .icms_v_bc_fcp(Cents(1000))
2710 .icms_p_fcp(Rate(200))
2711 .icms_v_fcp(Cents(20));
2712 let data = normal_build_data();
2713 let result = build_det(&item, &data).expect("build_det should succeed");
2714
2715 assert!(result.xml.contains("<ICMS51>"));
2716 assert!(result.xml.contains("<CST>51</CST>"));
2717 assert!(result.xml.contains("<modBC>3</modBC>"));
2718 assert!(result.xml.contains("<pRedBC>"));
2719 assert!(result.xml.contains("<vICMS>"));
2720 }
2721
2722 #[test]
2723 fn cst_60_produces_correct_xml() {
2724 let item = InvoiceItemData::new(
2725 1,
2726 "001",
2727 "Produto",
2728 "27101259",
2729 "5102",
2730 "UN",
2731 1.0,
2732 Cents(1000),
2733 Cents(1000),
2734 "60",
2735 Rate(0),
2736 Cents(0),
2737 "99",
2738 "99",
2739 )
2740 .icms_v_icms_substituto(Cents(200));
2741 let data = normal_build_data();
2742 let result = build_det(&item, &data).expect("build_det should succeed");
2743
2744 assert!(result.xml.contains("<ICMS60>"));
2745 assert!(result.xml.contains("<CST>60</CST>"));
2746 assert!(result.xml.contains("<vICMSSubstituto>"));
2747 }
2748
2749 #[test]
2750 fn cst_70_produces_correct_xml() {
2751 let item = InvoiceItemData::new(
2752 1,
2753 "001",
2754 "Produto",
2755 "27101259",
2756 "5102",
2757 "UN",
2758 1.0,
2759 Cents(1000),
2760 Cents(1000),
2761 "70",
2762 Rate(1800),
2763 Cents(180),
2764 "99",
2765 "99",
2766 )
2767 .icms_mod_bc(3)
2768 .icms_red_bc(Rate(2000))
2769 .icms_mod_bc_st(4)
2770 .icms_v_bc_st(Cents(1200))
2771 .icms_p_icms_st(Rate(1200))
2772 .icms_v_icms_st(Cents(144))
2773 .icms_v_bc_fcp(Cents(1000))
2774 .icms_p_fcp(Rate(200))
2775 .icms_v_fcp(Cents(20))
2776 .icms_p_mva_st(Rate(5000))
2777 .icms_red_bc_st(Rate(1000))
2778 .icms_v_bc_fcp_st(Cents(1200))
2779 .icms_p_fcp_st(Rate(200))
2780 .icms_v_fcp_st(Cents(24))
2781 .icms_v_icms_deson(Cents(50))
2782 .icms_mot_des_icms(9)
2783 .icms_ind_deduz_deson("1");
2784 let data = normal_build_data();
2785 let result = build_det(&item, &data).expect("build_det should succeed");
2786
2787 assert!(result.xml.contains("<ICMS70>"));
2788 assert!(result.xml.contains("<CST>70</CST>"));
2789 assert!(result.xml.contains("<pRedBC>"));
2790 assert!(result.xml.contains("<modBCST>4</modBCST>"));
2791 assert!(result.xml.contains("<vICMSDeson>"));
2792 }
2793
2794 #[test]
2795 fn cst_90_produces_correct_xml() {
2796 let item = InvoiceItemData::new(
2797 1,
2798 "001",
2799 "Produto",
2800 "27101259",
2801 "5102",
2802 "UN",
2803 1.0,
2804 Cents(1000),
2805 Cents(1000),
2806 "90",
2807 Rate(1800),
2808 Cents(180),
2809 "99",
2810 "99",
2811 )
2812 .icms_mod_bc(3)
2813 .icms_red_bc(Rate(1000))
2814 .icms_v_bc_fcp(Cents(1000))
2815 .icms_p_fcp(Rate(200))
2816 .icms_v_fcp(Cents(20))
2817 .icms_mod_bc_st(4)
2818 .icms_p_mva_st(Rate(5000))
2819 .icms_red_bc_st(Rate(1000))
2820 .icms_v_bc_st(Cents(1200))
2821 .icms_p_icms_st(Rate(1200))
2822 .icms_v_icms_st(Cents(144))
2823 .icms_v_bc_fcp_st(Cents(1200))
2824 .icms_p_fcp_st(Rate(200))
2825 .icms_v_fcp_st(Cents(24))
2826 .icms_v_icms_deson(Cents(50))
2827 .icms_mot_des_icms(9)
2828 .icms_ind_deduz_deson("1");
2829 let data = normal_build_data();
2830 let result = build_det(&item, &data).expect("build_det should succeed");
2831
2832 assert!(result.xml.contains("<ICMS90>"));
2833 assert!(result.xml.contains("<CST>90</CST>"));
2834 assert!(result.xml.contains("<modBC>3</modBC>"));
2835 assert!(result.xml.contains("<modBCST>4</modBCST>"));
2836 assert!(result.xml.contains("<vICMSDeson>"));
2837 }
2838
2839 #[test]
2840 fn cst_unsupported_returns_error() {
2841 let item = InvoiceItemData::new(
2842 1,
2843 "001",
2844 "Produto",
2845 "27101259",
2846 "5102",
2847 "UN",
2848 1.0,
2849 Cents(1000),
2850 Cents(1000),
2851 "99",
2852 Rate(0),
2853 Cents(0),
2854 "99",
2855 "99",
2856 );
2857 let data = normal_build_data();
2858 let result = build_det(&item, &data);
2859 assert!(result.is_err());
2860 let err = result.unwrap_err();
2861 assert!(
2862 matches!(err, FiscalError::UnsupportedIcmsCst(ref c) if c == "99"),
2863 "expected UnsupportedIcmsCst, got {:?}",
2864 err
2865 );
2866 }
2867
2868 #[test]
2871 fn ipi_produces_correct_xml() {
2872 let item = sample_item()
2873 .ipi_cst("50")
2874 .ipi_c_enq("999")
2875 .ipi_v_bc(Cents(10000))
2876 .ipi_p_ipi(Rate(500))
2877 .ipi_v_ipi(Cents(500));
2878 let data = sample_build_data();
2879 let result = build_det(&item, &data).expect("build_det should succeed");
2880
2881 assert!(result.xml.contains("<IPI>"));
2882 assert!(result.xml.contains("<CST>50</CST>"));
2883 assert!(result.xml.contains("<cEnq>999</cEnq>"));
2884 assert!(result.xml.contains("<vIPI>5.00</vIPI>"));
2885 assert_eq!(result.v_ipi, 500);
2886 }
2887
2888 #[test]
2889 fn ipi_default_c_enq_when_missing() {
2890 let item = sample_item()
2891 .ipi_cst("50")
2892 .ipi_v_bc(Cents(10000))
2893 .ipi_p_ipi(Rate(500))
2894 .ipi_v_ipi(Cents(500));
2895 let data = sample_build_data();
2896 let result = build_det(&item, &data).expect("build_det should succeed");
2897
2898 assert!(result.xml.contains("<cEnq>999</cEnq>"));
2899 }
2900
2901 #[test]
2902 fn ipi_with_quantity_based() {
2903 let item = sample_item()
2904 .ipi_cst("50")
2905 .ipi_q_unid(100)
2906 .ipi_v_unid(50)
2907 .ipi_v_ipi(Cents(5000));
2908 let data = sample_build_data();
2909 let result = build_det(&item, &data).expect("build_det should succeed");
2910
2911 assert!(result.xml.contains("<IPI>"));
2912 assert_eq!(result.v_ipi, 5000);
2913 }
2914
2915 #[test]
2916 fn no_ipi_when_cst_absent() {
2917 let item = sample_item();
2918 let data = sample_build_data();
2919 let result = build_det(&item, &data).expect("build_det should succeed");
2920
2921 assert!(!result.xml.contains("<IPI>"));
2922 assert_eq!(result.v_ipi, 0);
2923 }
2924
2925 #[test]
2928 fn ii_produces_correct_xml() {
2929 let item = sample_item()
2930 .ii_v_bc(Cents(50000))
2931 .ii_v_desp_adu(Cents(5000))
2932 .ii_v_ii(Cents(10000))
2933 .ii_v_iof(Cents(2000));
2934 let data = sample_build_data();
2935 let result = build_det(&item, &data).expect("build_det should succeed");
2936
2937 assert!(result.xml.contains("<II>"));
2938 assert!(result.xml.contains("<vBC>500.00</vBC>"));
2939 assert!(result.xml.contains("<vDespAdu>50.00</vDespAdu>"));
2940 assert!(result.xml.contains("<vII>100.00</vII>"));
2941 assert!(result.xml.contains("<vIOF>20.00</vIOF>"));
2942 assert_eq!(result.v_ii, 10000);
2943 }
2944
2945 #[test]
2946 fn no_ii_when_absent() {
2947 let item = sample_item();
2948 let data = sample_build_data();
2949 let result = build_det(&item, &data).expect("build_det should succeed");
2950
2951 assert!(!result.xml.contains("<II>"));
2952 assert_eq!(result.v_ii, 0);
2953 }
2954
2955 #[test]
2958 fn pis_st_replaces_pis_and_accumulates_when_ind_soma_1() {
2959 use crate::tax_pis_cofins_ipi::PisStData;
2960 let pis_st = PisStData::new(Cents(500))
2961 .v_bc(Cents(10000))
2962 .p_pis(Rate4(16500))
2963 .ind_soma_pis_st(1);
2964 let item = sample_item().pis_st(pis_st);
2965 let data = sample_build_data();
2966 let result = build_det(&item, &data).expect("build_det should succeed");
2967
2968 assert!(result.xml.contains("<PISST>"));
2969 assert!(!result.xml.contains("<PISAliq>"));
2970 assert_eq!(result.v_pis_st, 500);
2971 }
2972
2973 #[test]
2974 fn pis_st_does_not_accumulate_when_ind_soma_0() {
2975 use crate::tax_pis_cofins_ipi::PisStData;
2976 let pis_st = PisStData::new(Cents(500))
2977 .v_bc(Cents(10000))
2978 .p_pis(Rate4(16500))
2979 .ind_soma_pis_st(0);
2980 let item = sample_item().pis_st(pis_st);
2981 let data = sample_build_data();
2982 let result = build_det(&item, &data).expect("build_det should succeed");
2983
2984 assert!(result.xml.contains("<PISST>"));
2985 assert_eq!(result.v_pis_st, 0);
2986 }
2987
2988 #[test]
2989 fn cofins_st_replaces_cofins_and_accumulates_when_ind_soma_1() {
2990 use crate::tax_pis_cofins_ipi::CofinsStData;
2991 let cofins_st = CofinsStData::new(Cents(750))
2992 .v_bc(Cents(10000))
2993 .p_cofins(Rate4(76000))
2994 .ind_soma_cofins_st(1);
2995 let item = sample_item().cofins_st(cofins_st);
2996 let data = sample_build_data();
2997 let result = build_det(&item, &data).expect("build_det should succeed");
2998
2999 assert!(result.xml.contains("<COFINSST>"));
3000 assert!(!result.xml.contains("<COFINSAliq>"));
3001 assert_eq!(result.v_cofins_st, 750);
3002 }
3003
3004 #[test]
3005 fn cofins_st_does_not_accumulate_when_ind_soma_0() {
3006 use crate::tax_pis_cofins_ipi::CofinsStData;
3007 let cofins_st = CofinsStData::new(Cents(750))
3008 .v_bc(Cents(10000))
3009 .p_cofins(Rate4(76000))
3010 .ind_soma_cofins_st(0);
3011 let item = sample_item().cofins_st(cofins_st);
3012 let data = sample_build_data();
3013 let result = build_det(&item, &data).expect("build_det should succeed");
3014
3015 assert!(result.xml.contains("<COFINSST>"));
3016 assert_eq!(result.v_cofins_st, 0);
3017 }
3018
3019 #[test]
3022 fn is_data_emitted_with_pl010_schema() {
3023 use crate::tax_is::IsData;
3024 let is = IsData::new("00", "001", "5.00");
3025 let item = sample_item().is_data(is);
3026 let data = pl010_build_data();
3027 let result = build_det(&item, &data).expect("build_det should succeed");
3028
3029 assert!(result.xml.contains("<IS>"));
3030 }
3031
3032 #[test]
3033 fn is_data_not_emitted_with_pl009_schema() {
3034 use crate::tax_is::IsData;
3035 let is = IsData::new("00", "001", "5.00");
3036 let item = sample_item().is_data(is);
3037 let data = sample_build_data(); let result = build_det(&item, &data).expect("build_det should succeed");
3039
3040 assert!(!result.xml.contains("<IS>"));
3041 }
3042
3043 #[test]
3044 fn ibs_cbs_data_emitted_with_pl010_schema() {
3045 use crate::tax_ibs_cbs::IbsCbsData;
3046 let ibs_cbs = IbsCbsData::new("00", "001");
3047 let item = sample_item().ibs_cbs(ibs_cbs);
3048 let data = pl010_build_data();
3049 let result = build_det(&item, &data).expect("build_det should succeed");
3050
3051 assert!(result.xml.contains("<IBSCBS>"));
3052 }
3053
3054 #[test]
3057 fn cest_with_ind_escala_and_cnpj_fab() {
3058 let item = sample_item()
3059 .cest("1234567")
3060 .cest_ind_escala("S")
3061 .cest_cnpj_fab("12345678000199");
3062 let data = sample_build_data();
3063 let result = build_det(&item, &data).expect("build_det should succeed");
3064
3065 assert!(result.xml.contains("<CEST>1234567</CEST>"));
3066 assert!(result.xml.contains("<indEscala>S</indEscala>"));
3067 assert!(result.xml.contains("<CNPJFab>12345678000199</CNPJFab>"));
3068 }
3069
3070 #[test]
3073 fn extipi_produces_correct_xml() {
3074 let item = sample_item().extipi("01");
3075 let data = sample_build_data();
3076 let result = build_det(&item, &data).expect("build_det should succeed");
3077
3078 assert!(result.xml.contains("<EXTIPI>01</EXTIPI>"));
3079 let ncm_pos = result.xml.find("<NCM>").unwrap();
3081 let extipi_pos = result.xml.find("<EXTIPI>").unwrap();
3082 let cfop_pos = result.xml.find("<CFOP>").unwrap();
3083 assert!(extipi_pos > ncm_pos);
3084 assert!(extipi_pos < cfop_pos);
3085 }
3086
3087 #[test]
3090 fn n_item_ped_produces_correct_xml() {
3091 let item = sample_item().n_item_ped("5");
3092 let data = sample_build_data();
3093 let result = build_det(&item, &data).expect("build_det should succeed");
3094
3095 assert!(result.xml.contains("<nItemPed>5</nItemPed>"));
3096 }
3097
3098 #[test]
3099 fn n_fci_produces_correct_xml() {
3100 let item = sample_item().n_fci("B01F70AF-10BF-4B1F-848C-65FF57F616FE");
3101 let data = sample_build_data();
3102 let result = build_det(&item, &data).expect("build_det should succeed");
3103
3104 assert!(
3105 result
3106 .xml
3107 .contains("<nFCI>B01F70AF-10BF-4B1F-848C-65FF57F616FE</nFCI>")
3108 );
3109 }
3110
3111 #[test]
3114 fn veic_prod_produces_correct_xml() {
3115 let veic = VeicProdData::new(
3116 "1",
3117 "9BWZZZ377VT004251",
3118 "1",
3119 "PRATA",
3120 "100",
3121 "1600",
3122 "1050",
3123 "1250",
3124 "ABC123",
3125 "1",
3126 "MOT123",
3127 "1500",
3128 "2600",
3129 "2025",
3130 "2025",
3131 "M",
3132 "06",
3133 "1",
3134 "R",
3135 "1",
3136 "MOD001",
3137 "02",
3138 "5",
3139 "0",
3140 );
3141 let item = sample_item().veic_prod(veic);
3142 let data = sample_build_data();
3143 let result = build_det(&item, &data).expect("build_det should succeed");
3144
3145 assert!(result.xml.contains("<veicProd>"));
3146 assert!(result.xml.contains("<tpOp>1</tpOp>"));
3147 assert!(result.xml.contains("<chassi>9BWZZZ377VT004251</chassi>"));
3148 assert!(result.xml.contains("<cCor>1</cCor>"));
3149 assert!(result.xml.contains("<xCor>PRATA</xCor>"));
3150 assert!(result.xml.contains("<pot>100</pot>"));
3151 assert!(result.xml.contains("<cilin>1600</cilin>"));
3152 assert!(result.xml.contains("<pesoL>1050</pesoL>"));
3153 assert!(result.xml.contains("<pesoB>1250</pesoB>"));
3154 assert!(result.xml.contains("<nSerie>ABC123</nSerie>"));
3155 assert!(result.xml.contains("<tpComb>1</tpComb>"));
3156 assert!(result.xml.contains("<nMotor>MOT123</nMotor>"));
3157 assert!(result.xml.contains("<CMT>1500</CMT>"));
3158 assert!(result.xml.contains("<dist>2600</dist>"));
3159 assert!(result.xml.contains("<anoMod>2025</anoMod>"));
3160 assert!(result.xml.contains("<anoFab>2025</anoFab>"));
3161 assert!(result.xml.contains("<tpPint>M</tpPint>"));
3162 assert!(result.xml.contains("<tpVeic>06</tpVeic>"));
3163 assert!(result.xml.contains("<espVeic>1</espVeic>"));
3164 assert!(result.xml.contains("<VIN>R</VIN>"));
3165 assert!(result.xml.contains("<condVeic>1</condVeic>"));
3166 assert!(result.xml.contains("<cMod>MOD001</cMod>"));
3167 assert!(result.xml.contains("<cCorDENATRAN>02</cCorDENATRAN>"));
3168 assert!(result.xml.contains("<lota>5</lota>"));
3169 assert!(result.xml.contains("<tpRest>0</tpRest>"));
3170 assert!(result.xml.contains("</veicProd>"));
3171 }
3172
3173 #[test]
3176 fn med_with_anvisa_code() {
3177 let med = MedData::new(Cents(5000)).c_prod_anvisa("1234567890123");
3178 let item = sample_item().med(med);
3179 let data = sample_build_data();
3180 let result = build_det(&item, &data).expect("build_det should succeed");
3181
3182 assert!(result.xml.contains("<med>"));
3183 assert!(
3184 result
3185 .xml
3186 .contains("<cProdANVISA>1234567890123</cProdANVISA>")
3187 );
3188 assert!(result.xml.contains("<vPMC>50.00</vPMC>"));
3189 assert!(result.xml.contains("</med>"));
3190 }
3191
3192 #[test]
3193 fn med_with_exemption_reason() {
3194 let med = MedData::new(Cents(3000)).x_motivo_isencao("Medicamento isento de registro");
3195 let item = sample_item().med(med);
3196 let data = sample_build_data();
3197 let result = build_det(&item, &data).expect("build_det should succeed");
3198
3199 assert!(result.xml.contains("<med>"));
3200 assert!(
3201 result
3202 .xml
3203 .contains("<xMotivoIsencao>Medicamento isento de registro</xMotivoIsencao>")
3204 );
3205 assert!(result.xml.contains("<vPMC>30.00</vPMC>"));
3206 assert!(!result.xml.contains("<cProdANVISA>"));
3207 }
3208
3209 #[test]
3212 fn arma_single_produces_correct_xml() {
3213 let arma = ArmaData::new("0", "SN12345", "CN6789", "Pistola Taurus");
3214 let item = sample_item().arma(vec![arma]);
3215 let data = sample_build_data();
3216 let result = build_det(&item, &data).expect("build_det should succeed");
3217
3218 assert!(result.xml.contains("<arma>"));
3219 assert!(result.xml.contains("<tpArma>0</tpArma>"));
3220 assert!(result.xml.contains("<nSerie>SN12345</nSerie>"));
3221 assert!(result.xml.contains("<nCano>CN6789</nCano>"));
3222 assert!(result.xml.contains("<descr>Pistola Taurus</descr>"));
3223 assert!(result.xml.contains("</arma>"));
3224 }
3225
3226 #[test]
3227 fn arma_multiple_produces_multiple_elements() {
3228 let a1 = ArmaData::new("0", "SN001", "CN001", "Arma 1");
3229 let a2 = ArmaData::new("1", "SN002", "CN002", "Arma 2");
3230 let item = sample_item().arma(vec![a1, a2]);
3231 let data = sample_build_data();
3232 let result = build_det(&item, &data).expect("build_det should succeed");
3233
3234 assert_eq!(result.xml.matches("<arma>").count(), 2);
3235 assert!(result.xml.contains("<nSerie>SN001</nSerie>"));
3236 assert!(result.xml.contains("<nSerie>SN002</nSerie>"));
3237 }
3238
3239 #[test]
3242 fn n_recopi_produces_correct_xml() {
3243 let item = sample_item().n_recopi("20250000001234567890");
3244 let data = sample_build_data();
3245 let result = build_det(&item, &data).expect("build_det should succeed");
3246
3247 assert!(
3248 result
3249 .xml
3250 .contains("<nRECOPI>20250000001234567890</nRECOPI>")
3251 );
3252 }
3253
3254 #[test]
3255 fn n_recopi_empty_not_emitted() {
3256 let item = sample_item().n_recopi("");
3257 let data = sample_build_data();
3258 let result = build_det(&item, &data).expect("build_det should succeed");
3259
3260 assert!(!result.xml.contains("<nRECOPI>"));
3261 }
3262
3263 #[test]
3266 fn rastro_single_produces_correct_xml() {
3267 let r = RastroData::new("LOTE001", 10.5, "2025-01-01", "2026-01-01");
3268 let item = sample_item().rastro(vec![r]);
3269 let data = sample_build_data();
3270 let result = build_det(&item, &data).expect("build_det should succeed");
3271
3272 assert!(result.xml.contains("<rastro>"));
3273 assert!(result.xml.contains("<nLote>LOTE001</nLote>"));
3274 assert!(result.xml.contains("<qLote>10.500</qLote>"));
3275 assert!(result.xml.contains("<dFab>2025-01-01</dFab>"));
3276 assert!(result.xml.contains("<dVal>2026-01-01</dVal>"));
3277 assert!(result.xml.contains("</rastro>"));
3278 }
3279
3280 #[test]
3281 fn rastro_with_c_agreg() {
3282 let r = RastroData::new("LOTE002", 5.0, "2025-06-01", "2026-06-01").c_agreg("AGREG001");
3283 let item = sample_item().rastro(vec![r]);
3284 let data = sample_build_data();
3285 let result = build_det(&item, &data).expect("build_det should succeed");
3286
3287 assert!(result.xml.contains("<cAgreg>AGREG001</cAgreg>"));
3288 }
3289
3290 #[test]
3293 fn obs_item_with_obs_cont_only() {
3294 use crate::types::{ObsField, ObsItemData};
3295 let obs = ObsItemData::new().obs_cont(ObsField::new("campo1", "texto1"));
3296 let item = sample_item().obs_item(obs);
3297 let data = sample_build_data();
3298 let result = build_det(&item, &data).expect("build_det should succeed");
3299
3300 assert!(result.xml.contains("<obsItem>"));
3301 assert!(result.xml.contains("<obsCont xCampo=\"campo1\">"));
3302 assert!(result.xml.contains("<xTexto>texto1</xTexto>"));
3303 assert!(!result.xml.contains("<obsFisco"));
3304 }
3305
3306 #[test]
3307 fn obs_item_with_obs_fisco() {
3308 use crate::types::{ObsField, ObsItemData};
3309 let obs = ObsItemData::new().obs_fisco(ObsField::new("campo_fisco", "texto_fisco"));
3310 let item = sample_item().obs_item(obs);
3311 let data = sample_build_data();
3312 let result = build_det(&item, &data).expect("build_det should succeed");
3313
3314 assert!(result.xml.contains("<obsItem>"));
3315 assert!(result.xml.contains("<obsFisco xCampo=\"campo_fisco\">"));
3316 assert!(result.xml.contains("<xTexto>texto_fisco</xTexto>"));
3317 }
3318
3319 #[test]
3320 fn obs_item_with_both_obs_cont_and_obs_fisco() {
3321 use crate::types::{ObsField, ObsItemData};
3322 let obs = ObsItemData::new()
3323 .obs_cont(ObsField::new("campo_cont", "texto_cont"))
3324 .obs_fisco(ObsField::new("campo_fisco", "texto_fisco"));
3325 let item = sample_item().obs_item(obs);
3326 let data = sample_build_data();
3327 let result = build_det(&item, &data).expect("build_det should succeed");
3328
3329 assert!(result.xml.contains("<obsCont xCampo=\"campo_cont\">"));
3330 assert!(result.xml.contains("<obsFisco xCampo=\"campo_fisco\">"));
3331 }
3332
3333 #[test]
3336 fn dfe_referenciado_without_n_item() {
3337 use crate::types::DFeReferenciadoData;
3338 let dfe = DFeReferenciadoData::new("12345678901234567890123456789012345678901234");
3339 let item = sample_item().dfe_referenciado(dfe);
3340 let data = sample_build_data();
3341 let result = build_det(&item, &data).expect("build_det should succeed");
3342
3343 assert!(result.xml.contains("<DFeReferenciado>"));
3344 assert!(
3345 result.xml.contains(
3346 "<chaveAcesso>12345678901234567890123456789012345678901234</chaveAcesso>"
3347 )
3348 );
3349 assert!(!result.xml.contains("<nItem>"));
3350 }
3351
3352 #[test]
3353 fn dfe_referenciado_with_n_item() {
3354 use crate::types::DFeReferenciadoData;
3355 let dfe =
3356 DFeReferenciadoData::new("12345678901234567890123456789012345678901234").n_item("3");
3357 let item = sample_item().dfe_referenciado(dfe);
3358 let data = sample_build_data();
3359 let result = build_det(&item, &data).expect("build_det should succeed");
3360
3361 assert!(result.xml.contains("<DFeReferenciado>"));
3362 assert!(result.xml.contains("<nItem>3</nItem>"));
3363 }
3364
3365 #[test]
3368 fn nfce_homologation_substitutes_xprod_for_item_1() {
3369 let item = sample_item();
3370 let mut data = sample_build_data();
3371 data.model = InvoiceModel::Nfce;
3372 data.environment = SefazEnvironment::Homologation;
3373 let result = build_det(&item, &data).expect("build_det should succeed");
3374
3375 assert!(result.xml.contains(HOMOLOGATION_XPROD));
3376 }
3377
3378 #[test]
3379 fn nfce_homologation_does_not_substitute_for_item_2() {
3380 let mut item = sample_item();
3381 item.item_number = 2;
3382 let mut data = sample_build_data();
3383 data.model = InvoiceModel::Nfce;
3384 data.environment = SefazEnvironment::Homologation;
3385 let result = build_det(&item, &data).expect("build_det should succeed");
3386
3387 assert!(!result.xml.contains(HOMOLOGATION_XPROD));
3388 assert!(result.xml.contains("<xProd>Gasolina Comum</xProd>"));
3389 }
3390
3391 #[test]
3394 fn optional_value_fields_in_det_result() {
3395 let item = sample_item()
3396 .v_frete(Cents(1000))
3397 .v_seg(Cents(500))
3398 .v_desc(Cents(200))
3399 .v_outro(Cents(300));
3400 let data = sample_build_data();
3401 let result = build_det(&item, &data).expect("build_det should succeed");
3402
3403 assert!(result.xml.contains("<vFrete>10.00</vFrete>"));
3404 assert!(result.xml.contains("<vSeg>5.00</vSeg>"));
3405 assert!(result.xml.contains("<vDesc>2.00</vDesc>"));
3406 assert!(result.xml.contains("<vOutro>3.00</vOutro>"));
3407 assert_eq!(result.v_frete, 1000);
3408 assert_eq!(result.v_seg, 500);
3409 assert_eq!(result.v_desc, 200);
3410 assert_eq!(result.v_outro, 300);
3411 }
3412
3413 #[test]
3416 fn ind_tot_zero_excludes_from_total() {
3417 let item = sample_item().ind_tot(0);
3418 let data = sample_build_data();
3419 let result = build_det(&item, &data).expect("build_det should succeed");
3420
3421 assert!(result.xml.contains("<indTot>0</indTot>"));
3422 assert_eq!(result.ind_tot, 0);
3423 }
3424
3425 #[test]
3428 fn v_tot_trib_propagated_to_result() {
3429 let item = sample_item().v_tot_trib(Cents(1234));
3430 let data = sample_build_data();
3431 let result = build_det(&item, &data).expect("build_det should succeed");
3432
3433 assert_eq!(result.v_tot_trib, 1234);
3434 }
3435
3436 #[test]
3439 fn x_ped_produces_correct_xml() {
3440 let item = sample_item().x_ped("PEDIDO-001");
3441 let data = sample_build_data();
3442 let result = build_det(&item, &data).expect("build_det should succeed");
3443
3444 assert!(result.xml.contains("<xPed>PEDIDO-001</xPed>"));
3445 }
3446
3447 #[test]
3450 fn inf_ad_prod_produces_correct_xml() {
3451 let item = sample_item().inf_ad_prod("informacao adicional do produto");
3452 let data = sample_build_data();
3453 let result = build_det(&item, &data).expect("build_det should succeed");
3454
3455 assert!(
3456 result
3457 .xml
3458 .contains("<infAdProd>informacao adicional do produto</infAdProd>")
3459 );
3460 }
3461
3462 #[test]
3465 fn ind_deduz_deson_true_when_set_to_1() {
3466 let item = InvoiceItemData::new(
3467 1,
3468 "001",
3469 "Produto",
3470 "27101259",
3471 "5102",
3472 "UN",
3473 1.0,
3474 Cents(1000),
3475 Cents(1000),
3476 "40",
3477 Rate(0),
3478 Cents(0),
3479 "99",
3480 "99",
3481 )
3482 .icms_v_icms_deson(Cents(100))
3483 .icms_mot_des_icms(1)
3484 .icms_ind_deduz_deson("1");
3485 let data = normal_build_data();
3486 let result = build_det(&item, &data).expect("build_det should succeed");
3487 assert!(result.ind_deduz_deson);
3488 }
3489
3490 #[test]
3491 fn ind_deduz_deson_false_when_not_set() {
3492 let item = sample_item();
3493 let data = sample_build_data();
3494 let result = build_det(&item, &data).expect("build_det should succeed");
3495 assert!(!result.ind_deduz_deson);
3496 }
3497
3498 #[test]
3501 fn custom_orig_used_in_icms() {
3502 let item = InvoiceItemData::new(
3503 1,
3504 "001",
3505 "Produto",
3506 "27101259",
3507 "5102",
3508 "UN",
3509 1.0,
3510 Cents(1000),
3511 Cents(1000),
3512 "00",
3513 Rate(1800),
3514 Cents(180),
3515 "99",
3516 "99",
3517 )
3518 .orig("1");
3519 let data = normal_build_data();
3520 let result = build_det(&item, &data).expect("build_det should succeed");
3521
3522 assert!(result.xml.contains("<orig>1</orig>"));
3523 }
3524}