Skip to main content

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}