1use super::tax_id::TaxId;
5use crate::format_utils::format_cents;
6use crate::types::*;
7use crate::xml_utils::{TagContent, tag};
8use base64::Engine as _;
9
10pub fn build_cobr(billing: &BillingData) -> String {
12 let fc2 = |c: i64| format_cents(c, 2);
13 let mut children = Vec::new();
14
15 if let Some(ref inv) = billing.invoice {
16 let mut fat_children = vec![
17 tag("nFat", &[], TagContent::Text(&inv.number)),
18 tag("vOrig", &[], TagContent::Text(&fc2(inv.original_value.0))),
19 ];
20 if let Some(disc) = inv.discount_value {
21 fat_children.push(tag("vDesc", &[], TagContent::Text(&fc2(disc.0))));
22 }
23 fat_children.push(tag("vLiq", &[], TagContent::Text(&fc2(inv.net_value.0))));
24 children.push(tag("fat", &[], TagContent::Children(fat_children)));
25 }
26
27 if let Some(ref installments) = billing.installments {
28 for inst in installments {
29 children.push(tag(
30 "dup",
31 &[],
32 TagContent::Children(vec![
33 tag("nDup", &[], TagContent::Text(&inst.number)),
34 tag("dVenc", &[], TagContent::Text(&inst.due_date)),
35 tag("vDup", &[], TagContent::Text(&fc2(inst.value.0))),
36 ]),
37 ));
38 }
39 }
40
41 tag("cobr", &[], TagContent::Children(children))
42}
43
44pub(crate) fn build_inf_adic(data: &InvoiceBuildData) -> String {
46 let mut notes: Vec<String> = Vec::new();
47
48 if let Some(ref cont) = data.contingency {
49 notes.push(format!(
50 "Emitida em contingencia ({}). Motivo: {}",
51 cont.contingency_type.as_str(),
52 cont.reason
53 ));
54 }
55
56 let add_info = data.additional_info.as_ref();
59 let has_additional = add_info.is_some_and(|a| {
60 a.taxpayer_note.is_some()
61 || a.tax_authority_note.is_some()
62 || a.contributor_obs.as_ref().is_some_and(|v| !v.is_empty())
63 || a.fiscal_obs.as_ref().is_some_and(|v| !v.is_empty())
64 || a.process_refs.as_ref().is_some_and(|v| !v.is_empty())
65 });
66
67 if notes.is_empty() && !has_additional {
68 return String::new();
69 }
70
71 let mut children = Vec::new();
72
73 if let Some(note) = add_info.and_then(|a| a.tax_authority_note.as_ref()) {
75 children.push(tag("infAdFisco", &[], TagContent::Text(note)));
76 }
77
78 if let Some(tn) = add_info.and_then(|a| a.taxpayer_note.as_ref()) {
80 notes.push(tn.to_string());
81 }
82 if !notes.is_empty() {
83 children.push(tag("infCpl", &[], TagContent::Text(¬es.join("; "))));
84 }
85
86 if let Some(obs_list) = add_info.and_then(|a| a.contributor_obs.as_ref()) {
88 for obs in obs_list.iter().take(10) {
89 children.push(tag(
90 "obsCont",
91 &[("xCampo", &obs.field)],
92 TagContent::Children(vec![tag("xTexto", &[], TagContent::Text(&obs.text))]),
93 ));
94 }
95 }
96
97 if let Some(obs_list) = add_info.and_then(|a| a.fiscal_obs.as_ref()) {
99 for obs in obs_list.iter().take(10) {
100 children.push(tag(
101 "obsFisco",
102 &[("xCampo", &obs.field)],
103 TagContent::Children(vec![tag("xTexto", &[], TagContent::Text(&obs.text))]),
104 ));
105 }
106 }
107
108 if let Some(procs) = add_info.and_then(|a| a.process_refs.as_ref()) {
110 for p in procs.iter().take(100) {
111 let mut proc_children = vec![
112 tag("nProc", &[], TagContent::Text(&p.number)),
113 tag("indProc", &[], TagContent::Text(&p.origin)),
114 ];
115 if let Some(tp_ato) = &p.tp_ato {
116 proc_children.push(tag("tpAto", &[], TagContent::Text(tp_ato)));
117 }
118 children.push(tag("procRef", &[], TagContent::Children(proc_children)));
119 }
120 }
121
122 tag("infAdic", &[], TagContent::Children(children))
123}
124
125pub fn build_intermediary(intermed: &IntermediaryData) -> String {
127 let mut children = vec![tag("CNPJ", &[], TagContent::Text(&intermed.tax_id))];
128 if let Some(ref id) = intermed.id_cad_int_tran {
129 children.push(tag("idCadIntTran", &[], TagContent::Text(id)));
130 }
131 tag("infIntermed", &[], TagContent::Children(children))
132}
133
134pub fn build_tech_responsible(tech: &TechResponsibleData) -> String {
140 build_tech_responsible_with_key(tech, "")
141}
142
143pub fn build_tech_responsible_with_key(tech: &TechResponsibleData, access_key: &str) -> String {
149 let mut children = vec![
150 tag("CNPJ", &[], TagContent::Text(&tech.tax_id)),
151 tag("xContato", &[], TagContent::Text(&tech.contact)),
152 tag("email", &[], TagContent::Text(&tech.email)),
153 ];
154 if let Some(ref phone) = tech.phone {
155 children.push(tag("fone", &[], TagContent::Text(phone)));
156 }
157 if let (Some(csrt), Some(csrt_id)) = (&tech.csrt, &tech.csrt_id) {
158 if !access_key.is_empty() {
159 children.push(tag("idCSRT", &[], TagContent::Text(csrt_id)));
160 let hash = compute_hash_csrt(csrt, access_key);
161 children.push(tag("hashCSRT", &[], TagContent::Text(&hash)));
162 }
163 }
164 tag("infRespTec", &[], TagContent::Children(children))
165}
166
167fn compute_hash_csrt(csrt: &str, access_key: &str) -> String {
172 use sha1::{Digest, Sha1};
173 let combined = format!("{csrt}{access_key}");
174 let mut hasher = Sha1::new();
175 hasher.update(combined.as_bytes());
176 let raw_hash = hasher.finalize();
177 base64::engine::general_purpose::STANDARD.encode(raw_hash)
178}
179
180pub fn build_purchase(purchase: &PurchaseData) -> String {
182 let mut children = Vec::new();
183 if let Some(ref note) = purchase.purchase_note {
184 children.push(tag("xNEmp", &[], TagContent::Text(note)));
185 }
186 if let Some(ref order) = purchase.order_number {
187 children.push(tag("xPed", &[], TagContent::Text(order)));
188 }
189 if let Some(ref contract) = purchase.contract_number {
190 children.push(tag("xCont", &[], TagContent::Text(contract)));
191 }
192 tag("compra", &[], TagContent::Children(children))
193}
194
195pub fn build_export(exp: &ExportData) -> String {
197 let mut children = vec![
198 tag("UFSaidaPais", &[], TagContent::Text(&exp.exit_state)),
199 tag("xLocExporta", &[], TagContent::Text(&exp.export_location)),
200 ];
201 if let Some(ref dispatch) = exp.dispatch_location {
202 children.push(tag("xLocDespacho", &[], TagContent::Text(dispatch)));
203 }
204 tag("exporta", &[], TagContent::Children(children))
205}
206
207pub fn build_withdrawal(w: &LocationData) -> String {
213 tag(
214 "retirada",
215 &[],
216 TagContent::Children(build_location_children(w)),
217 )
218}
219
220pub fn build_delivery(d: &LocationData) -> String {
226 tag(
227 "entrega",
228 &[],
229 TagContent::Children(build_location_children(d)),
230 )
231}
232
233fn build_location_children(loc: &LocationData) -> Vec<String> {
236 let tid = TaxId::new(&loc.tax_id);
237 let padded = tid.padded();
238 let mut children = vec![tag(tid.tag_name(), &[], TagContent::Text(&padded))];
239 if let Some(ref name) = loc.name {
240 children.push(tag("xNome", &[], TagContent::Text(name)));
241 }
242 children.push(tag("xLgr", &[], TagContent::Text(&loc.street)));
243 children.push(tag("nro", &[], TagContent::Text(&loc.number)));
244 if let Some(ref cpl) = loc.complement {
245 children.push(tag("xCpl", &[], TagContent::Text(cpl)));
246 }
247 children.push(tag("xBairro", &[], TagContent::Text(&loc.district)));
248 children.push(tag("cMun", &[], TagContent::Text(&loc.city_code.0)));
249 children.push(tag("xMun", &[], TagContent::Text(&loc.city_name)));
250 children.push(tag("UF", &[], TagContent::Text(&loc.state_code)));
251 if let Some(ref cep) = loc.zip_code {
252 children.push(tag("CEP", &[], TagContent::Text(cep)));
253 }
254 if let Some(ref c_pais) = loc.c_pais {
255 children.push(tag("cPais", &[], TagContent::Text(c_pais)));
256 }
257 if let Some(ref x_pais) = loc.x_pais {
258 children.push(tag("xPais", &[], TagContent::Text(x_pais)));
259 }
260 if let Some(ref fone) = loc.fone {
261 children.push(tag("fone", &[], TagContent::Text(fone)));
262 }
263 if let Some(ref email) = loc.email {
264 children.push(tag("email", &[], TagContent::Text(email)));
265 }
266 if let Some(ref ie) = loc.ie {
267 children.push(tag("IE", &[], TagContent::Text(ie)));
268 }
269 children
270}
271
272pub fn build_cana(cana: &CanaData) -> String {
290 let fc2 = |c: i64| format_cents(c, 2);
291 let fc10 = |c: i64| format_cents(c, 10);
292
293 let mut children = vec![
294 tag("safra", &[], TagContent::Text(&cana.safra)),
295 tag("ref", &[], TagContent::Text(&cana.referencia)),
296 ];
297
298 for fd in cana.for_dia.iter().take(31) {
300 let dia_str = fd.dia.to_string();
301 children.push(tag(
302 "forDia",
303 &[("dia", &dia_str)],
304 TagContent::Children(vec![tag("qtde", &[], TagContent::Text(&fc10(fd.qtde.0)))]),
305 ));
306 }
307
308 children.push(tag(
309 "qTotMes",
310 &[],
311 TagContent::Text(&fc10(cana.q_tot_mes.0)),
312 ));
313 children.push(tag(
314 "qTotAnt",
315 &[],
316 TagContent::Text(&fc10(cana.q_tot_ant.0)),
317 ));
318 children.push(tag(
319 "qTotGer",
320 &[],
321 TagContent::Text(&fc10(cana.q_tot_ger.0)),
322 ));
323
324 if let Some(ref deducs) = cana.deducoes {
326 for d in deducs.iter().take(10) {
327 children.push(tag(
328 "deduc",
329 &[],
330 TagContent::Children(vec![
331 tag("xDed", &[], TagContent::Text(&d.x_ded)),
332 tag("vDed", &[], TagContent::Text(&fc2(d.v_ded.0))),
333 ]),
334 ));
335 }
336 }
337
338 children.push(tag("vFor", &[], TagContent::Text(&fc2(cana.v_for.0))));
339 children.push(tag(
340 "vTotDed",
341 &[],
342 TagContent::Text(&fc2(cana.v_tot_ded.0)),
343 ));
344 children.push(tag(
345 "vLiqFor",
346 &[],
347 TagContent::Text(&fc2(cana.v_liq_for.0)),
348 ));
349
350 tag("cana", &[], TagContent::Children(children))
351}
352
353pub fn build_agropecuario(data: &AgropecuarioData) -> String {
358 let children = match data {
359 AgropecuarioData::Guia(guia) => {
360 let mut kids = vec![tag("tpGuia", &[], TagContent::Text(&guia.tp_guia))];
361 if let Some(ref uf) = guia.uf_guia {
362 kids.push(tag("UFGuia", &[], TagContent::Text(uf)));
363 }
364 if let Some(ref serie) = guia.serie_guia {
365 kids.push(tag("serieGuia", &[], TagContent::Text(serie)));
366 }
367 kids.push(tag("nGuia", &[], TagContent::Text(&guia.n_guia)));
368 vec![tag("guiaTransito", &[], TagContent::Children(kids))]
369 }
370 AgropecuarioData::Defensivos(defs) => defs
371 .iter()
372 .take(20)
373 .map(|d| {
374 tag(
375 "defensivo",
376 &[],
377 TagContent::Children(vec![
378 tag("nReceituario", &[], TagContent::Text(&d.n_receituario)),
379 tag("CPFRespTec", &[], TagContent::Text(&d.cpf_resp_tec)),
380 ]),
381 )
382 })
383 .collect(),
384 };
385
386 tag("agropecuario", &[], TagContent::Children(children))
387}
388
389pub fn build_compra_gov(data: &CompraGovData) -> String {
393 tag(
394 "gCompraGov",
395 &[],
396 TagContent::Children(vec![
397 tag("tpEnteGov", &[], TagContent::Text(&data.tp_ente_gov)),
398 tag("pRedutor", &[], TagContent::Text(&data.p_redutor)),
399 tag("tpOperGov", &[], TagContent::Text(&data.tp_oper_gov)),
400 ]),
401 )
402}
403
404pub fn build_pag_antecipado(data: &PagAntecipadoData) -> String {
408 let children: Vec<String> = data
409 .ref_nfe
410 .iter()
411 .map(|key| tag("refNFe", &[], TagContent::Text(key)))
412 .collect();
413 tag("gPagAntecipado", &[], TagContent::Children(children))
414}
415
416pub fn build_aut_xml(entry: &AuthorizedXml) -> String {
418 let tid = TaxId::new(&entry.tax_id);
419 let padded = tid.padded();
420 tag(
421 "autXML",
422 &[],
423 TagContent::Children(vec![tag(tid.tag_name(), &[], TagContent::Text(&padded))]),
424 )
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430 use crate::newtypes::Cents;
431 use crate::xml_builder::IbgeCode;
432
433 #[test]
434 fn build_cana_minimal_without_deducoes() {
435 let cana = CanaData::new(
436 "2025/2026",
437 "03/2026",
438 vec![
439 ForDiaData::new(1, Cents(1000000)),
440 ForDiaData::new(2, Cents(1500000)),
441 ],
442 Cents(2500000), Cents(5000000), Cents(7500000), Cents(150000), Cents(0), Cents(150000), );
449
450 let xml = build_cana(&cana);
451
452 assert_eq!(
453 xml,
454 "<cana>\
455 <safra>2025/2026</safra>\
456 <ref>03/2026</ref>\
457 <forDia dia=\"1\"><qtde>10000.0000000000</qtde></forDia>\
458 <forDia dia=\"2\"><qtde>15000.0000000000</qtde></forDia>\
459 <qTotMes>25000.0000000000</qTotMes>\
460 <qTotAnt>50000.0000000000</qTotAnt>\
461 <qTotGer>75000.0000000000</qTotGer>\
462 <vFor>1500.00</vFor>\
463 <vTotDed>0.00</vTotDed>\
464 <vLiqFor>1500.00</vLiqFor>\
465 </cana>"
466 );
467 }
468
469 #[test]
470 fn build_cana_with_deducoes() {
471 let cana = CanaData::new(
472 "2024/2025",
473 "06/2025",
474 vec![ForDiaData::new(15, Cents(2000000))],
475 Cents(2000000), Cents(10000000), Cents(12000000), Cents(500000), Cents(50000), Cents(450000), )
482 .deducoes(vec![
483 DeducData::new("TAXA PRODUCAO", Cents(30000)),
484 DeducData::new("FUNRURAL", Cents(20000)),
485 ]);
486
487 let xml = build_cana(&cana);
488
489 assert!(xml.contains("<safra>2024/2025</safra>"));
490 assert!(xml.contains("<ref>06/2025</ref>"));
491 assert!(xml.contains("<forDia dia=\"15\"><qtde>20000.0000000000</qtde></forDia>"));
492 assert!(xml.contains("<qTotMes>20000.0000000000</qTotMes>"));
493 assert!(xml.contains("<qTotAnt>100000.0000000000</qTotAnt>"));
494 assert!(xml.contains("<qTotGer>120000.0000000000</qTotGer>"));
495 assert!(xml.contains("<deduc><xDed>TAXA PRODUCAO</xDed><vDed>300.00</vDed></deduc>"));
496 assert!(xml.contains("<deduc><xDed>FUNRURAL</xDed><vDed>200.00</vDed></deduc>"));
497 assert!(xml.contains("<vFor>5000.00</vFor>"));
498 assert!(xml.contains("<vTotDed>500.00</vTotDed>"));
499 assert!(xml.contains("<vLiqFor>4500.00</vLiqFor>"));
500
501 let deduc_pos = xml.find("<deduc>").expect("deduc must be present");
503 let vfor_pos = xml.find("<vFor>").expect("vFor must be present");
504 assert!(
505 deduc_pos < vfor_pos,
506 "deduc must come before vFor in the XML"
507 );
508
509 let fordia_pos = xml.find("<forDia").expect("forDia must be present");
511 let qtotmes_pos = xml.find("<qTotMes>").expect("qTotMes must be present");
512 assert!(
513 fordia_pos < qtotmes_pos,
514 "forDia must come before qTotMes in the XML"
515 );
516 }
517
518 #[test]
519 fn build_cana_limits_fordia_to_31() {
520 let mut for_dia = Vec::new();
521 for day in 1..=35 {
522 for_dia.push(ForDiaData::new(day, Cents(100000)));
523 }
524
525 let cana = CanaData::new(
526 "2025/2026",
527 "01/2026",
528 for_dia,
529 Cents(0),
530 Cents(0),
531 Cents(0),
532 Cents(0),
533 Cents(0),
534 Cents(0),
535 );
536
537 let xml = build_cana(&cana);
538
539 let count = xml.matches("<forDia").count();
541 assert_eq!(count, 31, "forDia entries must be capped at 31");
542 }
543
544 #[test]
545 fn build_cana_limits_deduc_to_10() {
546 let mut deducs = Vec::new();
547 for i in 1..=15 {
548 deducs.push(DeducData::new(format!("DEDUC {i}"), Cents(1000)));
549 }
550
551 let cana = CanaData::new(
552 "2025/2026",
553 "01/2026",
554 vec![ForDiaData::new(1, Cents(100000))],
555 Cents(0),
556 Cents(0),
557 Cents(0),
558 Cents(0),
559 Cents(0),
560 Cents(0),
561 )
562 .deducoes(deducs);
563
564 let xml = build_cana(&cana);
565
566 let count = xml.matches("<deduc>").count();
568 assert_eq!(count, 10, "deduc entries must be capped at 10");
569 }
570
571 #[test]
574 fn build_agropecuario_guia() {
575 let guia = AgropecuarioGuiaData::new("1", "12345")
576 .uf_guia("SP")
577 .serie_guia("A");
578 let data = AgropecuarioData::Guia(guia);
579 let xml = build_agropecuario(&data);
580
581 assert_eq!(
582 xml,
583 "<agropecuario>\
584 <guiaTransito>\
585 <tpGuia>1</tpGuia>\
586 <UFGuia>SP</UFGuia>\
587 <serieGuia>A</serieGuia>\
588 <nGuia>12345</nGuia>\
589 </guiaTransito>\
590 </agropecuario>"
591 );
592 }
593
594 #[test]
595 fn build_agropecuario_guia_minimal() {
596 let guia = AgropecuarioGuiaData::new("2", "99999");
597 let data = AgropecuarioData::Guia(guia);
598 let xml = build_agropecuario(&data);
599
600 assert_eq!(
601 xml,
602 "<agropecuario>\
603 <guiaTransito>\
604 <tpGuia>2</tpGuia>\
605 <nGuia>99999</nGuia>\
606 </guiaTransito>\
607 </agropecuario>"
608 );
609 }
610
611 #[test]
612 fn build_agropecuario_defensivos() {
613 let defs = vec![
614 AgropecuarioDefensivoData::new("REC001", "12345678901"),
615 AgropecuarioDefensivoData::new("REC002", "98765432109"),
616 ];
617 let data = AgropecuarioData::Defensivos(defs);
618 let xml = build_agropecuario(&data);
619
620 assert_eq!(
621 xml,
622 "<agropecuario>\
623 <defensivo>\
624 <nReceituario>REC001</nReceituario>\
625 <CPFRespTec>12345678901</CPFRespTec>\
626 </defensivo>\
627 <defensivo>\
628 <nReceituario>REC002</nReceituario>\
629 <CPFRespTec>98765432109</CPFRespTec>\
630 </defensivo>\
631 </agropecuario>"
632 );
633 }
634
635 #[test]
636 fn build_agropecuario_defensivos_capped_at_20() {
637 let defs: Vec<AgropecuarioDefensivoData> = (0..25)
638 .map(|i| AgropecuarioDefensivoData::new(format!("REC{i:03}"), "12345678901"))
639 .collect();
640 let data = AgropecuarioData::Defensivos(defs);
641 let xml = build_agropecuario(&data);
642
643 let count = xml.matches("<defensivo>").count();
644 assert_eq!(count, 20, "defensivo entries must be capped at 20");
645 }
646
647 #[test]
650 fn build_compra_gov_all_fields() {
651 let cg = CompraGovData::new("1", "10.5000", "2");
652 let xml = build_compra_gov(&cg);
653
654 assert_eq!(
655 xml,
656 "<gCompraGov>\
657 <tpEnteGov>1</tpEnteGov>\
658 <pRedutor>10.5000</pRedutor>\
659 <tpOperGov>2</tpOperGov>\
660 </gCompraGov>"
661 );
662 }
663
664 #[test]
667 fn build_pag_antecipado_single_ref() {
668 let pa = PagAntecipadoData::new(vec![
669 "41260304123456000190550010000001231123456780".to_string(),
670 ]);
671 let xml = build_pag_antecipado(&pa);
672
673 assert_eq!(
674 xml,
675 "<gPagAntecipado>\
676 <refNFe>41260304123456000190550010000001231123456780</refNFe>\
677 </gPagAntecipado>"
678 );
679 }
680
681 #[test]
682 fn build_pag_antecipado_multiple_refs() {
683 let pa = PagAntecipadoData::new(vec![
684 "41260304123456000190550010000001231123456780".to_string(),
685 "41260304123456000190550010000001241123456781".to_string(),
686 ]);
687 let xml = build_pag_antecipado(&pa);
688
689 assert!(xml.contains("<gPagAntecipado>"));
690 assert_eq!(xml.matches("<refNFe>").count(), 2);
691 }
692
693 #[test]
696 fn build_withdrawal_all_fields_cnpj() {
697 let loc = LocationData::new(
698 "12345678000199",
699 "Rua das Flores",
700 "100",
701 "Centro",
702 IbgeCode("3550308".to_string()),
703 "São Paulo",
704 "SP",
705 )
706 .name("Empresa Teste")
707 .complement("Sala 5")
708 .zip_code("01001000")
709 .c_pais("1058")
710 .x_pais("Brasil")
711 .fone("1155551234")
712 .email("teste@empresa.com")
713 .ie("123456789");
714
715 let xml = build_withdrawal(&loc);
716
717 assert_eq!(
718 xml,
719 "<retirada>\
720 <CNPJ>12345678000199</CNPJ>\
721 <xNome>Empresa Teste</xNome>\
722 <xLgr>Rua das Flores</xLgr>\
723 <nro>100</nro>\
724 <xCpl>Sala 5</xCpl>\
725 <xBairro>Centro</xBairro>\
726 <cMun>3550308</cMun>\
727 <xMun>São Paulo</xMun>\
728 <UF>SP</UF>\
729 <CEP>01001000</CEP>\
730 <cPais>1058</cPais>\
731 <xPais>Brasil</xPais>\
732 <fone>1155551234</fone>\
733 <email>teste@empresa.com</email>\
734 <IE>123456789</IE>\
735 </retirada>"
736 );
737 }
738
739 #[test]
740 fn build_delivery_all_fields_cpf() {
741 let loc = LocationData::new(
742 "12345678901",
743 "Av. Brasil",
744 "200",
745 "Bela Vista",
746 IbgeCode("3304557".to_string()),
747 "Rio de Janeiro",
748 "RJ",
749 )
750 .name("João da Silva")
751 .complement("Apto 301")
752 .zip_code("20040020")
753 .c_pais("1058")
754 .x_pais("Brasil")
755 .fone("2199998888")
756 .email("joao@email.com")
757 .ie("ISENTO");
758
759 let xml = build_delivery(&loc);
760
761 assert_eq!(
762 xml,
763 "<entrega>\
764 <CPF>12345678901</CPF>\
765 <xNome>João da Silva</xNome>\
766 <xLgr>Av. Brasil</xLgr>\
767 <nro>200</nro>\
768 <xCpl>Apto 301</xCpl>\
769 <xBairro>Bela Vista</xBairro>\
770 <cMun>3304557</cMun>\
771 <xMun>Rio de Janeiro</xMun>\
772 <UF>RJ</UF>\
773 <CEP>20040020</CEP>\
774 <cPais>1058</cPais>\
775 <xPais>Brasil</xPais>\
776 <fone>2199998888</fone>\
777 <email>joao@email.com</email>\
778 <IE>ISENTO</IE>\
779 </entrega>"
780 );
781 }
782
783 #[test]
784 fn build_withdrawal_minimal_only_required() {
785 let loc = LocationData::new(
786 "12345678000199",
787 "Rua A",
788 "1",
789 "Centro",
790 IbgeCode("3550308".to_string()),
791 "São Paulo",
792 "SP",
793 );
794
795 let xml = build_withdrawal(&loc);
796
797 assert_eq!(
798 xml,
799 "<retirada>\
800 <CNPJ>12345678000199</CNPJ>\
801 <xLgr>Rua A</xLgr>\
802 <nro>1</nro>\
803 <xBairro>Centro</xBairro>\
804 <cMun>3550308</cMun>\
805 <xMun>São Paulo</xMun>\
806 <UF>SP</UF>\
807 </retirada>"
808 );
809 }
810
811 #[test]
812 fn build_delivery_partial_new_fields() {
813 let loc = LocationData::new(
814 "98765432000111",
815 "Rua B",
816 "50",
817 "Jardim",
818 IbgeCode("4106902".to_string()),
819 "Curitiba",
820 "PR",
821 )
822 .fone("4133334444")
823 .email("contato@loja.com");
824
825 let xml = build_delivery(&loc);
826
827 assert_eq!(
829 xml,
830 "<entrega>\
831 <CNPJ>98765432000111</CNPJ>\
832 <xLgr>Rua B</xLgr>\
833 <nro>50</nro>\
834 <xBairro>Jardim</xBairro>\
835 <cMun>4106902</cMun>\
836 <xMun>Curitiba</xMun>\
837 <UF>PR</UF>\
838 <fone>4133334444</fone>\
839 <email>contato@loja.com</email>\
840 </entrega>"
841 );
842 }
843
844 #[test]
845 fn build_withdrawal_tag_order_matches_php() {
846 let loc = LocationData::new(
848 "12345678000199",
849 "Rua X",
850 "10",
851 "Bairro Y",
852 IbgeCode("1100015".to_string()),
853 "Porto Velho",
854 "RO",
855 )
856 .name("Empresa")
857 .complement("Loja 1")
858 .zip_code("76801000")
859 .c_pais("1058")
860 .x_pais("Brasil")
861 .fone("6932221111")
862 .email("a@b.com")
863 .ie("1234");
864
865 let xml = build_withdrawal(&loc);
866
867 let tags_in_order = [
869 "<CNPJ>",
870 "<xNome>",
871 "<xLgr>",
872 "<nro>",
873 "<xCpl>",
874 "<xBairro>",
875 "<cMun>",
876 "<xMun>",
877 "<UF>",
878 "<CEP>",
879 "<cPais>",
880 "<xPais>",
881 "<fone>",
882 "<email>",
883 "<IE>",
884 ];
885 let mut last_pos = 0;
886 for tag_name in &tags_in_order {
887 let pos = xml
888 .find(tag_name)
889 .unwrap_or_else(|| panic!("tag {tag_name} não encontrada no XML"));
890 assert!(
891 pos >= last_pos,
892 "tag {tag_name} está fora de ordem (pos {pos} < {last_pos})"
893 );
894 last_pos = pos;
895 }
896 }
897}