Skip to main content

fiscal_core/xml_builder/
optional.rs

1//! Build optional XML groups: cobr, infAdic, infIntermed, exporta, compra,
2//! infRespTec, retirada, entrega, autXML.
3
4use super::emit::build_address_fields;
5use super::tax_id::TaxId;
6use crate::format_utils::format_cents;
7use crate::types::*;
8use crate::xml_utils::{TagContent, tag};
9use base64::Engine as _;
10
11/// Build `<cobr>` (billing) element.
12pub fn build_cobr(billing: &BillingData) -> String {
13    let fc2 = |c: i64| format_cents(c, 2);
14    let mut children = Vec::new();
15
16    if let Some(ref inv) = billing.invoice {
17        let mut fat_children = vec![
18            tag("nFat", &[], TagContent::Text(&inv.number)),
19            tag("vOrig", &[], TagContent::Text(&fc2(inv.original_value.0))),
20        ];
21        if let Some(disc) = inv.discount_value {
22            fat_children.push(tag("vDesc", &[], TagContent::Text(&fc2(disc.0))));
23        }
24        fat_children.push(tag("vLiq", &[], TagContent::Text(&fc2(inv.net_value.0))));
25        children.push(tag("fat", &[], TagContent::Children(fat_children)));
26    }
27
28    if let Some(ref installments) = billing.installments {
29        for inst in installments {
30            children.push(tag(
31                "dup",
32                &[],
33                TagContent::Children(vec![
34                    tag("nDup", &[], TagContent::Text(&inst.number)),
35                    tag("dVenc", &[], TagContent::Text(&inst.due_date)),
36                    tag("vDup", &[], TagContent::Text(&fc2(inst.value.0))),
37                ]),
38            ));
39        }
40    }
41
42    tag("cobr", &[], TagContent::Children(children))
43}
44
45/// Build `<infAdic>` (additional info) element.
46pub(crate) fn build_inf_adic(data: &InvoiceBuildData) -> String {
47    let mut notes: Vec<String> = Vec::new();
48
49    if let Some(ref cont) = data.contingency {
50        notes.push(format!(
51            "Emitida em contingencia ({}). Motivo: {}",
52            cont.contingency_type.as_str(),
53            cont.reason
54        ));
55    }
56
57    if data.environment == SefazEnvironment::Homologation {
58        notes.push("EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL".to_string());
59    }
60
61    let add_info = data.additional_info.as_ref();
62    let has_additional = add_info.is_some_and(|a| {
63        a.taxpayer_note.is_some()
64            || a.tax_authority_note.is_some()
65            || a.contributor_obs.as_ref().is_some_and(|v| !v.is_empty())
66            || a.fiscal_obs.as_ref().is_some_and(|v| !v.is_empty())
67            || a.process_refs.as_ref().is_some_and(|v| !v.is_empty())
68    });
69
70    if notes.is_empty() && !has_additional {
71        return String::new();
72    }
73
74    let mut children = Vec::new();
75
76    // infAdFisco before infCpl per schema
77    if let Some(note) = add_info.and_then(|a| a.tax_authority_note.as_ref()) {
78        children.push(tag("infAdFisco", &[], TagContent::Text(note)));
79    }
80
81    // Merge contingency/env notes with taxpayer note
82    if let Some(tn) = add_info.and_then(|a| a.taxpayer_note.as_ref()) {
83        notes.push(tn.to_string());
84    }
85    if !notes.is_empty() {
86        children.push(tag("infCpl", &[], TagContent::Text(&notes.join("; "))));
87    }
88
89    // obsCont (max 10)
90    if let Some(obs_list) = add_info.and_then(|a| a.contributor_obs.as_ref()) {
91        for obs in obs_list.iter().take(10) {
92            children.push(tag(
93                "obsCont",
94                &[("xCampo", &obs.field)],
95                TagContent::Children(vec![tag("xTexto", &[], TagContent::Text(&obs.text))]),
96            ));
97        }
98    }
99
100    // obsFisco (max 10)
101    if let Some(obs_list) = add_info.and_then(|a| a.fiscal_obs.as_ref()) {
102        for obs in obs_list.iter().take(10) {
103            children.push(tag(
104                "obsFisco",
105                &[("xCampo", &obs.field)],
106                TagContent::Children(vec![tag("xTexto", &[], TagContent::Text(&obs.text))]),
107            ));
108        }
109    }
110
111    // procRef (max 100)
112    if let Some(procs) = add_info.and_then(|a| a.process_refs.as_ref()) {
113        for p in procs.iter().take(100) {
114            children.push(tag(
115                "procRef",
116                &[],
117                TagContent::Children(vec![
118                    tag("nProc", &[], TagContent::Text(&p.number)),
119                    tag("indProc", &[], TagContent::Text(&p.origin)),
120                ]),
121            ));
122        }
123    }
124
125    tag("infAdic", &[], TagContent::Children(children))
126}
127
128/// Build `<infIntermed>` element.
129pub fn build_intermediary(intermed: &IntermediaryData) -> String {
130    let mut children = vec![tag("CNPJ", &[], TagContent::Text(&intermed.tax_id))];
131    if let Some(ref id) = intermed.id_cad_int_tran {
132        children.push(tag("idCadIntTran", &[], TagContent::Text(id)));
133    }
134    tag("infIntermed", &[], TagContent::Children(children))
135}
136
137/// Build `<infRespTec>` element.
138///
139/// When both `csrt` and `csrt_id` are present, generates `<idCSRT>` and
140/// `<hashCSRT>` tags. The hash follows the PHP sped-nfe algorithm:
141/// `base64(sha1(CSRT + chNFe, raw_binary))`.
142pub fn build_tech_responsible(tech: &TechResponsibleData) -> String {
143    build_tech_responsible_with_key(tech, "")
144}
145
146/// Build `<infRespTec>` element with access key for CSRT hash computation.
147///
148/// When both `csrt` and `csrt_id` are present on `tech`, generates `<idCSRT>` and
149/// `<hashCSRT>` tags. The hash follows the PHP sped-nfe algorithm:
150/// `base64(sha1(CSRT + chNFe, raw_binary))`.
151pub fn build_tech_responsible_with_key(tech: &TechResponsibleData, access_key: &str) -> String {
152    let mut children = vec![
153        tag("CNPJ", &[], TagContent::Text(&tech.tax_id)),
154        tag("xContato", &[], TagContent::Text(&tech.contact)),
155        tag("email", &[], TagContent::Text(&tech.email)),
156    ];
157    if let Some(ref phone) = tech.phone {
158        children.push(tag("fone", &[], TagContent::Text(phone)));
159    }
160    if let (Some(csrt), Some(csrt_id)) = (&tech.csrt, &tech.csrt_id) {
161        if !access_key.is_empty() {
162            children.push(tag("idCSRT", &[], TagContent::Text(csrt_id)));
163            let hash = compute_hash_csrt(csrt, access_key);
164            children.push(tag("hashCSRT", &[], TagContent::Text(&hash)));
165        }
166    }
167    tag("infRespTec", &[], TagContent::Children(children))
168}
169
170/// Compute hashCSRT as defined by the SEFAZ specification.
171///
172/// Algorithm: `base64(sha1(CSRT + chNFe))` — matching PHP's
173/// `base64_encode(sha1($CSRT . $this->chNFe, true))`.
174fn compute_hash_csrt(csrt: &str, access_key: &str) -> String {
175    use sha1::{Digest, Sha1};
176    let combined = format!("{csrt}{access_key}");
177    let mut hasher = Sha1::new();
178    hasher.update(combined.as_bytes());
179    let raw_hash = hasher.finalize();
180    base64::engine::general_purpose::STANDARD.encode(raw_hash)
181}
182
183/// Build `<compra>` (purchase) element.
184pub fn build_purchase(purchase: &PurchaseData) -> String {
185    let mut children = Vec::new();
186    if let Some(ref note) = purchase.purchase_note {
187        children.push(tag("xNEmp", &[], TagContent::Text(note)));
188    }
189    if let Some(ref order) = purchase.order_number {
190        children.push(tag("xPed", &[], TagContent::Text(order)));
191    }
192    if let Some(ref contract) = purchase.contract_number {
193        children.push(tag("xCont", &[], TagContent::Text(contract)));
194    }
195    tag("compra", &[], TagContent::Children(children))
196}
197
198/// Build `<exporta>` element.
199pub fn build_export(exp: &ExportData) -> String {
200    let mut children = vec![
201        tag("UFSaidaPais", &[], TagContent::Text(&exp.exit_state)),
202        tag("xLocExporta", &[], TagContent::Text(&exp.export_location)),
203    ];
204    if let Some(ref dispatch) = exp.dispatch_location {
205        children.push(tag("xLocDespacho", &[], TagContent::Text(dispatch)));
206    }
207    tag("exporta", &[], TagContent::Children(children))
208}
209
210/// Build `<retirada>` (withdrawal) element.
211pub fn build_withdrawal(w: &LocationData) -> String {
212    let tid = TaxId::new(&w.tax_id);
213    let padded = tid.padded();
214    let mut children = vec![tag(tid.tag_name(), &[], TagContent::Text(&padded))];
215    if let Some(ref name) = w.name {
216        children.push(tag("xNome", &[], TagContent::Text(name)));
217    }
218    children.extend(build_address_fields(
219        &w.street,
220        &w.number,
221        w.complement.as_deref(),
222        &w.district,
223        &w.city_code.0,
224        &w.city_name,
225        &w.state_code,
226        w.zip_code.as_deref(),
227        false,
228    ));
229    tag("retirada", &[], TagContent::Children(children))
230}
231
232/// Build `<entrega>` (delivery) element.
233pub fn build_delivery(d: &LocationData) -> String {
234    let tid = TaxId::new(&d.tax_id);
235    let padded = tid.padded();
236    let mut children = vec![tag(tid.tag_name(), &[], TagContent::Text(&padded))];
237    if let Some(ref name) = d.name {
238        children.push(tag("xNome", &[], TagContent::Text(name)));
239    }
240    children.extend(build_address_fields(
241        &d.street,
242        &d.number,
243        d.complement.as_deref(),
244        &d.district,
245        &d.city_code.0,
246        &d.city_name,
247        &d.state_code,
248        d.zip_code.as_deref(),
249        false,
250    ));
251    tag("entrega", &[], TagContent::Children(children))
252}
253
254/// Build `<autXML>` element.
255pub fn build_aut_xml(entry: &AuthorizedXml) -> String {
256    let tid = TaxId::new(&entry.tax_id);
257    let padded = tid.padded();
258    tag(
259        "autXML",
260        &[],
261        TagContent::Children(vec![tag(tid.tag_name(), &[], TagContent::Text(&padded))]),
262    )
263}