use serde::{Deserialize, Serialize};
use crate::format_utils::{format_cents_or_zero, format_rate_4, format_rate4_or_zero};
use crate::newtypes::{Cents, Rate, Rate4};
use crate::tax_element::{TaxElement, TaxField, serialize_tax_element};
const ALIQ_CSTS: &[&str] = &["01", "02"];
const QTDE_CSTS: &[&str] = &["03"];
const NT_CSTS: &[&str] = &["04", "05", "06", "07", "08", "09"];
const OUTR_CSTS: &[&str] = &[
"49", "50", "51", "52", "53", "54", "55", "56", "60", "61", "62", "63", "64", "65", "66", "67",
"70", "71", "72", "73", "74", "75", "98", "99",
];
const IPI_TRIB_CSTS: &[&str] = &["00", "49", "50", "99"];
#[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 PisData {
pub cst: String,
pub v_bc: Option<Cents>,
pub p_pis: Option<Rate4>,
pub v_pis: Option<Cents>,
pub q_bc_prod: Option<i64>,
pub v_aliq_prod: Option<i64>,
}
impl PisData {
pub fn new(cst: impl Into<String>) -> Self {
Self {
cst: cst.into(),
..Default::default()
}
}
pub fn v_bc(mut self, v: Cents) -> Self {
self.v_bc = Some(v);
self
}
pub fn p_pis(mut self, v: Rate4) -> Self {
self.p_pis = Some(v);
self
}
pub fn v_pis(mut self, v: Cents) -> Self {
self.v_pis = Some(v);
self
}
pub fn q_bc_prod(mut self, v: i64) -> Self {
self.q_bc_prod = Some(v);
self
}
pub fn v_aliq_prod(mut self, v: i64) -> Self {
self.v_aliq_prod = Some(v);
self
}
}
#[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 PisStData {
pub v_bc: Option<Cents>,
pub p_pis: Option<Rate4>,
pub q_bc_prod: Option<i64>,
pub v_aliq_prod: Option<i64>,
pub v_pis: Cents,
pub ind_soma_pis_st: Option<i64>,
}
impl PisStData {
pub fn new(v_pis: Cents) -> Self {
Self {
v_pis,
..Default::default()
}
}
pub fn v_bc(mut self, v: Cents) -> Self {
self.v_bc = Some(v);
self
}
pub fn p_pis(mut self, v: Rate4) -> Self {
self.p_pis = Some(v);
self
}
pub fn q_bc_prod(mut self, v: i64) -> Self {
self.q_bc_prod = Some(v);
self
}
pub fn v_aliq_prod(mut self, v: i64) -> Self {
self.v_aliq_prod = Some(v);
self
}
pub fn ind_soma_pis_st(mut self, v: i64) -> Self {
self.ind_soma_pis_st = Some(v);
self
}
}
#[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 CofinsData {
pub cst: String,
pub v_bc: Option<Cents>,
pub p_cofins: Option<Rate4>,
pub v_cofins: Option<Cents>,
pub q_bc_prod: Option<i64>,
pub v_aliq_prod: Option<i64>,
}
impl CofinsData {
pub fn new(cst: impl Into<String>) -> Self {
Self {
cst: cst.into(),
..Default::default()
}
}
pub fn v_bc(mut self, v: Cents) -> Self {
self.v_bc = Some(v);
self
}
pub fn p_cofins(mut self, v: Rate4) -> Self {
self.p_cofins = Some(v);
self
}
pub fn v_cofins(mut self, v: Cents) -> Self {
self.v_cofins = Some(v);
self
}
pub fn q_bc_prod(mut self, v: i64) -> Self {
self.q_bc_prod = Some(v);
self
}
pub fn v_aliq_prod(mut self, v: i64) -> Self {
self.v_aliq_prod = Some(v);
self
}
}
#[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 CofinsStData {
pub v_bc: Option<Cents>,
pub p_cofins: Option<Rate4>,
pub q_bc_prod: Option<i64>,
pub v_aliq_prod: Option<i64>,
pub v_cofins: Cents,
pub ind_soma_cofins_st: Option<i64>,
}
impl CofinsStData {
pub fn new(v_cofins: Cents) -> Self {
Self {
v_cofins,
..Default::default()
}
}
pub fn v_bc(mut self, v: Cents) -> Self {
self.v_bc = Some(v);
self
}
pub fn p_cofins(mut self, v: Rate4) -> Self {
self.p_cofins = Some(v);
self
}
pub fn q_bc_prod(mut self, v: i64) -> Self {
self.q_bc_prod = Some(v);
self
}
pub fn v_aliq_prod(mut self, v: i64) -> Self {
self.v_aliq_prod = Some(v);
self
}
pub fn ind_soma_cofins_st(mut self, v: i64) -> Self {
self.ind_soma_cofins_st = Some(v);
self
}
}
#[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 IpiData {
pub cst: String,
pub c_enq: String,
pub cnpj_prod: Option<String>,
pub c_selo: Option<String>,
pub q_selo: Option<i64>,
pub v_bc: Option<Cents>,
pub p_ipi: Option<Rate>,
pub q_unid: Option<i64>,
pub v_unid: Option<i64>,
pub v_ipi: Option<Cents>,
}
impl IpiData {
pub fn new(cst: impl Into<String>, c_enq: impl Into<String>) -> Self {
Self {
cst: cst.into(),
c_enq: c_enq.into(),
..Default::default()
}
}
pub fn cnpj_prod(mut self, v: impl Into<String>) -> Self {
self.cnpj_prod = Some(v.into());
self
}
pub fn c_selo(mut self, v: impl Into<String>) -> Self {
self.c_selo = Some(v.into());
self
}
pub fn q_selo(mut self, v: i64) -> Self {
self.q_selo = Some(v);
self
}
pub fn v_bc(mut self, v: Cents) -> Self {
self.v_bc = Some(v);
self
}
pub fn p_ipi(mut self, v: Rate) -> Self {
self.p_ipi = Some(v);
self
}
pub fn q_unid(mut self, v: i64) -> Self {
self.q_unid = Some(v);
self
}
pub fn v_unid(mut self, v: i64) -> Self {
self.v_unid = Some(v);
self
}
pub fn v_ipi(mut self, v: Cents) -> Self {
self.v_ipi = Some(v);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts", ts(export))]
#[non_exhaustive]
pub struct IiData {
pub v_bc: Cents,
pub v_desp_adu: Cents,
pub v_ii: Cents,
pub v_iof: Cents,
}
impl IiData {
pub fn new(v_bc: Cents, v_desp_adu: Cents, v_ii: Cents, v_iof: Cents) -> Self {
Self {
v_bc,
v_desp_adu,
v_ii,
v_iof,
}
}
}
struct ContributionTaxConfig {
tax_name: &'static str,
rate_field: &'static str,
value_field: &'static str,
st_tag: &'static str,
st_indicator: &'static str,
}
const PIS_CONFIG: ContributionTaxConfig = ContributionTaxConfig {
tax_name: "PIS",
rate_field: "pPIS",
value_field: "vPIS",
st_tag: "PISST",
st_indicator: "indSomaPISST",
};
const COFINS_CONFIG: ContributionTaxConfig = ContributionTaxConfig {
tax_name: "COFINS",
rate_field: "pCOFINS",
value_field: "vCOFINS",
st_tag: "COFINSST",
st_indicator: "indSomaCOFINSST",
};
struct ContributionTaxInput<'a> {
cst: &'a str,
v_bc: Option<Cents>,
rate: Option<Rate4>,
value: Option<Cents>,
q_bc_prod: Option<i64>,
v_aliq_prod: Option<i64>,
}
struct ContributionTaxStInput {
v_bc: Option<Cents>,
rate: Option<Rate4>,
q_bc_prod: Option<i64>,
v_aliq_prod: Option<i64>,
value: Cents,
st_indicator: Option<i64>,
}
fn build_contribution_outr_fields(
d: &ContributionTaxInput,
cfg: &ContributionTaxConfig,
) -> Vec<TaxField> {
let mut fields = vec![TaxField::new("CST", d.cst)];
if d.q_bc_prod.is_some() {
fields.push(TaxField::new("qBCProd", format_rate4_or_zero(d.q_bc_prod)));
fields.push(TaxField::new(
"vAliqProd",
format_rate4_or_zero(d.v_aliq_prod),
));
} else {
fields.push(TaxField::new(
"vBC",
format_cents_or_zero(d.v_bc.map(|c| c.0), 2),
));
fields.push(TaxField::new(
cfg.rate_field,
format_rate4_or_zero(d.rate.map(|r| r.0)),
));
}
fields.push(TaxField::new(
cfg.value_field,
format_cents_or_zero(d.value.map(|c| c.0), 2),
));
fields
}
fn calculate_contribution_tax(d: &ContributionTaxInput, cfg: &ContributionTaxConfig) -> TaxElement {
let cst = d.cst;
let (variant_tag, fields) = if ALIQ_CSTS.contains(&cst) {
let tag = format!("{}Aliq", cfg.tax_name);
let fields = vec![
TaxField::new("CST", cst),
TaxField::new("vBC", format_cents_or_zero(d.v_bc.map(|c| c.0), 2)),
TaxField::new(cfg.rate_field, format_rate4_or_zero(d.rate.map(|r| r.0))),
TaxField::new(
cfg.value_field,
format_cents_or_zero(d.value.map(|c| c.0), 2),
),
];
(tag, fields)
} else if QTDE_CSTS.contains(&cst) {
let tag = format!("{}Qtde", cfg.tax_name);
let fields = vec![
TaxField::new("CST", cst),
TaxField::new("qBCProd", format_rate4_or_zero(d.q_bc_prod)),
TaxField::new("vAliqProd", format_rate4_or_zero(d.v_aliq_prod)),
TaxField::new(
cfg.value_field,
format_cents_or_zero(d.value.map(|c| c.0), 2),
),
];
(tag, fields)
} else if NT_CSTS.contains(&cst) {
let tag = format!("{}NT", cfg.tax_name);
let fields = vec![TaxField::new("CST", cst)];
(tag, fields)
} else if OUTR_CSTS.contains(&cst) {
let tag = format!("{}Outr", cfg.tax_name);
let fields = build_contribution_outr_fields(d, cfg);
(tag, fields)
} else {
let tag = format!("{}NT", cfg.tax_name);
let fields = vec![TaxField::new("CST", cst)];
(tag, fields)
};
TaxElement {
outer_tag: Some(cfg.tax_name.to_string()),
outer_fields: vec![],
variant_tag,
fields,
}
}
fn calculate_contribution_tax_st(
d: &ContributionTaxStInput,
cfg: &ContributionTaxConfig,
) -> TaxElement {
let mut fields = Vec::new();
if d.q_bc_prod.is_some() {
fields.push(TaxField::new("qBCProd", format_rate4_or_zero(d.q_bc_prod)));
fields.push(TaxField::new(
"vAliqProd",
format_rate4_or_zero(d.v_aliq_prod),
));
} else {
fields.push(TaxField::new(
"vBC",
format_cents_or_zero(d.v_bc.map(|c| c.0), 2),
));
fields.push(TaxField::new(
cfg.rate_field,
format_rate4_or_zero(d.rate.map(|r| r.0)),
));
}
fields.push(TaxField::new(
cfg.value_field,
format_cents_or_zero(Some(d.value.0), 2),
));
if let Some(indicator) = d.st_indicator {
fields.push(TaxField::new(cfg.st_indicator, indicator.to_string()));
}
TaxElement {
outer_tag: None,
outer_fields: vec![],
variant_tag: cfg.st_tag.to_string(),
fields,
}
}
pub fn build_pis_xml(data: &PisData) -> String {
let input = ContributionTaxInput {
cst: &data.cst,
v_bc: data.v_bc,
rate: data.p_pis,
value: data.v_pis,
q_bc_prod: data.q_bc_prod,
v_aliq_prod: data.v_aliq_prod,
};
let element = calculate_contribution_tax(&input, &PIS_CONFIG);
serialize_tax_element(&element)
}
pub fn build_pis_st_xml(data: &PisStData) -> String {
let input = ContributionTaxStInput {
v_bc: data.v_bc,
rate: data.p_pis,
q_bc_prod: data.q_bc_prod,
v_aliq_prod: data.v_aliq_prod,
value: data.v_pis,
st_indicator: data.ind_soma_pis_st,
};
let element = calculate_contribution_tax_st(&input, &PIS_CONFIG);
serialize_tax_element(&element)
}
pub fn build_cofins_xml(data: &CofinsData) -> String {
let input = ContributionTaxInput {
cst: &data.cst,
v_bc: data.v_bc,
rate: data.p_cofins,
value: data.v_cofins,
q_bc_prod: data.q_bc_prod,
v_aliq_prod: data.v_aliq_prod,
};
let element = calculate_contribution_tax(&input, &COFINS_CONFIG);
serialize_tax_element(&element)
}
pub fn build_cofins_st_xml(data: &CofinsStData) -> String {
let input = ContributionTaxStInput {
v_bc: data.v_bc,
rate: data.p_cofins,
q_bc_prod: data.q_bc_prod,
v_aliq_prod: data.v_aliq_prod,
value: data.v_cofins,
st_indicator: data.ind_soma_cofins_st,
};
let element = calculate_contribution_tax_st(&input, &COFINS_CONFIG);
serialize_tax_element(&element)
}
pub fn build_ipi_xml(data: &IpiData) -> String {
let mut outer_fields = Vec::new();
if let Some(ref cnpj) = data.cnpj_prod {
outer_fields.push(TaxField::new("CNPJProd", cnpj.as_str()));
}
if let Some(ref selo) = data.c_selo {
outer_fields.push(TaxField::new("cSelo", selo.as_str()));
}
if let Some(q) = data.q_selo {
outer_fields.push(TaxField::new("qSelo", q.to_string()));
}
outer_fields.push(TaxField::new("cEnq", &data.c_enq));
let (variant_tag, fields) = if IPI_TRIB_CSTS.contains(&data.cst.as_str()) {
let mut fields = vec![TaxField::new("CST", &data.cst)];
if data.v_bc.is_some() && data.p_ipi.is_some() {
fields.push(TaxField::new(
"vBC",
format_cents_or_zero(data.v_bc.map(|c| c.0), 2),
));
fields.push(TaxField::new(
"pIPI",
format_rate_4(data.p_ipi.map(|r| r.0).unwrap_or(0)),
));
} else {
fields.push(TaxField::new("qUnid", format_rate4_or_zero(data.q_unid)));
fields.push(TaxField::new("vUnid", format_rate4_or_zero(data.v_unid)));
}
fields.push(TaxField::new(
"vIPI",
format_cents_or_zero(data.v_ipi.map(|c| c.0), 2),
));
("IPITrib".to_string(), fields)
} else {
let fields = vec![TaxField::new("CST", &data.cst)];
("IPINT".to_string(), fields)
};
let element = TaxElement {
outer_tag: Some("IPI".to_string()),
outer_fields,
variant_tag,
fields,
};
serialize_tax_element(&element)
}
pub fn build_ii_xml(data: &IiData) -> String {
let element = TaxElement {
outer_tag: None,
outer_fields: vec![],
variant_tag: "II".to_string(),
fields: vec![
TaxField::new("vBC", format_cents_or_zero(Some(data.v_bc.0), 2)),
TaxField::new("vDespAdu", format_cents_or_zero(Some(data.v_desp_adu.0), 2)),
TaxField::new("vII", format_cents_or_zero(Some(data.v_ii.0), 2)),
TaxField::new("vIOF", format_cents_or_zero(Some(data.v_iof.0), 2)),
],
};
serialize_tax_element(&element)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_pis_xml_unknown_cst_fallback_to_nt() {
let data = PisData::new("88");
let xml = build_pis_xml(&data);
assert!(xml.contains("<PISNT>"));
assert!(xml.contains("<CST>88</CST>"));
}
#[test]
fn build_cofins_xml_unknown_cst_fallback_to_nt() {
let data = CofinsData::new("88");
let xml = build_cofins_xml(&data);
assert!(xml.contains("<COFINSNT>"));
assert!(xml.contains("<CST>88</CST>"));
}
}