Skip to main content

fiscal_core/
tax_icms.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
17use crate::FiscalError;
18use crate::format_utils::format_cents_or_none;
19use crate::newtypes::{Cents, Rate};
20use crate::tax_element::{
21    TaxElement, TaxField, filter_fields, optional_field, required_field, serialize_tax_element,
22};
23
24/// Accumulate a value into a totals field.
25fn accum(current: Cents, value: Option<Cents>) -> Cents {
26    current + value.unwrap_or(Cents(0))
27}
28
29/// Accumulate a raw i64 quantity into a totals field.
30fn accum_raw(current: i64, value: Option<i64>) -> i64 {
31    current + value.unwrap_or(0)
32}
33
34// ── Types ───────────────────────────────────────────────────────────────────
35
36/// Unified ICMS variant wrapping both normal-regime CSTs and Simples Nacional
37/// CSOSNs. Pass one of these to [`build_icms_xml`] for compile-time-safe XML
38/// generation.
39#[derive(Debug, Clone)]
40#[non_exhaustive]
41pub enum IcmsVariant {
42    /// Normal tax regime (Lucro Real / Presumido).
43    Cst(Box<IcmsCst>),
44    /// Simples Nacional tax regime (CRT 1/2).
45    Csosn(Box<IcmsCsosn>),
46}
47
48impl From<IcmsCst> for IcmsVariant {
49    fn from(cst: IcmsCst) -> Self {
50        Self::Cst(Box::new(cst))
51    }
52}
53
54impl From<IcmsCsosn> for IcmsVariant {
55    fn from(csosn: IcmsCsosn) -> Self {
56        Self::Csosn(Box::new(csosn))
57    }
58}
59
60/// Data for building the ICMSPart XML group (ICMS partition between states).
61///
62/// Used for interstate operations where the ICMS is split between origin and
63/// destination states.
64#[derive(Debug, Clone)]
65#[non_exhaustive]
66pub struct IcmsPartData {
67    /// Product origin code (`orig`).
68    pub orig: String,
69    /// ICMS CST code.
70    pub cst: String,
71    /// Base calculation modality (`modBC`).
72    pub mod_bc: String,
73    /// ICMS calculation base value (`vBC`).
74    pub v_bc: Cents,
75    /// Base reduction rate (`pRedBC`). Optional.
76    pub p_red_bc: Option<Rate>,
77    /// ICMS rate (`pICMS`).
78    pub p_icms: Rate,
79    /// ICMS value (`vICMS`).
80    pub v_icms: Cents,
81    /// ST base calculation modality (`modBCST`).
82    pub mod_bc_st: String,
83    /// ST added value margin (`pMVAST`). Optional.
84    pub p_mva_st: Option<Rate>,
85    /// ST base reduction rate (`pRedBCST`). Optional.
86    pub p_red_bc_st: Option<Rate>,
87    /// ST calculation base value (`vBCST`).
88    pub v_bc_st: Cents,
89    /// ST rate (`pICMSST`).
90    pub p_icms_st: Rate,
91    /// ST value (`vICMSST`).
92    pub v_icms_st: Cents,
93    /// FCP-ST calculation base (`vBCFCPST`). Optional.
94    pub v_bc_fcp_st: Option<Cents>,
95    /// FCP-ST rate (`pFCPST`). Optional.
96    pub p_fcp_st: Option<Rate>,
97    /// FCP-ST value (`vFCPST`). Optional.
98    pub v_fcp_st: Option<Cents>,
99    /// Partition percentage applied at origin state (`pBCOp`).
100    pub p_bc_op: Rate,
101    /// Destination state abbreviation for ST (`UFST`).
102    pub uf_st: String,
103    /// Desonerated ICMS value (`vICMSDeson`). Optional.
104    pub v_icms_deson: Option<Cents>,
105    /// Reason code for ICMS desoneration (`motDesICMS`). Optional.
106    pub mot_des_icms: Option<String>,
107    /// Indicator whether desoneration is deducted (`indDeduzDeson`). Optional.
108    pub ind_deduz_deson: Option<String>,
109}
110
111impl IcmsPartData {
112    /// Create a new `IcmsPartData` with all required fields.
113    #[allow(clippy::too_many_arguments)]
114    pub fn new(
115        orig: impl Into<String>,
116        cst: impl Into<String>,
117        mod_bc: impl Into<String>,
118        v_bc: Cents,
119        p_icms: Rate,
120        v_icms: Cents,
121        mod_bc_st: impl Into<String>,
122        v_bc_st: Cents,
123        p_icms_st: Rate,
124        v_icms_st: Cents,
125        p_bc_op: Rate,
126        uf_st: impl Into<String>,
127    ) -> Self {
128        Self {
129            orig: orig.into(),
130            cst: cst.into(),
131            mod_bc: mod_bc.into(),
132            v_bc,
133            p_red_bc: None,
134            p_icms,
135            v_icms,
136            mod_bc_st: mod_bc_st.into(),
137            p_mva_st: None,
138            p_red_bc_st: None,
139            v_bc_st,
140            p_icms_st,
141            v_icms_st,
142            v_bc_fcp_st: None,
143            p_fcp_st: None,
144            v_fcp_st: None,
145            p_bc_op,
146            uf_st: uf_st.into(),
147            v_icms_deson: None,
148            mot_des_icms: None,
149            ind_deduz_deson: None,
150        }
151    }
152    /// Set the ICMS base reduction rate (`pRedBC`).
153    pub fn p_red_bc(mut self, v: Rate) -> Self {
154        self.p_red_bc = Some(v);
155        self
156    }
157    /// Set the ST added value margin (`pMVAST`).
158    pub fn p_mva_st(mut self, v: Rate) -> Self {
159        self.p_mva_st = Some(v);
160        self
161    }
162    /// Set the ST base reduction rate (`pRedBCST`).
163    pub fn p_red_bc_st(mut self, v: Rate) -> Self {
164        self.p_red_bc_st = Some(v);
165        self
166    }
167    /// Set the FCP-ST calculation base (`vBCFCPST`).
168    pub fn v_bc_fcp_st(mut self, v: Cents) -> Self {
169        self.v_bc_fcp_st = Some(v);
170        self
171    }
172    /// Set the FCP-ST rate (`pFCPST`).
173    pub fn p_fcp_st(mut self, v: Rate) -> Self {
174        self.p_fcp_st = Some(v);
175        self
176    }
177    /// Set the FCP-ST value (`vFCPST`).
178    pub fn v_fcp_st(mut self, v: Cents) -> Self {
179        self.v_fcp_st = Some(v);
180        self
181    }
182    /// Set the desonerated ICMS value (`vICMSDeson`).
183    pub fn v_icms_deson(mut self, v: Cents) -> Self {
184        self.v_icms_deson = Some(v);
185        self
186    }
187    /// Set the ICMS desoneration reason code (`motDesICMS`).
188    pub fn mot_des_icms(mut self, v: impl Into<String>) -> Self {
189        self.mot_des_icms = Some(v.into());
190        self
191    }
192    /// Set the desoneration deduction indicator (`indDeduzDeson`).
193    pub fn ind_deduz_deson(mut self, v: impl Into<String>) -> Self {
194        self.ind_deduz_deson = Some(v.into());
195        self
196    }
197}
198
199/// Data for building the ICMSST XML group (ST repasse).
200///
201/// Used for CST 41 or 60 operations with ST transfer (`repasse`) between states.
202#[derive(Debug, Clone)]
203#[non_exhaustive]
204pub struct IcmsStData {
205    /// Product origin code (`orig`).
206    pub orig: String,
207    /// ICMS CST code.
208    pub cst: String,
209    /// ST retained calculation base (`vBCSTRet`).
210    pub v_bc_st_ret: Cents,
211    /// ST rate applied at retention (`pST`). Optional.
212    pub p_st: Option<Rate>,
213    /// ICMS value paid by the substitutor (`vICMSSubstituto`). Optional.
214    pub v_icms_substituto: Option<Cents>,
215    /// Retained ST ICMS value (`vICMSSTRet`).
216    pub v_icms_st_ret: Cents,
217    /// FCP-ST retained calculation base (`vBCFCPSTRet`). Optional.
218    pub v_bc_fcp_st_ret: Option<Cents>,
219    /// FCP-ST retained rate (`pFCPSTRet`). Optional.
220    pub p_fcp_st_ret: Option<Rate>,
221    /// FCP-ST retained value (`vFCPSTRet`). Optional.
222    pub v_fcp_st_ret: Option<Cents>,
223    /// ST calculation base for destination state (`vBCSTDest`).
224    pub v_bc_st_dest: Cents,
225    /// ICMS ST value for destination state (`vICMSSTDest`).
226    pub v_icms_st_dest: Cents,
227    /// Effective base reduction rate (`pRedBCEfet`). Optional.
228    pub p_red_bc_efet: Option<Rate>,
229    /// Effective calculation base (`vBCEfet`). Optional.
230    pub v_bc_efet: Option<Cents>,
231    /// Effective ICMS rate (`pICMSEfet`). Optional.
232    pub p_icms_efet: Option<Rate>,
233    /// Effective ICMS value (`vICMSEfet`). Optional.
234    pub v_icms_efet: Option<Cents>,
235}
236
237impl IcmsStData {
238    /// Create a new `IcmsStData` with required fields.
239    #[allow(clippy::too_many_arguments)]
240    pub fn new(
241        orig: impl Into<String>,
242        cst: impl Into<String>,
243        v_bc_st_ret: Cents,
244        v_icms_st_ret: Cents,
245        v_bc_st_dest: Cents,
246        v_icms_st_dest: Cents,
247    ) -> Self {
248        Self {
249            orig: orig.into(),
250            cst: cst.into(),
251            v_bc_st_ret,
252            p_st: None,
253            v_icms_substituto: None,
254            v_icms_st_ret,
255            v_bc_fcp_st_ret: None,
256            p_fcp_st_ret: None,
257            v_fcp_st_ret: None,
258            v_bc_st_dest,
259            v_icms_st_dest,
260            p_red_bc_efet: None,
261            v_bc_efet: None,
262            p_icms_efet: None,
263            v_icms_efet: None,
264        }
265    }
266    /// Set the ST rate at retention (`pST`).
267    pub fn p_st(mut self, v: Rate) -> Self {
268        self.p_st = Some(v);
269        self
270    }
271    /// Set the ICMS value paid by the substitutor (`vICMSSubstituto`).
272    pub fn v_icms_substituto(mut self, v: Cents) -> Self {
273        self.v_icms_substituto = Some(v);
274        self
275    }
276    /// Set the FCP-ST retained calculation base (`vBCFCPSTRet`).
277    pub fn v_bc_fcp_st_ret(mut self, v: Cents) -> Self {
278        self.v_bc_fcp_st_ret = Some(v);
279        self
280    }
281    /// Set the FCP-ST retained rate (`pFCPSTRet`).
282    pub fn p_fcp_st_ret(mut self, v: Rate) -> Self {
283        self.p_fcp_st_ret = Some(v);
284        self
285    }
286    /// Set the FCP-ST retained value (`vFCPSTRet`).
287    pub fn v_fcp_st_ret(mut self, v: Cents) -> Self {
288        self.v_fcp_st_ret = Some(v);
289        self
290    }
291    /// Set the effective base reduction rate (`pRedBCEfet`).
292    pub fn p_red_bc_efet(mut self, v: Rate) -> Self {
293        self.p_red_bc_efet = Some(v);
294        self
295    }
296    /// Set the effective calculation base (`vBCEfet`).
297    pub fn v_bc_efet(mut self, v: Cents) -> Self {
298        self.v_bc_efet = Some(v);
299        self
300    }
301    /// Set the effective ICMS rate (`pICMSEfet`).
302    pub fn p_icms_efet(mut self, v: Rate) -> Self {
303        self.p_icms_efet = Some(v);
304        self
305    }
306    /// Set the effective ICMS value (`vICMSEfet`).
307    pub fn v_icms_efet(mut self, v: Cents) -> Self {
308        self.v_icms_efet = Some(v);
309        self
310    }
311}
312
313/// Data for building the ICMSUFDest XML group (interstate destination).
314///
315/// Represents the ICMS differential (`DIFAL`) owed to the destination state
316/// for interstate B2C operations (EC 87/2015).
317#[derive(Debug, Clone)]
318#[non_exhaustive]
319pub struct IcmsUfDestData {
320    /// ICMS calculation base for destination state (`vBCUFDest`).
321    pub v_bc_uf_dest: Cents,
322    /// FCP calculation base for destination state (`vBCFCPUFDest`). Optional.
323    pub v_bc_fcp_uf_dest: Option<Cents>,
324    /// FCP rate for destination state (`pFCPUFDest`). Optional.
325    pub p_fcp_uf_dest: Option<Rate>,
326    /// Internal ICMS rate for destination state (`pICMSUFDest`).
327    pub p_icms_uf_dest: Rate,
328    /// Interstate ICMS rate (`pICMSInter`).
329    pub p_icms_inter: Rate,
330    /// FCP value for destination state (`vFCPUFDest`). Optional.
331    pub v_fcp_uf_dest: Option<Cents>,
332    /// ICMS value destined to destination state (`vICMSUFDest`).
333    pub v_icms_uf_dest: Cents,
334    /// ICMS value to be paid to origin state (`vICMSUFRemet`). Optional.
335    pub v_icms_uf_remet: Option<Cents>,
336}
337
338impl IcmsUfDestData {
339    /// Create a new `IcmsUfDestData` with required fields.
340    pub fn new(
341        v_bc_uf_dest: Cents,
342        p_icms_uf_dest: Rate,
343        p_icms_inter: Rate,
344        v_icms_uf_dest: Cents,
345    ) -> Self {
346        Self {
347            v_bc_uf_dest,
348            v_bc_fcp_uf_dest: None,
349            p_fcp_uf_dest: None,
350            p_icms_uf_dest,
351            p_icms_inter,
352            v_fcp_uf_dest: None,
353            v_icms_uf_dest,
354            v_icms_uf_remet: None,
355        }
356    }
357    /// Set the FCP base value for destination.
358    pub fn v_bc_fcp_uf_dest(mut self, v: Cents) -> Self {
359        self.v_bc_fcp_uf_dest = Some(v);
360        self
361    }
362    /// Set the FCP rate for destination.
363    pub fn p_fcp_uf_dest(mut self, v: Rate) -> Self {
364        self.p_fcp_uf_dest = Some(v);
365        self
366    }
367    /// Set the FCP value for destination.
368    pub fn v_fcp_uf_dest(mut self, v: Cents) -> Self {
369        self.v_fcp_uf_dest = Some(v);
370        self
371    }
372    /// Set the ICMS value for origin state.
373    pub fn v_icms_uf_remet(mut self, v: Cents) -> Self {
374        self.v_icms_uf_remet = Some(v);
375        self
376    }
377}
378
379/// Accumulated ICMS totals across all NF-e items.
380///
381/// This struct is filled incrementally by [`build_icms_xml`] /
382/// [`build_icms_cst_xml`] / [`build_icms_csosn_xml`] as each item is
383/// processed, then passed to the XML builder to generate the `<ICMSTot>`
384/// element. Start with [`IcmsTotals::new`] (or [`create_icms_totals`]) and
385/// use [`merge_icms_totals`] when accumulating per-item sub-totals.
386#[derive(Debug, Clone, Default, PartialEq, Eq)]
387#[non_exhaustive]
388pub struct IcmsTotals {
389    /// Total ICMS calculation base (`vBC`).
390    pub v_bc: Cents,
391    /// Total ICMS value (`vICMS`).
392    pub v_icms: Cents,
393    /// Total desonerated ICMS value (`vICMSDeson`).
394    pub v_icms_deson: Cents,
395    /// Total ST calculation base (`vBCST`).
396    pub v_bc_st: Cents,
397    /// Total ST ICMS value (`vST`).
398    pub v_st: Cents,
399    /// Total FCP value (`vFCP`).
400    pub v_fcp: Cents,
401    /// Total FCP-ST value (`vFCPST`).
402    pub v_fcp_st: Cents,
403    /// Total retained FCP-ST value (`vFCPSTRet`).
404    pub v_fcp_st_ret: Cents,
405    /// Total FCP value for destination state (`vFCPUFDest`).
406    pub v_fcp_uf_dest: Cents,
407    /// Total ICMS value for destination state (`vICMSUFDest`).
408    pub v_icms_uf_dest: Cents,
409    /// Total ICMS value for origin state / remitter (`vICMSUFRemet`).
410    pub v_icms_uf_remet: Cents,
411    /// Total monophasic calculation base quantity (`qBCMono`).
412    pub q_bc_mono: i64,
413    /// Total monophasic ICMS value (`vICMSMono`).
414    pub v_icms_mono: Cents,
415    /// Total monophasic retained calculation base quantity (`qBCMonoReten`).
416    pub q_bc_mono_reten: i64,
417    /// Total monophasic retained ICMS value (`vICMSMonoReten`).
418    pub v_icms_mono_reten: Cents,
419    /// Total monophasic previously-collected calculation base quantity (`qBCMonoRet`).
420    pub q_bc_mono_ret: i64,
421    /// Total monophasic previously-collected ICMS value (`vICMSMonoRet`).
422    pub v_icms_mono_ret: Cents,
423}
424
425impl IcmsTotals {
426    /// Create a new zeroed-out `IcmsTotals`.
427    pub fn new() -> Self {
428        Self::default()
429    }
430    /// Set the total ICMS calculation base (`vBC`).
431    pub fn v_bc(mut self, v: Cents) -> Self {
432        self.v_bc = v;
433        self
434    }
435    /// Set the total ICMS value (`vICMS`).
436    pub fn v_icms(mut self, v: Cents) -> Self {
437        self.v_icms = v;
438        self
439    }
440    /// Set the total desonerated ICMS value (`vICMSDeson`).
441    pub fn v_icms_deson(mut self, v: Cents) -> Self {
442        self.v_icms_deson = v;
443        self
444    }
445    /// Set the total ST calculation base (`vBCST`).
446    pub fn v_bc_st(mut self, v: Cents) -> Self {
447        self.v_bc_st = v;
448        self
449    }
450    /// Set the total ST ICMS value (`vST`).
451    pub fn v_st(mut self, v: Cents) -> Self {
452        self.v_st = v;
453        self
454    }
455    /// Set the total FCP value (`vFCP`).
456    pub fn v_fcp(mut self, v: Cents) -> Self {
457        self.v_fcp = v;
458        self
459    }
460    /// Set the total FCP-ST value (`vFCPST`).
461    pub fn v_fcp_st(mut self, v: Cents) -> Self {
462        self.v_fcp_st = v;
463        self
464    }
465    /// Set the total retained FCP-ST value (`vFCPSTRet`).
466    pub fn v_fcp_st_ret(mut self, v: Cents) -> Self {
467        self.v_fcp_st_ret = v;
468        self
469    }
470    /// Set the total FCP value for destination state (`vFCPUFDest`).
471    pub fn v_fcp_uf_dest(mut self, v: Cents) -> Self {
472        self.v_fcp_uf_dest = v;
473        self
474    }
475    /// Set the total ICMS value for destination state (`vICMSUFDest`).
476    pub fn v_icms_uf_dest(mut self, v: Cents) -> Self {
477        self.v_icms_uf_dest = v;
478        self
479    }
480    /// Set the total ICMS value for origin state (`vICMSUFRemet`).
481    pub fn v_icms_uf_remet(mut self, v: Cents) -> Self {
482        self.v_icms_uf_remet = v;
483        self
484    }
485    /// Set the total monophasic ICMS value (`vICMSMono`).
486    pub fn v_icms_mono(mut self, v: Cents) -> Self {
487        self.v_icms_mono = v;
488        self
489    }
490    /// Set the total monophasic retained ICMS value (`vICMSMonoReten`).
491    pub fn v_icms_mono_reten(mut self, v: Cents) -> Self {
492        self.v_icms_mono_reten = v;
493        self
494    }
495    /// Set the total monophasic previously-collected ICMS value (`vICMSMonoRet`).
496    pub fn v_icms_mono_ret(mut self, v: Cents) -> Self {
497        self.v_icms_mono_ret = v;
498        self
499    }
500}
501
502/// Create a zeroed-out [`IcmsTotals`] accumulator.
503///
504/// Equivalent to `IcmsTotals::new()`. Provided as a free function for
505/// ergonomic use in XML builder pipelines.
506///
507/// # Examples
508///
509/// ```
510/// use fiscal_core::tax_icms::create_icms_totals;
511/// let totals = create_icms_totals();
512/// use fiscal_core::newtypes::Cents;
513/// assert_eq!(totals.v_bc, Cents(0));
514/// ```
515pub fn create_icms_totals() -> IcmsTotals {
516    IcmsTotals::default()
517}
518
519// ── IcmsCst enum (normal regime) ────────────────────────────────────────────
520
521/// ICMS CST variant for normal tax regime (Lucro Real / Presumido).
522///
523/// Each variant carries **only** the fields that are valid for that CST,
524/// giving compile-time safety instead of runtime string matching against a
525/// flat struct full of `Option`s.
526///
527/// Simples Nacional / CSOSN variants are **not** included here (see R7).
528#[derive(Debug, Clone)]
529#[non_exhaustive]
530pub enum IcmsCst {
531    /// CST 00 — Tributada integralmente.
532    Cst00 {
533        /// Product origin code (`orig`).
534        orig: String,
535        /// Base calculation modality (`modBC`).
536        mod_bc: String,
537        /// Calculation base value (`vBC`).
538        v_bc: Cents,
539        /// ICMS rate (`pICMS`).
540        p_icms: Rate,
541        /// ICMS value (`vICMS`).
542        v_icms: Cents,
543        /// FCP rate (`pFCP`). Optional.
544        p_fcp: Option<Rate>,
545        /// FCP value (`vFCP`). Optional.
546        v_fcp: Option<Cents>,
547    },
548    /// CST 02 — Tributacao monofasica propria sobre combustiveis.
549    Cst02 {
550        /// Product origin code (`orig`).
551        orig: String,
552        /// Monophasic calculation base quantity (`qBCMono`). Optional.
553        q_bc_mono: Option<i64>,
554        /// Monophasic ad-rem ICMS rate (`adRemICMS`).
555        ad_rem_icms: Rate,
556        /// Monophasic ICMS value (`vICMSMono`).
557        v_icms_mono: Cents,
558    },
559    /// CST 10 — Tributada e com cobranca do ICMS por substituicao tributaria.
560    Cst10 {
561        /// Product origin code (`orig`).
562        orig: String,
563        /// Base calculation modality (`modBC`).
564        mod_bc: String,
565        /// ICMS calculation base value (`vBC`).
566        v_bc: Cents,
567        /// ICMS rate (`pICMS`).
568        p_icms: Rate,
569        /// ICMS value (`vICMS`).
570        v_icms: Cents,
571        /// FCP calculation base (`vBCFCP`). Optional.
572        v_bc_fcp: Option<Cents>,
573        /// FCP rate (`pFCP`). Optional.
574        p_fcp: Option<Rate>,
575        /// FCP value (`vFCP`). Optional.
576        v_fcp: Option<Cents>,
577        /// ST base calculation modality (`modBCST`).
578        mod_bc_st: String,
579        /// ST added value margin (`pMVAST`). Optional.
580        p_mva_st: Option<Rate>,
581        /// ST base reduction rate (`pRedBCST`). Optional.
582        p_red_bc_st: Option<Rate>,
583        /// ST calculation base value (`vBCST`).
584        v_bc_st: Cents,
585        /// ST rate (`pICMSST`).
586        p_icms_st: Rate,
587        /// ST ICMS value (`vICMSST`).
588        v_icms_st: Cents,
589        /// FCP-ST calculation base (`vBCFCPST`). Optional.
590        v_bc_fcp_st: Option<Cents>,
591        /// FCP-ST rate (`pFCPST`). Optional.
592        p_fcp_st: Option<Rate>,
593        /// FCP-ST value (`vFCPST`). Optional.
594        v_fcp_st: Option<Cents>,
595        /// ST desonerated ICMS value (`vICMSSTDeson`). Optional.
596        v_icms_st_deson: Option<Cents>,
597        /// ST desoneration reason code (`motDesICMSST`). Optional.
598        mot_des_icms_st: Option<String>,
599    },
600    /// CST 15 — Tributacao monofasica propria e com responsabilidade pela
601    /// retencao sobre combustiveis.
602    Cst15 {
603        /// Product origin code (`orig`).
604        orig: String,
605        /// Monophasic calculation base quantity (`qBCMono`). Optional.
606        q_bc_mono: Option<i64>,
607        /// Monophasic ad-rem ICMS rate (`adRemICMS`).
608        ad_rem_icms: Rate,
609        /// Monophasic ICMS value (`vICMSMono`).
610        v_icms_mono: Cents,
611        /// Retained monophasic calculation base quantity (`qBCMonoReten`). Optional.
612        q_bc_mono_reten: Option<i64>,
613        /// Retained monophasic ad-rem ICMS rate (`adRemICMSReten`).
614        ad_rem_icms_reten: Rate,
615        /// Retained monophasic ICMS value (`vICMSMonoReten`).
616        v_icms_mono_reten: Cents,
617        /// Ad-rem reduction rate (`pRedAdRem`). Optional.
618        p_red_ad_rem: Option<Rate>,
619        /// Ad-rem reduction reason (`motRedAdRem`). Required when `p_red_ad_rem` is set.
620        mot_red_ad_rem: Option<String>,
621    },
622    /// CST 20 — Com reducao de base de calculo.
623    Cst20 {
624        /// Product origin code (`orig`).
625        orig: String,
626        /// Base calculation modality (`modBC`).
627        mod_bc: String,
628        /// Base reduction rate (`pRedBC`).
629        p_red_bc: Rate,
630        /// Calculation base value (`vBC`).
631        v_bc: Cents,
632        /// ICMS rate (`pICMS`).
633        p_icms: Rate,
634        /// ICMS value (`vICMS`).
635        v_icms: Cents,
636        /// FCP calculation base (`vBCFCP`). Optional.
637        v_bc_fcp: Option<Cents>,
638        /// FCP rate (`pFCP`). Optional.
639        p_fcp: Option<Rate>,
640        /// FCP value (`vFCP`). Optional.
641        v_fcp: Option<Cents>,
642        /// Desonerated ICMS value (`vICMSDeson`). Optional.
643        v_icms_deson: Option<Cents>,
644        /// Desoneration reason code (`motDesICMS`). Optional.
645        mot_des_icms: Option<String>,
646        /// Desoneration deduction indicator (`indDeduzDeson`). Optional.
647        ind_deduz_deson: Option<String>,
648    },
649    /// CST 30 — Isenta ou nao tributada e com cobranca do ICMS por ST.
650    Cst30 {
651        /// Product origin code (`orig`).
652        orig: String,
653        /// ST base calculation modality (`modBCST`).
654        mod_bc_st: String,
655        /// ST added value margin (`pMVAST`). Optional.
656        p_mva_st: Option<Rate>,
657        /// ST base reduction rate (`pRedBCST`). Optional.
658        p_red_bc_st: Option<Rate>,
659        /// ST calculation base value (`vBCST`).
660        v_bc_st: Cents,
661        /// ST rate (`pICMSST`).
662        p_icms_st: Rate,
663        /// ST ICMS value (`vICMSST`).
664        v_icms_st: Cents,
665        /// FCP-ST calculation base (`vBCFCPST`). Optional.
666        v_bc_fcp_st: Option<Cents>,
667        /// FCP-ST rate (`pFCPST`). Optional.
668        p_fcp_st: Option<Rate>,
669        /// FCP-ST value (`vFCPST`). Optional.
670        v_fcp_st: Option<Cents>,
671        /// Desonerated ICMS value (`vICMSDeson`). Optional.
672        v_icms_deson: Option<Cents>,
673        /// Desoneration reason code (`motDesICMS`). Optional.
674        mot_des_icms: Option<String>,
675        /// Desoneration deduction indicator (`indDeduzDeson`). Optional.
676        ind_deduz_deson: Option<String>,
677    },
678    /// CST 40 — Isenta.
679    Cst40 {
680        /// Product origin code (`orig`).
681        orig: String,
682        /// Desonerated ICMS value (`vICMSDeson`). Optional.
683        v_icms_deson: Option<Cents>,
684        /// Desoneration reason code (`motDesICMS`). Optional.
685        mot_des_icms: Option<String>,
686        /// Desoneration deduction indicator (`indDeduzDeson`). Optional.
687        ind_deduz_deson: Option<String>,
688    },
689    /// CST 41 — Nao tributada.
690    Cst41 {
691        /// Product origin code (`orig`).
692        orig: String,
693        /// Desonerated ICMS value (`vICMSDeson`). Optional.
694        v_icms_deson: Option<Cents>,
695        /// Desoneration reason code (`motDesICMS`). Optional.
696        mot_des_icms: Option<String>,
697        /// Desoneration deduction indicator (`indDeduzDeson`). Optional.
698        ind_deduz_deson: Option<String>,
699    },
700    /// CST 50 — Suspensao.
701    Cst50 {
702        /// Product origin code (`orig`).
703        orig: String,
704        /// Desonerated ICMS value (`vICMSDeson`). Optional.
705        v_icms_deson: Option<Cents>,
706        /// Desoneration reason code (`motDesICMS`). Optional.
707        mot_des_icms: Option<String>,
708        /// Desoneration deduction indicator (`indDeduzDeson`). Optional.
709        ind_deduz_deson: Option<String>,
710    },
711    /// CST 51 — Diferimento.
712    Cst51 {
713        /// Product origin code (`orig`).
714        orig: String,
715        /// Base calculation modality (`modBC`). Optional.
716        mod_bc: Option<String>,
717        /// Base reduction rate (`pRedBC`). Optional.
718        p_red_bc: Option<Rate>,
719        /// Fiscal benefit code for base reduction (`cBenefRBC`). Optional.
720        c_benef_rbc: Option<String>,
721        /// Calculation base value (`vBC`). Optional.
722        v_bc: Option<Cents>,
723        /// ICMS rate (`pICMS`). Optional.
724        p_icms: Option<Rate>,
725        /// ICMS value before deferral (`vICMSOp`). Optional.
726        v_icms_op: Option<Cents>,
727        /// Deferral percentage (`pDif`). Optional.
728        p_dif: Option<Rate>,
729        /// Deferred ICMS value (`vICMSDif`). Optional.
730        v_icms_dif: Option<Cents>,
731        /// ICMS value payable after deferral (`vICMS`). Optional.
732        v_icms: Option<Cents>,
733        /// FCP calculation base (`vBCFCP`). Optional.
734        v_bc_fcp: Option<Cents>,
735        /// FCP rate (`pFCP`). Optional.
736        p_fcp: Option<Rate>,
737        /// FCP value (`vFCP`). Optional.
738        v_fcp: Option<Cents>,
739        /// FCP deferral rate (`pFCPDif`). Optional.
740        p_fcp_dif: Option<Rate>,
741        /// FCP deferred value (`vFCPDif`). Optional.
742        v_fcp_dif: Option<Cents>,
743        /// FCP effective value after deferral (`vFCPEfet`). Optional.
744        v_fcp_efet: Option<Cents>,
745    },
746    /// CST 53 — Tributacao monofasica sobre combustiveis com recolhimento
747    /// diferido.
748    Cst53 {
749        /// Product origin code (`orig`).
750        orig: String,
751        /// Monophasic calculation base quantity (`qBCMono`). Optional.
752        q_bc_mono: Option<i64>,
753        /// Monophasic ad-rem ICMS rate (`adRemICMS`). Optional.
754        ad_rem_icms: Option<Rate>,
755        /// Monophasic ICMS value before deferral (`vICMSMonoOp`). Optional.
756        v_icms_mono_op: Option<Cents>,
757        /// Deferral percentage (`pDif`). Optional.
758        p_dif: Option<Rate>,
759        /// Deferred monophasic ICMS value (`vICMSMonoDif`). Optional.
760        v_icms_mono_dif: Option<Cents>,
761        /// Monophasic ICMS value payable after deferral (`vICMSMono`). Optional.
762        v_icms_mono: Option<Cents>,
763    },
764    /// CST 60 — ICMS cobrado anteriormente por substituicao tributaria.
765    Cst60 {
766        /// Product origin code (`orig`).
767        orig: String,
768        /// ST retained calculation base (`vBCSTRet`). Optional.
769        v_bc_st_ret: Option<Cents>,
770        /// ST rate at retention (`pST`). Optional.
771        p_st: Option<Rate>,
772        /// ICMS value paid by the substitutor (`vICMSSubstituto`). Optional.
773        v_icms_substituto: Option<Cents>,
774        /// Retained ST ICMS value (`vICMSSTRet`). Optional.
775        v_icms_st_ret: Option<Cents>,
776        /// FCP-ST retained calculation base (`vBCFCPSTRet`). Optional.
777        v_bc_fcp_st_ret: Option<Cents>,
778        /// FCP-ST retained rate (`pFCPSTRet`). Optional.
779        p_fcp_st_ret: Option<Rate>,
780        /// FCP-ST retained value (`vFCPSTRet`). Optional.
781        v_fcp_st_ret: Option<Cents>,
782        /// Effective base reduction rate (`pRedBCEfet`). Optional.
783        p_red_bc_efet: Option<Rate>,
784        /// Effective calculation base (`vBCEfet`). Optional.
785        v_bc_efet: Option<Cents>,
786        /// Effective ICMS rate (`pICMSEfet`). Optional.
787        p_icms_efet: Option<Rate>,
788        /// Effective ICMS value (`vICMSEfet`). Optional.
789        v_icms_efet: Option<Cents>,
790    },
791    /// CST 61 — Tributacao monofasica sobre combustiveis cobrada anteriormente.
792    Cst61 {
793        /// Product origin code (`orig`).
794        orig: String,
795        /// Monophasic previously-collected calculation base quantity (`qBCMonoRet`). Optional.
796        q_bc_mono_ret: Option<i64>,
797        /// Monophasic previously-collected ad-rem ICMS rate (`adRemICMSRet`).
798        ad_rem_icms_ret: Rate,
799        /// Monophasic previously-collected ICMS value (`vICMSMonoRet`).
800        v_icms_mono_ret: Cents,
801    },
802    /// CST 70 — Reducao de base de calculo e cobranca do ICMS por ST.
803    Cst70 {
804        /// Product origin code (`orig`).
805        orig: String,
806        /// Base calculation modality (`modBC`).
807        mod_bc: String,
808        /// Base reduction rate (`pRedBC`).
809        p_red_bc: Rate,
810        /// ICMS calculation base value (`vBC`).
811        v_bc: Cents,
812        /// ICMS rate (`pICMS`).
813        p_icms: Rate,
814        /// ICMS value (`vICMS`).
815        v_icms: Cents,
816        /// FCP calculation base (`vBCFCP`). Optional.
817        v_bc_fcp: Option<Cents>,
818        /// FCP rate (`pFCP`). Optional.
819        p_fcp: Option<Rate>,
820        /// FCP value (`vFCP`). Optional.
821        v_fcp: Option<Cents>,
822        /// ST base calculation modality (`modBCST`).
823        mod_bc_st: String,
824        /// ST added value margin (`pMVAST`). Optional.
825        p_mva_st: Option<Rate>,
826        /// ST base reduction rate (`pRedBCST`). Optional.
827        p_red_bc_st: Option<Rate>,
828        /// ST calculation base value (`vBCST`).
829        v_bc_st: Cents,
830        /// ST rate (`pICMSST`).
831        p_icms_st: Rate,
832        /// ST ICMS value (`vICMSST`).
833        v_icms_st: Cents,
834        /// FCP-ST calculation base (`vBCFCPST`). Optional.
835        v_bc_fcp_st: Option<Cents>,
836        /// FCP-ST rate (`pFCPST`). Optional.
837        p_fcp_st: Option<Rate>,
838        /// FCP-ST value (`vFCPST`). Optional.
839        v_fcp_st: Option<Cents>,
840        /// Desonerated ICMS value (`vICMSDeson`). Optional.
841        v_icms_deson: Option<Cents>,
842        /// Desoneration reason code (`motDesICMS`). Optional.
843        mot_des_icms: Option<String>,
844        /// Desoneration deduction indicator (`indDeduzDeson`). Optional.
845        ind_deduz_deson: Option<String>,
846        /// ST desonerated ICMS value (`vICMSSTDeson`). Optional.
847        v_icms_st_deson: Option<Cents>,
848        /// ST desoneration reason code (`motDesICMSST`). Optional.
849        mot_des_icms_st: Option<String>,
850    },
851    /// CST 90 — Outros.
852    Cst90 {
853        /// Product origin code (`orig`).
854        orig: String,
855        /// Base calculation modality (`modBC`). Optional.
856        mod_bc: Option<String>,
857        /// ICMS calculation base value (`vBC`). Optional.
858        v_bc: Option<Cents>,
859        /// Base reduction rate (`pRedBC`). Optional.
860        p_red_bc: Option<Rate>,
861        /// Fiscal benefit code for base reduction (`cBenefRBC`). Optional.
862        c_benef_rbc: Option<String>,
863        /// ICMS rate (`pICMS`). Optional.
864        p_icms: Option<Rate>,
865        /// ICMS value before deferral (`vICMSOp`). Optional.
866        v_icms_op: Option<Cents>,
867        /// Deferral percentage (`pDif`). Optional.
868        p_dif: Option<Rate>,
869        /// Deferred ICMS value (`vICMSDif`). Optional.
870        v_icms_dif: Option<Cents>,
871        /// ICMS value (`vICMS`). Optional.
872        v_icms: Option<Cents>,
873        /// FCP calculation base (`vBCFCP`). Optional.
874        v_bc_fcp: Option<Cents>,
875        /// FCP rate (`pFCP`). Optional.
876        p_fcp: Option<Rate>,
877        /// FCP value (`vFCP`). Optional.
878        v_fcp: Option<Cents>,
879        /// FCP deferral rate (`pFCPDif`). Optional.
880        p_fcp_dif: Option<Rate>,
881        /// FCP deferred value (`vFCPDif`). Optional.
882        v_fcp_dif: Option<Cents>,
883        /// FCP effective value (`vFCPEfet`). Optional.
884        v_fcp_efet: Option<Cents>,
885        /// ST base calculation modality (`modBCST`). Optional.
886        mod_bc_st: Option<String>,
887        /// ST added value margin (`pMVAST`). Optional.
888        p_mva_st: Option<Rate>,
889        /// ST base reduction rate (`pRedBCST`). Optional.
890        p_red_bc_st: Option<Rate>,
891        /// ST calculation base value (`vBCST`). Optional.
892        v_bc_st: Option<Cents>,
893        /// ST rate (`pICMSST`). Optional.
894        p_icms_st: Option<Rate>,
895        /// ST ICMS value (`vICMSST`). Optional.
896        v_icms_st: Option<Cents>,
897        /// FCP-ST calculation base (`vBCFCPST`). Optional.
898        v_bc_fcp_st: Option<Cents>,
899        /// FCP-ST rate (`pFCPST`). Optional.
900        p_fcp_st: Option<Rate>,
901        /// FCP-ST value (`vFCPST`). Optional.
902        v_fcp_st: Option<Cents>,
903        /// Desonerated ICMS value (`vICMSDeson`). Optional.
904        v_icms_deson: Option<Cents>,
905        /// Desoneration reason code (`motDesICMS`). Optional.
906        mot_des_icms: Option<String>,
907        /// Desoneration deduction indicator (`indDeduzDeson`). Optional.
908        ind_deduz_deson: Option<String>,
909        /// ST desonerated ICMS value (`vICMSSTDeson`). Optional.
910        v_icms_st_deson: Option<Cents>,
911        /// ST desoneration reason code (`motDesICMSST`). Optional.
912        mot_des_icms_st: Option<String>,
913    },
914}
915
916impl IcmsCst {
917    /// Return the two-character CST code for this variant.
918    pub fn cst_code(&self) -> &str {
919        match self {
920            Self::Cst00 { .. } => "00",
921            Self::Cst02 { .. } => "02",
922            Self::Cst10 { .. } => "10",
923            Self::Cst15 { .. } => "15",
924            Self::Cst20 { .. } => "20",
925            Self::Cst30 { .. } => "30",
926            Self::Cst40 { .. } => "40",
927            Self::Cst41 { .. } => "41",
928            Self::Cst50 { .. } => "50",
929            Self::Cst51 { .. } => "51",
930            Self::Cst53 { .. } => "53",
931            Self::Cst60 { .. } => "60",
932            Self::Cst61 { .. } => "61",
933            Self::Cst70 { .. } => "70",
934            Self::Cst90 { .. } => "90",
935        }
936    }
937}
938
939/// Build the ICMS XML fragment and accumulate totals from a typed [`IcmsCst`]
940/// variant.
941///
942/// This is the compile-time-safe counterpart of the original
943/// [`build_icms_xml`] code path for normal-regime CSTs. It can be used
944/// directly by new code that already has an `IcmsCst`, or indirectly via the
945/// unchanged [`build_icms_xml`] public API (which converts internally).
946///
947/// # Errors
948///
949/// Returns [`FiscalError`] if XML field serialization fails (should not happen
950/// when the enum is correctly constructed).
951pub fn build_icms_cst_xml(
952    cst: &IcmsCst,
953    totals: &mut IcmsTotals,
954) -> Result<(String, Vec<TaxField>), FiscalError> {
955    match cst {
956        IcmsCst::Cst00 {
957            orig,
958            mod_bc,
959            v_bc,
960            p_icms,
961            v_icms,
962            p_fcp,
963            v_fcp,
964        } => {
965            totals.v_bc = accum(totals.v_bc, Some(*v_bc));
966            totals.v_icms = accum(totals.v_icms, Some(*v_icms));
967            totals.v_fcp = accum(totals.v_fcp, *v_fcp);
968            let fields = filter_fields(vec![
969                Some(TaxField::new("orig", orig.as_str())),
970                Some(TaxField::new("CST", "00")),
971                Some(TaxField::new("modBC", mod_bc.as_str())),
972                Some(TaxField::new("vBC", fc2(Some(*v_bc)).unwrap())),
973                Some(TaxField::new("pICMS", fc4(Some(*p_icms)).unwrap())),
974                Some(TaxField::new("vICMS", fc2(Some(*v_icms)).unwrap())),
975                optional_field("pFCP", fc4(*p_fcp).as_deref()),
976                optional_field("vFCP", fc2(*v_fcp).as_deref()),
977            ]);
978            Ok(("ICMS00".to_string(), fields))
979        }
980
981        IcmsCst::Cst02 {
982            orig,
983            q_bc_mono,
984            ad_rem_icms,
985            v_icms_mono,
986        } => {
987            totals.q_bc_mono = accum_raw(totals.q_bc_mono, *q_bc_mono);
988            totals.v_icms_mono = accum(totals.v_icms_mono, Some(*v_icms_mono));
989            let fields = filter_fields(vec![
990                Some(TaxField::new("orig", orig.as_str())),
991                Some(TaxField::new("CST", "02")),
992                optional_field("qBCMono", fc4_raw(*q_bc_mono).as_deref()),
993                Some(TaxField::new("adRemICMS", fc4(Some(*ad_rem_icms)).unwrap())),
994                Some(TaxField::new("vICMSMono", fc2(Some(*v_icms_mono)).unwrap())),
995            ]);
996            Ok(("ICMS02".to_string(), fields))
997        }
998
999        IcmsCst::Cst10 {
1000            orig,
1001            mod_bc,
1002            v_bc,
1003            p_icms,
1004            v_icms,
1005            v_bc_fcp,
1006            p_fcp,
1007            v_fcp,
1008            mod_bc_st,
1009            p_mva_st,
1010            p_red_bc_st,
1011            v_bc_st,
1012            p_icms_st,
1013            v_icms_st,
1014            v_bc_fcp_st,
1015            p_fcp_st,
1016            v_fcp_st,
1017            v_icms_st_deson,
1018            mot_des_icms_st,
1019        } => {
1020            totals.v_bc = accum(totals.v_bc, Some(*v_bc));
1021            totals.v_icms = accum(totals.v_icms, Some(*v_icms));
1022            totals.v_bc_st = accum(totals.v_bc_st, Some(*v_bc_st));
1023            totals.v_st = accum(totals.v_st, Some(*v_icms_st));
1024            totals.v_fcp_st = accum(totals.v_fcp_st, *v_fcp_st);
1025            totals.v_fcp = accum(totals.v_fcp, *v_fcp);
1026            let mut fields_opt: Vec<Option<TaxField>> = vec![
1027                Some(TaxField::new("orig", orig.as_str())),
1028                Some(TaxField::new("CST", "10")),
1029                Some(TaxField::new("modBC", mod_bc.as_str())),
1030                Some(TaxField::new("vBC", fc2(Some(*v_bc)).unwrap())),
1031                Some(TaxField::new("pICMS", fc4(Some(*p_icms)).unwrap())),
1032                Some(TaxField::new("vICMS", fc2(Some(*v_icms)).unwrap())),
1033            ];
1034            // FCP fields
1035            fields_opt.push(optional_field("vBCFCP", fc2(*v_bc_fcp).as_deref()));
1036            fields_opt.push(optional_field("pFCP", fc4(*p_fcp).as_deref()));
1037            fields_opt.push(optional_field("vFCP", fc2(*v_fcp).as_deref()));
1038            // ST fields
1039            fields_opt.push(Some(TaxField::new("modBCST", mod_bc_st.as_str())));
1040            if let Some(v) = p_mva_st {
1041                fields_opt.push(Some(TaxField::new("pMVAST", fc4(Some(*v)).unwrap())));
1042            }
1043            if let Some(v) = p_red_bc_st {
1044                fields_opt.push(Some(TaxField::new("pRedBCST", fc4(Some(*v)).unwrap())));
1045            }
1046            fields_opt.push(Some(TaxField::new("vBCST", fc2(Some(*v_bc_st)).unwrap())));
1047            fields_opt.push(Some(TaxField::new(
1048                "pICMSST",
1049                fc4(Some(*p_icms_st)).unwrap(),
1050            )));
1051            fields_opt.push(Some(TaxField::new(
1052                "vICMSST",
1053                fc2(Some(*v_icms_st)).unwrap(),
1054            )));
1055            // FCP ST fields
1056            fields_opt.push(optional_field("vBCFCPST", fc2(*v_bc_fcp_st).as_deref()));
1057            fields_opt.push(optional_field("pFCPST", fc4(*p_fcp_st).as_deref()));
1058            fields_opt.push(optional_field("vFCPST", fc2(*v_fcp_st).as_deref()));
1059            // ST desoneration
1060            fields_opt.push(optional_field(
1061                "vICMSSTDeson",
1062                fc2(*v_icms_st_deson).as_deref(),
1063            ));
1064            fields_opt.push(optional_field("motDesICMSST", mot_des_icms_st.as_deref()));
1065            Ok(("ICMS10".to_string(), filter_fields(fields_opt)))
1066        }
1067
1068        IcmsCst::Cst15 {
1069            orig,
1070            q_bc_mono,
1071            ad_rem_icms,
1072            v_icms_mono,
1073            q_bc_mono_reten,
1074            ad_rem_icms_reten,
1075            v_icms_mono_reten,
1076            p_red_ad_rem,
1077            mot_red_ad_rem,
1078        } => {
1079            totals.q_bc_mono = accum_raw(totals.q_bc_mono, *q_bc_mono);
1080            totals.v_icms_mono = accum(totals.v_icms_mono, Some(*v_icms_mono));
1081            totals.q_bc_mono_reten = accum_raw(totals.q_bc_mono_reten, *q_bc_mono_reten);
1082            totals.v_icms_mono_reten = accum(totals.v_icms_mono_reten, Some(*v_icms_mono_reten));
1083            let mut fields = filter_fields(vec![
1084                Some(TaxField::new("orig", orig.as_str())),
1085                Some(TaxField::new("CST", "15")),
1086                optional_field("qBCMono", fc4_raw(*q_bc_mono).as_deref()),
1087                Some(TaxField::new("adRemICMS", fc4(Some(*ad_rem_icms)).unwrap())),
1088                Some(TaxField::new("vICMSMono", fc2(Some(*v_icms_mono)).unwrap())),
1089                optional_field("qBCMonoReten", fc4_raw(*q_bc_mono_reten).as_deref()),
1090                Some(TaxField::new(
1091                    "adRemICMSReten",
1092                    fc4(Some(*ad_rem_icms_reten)).unwrap(),
1093                )),
1094                Some(TaxField::new(
1095                    "vICMSMonoReten",
1096                    fc2(Some(*v_icms_mono_reten)).unwrap(),
1097                )),
1098            ]);
1099            if p_red_ad_rem.is_some() {
1100                fields.push(TaxField::new("pRedAdRem", fc4(*p_red_ad_rem).unwrap()));
1101                fields.push(required_field("motRedAdRem", mot_red_ad_rem.as_deref())?);
1102            }
1103            Ok(("ICMS15".to_string(), fields))
1104        }
1105
1106        IcmsCst::Cst20 {
1107            orig,
1108            mod_bc,
1109            p_red_bc,
1110            v_bc,
1111            p_icms,
1112            v_icms,
1113            v_bc_fcp,
1114            p_fcp,
1115            v_fcp,
1116            v_icms_deson,
1117            mot_des_icms,
1118            ind_deduz_deson,
1119        } => {
1120            totals.v_icms_deson = accum(totals.v_icms_deson, *v_icms_deson);
1121            totals.v_bc = accum(totals.v_bc, Some(*v_bc));
1122            totals.v_icms = accum(totals.v_icms, Some(*v_icms));
1123            totals.v_fcp = accum(totals.v_fcp, *v_fcp);
1124            let mut fields_opt: Vec<Option<TaxField>> = vec![
1125                Some(TaxField::new("orig", orig.as_str())),
1126                Some(TaxField::new("CST", "20")),
1127                Some(TaxField::new("modBC", mod_bc.as_str())),
1128                Some(TaxField::new("pRedBC", fc4(Some(*p_red_bc)).unwrap())),
1129                Some(TaxField::new("vBC", fc2(Some(*v_bc)).unwrap())),
1130                Some(TaxField::new("pICMS", fc4(Some(*p_icms)).unwrap())),
1131                Some(TaxField::new("vICMS", fc2(Some(*v_icms)).unwrap())),
1132            ];
1133            // FCP fields
1134            fields_opt.push(optional_field("vBCFCP", fc2(*v_bc_fcp).as_deref()));
1135            fields_opt.push(optional_field("pFCP", fc4(*p_fcp).as_deref()));
1136            fields_opt.push(optional_field("vFCP", fc2(*v_fcp).as_deref()));
1137            // Desoneration
1138            fields_opt.push(optional_field("vICMSDeson", fc2(*v_icms_deson).as_deref()));
1139            fields_opt.push(optional_field("motDesICMS", mot_des_icms.as_deref()));
1140            fields_opt.push(optional_field("indDeduzDeson", ind_deduz_deson.as_deref()));
1141            Ok(("ICMS20".to_string(), filter_fields(fields_opt)))
1142        }
1143
1144        IcmsCst::Cst30 {
1145            orig,
1146            mod_bc_st,
1147            p_mva_st,
1148            p_red_bc_st,
1149            v_bc_st,
1150            p_icms_st,
1151            v_icms_st,
1152            v_bc_fcp_st,
1153            p_fcp_st,
1154            v_fcp_st,
1155            v_icms_deson,
1156            mot_des_icms,
1157            ind_deduz_deson,
1158        } => {
1159            totals.v_icms_deson = accum(totals.v_icms_deson, *v_icms_deson);
1160            totals.v_bc_st = accum(totals.v_bc_st, Some(*v_bc_st));
1161            totals.v_st = accum(totals.v_st, Some(*v_icms_st));
1162            totals.v_fcp_st = accum(totals.v_fcp_st, *v_fcp_st);
1163            let mut fields_opt: Vec<Option<TaxField>> = vec![
1164                Some(TaxField::new("orig", orig.as_str())),
1165                Some(TaxField::new("CST", "30")),
1166            ];
1167            // ST fields
1168            fields_opt.push(Some(TaxField::new("modBCST", mod_bc_st.as_str())));
1169            if let Some(v) = p_mva_st {
1170                fields_opt.push(Some(TaxField::new("pMVAST", fc4(Some(*v)).unwrap())));
1171            }
1172            if let Some(v) = p_red_bc_st {
1173                fields_opt.push(Some(TaxField::new("pRedBCST", fc4(Some(*v)).unwrap())));
1174            }
1175            fields_opt.push(Some(TaxField::new("vBCST", fc2(Some(*v_bc_st)).unwrap())));
1176            fields_opt.push(Some(TaxField::new(
1177                "pICMSST",
1178                fc4(Some(*p_icms_st)).unwrap(),
1179            )));
1180            fields_opt.push(Some(TaxField::new(
1181                "vICMSST",
1182                fc2(Some(*v_icms_st)).unwrap(),
1183            )));
1184            // FCP ST
1185            fields_opt.push(optional_field("vBCFCPST", fc2(*v_bc_fcp_st).as_deref()));
1186            fields_opt.push(optional_field("pFCPST", fc4(*p_fcp_st).as_deref()));
1187            fields_opt.push(optional_field("vFCPST", fc2(*v_fcp_st).as_deref()));
1188            // Desoneration
1189            fields_opt.push(optional_field("vICMSDeson", fc2(*v_icms_deson).as_deref()));
1190            fields_opt.push(optional_field("motDesICMS", mot_des_icms.as_deref()));
1191            fields_opt.push(optional_field("indDeduzDeson", ind_deduz_deson.as_deref()));
1192            Ok(("ICMS30".to_string(), filter_fields(fields_opt)))
1193        }
1194
1195        IcmsCst::Cst40 {
1196            orig,
1197            v_icms_deson,
1198            mot_des_icms,
1199            ind_deduz_deson,
1200        }
1201        | IcmsCst::Cst41 {
1202            orig,
1203            v_icms_deson,
1204            mot_des_icms,
1205            ind_deduz_deson,
1206        }
1207        | IcmsCst::Cst50 {
1208            orig,
1209            v_icms_deson,
1210            mot_des_icms,
1211            ind_deduz_deson,
1212        } => {
1213            totals.v_icms_deson = accum(totals.v_icms_deson, *v_icms_deson);
1214            let mut fields_opt: Vec<Option<TaxField>> = vec![
1215                Some(TaxField::new("orig", orig.as_str())),
1216                Some(TaxField::new("CST", cst.cst_code())),
1217            ];
1218            // Desoneration
1219            fields_opt.push(optional_field("vICMSDeson", fc2(*v_icms_deson).as_deref()));
1220            fields_opt.push(optional_field("motDesICMS", mot_des_icms.as_deref()));
1221            fields_opt.push(optional_field("indDeduzDeson", ind_deduz_deson.as_deref()));
1222            Ok(("ICMS40".to_string(), filter_fields(fields_opt)))
1223        }
1224
1225        IcmsCst::Cst51 {
1226            orig,
1227            mod_bc,
1228            p_red_bc,
1229            c_benef_rbc,
1230            v_bc,
1231            p_icms,
1232            v_icms_op,
1233            p_dif,
1234            v_icms_dif,
1235            v_icms,
1236            v_bc_fcp,
1237            p_fcp,
1238            v_fcp,
1239            p_fcp_dif,
1240            v_fcp_dif,
1241            v_fcp_efet,
1242        } => {
1243            totals.v_bc = accum(totals.v_bc, *v_bc);
1244            totals.v_icms = accum(totals.v_icms, *v_icms);
1245            totals.v_fcp = accum(totals.v_fcp, *v_fcp);
1246            let fields_opt: Vec<Option<TaxField>> = vec![
1247                Some(TaxField::new("orig", orig.as_str())),
1248                Some(TaxField::new("CST", "51")),
1249                optional_field("modBC", mod_bc.as_deref()),
1250                optional_field("pRedBC", fc4(*p_red_bc).as_deref()),
1251                optional_field("cBenefRBC", c_benef_rbc.as_deref()),
1252                optional_field("vBC", fc2(*v_bc).as_deref()),
1253                optional_field("pICMS", fc4(*p_icms).as_deref()),
1254                optional_field("vICMSOp", fc2(*v_icms_op).as_deref()),
1255                optional_field("pDif", fc4(*p_dif).as_deref()),
1256                optional_field("vICMSDif", fc2(*v_icms_dif).as_deref()),
1257                optional_field("vICMS", fc2(*v_icms).as_deref()),
1258                optional_field("vBCFCP", fc2(*v_bc_fcp).as_deref()),
1259                optional_field("pFCP", fc4(*p_fcp).as_deref()),
1260                optional_field("vFCP", fc2(*v_fcp).as_deref()),
1261                optional_field("pFCPDif", fc4(*p_fcp_dif).as_deref()),
1262                optional_field("vFCPDif", fc2(*v_fcp_dif).as_deref()),
1263                optional_field("vFCPEfet", fc2(*v_fcp_efet).as_deref()),
1264            ];
1265            Ok(("ICMS51".to_string(), filter_fields(fields_opt)))
1266        }
1267
1268        IcmsCst::Cst53 {
1269            orig,
1270            q_bc_mono,
1271            ad_rem_icms,
1272            v_icms_mono_op,
1273            p_dif,
1274            v_icms_mono_dif,
1275            v_icms_mono,
1276        } => {
1277            totals.q_bc_mono = accum_raw(totals.q_bc_mono, *q_bc_mono);
1278            totals.v_icms_mono = accum(totals.v_icms_mono, *v_icms_mono);
1279            totals.q_bc_mono_reten = accum_raw(totals.q_bc_mono_reten, None);
1280            totals.v_icms_mono_reten = accum(totals.v_icms_mono_reten, None);
1281            let fields_opt: Vec<Option<TaxField>> = vec![
1282                Some(TaxField::new("orig", orig.as_str())),
1283                Some(TaxField::new("CST", "53")),
1284                optional_field("qBCMono", fc4_raw(*q_bc_mono).as_deref()),
1285                optional_field("adRemICMS", fc4(*ad_rem_icms).as_deref()),
1286                optional_field("vICMSMonoOp", fc2(*v_icms_mono_op).as_deref()),
1287                optional_field("pDif", fc4(*p_dif).as_deref()),
1288                optional_field("vICMSMonoDif", fc2(*v_icms_mono_dif).as_deref()),
1289                optional_field("vICMSMono", fc2(*v_icms_mono).as_deref()),
1290            ];
1291            Ok(("ICMS53".to_string(), filter_fields(fields_opt)))
1292        }
1293
1294        IcmsCst::Cst60 {
1295            orig,
1296            v_bc_st_ret,
1297            p_st,
1298            v_icms_substituto,
1299            v_icms_st_ret,
1300            v_bc_fcp_st_ret,
1301            p_fcp_st_ret,
1302            v_fcp_st_ret,
1303            p_red_bc_efet,
1304            v_bc_efet,
1305            p_icms_efet,
1306            v_icms_efet,
1307        } => {
1308            totals.v_fcp_st_ret = accum(totals.v_fcp_st_ret, *v_fcp_st_ret);
1309            let fields_opt: Vec<Option<TaxField>> = vec![
1310                Some(TaxField::new("orig", orig.as_str())),
1311                Some(TaxField::new("CST", "60")),
1312                optional_field("vBCSTRet", fc2(*v_bc_st_ret).as_deref()),
1313                optional_field("pST", fc4(*p_st).as_deref()),
1314                optional_field("vICMSSubstituto", fc2(*v_icms_substituto).as_deref()),
1315                optional_field("vICMSSTRet", fc2(*v_icms_st_ret).as_deref()),
1316                optional_field("vBCFCPSTRet", fc2(*v_bc_fcp_st_ret).as_deref()),
1317                optional_field("pFCPSTRet", fc4(*p_fcp_st_ret).as_deref()),
1318                optional_field("vFCPSTRet", fc2(*v_fcp_st_ret).as_deref()),
1319                optional_field("pRedBCEfet", fc4(*p_red_bc_efet).as_deref()),
1320                optional_field("vBCEfet", fc2(*v_bc_efet).as_deref()),
1321                optional_field("pICMSEfet", fc4(*p_icms_efet).as_deref()),
1322                optional_field("vICMSEfet", fc2(*v_icms_efet).as_deref()),
1323            ];
1324            Ok(("ICMS60".to_string(), filter_fields(fields_opt)))
1325        }
1326
1327        IcmsCst::Cst61 {
1328            orig,
1329            q_bc_mono_ret,
1330            ad_rem_icms_ret,
1331            v_icms_mono_ret,
1332        } => {
1333            totals.q_bc_mono_ret = accum_raw(totals.q_bc_mono_ret, *q_bc_mono_ret);
1334            totals.v_icms_mono_ret = accum(totals.v_icms_mono_ret, Some(*v_icms_mono_ret));
1335            let fields = filter_fields(vec![
1336                Some(TaxField::new("orig", orig.as_str())),
1337                Some(TaxField::new("CST", "61")),
1338                optional_field("qBCMonoRet", fc4_raw(*q_bc_mono_ret).as_deref()),
1339                Some(TaxField::new(
1340                    "adRemICMSRet",
1341                    fc4(Some(*ad_rem_icms_ret)).unwrap(),
1342                )),
1343                Some(TaxField::new(
1344                    "vICMSMonoRet",
1345                    fc2(Some(*v_icms_mono_ret)).unwrap(),
1346                )),
1347            ]);
1348            Ok(("ICMS61".to_string(), fields))
1349        }
1350
1351        IcmsCst::Cst70 {
1352            orig,
1353            mod_bc,
1354            p_red_bc,
1355            v_bc,
1356            p_icms,
1357            v_icms,
1358            v_bc_fcp,
1359            p_fcp,
1360            v_fcp,
1361            mod_bc_st,
1362            p_mva_st,
1363            p_red_bc_st,
1364            v_bc_st,
1365            p_icms_st,
1366            v_icms_st,
1367            v_bc_fcp_st,
1368            p_fcp_st,
1369            v_fcp_st,
1370            v_icms_deson,
1371            mot_des_icms,
1372            ind_deduz_deson,
1373            v_icms_st_deson,
1374            mot_des_icms_st,
1375        } => {
1376            totals.v_icms_deson = accum(totals.v_icms_deson, *v_icms_deson);
1377            totals.v_bc = accum(totals.v_bc, Some(*v_bc));
1378            totals.v_icms = accum(totals.v_icms, Some(*v_icms));
1379            totals.v_bc_st = accum(totals.v_bc_st, Some(*v_bc_st));
1380            totals.v_st = accum(totals.v_st, Some(*v_icms_st));
1381            totals.v_fcp_st = accum(totals.v_fcp_st, *v_fcp_st);
1382            totals.v_fcp = accum(totals.v_fcp, *v_fcp);
1383            let mut fields_opt: Vec<Option<TaxField>> = vec![
1384                Some(TaxField::new("orig", orig.as_str())),
1385                Some(TaxField::new("CST", "70")),
1386                Some(TaxField::new("modBC", mod_bc.as_str())),
1387                Some(TaxField::new("pRedBC", fc4(Some(*p_red_bc)).unwrap())),
1388                Some(TaxField::new("vBC", fc2(Some(*v_bc)).unwrap())),
1389                Some(TaxField::new("pICMS", fc4(Some(*p_icms)).unwrap())),
1390                Some(TaxField::new("vICMS", fc2(Some(*v_icms)).unwrap())),
1391            ];
1392            // FCP
1393            fields_opt.push(optional_field("vBCFCP", fc2(*v_bc_fcp).as_deref()));
1394            fields_opt.push(optional_field("pFCP", fc4(*p_fcp).as_deref()));
1395            fields_opt.push(optional_field("vFCP", fc2(*v_fcp).as_deref()));
1396            // ST
1397            fields_opt.push(Some(TaxField::new("modBCST", mod_bc_st.as_str())));
1398            if let Some(v) = p_mva_st {
1399                fields_opt.push(Some(TaxField::new("pMVAST", fc4(Some(*v)).unwrap())));
1400            }
1401            if let Some(v) = p_red_bc_st {
1402                fields_opt.push(Some(TaxField::new("pRedBCST", fc4(Some(*v)).unwrap())));
1403            }
1404            fields_opt.push(Some(TaxField::new("vBCST", fc2(Some(*v_bc_st)).unwrap())));
1405            fields_opt.push(Some(TaxField::new(
1406                "pICMSST",
1407                fc4(Some(*p_icms_st)).unwrap(),
1408            )));
1409            fields_opt.push(Some(TaxField::new(
1410                "vICMSST",
1411                fc2(Some(*v_icms_st)).unwrap(),
1412            )));
1413            // FCP ST
1414            fields_opt.push(optional_field("vBCFCPST", fc2(*v_bc_fcp_st).as_deref()));
1415            fields_opt.push(optional_field("pFCPST", fc4(*p_fcp_st).as_deref()));
1416            fields_opt.push(optional_field("vFCPST", fc2(*v_fcp_st).as_deref()));
1417            // Desoneration
1418            fields_opt.push(optional_field("vICMSDeson", fc2(*v_icms_deson).as_deref()));
1419            fields_opt.push(optional_field("motDesICMS", mot_des_icms.as_deref()));
1420            fields_opt.push(optional_field("indDeduzDeson", ind_deduz_deson.as_deref()));
1421            // ST desoneration
1422            fields_opt.push(optional_field(
1423                "vICMSSTDeson",
1424                fc2(*v_icms_st_deson).as_deref(),
1425            ));
1426            fields_opt.push(optional_field("motDesICMSST", mot_des_icms_st.as_deref()));
1427            Ok(("ICMS70".to_string(), filter_fields(fields_opt)))
1428        }
1429
1430        IcmsCst::Cst90 {
1431            orig,
1432            mod_bc,
1433            v_bc,
1434            p_red_bc,
1435            c_benef_rbc,
1436            p_icms,
1437            v_icms_op,
1438            p_dif,
1439            v_icms_dif,
1440            v_icms,
1441            v_bc_fcp,
1442            p_fcp,
1443            v_fcp,
1444            p_fcp_dif,
1445            v_fcp_dif,
1446            v_fcp_efet,
1447            mod_bc_st,
1448            p_mva_st,
1449            p_red_bc_st,
1450            v_bc_st,
1451            p_icms_st,
1452            v_icms_st,
1453            v_bc_fcp_st,
1454            p_fcp_st,
1455            v_fcp_st,
1456            v_icms_deson,
1457            mot_des_icms,
1458            ind_deduz_deson,
1459            v_icms_st_deson,
1460            mot_des_icms_st,
1461        } => {
1462            totals.v_icms_deson = accum(totals.v_icms_deson, *v_icms_deson);
1463            totals.v_bc = accum(totals.v_bc, *v_bc);
1464            totals.v_icms = accum(totals.v_icms, *v_icms);
1465            totals.v_bc_st = accum(totals.v_bc_st, *v_bc_st);
1466            totals.v_st = accum(totals.v_st, *v_icms_st);
1467            totals.v_fcp_st = accum(totals.v_fcp_st, *v_fcp_st);
1468            totals.v_fcp = accum(totals.v_fcp, *v_fcp);
1469            let mut fields_opt: Vec<Option<TaxField>> = vec![
1470                Some(TaxField::new("orig", orig.as_str())),
1471                Some(TaxField::new("CST", "90")),
1472                optional_field("modBC", mod_bc.as_deref()),
1473                optional_field("vBC", fc2(*v_bc).as_deref()),
1474                optional_field("pRedBC", fc4(*p_red_bc).as_deref()),
1475                optional_field("cBenefRBC", c_benef_rbc.as_deref()),
1476                optional_field("pICMS", fc4(*p_icms).as_deref()),
1477                optional_field("vICMSOp", fc2(*v_icms_op).as_deref()),
1478                optional_field("pDif", fc4(*p_dif).as_deref()),
1479                optional_field("vICMSDif", fc2(*v_icms_dif).as_deref()),
1480                optional_field("vICMS", fc2(*v_icms).as_deref()),
1481            ];
1482            // FCP
1483            fields_opt.push(optional_field("vBCFCP", fc2(*v_bc_fcp).as_deref()));
1484            fields_opt.push(optional_field("pFCP", fc4(*p_fcp).as_deref()));
1485            fields_opt.push(optional_field("vFCP", fc2(*v_fcp).as_deref()));
1486            // FCP deferral
1487            fields_opt.push(optional_field("pFCPDif", fc4(*p_fcp_dif).as_deref()));
1488            fields_opt.push(optional_field("vFCPDif", fc2(*v_fcp_dif).as_deref()));
1489            fields_opt.push(optional_field("vFCPEfet", fc2(*v_fcp_efet).as_deref()));
1490            // ST (all optional for CST 90)
1491            fields_opt.push(optional_field("modBCST", mod_bc_st.as_deref()));
1492            fields_opt.push(optional_field("pMVAST", fc4(*p_mva_st).as_deref()));
1493            fields_opt.push(optional_field("pRedBCST", fc4(*p_red_bc_st).as_deref()));
1494            fields_opt.push(optional_field("vBCST", fc2(*v_bc_st).as_deref()));
1495            fields_opt.push(optional_field("pICMSST", fc4(*p_icms_st).as_deref()));
1496            fields_opt.push(optional_field("vICMSST", fc2(*v_icms_st).as_deref()));
1497            // FCP ST
1498            fields_opt.push(optional_field("vBCFCPST", fc2(*v_bc_fcp_st).as_deref()));
1499            fields_opt.push(optional_field("pFCPST", fc4(*p_fcp_st).as_deref()));
1500            fields_opt.push(optional_field("vFCPST", fc2(*v_fcp_st).as_deref()));
1501            // Desoneration
1502            fields_opt.push(optional_field("vICMSDeson", fc2(*v_icms_deson).as_deref()));
1503            fields_opt.push(optional_field("motDesICMS", mot_des_icms.as_deref()));
1504            fields_opt.push(optional_field("indDeduzDeson", ind_deduz_deson.as_deref()));
1505            // ST desoneration
1506            fields_opt.push(optional_field(
1507                "vICMSSTDeson",
1508                fc2(*v_icms_st_deson).as_deref(),
1509            ));
1510            fields_opt.push(optional_field("motDesICMSST", mot_des_icms_st.as_deref()));
1511            Ok(("ICMS90".to_string(), filter_fields(fields_opt)))
1512        }
1513    }
1514}
1515
1516// ── IcmsCsosn enum (Simples Nacional) ────────────────────────────────────────
1517
1518/// ICMS CSOSN variant for Simples Nacional tax regime (CRT 1/2).
1519///
1520/// Each variant carries **only** the fields that are valid for that CSOSN,
1521/// giving compile-time safety instead of runtime string matching against a
1522/// flat struct full of `Option`s.
1523///
1524/// Normal regime CSTs use [`IcmsCst`] instead.
1525#[derive(Debug, Clone)]
1526#[non_exhaustive]
1527pub enum IcmsCsosn {
1528    /// CSOSN 101 — Tributada pelo Simples Nacional com permissao de credito.
1529    Csosn101 {
1530        /// Product origin code (`orig`).
1531        orig: String,
1532        /// CSOSN code, always `"101"`.
1533        csosn: String,
1534        /// Simples Nacional credit rate (`pCredSN`).
1535        p_cred_sn: Rate,
1536        /// Simples Nacional credit value (`vCredICMSSN`).
1537        v_cred_icms_sn: Cents,
1538    },
1539    /// CSOSN 102/103/300/400 — Tributada sem permissao de credito / Imune /
1540    /// Nao tributada.
1541    Csosn102 {
1542        /// Product origin code (`orig`). May be empty for CSOSN 300/400.
1543        orig: String,
1544        /// CSOSN code — `"102"`, `"103"`, `"300"`, or `"400"`.
1545        csosn: String,
1546    },
1547    /// CSOSN 201 — Tributada com permissao de credito e com cobranca do ICMS
1548    /// por ST.
1549    Csosn201 {
1550        /// Product origin code (`orig`).
1551        orig: String,
1552        /// CSOSN code, always `"201"`.
1553        csosn: String,
1554        /// ST base calculation modality (`modBCST`).
1555        mod_bc_st: String,
1556        /// ST added value margin (`pMVAST`). Optional.
1557        p_mva_st: Option<Rate>,
1558        /// ST base reduction rate (`pRedBCST`). Optional.
1559        p_red_bc_st: Option<Rate>,
1560        /// ST calculation base value (`vBCST`).
1561        v_bc_st: Cents,
1562        /// ST rate (`pICMSST`).
1563        p_icms_st: Rate,
1564        /// ST ICMS value (`vICMSST`).
1565        v_icms_st: Cents,
1566        /// FCP-ST calculation base (`vBCFCPST`). Optional.
1567        v_bc_fcp_st: Option<Cents>,
1568        /// FCP-ST rate (`pFCPST`). Optional.
1569        p_fcp_st: Option<Rate>,
1570        /// FCP-ST value (`vFCPST`). Optional.
1571        v_fcp_st: Option<Cents>,
1572        /// Simples Nacional credit rate (`pCredSN`). Optional.
1573        p_cred_sn: Option<Rate>,
1574        /// Simples Nacional credit value (`vCredICMSSN`). Optional.
1575        v_cred_icms_sn: Option<Cents>,
1576    },
1577    /// CSOSN 202/203 — Tributada sem permissao de credito e com cobranca do
1578    /// ICMS por ST.
1579    Csosn202 {
1580        /// Product origin code (`orig`).
1581        orig: String,
1582        /// CSOSN code — `"202"` or `"203"`.
1583        csosn: String,
1584        /// ST base calculation modality (`modBCST`).
1585        mod_bc_st: String,
1586        /// ST added value margin (`pMVAST`). Optional.
1587        p_mva_st: Option<Rate>,
1588        /// ST base reduction rate (`pRedBCST`). Optional.
1589        p_red_bc_st: Option<Rate>,
1590        /// ST calculation base value (`vBCST`).
1591        v_bc_st: Cents,
1592        /// ST rate (`pICMSST`).
1593        p_icms_st: Rate,
1594        /// ST ICMS value (`vICMSST`).
1595        v_icms_st: Cents,
1596        /// FCP-ST calculation base (`vBCFCPST`). Optional.
1597        v_bc_fcp_st: Option<Cents>,
1598        /// FCP-ST rate (`pFCPST`). Optional.
1599        p_fcp_st: Option<Rate>,
1600        /// FCP-ST value (`vFCPST`). Optional.
1601        v_fcp_st: Option<Cents>,
1602    },
1603    /// CSOSN 500 — ICMS cobrado anteriormente por ST ou por antecipacao.
1604    Csosn500 {
1605        /// Product origin code (`orig`).
1606        orig: String,
1607        /// CSOSN code, always `"500"`.
1608        csosn: String,
1609        /// ST retained calculation base (`vBCSTRet`). Optional.
1610        v_bc_st_ret: Option<Cents>,
1611        /// ST rate at retention (`pST`). Optional.
1612        p_st: Option<Rate>,
1613        /// ICMS value paid by the substitutor (`vICMSSubstituto`). Optional.
1614        v_icms_substituto: Option<Cents>,
1615        /// Retained ST ICMS value (`vICMSSTRet`). Optional.
1616        v_icms_st_ret: Option<Cents>,
1617        /// FCP-ST retained calculation base (`vBCFCPSTRet`). Optional.
1618        v_bc_fcp_st_ret: Option<Cents>,
1619        /// FCP-ST retained rate (`pFCPSTRet`). Optional.
1620        p_fcp_st_ret: Option<Rate>,
1621        /// FCP-ST retained value (`vFCPSTRet`). Optional.
1622        v_fcp_st_ret: Option<Cents>,
1623        /// Effective base reduction rate (`pRedBCEfet`). Optional.
1624        p_red_bc_efet: Option<Rate>,
1625        /// Effective calculation base (`vBCEfet`). Optional.
1626        v_bc_efet: Option<Cents>,
1627        /// Effective ICMS rate (`pICMSEfet`). Optional.
1628        p_icms_efet: Option<Rate>,
1629        /// Effective ICMS value (`vICMSEfet`). Optional.
1630        v_icms_efet: Option<Cents>,
1631    },
1632    /// CSOSN 900 — Outros.
1633    Csosn900 {
1634        /// Product origin code (`orig`). May be empty.
1635        orig: String,
1636        /// CSOSN code, always `"900"`.
1637        csosn: String,
1638        /// Base calculation modality (`modBC`). Optional.
1639        mod_bc: Option<String>,
1640        /// ICMS calculation base value (`vBC`). Optional.
1641        v_bc: Option<Cents>,
1642        /// Base reduction rate (`pRedBC`). Optional.
1643        p_red_bc: Option<Rate>,
1644        /// ICMS rate (`pICMS`). Optional.
1645        p_icms: Option<Rate>,
1646        /// ICMS value (`vICMS`). Optional.
1647        v_icms: Option<Cents>,
1648        /// ST base calculation modality (`modBCST`). Optional.
1649        mod_bc_st: Option<String>,
1650        /// ST added value margin (`pMVAST`). Optional.
1651        p_mva_st: Option<Rate>,
1652        /// ST base reduction rate (`pRedBCST`). Optional.
1653        p_red_bc_st: Option<Rate>,
1654        /// ST calculation base value (`vBCST`). Optional.
1655        v_bc_st: Option<Cents>,
1656        /// ST rate (`pICMSST`). Optional.
1657        p_icms_st: Option<Rate>,
1658        /// ST ICMS value (`vICMSST`). Optional.
1659        v_icms_st: Option<Cents>,
1660        /// FCP-ST calculation base (`vBCFCPST`). Optional.
1661        v_bc_fcp_st: Option<Cents>,
1662        /// FCP-ST rate (`pFCPST`). Optional.
1663        p_fcp_st: Option<Rate>,
1664        /// FCP-ST value (`vFCPST`). Optional.
1665        v_fcp_st: Option<Cents>,
1666        /// Simples Nacional credit rate (`pCredSN`). Optional.
1667        p_cred_sn: Option<Rate>,
1668        /// Simples Nacional credit value (`vCredICMSSN`). Optional.
1669        v_cred_icms_sn: Option<Cents>,
1670    },
1671}
1672
1673impl IcmsCsosn {
1674    /// Return the CSOSN code string for this variant (e.g. "101", "202").
1675    pub fn csosn_code(&self) -> &str {
1676        match self {
1677            Self::Csosn101 { csosn, .. } => csosn.as_str(),
1678            Self::Csosn102 { csosn, .. } => csosn.as_str(),
1679            Self::Csosn201 { csosn, .. } => csosn.as_str(),
1680            Self::Csosn202 { csosn, .. } => csosn.as_str(),
1681            Self::Csosn500 { csosn, .. } => csosn.as_str(),
1682            Self::Csosn900 { csosn, .. } => csosn.as_str(),
1683        }
1684    }
1685}
1686
1687/// Build the ICMS XML fragment and accumulate totals from a typed
1688/// [`IcmsCsosn`] variant.
1689///
1690/// This is the compile-time-safe counterpart of the original
1691/// [`build_icms_xml`] code path for Simples Nacional CSOSNs. It can be used
1692/// directly by new code that already has an `IcmsCsosn`, or indirectly via
1693/// the unchanged [`build_icms_xml`] public API (which converts internally).
1694///
1695/// # Errors
1696///
1697/// Returns [`FiscalError`] if XML field serialization fails (should not happen
1698/// when the enum is correctly constructed).
1699pub fn build_icms_csosn_xml(
1700    csosn: &IcmsCsosn,
1701    totals: &mut IcmsTotals,
1702) -> Result<(String, Vec<TaxField>), FiscalError> {
1703    match csosn {
1704        IcmsCsosn::Csosn101 {
1705            orig,
1706            csosn,
1707            p_cred_sn,
1708            v_cred_icms_sn,
1709        } => {
1710            let fields = filter_fields(vec![
1711                Some(TaxField::new("orig", orig.as_str())),
1712                Some(TaxField::new("CSOSN", csosn.as_str())),
1713                Some(TaxField::new("pCredSN", fc4(Some(*p_cred_sn)).unwrap())),
1714                Some(TaxField::new(
1715                    "vCredICMSSN",
1716                    fc2(Some(*v_cred_icms_sn)).unwrap(),
1717                )),
1718            ]);
1719            Ok(("ICMSSN101".to_string(), fields))
1720        }
1721
1722        IcmsCsosn::Csosn102 { orig, csosn } => {
1723            let orig_val = if orig.is_empty() {
1724                None
1725            } else {
1726                Some(orig.as_str())
1727            };
1728            let fields = filter_fields(vec![
1729                optional_field("orig", orig_val),
1730                Some(TaxField::new("CSOSN", csosn.as_str())),
1731            ]);
1732            Ok(("ICMSSN102".to_string(), fields))
1733        }
1734
1735        IcmsCsosn::Csosn201 {
1736            orig,
1737            csosn,
1738            mod_bc_st,
1739            p_mva_st,
1740            p_red_bc_st,
1741            v_bc_st,
1742            p_icms_st,
1743            v_icms_st,
1744            v_bc_fcp_st,
1745            p_fcp_st,
1746            v_fcp_st,
1747            p_cred_sn,
1748            v_cred_icms_sn,
1749        } => {
1750            totals.v_bc_st = accum(totals.v_bc_st, Some(*v_bc_st));
1751            totals.v_st = accum(totals.v_st, Some(*v_icms_st));
1752
1753            let mut fields_opt: Vec<Option<TaxField>> = vec![
1754                Some(TaxField::new("orig", orig.as_str())),
1755                Some(TaxField::new("CSOSN", csosn.as_str())),
1756            ];
1757            // ST fields
1758            fields_opt.push(Some(TaxField::new("modBCST", mod_bc_st.as_str())));
1759            if let Some(v) = p_mva_st {
1760                fields_opt.push(Some(TaxField::new("pMVAST", fc4(Some(*v)).unwrap())));
1761            }
1762            if let Some(v) = p_red_bc_st {
1763                fields_opt.push(Some(TaxField::new("pRedBCST", fc4(Some(*v)).unwrap())));
1764            }
1765            fields_opt.push(Some(TaxField::new("vBCST", fc2(Some(*v_bc_st)).unwrap())));
1766            fields_opt.push(Some(TaxField::new(
1767                "pICMSST",
1768                fc4(Some(*p_icms_st)).unwrap(),
1769            )));
1770            fields_opt.push(Some(TaxField::new(
1771                "vICMSST",
1772                fc2(Some(*v_icms_st)).unwrap(),
1773            )));
1774            // FCP ST fields
1775            fields_opt.push(optional_field("vBCFCPST", fc2(*v_bc_fcp_st).as_deref()));
1776            fields_opt.push(optional_field("pFCPST", fc4(*p_fcp_st).as_deref()));
1777            fields_opt.push(optional_field("vFCPST", fc2(*v_fcp_st).as_deref()));
1778            // SN credit fields
1779            fields_opt.push(optional_field("pCredSN", fc4(*p_cred_sn).as_deref()));
1780            fields_opt.push(optional_field(
1781                "vCredICMSSN",
1782                fc2(*v_cred_icms_sn).as_deref(),
1783            ));
1784            Ok(("ICMSSN201".to_string(), filter_fields(fields_opt)))
1785        }
1786
1787        IcmsCsosn::Csosn202 {
1788            orig,
1789            csosn,
1790            mod_bc_st,
1791            p_mva_st,
1792            p_red_bc_st,
1793            v_bc_st,
1794            p_icms_st,
1795            v_icms_st,
1796            v_bc_fcp_st,
1797            p_fcp_st,
1798            v_fcp_st,
1799        } => {
1800            totals.v_bc_st = accum(totals.v_bc_st, Some(*v_bc_st));
1801            totals.v_st = accum(totals.v_st, Some(*v_icms_st));
1802
1803            let mut fields_opt: Vec<Option<TaxField>> = vec![
1804                Some(TaxField::new("orig", orig.as_str())),
1805                Some(TaxField::new("CSOSN", csosn.as_str())),
1806            ];
1807            // ST fields
1808            fields_opt.push(Some(TaxField::new("modBCST", mod_bc_st.as_str())));
1809            if let Some(v) = p_mva_st {
1810                fields_opt.push(Some(TaxField::new("pMVAST", fc4(Some(*v)).unwrap())));
1811            }
1812            if let Some(v) = p_red_bc_st {
1813                fields_opt.push(Some(TaxField::new("pRedBCST", fc4(Some(*v)).unwrap())));
1814            }
1815            fields_opt.push(Some(TaxField::new("vBCST", fc2(Some(*v_bc_st)).unwrap())));
1816            fields_opt.push(Some(TaxField::new(
1817                "pICMSST",
1818                fc4(Some(*p_icms_st)).unwrap(),
1819            )));
1820            fields_opt.push(Some(TaxField::new(
1821                "vICMSST",
1822                fc2(Some(*v_icms_st)).unwrap(),
1823            )));
1824            // FCP ST fields
1825            fields_opt.push(optional_field("vBCFCPST", fc2(*v_bc_fcp_st).as_deref()));
1826            fields_opt.push(optional_field("pFCPST", fc4(*p_fcp_st).as_deref()));
1827            fields_opt.push(optional_field("vFCPST", fc2(*v_fcp_st).as_deref()));
1828            Ok(("ICMSSN202".to_string(), filter_fields(fields_opt)))
1829        }
1830
1831        IcmsCsosn::Csosn500 {
1832            orig,
1833            csosn,
1834            v_bc_st_ret,
1835            p_st,
1836            v_icms_substituto,
1837            v_icms_st_ret,
1838            v_bc_fcp_st_ret,
1839            p_fcp_st_ret,
1840            v_fcp_st_ret,
1841            p_red_bc_efet,
1842            v_bc_efet,
1843            p_icms_efet,
1844            v_icms_efet,
1845        } => {
1846            let fields = filter_fields(vec![
1847                Some(TaxField::new("orig", orig.as_str())),
1848                Some(TaxField::new("CSOSN", csosn.as_str())),
1849                optional_field("vBCSTRet", fc2(*v_bc_st_ret).as_deref()),
1850                optional_field("pST", fc4(*p_st).as_deref()),
1851                optional_field("vICMSSubstituto", fc2(*v_icms_substituto).as_deref()),
1852                optional_field("vICMSSTRet", fc2(*v_icms_st_ret).as_deref()),
1853                optional_field("vBCFCPSTRet", fc2(*v_bc_fcp_st_ret).as_deref()),
1854                optional_field("pFCPSTRet", fc4(*p_fcp_st_ret).as_deref()),
1855                optional_field("vFCPSTRet", fc2(*v_fcp_st_ret).as_deref()),
1856                optional_field("pRedBCEfet", fc4(*p_red_bc_efet).as_deref()),
1857                optional_field("vBCEfet", fc2(*v_bc_efet).as_deref()),
1858                optional_field("pICMSEfet", fc4(*p_icms_efet).as_deref()),
1859                optional_field("vICMSEfet", fc2(*v_icms_efet).as_deref()),
1860            ]);
1861            Ok(("ICMSSN500".to_string(), fields))
1862        }
1863
1864        IcmsCsosn::Csosn900 {
1865            orig,
1866            csosn,
1867            mod_bc,
1868            v_bc,
1869            p_red_bc,
1870            p_icms,
1871            v_icms,
1872            mod_bc_st,
1873            p_mva_st,
1874            p_red_bc_st,
1875            v_bc_st,
1876            p_icms_st,
1877            v_icms_st,
1878            v_bc_fcp_st,
1879            p_fcp_st,
1880            v_fcp_st,
1881            p_cred_sn,
1882            v_cred_icms_sn,
1883        } => {
1884            totals.v_bc = accum(totals.v_bc, *v_bc);
1885            totals.v_icms = accum(totals.v_icms, *v_icms);
1886            totals.v_bc_st = accum(totals.v_bc_st, *v_bc_st);
1887            totals.v_st = accum(totals.v_st, *v_icms_st);
1888
1889            let orig_val = if orig.is_empty() {
1890                None
1891            } else {
1892                Some(orig.as_str())
1893            };
1894            let mut fields_opt: Vec<Option<TaxField>> = vec![
1895                optional_field("orig", orig_val),
1896                Some(TaxField::new("CSOSN", csosn.as_str())),
1897                optional_field("modBC", mod_bc.as_deref()),
1898                optional_field("vBC", fc2(*v_bc).as_deref()),
1899                optional_field("pRedBC", fc4(*p_red_bc).as_deref()),
1900                optional_field("pICMS", fc4(*p_icms).as_deref()),
1901                optional_field("vICMS", fc2(*v_icms).as_deref()),
1902                // ST fields are all optional for CSOSN 900
1903                optional_field("modBCST", mod_bc_st.as_deref()),
1904                optional_field("pMVAST", fc4(*p_mva_st).as_deref()),
1905                optional_field("pRedBCST", fc4(*p_red_bc_st).as_deref()),
1906                optional_field("vBCST", fc2(*v_bc_st).as_deref()),
1907                optional_field("pICMSST", fc4(*p_icms_st).as_deref()),
1908                optional_field("vICMSST", fc2(*v_icms_st).as_deref()),
1909            ];
1910            // FCP ST fields
1911            fields_opt.push(optional_field("vBCFCPST", fc2(*v_bc_fcp_st).as_deref()));
1912            fields_opt.push(optional_field("pFCPST", fc4(*p_fcp_st).as_deref()));
1913            fields_opt.push(optional_field("vFCPST", fc2(*v_fcp_st).as_deref()));
1914            // SN credit fields
1915            fields_opt.push(optional_field("pCredSN", fc4(*p_cred_sn).as_deref()));
1916            fields_opt.push(optional_field(
1917                "vCredICMSSN",
1918                fc2(*v_cred_icms_sn).as_deref(),
1919            ));
1920            Ok(("ICMSSN900".to_string(), filter_fields(fields_opt)))
1921        }
1922    }
1923}
1924
1925// ── Helper: format field values ─────────────────────────────────────────────
1926
1927/// Format a monetary [`Cents`] value (2 decimal places) returning `Option<String>`.
1928fn fc2(v: Option<Cents>) -> Option<String> {
1929    format_cents_or_none(v.map(|c| c.0), 2)
1930}
1931
1932/// Format a [`Rate`] value (4 decimal places) returning `Option<String>`.
1933fn fc4(v: Option<Rate>) -> Option<String> {
1934    format_cents_or_none(v.map(|r| r.0), 4)
1935}
1936
1937/// Format a raw i64 quantity (4 decimal places) returning `Option<String>`.
1938fn fc4_raw(v: Option<i64>) -> Option<String> {
1939    format_cents_or_none(v, 4)
1940}
1941
1942// ── Main builders ───────────────────────────────────────────────────────────
1943
1944/// Build ICMS XML string from a typed [`IcmsVariant`].
1945///
1946/// Delegates to [`build_icms_cst_xml`] or [`build_icms_csosn_xml`] depending
1947/// on the variant, then wraps the result in an `<ICMS>` element and
1948/// accumulates totals.
1949///
1950/// # Errors
1951///
1952/// Returns [`FiscalError`] if XML field serialization fails (should not happen
1953/// when the enum is correctly constructed).
1954pub fn build_icms_xml(
1955    variant: &IcmsVariant,
1956    totals: &mut IcmsTotals,
1957) -> Result<String, FiscalError> {
1958    let (variant_tag, fields) = match variant {
1959        IcmsVariant::Cst(cst) => build_icms_cst_xml(cst, totals)?,
1960        IcmsVariant::Csosn(csosn) => build_icms_csosn_xml(csosn, totals)?,
1961    };
1962
1963    let element = TaxElement {
1964        outer_tag: Some("ICMS".to_string()),
1965        outer_fields: vec![],
1966        variant_tag,
1967        fields,
1968    };
1969
1970    Ok(serialize_tax_element(&element))
1971}
1972
1973/// Build the ICMSPart XML group (partition between states).
1974///
1975/// Used inside `<ICMS>` for CST 10 or 90 with interstate partition.
1976///
1977/// # Errors
1978///
1979/// Returns [`FiscalError::MissingRequiredField`] if any required field is
1980/// missing in the data.
1981pub fn build_icms_part_xml(data: &IcmsPartData) -> Result<(String, IcmsTotals), FiscalError> {
1982    let mut totals = create_icms_totals();
1983    totals.v_bc = accum(totals.v_bc, Some(data.v_bc));
1984    totals.v_icms = accum(totals.v_icms, Some(data.v_icms));
1985    totals.v_bc_st = accum(totals.v_bc_st, Some(data.v_bc_st));
1986    totals.v_st = accum(totals.v_st, Some(data.v_icms_st));
1987
1988    let mut fields_opt: Vec<Option<TaxField>> = vec![
1989        Some(TaxField::new("orig", data.orig.as_str())),
1990        Some(TaxField::new("CST", data.cst.as_str())),
1991        Some(TaxField::new("modBC", data.mod_bc.as_str())),
1992        Some(TaxField::new("vBC", fc2(Some(data.v_bc)).unwrap())),
1993        optional_field("pRedBC", fc4(data.p_red_bc).as_deref()),
1994        Some(TaxField::new("pICMS", fc4(Some(data.p_icms)).unwrap())),
1995        Some(TaxField::new("vICMS", fc2(Some(data.v_icms)).unwrap())),
1996    ];
1997
1998    // ST fields
1999    fields_opt.push(Some(TaxField::new("modBCST", data.mod_bc_st.as_str())));
2000    if let Some(v) = data.p_mva_st {
2001        fields_opt.push(Some(TaxField::new("pMVAST", fc4(Some(v)).unwrap())));
2002    }
2003    if let Some(v) = data.p_red_bc_st {
2004        fields_opt.push(Some(TaxField::new("pRedBCST", fc4(Some(v)).unwrap())));
2005    }
2006    fields_opt.push(Some(TaxField::new(
2007        "vBCST",
2008        fc2(Some(data.v_bc_st)).unwrap(),
2009    )));
2010    fields_opt.push(Some(TaxField::new(
2011        "pICMSST",
2012        fc4(Some(data.p_icms_st)).unwrap(),
2013    )));
2014    fields_opt.push(Some(TaxField::new(
2015        "vICMSST",
2016        fc2(Some(data.v_icms_st)).unwrap(),
2017    )));
2018
2019    // FCP ST fields
2020    fields_opt.push(optional_field("vBCFCPST", fc2(data.v_bc_fcp_st).as_deref()));
2021    fields_opt.push(optional_field("pFCPST", fc4(data.p_fcp_st).as_deref()));
2022    fields_opt.push(optional_field("vFCPST", fc2(data.v_fcp_st).as_deref()));
2023
2024    // pBCOp, UFST
2025    fields_opt.push(Some(TaxField::new(
2026        "pBCOp",
2027        fc4(Some(data.p_bc_op)).unwrap(),
2028    )));
2029    fields_opt.push(Some(TaxField::new("UFST", data.uf_st.as_str())));
2030
2031    // Desoneration
2032    fields_opt.push(optional_field(
2033        "vICMSDeson",
2034        fc2(data.v_icms_deson).as_deref(),
2035    ));
2036    fields_opt.push(optional_field("motDesICMS", data.mot_des_icms.as_deref()));
2037    fields_opt.push(optional_field(
2038        "indDeduzDeson",
2039        data.ind_deduz_deson.as_deref(),
2040    ));
2041
2042    let fields = filter_fields(fields_opt);
2043
2044    let element = TaxElement {
2045        outer_tag: Some("ICMS".to_string()),
2046        outer_fields: vec![],
2047        variant_tag: "ICMSPart".to_string(),
2048        fields,
2049    };
2050
2051    Ok((serialize_tax_element(&element), totals))
2052}
2053
2054/// Build the ICMSST XML group (ST repasse).
2055///
2056/// Used inside `<ICMS>` for CST 41 or 60 with interstate ST repasse.
2057///
2058/// # Errors
2059///
2060/// Returns [`FiscalError`] if serialization fails.
2061pub fn build_icms_st_xml(data: &IcmsStData) -> Result<(String, IcmsTotals), FiscalError> {
2062    let mut totals = create_icms_totals();
2063    totals.v_fcp_st_ret = accum(totals.v_fcp_st_ret, data.v_fcp_st_ret);
2064
2065    let fields_opt: Vec<Option<TaxField>> = vec![
2066        Some(TaxField::new("orig", data.orig.as_str())),
2067        Some(TaxField::new("CST", data.cst.as_str())),
2068        Some(TaxField::new(
2069            "vBCSTRet",
2070            fc2(Some(data.v_bc_st_ret)).unwrap(),
2071        )),
2072        optional_field("pST", fc4(data.p_st).as_deref()),
2073        optional_field("vICMSSubstituto", fc2(data.v_icms_substituto).as_deref()),
2074        Some(TaxField::new(
2075            "vICMSSTRet",
2076            fc2(Some(data.v_icms_st_ret)).unwrap(),
2077        )),
2078        optional_field("vBCFCPSTRet", fc2(data.v_bc_fcp_st_ret).as_deref()),
2079        optional_field("pFCPSTRet", fc4(data.p_fcp_st_ret).as_deref()),
2080        optional_field("vFCPSTRet", fc2(data.v_fcp_st_ret).as_deref()),
2081        Some(TaxField::new(
2082            "vBCSTDest",
2083            fc2(Some(data.v_bc_st_dest)).unwrap(),
2084        )),
2085        Some(TaxField::new(
2086            "vICMSSTDest",
2087            fc2(Some(data.v_icms_st_dest)).unwrap(),
2088        )),
2089        optional_field("pRedBCEfet", fc4(data.p_red_bc_efet).as_deref()),
2090        optional_field("vBCEfet", fc2(data.v_bc_efet).as_deref()),
2091        optional_field("pICMSEfet", fc4(data.p_icms_efet).as_deref()),
2092        optional_field("vICMSEfet", fc2(data.v_icms_efet).as_deref()),
2093    ];
2094
2095    let fields = filter_fields(fields_opt);
2096
2097    let element = TaxElement {
2098        outer_tag: Some("ICMS".to_string()),
2099        outer_fields: vec![],
2100        variant_tag: "ICMSST".to_string(),
2101        fields,
2102    };
2103
2104    Ok((serialize_tax_element(&element), totals))
2105}
2106
2107/// Build the ICMSUFDest XML group (interstate destination).
2108///
2109/// This is a sibling of `<ICMS>`, placed directly inside `<imposto>`.
2110///
2111/// # Errors
2112///
2113/// Returns [`FiscalError`] if serialization fails.
2114pub fn build_icms_uf_dest_xml(data: &IcmsUfDestData) -> Result<(String, IcmsTotals), FiscalError> {
2115    let mut totals = create_icms_totals();
2116    totals.v_icms_uf_dest = accum(totals.v_icms_uf_dest, Some(data.v_icms_uf_dest));
2117    totals.v_fcp_uf_dest = accum(totals.v_fcp_uf_dest, data.v_fcp_uf_dest);
2118    totals.v_icms_uf_remet = accum(totals.v_icms_uf_remet, data.v_icms_uf_remet);
2119
2120    let fields_opt: Vec<Option<TaxField>> = vec![
2121        Some(TaxField::new(
2122            "vBCUFDest",
2123            fc2(Some(data.v_bc_uf_dest)).unwrap(),
2124        )),
2125        optional_field("vBCFCPUFDest", fc2(data.v_bc_fcp_uf_dest).as_deref()),
2126        optional_field("pFCPUFDest", fc4(data.p_fcp_uf_dest).as_deref()),
2127        Some(TaxField::new(
2128            "pICMSUFDest",
2129            fc4(Some(data.p_icms_uf_dest)).unwrap(),
2130        )),
2131        Some(TaxField::new(
2132            "pICMSInter",
2133            fc4(Some(data.p_icms_inter)).unwrap(),
2134        )),
2135        Some(TaxField::new("pICMSInterPart", "100.0000")),
2136        optional_field("vFCPUFDest", fc2(data.v_fcp_uf_dest).as_deref()),
2137        Some(TaxField::new(
2138            "vICMSUFDest",
2139            fc2(Some(data.v_icms_uf_dest)).unwrap(),
2140        )),
2141        Some(TaxField::new(
2142            "vICMSUFRemet",
2143            fc2(Some(data.v_icms_uf_remet.unwrap_or(Cents(0)))).unwrap(),
2144        )),
2145    ];
2146
2147    let fields = filter_fields(fields_opt);
2148
2149    let element = TaxElement {
2150        outer_tag: None,
2151        outer_fields: vec![],
2152        variant_tag: "ICMSUFDest".to_string(),
2153        fields,
2154    };
2155
2156    Ok((serialize_tax_element(&element), totals))
2157}
2158
2159/// Merge item-level ICMS totals into a running accumulator.
2160///
2161/// All monetary fields in `source` are added to the corresponding fields of
2162/// `target`. Call this after each item's ICMS XML has been generated via
2163/// [`build_icms_part_xml`] or [`build_icms_st_xml`] (which return their own
2164/// per-item sub-totals) to keep a running document total.
2165pub fn merge_icms_totals(target: &mut IcmsTotals, source: &IcmsTotals) {
2166    target.v_bc += source.v_bc;
2167    target.v_icms += source.v_icms;
2168    target.v_icms_deson += source.v_icms_deson;
2169    target.v_bc_st += source.v_bc_st;
2170    target.v_st += source.v_st;
2171    target.v_fcp += source.v_fcp;
2172    target.v_fcp_st += source.v_fcp_st;
2173    target.v_fcp_st_ret += source.v_fcp_st_ret;
2174    target.v_fcp_uf_dest += source.v_fcp_uf_dest;
2175    target.v_icms_uf_dest += source.v_icms_uf_dest;
2176    target.v_icms_uf_remet += source.v_icms_uf_remet;
2177    target.q_bc_mono += source.q_bc_mono;
2178    target.v_icms_mono += source.v_icms_mono;
2179    target.q_bc_mono_reten += source.q_bc_mono_reten;
2180    target.v_icms_mono_reten += source.v_icms_mono_reten;
2181    target.q_bc_mono_ret += source.q_bc_mono_ret;
2182    target.v_icms_mono_ret += source.v_icms_mono_ret;
2183}