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