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    // PHP does NOT auto-add homologation note to infAdic — removed to match PHP
58
59    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    // infAdFisco before infCpl per schema
75    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    // Merge contingency/env notes with taxpayer note
80    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(&notes.join("; "))));
85    }
86
87    // obsCont (max 10)
88    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    // obsFisco (max 10)
99    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    // procRef (max 100)
110    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
126/// Build `<infIntermed>` element.
127pub 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
135/// Build `<infRespTec>` element.
136///
137/// When both `csrt` and `csrt_id` are present, generates `<idCSRT>` and
138/// `<hashCSRT>` tags. The hash follows the PHP sped-nfe algorithm:
139/// `base64(sha1(CSRT + chNFe, raw_binary))`.
140pub fn build_tech_responsible(tech: &TechResponsibleData) -> String {
141    build_tech_responsible_with_key(tech, "")
142}
143
144/// Build `<infRespTec>` element with access key for CSRT hash computation.
145///
146/// When both `csrt` and `csrt_id` are present on `tech`, generates `<idCSRT>` and
147/// `<hashCSRT>` tags. The hash follows the PHP sped-nfe algorithm:
148/// `base64(sha1(CSRT + chNFe, raw_binary))`.
149pub 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
168/// Compute hashCSRT as defined by the SEFAZ specification.
169///
170/// Algorithm: `base64(sha1(CSRT + chNFe))` — matching PHP's
171/// `base64_encode(sha1($CSRT . $this->chNFe, true))`.
172fn 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
181/// Build `<compra>` (purchase) element.
182pub 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
196/// Build `<exporta>` element.
197pub 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
208/// Build `<retirada>` (withdrawal) element.
209pub 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
231/// Build `<entrega>` (delivery) element.
232pub 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
254/// Build `<cana>` (sugarcane supply) element.
255///
256/// XML schema order (Grupo ZC01):
257/// ```xml
258/// <cana>
259///   <safra>...</safra>
260///   <ref>...</ref>
261///   <forDia dia="N"><qtde>...</qtde></forDia>  <!-- up to 31 -->
262///   <qTotMes>...</qTotMes>
263///   <qTotAnt>...</qTotAnt>
264///   <qTotGer>...</qTotGer>
265///   <deduc><xDed>...</xDed><vDed>...</vDed></deduc>  <!-- up to 10 -->
266///   <vFor>...</vFor>
267///   <vTotDed>...</vTotDed>
268///   <vLiqFor>...</vLiqFor>
269/// </cana>
270/// ```
271pub 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    // forDia entries (up to 31, one per day)
281    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    // deduc entries (up to 10)
307    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
335/// Build `<agropecuario>` element (Grupo ZF01).
336///
337/// Contains either a `<guiaTransito>` or up to 20 `<defensivo>` entries.
338/// Positioned inside `<infNFe>` after `<infRespTec>`.
339pub 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
371/// Build `<gCompraGov>` element (Grupo B31, PL_010+).
372///
373/// Placed inside `<ide>` after `<NFref>` elements.
374pub 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
386/// Build `<gPagAntecipado>` element (Grupo B34, PL_010+).
387///
388/// Placed inside `<ide>` after `<gCompraGov>`.
389pub 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
398/// Build `<autXML>` element.
399pub 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), // qTotMes
424            Cents(5000000), // qTotAnt
425            Cents(7500000), // qTotGer
426            Cents(150000),  // vFor = 1500.00
427            Cents(0),       // vTotDed = 0.00
428            Cents(150000),  // vLiqFor = 1500.00
429        );
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),  // qTotMes
457            Cents(10000000), // qTotAnt
458            Cents(12000000), // qTotGer
459            Cents(500000),   // vFor = 5000.00
460            Cents(50000),    // vTotDed = 500.00
461            Cents(450000),   // vLiqFor = 4500.00
462        )
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        // Verify order: deduc comes before vFor
483        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        // Verify order: forDia comes before qTotMes
491        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        // Count forDia occurrences — should be capped at 31
521        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        // Count deduc occurrences — should be capped at 10
548        let count = xml.matches("<deduc>").count();
549        assert_eq!(count, 10, "deduc entries must be capped at 10");
550    }
551
552    // ── Agropecuário tests ──────────────────────────────────────────────
553
554    #[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    // ── Compra Governamental tests ──────────────────────────────────────
629
630    #[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    // ── Pagamento Antecipado tests ──────────────────────────────────────
646
647    #[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}