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    /// Whether ICMS desoneration should be deducted from vNF (`indDeduzDeson`).
424    ///
425    /// Mirrors the PHP class-level `$this->indDeduzDeson` flag.  When any item
426    /// sets `indDeduzDeson=1`, this flag becomes `true` and the accumulated
427    /// `v_icms_deson` is subtracted from vNF.
428    pub ind_deduz_deson: bool,
429}
430
431impl IcmsTotals {
432    /// Create a new zeroed-out `IcmsTotals`.
433    pub fn new() -> Self {
434        Self::default()
435    }
436    /// Set the total ICMS calculation base (`vBC`).
437    pub fn v_bc(mut self, v: Cents) -> Self {
438        self.v_bc = v;
439        self
440    }
441    /// Set the total ICMS value (`vICMS`).
442    pub fn v_icms(mut self, v: Cents) -> Self {
443        self.v_icms = v;
444        self
445    }
446    /// Set the total desonerated ICMS value (`vICMSDeson`).
447    pub fn v_icms_deson(mut self, v: Cents) -> Self {
448        self.v_icms_deson = v;
449        self
450    }
451    /// Set the total ST calculation base (`vBCST`).
452    pub fn v_bc_st(mut self, v: Cents) -> Self {
453        self.v_bc_st = v;
454        self
455    }
456    /// Set the total ST ICMS value (`vST`).
457    pub fn v_st(mut self, v: Cents) -> Self {
458        self.v_st = v;
459        self
460    }
461    /// Set the total FCP value (`vFCP`).
462    pub fn v_fcp(mut self, v: Cents) -> Self {
463        self.v_fcp = v;
464        self
465    }
466    /// Set the total FCP-ST value (`vFCPST`).
467    pub fn v_fcp_st(mut self, v: Cents) -> Self {
468        self.v_fcp_st = v;
469        self
470    }
471    /// Set the total retained FCP-ST value (`vFCPSTRet`).
472    pub fn v_fcp_st_ret(mut self, v: Cents) -> Self {
473        self.v_fcp_st_ret = v;
474        self
475    }
476    /// Set the total FCP value for destination state (`vFCPUFDest`).
477    pub fn v_fcp_uf_dest(mut self, v: Cents) -> Self {
478        self.v_fcp_uf_dest = v;
479        self
480    }
481    /// Set the total ICMS value for destination state (`vICMSUFDest`).
482    pub fn v_icms_uf_dest(mut self, v: Cents) -> Self {
483        self.v_icms_uf_dest = v;
484        self
485    }
486    /// Set the total ICMS value for origin state (`vICMSUFRemet`).
487    pub fn v_icms_uf_remet(mut self, v: Cents) -> Self {
488        self.v_icms_uf_remet = v;
489        self
490    }
491    /// Set the total monophasic ICMS value (`vICMSMono`).
492    pub fn v_icms_mono(mut self, v: Cents) -> Self {
493        self.v_icms_mono = v;
494        self
495    }
496    /// Set the total monophasic retained ICMS value (`vICMSMonoReten`).
497    pub fn v_icms_mono_reten(mut self, v: Cents) -> Self {
498        self.v_icms_mono_reten = v;
499        self
500    }
501    /// Set the total monophasic previously-collected ICMS value (`vICMSMonoRet`).
502    pub fn v_icms_mono_ret(mut self, v: Cents) -> Self {
503        self.v_icms_mono_ret = v;
504        self
505    }
506    /// Set the desoneration deduction indicator (`indDeduzDeson`).
507    ///
508    /// When `true`, the accumulated `v_icms_deson` is subtracted from vNF.
509    pub fn ind_deduz_deson(mut self, v: bool) -> Self {
510        self.ind_deduz_deson = v;
511        self
512    }
513}
514
515/// Create a zeroed-out [`IcmsTotals`] accumulator.
516///
517/// Equivalent to `IcmsTotals::new()`. Provided as a free function for
518/// ergonomic use in XML builder pipelines.
519///
520/// # Examples
521///
522/// ```
523/// use fiscal_core::tax_icms::create_icms_totals;
524/// let totals = create_icms_totals();
525/// use fiscal_core::newtypes::Cents;
526/// assert_eq!(totals.v_bc, Cents(0));
527/// ```
528pub fn create_icms_totals() -> IcmsTotals {
529    IcmsTotals::default()
530}
531
532// ── IcmsCst enum (normal regime) ────────────────────────────────────────────
533
534/// ICMS CST variant for normal tax regime (Lucro Real / Presumido).
535///
536/// Each variant carries **only** the fields that are valid for that CST,
537/// giving compile-time safety instead of runtime string matching against a
538/// flat struct full of `Option`s.
539///
540/// Simples Nacional / CSOSN variants are **not** included here (see R7).
541#[derive(Debug, Clone)]
542#[non_exhaustive]
543pub enum IcmsCst {
544    /// CST 00 — Tributada integralmente.
545    Cst00 {
546        /// Product origin code (`orig`).
547        orig: String,
548        /// Base calculation modality (`modBC`).
549        mod_bc: String,
550        /// Calculation base value (`vBC`).
551        v_bc: Cents,
552        /// ICMS rate (`pICMS`).
553        p_icms: Rate,
554        /// ICMS value (`vICMS`).
555        v_icms: Cents,
556        /// FCP rate (`pFCP`). Optional.
557        p_fcp: Option<Rate>,
558        /// FCP value (`vFCP`). Optional.
559        v_fcp: Option<Cents>,
560    },
561    /// CST 02 — Tributacao monofasica propria sobre combustiveis.
562    Cst02 {
563        /// Product origin code (`orig`).
564        orig: String,
565        /// Monophasic calculation base quantity (`qBCMono`). Optional.
566        q_bc_mono: Option<i64>,
567        /// Monophasic ad-rem ICMS rate (`adRemICMS`).
568        ad_rem_icms: Rate,
569        /// Monophasic ICMS value (`vICMSMono`).
570        v_icms_mono: Cents,
571    },
572    /// CST 10 — Tributada e com cobranca do ICMS por substituicao tributaria.
573    Cst10 {
574        /// Product origin code (`orig`).
575        orig: String,
576        /// Base calculation modality (`modBC`).
577        mod_bc: String,
578        /// ICMS calculation base value (`vBC`).
579        v_bc: Cents,
580        /// ICMS rate (`pICMS`).
581        p_icms: Rate,
582        /// ICMS value (`vICMS`).
583        v_icms: Cents,
584        /// FCP calculation base (`vBCFCP`). Optional.
585        v_bc_fcp: Option<Cents>,
586        /// FCP rate (`pFCP`). Optional.
587        p_fcp: Option<Rate>,
588        /// FCP value (`vFCP`). Optional.
589        v_fcp: Option<Cents>,
590        /// ST base calculation modality (`modBCST`).
591        mod_bc_st: String,
592        /// ST added value margin (`pMVAST`). Optional.
593        p_mva_st: Option<Rate>,
594        /// ST base reduction rate (`pRedBCST`). Optional.
595        p_red_bc_st: Option<Rate>,
596        /// ST calculation base value (`vBCST`).
597        v_bc_st: Cents,
598        /// ST rate (`pICMSST`).
599        p_icms_st: Rate,
600        /// ST ICMS value (`vICMSST`).
601        v_icms_st: Cents,
602        /// FCP-ST calculation base (`vBCFCPST`). Optional.
603        v_bc_fcp_st: Option<Cents>,
604        /// FCP-ST rate (`pFCPST`). Optional.
605        p_fcp_st: Option<Rate>,
606        /// FCP-ST value (`vFCPST`). Optional.
607        v_fcp_st: Option<Cents>,
608        /// ST desonerated ICMS value (`vICMSSTDeson`). Optional.
609        v_icms_st_deson: Option<Cents>,
610        /// ST desoneration reason code (`motDesICMSST`). Optional.
611        mot_des_icms_st: Option<String>,
612    },
613    /// CST 15 — Tributacao monofasica propria e com responsabilidade pela
614    /// retencao sobre combustiveis.
615    Cst15 {
616        /// Product origin code (`orig`).
617        orig: String,
618        /// Monophasic calculation base quantity (`qBCMono`). Optional.
619        q_bc_mono: Option<i64>,
620        /// Monophasic ad-rem ICMS rate (`adRemICMS`).
621        ad_rem_icms: Rate,
622        /// Monophasic ICMS value (`vICMSMono`).
623        v_icms_mono: Cents,
624        /// Retained monophasic calculation base quantity (`qBCMonoReten`). Optional.
625        q_bc_mono_reten: Option<i64>,
626        /// Retained monophasic ad-rem ICMS rate (`adRemICMSReten`).
627        ad_rem_icms_reten: Rate,
628        /// Retained monophasic ICMS value (`vICMSMonoReten`).
629        v_icms_mono_reten: Cents,
630        /// Ad-rem reduction rate (`pRedAdRem`). Optional.
631        p_red_ad_rem: Option<Rate>,
632        /// Ad-rem reduction reason (`motRedAdRem`). Required when `p_red_ad_rem` is set.
633        mot_red_ad_rem: Option<String>,
634    },
635    /// CST 20 — Com reducao de base de calculo.
636    Cst20 {
637        /// Product origin code (`orig`).
638        orig: String,
639        /// Base calculation modality (`modBC`).
640        mod_bc: String,
641        /// Base reduction rate (`pRedBC`).
642        p_red_bc: Rate,
643        /// Calculation base value (`vBC`).
644        v_bc: Cents,
645        /// ICMS rate (`pICMS`).
646        p_icms: Rate,
647        /// ICMS value (`vICMS`).
648        v_icms: Cents,
649        /// FCP calculation base (`vBCFCP`). Optional.
650        v_bc_fcp: Option<Cents>,
651        /// FCP rate (`pFCP`). Optional.
652        p_fcp: Option<Rate>,
653        /// FCP value (`vFCP`). Optional.
654        v_fcp: Option<Cents>,
655        /// Desonerated ICMS value (`vICMSDeson`). Optional.
656        v_icms_deson: Option<Cents>,
657        /// Desoneration reason code (`motDesICMS`). Optional.
658        mot_des_icms: Option<String>,
659        /// Desoneration deduction indicator (`indDeduzDeson`). Optional.
660        ind_deduz_deson: Option<String>,
661    },
662    /// CST 30 — Isenta ou nao tributada e com cobranca do ICMS por ST.
663    Cst30 {
664        /// Product origin code (`orig`).
665        orig: String,
666        /// ST base calculation modality (`modBCST`).
667        mod_bc_st: String,
668        /// ST added value margin (`pMVAST`). Optional.
669        p_mva_st: Option<Rate>,
670        /// ST base reduction rate (`pRedBCST`). Optional.
671        p_red_bc_st: Option<Rate>,
672        /// ST calculation base value (`vBCST`).
673        v_bc_st: Cents,
674        /// ST rate (`pICMSST`).
675        p_icms_st: Rate,
676        /// ST ICMS value (`vICMSST`).
677        v_icms_st: Cents,
678        /// FCP-ST calculation base (`vBCFCPST`). Optional.
679        v_bc_fcp_st: Option<Cents>,
680        /// FCP-ST rate (`pFCPST`). Optional.
681        p_fcp_st: Option<Rate>,
682        /// FCP-ST value (`vFCPST`). Optional.
683        v_fcp_st: Option<Cents>,
684        /// Desonerated ICMS value (`vICMSDeson`). Optional.
685        v_icms_deson: Option<Cents>,
686        /// Desoneration reason code (`motDesICMS`). Optional.
687        mot_des_icms: Option<String>,
688        /// Desoneration deduction indicator (`indDeduzDeson`). Optional.
689        ind_deduz_deson: Option<String>,
690    },
691    /// CST 40 — Isenta.
692    Cst40 {
693        /// Product origin code (`orig`).
694        orig: String,
695        /// Desonerated ICMS value (`vICMSDeson`). Optional.
696        v_icms_deson: Option<Cents>,
697        /// Desoneration reason code (`motDesICMS`). Optional.
698        mot_des_icms: Option<String>,
699        /// Desoneration deduction indicator (`indDeduzDeson`). Optional.
700        ind_deduz_deson: Option<String>,
701    },
702    /// CST 41 — Nao tributada.
703    Cst41 {
704        /// Product origin code (`orig`).
705        orig: String,
706        /// Desonerated ICMS value (`vICMSDeson`). Optional.
707        v_icms_deson: Option<Cents>,
708        /// Desoneration reason code (`motDesICMS`). Optional.
709        mot_des_icms: Option<String>,
710        /// Desoneration deduction indicator (`indDeduzDeson`). Optional.
711        ind_deduz_deson: Option<String>,
712    },
713    /// CST 50 — Suspensao.
714    Cst50 {
715        /// Product origin code (`orig`).
716        orig: String,
717        /// Desonerated ICMS value (`vICMSDeson`). Optional.
718        v_icms_deson: Option<Cents>,
719        /// Desoneration reason code (`motDesICMS`). Optional.
720        mot_des_icms: Option<String>,
721        /// Desoneration deduction indicator (`indDeduzDeson`). Optional.
722        ind_deduz_deson: Option<String>,
723    },
724    /// CST 51 — Diferimento.
725    Cst51 {
726        /// Product origin code (`orig`).
727        orig: String,
728        /// Base calculation modality (`modBC`). Optional.
729        mod_bc: Option<String>,
730        /// Base reduction rate (`pRedBC`). Optional.
731        p_red_bc: Option<Rate>,
732        /// Fiscal benefit code for base reduction (`cBenefRBC`). Optional.
733        c_benef_rbc: Option<String>,
734        /// Calculation base value (`vBC`). Optional.
735        v_bc: Option<Cents>,
736        /// ICMS rate (`pICMS`). Optional.
737        p_icms: Option<Rate>,
738        /// ICMS value before deferral (`vICMSOp`). Optional.
739        v_icms_op: Option<Cents>,
740        /// Deferral percentage (`pDif`). Optional.
741        p_dif: Option<Rate>,
742        /// Deferred ICMS value (`vICMSDif`). Optional.
743        v_icms_dif: Option<Cents>,
744        /// ICMS value payable after deferral (`vICMS`). Optional.
745        v_icms: Option<Cents>,
746        /// FCP calculation base (`vBCFCP`). Optional.
747        v_bc_fcp: Option<Cents>,
748        /// FCP rate (`pFCP`). Optional.
749        p_fcp: Option<Rate>,
750        /// FCP value (`vFCP`). Optional.
751        v_fcp: Option<Cents>,
752        /// FCP deferral rate (`pFCPDif`). Optional.
753        p_fcp_dif: Option<Rate>,
754        /// FCP deferred value (`vFCPDif`). Optional.
755        v_fcp_dif: Option<Cents>,
756        /// FCP effective value after deferral (`vFCPEfet`). Optional.
757        v_fcp_efet: Option<Cents>,
758    },
759    /// CST 53 — Tributacao monofasica sobre combustiveis com recolhimento
760    /// diferido.
761    Cst53 {
762        /// Product origin code (`orig`).
763        orig: String,
764        /// Monophasic calculation base quantity (`qBCMono`). Optional.
765        q_bc_mono: Option<i64>,
766        /// Monophasic ad-rem ICMS rate (`adRemICMS`). Optional.
767        ad_rem_icms: Option<Rate>,
768        /// Monophasic ICMS value before deferral (`vICMSMonoOp`). Optional.
769        v_icms_mono_op: Option<Cents>,
770        /// Deferral percentage (`pDif`). Optional.
771        p_dif: Option<Rate>,
772        /// Deferred monophasic ICMS value (`vICMSMonoDif`). Optional.
773        v_icms_mono_dif: Option<Cents>,
774        /// Monophasic ICMS value payable after deferral (`vICMSMono`). Optional.
775        v_icms_mono: Option<Cents>,
776    },
777    /// CST 60 — ICMS cobrado anteriormente por substituicao tributaria.
778    Cst60 {
779        /// Product origin code (`orig`).
780        orig: String,
781        /// ST retained calculation base (`vBCSTRet`). Optional.
782        v_bc_st_ret: Option<Cents>,
783        /// ST rate at retention (`pST`). Optional.
784        p_st: Option<Rate>,
785        /// ICMS value paid by the substitutor (`vICMSSubstituto`). Optional.
786        v_icms_substituto: Option<Cents>,
787        /// Retained ST ICMS value (`vICMSSTRet`). Optional.
788        v_icms_st_ret: Option<Cents>,
789        /// FCP-ST retained calculation base (`vBCFCPSTRet`). Optional.
790        v_bc_fcp_st_ret: Option<Cents>,
791        /// FCP-ST retained rate (`pFCPSTRet`). Optional.
792        p_fcp_st_ret: Option<Rate>,
793        /// FCP-ST retained value (`vFCPSTRet`). Optional.
794        v_fcp_st_ret: Option<Cents>,
795        /// Effective base reduction rate (`pRedBCEfet`). Optional.
796        p_red_bc_efet: Option<Rate>,
797        /// Effective calculation base (`vBCEfet`). Optional.
798        v_bc_efet: Option<Cents>,
799        /// Effective ICMS rate (`pICMSEfet`). Optional.
800        p_icms_efet: Option<Rate>,
801        /// Effective ICMS value (`vICMSEfet`). Optional.
802        v_icms_efet: Option<Cents>,
803    },
804    /// CST 61 — Tributacao monofasica sobre combustiveis cobrada anteriormente.
805    Cst61 {
806        /// Product origin code (`orig`).
807        orig: String,
808        /// Monophasic previously-collected calculation base quantity (`qBCMonoRet`). Optional.
809        q_bc_mono_ret: Option<i64>,
810        /// Monophasic previously-collected ad-rem ICMS rate (`adRemICMSRet`).
811        ad_rem_icms_ret: Rate,
812        /// Monophasic previously-collected ICMS value (`vICMSMonoRet`).
813        v_icms_mono_ret: Cents,
814    },
815    /// CST 70 — Reducao de base de calculo e cobranca do ICMS por ST.
816    Cst70 {
817        /// Product origin code (`orig`).
818        orig: String,
819        /// Base calculation modality (`modBC`).
820        mod_bc: String,
821        /// Base reduction rate (`pRedBC`).
822        p_red_bc: Rate,
823        /// ICMS calculation base value (`vBC`).
824        v_bc: Cents,
825        /// ICMS rate (`pICMS`).
826        p_icms: Rate,
827        /// ICMS value (`vICMS`).
828        v_icms: Cents,
829        /// FCP calculation base (`vBCFCP`). Optional.
830        v_bc_fcp: Option<Cents>,
831        /// FCP rate (`pFCP`). Optional.
832        p_fcp: Option<Rate>,
833        /// FCP value (`vFCP`). Optional.
834        v_fcp: Option<Cents>,
835        /// ST base calculation modality (`modBCST`).
836        mod_bc_st: String,
837        /// ST added value margin (`pMVAST`). Optional.
838        p_mva_st: Option<Rate>,
839        /// ST base reduction rate (`pRedBCST`). Optional.
840        p_red_bc_st: Option<Rate>,
841        /// ST calculation base value (`vBCST`).
842        v_bc_st: Cents,
843        /// ST rate (`pICMSST`).
844        p_icms_st: Rate,
845        /// ST ICMS value (`vICMSST`).
846        v_icms_st: Cents,
847        /// FCP-ST calculation base (`vBCFCPST`). Optional.
848        v_bc_fcp_st: Option<Cents>,
849        /// FCP-ST rate (`pFCPST`). Optional.
850        p_fcp_st: Option<Rate>,
851        /// FCP-ST value (`vFCPST`). Optional.
852        v_fcp_st: Option<Cents>,
853        /// Desonerated ICMS value (`vICMSDeson`). Optional.
854        v_icms_deson: Option<Cents>,
855        /// Desoneration reason code (`motDesICMS`). Optional.
856        mot_des_icms: Option<String>,
857        /// Desoneration deduction indicator (`indDeduzDeson`). Optional.
858        ind_deduz_deson: Option<String>,
859        /// ST desonerated ICMS value (`vICMSSTDeson`). Optional.
860        v_icms_st_deson: Option<Cents>,
861        /// ST desoneration reason code (`motDesICMSST`). Optional.
862        mot_des_icms_st: Option<String>,
863    },
864    /// CST 90 — Outros.
865    Cst90 {
866        /// Product origin code (`orig`).
867        orig: String,
868        /// Base calculation modality (`modBC`). Optional.
869        mod_bc: Option<String>,
870        /// ICMS calculation base value (`vBC`). Optional.
871        v_bc: Option<Cents>,
872        /// Base reduction rate (`pRedBC`). Optional.
873        p_red_bc: Option<Rate>,
874        /// Fiscal benefit code for base reduction (`cBenefRBC`). Optional.
875        c_benef_rbc: Option<String>,
876        /// ICMS rate (`pICMS`). Optional.
877        p_icms: Option<Rate>,
878        /// ICMS value before deferral (`vICMSOp`). Optional.
879        v_icms_op: Option<Cents>,
880        /// Deferral percentage (`pDif`). Optional.
881        p_dif: Option<Rate>,
882        /// Deferred ICMS value (`vICMSDif`). Optional.
883        v_icms_dif: Option<Cents>,
884        /// ICMS value (`vICMS`). Optional.
885        v_icms: Option<Cents>,
886        /// FCP calculation base (`vBCFCP`). Optional.
887        v_bc_fcp: Option<Cents>,
888        /// FCP rate (`pFCP`). Optional.
889        p_fcp: Option<Rate>,
890        /// FCP value (`vFCP`). Optional.
891        v_fcp: Option<Cents>,
892        /// FCP deferral rate (`pFCPDif`). Optional.
893        p_fcp_dif: Option<Rate>,
894        /// FCP deferred value (`vFCPDif`). Optional.
895        v_fcp_dif: Option<Cents>,
896        /// FCP effective value (`vFCPEfet`). Optional.
897        v_fcp_efet: Option<Cents>,
898        /// ST base calculation modality (`modBCST`). Optional.
899        mod_bc_st: Option<String>,
900        /// ST added value margin (`pMVAST`). Optional.
901        p_mva_st: Option<Rate>,
902        /// ST base reduction rate (`pRedBCST`). Optional.
903        p_red_bc_st: Option<Rate>,
904        /// ST calculation base value (`vBCST`). Optional.
905        v_bc_st: Option<Cents>,
906        /// ST rate (`pICMSST`). Optional.
907        p_icms_st: Option<Rate>,
908        /// ST ICMS value (`vICMSST`). Optional.
909        v_icms_st: Option<Cents>,
910        /// FCP-ST calculation base (`vBCFCPST`). Optional.
911        v_bc_fcp_st: Option<Cents>,
912        /// FCP-ST rate (`pFCPST`). Optional.
913        p_fcp_st: Option<Rate>,
914        /// FCP-ST value (`vFCPST`). Optional.
915        v_fcp_st: Option<Cents>,
916        /// Desonerated ICMS value (`vICMSDeson`). Optional.
917        v_icms_deson: Option<Cents>,
918        /// Desoneration reason code (`motDesICMS`). Optional.
919        mot_des_icms: Option<String>,
920        /// Desoneration deduction indicator (`indDeduzDeson`). Optional.
921        ind_deduz_deson: Option<String>,
922        /// ST desonerated ICMS value (`vICMSSTDeson`). Optional.
923        v_icms_st_deson: Option<Cents>,
924        /// ST desoneration reason code (`motDesICMSST`). Optional.
925        mot_des_icms_st: Option<String>,
926    },
927}
928
929impl IcmsCst {
930    /// Return the two-character CST code for this variant.
931    pub fn cst_code(&self) -> &str {
932        match self {
933            Self::Cst00 { .. } => "00",
934            Self::Cst02 { .. } => "02",
935            Self::Cst10 { .. } => "10",
936            Self::Cst15 { .. } => "15",
937            Self::Cst20 { .. } => "20",
938            Self::Cst30 { .. } => "30",
939            Self::Cst40 { .. } => "40",
940            Self::Cst41 { .. } => "41",
941            Self::Cst50 { .. } => "50",
942            Self::Cst51 { .. } => "51",
943            Self::Cst53 { .. } => "53",
944            Self::Cst60 { .. } => "60",
945            Self::Cst61 { .. } => "61",
946            Self::Cst70 { .. } => "70",
947            Self::Cst90 { .. } => "90",
948        }
949    }
950}
951
952/// Build the ICMS XML fragment and accumulate totals from a typed [`IcmsCst`]
953/// variant.
954///
955/// This is the compile-time-safe counterpart of the original
956/// [`build_icms_xml`] code path for normal-regime CSTs. It can be used
957/// directly by new code that already has an `IcmsCst`, or indirectly via the
958/// unchanged [`build_icms_xml`] public API (which converts internally).
959///
960/// # Errors
961///
962/// Returns [`FiscalError`] if XML field serialization fails (should not happen
963/// when the enum is correctly constructed).
964pub fn build_icms_cst_xml(
965    cst: &IcmsCst,
966    totals: &mut IcmsTotals,
967) -> Result<(String, Vec<TaxField>), FiscalError> {
968    match cst {
969        IcmsCst::Cst00 {
970            orig,
971            mod_bc,
972            v_bc,
973            p_icms,
974            v_icms,
975            p_fcp,
976            v_fcp,
977        } => {
978            totals.v_bc = accum(totals.v_bc, Some(*v_bc));
979            totals.v_icms = accum(totals.v_icms, Some(*v_icms));
980            totals.v_fcp = accum(totals.v_fcp, *v_fcp);
981            let fields = filter_fields(vec![
982                Some(TaxField::new("orig", orig.as_str())),
983                Some(TaxField::new("CST", "00")),
984                Some(TaxField::new("modBC", mod_bc.as_str())),
985                Some(TaxField::new("vBC", fc2(Some(*v_bc)).unwrap())),
986                Some(TaxField::new("pICMS", fc4(Some(*p_icms)).unwrap())),
987                Some(TaxField::new("vICMS", fc2(Some(*v_icms)).unwrap())),
988                optional_field("pFCP", fc4(*p_fcp).as_deref()),
989                optional_field("vFCP", fc2(*v_fcp).as_deref()),
990            ]);
991            Ok(("ICMS00".to_string(), fields))
992        }
993
994        IcmsCst::Cst02 {
995            orig,
996            q_bc_mono,
997            ad_rem_icms,
998            v_icms_mono,
999        } => {
1000            totals.q_bc_mono = accum_raw(totals.q_bc_mono, *q_bc_mono);
1001            totals.v_icms_mono = accum(totals.v_icms_mono, Some(*v_icms_mono));
1002            let fields = filter_fields(vec![
1003                Some(TaxField::new("orig", orig.as_str())),
1004                Some(TaxField::new("CST", "02")),
1005                optional_field("qBCMono", fc4_raw(*q_bc_mono).as_deref()),
1006                Some(TaxField::new("adRemICMS", fc4(Some(*ad_rem_icms)).unwrap())),
1007                Some(TaxField::new("vICMSMono", fc2(Some(*v_icms_mono)).unwrap())),
1008            ]);
1009            Ok(("ICMS02".to_string(), fields))
1010        }
1011
1012        IcmsCst::Cst10 {
1013            orig,
1014            mod_bc,
1015            v_bc,
1016            p_icms,
1017            v_icms,
1018            v_bc_fcp,
1019            p_fcp,
1020            v_fcp,
1021            mod_bc_st,
1022            p_mva_st,
1023            p_red_bc_st,
1024            v_bc_st,
1025            p_icms_st,
1026            v_icms_st,
1027            v_bc_fcp_st,
1028            p_fcp_st,
1029            v_fcp_st,
1030            v_icms_st_deson,
1031            mot_des_icms_st,
1032        } => {
1033            totals.v_bc = accum(totals.v_bc, Some(*v_bc));
1034            totals.v_icms = accum(totals.v_icms, Some(*v_icms));
1035            totals.v_bc_st = accum(totals.v_bc_st, Some(*v_bc_st));
1036            totals.v_st = accum(totals.v_st, Some(*v_icms_st));
1037            totals.v_fcp_st = accum(totals.v_fcp_st, *v_fcp_st);
1038            totals.v_fcp = accum(totals.v_fcp, *v_fcp);
1039            let mut fields_opt: Vec<Option<TaxField>> = vec![
1040                Some(TaxField::new("orig", orig.as_str())),
1041                Some(TaxField::new("CST", "10")),
1042                Some(TaxField::new("modBC", mod_bc.as_str())),
1043                Some(TaxField::new("vBC", fc2(Some(*v_bc)).unwrap())),
1044                Some(TaxField::new("pICMS", fc4(Some(*p_icms)).unwrap())),
1045                Some(TaxField::new("vICMS", fc2(Some(*v_icms)).unwrap())),
1046            ];
1047            // FCP fields
1048            fields_opt.push(optional_field("vBCFCP", fc2(*v_bc_fcp).as_deref()));
1049            fields_opt.push(optional_field("pFCP", fc4(*p_fcp).as_deref()));
1050            fields_opt.push(optional_field("vFCP", fc2(*v_fcp).as_deref()));
1051            // ST fields
1052            fields_opt.push(Some(TaxField::new("modBCST", mod_bc_st.as_str())));
1053            if let Some(v) = p_mva_st {
1054                fields_opt.push(Some(TaxField::new("pMVAST", fc4(Some(*v)).unwrap())));
1055            }
1056            if let Some(v) = p_red_bc_st {
1057                fields_opt.push(Some(TaxField::new("pRedBCST", fc4(Some(*v)).unwrap())));
1058            }
1059            fields_opt.push(Some(TaxField::new("vBCST", fc2(Some(*v_bc_st)).unwrap())));
1060            fields_opt.push(Some(TaxField::new(
1061                "pICMSST",
1062                fc4(Some(*p_icms_st)).unwrap(),
1063            )));
1064            fields_opt.push(Some(TaxField::new(
1065                "vICMSST",
1066                fc2(Some(*v_icms_st)).unwrap(),
1067            )));
1068            // FCP ST fields
1069            fields_opt.push(optional_field("vBCFCPST", fc2(*v_bc_fcp_st).as_deref()));
1070            fields_opt.push(optional_field("pFCPST", fc4(*p_fcp_st).as_deref()));
1071            fields_opt.push(optional_field("vFCPST", fc2(*v_fcp_st).as_deref()));
1072            // ST desoneration
1073            fields_opt.push(optional_field(
1074                "vICMSSTDeson",
1075                fc2(*v_icms_st_deson).as_deref(),
1076            ));
1077            fields_opt.push(optional_field("motDesICMSST", mot_des_icms_st.as_deref()));
1078            Ok(("ICMS10".to_string(), filter_fields(fields_opt)))
1079        }
1080
1081        IcmsCst::Cst15 {
1082            orig,
1083            q_bc_mono,
1084            ad_rem_icms,
1085            v_icms_mono,
1086            q_bc_mono_reten,
1087            ad_rem_icms_reten,
1088            v_icms_mono_reten,
1089            p_red_ad_rem,
1090            mot_red_ad_rem,
1091        } => {
1092            totals.q_bc_mono = accum_raw(totals.q_bc_mono, *q_bc_mono);
1093            totals.v_icms_mono = accum(totals.v_icms_mono, Some(*v_icms_mono));
1094            totals.q_bc_mono_reten = accum_raw(totals.q_bc_mono_reten, *q_bc_mono_reten);
1095            totals.v_icms_mono_reten = accum(totals.v_icms_mono_reten, Some(*v_icms_mono_reten));
1096            let mut fields = filter_fields(vec![
1097                Some(TaxField::new("orig", orig.as_str())),
1098                Some(TaxField::new("CST", "15")),
1099                optional_field("qBCMono", fc4_raw(*q_bc_mono).as_deref()),
1100                Some(TaxField::new("adRemICMS", fc4(Some(*ad_rem_icms)).unwrap())),
1101                Some(TaxField::new("vICMSMono", fc2(Some(*v_icms_mono)).unwrap())),
1102                optional_field("qBCMonoReten", fc4_raw(*q_bc_mono_reten).as_deref()),
1103                Some(TaxField::new(
1104                    "adRemICMSReten",
1105                    fc4(Some(*ad_rem_icms_reten)).unwrap(),
1106                )),
1107                Some(TaxField::new(
1108                    "vICMSMonoReten",
1109                    fc2(Some(*v_icms_mono_reten)).unwrap(),
1110                )),
1111            ]);
1112            if p_red_ad_rem.is_some() {
1113                fields.push(TaxField::new("pRedAdRem", fc4(*p_red_ad_rem).unwrap()));
1114                fields.push(required_field("motRedAdRem", mot_red_ad_rem.as_deref())?);
1115            }
1116            Ok(("ICMS15".to_string(), fields))
1117        }
1118
1119        IcmsCst::Cst20 {
1120            orig,
1121            mod_bc,
1122            p_red_bc,
1123            v_bc,
1124            p_icms,
1125            v_icms,
1126            v_bc_fcp,
1127            p_fcp,
1128            v_fcp,
1129            v_icms_deson,
1130            mot_des_icms,
1131            ind_deduz_deson,
1132        } => {
1133            totals.v_icms_deson = accum(totals.v_icms_deson, *v_icms_deson);
1134            if ind_deduz_deson.as_deref() == Some("1") {
1135                totals.ind_deduz_deson = true;
1136            }
1137            totals.v_bc = accum(totals.v_bc, Some(*v_bc));
1138            totals.v_icms = accum(totals.v_icms, Some(*v_icms));
1139            totals.v_fcp = accum(totals.v_fcp, *v_fcp);
1140            let mut fields_opt: Vec<Option<TaxField>> = vec![
1141                Some(TaxField::new("orig", orig.as_str())),
1142                Some(TaxField::new("CST", "20")),
1143                Some(TaxField::new("modBC", mod_bc.as_str())),
1144                Some(TaxField::new("pRedBC", fc4(Some(*p_red_bc)).unwrap())),
1145                Some(TaxField::new("vBC", fc2(Some(*v_bc)).unwrap())),
1146                Some(TaxField::new("pICMS", fc4(Some(*p_icms)).unwrap())),
1147                Some(TaxField::new("vICMS", fc2(Some(*v_icms)).unwrap())),
1148            ];
1149            // FCP fields
1150            fields_opt.push(optional_field("vBCFCP", fc2(*v_bc_fcp).as_deref()));
1151            fields_opt.push(optional_field("pFCP", fc4(*p_fcp).as_deref()));
1152            fields_opt.push(optional_field("vFCP", fc2(*v_fcp).as_deref()));
1153            // Desoneration
1154            fields_opt.push(optional_field("vICMSDeson", fc2(*v_icms_deson).as_deref()));
1155            fields_opt.push(optional_field("motDesICMS", mot_des_icms.as_deref()));
1156            fields_opt.push(optional_field("indDeduzDeson", ind_deduz_deson.as_deref()));
1157            Ok(("ICMS20".to_string(), filter_fields(fields_opt)))
1158        }
1159
1160        IcmsCst::Cst30 {
1161            orig,
1162            mod_bc_st,
1163            p_mva_st,
1164            p_red_bc_st,
1165            v_bc_st,
1166            p_icms_st,
1167            v_icms_st,
1168            v_bc_fcp_st,
1169            p_fcp_st,
1170            v_fcp_st,
1171            v_icms_deson,
1172            mot_des_icms,
1173            ind_deduz_deson,
1174        } => {
1175            totals.v_icms_deson = accum(totals.v_icms_deson, *v_icms_deson);
1176            if ind_deduz_deson.as_deref() == Some("1") {
1177                totals.ind_deduz_deson = true;
1178            }
1179            totals.v_bc_st = accum(totals.v_bc_st, Some(*v_bc_st));
1180            totals.v_st = accum(totals.v_st, Some(*v_icms_st));
1181            totals.v_fcp_st = accum(totals.v_fcp_st, *v_fcp_st);
1182            let mut fields_opt: Vec<Option<TaxField>> = vec![
1183                Some(TaxField::new("orig", orig.as_str())),
1184                Some(TaxField::new("CST", "30")),
1185            ];
1186            // ST fields
1187            fields_opt.push(Some(TaxField::new("modBCST", mod_bc_st.as_str())));
1188            if let Some(v) = p_mva_st {
1189                fields_opt.push(Some(TaxField::new("pMVAST", fc4(Some(*v)).unwrap())));
1190            }
1191            if let Some(v) = p_red_bc_st {
1192                fields_opt.push(Some(TaxField::new("pRedBCST", fc4(Some(*v)).unwrap())));
1193            }
1194            fields_opt.push(Some(TaxField::new("vBCST", fc2(Some(*v_bc_st)).unwrap())));
1195            fields_opt.push(Some(TaxField::new(
1196                "pICMSST",
1197                fc4(Some(*p_icms_st)).unwrap(),
1198            )));
1199            fields_opt.push(Some(TaxField::new(
1200                "vICMSST",
1201                fc2(Some(*v_icms_st)).unwrap(),
1202            )));
1203            // FCP ST
1204            fields_opt.push(optional_field("vBCFCPST", fc2(*v_bc_fcp_st).as_deref()));
1205            fields_opt.push(optional_field("pFCPST", fc4(*p_fcp_st).as_deref()));
1206            fields_opt.push(optional_field("vFCPST", fc2(*v_fcp_st).as_deref()));
1207            // Desoneration
1208            fields_opt.push(optional_field("vICMSDeson", fc2(*v_icms_deson).as_deref()));
1209            fields_opt.push(optional_field("motDesICMS", mot_des_icms.as_deref()));
1210            fields_opt.push(optional_field("indDeduzDeson", ind_deduz_deson.as_deref()));
1211            Ok(("ICMS30".to_string(), filter_fields(fields_opt)))
1212        }
1213
1214        IcmsCst::Cst40 {
1215            orig,
1216            v_icms_deson,
1217            mot_des_icms,
1218            ind_deduz_deson,
1219        }
1220        | IcmsCst::Cst41 {
1221            orig,
1222            v_icms_deson,
1223            mot_des_icms,
1224            ind_deduz_deson,
1225        }
1226        | IcmsCst::Cst50 {
1227            orig,
1228            v_icms_deson,
1229            mot_des_icms,
1230            ind_deduz_deson,
1231        } => {
1232            totals.v_icms_deson = accum(totals.v_icms_deson, *v_icms_deson);
1233            if ind_deduz_deson.as_deref() == Some("1") {
1234                totals.ind_deduz_deson = true;
1235            }
1236            let mut fields_opt: Vec<Option<TaxField>> = vec![
1237                Some(TaxField::new("orig", orig.as_str())),
1238                Some(TaxField::new("CST", cst.cst_code())),
1239            ];
1240            // Desoneration
1241            fields_opt.push(optional_field("vICMSDeson", fc2(*v_icms_deson).as_deref()));
1242            fields_opt.push(optional_field("motDesICMS", mot_des_icms.as_deref()));
1243            fields_opt.push(optional_field("indDeduzDeson", ind_deduz_deson.as_deref()));
1244            Ok(("ICMS40".to_string(), filter_fields(fields_opt)))
1245        }
1246
1247        IcmsCst::Cst51 {
1248            orig,
1249            mod_bc,
1250            p_red_bc,
1251            c_benef_rbc,
1252            v_bc,
1253            p_icms,
1254            v_icms_op,
1255            p_dif,
1256            v_icms_dif,
1257            v_icms,
1258            v_bc_fcp,
1259            p_fcp,
1260            v_fcp,
1261            p_fcp_dif,
1262            v_fcp_dif,
1263            v_fcp_efet,
1264        } => {
1265            totals.v_bc = accum(totals.v_bc, *v_bc);
1266            totals.v_icms = accum(totals.v_icms, *v_icms);
1267            totals.v_fcp = accum(totals.v_fcp, *v_fcp);
1268            let fields_opt: Vec<Option<TaxField>> = vec![
1269                Some(TaxField::new("orig", orig.as_str())),
1270                Some(TaxField::new("CST", "51")),
1271                optional_field("modBC", mod_bc.as_deref()),
1272                optional_field("pRedBC", fc4(*p_red_bc).as_deref()),
1273                optional_field("cBenefRBC", c_benef_rbc.as_deref()),
1274                optional_field("vBC", fc2(*v_bc).as_deref()),
1275                optional_field("pICMS", fc4(*p_icms).as_deref()),
1276                optional_field("vICMSOp", fc2(*v_icms_op).as_deref()),
1277                optional_field("pDif", fc4(*p_dif).as_deref()),
1278                optional_field("vICMSDif", fc2(*v_icms_dif).as_deref()),
1279                optional_field("vICMS", fc2(*v_icms).as_deref()),
1280                optional_field("vBCFCP", fc2(*v_bc_fcp).as_deref()),
1281                optional_field("pFCP", fc4(*p_fcp).as_deref()),
1282                optional_field("vFCP", fc2(*v_fcp).as_deref()),
1283                optional_field("pFCPDif", fc4(*p_fcp_dif).as_deref()),
1284                optional_field("vFCPDif", fc2(*v_fcp_dif).as_deref()),
1285                optional_field("vFCPEfet", fc2(*v_fcp_efet).as_deref()),
1286            ];
1287            Ok(("ICMS51".to_string(), filter_fields(fields_opt)))
1288        }
1289
1290        IcmsCst::Cst53 {
1291            orig,
1292            q_bc_mono,
1293            ad_rem_icms,
1294            v_icms_mono_op,
1295            p_dif,
1296            v_icms_mono_dif,
1297            v_icms_mono,
1298        } => {
1299            totals.q_bc_mono = accum_raw(totals.q_bc_mono, *q_bc_mono);
1300            totals.v_icms_mono = accum(totals.v_icms_mono, *v_icms_mono);
1301            totals.q_bc_mono_reten = accum_raw(totals.q_bc_mono_reten, None);
1302            totals.v_icms_mono_reten = accum(totals.v_icms_mono_reten, None);
1303            let fields_opt: Vec<Option<TaxField>> = vec![
1304                Some(TaxField::new("orig", orig.as_str())),
1305                Some(TaxField::new("CST", "53")),
1306                optional_field("qBCMono", fc4_raw(*q_bc_mono).as_deref()),
1307                optional_field("adRemICMS", fc4(*ad_rem_icms).as_deref()),
1308                optional_field("vICMSMonoOp", fc2(*v_icms_mono_op).as_deref()),
1309                optional_field("pDif", fc4(*p_dif).as_deref()),
1310                optional_field("vICMSMonoDif", fc2(*v_icms_mono_dif).as_deref()),
1311                optional_field("vICMSMono", fc2(*v_icms_mono).as_deref()),
1312            ];
1313            Ok(("ICMS53".to_string(), filter_fields(fields_opt)))
1314        }
1315
1316        IcmsCst::Cst60 {
1317            orig,
1318            v_bc_st_ret,
1319            p_st,
1320            v_icms_substituto,
1321            v_icms_st_ret,
1322            v_bc_fcp_st_ret,
1323            p_fcp_st_ret,
1324            v_fcp_st_ret,
1325            p_red_bc_efet,
1326            v_bc_efet,
1327            p_icms_efet,
1328            v_icms_efet,
1329        } => {
1330            totals.v_fcp_st_ret = accum(totals.v_fcp_st_ret, *v_fcp_st_ret);
1331            let fields_opt: Vec<Option<TaxField>> = vec![
1332                Some(TaxField::new("orig", orig.as_str())),
1333                Some(TaxField::new("CST", "60")),
1334                optional_field("vBCSTRet", fc2(*v_bc_st_ret).as_deref()),
1335                optional_field("pST", fc4(*p_st).as_deref()),
1336                optional_field("vICMSSubstituto", fc2(*v_icms_substituto).as_deref()),
1337                optional_field("vICMSSTRet", fc2(*v_icms_st_ret).as_deref()),
1338                optional_field("vBCFCPSTRet", fc2(*v_bc_fcp_st_ret).as_deref()),
1339                optional_field("pFCPSTRet", fc4(*p_fcp_st_ret).as_deref()),
1340                optional_field("vFCPSTRet", fc2(*v_fcp_st_ret).as_deref()),
1341                optional_field("pRedBCEfet", fc4(*p_red_bc_efet).as_deref()),
1342                optional_field("vBCEfet", fc2(*v_bc_efet).as_deref()),
1343                optional_field("pICMSEfet", fc4(*p_icms_efet).as_deref()),
1344                optional_field("vICMSEfet", fc2(*v_icms_efet).as_deref()),
1345            ];
1346            Ok(("ICMS60".to_string(), filter_fields(fields_opt)))
1347        }
1348
1349        IcmsCst::Cst61 {
1350            orig,
1351            q_bc_mono_ret,
1352            ad_rem_icms_ret,
1353            v_icms_mono_ret,
1354        } => {
1355            totals.q_bc_mono_ret = accum_raw(totals.q_bc_mono_ret, *q_bc_mono_ret);
1356            totals.v_icms_mono_ret = accum(totals.v_icms_mono_ret, Some(*v_icms_mono_ret));
1357            let fields = filter_fields(vec![
1358                Some(TaxField::new("orig", orig.as_str())),
1359                Some(TaxField::new("CST", "61")),
1360                optional_field("qBCMonoRet", fc4_raw(*q_bc_mono_ret).as_deref()),
1361                Some(TaxField::new(
1362                    "adRemICMSRet",
1363                    fc4(Some(*ad_rem_icms_ret)).unwrap(),
1364                )),
1365                Some(TaxField::new(
1366                    "vICMSMonoRet",
1367                    fc2(Some(*v_icms_mono_ret)).unwrap(),
1368                )),
1369            ]);
1370            Ok(("ICMS61".to_string(), fields))
1371        }
1372
1373        IcmsCst::Cst70 {
1374            orig,
1375            mod_bc,
1376            p_red_bc,
1377            v_bc,
1378            p_icms,
1379            v_icms,
1380            v_bc_fcp,
1381            p_fcp,
1382            v_fcp,
1383            mod_bc_st,
1384            p_mva_st,
1385            p_red_bc_st,
1386            v_bc_st,
1387            p_icms_st,
1388            v_icms_st,
1389            v_bc_fcp_st,
1390            p_fcp_st,
1391            v_fcp_st,
1392            v_icms_deson,
1393            mot_des_icms,
1394            ind_deduz_deson,
1395            v_icms_st_deson,
1396            mot_des_icms_st,
1397        } => {
1398            totals.v_icms_deson = accum(totals.v_icms_deson, *v_icms_deson);
1399            if ind_deduz_deson.as_deref() == Some("1") {
1400                totals.ind_deduz_deson = true;
1401            }
1402            totals.v_bc = accum(totals.v_bc, Some(*v_bc));
1403            totals.v_icms = accum(totals.v_icms, Some(*v_icms));
1404            totals.v_bc_st = accum(totals.v_bc_st, Some(*v_bc_st));
1405            totals.v_st = accum(totals.v_st, Some(*v_icms_st));
1406            totals.v_fcp_st = accum(totals.v_fcp_st, *v_fcp_st);
1407            totals.v_fcp = accum(totals.v_fcp, *v_fcp);
1408            let mut fields_opt: Vec<Option<TaxField>> = vec![
1409                Some(TaxField::new("orig", orig.as_str())),
1410                Some(TaxField::new("CST", "70")),
1411                Some(TaxField::new("modBC", mod_bc.as_str())),
1412                Some(TaxField::new("pRedBC", fc4(Some(*p_red_bc)).unwrap())),
1413                Some(TaxField::new("vBC", fc2(Some(*v_bc)).unwrap())),
1414                Some(TaxField::new("pICMS", fc4(Some(*p_icms)).unwrap())),
1415                Some(TaxField::new("vICMS", fc2(Some(*v_icms)).unwrap())),
1416            ];
1417            // FCP
1418            fields_opt.push(optional_field("vBCFCP", fc2(*v_bc_fcp).as_deref()));
1419            fields_opt.push(optional_field("pFCP", fc4(*p_fcp).as_deref()));
1420            fields_opt.push(optional_field("vFCP", fc2(*v_fcp).as_deref()));
1421            // ST
1422            fields_opt.push(Some(TaxField::new("modBCST", mod_bc_st.as_str())));
1423            if let Some(v) = p_mva_st {
1424                fields_opt.push(Some(TaxField::new("pMVAST", fc4(Some(*v)).unwrap())));
1425            }
1426            if let Some(v) = p_red_bc_st {
1427                fields_opt.push(Some(TaxField::new("pRedBCST", fc4(Some(*v)).unwrap())));
1428            }
1429            fields_opt.push(Some(TaxField::new("vBCST", fc2(Some(*v_bc_st)).unwrap())));
1430            fields_opt.push(Some(TaxField::new(
1431                "pICMSST",
1432                fc4(Some(*p_icms_st)).unwrap(),
1433            )));
1434            fields_opt.push(Some(TaxField::new(
1435                "vICMSST",
1436                fc2(Some(*v_icms_st)).unwrap(),
1437            )));
1438            // FCP ST
1439            fields_opt.push(optional_field("vBCFCPST", fc2(*v_bc_fcp_st).as_deref()));
1440            fields_opt.push(optional_field("pFCPST", fc4(*p_fcp_st).as_deref()));
1441            fields_opt.push(optional_field("vFCPST", fc2(*v_fcp_st).as_deref()));
1442            // Desoneration
1443            fields_opt.push(optional_field("vICMSDeson", fc2(*v_icms_deson).as_deref()));
1444            fields_opt.push(optional_field("motDesICMS", mot_des_icms.as_deref()));
1445            fields_opt.push(optional_field("indDeduzDeson", ind_deduz_deson.as_deref()));
1446            // ST desoneration
1447            fields_opt.push(optional_field(
1448                "vICMSSTDeson",
1449                fc2(*v_icms_st_deson).as_deref(),
1450            ));
1451            fields_opt.push(optional_field("motDesICMSST", mot_des_icms_st.as_deref()));
1452            Ok(("ICMS70".to_string(), filter_fields(fields_opt)))
1453        }
1454
1455        IcmsCst::Cst90 {
1456            orig,
1457            mod_bc,
1458            v_bc,
1459            p_red_bc,
1460            c_benef_rbc,
1461            p_icms,
1462            v_icms_op,
1463            p_dif,
1464            v_icms_dif,
1465            v_icms,
1466            v_bc_fcp,
1467            p_fcp,
1468            v_fcp,
1469            p_fcp_dif,
1470            v_fcp_dif,
1471            v_fcp_efet,
1472            mod_bc_st,
1473            p_mva_st,
1474            p_red_bc_st,
1475            v_bc_st,
1476            p_icms_st,
1477            v_icms_st,
1478            v_bc_fcp_st,
1479            p_fcp_st,
1480            v_fcp_st,
1481            v_icms_deson,
1482            mot_des_icms,
1483            ind_deduz_deson,
1484            v_icms_st_deson,
1485            mot_des_icms_st,
1486        } => {
1487            totals.v_icms_deson = accum(totals.v_icms_deson, *v_icms_deson);
1488            if ind_deduz_deson.as_deref() == Some("1") {
1489                totals.ind_deduz_deson = true;
1490            }
1491            totals.v_bc = accum(totals.v_bc, *v_bc);
1492            totals.v_icms = accum(totals.v_icms, *v_icms);
1493            totals.v_bc_st = accum(totals.v_bc_st, *v_bc_st);
1494            totals.v_st = accum(totals.v_st, *v_icms_st);
1495            totals.v_fcp_st = accum(totals.v_fcp_st, *v_fcp_st);
1496            totals.v_fcp = accum(totals.v_fcp, *v_fcp);
1497            let mut fields_opt: Vec<Option<TaxField>> = vec![
1498                Some(TaxField::new("orig", orig.as_str())),
1499                Some(TaxField::new("CST", "90")),
1500                optional_field("modBC", mod_bc.as_deref()),
1501                optional_field("vBC", fc2(*v_bc).as_deref()),
1502                optional_field("pRedBC", fc4(*p_red_bc).as_deref()),
1503                optional_field("cBenefRBC", c_benef_rbc.as_deref()),
1504                optional_field("pICMS", fc4(*p_icms).as_deref()),
1505                optional_field("vICMSOp", fc2(*v_icms_op).as_deref()),
1506                optional_field("pDif", fc4(*p_dif).as_deref()),
1507                optional_field("vICMSDif", fc2(*v_icms_dif).as_deref()),
1508                optional_field("vICMS", fc2(*v_icms).as_deref()),
1509            ];
1510            // FCP
1511            fields_opt.push(optional_field("vBCFCP", fc2(*v_bc_fcp).as_deref()));
1512            fields_opt.push(optional_field("pFCP", fc4(*p_fcp).as_deref()));
1513            fields_opt.push(optional_field("vFCP", fc2(*v_fcp).as_deref()));
1514            // FCP deferral
1515            fields_opt.push(optional_field("pFCPDif", fc4(*p_fcp_dif).as_deref()));
1516            fields_opt.push(optional_field("vFCPDif", fc2(*v_fcp_dif).as_deref()));
1517            fields_opt.push(optional_field("vFCPEfet", fc2(*v_fcp_efet).as_deref()));
1518            // ST (all optional for CST 90)
1519            fields_opt.push(optional_field("modBCST", mod_bc_st.as_deref()));
1520            fields_opt.push(optional_field("pMVAST", fc4(*p_mva_st).as_deref()));
1521            fields_opt.push(optional_field("pRedBCST", fc4(*p_red_bc_st).as_deref()));
1522            fields_opt.push(optional_field("vBCST", fc2(*v_bc_st).as_deref()));
1523            fields_opt.push(optional_field("pICMSST", fc4(*p_icms_st).as_deref()));
1524            fields_opt.push(optional_field("vICMSST", fc2(*v_icms_st).as_deref()));
1525            // FCP ST
1526            fields_opt.push(optional_field("vBCFCPST", fc2(*v_bc_fcp_st).as_deref()));
1527            fields_opt.push(optional_field("pFCPST", fc4(*p_fcp_st).as_deref()));
1528            fields_opt.push(optional_field("vFCPST", fc2(*v_fcp_st).as_deref()));
1529            // Desoneration
1530            fields_opt.push(optional_field("vICMSDeson", fc2(*v_icms_deson).as_deref()));
1531            fields_opt.push(optional_field("motDesICMS", mot_des_icms.as_deref()));
1532            fields_opt.push(optional_field("indDeduzDeson", ind_deduz_deson.as_deref()));
1533            // ST desoneration
1534            fields_opt.push(optional_field(
1535                "vICMSSTDeson",
1536                fc2(*v_icms_st_deson).as_deref(),
1537            ));
1538            fields_opt.push(optional_field("motDesICMSST", mot_des_icms_st.as_deref()));
1539            Ok(("ICMS90".to_string(), filter_fields(fields_opt)))
1540        }
1541    }
1542}
1543
1544// ── IcmsCsosn enum (Simples Nacional) ────────────────────────────────────────
1545
1546/// ICMS CSOSN variant for Simples Nacional tax regime (CRT 1/2).
1547///
1548/// Each variant carries **only** the fields that are valid for that CSOSN,
1549/// giving compile-time safety instead of runtime string matching against a
1550/// flat struct full of `Option`s.
1551///
1552/// Normal regime CSTs use [`IcmsCst`] instead.
1553#[derive(Debug, Clone)]
1554#[non_exhaustive]
1555pub enum IcmsCsosn {
1556    /// CSOSN 101 — Tributada pelo Simples Nacional com permissao de credito.
1557    Csosn101 {
1558        /// Product origin code (`orig`).
1559        orig: String,
1560        /// CSOSN code, always `"101"`.
1561        csosn: String,
1562        /// Simples Nacional credit rate (`pCredSN`).
1563        p_cred_sn: Rate,
1564        /// Simples Nacional credit value (`vCredICMSSN`).
1565        v_cred_icms_sn: Cents,
1566    },
1567    /// CSOSN 102 — Tributada pelo Simples Nacional sem permissão de crédito.
1568    Csosn102 {
1569        /// Product origin code (`orig`). May be empty when CRT=4.
1570        orig: String,
1571        /// CSOSN code, always `"102"`.
1572        csosn: String,
1573    },
1574    /// CSOSN 103 — Isenção do ICMS no Simples Nacional para faixa de receita
1575    /// bruta.
1576    Csosn103 {
1577        /// Product origin code (`orig`).
1578        orig: String,
1579        /// CSOSN code, always `"103"`.
1580        csosn: String,
1581    },
1582    /// CSOSN 300 — Imune.
1583    Csosn300 {
1584        /// Product origin code (`orig`). May be empty.
1585        orig: String,
1586        /// CSOSN code, always `"300"`.
1587        csosn: String,
1588    },
1589    /// CSOSN 400 — Não tributada pelo Simples Nacional.
1590    Csosn400 {
1591        /// Product origin code (`orig`). May be empty.
1592        orig: String,
1593        /// CSOSN code, always `"400"`.
1594        csosn: String,
1595    },
1596    /// CSOSN 201 — Tributada com permissao de credito e com cobranca do ICMS
1597    /// por ST.
1598    Csosn201 {
1599        /// Product origin code (`orig`).
1600        orig: String,
1601        /// CSOSN code, always `"201"`.
1602        csosn: String,
1603        /// ST base calculation modality (`modBCST`).
1604        mod_bc_st: String,
1605        /// ST added value margin (`pMVAST`). Optional.
1606        p_mva_st: Option<Rate>,
1607        /// ST base reduction rate (`pRedBCST`). Optional.
1608        p_red_bc_st: Option<Rate>,
1609        /// ST calculation base value (`vBCST`).
1610        v_bc_st: Cents,
1611        /// ST rate (`pICMSST`).
1612        p_icms_st: Rate,
1613        /// ST ICMS value (`vICMSST`).
1614        v_icms_st: Cents,
1615        /// FCP-ST calculation base (`vBCFCPST`). Optional.
1616        v_bc_fcp_st: Option<Cents>,
1617        /// FCP-ST rate (`pFCPST`). Optional.
1618        p_fcp_st: Option<Rate>,
1619        /// FCP-ST value (`vFCPST`). Optional.
1620        v_fcp_st: Option<Cents>,
1621        /// Simples Nacional credit rate (`pCredSN`). Optional.
1622        p_cred_sn: Option<Rate>,
1623        /// Simples Nacional credit value (`vCredICMSSN`). Optional.
1624        v_cred_icms_sn: Option<Cents>,
1625    },
1626    /// CSOSN 202 — Tributada sem permissão de crédito e com cobrança do
1627    /// ICMS por ST.
1628    Csosn202 {
1629        /// Product origin code (`orig`).
1630        orig: String,
1631        /// CSOSN code, always `"202"`.
1632        csosn: String,
1633        /// ST base calculation modality (`modBCST`).
1634        mod_bc_st: String,
1635        /// ST added value margin (`pMVAST`). Optional.
1636        p_mva_st: Option<Rate>,
1637        /// ST base reduction rate (`pRedBCST`). Optional.
1638        p_red_bc_st: Option<Rate>,
1639        /// ST calculation base value (`vBCST`).
1640        v_bc_st: Cents,
1641        /// ST rate (`pICMSST`).
1642        p_icms_st: Rate,
1643        /// ST ICMS value (`vICMSST`).
1644        v_icms_st: Cents,
1645        /// FCP-ST calculation base (`vBCFCPST`). Optional.
1646        v_bc_fcp_st: Option<Cents>,
1647        /// FCP-ST rate (`pFCPST`). Optional.
1648        p_fcp_st: Option<Rate>,
1649        /// FCP-ST value (`vFCPST`). Optional.
1650        v_fcp_st: Option<Cents>,
1651    },
1652    /// CSOSN 203 — Isenção do ICMS no Simples Nacional para faixa de receita
1653    /// bruta e com cobrança do ICMS por ST.
1654    Csosn203 {
1655        /// Product origin code (`orig`).
1656        orig: String,
1657        /// CSOSN code, always `"203"`.
1658        csosn: String,
1659        /// ST base calculation modality (`modBCST`).
1660        mod_bc_st: String,
1661        /// ST added value margin (`pMVAST`). Optional.
1662        p_mva_st: Option<Rate>,
1663        /// ST base reduction rate (`pRedBCST`). Optional.
1664        p_red_bc_st: Option<Rate>,
1665        /// ST calculation base value (`vBCST`).
1666        v_bc_st: Cents,
1667        /// ST rate (`pICMSST`).
1668        p_icms_st: Rate,
1669        /// ST ICMS value (`vICMSST`).
1670        v_icms_st: Cents,
1671        /// FCP-ST calculation base (`vBCFCPST`). Optional.
1672        v_bc_fcp_st: Option<Cents>,
1673        /// FCP-ST rate (`pFCPST`). Optional.
1674        p_fcp_st: Option<Rate>,
1675        /// FCP-ST value (`vFCPST`). Optional.
1676        v_fcp_st: Option<Cents>,
1677    },
1678    /// CSOSN 500 — ICMS cobrado anteriormente por ST ou por antecipacao.
1679    Csosn500 {
1680        /// Product origin code (`orig`).
1681        orig: String,
1682        /// CSOSN code, always `"500"`.
1683        csosn: String,
1684        /// ST retained calculation base (`vBCSTRet`). Optional.
1685        v_bc_st_ret: Option<Cents>,
1686        /// ST rate at retention (`pST`). Optional.
1687        p_st: Option<Rate>,
1688        /// ICMS value paid by the substitutor (`vICMSSubstituto`). Optional.
1689        v_icms_substituto: Option<Cents>,
1690        /// Retained ST ICMS value (`vICMSSTRet`). Optional.
1691        v_icms_st_ret: Option<Cents>,
1692        /// FCP-ST retained calculation base (`vBCFCPSTRet`). Optional.
1693        v_bc_fcp_st_ret: Option<Cents>,
1694        /// FCP-ST retained rate (`pFCPSTRet`). Optional.
1695        p_fcp_st_ret: Option<Rate>,
1696        /// FCP-ST retained value (`vFCPSTRet`). Optional.
1697        v_fcp_st_ret: Option<Cents>,
1698        /// Effective base reduction rate (`pRedBCEfet`). Optional.
1699        p_red_bc_efet: Option<Rate>,
1700        /// Effective calculation base (`vBCEfet`). Optional.
1701        v_bc_efet: Option<Cents>,
1702        /// Effective ICMS rate (`pICMSEfet`). Optional.
1703        p_icms_efet: Option<Rate>,
1704        /// Effective ICMS value (`vICMSEfet`). Optional.
1705        v_icms_efet: Option<Cents>,
1706    },
1707    /// CSOSN 900 — Outros.
1708    Csosn900 {
1709        /// Product origin code (`orig`). May be empty.
1710        orig: String,
1711        /// CSOSN code, always `"900"`.
1712        csosn: String,
1713        /// Base calculation modality (`modBC`). Optional.
1714        mod_bc: Option<String>,
1715        /// ICMS calculation base value (`vBC`). Optional.
1716        v_bc: Option<Cents>,
1717        /// Base reduction rate (`pRedBC`). Optional.
1718        p_red_bc: Option<Rate>,
1719        /// ICMS rate (`pICMS`). Optional.
1720        p_icms: Option<Rate>,
1721        /// ICMS value (`vICMS`). Optional.
1722        v_icms: Option<Cents>,
1723        /// ST base calculation modality (`modBCST`). Optional.
1724        mod_bc_st: Option<String>,
1725        /// ST added value margin (`pMVAST`). Optional.
1726        p_mva_st: Option<Rate>,
1727        /// ST base reduction rate (`pRedBCST`). Optional.
1728        p_red_bc_st: Option<Rate>,
1729        /// ST calculation base value (`vBCST`). Optional.
1730        v_bc_st: Option<Cents>,
1731        /// ST rate (`pICMSST`). Optional.
1732        p_icms_st: Option<Rate>,
1733        /// ST ICMS value (`vICMSST`). Optional.
1734        v_icms_st: Option<Cents>,
1735        /// FCP-ST calculation base (`vBCFCPST`). Optional.
1736        v_bc_fcp_st: Option<Cents>,
1737        /// FCP-ST rate (`pFCPST`). Optional.
1738        p_fcp_st: Option<Rate>,
1739        /// FCP-ST value (`vFCPST`). Optional.
1740        v_fcp_st: Option<Cents>,
1741        /// Simples Nacional credit rate (`pCredSN`). Optional.
1742        p_cred_sn: Option<Rate>,
1743        /// Simples Nacional credit value (`vCredICMSSN`). Optional.
1744        v_cred_icms_sn: Option<Cents>,
1745    },
1746}
1747
1748impl IcmsCsosn {
1749    /// Return the CSOSN code string for this variant (e.g. "101", "202").
1750    pub fn csosn_code(&self) -> &str {
1751        match self {
1752            Self::Csosn101 { csosn, .. } => csosn.as_str(),
1753            Self::Csosn102 { csosn, .. } => csosn.as_str(),
1754            Self::Csosn103 { csosn, .. } => csosn.as_str(),
1755            Self::Csosn201 { csosn, .. } => csosn.as_str(),
1756            Self::Csosn202 { csosn, .. } => csosn.as_str(),
1757            Self::Csosn203 { csosn, .. } => csosn.as_str(),
1758            Self::Csosn300 { csosn, .. } => csosn.as_str(),
1759            Self::Csosn400 { csosn, .. } => csosn.as_str(),
1760            Self::Csosn500 { csosn, .. } => csosn.as_str(),
1761            Self::Csosn900 { csosn, .. } => csosn.as_str(),
1762        }
1763    }
1764}
1765
1766/// Build the ICMS XML fragment and accumulate totals from a typed
1767/// [`IcmsCsosn`] variant.
1768///
1769/// This is the compile-time-safe counterpart of the original
1770/// [`build_icms_xml`] code path for Simples Nacional CSOSNs. It can be used
1771/// directly by new code that already has an `IcmsCsosn`, or indirectly via
1772/// the unchanged [`build_icms_xml`] public API (which converts internally).
1773///
1774/// # Errors
1775///
1776/// Returns [`FiscalError`] if XML field serialization fails (should not happen
1777/// when the enum is correctly constructed).
1778pub fn build_icms_csosn_xml(
1779    csosn: &IcmsCsosn,
1780    totals: &mut IcmsTotals,
1781) -> Result<(String, Vec<TaxField>), FiscalError> {
1782    match csosn {
1783        IcmsCsosn::Csosn101 {
1784            orig,
1785            csosn,
1786            p_cred_sn,
1787            v_cred_icms_sn,
1788        } => {
1789            let fields = filter_fields(vec![
1790                Some(TaxField::new("orig", orig.as_str())),
1791                Some(TaxField::new("CSOSN", csosn.as_str())),
1792                Some(TaxField::new("pCredSN", fc4(Some(*p_cred_sn)).unwrap())),
1793                Some(TaxField::new(
1794                    "vCredICMSSN",
1795                    fc2(Some(*v_cred_icms_sn)).unwrap(),
1796                )),
1797            ]);
1798            Ok(("ICMSSN101".to_string(), fields))
1799        }
1800
1801        IcmsCsosn::Csosn102 { orig, csosn }
1802        | IcmsCsosn::Csosn103 { orig, csosn }
1803        | IcmsCsosn::Csosn300 { orig, csosn }
1804        | IcmsCsosn::Csosn400 { orig, csosn } => {
1805            let orig_val = if orig.is_empty() {
1806                None
1807            } else {
1808                Some(orig.as_str())
1809            };
1810            let fields = filter_fields(vec![
1811                optional_field("orig", orig_val),
1812                Some(TaxField::new("CSOSN", csosn.as_str())),
1813            ]);
1814            Ok(("ICMSSN102".to_string(), fields))
1815        }
1816
1817        IcmsCsosn::Csosn201 {
1818            orig,
1819            csosn,
1820            mod_bc_st,
1821            p_mva_st,
1822            p_red_bc_st,
1823            v_bc_st,
1824            p_icms_st,
1825            v_icms_st,
1826            v_bc_fcp_st,
1827            p_fcp_st,
1828            v_fcp_st,
1829            p_cred_sn,
1830            v_cred_icms_sn,
1831        } => {
1832            totals.v_bc_st = accum(totals.v_bc_st, Some(*v_bc_st));
1833            totals.v_st = accum(totals.v_st, Some(*v_icms_st));
1834
1835            let mut fields_opt: Vec<Option<TaxField>> = vec![
1836                Some(TaxField::new("orig", orig.as_str())),
1837                Some(TaxField::new("CSOSN", csosn.as_str())),
1838            ];
1839            // ST fields
1840            fields_opt.push(Some(TaxField::new("modBCST", mod_bc_st.as_str())));
1841            if let Some(v) = p_mva_st {
1842                fields_opt.push(Some(TaxField::new("pMVAST", fc4(Some(*v)).unwrap())));
1843            }
1844            if let Some(v) = p_red_bc_st {
1845                fields_opt.push(Some(TaxField::new("pRedBCST", fc4(Some(*v)).unwrap())));
1846            }
1847            fields_opt.push(Some(TaxField::new("vBCST", fc2(Some(*v_bc_st)).unwrap())));
1848            fields_opt.push(Some(TaxField::new(
1849                "pICMSST",
1850                fc4(Some(*p_icms_st)).unwrap(),
1851            )));
1852            fields_opt.push(Some(TaxField::new(
1853                "vICMSST",
1854                fc2(Some(*v_icms_st)).unwrap(),
1855            )));
1856            // FCP ST fields
1857            fields_opt.push(optional_field("vBCFCPST", fc2(*v_bc_fcp_st).as_deref()));
1858            fields_opt.push(optional_field("pFCPST", fc4(*p_fcp_st).as_deref()));
1859            fields_opt.push(optional_field("vFCPST", fc2(*v_fcp_st).as_deref()));
1860            // SN credit fields
1861            fields_opt.push(optional_field("pCredSN", fc4(*p_cred_sn).as_deref()));
1862            fields_opt.push(optional_field(
1863                "vCredICMSSN",
1864                fc2(*v_cred_icms_sn).as_deref(),
1865            ));
1866            Ok(("ICMSSN201".to_string(), filter_fields(fields_opt)))
1867        }
1868
1869        IcmsCsosn::Csosn202 {
1870            orig,
1871            csosn,
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        }
1882        | IcmsCsosn::Csosn203 {
1883            orig,
1884            csosn,
1885            mod_bc_st,
1886            p_mva_st,
1887            p_red_bc_st,
1888            v_bc_st,
1889            p_icms_st,
1890            v_icms_st,
1891            v_bc_fcp_st,
1892            p_fcp_st,
1893            v_fcp_st,
1894        } => {
1895            totals.v_bc_st = accum(totals.v_bc_st, Some(*v_bc_st));
1896            totals.v_st = accum(totals.v_st, Some(*v_icms_st));
1897
1898            let mut fields_opt: Vec<Option<TaxField>> = vec![
1899                Some(TaxField::new("orig", orig.as_str())),
1900                Some(TaxField::new("CSOSN", csosn.as_str())),
1901            ];
1902            // ST fields
1903            fields_opt.push(Some(TaxField::new("modBCST", mod_bc_st.as_str())));
1904            if let Some(v) = p_mva_st {
1905                fields_opt.push(Some(TaxField::new("pMVAST", fc4(Some(*v)).unwrap())));
1906            }
1907            if let Some(v) = p_red_bc_st {
1908                fields_opt.push(Some(TaxField::new("pRedBCST", fc4(Some(*v)).unwrap())));
1909            }
1910            fields_opt.push(Some(TaxField::new("vBCST", fc2(Some(*v_bc_st)).unwrap())));
1911            fields_opt.push(Some(TaxField::new(
1912                "pICMSST",
1913                fc4(Some(*p_icms_st)).unwrap(),
1914            )));
1915            fields_opt.push(Some(TaxField::new(
1916                "vICMSST",
1917                fc2(Some(*v_icms_st)).unwrap(),
1918            )));
1919            // FCP ST fields
1920            fields_opt.push(optional_field("vBCFCPST", fc2(*v_bc_fcp_st).as_deref()));
1921            fields_opt.push(optional_field("pFCPST", fc4(*p_fcp_st).as_deref()));
1922            fields_opt.push(optional_field("vFCPST", fc2(*v_fcp_st).as_deref()));
1923            Ok(("ICMSSN202".to_string(), filter_fields(fields_opt)))
1924        }
1925
1926        IcmsCsosn::Csosn500 {
1927            orig,
1928            csosn,
1929            v_bc_st_ret,
1930            p_st,
1931            v_icms_substituto,
1932            v_icms_st_ret,
1933            v_bc_fcp_st_ret,
1934            p_fcp_st_ret,
1935            v_fcp_st_ret,
1936            p_red_bc_efet,
1937            v_bc_efet,
1938            p_icms_efet,
1939            v_icms_efet,
1940        } => {
1941            let fields = filter_fields(vec![
1942                Some(TaxField::new("orig", orig.as_str())),
1943                Some(TaxField::new("CSOSN", csosn.as_str())),
1944                optional_field("vBCSTRet", fc2(*v_bc_st_ret).as_deref()),
1945                optional_field("pST", fc4(*p_st).as_deref()),
1946                optional_field("vICMSSubstituto", fc2(*v_icms_substituto).as_deref()),
1947                optional_field("vICMSSTRet", fc2(*v_icms_st_ret).as_deref()),
1948                optional_field("vBCFCPSTRet", fc2(*v_bc_fcp_st_ret).as_deref()),
1949                optional_field("pFCPSTRet", fc4(*p_fcp_st_ret).as_deref()),
1950                optional_field("vFCPSTRet", fc2(*v_fcp_st_ret).as_deref()),
1951                optional_field("pRedBCEfet", fc4(*p_red_bc_efet).as_deref()),
1952                optional_field("vBCEfet", fc2(*v_bc_efet).as_deref()),
1953                optional_field("pICMSEfet", fc4(*p_icms_efet).as_deref()),
1954                optional_field("vICMSEfet", fc2(*v_icms_efet).as_deref()),
1955            ]);
1956            Ok(("ICMSSN500".to_string(), fields))
1957        }
1958
1959        IcmsCsosn::Csosn900 {
1960            orig,
1961            csosn,
1962            mod_bc,
1963            v_bc,
1964            p_red_bc,
1965            p_icms,
1966            v_icms,
1967            mod_bc_st,
1968            p_mva_st,
1969            p_red_bc_st,
1970            v_bc_st,
1971            p_icms_st,
1972            v_icms_st,
1973            v_bc_fcp_st,
1974            p_fcp_st,
1975            v_fcp_st,
1976            p_cred_sn,
1977            v_cred_icms_sn,
1978        } => {
1979            totals.v_bc = accum(totals.v_bc, *v_bc);
1980            totals.v_icms = accum(totals.v_icms, *v_icms);
1981            totals.v_bc_st = accum(totals.v_bc_st, *v_bc_st);
1982            totals.v_st = accum(totals.v_st, *v_icms_st);
1983
1984            let orig_val = if orig.is_empty() {
1985                None
1986            } else {
1987                Some(orig.as_str())
1988            };
1989            let mut fields_opt: Vec<Option<TaxField>> = vec![
1990                optional_field("orig", orig_val),
1991                Some(TaxField::new("CSOSN", csosn.as_str())),
1992                optional_field("modBC", mod_bc.as_deref()),
1993                optional_field("vBC", fc2(*v_bc).as_deref()),
1994                optional_field("pRedBC", fc4(*p_red_bc).as_deref()),
1995                optional_field("pICMS", fc4(*p_icms).as_deref()),
1996                optional_field("vICMS", fc2(*v_icms).as_deref()),
1997                // ST fields are all optional for CSOSN 900
1998                optional_field("modBCST", mod_bc_st.as_deref()),
1999                optional_field("pMVAST", fc4(*p_mva_st).as_deref()),
2000                optional_field("pRedBCST", fc4(*p_red_bc_st).as_deref()),
2001                optional_field("vBCST", fc2(*v_bc_st).as_deref()),
2002                optional_field("pICMSST", fc4(*p_icms_st).as_deref()),
2003                optional_field("vICMSST", fc2(*v_icms_st).as_deref()),
2004            ];
2005            // FCP ST fields
2006            fields_opt.push(optional_field("vBCFCPST", fc2(*v_bc_fcp_st).as_deref()));
2007            fields_opt.push(optional_field("pFCPST", fc4(*p_fcp_st).as_deref()));
2008            fields_opt.push(optional_field("vFCPST", fc2(*v_fcp_st).as_deref()));
2009            // SN credit fields
2010            fields_opt.push(optional_field("pCredSN", fc4(*p_cred_sn).as_deref()));
2011            fields_opt.push(optional_field(
2012                "vCredICMSSN",
2013                fc2(*v_cred_icms_sn).as_deref(),
2014            ));
2015            Ok(("ICMSSN900".to_string(), filter_fields(fields_opt)))
2016        }
2017    }
2018}
2019
2020// ── Helper: format field values ─────────────────────────────────────────────
2021
2022/// Format a monetary [`Cents`] value (2 decimal places) returning `Option<String>`.
2023fn fc2(v: Option<Cents>) -> Option<String> {
2024    format_cents_or_none(v.map(|c| c.0), 2)
2025}
2026
2027/// Format a [`Rate`] value (4 decimal places) returning `Option<String>`.
2028fn fc4(v: Option<Rate>) -> Option<String> {
2029    format_cents_or_none(v.map(|r| r.0), 4)
2030}
2031
2032/// Format a raw i64 quantity (4 decimal places) returning `Option<String>`.
2033fn fc4_raw(v: Option<i64>) -> Option<String> {
2034    format_cents_or_none(v, 4)
2035}
2036
2037// ── Main builders ───────────────────────────────────────────────────────────
2038
2039/// Build ICMS XML string from a typed [`IcmsVariant`].
2040///
2041/// Delegates to [`build_icms_cst_xml`] or [`build_icms_csosn_xml`] depending
2042/// on the variant, then wraps the result in an `<ICMS>` element and
2043/// accumulates totals.
2044///
2045/// # Errors
2046///
2047/// Returns [`FiscalError`] if XML field serialization fails (should not happen
2048/// when the enum is correctly constructed).
2049pub fn build_icms_xml(
2050    variant: &IcmsVariant,
2051    totals: &mut IcmsTotals,
2052) -> Result<String, FiscalError> {
2053    let (variant_tag, fields) = match variant {
2054        IcmsVariant::Cst(cst) => build_icms_cst_xml(cst, totals)?,
2055        IcmsVariant::Csosn(csosn) => build_icms_csosn_xml(csosn, totals)?,
2056    };
2057
2058    let element = TaxElement {
2059        outer_tag: Some("ICMS".to_string()),
2060        outer_fields: vec![],
2061        variant_tag,
2062        fields,
2063    };
2064
2065    Ok(serialize_tax_element(&element))
2066}
2067
2068/// Build the ICMSPart XML group (partition between states).
2069///
2070/// Used inside `<ICMS>` for CST 10 or 90 with interstate partition.
2071///
2072/// # Errors
2073///
2074/// Returns [`FiscalError::MissingRequiredField`] if any required field is
2075/// missing in the data.
2076pub fn build_icms_part_xml(data: &IcmsPartData) -> Result<(String, IcmsTotals), FiscalError> {
2077    let mut totals = create_icms_totals();
2078    totals.v_bc = accum(totals.v_bc, Some(data.v_bc));
2079    totals.v_icms = accum(totals.v_icms, Some(data.v_icms));
2080    totals.v_bc_st = accum(totals.v_bc_st, Some(data.v_bc_st));
2081    totals.v_st = accum(totals.v_st, Some(data.v_icms_st));
2082    if data.ind_deduz_deson.as_deref() == Some("1") {
2083        totals.ind_deduz_deson = true;
2084    }
2085
2086    let mut fields_opt: Vec<Option<TaxField>> = vec![
2087        Some(TaxField::new("orig", data.orig.as_str())),
2088        Some(TaxField::new("CST", data.cst.as_str())),
2089        Some(TaxField::new("modBC", data.mod_bc.as_str())),
2090        Some(TaxField::new("vBC", fc2(Some(data.v_bc)).unwrap())),
2091        optional_field("pRedBC", fc4(data.p_red_bc).as_deref()),
2092        Some(TaxField::new("pICMS", fc4(Some(data.p_icms)).unwrap())),
2093        Some(TaxField::new("vICMS", fc2(Some(data.v_icms)).unwrap())),
2094    ];
2095
2096    // ST fields
2097    fields_opt.push(Some(TaxField::new("modBCST", data.mod_bc_st.as_str())));
2098    if let Some(v) = data.p_mva_st {
2099        fields_opt.push(Some(TaxField::new("pMVAST", fc4(Some(v)).unwrap())));
2100    }
2101    if let Some(v) = data.p_red_bc_st {
2102        fields_opt.push(Some(TaxField::new("pRedBCST", fc4(Some(v)).unwrap())));
2103    }
2104    fields_opt.push(Some(TaxField::new(
2105        "vBCST",
2106        fc2(Some(data.v_bc_st)).unwrap(),
2107    )));
2108    fields_opt.push(Some(TaxField::new(
2109        "pICMSST",
2110        fc4(Some(data.p_icms_st)).unwrap(),
2111    )));
2112    fields_opt.push(Some(TaxField::new(
2113        "vICMSST",
2114        fc2(Some(data.v_icms_st)).unwrap(),
2115    )));
2116
2117    // FCP ST fields
2118    fields_opt.push(optional_field("vBCFCPST", fc2(data.v_bc_fcp_st).as_deref()));
2119    fields_opt.push(optional_field("pFCPST", fc4(data.p_fcp_st).as_deref()));
2120    fields_opt.push(optional_field("vFCPST", fc2(data.v_fcp_st).as_deref()));
2121
2122    // pBCOp, UFST
2123    fields_opt.push(Some(TaxField::new(
2124        "pBCOp",
2125        fc4(Some(data.p_bc_op)).unwrap(),
2126    )));
2127    fields_opt.push(Some(TaxField::new("UFST", data.uf_st.as_str())));
2128
2129    // Desoneration
2130    fields_opt.push(optional_field(
2131        "vICMSDeson",
2132        fc2(data.v_icms_deson).as_deref(),
2133    ));
2134    fields_opt.push(optional_field("motDesICMS", data.mot_des_icms.as_deref()));
2135    fields_opt.push(optional_field(
2136        "indDeduzDeson",
2137        data.ind_deduz_deson.as_deref(),
2138    ));
2139
2140    let fields = filter_fields(fields_opt);
2141
2142    let element = TaxElement {
2143        outer_tag: Some("ICMS".to_string()),
2144        outer_fields: vec![],
2145        variant_tag: "ICMSPart".to_string(),
2146        fields,
2147    };
2148
2149    Ok((serialize_tax_element(&element), totals))
2150}
2151
2152/// Build the ICMSST XML group (ST repasse).
2153///
2154/// Used inside `<ICMS>` for CST 41 or 60 with interstate ST repasse.
2155///
2156/// # Errors
2157///
2158/// Returns [`FiscalError`] if serialization fails.
2159pub fn build_icms_st_xml(data: &IcmsStData) -> Result<(String, IcmsTotals), FiscalError> {
2160    let mut totals = create_icms_totals();
2161    totals.v_fcp_st_ret = accum(totals.v_fcp_st_ret, data.v_fcp_st_ret);
2162
2163    let fields_opt: Vec<Option<TaxField>> = vec![
2164        Some(TaxField::new("orig", data.orig.as_str())),
2165        Some(TaxField::new("CST", data.cst.as_str())),
2166        Some(TaxField::new(
2167            "vBCSTRet",
2168            fc2(Some(data.v_bc_st_ret)).unwrap(),
2169        )),
2170        optional_field("pST", fc4(data.p_st).as_deref()),
2171        optional_field("vICMSSubstituto", fc2(data.v_icms_substituto).as_deref()),
2172        Some(TaxField::new(
2173            "vICMSSTRet",
2174            fc2(Some(data.v_icms_st_ret)).unwrap(),
2175        )),
2176        optional_field("vBCFCPSTRet", fc2(data.v_bc_fcp_st_ret).as_deref()),
2177        optional_field("pFCPSTRet", fc4(data.p_fcp_st_ret).as_deref()),
2178        optional_field("vFCPSTRet", fc2(data.v_fcp_st_ret).as_deref()),
2179        Some(TaxField::new(
2180            "vBCSTDest",
2181            fc2(Some(data.v_bc_st_dest)).unwrap(),
2182        )),
2183        Some(TaxField::new(
2184            "vICMSSTDest",
2185            fc2(Some(data.v_icms_st_dest)).unwrap(),
2186        )),
2187        optional_field("pRedBCEfet", fc4(data.p_red_bc_efet).as_deref()),
2188        optional_field("vBCEfet", fc2(data.v_bc_efet).as_deref()),
2189        optional_field("pICMSEfet", fc4(data.p_icms_efet).as_deref()),
2190        optional_field("vICMSEfet", fc2(data.v_icms_efet).as_deref()),
2191    ];
2192
2193    let fields = filter_fields(fields_opt);
2194
2195    let element = TaxElement {
2196        outer_tag: Some("ICMS".to_string()),
2197        outer_fields: vec![],
2198        variant_tag: "ICMSST".to_string(),
2199        fields,
2200    };
2201
2202    Ok((serialize_tax_element(&element), totals))
2203}
2204
2205/// Build the ICMSUFDest XML group (interstate destination).
2206///
2207/// This is a sibling of `<ICMS>`, placed directly inside `<imposto>`.
2208///
2209/// # Errors
2210///
2211/// Returns [`FiscalError`] if serialization fails.
2212pub fn build_icms_uf_dest_xml(data: &IcmsUfDestData) -> Result<(String, IcmsTotals), FiscalError> {
2213    let mut totals = create_icms_totals();
2214    totals.v_icms_uf_dest = accum(totals.v_icms_uf_dest, Some(data.v_icms_uf_dest));
2215    totals.v_fcp_uf_dest = accum(totals.v_fcp_uf_dest, data.v_fcp_uf_dest);
2216    totals.v_icms_uf_remet = accum(totals.v_icms_uf_remet, data.v_icms_uf_remet);
2217
2218    let fields_opt: Vec<Option<TaxField>> = vec![
2219        Some(TaxField::new(
2220            "vBCUFDest",
2221            fc2(Some(data.v_bc_uf_dest)).unwrap(),
2222        )),
2223        optional_field("vBCFCPUFDest", fc2(data.v_bc_fcp_uf_dest).as_deref()),
2224        optional_field("pFCPUFDest", fc4(data.p_fcp_uf_dest).as_deref()),
2225        Some(TaxField::new(
2226            "pICMSUFDest",
2227            fc4(Some(data.p_icms_uf_dest)).unwrap(),
2228        )),
2229        Some(TaxField::new(
2230            "pICMSInter",
2231            fc4(Some(data.p_icms_inter)).unwrap(),
2232        )),
2233        Some(TaxField::new("pICMSInterPart", "100.0000")),
2234        optional_field("vFCPUFDest", fc2(data.v_fcp_uf_dest).as_deref()),
2235        Some(TaxField::new(
2236            "vICMSUFDest",
2237            fc2(Some(data.v_icms_uf_dest)).unwrap(),
2238        )),
2239        Some(TaxField::new(
2240            "vICMSUFRemet",
2241            fc2(Some(data.v_icms_uf_remet.unwrap_or(Cents(0)))).unwrap(),
2242        )),
2243    ];
2244
2245    let fields = filter_fields(fields_opt);
2246
2247    let element = TaxElement {
2248        outer_tag: None,
2249        outer_fields: vec![],
2250        variant_tag: "ICMSUFDest".to_string(),
2251        fields,
2252    };
2253
2254    Ok((serialize_tax_element(&element), totals))
2255}
2256
2257/// Merge item-level ICMS totals into a running accumulator.
2258///
2259/// All monetary fields in `source` are added to the corresponding fields of
2260/// `target`. Call this after each item's ICMS XML has been generated via
2261/// [`build_icms_part_xml`] or [`build_icms_st_xml`] (which return their own
2262/// per-item sub-totals) to keep a running document total.
2263pub fn merge_icms_totals(target: &mut IcmsTotals, source: &IcmsTotals) {
2264    target.v_bc += source.v_bc;
2265    target.v_icms += source.v_icms;
2266    target.v_icms_deson += source.v_icms_deson;
2267    target.v_bc_st += source.v_bc_st;
2268    target.v_st += source.v_st;
2269    target.v_fcp += source.v_fcp;
2270    target.v_fcp_st += source.v_fcp_st;
2271    target.v_fcp_st_ret += source.v_fcp_st_ret;
2272    target.v_fcp_uf_dest += source.v_fcp_uf_dest;
2273    target.v_icms_uf_dest += source.v_icms_uf_dest;
2274    target.v_icms_uf_remet += source.v_icms_uf_remet;
2275    target.q_bc_mono += source.q_bc_mono;
2276    target.v_icms_mono += source.v_icms_mono;
2277    target.q_bc_mono_reten += source.q_bc_mono_reten;
2278    target.v_icms_mono_reten += source.v_icms_mono_reten;
2279    target.q_bc_mono_ret += source.q_bc_mono_ret;
2280    target.v_icms_mono_ret += source.v_icms_mono_ret;
2281    // PHP uses "last one wins" for indDeduzDeson — if any source sets it, propagate.
2282    if source.ind_deduz_deson {
2283        target.ind_deduz_deson = true;
2284    }
2285}