fiscal_core/tax_is.rs
1//! IS (Imposto Seletivo) XML generation for NF-e items — PL_010 tax reform.
2//!
3//! The IS (Imposto Seletivo) is a new consumption tax introduced by Brazil's
4//! 2024 tax reform. This module provides [`IsData`] and [`build_is_xml`] to
5//! generate the `<IS>` element placed inside `<imposto>`.
6
7use crate::tax_element::{
8 TaxElement, TaxField, filter_fields, optional_field, serialize_tax_element,
9};
10
11/// IS (Imposto Seletivo / IBS+CBS) input data -- PL_010 tax reform.
12/// Goes inside `<imposto>` as an alternative/addition to ICMS.
13///
14/// String fields are pre-formatted (e.g. "100.00", "5.0000") because
15/// the IS schema uses mixed decimal precisions that don't map to a
16/// single cents/rate convention.
17#[derive(Debug, Clone, Default)]
18#[non_exhaustive]
19pub struct IsData {
20 /// IS tax situation code
21 pub cst_is: String,
22 /// IS tax classification code
23 pub c_class_trib_is: String,
24 /// Tax base (optional, e.g. "100.00")
25 pub v_bc_is: Option<String>,
26 /// IS rate (optional, e.g. "5.0000")
27 pub p_is: Option<String>,
28 /// Specific rate (optional, e.g. "1.5000")
29 pub p_is_espec: Option<String>,
30 /// Taxable unit of measure (optional, e.g. "LT")
31 pub u_trib: Option<String>,
32 /// Taxable quantity (optional, e.g. "10.0000")
33 pub q_trib: Option<String>,
34 /// IS tax value (e.g. "5.00")
35 pub v_is: String,
36}
37
38impl IsData {
39 /// Create a new `IsData` with required fields.
40 ///
41 /// `cst_is` is the IS tax situation code, `c_class_trib_is` is the
42 /// IS classification code, and `v_is` is the pre-formatted IS value string
43 /// (e.g. `"5.00"`).
44 pub fn new(
45 cst_is: impl Into<String>,
46 c_class_trib_is: impl Into<String>,
47 v_is: impl Into<String>,
48 ) -> Self {
49 Self {
50 cst_is: cst_is.into(),
51 c_class_trib_is: c_class_trib_is.into(),
52 v_is: v_is.into(),
53 ..Default::default()
54 }
55 }
56 /// Set the IS calculation base (`vBCIS`), e.g. `"100.00"`.
57 pub fn v_bc_is(mut self, v: impl Into<String>) -> Self {
58 self.v_bc_is = Some(v.into());
59 self
60 }
61 /// Set the IS ad-valorem rate (`pIS`), e.g. `"5.0000"`.
62 pub fn p_is(mut self, v: impl Into<String>) -> Self {
63 self.p_is = Some(v.into());
64 self
65 }
66 /// Set the IS specific rate (`pISEspec`), e.g. `"1.5000"`.
67 pub fn p_is_espec(mut self, v: impl Into<String>) -> Self {
68 self.p_is_espec = Some(v.into());
69 self
70 }
71 /// Set the taxable unit of measure (`uTrib`), e.g. `"LT"`.
72 pub fn u_trib(mut self, v: impl Into<String>) -> Self {
73 self.u_trib = Some(v.into());
74 self
75 }
76 /// Set the taxable quantity (`qTrib`), e.g. `"10.0000"`.
77 pub fn q_trib(mut self, v: impl Into<String>) -> Self {
78 self.q_trib = Some(v.into());
79 self
80 }
81}
82
83/// Calculate IS tax element (domain logic, no XML dependency).
84///
85/// Three mutually exclusive modes based on which fields are present:
86/// - `v_bc_is` present: ad-valorem mode (includes pIS, pISEspec)
87/// - `u_trib` + `q_trib` present: specific quantity mode
88/// - Neither: simple CST + classification + value
89fn calculate_is(data: &IsData) -> TaxElement {
90 let mut fields: Vec<Option<TaxField>> = vec![
91 Some(TaxField::new("CSTIS", &data.cst_is)),
92 Some(TaxField::new("cClassTribIS", &data.c_class_trib_is)),
93 ];
94
95 if let Some(ref v_bc_is) = data.v_bc_is {
96 fields.push(Some(TaxField::new("vBCIS", v_bc_is)));
97 fields.push(optional_field("pIS", data.p_is.as_deref()));
98 fields.push(optional_field("pISEspec", data.p_is_espec.as_deref()));
99 }
100
101 if let (Some(u_trib), Some(q_trib)) = (&data.u_trib, &data.q_trib) {
102 fields.push(Some(TaxField::new("uTrib", u_trib)));
103 fields.push(Some(TaxField::new("qTrib", q_trib)));
104 }
105
106 fields.push(Some(TaxField::new("vIS", &data.v_is)));
107
108 TaxElement {
109 outer_tag: None,
110 outer_fields: vec![],
111 variant_tag: "IS".into(),
112 fields: filter_fields(fields),
113 }
114}
115
116/// Build IS (IBS/CBS) XML string.
117pub fn build_is_xml(data: &IsData) -> String {
118 serialize_tax_element(&calculate_is(data))
119}