Skip to main content

fiscal_core/tax_icms/
mod.rs

1//! ICMS tax computation and XML generation for NF-e / NFC-e documents.
2//!
3//! This module provides two main enum types:
4//! - [`IcmsCst`] — for normal tax regime (Lucro Real / Lucro Presumido), covering
5//!   CSTs 00, 02, 10, 15, 20, 30, 40, 41, 50, 51, 53, 60, 61, 70, and 90.
6//! - [`IcmsCsosn`] — for Simples Nacional regime (CRT 1/2), covering CSOSNs
7//!   101, 102, 103, 201, 202, 203, 300, 400, 500, and 900.
8//!
9//! Both are wrapped by the [`IcmsVariant`] enum, which is consumed by
10//! [`build_icms_cst_xml`] / [`build_icms_csosn_xml`] to produce the `<ICMS>`
11//! XML fragment and accumulate [`IcmsTotals`].
12//!
13//! There are also three auxiliary data structs for special ICMS groups:
14//! [`IcmsPartData`] (partition), [`IcmsStData`] (ST repasse), and
15//! [`IcmsUfDestData`] (interstate destination differential).
16
17mod builders;
18mod csosn;
19mod cst;
20mod cst_xml;
21mod data;
22mod totals;
23
24use crate::format_utils::format_cents_or_none;
25use crate::newtypes::{Cents, Rate};
26
27// ── Internal helpers ────────────────────────────────────────────────────────
28
29/// Accumulate a value into a totals field.
30fn accum(current: Cents, value: Option<Cents>) -> Cents {
31    current + value.unwrap_or(Cents(0))
32}
33
34/// Accumulate a raw i64 quantity into a totals field.
35fn accum_raw(current: i64, value: Option<i64>) -> i64 {
36    current + value.unwrap_or(0)
37}
38
39/// Format a monetary [`Cents`] value (2 decimal places) returning `Option<String>`.
40fn fc2(v: Option<Cents>) -> Option<String> {
41    format_cents_or_none(v.map(|c| c.0), 2)
42}
43
44/// Format a [`Rate`] value (4 decimal places) returning `Option<String>`.
45fn fc4(v: Option<Rate>) -> Option<String> {
46    format_cents_or_none(v.map(|r| r.0), 4)
47}
48
49/// Format a raw i64 quantity (4 decimal places) returning `Option<String>`.
50fn fc4_raw(v: Option<i64>) -> Option<String> {
51    format_cents_or_none(v, 4)
52}
53
54// ── IcmsVariant ─────────────────────────────────────────────────────────────
55
56/// Unified ICMS variant wrapping both normal-regime CSTs and Simples Nacional
57/// CSOSNs. Pass one of these to [`build_icms_xml`] for compile-time-safe XML
58/// generation.
59#[derive(Debug, Clone)]
60#[non_exhaustive]
61pub enum IcmsVariant {
62    /// Normal tax regime (Lucro Real / Presumido).
63    Cst(Box<IcmsCst>),
64    /// Simples Nacional tax regime (CRT 1/2).
65    Csosn(Box<IcmsCsosn>),
66}
67
68impl From<IcmsCst> for IcmsVariant {
69    fn from(cst: IcmsCst) -> Self {
70        Self::Cst(Box::new(cst))
71    }
72}
73
74impl From<IcmsCsosn> for IcmsVariant {
75    fn from(csosn: IcmsCsosn) -> Self {
76        Self::Csosn(Box::new(csosn))
77    }
78}
79
80// ── Public re-exports ───────────────────────────────────────────────────────
81
82pub use builders::{
83    build_icms_part_xml, build_icms_st_xml, build_icms_uf_dest_xml, build_icms_xml,
84};
85pub use csosn::{IcmsCsosn, build_icms_csosn_xml};
86pub use cst::IcmsCst;
87pub use cst_xml::build_icms_cst_xml;
88pub use data::{IcmsPartData, IcmsStData, IcmsUfDestData};
89pub use totals::{IcmsTotals, create_icms_totals, merge_icms_totals};
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use crate::newtypes::{Cents, Rate};
95
96    // ── IcmsTotals builder methods ──────────────────────────────────
97
98    #[test]
99    fn icms_totals_builder_v_icms_uf_remet() {
100        let t = IcmsTotals::new().v_icms_uf_remet(Cents(500));
101        assert_eq!(t.v_icms_uf_remet, Cents(500));
102    }
103
104    #[test]
105    fn icms_totals_builder_v_icms_mono() {
106        let t = IcmsTotals::new().v_icms_mono(Cents(300));
107        assert_eq!(t.v_icms_mono, Cents(300));
108    }
109
110    #[test]
111    fn icms_totals_builder_v_icms_mono_reten() {
112        let t = IcmsTotals::new().v_icms_mono_reten(Cents(200));
113        assert_eq!(t.v_icms_mono_reten, Cents(200));
114    }
115
116    #[test]
117    fn icms_totals_builder_v_icms_mono_ret() {
118        let t = IcmsTotals::new().v_icms_mono_ret(Cents(100));
119        assert_eq!(t.v_icms_mono_ret, Cents(100));
120    }
121
122    #[test]
123    fn icms_totals_builder_ind_deduz_deson() {
124        let t = IcmsTotals::new().ind_deduz_deson(true);
125        assert!(t.ind_deduz_deson);
126        let t2 = IcmsTotals::new().ind_deduz_deson(false);
127        assert!(!t2.ind_deduz_deson);
128    }
129
130    // ── IcmsCst::cst_code() for uncovered variants ─────────────────
131
132    #[test]
133    fn cst_code_cst30() {
134        let cst = IcmsCst::Cst30 {
135            orig: "0".into(),
136            mod_bc_st: "4".into(),
137            p_mva_st: None,
138            p_red_bc_st: None,
139            v_bc_st: Cents(1000),
140            p_icms_st: Rate(1800),
141            v_icms_st: Cents(180),
142            v_bc_fcp_st: None,
143            p_fcp_st: None,
144            v_fcp_st: None,
145            v_icms_deson: None,
146            mot_des_icms: None,
147            ind_deduz_deson: None,
148        };
149        assert_eq!(cst.cst_code(), "30");
150    }
151
152    #[test]
153    fn cst_code_cst51() {
154        let cst = IcmsCst::Cst51 {
155            orig: "0".into(),
156            mod_bc: None,
157            p_red_bc: None,
158            c_benef_rbc: None,
159            v_bc: None,
160            p_icms: None,
161            v_icms_op: None,
162            p_dif: None,
163            v_icms_dif: None,
164            v_icms: None,
165            v_bc_fcp: None,
166            p_fcp: None,
167            v_fcp: None,
168            p_fcp_dif: None,
169            v_fcp_dif: None,
170            v_fcp_efet: None,
171        };
172        assert_eq!(cst.cst_code(), "51");
173    }
174
175    #[test]
176    fn cst_code_cst53() {
177        let cst = IcmsCst::Cst53 {
178            orig: "0".into(),
179            q_bc_mono: None,
180            ad_rem_icms: None,
181            v_icms_mono_op: None,
182            p_dif: None,
183            v_icms_mono_dif: None,
184            v_icms_mono: None,
185        };
186        assert_eq!(cst.cst_code(), "53");
187    }
188
189    #[test]
190    fn cst_code_cst61() {
191        let cst = IcmsCst::Cst61 {
192            orig: "0".into(),
193            q_bc_mono_ret: None,
194            ad_rem_icms_ret: Rate(100),
195            v_icms_mono_ret: Cents(50),
196        };
197        assert_eq!(cst.cst_code(), "61");
198    }
199
200    // ── IcmsCsosn::csosn_code() for uncovered variants ─────────────
201
202    #[test]
203    fn csosn_code_102() {
204        let c = IcmsCsosn::Csosn102 {
205            orig: "0".into(),
206            csosn: "102".into(),
207        };
208        assert_eq!(c.csosn_code(), "102");
209    }
210
211    #[test]
212    fn csosn_code_103() {
213        let c = IcmsCsosn::Csosn103 {
214            orig: "0".into(),
215            csosn: "103".into(),
216        };
217        assert_eq!(c.csosn_code(), "103");
218    }
219
220    #[test]
221    fn csosn_code_201() {
222        let c = IcmsCsosn::Csosn201 {
223            orig: "0".into(),
224            csosn: "201".into(),
225            mod_bc_st: "4".into(),
226            p_mva_st: None,
227            p_red_bc_st: None,
228            v_bc_st: Cents(0),
229            p_icms_st: Rate(0),
230            v_icms_st: Cents(0),
231            v_bc_fcp_st: None,
232            p_fcp_st: None,
233            v_fcp_st: None,
234            p_cred_sn: None,
235            v_cred_icms_sn: None,
236        };
237        assert_eq!(c.csosn_code(), "201");
238    }
239
240    #[test]
241    fn csosn_code_202() {
242        let c = IcmsCsosn::Csosn202 {
243            orig: "0".into(),
244            csosn: "202".into(),
245            mod_bc_st: "4".into(),
246            p_mva_st: None,
247            p_red_bc_st: None,
248            v_bc_st: Cents(0),
249            p_icms_st: Rate(0),
250            v_icms_st: Cents(0),
251            v_bc_fcp_st: None,
252            p_fcp_st: None,
253            v_fcp_st: None,
254        };
255        assert_eq!(c.csosn_code(), "202");
256    }
257
258    #[test]
259    fn csosn_code_203() {
260        let c = IcmsCsosn::Csosn203 {
261            orig: "0".into(),
262            csosn: "203".into(),
263            mod_bc_st: "4".into(),
264            p_mva_st: None,
265            p_red_bc_st: None,
266            v_bc_st: Cents(0),
267            p_icms_st: Rate(0),
268            v_icms_st: Cents(0),
269            v_bc_fcp_st: None,
270            p_fcp_st: None,
271            v_fcp_st: None,
272        };
273        assert_eq!(c.csosn_code(), "203");
274    }
275
276    #[test]
277    fn csosn_code_300() {
278        let c = IcmsCsosn::Csosn300 {
279            orig: "0".into(),
280            csosn: "300".into(),
281        };
282        assert_eq!(c.csosn_code(), "300");
283    }
284
285    #[test]
286    fn csosn_code_400() {
287        let c = IcmsCsosn::Csosn400 {
288            orig: "0".into(),
289            csosn: "400".into(),
290        };
291        assert_eq!(c.csosn_code(), "400");
292    }
293
294    #[test]
295    fn csosn_code_500() {
296        let c = IcmsCsosn::Csosn500 {
297            orig: "0".into(),
298            csosn: "500".into(),
299            v_bc_st_ret: None,
300            p_st: None,
301            v_icms_substituto: None,
302            v_icms_st_ret: None,
303            v_bc_fcp_st_ret: None,
304            p_fcp_st_ret: None,
305            v_fcp_st_ret: None,
306            p_red_bc_efet: None,
307            v_bc_efet: None,
308            p_icms_efet: None,
309            v_icms_efet: None,
310        };
311        assert_eq!(c.csosn_code(), "500");
312    }
313
314    #[test]
315    fn csosn_code_900() {
316        let c = IcmsCsosn::Csosn900 {
317            orig: "0".into(),
318            csosn: "900".into(),
319            mod_bc: None,
320            v_bc: None,
321            p_red_bc: None,
322            p_icms: None,
323            v_icms: None,
324            mod_bc_st: None,
325            p_mva_st: None,
326            p_red_bc_st: None,
327            v_bc_st: None,
328            p_icms_st: None,
329            v_icms_st: None,
330            v_bc_fcp_st: None,
331            p_fcp_st: None,
332            v_fcp_st: None,
333            p_cred_sn: None,
334            v_cred_icms_sn: None,
335        };
336        assert_eq!(c.csosn_code(), "900");
337    }
338
339    // ── empty orig in Csosn102/103/300/400 ──────────────────────────
340
341    #[test]
342    fn csosn102_empty_orig_omits_orig_field() {
343        let c = IcmsCsosn::Csosn102 {
344            orig: String::new(),
345            csosn: "102".into(),
346        };
347        let mut totals = IcmsTotals::new();
348        let (tag, fields) = build_icms_csosn_xml(&c, &mut totals).unwrap();
349        assert_eq!(tag, "ICMSSN102");
350        // When orig is empty, the orig field should not be present
351        assert!(fields.iter().all(|f| f.name != "orig"));
352    }
353
354    // ── merge_icms_totals with ind_deduz_deson=true ─────────────────
355
356    #[test]
357    fn merge_icms_totals_propagates_ind_deduz_deson() {
358        let mut target = IcmsTotals::new();
359        assert!(!target.ind_deduz_deson);
360
361        let source = IcmsTotals::new()
362            .v_bc(Cents(1000))
363            .v_icms(Cents(180))
364            .v_icms_mono(Cents(50))
365            .v_icms_mono_reten(Cents(25))
366            .v_icms_mono_ret(Cents(10))
367            .v_icms_uf_remet(Cents(30))
368            .ind_deduz_deson(true);
369
370        merge_icms_totals(&mut target, &source);
371        assert!(target.ind_deduz_deson);
372        assert_eq!(target.v_bc, Cents(1000));
373        assert_eq!(target.v_icms, Cents(180));
374        assert_eq!(target.v_icms_mono, Cents(50));
375        assert_eq!(target.v_icms_mono_reten, Cents(25));
376        assert_eq!(target.v_icms_mono_ret, Cents(10));
377        assert_eq!(target.v_icms_uf_remet, Cents(30));
378    }
379
380    #[test]
381    fn merge_icms_totals_does_not_set_false_on_target() {
382        let mut target = IcmsTotals::new().ind_deduz_deson(true);
383        let source = IcmsTotals::new(); // ind_deduz_deson = false
384        merge_icms_totals(&mut target, &source);
385        // target should remain true
386        assert!(target.ind_deduz_deson);
387    }
388
389    // ── IcmsCst::cst_code() for previously uncovered variants ────────────
390
391    #[test]
392    fn cst_code_cst00() {
393        let cst = IcmsCst::Cst00 {
394            orig: "0".into(),
395            mod_bc: "3".into(),
396            v_bc: Cents(10000),
397            p_icms: Rate(1800),
398            v_icms: Cents(1800),
399            p_fcp: None,
400            v_fcp: None,
401        };
402        assert_eq!(cst.cst_code(), "00");
403    }
404
405    #[test]
406    fn cst_code_cst02() {
407        let cst = IcmsCst::Cst02 {
408            orig: "0".into(),
409            q_bc_mono: None,
410            ad_rem_icms: Rate(100),
411            v_icms_mono: Cents(50),
412        };
413        assert_eq!(cst.cst_code(), "02");
414    }
415
416    #[test]
417    fn cst_code_cst10() {
418        let cst = IcmsCst::Cst10 {
419            orig: "0".into(),
420            mod_bc: "3".into(),
421            v_bc: Cents(10000),
422            p_icms: Rate(1800),
423            v_icms: Cents(1800),
424            v_bc_fcp: None,
425            p_fcp: None,
426            v_fcp: None,
427            mod_bc_st: "4".into(),
428            p_mva_st: None,
429            p_red_bc_st: None,
430            v_bc_st: Cents(10000),
431            p_icms_st: Rate(1800),
432            v_icms_st: Cents(1800),
433            v_bc_fcp_st: None,
434            p_fcp_st: None,
435            v_fcp_st: None,
436            v_icms_st_deson: None,
437            mot_des_icms_st: None,
438        };
439        assert_eq!(cst.cst_code(), "10");
440    }
441
442    #[test]
443    fn cst_code_cst15() {
444        let cst = IcmsCst::Cst15 {
445            orig: "0".into(),
446            q_bc_mono: None,
447            ad_rem_icms: Rate(100),
448            v_icms_mono: Cents(50),
449            q_bc_mono_reten: None,
450            ad_rem_icms_reten: Rate(80),
451            v_icms_mono_reten: Cents(40),
452            p_red_ad_rem: None,
453            mot_red_ad_rem: None,
454        };
455        assert_eq!(cst.cst_code(), "15");
456    }
457
458    #[test]
459    fn cst_code_cst20() {
460        let cst = IcmsCst::Cst20 {
461            orig: "0".into(),
462            mod_bc: "3".into(),
463            p_red_bc: Rate(5000),
464            v_bc: Cents(5000),
465            p_icms: Rate(1800),
466            v_icms: Cents(900),
467            v_bc_fcp: None,
468            p_fcp: None,
469            v_fcp: None,
470            v_icms_deson: None,
471            mot_des_icms: None,
472            ind_deduz_deson: None,
473        };
474        assert_eq!(cst.cst_code(), "20");
475    }
476
477    #[test]
478    fn cst_code_cst60() {
479        let cst = IcmsCst::Cst60 {
480            orig: "0".into(),
481            v_bc_st_ret: None,
482            p_st: None,
483            v_icms_substituto: None,
484            v_icms_st_ret: None,
485            v_bc_fcp_st_ret: None,
486            p_fcp_st_ret: None,
487            v_fcp_st_ret: None,
488            p_red_bc_efet: None,
489            v_bc_efet: None,
490            p_icms_efet: None,
491            v_icms_efet: None,
492        };
493        assert_eq!(cst.cst_code(), "60");
494    }
495
496    #[test]
497    fn cst_code_cst70() {
498        let cst = IcmsCst::Cst70 {
499            orig: "0".into(),
500            mod_bc: "3".into(),
501            p_red_bc: Rate(5000),
502            v_bc: Cents(5000),
503            p_icms: Rate(1800),
504            v_icms: Cents(900),
505            v_bc_fcp: None,
506            p_fcp: None,
507            v_fcp: None,
508            mod_bc_st: "4".into(),
509            p_mva_st: None,
510            p_red_bc_st: None,
511            v_bc_st: Cents(5000),
512            p_icms_st: Rate(1800),
513            v_icms_st: Cents(900),
514            v_bc_fcp_st: None,
515            p_fcp_st: None,
516            v_fcp_st: None,
517            v_icms_deson: None,
518            mot_des_icms: None,
519            ind_deduz_deson: None,
520            v_icms_st_deson: None,
521            mot_des_icms_st: None,
522        };
523        assert_eq!(cst.cst_code(), "70");
524    }
525
526    #[test]
527    fn cst_code_cst90() {
528        let cst = IcmsCst::Cst90 {
529            orig: "0".into(),
530            mod_bc: None,
531            v_bc: None,
532            p_red_bc: None,
533            c_benef_rbc: None,
534            p_icms: None,
535            v_icms_op: None,
536            p_dif: None,
537            v_icms_dif: None,
538            v_icms: None,
539            v_bc_fcp: None,
540            p_fcp: None,
541            v_fcp: None,
542            p_fcp_dif: None,
543            v_fcp_dif: None,
544            v_fcp_efet: None,
545            mod_bc_st: None,
546            p_mva_st: None,
547            p_red_bc_st: None,
548            v_bc_st: None,
549            p_icms_st: None,
550            v_icms_st: None,
551            v_bc_fcp_st: None,
552            p_fcp_st: None,
553            v_fcp_st: None,
554            v_icms_deson: None,
555            mot_des_icms: None,
556            ind_deduz_deson: None,
557            v_icms_st_deson: None,
558            mot_des_icms_st: None,
559        };
560        assert_eq!(cst.cst_code(), "90");
561    }
562
563    // ── IcmsCsosn::csosn_code() for Csosn101 (line 1752) ────────────────
564
565    #[test]
566    fn csosn_code_101() {
567        let c = IcmsCsosn::Csosn101 {
568            orig: "0".into(),
569            csosn: "101".into(),
570            p_cred_sn: Rate(150),
571            v_cred_icms_sn: Cents(30),
572        };
573        assert_eq!(c.csosn_code(), "101");
574    }
575}