Skip to main content

fiscal_core/
traits.rs

1//! Sealed public traits for tax calculation and XML serialization.
2
3use crate::sealed::private::Sealed;
4use crate::tax_element::{TaxElement, serialize_tax_element};
5use crate::tax_icms::{IcmsVariant, build_icms_xml, create_icms_totals};
6use crate::tax_is::{IsData, build_is_xml};
7use crate::tax_issqn::{IssqnData, build_issqn_xml};
8use crate::tax_pis_cofins_ipi::{
9    CofinsData, IiData, IpiData, PisData, build_cofins_xml, build_ii_xml, build_ipi_xml,
10    build_pis_xml,
11};
12
13// ── TaxCalculation ──────────────────────────────────────────────────────────
14
15/// Tax data that can build an XML string fragment.
16///
17/// This trait is sealed — only types within this crate can implement it.
18/// External crates can call methods on implementors but cannot add new ones.
19pub trait TaxCalculation: Sealed {
20    /// Build the XML string for this tax data.
21    fn build_xml(&self) -> String;
22}
23
24impl Sealed for IcmsVariant {}
25impl TaxCalculation for IcmsVariant {
26    /// Build the ICMS XML string, delegating to [`build_icms_xml`].
27    ///
28    /// A throwaway [`crate::tax_icms::IcmsTotals`] accumulator is used since
29    /// the trait cannot surface totals side-effects. Returns an empty string
30    /// if the underlying call returns an error.
31    fn build_xml(&self) -> String {
32        let mut totals = create_icms_totals();
33        build_icms_xml(self, &mut totals).unwrap_or_default()
34    }
35}
36
37impl Sealed for PisData {}
38impl TaxCalculation for PisData {
39    /// Build the PIS XML string, delegating to [`build_pis_xml`].
40    fn build_xml(&self) -> String {
41        build_pis_xml(self)
42    }
43}
44
45impl Sealed for CofinsData {}
46impl TaxCalculation for CofinsData {
47    /// Build the COFINS XML string, delegating to [`build_cofins_xml`].
48    fn build_xml(&self) -> String {
49        build_cofins_xml(self)
50    }
51}
52
53impl Sealed for IpiData {}
54impl TaxCalculation for IpiData {
55    /// Build the IPI XML string, delegating to [`build_ipi_xml`].
56    fn build_xml(&self) -> String {
57        build_ipi_xml(self)
58    }
59}
60
61impl Sealed for IiData {}
62impl TaxCalculation for IiData {
63    /// Build the II (import tax) XML string, delegating to [`build_ii_xml`].
64    fn build_xml(&self) -> String {
65        build_ii_xml(self)
66    }
67}
68
69impl Sealed for IssqnData {}
70impl TaxCalculation for IssqnData {
71    /// Build the ISSQN XML string, delegating to [`build_issqn_xml`].
72    fn build_xml(&self) -> String {
73        build_issqn_xml(self)
74    }
75}
76
77impl Sealed for IsData {}
78impl TaxCalculation for IsData {
79    /// Build the IS (IBS/CBS) XML string, delegating to [`build_is_xml`].
80    fn build_xml(&self) -> String {
81        build_is_xml(self)
82    }
83}
84
85// ── XmlSerializable ─────────────────────────────────────────────────────────
86
87/// Types that can serialize themselves to NF-e XML fragments.
88///
89/// This trait is sealed — only types within this crate can implement it.
90pub trait XmlSerializable: Sealed {
91    /// Serialize to an XML string fragment.
92    fn to_xml(&self) -> String;
93}
94
95impl Sealed for TaxElement {}
96impl XmlSerializable for TaxElement {
97    /// Serialize the element to XML, delegating to [`serialize_tax_element`].
98    fn to_xml(&self) -> String {
99        serialize_tax_element(self)
100    }
101}
102
103// ── Tests ────────────────────────────────────────────────────────────────────
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::newtypes::{Cents, Rate, Rate4};
109    use crate::tax_element::TaxField;
110    use crate::tax_icms::{IcmsCst, IcmsVariant};
111    use crate::tax_is::IsData;
112    use crate::tax_issqn::IssqnData;
113    use crate::tax_pis_cofins_ipi::{CofinsData, IiData, IpiData, PisData};
114
115    // -- IcmsVariant ----------------------------------------------------------
116
117    #[test]
118    fn icms_variant_trait_matches_free_fn() {
119        let cst = IcmsCst::Cst00 {
120            orig: "0".to_string(),
121            mod_bc: "3".to_string(),
122            v_bc: Cents(10000),
123            p_icms: Rate(1200),
124            v_icms: Cents(1200),
125            p_fcp: None,
126            v_fcp: None,
127        };
128        let variant = IcmsVariant::from(cst);
129
130        let mut totals = create_icms_totals();
131        let expected = build_icms_xml(&variant, &mut totals).unwrap();
132        let got = variant.build_xml();
133        assert_eq!(got, expected);
134    }
135
136    // -- PisData --------------------------------------------------------------
137
138    #[test]
139    fn pis_data_trait_matches_free_fn() {
140        let data = PisData::new("01")
141            .v_bc(Cents(10000))
142            .p_pis(Rate4(16500))
143            .v_pis(Cents(165));
144
145        assert_eq!(data.build_xml(), build_pis_xml(&data));
146    }
147
148    // -- CofinsData -----------------------------------------------------------
149
150    #[test]
151    fn cofins_data_trait_matches_free_fn() {
152        let data = CofinsData::new("01")
153            .v_bc(Cents(10000))
154            .p_cofins(Rate4(76000))
155            .v_cofins(Cents(760));
156
157        assert_eq!(data.build_xml(), build_cofins_xml(&data));
158    }
159
160    // -- IpiData --------------------------------------------------------------
161
162    #[test]
163    fn ipi_data_trait_matches_free_fn() {
164        let data = IpiData::new("00", "999")
165            .v_bc(Cents(10000))
166            .p_ipi(Rate(500))
167            .v_ipi(Cents(500));
168
169        assert_eq!(data.build_xml(), build_ipi_xml(&data));
170    }
171
172    // -- IiData ---------------------------------------------------------------
173
174    #[test]
175    fn ii_data_trait_matches_free_fn() {
176        let data = IiData::new(Cents(10000), Cents(200), Cents(1000), Cents(50));
177
178        assert_eq!(data.build_xml(), build_ii_xml(&data));
179    }
180
181    // -- IssqnData ------------------------------------------------------------
182
183    #[test]
184    fn issqn_data_trait_matches_free_fn() {
185        let data = IssqnData::new(10000, 500, 500, "4106902", "01.01");
186
187        assert_eq!(data.build_xml(), build_issqn_xml(&data));
188    }
189
190    // -- IsData ---------------------------------------------------------------
191
192    #[test]
193    fn is_data_trait_matches_free_fn() {
194        let data = IsData::new("00", "1234", "5.00")
195            .v_bc_is("100.00")
196            .p_is("5.0000");
197
198        assert_eq!(data.build_xml(), build_is_xml(&data));
199    }
200
201    // -- TaxElement -----------------------------------------------------------
202
203    #[test]
204    fn tax_element_trait_matches_free_fn() {
205        let element = TaxElement {
206            outer_tag: Some("PIS".to_string()),
207            outer_fields: vec![],
208            variant_tag: "PISAliq".to_string(),
209            fields: vec![
210                TaxField::new("CST", "01"),
211                TaxField::new("vBC", "100.00"),
212                TaxField::new("pPIS", "1.6500"),
213                TaxField::new("vPIS", "1.65"),
214            ],
215        };
216
217        assert_eq!(element.to_xml(), serialize_tax_element(&element));
218    }
219}