use serde::{Deserialize, Serialize};
use crate::format_utils::{format_cents_2, format_rate_4};
use crate::tax_element::{
TaxElement, TaxField, filter_fields, optional_field, serialize_tax_element,
};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts", ts(export))]
#[non_exhaustive]
pub struct IssqnData {
pub v_bc: i64,
pub v_aliq: i64,
pub v_issqn: i64,
pub c_mun_fg: String,
pub c_list_serv: String,
pub v_deducao: Option<i64>,
pub v_outro: Option<i64>,
pub v_desc_incond: Option<i64>,
pub v_desc_cond: Option<i64>,
pub v_iss_ret: Option<i64>,
pub ind_iss: Option<String>,
pub c_servico: Option<String>,
pub c_mun: Option<String>,
pub c_pais: Option<String>,
pub n_processo: Option<String>,
pub ind_incentivo: Option<String>,
}
impl IssqnData {
pub fn new(
v_bc: i64,
v_aliq: i64,
v_issqn: i64,
c_mun_fg: impl Into<String>,
c_list_serv: impl Into<String>,
) -> Self {
Self {
v_bc,
v_aliq,
v_issqn,
c_mun_fg: c_mun_fg.into(),
c_list_serv: c_list_serv.into(),
..Default::default()
}
}
pub fn v_deducao(mut self, v: i64) -> Self {
self.v_deducao = Some(v);
self
}
pub fn v_outro(mut self, v: i64) -> Self {
self.v_outro = Some(v);
self
}
pub fn v_desc_incond(mut self, v: i64) -> Self {
self.v_desc_incond = Some(v);
self
}
pub fn v_desc_cond(mut self, v: i64) -> Self {
self.v_desc_cond = Some(v);
self
}
pub fn v_iss_ret(mut self, v: i64) -> Self {
self.v_iss_ret = Some(v);
self
}
pub fn ind_iss(mut self, v: impl Into<String>) -> Self {
self.ind_iss = Some(v.into());
self
}
pub fn c_servico(mut self, v: impl Into<String>) -> Self {
self.c_servico = Some(v.into());
self
}
pub fn c_mun(mut self, v: impl Into<String>) -> Self {
self.c_mun = Some(v.into());
self
}
pub fn c_pais(mut self, v: impl Into<String>) -> Self {
self.c_pais = Some(v.into());
self
}
pub fn n_processo(mut self, v: impl Into<String>) -> Self {
self.n_processo = Some(v.into());
self
}
pub fn ind_incentivo(mut self, v: impl Into<String>) -> Self {
self.ind_incentivo = Some(v.into());
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct IssqnTotals {
pub v_bc: i64,
pub v_iss: i64,
pub v_iss_ret: i64,
pub v_deducao: i64,
pub v_outro: i64,
pub v_desc_incond: i64,
pub v_desc_cond: i64,
}
pub fn create_issqn_totals() -> IssqnTotals {
IssqnTotals::default()
}
fn calculate_issqn(data: &IssqnData, totals: Option<&mut IssqnTotals>) -> TaxElement {
if let Some(t) = totals {
if data.v_bc > 0 {
t.v_bc += data.v_bc;
t.v_iss += data.v_issqn;
t.v_iss_ret += data.v_iss_ret.unwrap_or(0);
t.v_deducao += data.v_deducao.unwrap_or(0);
t.v_outro += data.v_outro.unwrap_or(0);
t.v_desc_incond += data.v_desc_incond.unwrap_or(0);
t.v_desc_cond += data.v_desc_cond.unwrap_or(0);
}
}
let fields: Vec<Option<TaxField>> = vec![
Some(TaxField::new("vBC", format_cents_2(data.v_bc))),
Some(TaxField::new("vAliq", format_rate_4(data.v_aliq))),
Some(TaxField::new("vISSQN", format_cents_2(data.v_issqn))),
Some(TaxField::new("cMunFG", &data.c_mun_fg)),
Some(TaxField::new("cListServ", &data.c_list_serv)),
optional_field("vDeducao", data.v_deducao.map(format_cents_2).as_deref()),
optional_field("vOutro", data.v_outro.map(format_cents_2).as_deref()),
optional_field(
"vDescIncond",
data.v_desc_incond.map(format_cents_2).as_deref(),
),
optional_field("vDescCond", data.v_desc_cond.map(format_cents_2).as_deref()),
optional_field("vISSRet", data.v_iss_ret.map(format_cents_2).as_deref()),
optional_field("indISS", data.ind_iss.as_deref()),
optional_field("cServico", data.c_servico.as_deref()),
optional_field("cMun", data.c_mun.as_deref()),
optional_field("cPais", data.c_pais.as_deref()),
optional_field("nProcesso", data.n_processo.as_deref()),
optional_field("indIncentivo", data.ind_incentivo.as_deref()),
];
TaxElement {
outer_tag: None,
outer_fields: vec![],
variant_tag: "ISSQN".into(),
fields: filter_fields(fields),
}
}
pub fn build_issqn_xml(data: &IssqnData) -> String {
serialize_tax_element(&calculate_issqn(data, None))
}
pub fn build_issqn_xml_with_totals(data: &IssqnData, totals: &mut IssqnTotals) -> String {
serialize_tax_element(&calculate_issqn(data, Some(totals)))
}
fn calculate_imposto_devol(p_devol: i64, v_ipi_devol: i64) -> TaxElement {
TaxElement {
outer_tag: Some("impostoDevol".into()),
outer_fields: vec![TaxField::new(
"pDevol",
format!("{:.2}", p_devol as f64 / 100.0),
)],
variant_tag: "IPI".into(),
fields: vec![TaxField::new("vIPIDevol", format_cents_2(v_ipi_devol))],
}
}
pub fn build_imposto_devol(p_devol: i64, v_ipi_devol: i64) -> String {
serialize_tax_element(&calculate_imposto_devol(p_devol, v_ipi_devol))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn issqn_xml_required_fields_only() {
let data = IssqnData::new(10000, 500, 500, "3550308", "14.01");
let xml = build_issqn_xml(&data);
assert_eq!(
xml,
"<ISSQN>\
<vBC>100.00</vBC>\
<vAliq>5.0000</vAliq>\
<vISSQN>5.00</vISSQN>\
<cMunFG>3550308</cMunFG>\
<cListServ>14.01</cListServ>\
</ISSQN>"
);
}
#[test]
fn issqn_xml_with_all_optional_fields() {
let data = IssqnData::new(20000, 300, 600, "3304557", "07.02")
.v_deducao(1000)
.v_outro(500)
.v_desc_incond(200)
.v_desc_cond(100)
.v_iss_ret(300)
.ind_iss("1")
.c_servico("1234")
.c_mun("3304557")
.c_pais("1058")
.n_processo("PROC-2026")
.ind_incentivo("2");
let xml = build_issqn_xml(&data);
assert_eq!(
xml,
"<ISSQN>\
<vBC>200.00</vBC>\
<vAliq>3.0000</vAliq>\
<vISSQN>6.00</vISSQN>\
<cMunFG>3304557</cMunFG>\
<cListServ>07.02</cListServ>\
<vDeducao>10.00</vDeducao>\
<vOutro>5.00</vOutro>\
<vDescIncond>2.00</vDescIncond>\
<vDescCond>1.00</vDescCond>\
<vISSRet>3.00</vISSRet>\
<indISS>1</indISS>\
<cServico>1234</cServico>\
<cMun>3304557</cMun>\
<cPais>1058</cPais>\
<nProcesso>PROC-2026</nProcesso>\
<indIncentivo>2</indIncentivo>\
</ISSQN>"
);
}
#[test]
fn issqn_totals_accumulate_when_vbc_positive() {
let mut totals = create_issqn_totals();
let data1 = IssqnData::new(10000, 500, 500, "3550308", "14.01")
.v_iss_ret(100)
.v_deducao(200)
.v_outro(50)
.v_desc_incond(30)
.v_desc_cond(20);
let _ = build_issqn_xml_with_totals(&data1, &mut totals);
let data2 = IssqnData::new(20000, 300, 600, "3304557", "07.02")
.v_iss_ret(150)
.v_deducao(100);
let _ = build_issqn_xml_with_totals(&data2, &mut totals);
assert_eq!(totals.v_bc, 30000);
assert_eq!(totals.v_iss, 1100);
assert_eq!(totals.v_iss_ret, 250);
assert_eq!(totals.v_deducao, 300);
assert_eq!(totals.v_outro, 50);
assert_eq!(totals.v_desc_incond, 30);
assert_eq!(totals.v_desc_cond, 20);
}
#[test]
fn issqn_totals_skip_when_vbc_is_zero() {
let mut totals = create_issqn_totals();
let data = IssqnData::new(0, 500, 0, "3550308", "14.01").v_iss_ret(100);
let _ = build_issqn_xml_with_totals(&data, &mut totals);
assert_eq!(totals.v_bc, 0);
assert_eq!(totals.v_iss, 0);
assert_eq!(totals.v_iss_ret, 0);
}
#[test]
fn imposto_devol_xml() {
let xml = build_imposto_devol(10000, 5000);
assert_eq!(
xml,
"<impostoDevol>\
<pDevol>100.00</pDevol>\
<IPI>\
<vIPIDevol>50.00</vIPIDevol>\
</IPI>\
</impostoDevol>"
);
}
}