1use crate::format_utils::format_cents;
4use crate::newtypes::Cents;
5use crate::tax_ibs_cbs::{self, IbsCbsTotData, IsTotData};
6use crate::tax_icms::IcmsTotals;
7use crate::types::{CalculationMethod, IssqnTotData, RetTribData, SchemaVersion};
8use crate::xml_utils::{TagContent, tag};
9
10#[derive(Debug, Clone)]
12pub struct OtherTotals {
13 pub v_ipi: i64,
15 pub v_pis: i64,
17 pub v_cofins: i64,
19 pub v_ii: i64,
21 pub v_frete: i64,
23 pub v_seg: i64,
25 pub v_desc: i64,
27 pub v_outro: i64,
29 pub v_tot_trib: i64,
31 pub v_ipi_devol: i64,
33 pub v_pis_st: i64,
35 pub v_cofins_st: i64,
37}
38
39fn calculate_v_nf_v1(
50 total_products: i64,
51 icms: &IcmsTotals,
52 other: &OtherTotals,
53 v_serv: i64,
54) -> i64 {
55 let deson_deduction = if icms.ind_deduz_deson {
56 icms.v_icms_deson.0
57 } else {
58 0
59 };
60
61 total_products - other.v_desc - deson_deduction
62 + icms.v_st.0
63 + icms.v_fcp_st.0
64 + icms.v_icms_mono_reten.0
65 + other.v_frete
66 + other.v_seg
67 + other.v_outro
68 + other.v_ii
69 + other.v_ipi
70 + other.v_ipi_devol
71 + other.v_pis_st
72 + other.v_cofins_st
73 + v_serv
74}
75
76fn calculate_v_nf_v2(
82 total_products: i64,
83 icms: &IcmsTotals,
84 other: &OtherTotals,
85 v_serv: i64,
86) -> i64 {
87 calculate_v_nf_v1(total_products, icms, other, v_serv)
90}
91
92fn parse_decimal_to_cents(s: &str) -> i64 {
98 if s.is_empty() {
99 return 0;
100 }
101 s.parse::<f64>()
102 .map(|v| (v * 100.0).round() as i64)
103 .unwrap_or(0)
104}
105
106#[allow(clippy::too_many_arguments)]
116pub fn build_total(
117 total_products: i64,
118 icms: &IcmsTotals,
119 other: &OtherTotals,
120 ret_trib: Option<&RetTribData>,
121 issqn_tot: Option<&IssqnTotData>,
122 is_tot: Option<&IsTotData>,
123 ibs_cbs_tot: Option<&IbsCbsTotData>,
124 schema_version: SchemaVersion,
125 calculation_method: CalculationMethod,
126 v_nf_tot_override: Option<Cents>,
127) -> String {
128 let fc2 = |c: i64| format_cents(c, 2);
129
130 let v_serv = issqn_tot.and_then(|iq| iq.v_serv).map(|c| c.0).unwrap_or(0);
132
133 let v_nf = match calculation_method {
135 CalculationMethod::V1 => calculate_v_nf_v1(total_products, icms, other, v_serv),
136 CalculationMethod::V2 => calculate_v_nf_v2(total_products, icms, other, v_serv),
137 };
138
139 let mut icms_children = vec![
141 tag("vBC", &[], TagContent::Text(&fc2(icms.v_bc.0))),
142 tag("vICMS", &[], TagContent::Text(&fc2(icms.v_icms.0))),
143 tag(
144 "vICMSDeson",
145 &[],
146 TagContent::Text(&fc2(icms.v_icms_deson.0)),
147 ),
148 ];
149 if icms.v_fcp_uf_dest.0 > 0 {
151 icms_children.push(tag(
152 "vFCPUFDest",
153 &[],
154 TagContent::Text(&fc2(icms.v_fcp_uf_dest.0)),
155 ));
156 }
157 if icms.v_icms_uf_dest.0 > 0 {
158 icms_children.push(tag(
159 "vICMSUFDest",
160 &[],
161 TagContent::Text(&fc2(icms.v_icms_uf_dest.0)),
162 ));
163 }
164 if icms.v_icms_uf_remet.0 > 0 {
165 icms_children.push(tag(
166 "vICMSUFRemet",
167 &[],
168 TagContent::Text(&fc2(icms.v_icms_uf_remet.0)),
169 ));
170 }
171 icms_children.push(tag("vFCP", &[], TagContent::Text(&fc2(icms.v_fcp.0))));
172 icms_children.extend([
173 tag("vBCST", &[], TagContent::Text(&fc2(icms.v_bc_st.0))),
174 tag("vST", &[], TagContent::Text(&fc2(icms.v_st.0))),
175 tag("vFCPST", &[], TagContent::Text(&fc2(icms.v_fcp_st.0))),
176 tag(
177 "vFCPSTRet",
178 &[],
179 TagContent::Text(&fc2(icms.v_fcp_st_ret.0)),
180 ),
181 ]);
182
183 if icms.q_bc_mono > 0 {
185 icms_children.push(tag("qBCMono", &[], TagContent::Text(&fc2(icms.q_bc_mono))));
186 }
187 if icms.v_icms_mono.0 > 0 {
188 icms_children.push(tag(
189 "vICMSMono",
190 &[],
191 TagContent::Text(&fc2(icms.v_icms_mono.0)),
192 ));
193 }
194 if icms.q_bc_mono_reten > 0 {
195 icms_children.push(tag(
196 "qBCMonoReten",
197 &[],
198 TagContent::Text(&fc2(icms.q_bc_mono_reten)),
199 ));
200 }
201 if icms.v_icms_mono_reten.0 > 0 {
202 icms_children.push(tag(
203 "vICMSMonoReten",
204 &[],
205 TagContent::Text(&fc2(icms.v_icms_mono_reten.0)),
206 ));
207 }
208 if icms.q_bc_mono_ret > 0 {
209 icms_children.push(tag(
210 "qBCMonoRet",
211 &[],
212 TagContent::Text(&fc2(icms.q_bc_mono_ret)),
213 ));
214 }
215 if icms.v_icms_mono_ret.0 > 0 {
216 icms_children.push(tag(
217 "vICMSMonoRet",
218 &[],
219 TagContent::Text(&fc2(icms.v_icms_mono_ret.0)),
220 ));
221 }
222
223 icms_children.extend([
224 tag("vProd", &[], TagContent::Text(&fc2(total_products))),
225 tag("vFrete", &[], TagContent::Text(&fc2(other.v_frete))),
226 tag("vSeg", &[], TagContent::Text(&fc2(other.v_seg))),
227 tag("vDesc", &[], TagContent::Text(&fc2(other.v_desc))),
228 tag("vII", &[], TagContent::Text(&fc2(other.v_ii))),
229 tag("vIPI", &[], TagContent::Text(&fc2(other.v_ipi))),
230 tag("vIPIDevol", &[], TagContent::Text(&fc2(other.v_ipi_devol))),
231 tag("vPIS", &[], TagContent::Text(&fc2(other.v_pis))),
232 tag("vCOFINS", &[], TagContent::Text(&fc2(other.v_cofins))),
233 tag("vOutro", &[], TagContent::Text(&fc2(other.v_outro))),
234 tag("vNF", &[], TagContent::Text(&fc2(v_nf))),
235 ]);
236
237 if other.v_tot_trib > 0 {
239 icms_children.push(tag(
240 "vTotTrib",
241 &[],
242 TagContent::Text(&fc2(other.v_tot_trib)),
243 ));
244 }
245
246 let icms_tot = tag("ICMSTot", &[], TagContent::Children(icms_children));
247
248 let mut total_children = vec![icms_tot];
249
250 if let Some(iqt) = issqn_tot {
252 total_children.push(build_issqn_tot(iqt));
253 }
254
255 if schema_version.is_pl010() {
258 if let Some(ist) = is_tot {
260 total_children.push(tax_ibs_cbs::build_is_tot_xml(ist));
261 }
262
263 if let Some(ibst) = ibs_cbs_tot {
265 total_children.push(tax_ibs_cbs::build_ibs_cbs_tot_xml(ibst));
266
267 let v_nf_tot_value = if let Some(ov) = v_nf_tot_override {
271 ov.0
273 } else {
274 let v_ibs = ibst
276 .g_ibs_v_ibs
277 .as_deref()
278 .map(parse_decimal_to_cents)
279 .unwrap_or(0);
280 let v_cbs = ibst
281 .g_cbs_v_cbs
282 .as_deref()
283 .map(parse_decimal_to_cents)
284 .unwrap_or(0);
285 let v_is = is_tot
286 .map(|ist| parse_decimal_to_cents(&ist.v_is))
287 .unwrap_or(0);
288 v_nf + v_ibs + v_cbs + v_is
289 };
290
291 if v_nf_tot_value > 0 {
293 total_children.push(tag("vNFTot", &[], TagContent::Text(&fc2(v_nf_tot_value))));
294 }
295 }
296 }
297
298 if let Some(rt) = ret_trib {
299 let opt_tag = |name: &str, val: Option<crate::newtypes::Cents>| -> Option<String> {
300 val.map(|v| tag(name, &[], TagContent::Text(&fc2(v.0))))
301 };
302 let ret_children: Vec<String> = [
303 opt_tag("vRetPIS", rt.v_ret_pis),
304 opt_tag("vRetCOFINS", rt.v_ret_cofins),
305 opt_tag("vRetCSLL", rt.v_ret_csll),
306 opt_tag("vBCIRRF", rt.v_bc_irrf),
307 opt_tag("vIRRF", rt.v_irrf),
308 opt_tag("vBCRetPrev", rt.v_bc_ret_prev),
309 opt_tag("vRetPrev", rt.v_ret_prev),
310 ]
311 .into_iter()
312 .flatten()
313 .collect();
314
315 if !ret_children.is_empty() {
316 total_children.push(tag("retTrib", &[], TagContent::Children(ret_children)));
317 }
318 }
319
320 tag("total", &[], TagContent::Children(total_children))
321}
322
323fn build_issqn_tot(data: &IssqnTotData) -> String {
328 let fc2 = |c: i64| format_cents(c, 2);
329
330 let opt_cents = |name: &str, val: Option<Cents>| -> Option<String> {
332 val.and_then(|c| {
333 if c.0 > 0 {
334 Some(tag(name, &[], TagContent::Text(&fc2(c.0))))
335 } else {
336 None
337 }
338 })
339 };
340
341 let mut children: Vec<String> = Vec::new();
342
343 if let Some(t) = opt_cents("vServ", data.v_serv) {
344 children.push(t);
345 }
346 if let Some(t) = opt_cents("vBC", data.v_bc) {
347 children.push(t);
348 }
349 if let Some(t) = opt_cents("vISS", data.v_iss) {
350 children.push(t);
351 }
352 if let Some(t) = opt_cents("vPIS", data.v_pis) {
353 children.push(t);
354 }
355 if let Some(t) = opt_cents("vCOFINS", data.v_cofins) {
356 children.push(t);
357 }
358
359 children.push(tag("dCompet", &[], TagContent::Text(&data.d_compet)));
361
362 if let Some(t) = opt_cents("vDeducao", data.v_deducao) {
363 children.push(t);
364 }
365 if let Some(t) = opt_cents("vOutro", data.v_outro) {
366 children.push(t);
367 }
368 if let Some(t) = opt_cents("vDescIncond", data.v_desc_incond) {
369 children.push(t);
370 }
371 if let Some(t) = opt_cents("vDescCond", data.v_desc_cond) {
372 children.push(t);
373 }
374 if let Some(t) = opt_cents("vISSRet", data.v_iss_ret) {
375 children.push(t);
376 }
377
378 if let Some(ref reg) = data.c_reg_trib {
379 children.push(tag("cRegTrib", &[], TagContent::Text(reg)));
380 }
381
382 tag("ISSQNtot", &[], TagContent::Children(children))
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use crate::newtypes::Cents;
389 use crate::tax_icms::IcmsTotals;
390 use crate::types::IssqnTotData;
391
392 fn zero_icms() -> IcmsTotals {
393 IcmsTotals::default()
394 }
395
396 fn zero_other() -> OtherTotals {
397 OtherTotals {
398 v_ipi: 0,
399 v_pis: 0,
400 v_cofins: 0,
401 v_ii: 0,
402 v_frete: 0,
403 v_seg: 0,
404 v_desc: 0,
405 v_outro: 0,
406 v_tot_trib: 0,
407 v_ipi_devol: 0,
408 v_pis_st: 0,
409 v_cofins_st: 0,
410 }
411 }
412
413 fn default_method() -> CalculationMethod {
415 CalculationMethod::V2
416 }
417
418 #[test]
419 fn issqn_tot_minimal_only_dcompet() {
420 let data = IssqnTotData::new("2026-03-12");
421 let xml = build_issqn_tot(&data);
422
423 assert_eq!(xml, "<ISSQNtot><dCompet>2026-03-12</dCompet></ISSQNtot>");
424 }
425
426 #[test]
427 fn issqn_tot_with_all_positive_values() {
428 let data = IssqnTotData::new("2026-03-12")
429 .v_serv(Cents(100000))
430 .v_bc(Cents(100000))
431 .v_iss(Cents(5000))
432 .v_pis(Cents(1650))
433 .v_cofins(Cents(7600))
434 .v_deducao(Cents(2000))
435 .v_outro(Cents(500))
436 .v_desc_incond(Cents(300))
437 .v_desc_cond(Cents(200))
438 .v_iss_ret(Cents(1000))
439 .c_reg_trib("6");
440
441 let xml = build_issqn_tot(&data);
442
443 assert_eq!(
444 xml,
445 "<ISSQNtot>\
446 <vServ>1000.00</vServ>\
447 <vBC>1000.00</vBC>\
448 <vISS>50.00</vISS>\
449 <vPIS>16.50</vPIS>\
450 <vCOFINS>76.00</vCOFINS>\
451 <dCompet>2026-03-12</dCompet>\
452 <vDeducao>20.00</vDeducao>\
453 <vOutro>5.00</vOutro>\
454 <vDescIncond>3.00</vDescIncond>\
455 <vDescCond>2.00</vDescCond>\
456 <vISSRet>10.00</vISSRet>\
457 <cRegTrib>6</cRegTrib>\
458 </ISSQNtot>"
459 );
460 }
461
462 #[test]
463 fn issqn_tot_zero_values_omitted() {
464 let data = IssqnTotData::new("2026-01-01")
465 .v_serv(Cents(0))
466 .v_bc(Cents(0))
467 .v_iss(Cents(0));
468
469 let xml = build_issqn_tot(&data);
470
471 assert_eq!(xml, "<ISSQNtot><dCompet>2026-01-01</dCompet></ISSQNtot>");
473 }
474
475 #[test]
476 fn issqn_tot_in_total_element() {
477 let data = IssqnTotData::new("2026-03-12")
478 .v_serv(Cents(50000))
479 .v_bc(Cents(50000))
480 .v_iss(Cents(2500));
481
482 let xml = build_total(
483 0,
484 &zero_icms(),
485 &zero_other(),
486 None,
487 Some(&data),
488 None,
489 None,
490 SchemaVersion::PL009,
491 default_method(),
492 None,
493 );
494
495 let icms_end = xml.find("</ICMSTot>").expect("</ICMSTot> must exist");
497 let issqn_start = xml.find("<ISSQNtot>").expect("<ISSQNtot> must exist");
498 assert!(issqn_start > icms_end, "ISSQNtot must come after ICMSTot");
499
500 assert!(xml.contains("<vServ>500.00</vServ>"));
501 assert!(xml.contains("<vBC>500.00</vBC>"));
502 assert!(xml.contains("<vISS>25.00</vISS>"));
503 assert!(xml.contains("<dCompet>2026-03-12</dCompet>"));
504 }
505
506 #[test]
507 fn no_issqn_tot_when_none() {
508 let xml = build_total(
509 0,
510 &zero_icms(),
511 &zero_other(),
512 None,
513 None,
514 None,
515 None,
516 SchemaVersion::PL009,
517 default_method(),
518 None,
519 );
520 assert!(!xml.contains("<ISSQNtot>"));
521 }
522
523 #[test]
524 fn icms_mono_fields_emitted_when_positive() {
525 let mut icms = zero_icms();
526 icms.q_bc_mono = 10000;
527 icms.v_icms_mono = Cents(5000);
528 icms.q_bc_mono_reten = 8000;
529 icms.v_icms_mono_reten = Cents(4000);
530 icms.q_bc_mono_ret = 6000;
531 icms.v_icms_mono_ret = Cents(3000);
532
533 let xml = build_total(
534 100000,
535 &icms,
536 &zero_other(),
537 None,
538 None,
539 None,
540 None,
541 SchemaVersion::PL009,
542 default_method(),
543 None,
544 );
545
546 assert!(xml.contains("<qBCMono>100.00</qBCMono>"));
548 assert!(xml.contains("<vICMSMono>50.00</vICMSMono>"));
549 assert!(xml.contains("<qBCMonoReten>80.00</qBCMonoReten>"));
550 assert!(xml.contains("<vICMSMonoReten>40.00</vICMSMonoReten>"));
551 assert!(xml.contains("<qBCMonoRet>60.00</qBCMonoRet>"));
552 assert!(xml.contains("<vICMSMonoRet>30.00</vICMSMonoRet>"));
553
554 let fcp_st_ret_end = xml.find("</vFCPSTRet>").expect("vFCPSTRet must exist");
556 let q_bc_mono_pos = xml.find("<qBCMono>").expect("qBCMono must exist");
557 let v_prod_pos = xml.find("<vProd>").expect("vProd must exist");
558 assert!(
559 fcp_st_ret_end < q_bc_mono_pos,
560 "qBCMono must come after vFCPSTRet"
561 );
562 assert!(q_bc_mono_pos < v_prod_pos, "qBCMono must come before vProd");
563 }
564
565 #[test]
566 fn icms_mono_fields_omitted_when_zero() {
567 let xml = build_total(
568 100000,
569 &zero_icms(),
570 &zero_other(),
571 None,
572 None,
573 None,
574 None,
575 SchemaVersion::PL009,
576 default_method(),
577 None,
578 );
579
580 assert!(
581 !xml.contains("<qBCMono>"),
582 "qBCMono must be omitted when zero"
583 );
584 assert!(
585 !xml.contains("<vICMSMono>"),
586 "vICMSMono must be omitted when zero"
587 );
588 assert!(
589 !xml.contains("<qBCMonoReten>"),
590 "qBCMonoReten must be omitted when zero"
591 );
592 assert!(
593 !xml.contains("<vICMSMonoReten>"),
594 "vICMSMonoReten must be omitted when zero"
595 );
596 assert!(
597 !xml.contains("<qBCMonoRet>"),
598 "qBCMonoRet must be omitted when zero"
599 );
600 assert!(
601 !xml.contains("<vICMSMonoRet>"),
602 "vICMSMonoRet must be omitted when zero"
603 );
604 }
605
606 #[test]
607 fn icms_mono_partial_fields_emitted() {
608 let mut icms = zero_icms();
609 icms.v_icms_mono = Cents(2500);
611 icms.q_bc_mono_ret = 5000;
612
613 let xml = build_total(
614 100000,
615 &icms,
616 &zero_other(),
617 None,
618 None,
619 None,
620 None,
621 SchemaVersion::PL009,
622 default_method(),
623 None,
624 );
625
626 assert!(!xml.contains("<qBCMono>"));
628 assert!(xml.contains("<vICMSMono>25.00</vICMSMono>"));
630 assert!(!xml.contains("<qBCMonoReten>"));
632 assert!(!xml.contains("<vICMSMonoReten>"));
634 assert!(xml.contains("<qBCMonoRet>50.00</qBCMonoRet>"));
636 assert!(!xml.contains("<vICMSMonoRet>"));
638 }
639
640 #[test]
641 fn issqn_tot_creg_trib_without_monetary_values() {
642 let data = IssqnTotData::new("2026-06-15").c_reg_trib("1");
643 let xml = build_issqn_tot(&data);
644
645 assert_eq!(
646 xml,
647 "<ISSQNtot>\
648 <dCompet>2026-06-15</dCompet>\
649 <cRegTrib>1</cRegTrib>\
650 </ISSQNtot>"
651 );
652 }
653
654 #[test]
657 fn vnf_reduced_by_vicms_deson_when_ind_deduz_deson_is_true() {
658 let total_products: i64 = 100_000;
660 let mut icms = zero_icms();
661 icms.v_icms_deson = Cents(10_000); icms.ind_deduz_deson = true;
663
664 let xml = build_total(
665 total_products,
666 &icms,
667 &zero_other(),
668 None,
669 None,
670 None,
671 None,
672 SchemaVersion::PL009,
673 default_method(),
674 None,
675 );
676
677 assert_eq!(
679 xml.contains("<vNF>900.00</vNF>"),
680 true,
681 "vNF deve ser 900.00 quando indDeduzDeson=1 e vICMSDeson=100.00. XML: {}",
682 xml
683 );
684 }
685
686 #[test]
688 fn vnf_unchanged_without_desoneracao() {
689 let total_products: i64 = 100_000;
690 let mut icms = zero_icms();
691 icms.v_icms_deson = Cents(10_000); icms.ind_deduz_deson = false;
693
694 let xml = build_total(
695 total_products,
696 &icms,
697 &zero_other(),
698 None,
699 None,
700 None,
701 None,
702 SchemaVersion::PL009,
703 default_method(),
704 None,
705 );
706
707 assert_eq!(
709 xml.contains("<vNF>1000.00</vNF>"),
710 true,
711 "vNF deve ser 1000.00 quando indDeduzDeson=false. XML: {}",
712 xml
713 );
714 }
715
716 #[test]
718 fn vnf_includes_vserv_from_issqn() {
719 let total_products: i64 = 100_000; let issqn = IssqnTotData::new("2026-03-12").v_serv(Cents(50_000)); let xml = build_total(
723 total_products,
724 &zero_icms(),
725 &zero_other(),
726 None,
727 Some(&issqn),
728 None,
729 None,
730 SchemaVersion::PL009,
731 default_method(),
732 None,
733 );
734
735 assert_eq!(
737 xml.contains("<vNF>1500.00</vNF>"),
738 true,
739 "vNF deve ser 1500.00 incluindo vServ=500.00. XML: {}",
740 xml
741 );
742 }
743
744 #[test]
747 fn icms_uf_dest_fields_emitted_when_positive() {
748 let mut icms = zero_icms();
749 icms.v_fcp_uf_dest = Cents(1000);
750 icms.v_icms_uf_dest = Cents(2000);
751 icms.v_icms_uf_remet = Cents(3000);
752
753 let xml = build_total(
754 100000,
755 &icms,
756 &zero_other(),
757 None,
758 None,
759 None,
760 None,
761 SchemaVersion::PL009,
762 default_method(),
763 None,
764 );
765
766 assert!(
767 xml.contains("<vFCPUFDest>10.00</vFCPUFDest>"),
768 "vFCPUFDest must be present when > 0. XML: {xml}"
769 );
770 assert!(
771 xml.contains("<vICMSUFDest>20.00</vICMSUFDest>"),
772 "vICMSUFDest must be present when > 0. XML: {xml}"
773 );
774 assert!(
775 xml.contains("<vICMSUFRemet>30.00</vICMSUFRemet>"),
776 "vICMSUFRemet must be present when > 0. XML: {xml}"
777 );
778
779 let deson_end = xml.find("</vICMSDeson>").expect("vICMSDeson must exist");
781 let fcp_uf_start = xml.find("<vFCPUFDest>").expect("vFCPUFDest must exist");
782 let fcp_start = xml.find("<vFCP>").expect("vFCP must exist");
783 assert!(
784 fcp_uf_start > deson_end,
785 "vFCPUFDest must come after vICMSDeson"
786 );
787 assert!(fcp_start > fcp_uf_start, "vFCP must come after vFCPUFDest");
788 }
789
790 #[test]
791 fn icms_uf_dest_fields_omitted_when_zero() {
792 let xml = build_total(
793 100000,
794 &zero_icms(),
795 &zero_other(),
796 None,
797 None,
798 None,
799 None,
800 SchemaVersion::PL009,
801 default_method(),
802 None,
803 );
804
805 assert!(
806 !xml.contains("<vFCPUFDest>"),
807 "vFCPUFDest must be omitted when zero"
808 );
809 assert!(
810 !xml.contains("<vICMSUFDest>"),
811 "vICMSUFDest must be omitted when zero"
812 );
813 assert!(
814 !xml.contains("<vICMSUFRemet>"),
815 "vICMSUFRemet must be omitted when zero"
816 );
817 }
818
819 #[test]
822 fn v_nf_tot_override_emitted_on_pl010_with_ibs_cbs() {
823 use crate::tax_ibs_cbs::IbsCbsTotData;
824
825 let ibs_cbs = IbsCbsTotData::new("1000.00");
826 let xml = build_total(
827 100_000, &zero_icms(),
829 &zero_other(),
830 None,
831 None,
832 None,
833 Some(&ibs_cbs),
834 SchemaVersion::PL010,
835 default_method(),
836 Some(Cents(150_000)), );
838
839 assert!(
840 xml.contains("<vNFTot>1500.00</vNFTot>"),
841 "vNFTot override deve ser emitido com valor 1500.00. XML: {xml}"
842 );
843
844 let ibs_end = xml.find("</IBSCBSTot>").expect("</IBSCBSTot> must exist");
846 let vnftot_start = xml.find("<vNFTot>").expect("<vNFTot> must exist");
847 assert!(
848 vnftot_start > ibs_end,
849 "vNFTot deve vir depois de IBSCBSTot"
850 );
851 }
852
853 #[test]
854 fn v_nf_tot_auto_calculated_on_pl010() {
855 use crate::tax_ibs_cbs::{IbsCbsTotData, IsTotData};
856
857 let mut ibs_cbs = IbsCbsTotData::new("1000.00");
861 ibs_cbs.g_ibs_v_ibs = Some("50.00".to_string());
862 ibs_cbs.g_cbs_v_cbs = Some("30.00".to_string());
863
864 let is_tot = IsTotData::new("10.00");
865
866 let xml = build_total(
867 100_000, &zero_icms(),
869 &zero_other(),
870 None,
871 None,
872 Some(&is_tot),
873 Some(&ibs_cbs),
874 SchemaVersion::PL010,
875 default_method(),
876 None, );
878
879 assert!(
880 xml.contains("<vNFTot>1090.00</vNFTot>"),
881 "vNFTot auto-calculado deve ser 1090.00. XML: {xml}"
882 );
883 }
884
885 #[test]
886 fn v_nf_tot_not_emitted_on_pl009() {
887 use crate::tax_ibs_cbs::IbsCbsTotData;
888
889 let ibs_cbs = IbsCbsTotData::new("1000.00");
890 let xml = build_total(
891 100_000,
892 &zero_icms(),
893 &zero_other(),
894 None,
895 None,
896 None,
897 Some(&ibs_cbs),
898 SchemaVersion::PL009,
899 default_method(),
900 Some(Cents(150_000)),
901 );
902
903 assert!(
904 !xml.contains("<vNFTot>"),
905 "vNFTot não deve ser emitido no PL009. XML: {xml}"
906 );
907 }
908
909 #[test]
910 fn v_nf_tot_not_emitted_without_ibs_cbs() {
911 let xml = build_total(
912 100_000,
913 &zero_icms(),
914 &zero_other(),
915 None,
916 None,
917 None,
918 None, SchemaVersion::PL010,
920 default_method(),
921 Some(Cents(150_000)),
922 );
923
924 assert!(
925 !xml.contains("<vNFTot>"),
926 "vNFTot não deve ser emitido sem IBSCBSTot. XML: {xml}"
927 );
928 }
929
930 #[test]
931 fn v_nf_tot_zero_override_not_emitted() {
932 use crate::tax_ibs_cbs::IbsCbsTotData;
933
934 let ibs_cbs = IbsCbsTotData::new("1000.00");
935 let xml = build_total(
936 100_000,
937 &zero_icms(),
938 &zero_other(),
939 None,
940 None,
941 None,
942 Some(&ibs_cbs),
943 SchemaVersion::PL010,
944 default_method(),
945 Some(Cents(0)), );
947
948 assert!(
949 !xml.contains("<vNFTot>"),
950 "vNFTot não deve ser emitido quando override é zero. XML: {xml}"
951 );
952 }
953
954 #[test]
955 fn v_nf_tot_auto_without_is() {
956 use crate::tax_ibs_cbs::IbsCbsTotData;
957
958 let mut ibs_cbs = IbsCbsTotData::new("500.00");
962 ibs_cbs.g_ibs_v_ibs = Some("25.00".to_string());
963 ibs_cbs.g_cbs_v_cbs = Some("15.00".to_string());
964
965 let xml = build_total(
966 50_000, &zero_icms(),
968 &zero_other(),
969 None,
970 None,
971 None,
972 Some(&ibs_cbs),
973 SchemaVersion::PL010,
974 default_method(),
975 None,
976 );
977
978 assert!(
979 xml.contains("<vNFTot>540.00</vNFTot>"),
980 "vNFTot auto-calculado sem IS deve ser 540.00. XML: {xml}"
981 );
982 }
983}