1use 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
11pub 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
45pub(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 let add_info = data.additional_info.as_ref();
60 let has_additional = add_info.is_some_and(|a| {
61 a.taxpayer_note.is_some()
62 || a.tax_authority_note.is_some()
63 || a.contributor_obs.as_ref().is_some_and(|v| !v.is_empty())
64 || a.fiscal_obs.as_ref().is_some_and(|v| !v.is_empty())
65 || a.process_refs.as_ref().is_some_and(|v| !v.is_empty())
66 });
67
68 if notes.is_empty() && !has_additional {
69 return String::new();
70 }
71
72 let mut children = Vec::new();
73
74 if let Some(note) = add_info.and_then(|a| a.tax_authority_note.as_ref()) {
76 children.push(tag("infAdFisco", &[], TagContent::Text(note)));
77 }
78
79 if let Some(tn) = add_info.and_then(|a| a.taxpayer_note.as_ref()) {
81 notes.push(tn.to_string());
82 }
83 if !notes.is_empty() {
84 children.push(tag("infCpl", &[], TagContent::Text(¬es.join("; "))));
85 }
86
87 if let Some(obs_list) = add_info.and_then(|a| a.contributor_obs.as_ref()) {
89 for obs in obs_list.iter().take(10) {
90 children.push(tag(
91 "obsCont",
92 &[("xCampo", &obs.field)],
93 TagContent::Children(vec![tag("xTexto", &[], TagContent::Text(&obs.text))]),
94 ));
95 }
96 }
97
98 if let Some(obs_list) = add_info.and_then(|a| a.fiscal_obs.as_ref()) {
100 for obs in obs_list.iter().take(10) {
101 children.push(tag(
102 "obsFisco",
103 &[("xCampo", &obs.field)],
104 TagContent::Children(vec![tag("xTexto", &[], TagContent::Text(&obs.text))]),
105 ));
106 }
107 }
108
109 if let Some(procs) = add_info.and_then(|a| a.process_refs.as_ref()) {
111 for p in procs.iter().take(100) {
112 children.push(tag(
113 "procRef",
114 &[],
115 TagContent::Children(vec![
116 tag("nProc", &[], TagContent::Text(&p.number)),
117 tag("indProc", &[], TagContent::Text(&p.origin)),
118 ]),
119 ));
120 }
121 }
122
123 tag("infAdic", &[], TagContent::Children(children))
124}
125
126pub fn build_intermediary(intermed: &IntermediaryData) -> String {
128 let mut children = vec![tag("CNPJ", &[], TagContent::Text(&intermed.tax_id))];
129 if let Some(ref id) = intermed.id_cad_int_tran {
130 children.push(tag("idCadIntTran", &[], TagContent::Text(id)));
131 }
132 tag("infIntermed", &[], TagContent::Children(children))
133}
134
135pub fn build_tech_responsible(tech: &TechResponsibleData) -> String {
141 build_tech_responsible_with_key(tech, "")
142}
143
144pub fn build_tech_responsible_with_key(tech: &TechResponsibleData, access_key: &str) -> String {
150 let mut children = vec![
151 tag("CNPJ", &[], TagContent::Text(&tech.tax_id)),
152 tag("xContato", &[], TagContent::Text(&tech.contact)),
153 tag("email", &[], TagContent::Text(&tech.email)),
154 ];
155 if let Some(ref phone) = tech.phone {
156 children.push(tag("fone", &[], TagContent::Text(phone)));
157 }
158 if let (Some(csrt), Some(csrt_id)) = (&tech.csrt, &tech.csrt_id) {
159 if !access_key.is_empty() {
160 children.push(tag("idCSRT", &[], TagContent::Text(csrt_id)));
161 let hash = compute_hash_csrt(csrt, access_key);
162 children.push(tag("hashCSRT", &[], TagContent::Text(&hash)));
163 }
164 }
165 tag("infRespTec", &[], TagContent::Children(children))
166}
167
168fn compute_hash_csrt(csrt: &str, access_key: &str) -> String {
173 use sha1::{Digest, Sha1};
174 let combined = format!("{csrt}{access_key}");
175 let mut hasher = Sha1::new();
176 hasher.update(combined.as_bytes());
177 let raw_hash = hasher.finalize();
178 base64::engine::general_purpose::STANDARD.encode(raw_hash)
179}
180
181pub fn build_purchase(purchase: &PurchaseData) -> String {
183 let mut children = Vec::new();
184 if let Some(ref note) = purchase.purchase_note {
185 children.push(tag("xNEmp", &[], TagContent::Text(note)));
186 }
187 if let Some(ref order) = purchase.order_number {
188 children.push(tag("xPed", &[], TagContent::Text(order)));
189 }
190 if let Some(ref contract) = purchase.contract_number {
191 children.push(tag("xCont", &[], TagContent::Text(contract)));
192 }
193 tag("compra", &[], TagContent::Children(children))
194}
195
196pub fn build_export(exp: &ExportData) -> String {
198 let mut children = vec![
199 tag("UFSaidaPais", &[], TagContent::Text(&exp.exit_state)),
200 tag("xLocExporta", &[], TagContent::Text(&exp.export_location)),
201 ];
202 if let Some(ref dispatch) = exp.dispatch_location {
203 children.push(tag("xLocDespacho", &[], TagContent::Text(dispatch)));
204 }
205 tag("exporta", &[], TagContent::Children(children))
206}
207
208pub fn build_withdrawal(w: &LocationData) -> String {
210 let tid = TaxId::new(&w.tax_id);
211 let padded = tid.padded();
212 let mut children = vec![tag(tid.tag_name(), &[], TagContent::Text(&padded))];
213 if let Some(ref name) = w.name {
214 children.push(tag("xNome", &[], TagContent::Text(name)));
215 }
216 children.extend(build_address_fields(
217 &w.street,
218 &w.number,
219 w.complement.as_deref(),
220 &w.district,
221 &w.city_code.0,
222 &w.city_name,
223 &w.state_code,
224 w.zip_code.as_deref(),
225 false,
226 None,
227 ));
228 tag("retirada", &[], TagContent::Children(children))
229}
230
231pub fn build_delivery(d: &LocationData) -> String {
233 let tid = TaxId::new(&d.tax_id);
234 let padded = tid.padded();
235 let mut children = vec![tag(tid.tag_name(), &[], TagContent::Text(&padded))];
236 if let Some(ref name) = d.name {
237 children.push(tag("xNome", &[], TagContent::Text(name)));
238 }
239 children.extend(build_address_fields(
240 &d.street,
241 &d.number,
242 d.complement.as_deref(),
243 &d.district,
244 &d.city_code.0,
245 &d.city_name,
246 &d.state_code,
247 d.zip_code.as_deref(),
248 false,
249 None,
250 ));
251 tag("entrega", &[], TagContent::Children(children))
252}
253
254pub fn build_cana(cana: &CanaData) -> String {
272 let fc2 = |c: i64| format_cents(c, 2);
273 let fc10 = |c: i64| format_cents(c, 10);
274
275 let mut children = vec![
276 tag("safra", &[], TagContent::Text(&cana.safra)),
277 tag("ref", &[], TagContent::Text(&cana.referencia)),
278 ];
279
280 for fd in cana.for_dia.iter().take(31) {
282 let dia_str = fd.dia.to_string();
283 children.push(tag(
284 "forDia",
285 &[("dia", &dia_str)],
286 TagContent::Children(vec![tag("qtde", &[], TagContent::Text(&fc10(fd.qtde.0)))]),
287 ));
288 }
289
290 children.push(tag(
291 "qTotMes",
292 &[],
293 TagContent::Text(&fc10(cana.q_tot_mes.0)),
294 ));
295 children.push(tag(
296 "qTotAnt",
297 &[],
298 TagContent::Text(&fc10(cana.q_tot_ant.0)),
299 ));
300 children.push(tag(
301 "qTotGer",
302 &[],
303 TagContent::Text(&fc10(cana.q_tot_ger.0)),
304 ));
305
306 if let Some(ref deducs) = cana.deducoes {
308 for d in deducs.iter().take(10) {
309 children.push(tag(
310 "deduc",
311 &[],
312 TagContent::Children(vec![
313 tag("xDed", &[], TagContent::Text(&d.x_ded)),
314 tag("vDed", &[], TagContent::Text(&fc2(d.v_ded.0))),
315 ]),
316 ));
317 }
318 }
319
320 children.push(tag("vFor", &[], TagContent::Text(&fc2(cana.v_for.0))));
321 children.push(tag(
322 "vTotDed",
323 &[],
324 TagContent::Text(&fc2(cana.v_tot_ded.0)),
325 ));
326 children.push(tag(
327 "vLiqFor",
328 &[],
329 TagContent::Text(&fc2(cana.v_liq_for.0)),
330 ));
331
332 tag("cana", &[], TagContent::Children(children))
333}
334
335pub fn build_agropecuario(data: &AgropecuarioData) -> String {
340 let children = match data {
341 AgropecuarioData::Guia(guia) => {
342 let mut kids = vec![tag("tpGuia", &[], TagContent::Text(&guia.tp_guia))];
343 if let Some(ref uf) = guia.uf_guia {
344 kids.push(tag("UFGuia", &[], TagContent::Text(uf)));
345 }
346 if let Some(ref serie) = guia.serie_guia {
347 kids.push(tag("serieGuia", &[], TagContent::Text(serie)));
348 }
349 kids.push(tag("nGuia", &[], TagContent::Text(&guia.n_guia)));
350 vec![tag("guiaTransito", &[], TagContent::Children(kids))]
351 }
352 AgropecuarioData::Defensivos(defs) => defs
353 .iter()
354 .take(20)
355 .map(|d| {
356 tag(
357 "defensivo",
358 &[],
359 TagContent::Children(vec![
360 tag("nReceituario", &[], TagContent::Text(&d.n_receituario)),
361 tag("CPFRespTec", &[], TagContent::Text(&d.cpf_resp_tec)),
362 ]),
363 )
364 })
365 .collect(),
366 };
367
368 tag("agropecuario", &[], TagContent::Children(children))
369}
370
371pub fn build_compra_gov(data: &CompraGovData) -> String {
375 tag(
376 "gCompraGov",
377 &[],
378 TagContent::Children(vec![
379 tag("tpEnteGov", &[], TagContent::Text(&data.tp_ente_gov)),
380 tag("pRedutor", &[], TagContent::Text(&data.p_redutor)),
381 tag("tpOperGov", &[], TagContent::Text(&data.tp_oper_gov)),
382 ]),
383 )
384}
385
386pub fn build_pag_antecipado(data: &PagAntecipadoData) -> String {
390 let children: Vec<String> = data
391 .ref_nfe
392 .iter()
393 .map(|key| tag("refNFe", &[], TagContent::Text(key)))
394 .collect();
395 tag("gPagAntecipado", &[], TagContent::Children(children))
396}
397
398pub fn build_aut_xml(entry: &AuthorizedXml) -> String {
400 let tid = TaxId::new(&entry.tax_id);
401 let padded = tid.padded();
402 tag(
403 "autXML",
404 &[],
405 TagContent::Children(vec![tag(tid.tag_name(), &[], TagContent::Text(&padded))]),
406 )
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412 use crate::newtypes::Cents;
413
414 #[test]
415 fn build_cana_minimal_without_deducoes() {
416 let cana = CanaData::new(
417 "2025/2026",
418 "03/2026",
419 vec![
420 ForDiaData::new(1, Cents(1000000)),
421 ForDiaData::new(2, Cents(1500000)),
422 ],
423 Cents(2500000), Cents(5000000), Cents(7500000), Cents(150000), Cents(0), Cents(150000), );
430
431 let xml = build_cana(&cana);
432
433 assert_eq!(
434 xml,
435 "<cana>\
436 <safra>2025/2026</safra>\
437 <ref>03/2026</ref>\
438 <forDia dia=\"1\"><qtde>10000.0000000000</qtde></forDia>\
439 <forDia dia=\"2\"><qtde>15000.0000000000</qtde></forDia>\
440 <qTotMes>25000.0000000000</qTotMes>\
441 <qTotAnt>50000.0000000000</qTotAnt>\
442 <qTotGer>75000.0000000000</qTotGer>\
443 <vFor>1500.00</vFor>\
444 <vTotDed>0.00</vTotDed>\
445 <vLiqFor>1500.00</vLiqFor>\
446 </cana>"
447 );
448 }
449
450 #[test]
451 fn build_cana_with_deducoes() {
452 let cana = CanaData::new(
453 "2024/2025",
454 "06/2025",
455 vec![ForDiaData::new(15, Cents(2000000))],
456 Cents(2000000), Cents(10000000), Cents(12000000), Cents(500000), Cents(50000), Cents(450000), )
463 .deducoes(vec![
464 DeducData::new("TAXA PRODUCAO", Cents(30000)),
465 DeducData::new("FUNRURAL", Cents(20000)),
466 ]);
467
468 let xml = build_cana(&cana);
469
470 assert!(xml.contains("<safra>2024/2025</safra>"));
471 assert!(xml.contains("<ref>06/2025</ref>"));
472 assert!(xml.contains("<forDia dia=\"15\"><qtde>20000.0000000000</qtde></forDia>"));
473 assert!(xml.contains("<qTotMes>20000.0000000000</qTotMes>"));
474 assert!(xml.contains("<qTotAnt>100000.0000000000</qTotAnt>"));
475 assert!(xml.contains("<qTotGer>120000.0000000000</qTotGer>"));
476 assert!(xml.contains("<deduc><xDed>TAXA PRODUCAO</xDed><vDed>300.00</vDed></deduc>"));
477 assert!(xml.contains("<deduc><xDed>FUNRURAL</xDed><vDed>200.00</vDed></deduc>"));
478 assert!(xml.contains("<vFor>5000.00</vFor>"));
479 assert!(xml.contains("<vTotDed>500.00</vTotDed>"));
480 assert!(xml.contains("<vLiqFor>4500.00</vLiqFor>"));
481
482 let deduc_pos = xml.find("<deduc>").expect("deduc must be present");
484 let vfor_pos = xml.find("<vFor>").expect("vFor must be present");
485 assert!(
486 deduc_pos < vfor_pos,
487 "deduc must come before vFor in the XML"
488 );
489
490 let fordia_pos = xml.find("<forDia").expect("forDia must be present");
492 let qtotmes_pos = xml.find("<qTotMes>").expect("qTotMes must be present");
493 assert!(
494 fordia_pos < qtotmes_pos,
495 "forDia must come before qTotMes in the XML"
496 );
497 }
498
499 #[test]
500 fn build_cana_limits_fordia_to_31() {
501 let mut for_dia = Vec::new();
502 for day in 1..=35 {
503 for_dia.push(ForDiaData::new(day, Cents(100000)));
504 }
505
506 let cana = CanaData::new(
507 "2025/2026",
508 "01/2026",
509 for_dia,
510 Cents(0),
511 Cents(0),
512 Cents(0),
513 Cents(0),
514 Cents(0),
515 Cents(0),
516 );
517
518 let xml = build_cana(&cana);
519
520 let count = xml.matches("<forDia").count();
522 assert_eq!(count, 31, "forDia entries must be capped at 31");
523 }
524
525 #[test]
526 fn build_cana_limits_deduc_to_10() {
527 let mut deducs = Vec::new();
528 for i in 1..=15 {
529 deducs.push(DeducData::new(format!("DEDUC {i}"), Cents(1000)));
530 }
531
532 let cana = CanaData::new(
533 "2025/2026",
534 "01/2026",
535 vec![ForDiaData::new(1, Cents(100000))],
536 Cents(0),
537 Cents(0),
538 Cents(0),
539 Cents(0),
540 Cents(0),
541 Cents(0),
542 )
543 .deducoes(deducs);
544
545 let xml = build_cana(&cana);
546
547 let count = xml.matches("<deduc>").count();
549 assert_eq!(count, 10, "deduc entries must be capped at 10");
550 }
551
552 #[test]
555 fn build_agropecuario_guia() {
556 let guia = AgropecuarioGuiaData::new("1", "12345")
557 .uf_guia("SP")
558 .serie_guia("A");
559 let data = AgropecuarioData::Guia(guia);
560 let xml = build_agropecuario(&data);
561
562 assert_eq!(
563 xml,
564 "<agropecuario>\
565 <guiaTransito>\
566 <tpGuia>1</tpGuia>\
567 <UFGuia>SP</UFGuia>\
568 <serieGuia>A</serieGuia>\
569 <nGuia>12345</nGuia>\
570 </guiaTransito>\
571 </agropecuario>"
572 );
573 }
574
575 #[test]
576 fn build_agropecuario_guia_minimal() {
577 let guia = AgropecuarioGuiaData::new("2", "99999");
578 let data = AgropecuarioData::Guia(guia);
579 let xml = build_agropecuario(&data);
580
581 assert_eq!(
582 xml,
583 "<agropecuario>\
584 <guiaTransito>\
585 <tpGuia>2</tpGuia>\
586 <nGuia>99999</nGuia>\
587 </guiaTransito>\
588 </agropecuario>"
589 );
590 }
591
592 #[test]
593 fn build_agropecuario_defensivos() {
594 let defs = vec![
595 AgropecuarioDefensivoData::new("REC001", "12345678901"),
596 AgropecuarioDefensivoData::new("REC002", "98765432109"),
597 ];
598 let data = AgropecuarioData::Defensivos(defs);
599 let xml = build_agropecuario(&data);
600
601 assert_eq!(
602 xml,
603 "<agropecuario>\
604 <defensivo>\
605 <nReceituario>REC001</nReceituario>\
606 <CPFRespTec>12345678901</CPFRespTec>\
607 </defensivo>\
608 <defensivo>\
609 <nReceituario>REC002</nReceituario>\
610 <CPFRespTec>98765432109</CPFRespTec>\
611 </defensivo>\
612 </agropecuario>"
613 );
614 }
615
616 #[test]
617 fn build_agropecuario_defensivos_capped_at_20() {
618 let defs: Vec<AgropecuarioDefensivoData> = (0..25)
619 .map(|i| AgropecuarioDefensivoData::new(format!("REC{i:03}"), "12345678901"))
620 .collect();
621 let data = AgropecuarioData::Defensivos(defs);
622 let xml = build_agropecuario(&data);
623
624 let count = xml.matches("<defensivo>").count();
625 assert_eq!(count, 20, "defensivo entries must be capped at 20");
626 }
627
628 #[test]
631 fn build_compra_gov_all_fields() {
632 let cg = CompraGovData::new("1", "10.5000", "2");
633 let xml = build_compra_gov(&cg);
634
635 assert_eq!(
636 xml,
637 "<gCompraGov>\
638 <tpEnteGov>1</tpEnteGov>\
639 <pRedutor>10.5000</pRedutor>\
640 <tpOperGov>2</tpOperGov>\
641 </gCompraGov>"
642 );
643 }
644
645 #[test]
648 fn build_pag_antecipado_single_ref() {
649 let pa = PagAntecipadoData::new(vec![
650 "41260304123456000190550010000001231123456780".to_string(),
651 ]);
652 let xml = build_pag_antecipado(&pa);
653
654 assert_eq!(
655 xml,
656 "<gPagAntecipado>\
657 <refNFe>41260304123456000190550010000001231123456780</refNFe>\
658 </gPagAntecipado>"
659 );
660 }
661
662 #[test]
663 fn build_pag_antecipado_multiple_refs() {
664 let pa = PagAntecipadoData::new(vec![
665 "41260304123456000190550010000001231123456780".to_string(),
666 "41260304123456000190550010000001241123456781".to_string(),
667 ]);
668 let xml = build_pag_antecipado(&pa);
669
670 assert!(xml.contains("<gPagAntecipado>"));
671 assert_eq!(xml.matches("<refNFe>").count(), 2);
672 }
673}