1use std::marker::PhantomData;
20
21use chrono::{DateTime, FixedOffset};
22
23use crate::FiscalError;
24use crate::newtypes::Cents;
25use crate::types::*;
26
27pub struct Draft;
31
32pub struct Built;
34
35pub struct Signed;
37
38pub struct InvoiceBuilder<State = Draft> {
49 issuer: IssuerData,
51 environment: SefazEnvironment,
52 model: InvoiceModel,
53
54 series: u32,
56 invoice_number: u32,
57 emission_type: EmissionType,
58 issued_at: DateTime<FixedOffset>,
59 operation_nature: String,
60
61 items: Vec<InvoiceItemData>,
63 recipient: Option<RecipientData>,
64 payments: Vec<PaymentData>,
65 change_amount: Option<Cents>,
66 payment_card_details: Option<Vec<PaymentCardDetail>>,
67 contingency: Option<ContingencyData>,
68 exit_at: Option<DateTime<FixedOffset>>,
69
70 operation_type: Option<u8>,
72 purpose_code: Option<u8>,
73 destination_indicator: Option<String>,
74 intermediary_indicator: Option<String>,
75 emission_process: Option<String>,
76 consumer_type: Option<String>,
77 buyer_presence: Option<String>,
78 print_format: Option<String>,
79 ver_proc: Option<String>,
80 references: Option<Vec<ReferenceDoc>>,
81
82 transport: Option<TransportData>,
84 billing: Option<BillingData>,
85 withdrawal: Option<LocationData>,
86 delivery: Option<LocationData>,
87 authorized_xml: Option<Vec<AuthorizedXml>>,
88 additional_info: Option<AdditionalInfo>,
89 intermediary: Option<IntermediaryData>,
90 ret_trib: Option<RetTribData>,
91 tech_responsible: Option<TechResponsibleData>,
92 purchase: Option<PurchaseData>,
93 export: Option<ExportData>,
94 issqn_tot: Option<IssqnTotData>,
95 cana: Option<CanaData>,
96 agropecuario: Option<AgropecuarioData>,
97 compra_gov: Option<CompraGovData>,
98 pag_antecipado: Option<PagAntecipadoData>,
99 is_tot: Option<crate::tax_ibs_cbs::IsTotData>,
100 ibs_cbs_tot: Option<crate::tax_ibs_cbs::IbsCbsTotData>,
101
102 result_xml: Option<String>,
104 result_access_key: Option<String>,
105
106 result_signed_xml: Option<String>,
108
109 _state: PhantomData<State>,
110}
111
112impl InvoiceBuilder<Draft> {
115 pub fn new(issuer: IssuerData, environment: SefazEnvironment, model: InvoiceModel) -> Self {
120 let now = chrono::Utc::now()
121 .with_timezone(&FixedOffset::west_opt(3 * 3600).expect("valid offset"));
122
123 Self {
124 issuer,
125 environment,
126 model,
127 series: 1,
128 invoice_number: 1,
129 emission_type: EmissionType::Normal,
130 issued_at: now,
131 operation_nature: "VENDA".to_string(),
132 items: Vec::new(),
133 recipient: None,
134 payments: Vec::new(),
135 change_amount: None,
136 payment_card_details: None,
137 contingency: None,
138 exit_at: None,
139 operation_type: None,
140 purpose_code: None,
141 destination_indicator: None,
142 intermediary_indicator: None,
143 emission_process: None,
144 consumer_type: None,
145 buyer_presence: None,
146 print_format: None,
147 ver_proc: None,
148 references: None,
149 transport: None,
150 billing: None,
151 withdrawal: None,
152 delivery: None,
153 authorized_xml: None,
154 additional_info: None,
155 intermediary: None,
156 ret_trib: None,
157 tech_responsible: None,
158 purchase: None,
159 export: None,
160 issqn_tot: None,
161 cana: None,
162 agropecuario: None,
163 compra_gov: None,
164 pag_antecipado: None,
165 is_tot: None,
166 ibs_cbs_tot: None,
167 result_xml: None,
168 result_access_key: None,
169 result_signed_xml: None,
170 _state: PhantomData,
171 }
172 }
173
174 pub fn series(mut self, s: u32) -> Self {
178 self.series = s;
179 self
180 }
181
182 pub fn invoice_number(mut self, n: u32) -> Self {
184 self.invoice_number = n;
185 self
186 }
187
188 pub fn emission_type(mut self, et: EmissionType) -> Self {
190 self.emission_type = et;
191 self
192 }
193
194 pub fn issued_at(mut self, dt: DateTime<FixedOffset>) -> Self {
196 self.issued_at = dt;
197 self
198 }
199
200 pub fn operation_nature(mut self, n: impl Into<String>) -> Self {
202 self.operation_nature = n.into();
203 self
204 }
205
206 pub fn add_item(mut self, item: InvoiceItemData) -> Self {
208 self.items.push(item);
209 self
210 }
211
212 pub fn items(mut self, items: Vec<InvoiceItemData>) -> Self {
214 self.items = items;
215 self
216 }
217
218 pub fn recipient(mut self, r: RecipientData) -> Self {
220 self.recipient = Some(r);
221 self
222 }
223
224 pub fn payments(mut self, p: Vec<PaymentData>) -> Self {
226 self.payments = p;
227 self
228 }
229
230 pub fn change_amount(mut self, c: Cents) -> Self {
232 self.change_amount = Some(c);
233 self
234 }
235
236 pub fn payment_card_details(mut self, d: Vec<PaymentCardDetail>) -> Self {
238 self.payment_card_details = Some(d);
239 self
240 }
241
242 pub fn contingency(mut self, c: ContingencyData) -> Self {
244 self.contingency = Some(c);
245 self
246 }
247
248 pub fn exit_at(mut self, dt: DateTime<FixedOffset>) -> Self {
250 self.exit_at = Some(dt);
251 self
252 }
253
254 pub fn operation_type(mut self, v: u8) -> Self {
256 self.operation_type = Some(v);
257 self
258 }
259
260 pub fn purpose_code(mut self, v: u8) -> Self {
262 self.purpose_code = Some(v);
263 self
264 }
265
266 pub fn intermediary_indicator(mut self, v: impl Into<String>) -> Self {
268 self.intermediary_indicator = Some(v.into());
269 self
270 }
271
272 pub fn emission_process(mut self, v: impl Into<String>) -> Self {
274 self.emission_process = Some(v.into());
275 self
276 }
277
278 pub fn consumer_type(mut self, v: impl Into<String>) -> Self {
280 self.consumer_type = Some(v.into());
281 self
282 }
283
284 pub fn buyer_presence(mut self, v: impl Into<String>) -> Self {
286 self.buyer_presence = Some(v.into());
287 self
288 }
289
290 pub fn print_format(mut self, v: impl Into<String>) -> Self {
292 self.print_format = Some(v.into());
293 self
294 }
295
296 pub fn destination_indicator(mut self, v: impl Into<String>) -> Self {
298 self.destination_indicator = Some(v.into());
299 self
300 }
301
302 pub fn ver_proc(mut self, v: impl Into<String>) -> Self {
304 self.ver_proc = Some(v.into());
305 self
306 }
307
308 pub fn references(mut self, refs: Vec<ReferenceDoc>) -> Self {
310 self.references = Some(refs);
311 self
312 }
313
314 pub fn transport(mut self, t: TransportData) -> Self {
316 self.transport = Some(t);
317 self
318 }
319
320 pub fn billing(mut self, b: BillingData) -> Self {
322 self.billing = Some(b);
323 self
324 }
325
326 pub fn withdrawal(mut self, w: LocationData) -> Self {
328 self.withdrawal = Some(w);
329 self
330 }
331
332 pub fn delivery(mut self, d: LocationData) -> Self {
334 self.delivery = Some(d);
335 self
336 }
337
338 pub fn authorized_xml(mut self, a: Vec<AuthorizedXml>) -> Self {
340 self.authorized_xml = Some(a);
341 self
342 }
343
344 pub fn additional_info(mut self, a: AdditionalInfo) -> Self {
346 self.additional_info = Some(a);
347 self
348 }
349
350 pub fn intermediary(mut self, i: IntermediaryData) -> Self {
352 self.intermediary = Some(i);
353 self
354 }
355
356 pub fn ret_trib(mut self, r: RetTribData) -> Self {
358 self.ret_trib = Some(r);
359 self
360 }
361
362 pub fn tech_responsible(mut self, t: TechResponsibleData) -> Self {
364 self.tech_responsible = Some(t);
365 self
366 }
367
368 pub fn purchase(mut self, p: PurchaseData) -> Self {
370 self.purchase = Some(p);
371 self
372 }
373
374 pub fn export(mut self, e: ExportData) -> Self {
376 self.export = Some(e);
377 self
378 }
379
380 pub fn issqn_tot(mut self, t: IssqnTotData) -> Self {
382 self.issqn_tot = Some(t);
383 self
384 }
385
386 pub fn cana(mut self, c: CanaData) -> Self {
388 self.cana = Some(c);
389 self
390 }
391
392 pub fn agropecuario(mut self, a: AgropecuarioData) -> Self {
394 self.agropecuario = Some(a);
395 self
396 }
397
398 pub fn compra_gov(mut self, c: CompraGovData) -> Self {
400 self.compra_gov = Some(c);
401 self
402 }
403
404 pub fn pag_antecipado(mut self, p: PagAntecipadoData) -> Self {
406 self.pag_antecipado = Some(p);
407 self
408 }
409
410 pub fn is_tot(mut self, t: crate::tax_ibs_cbs::IsTotData) -> Self {
412 self.is_tot = Some(t);
413 self
414 }
415
416 pub fn ibs_cbs_tot(mut self, t: crate::tax_ibs_cbs::IbsCbsTotData) -> Self {
418 self.ibs_cbs_tot = Some(t);
419 self
420 }
421
422 pub fn build(self) -> Result<InvoiceBuilder<Built>, FiscalError> {
430 let data = InvoiceBuildData {
431 model: self.model,
432 series: self.series,
433 number: self.invoice_number,
434 emission_type: self.emission_type,
435 environment: self.environment,
436 issued_at: self.issued_at,
437 operation_nature: self.operation_nature,
438 issuer: self.issuer,
439 recipient: self.recipient,
440 items: self.items,
441 payments: self.payments,
442 change_amount: self.change_amount,
443 payment_card_details: self.payment_card_details,
444 contingency: self.contingency,
445 exit_at: self.exit_at,
446 operation_type: self.operation_type,
447 purpose_code: self.purpose_code,
448 destination_indicator: self.destination_indicator,
449 intermediary_indicator: self.intermediary_indicator,
450 emission_process: self.emission_process,
451 consumer_type: self.consumer_type,
452 buyer_presence: self.buyer_presence,
453 print_format: self.print_format,
454 ver_proc: self.ver_proc,
455 references: self.references,
456 transport: self.transport,
457 billing: self.billing,
458 withdrawal: self.withdrawal,
459 delivery: self.delivery,
460 authorized_xml: self.authorized_xml,
461 additional_info: self.additional_info,
462 intermediary: self.intermediary,
463 ret_trib: self.ret_trib,
464 tech_responsible: self.tech_responsible,
465 purchase: self.purchase,
466 export: self.export,
467 issqn_tot: self.issqn_tot,
468 cana: self.cana,
469 agropecuario: self.agropecuario,
470 compra_gov: self.compra_gov,
471 pag_antecipado: self.pag_antecipado,
472 is_tot: self.is_tot,
473 ibs_cbs_tot: self.ibs_cbs_tot,
474 };
475
476 let result = super::generate_xml(&data)?;
477
478 Ok(InvoiceBuilder {
479 issuer: data.issuer,
480 environment: data.environment,
481 model: data.model,
482 series: data.series,
483 invoice_number: data.number,
484 emission_type: data.emission_type,
485 issued_at: data.issued_at,
486 operation_nature: data.operation_nature,
487 items: data.items,
488 recipient: data.recipient,
489 payments: data.payments,
490 change_amount: data.change_amount,
491 payment_card_details: data.payment_card_details,
492 contingency: data.contingency,
493 exit_at: data.exit_at,
494 operation_type: data.operation_type,
495 purpose_code: data.purpose_code,
496 destination_indicator: data.destination_indicator,
497 intermediary_indicator: data.intermediary_indicator,
498 emission_process: data.emission_process,
499 consumer_type: data.consumer_type,
500 buyer_presence: data.buyer_presence,
501 print_format: data.print_format,
502 ver_proc: data.ver_proc,
503 references: data.references,
504 transport: data.transport,
505 billing: data.billing,
506 withdrawal: data.withdrawal,
507 delivery: data.delivery,
508 authorized_xml: data.authorized_xml,
509 additional_info: data.additional_info,
510 intermediary: data.intermediary,
511 ret_trib: data.ret_trib,
512 tech_responsible: data.tech_responsible,
513 purchase: data.purchase,
514 export: data.export,
515 issqn_tot: data.issqn_tot,
516 cana: data.cana,
517 agropecuario: data.agropecuario,
518 compra_gov: data.compra_gov,
519 pag_antecipado: data.pag_antecipado,
520 is_tot: data.is_tot,
521 ibs_cbs_tot: data.ibs_cbs_tot,
522 result_xml: Some(result.xml),
523 result_access_key: Some(result.access_key),
524 result_signed_xml: None,
525 _state: PhantomData,
526 })
527 }
528}
529
530impl InvoiceBuilder<Built> {
533 pub fn xml(&self) -> &str {
535 self.result_xml
536 .as_deref()
537 .expect("Built state always has XML")
538 }
539
540 pub fn access_key(&self) -> &str {
542 self.result_access_key
543 .as_deref()
544 .expect("Built state always has access key")
545 }
546
547 pub fn sign_with<F>(self, signer: F) -> Result<InvoiceBuilder<Signed>, FiscalError>
573 where
574 F: FnOnce(&str) -> Result<String, FiscalError>,
575 {
576 let unsigned_xml = self
577 .result_xml
578 .as_deref()
579 .expect("Built state always has XML");
580
581 let signed_xml = signer(unsigned_xml)?;
582
583 Ok(InvoiceBuilder {
584 issuer: self.issuer,
585 environment: self.environment,
586 model: self.model,
587 series: self.series,
588 invoice_number: self.invoice_number,
589 emission_type: self.emission_type,
590 issued_at: self.issued_at,
591 operation_nature: self.operation_nature,
592 items: self.items,
593 recipient: self.recipient,
594 payments: self.payments,
595 change_amount: self.change_amount,
596 payment_card_details: self.payment_card_details,
597 contingency: self.contingency,
598 exit_at: self.exit_at,
599 operation_type: self.operation_type,
600 purpose_code: self.purpose_code,
601 destination_indicator: self.destination_indicator,
602 intermediary_indicator: self.intermediary_indicator,
603 emission_process: self.emission_process,
604 consumer_type: self.consumer_type,
605 buyer_presence: self.buyer_presence,
606 print_format: self.print_format,
607 ver_proc: self.ver_proc,
608 references: self.references,
609 transport: self.transport,
610 billing: self.billing,
611 withdrawal: self.withdrawal,
612 delivery: self.delivery,
613 authorized_xml: self.authorized_xml,
614 additional_info: self.additional_info,
615 intermediary: self.intermediary,
616 ret_trib: self.ret_trib,
617 tech_responsible: self.tech_responsible,
618 purchase: self.purchase,
619 export: self.export,
620 issqn_tot: self.issqn_tot,
621 cana: self.cana,
622 agropecuario: self.agropecuario,
623 compra_gov: self.compra_gov,
624 pag_antecipado: self.pag_antecipado,
625 is_tot: self.is_tot,
626 ibs_cbs_tot: self.ibs_cbs_tot,
627 result_xml: self.result_xml,
628 result_access_key: self.result_access_key,
629 result_signed_xml: Some(signed_xml),
630 _state: PhantomData,
631 })
632 }
633}
634
635impl InvoiceBuilder<Signed> {
638 pub fn signed_xml(&self) -> &str {
640 self.result_signed_xml
641 .as_deref()
642 .expect("Signed state always has signed XML")
643 }
644
645 pub fn access_key(&self) -> &str {
647 self.result_access_key
648 .as_deref()
649 .expect("Signed state always has access key")
650 }
651
652 pub fn unsigned_xml(&self) -> &str {
654 self.result_xml
655 .as_deref()
656 .expect("Signed state always has unsigned XML")
657 }
658}
659
660#[cfg(test)]
661mod tests {
662 use super::*;
663 use crate::newtypes::{Cents, IbgeCode, Rate};
664 use crate::types::{
665 InvoiceItemData, InvoiceModel, IssuerData, PaymentData, SefazEnvironment, TaxRegime,
666 };
667
668 fn br_offset() -> chrono::FixedOffset {
670 chrono::FixedOffset::west_opt(3 * 3600).unwrap()
671 }
672
673 fn sample_builder() -> InvoiceBuilder<Draft> {
675 let issuer = IssuerData::new(
676 "12345678000199",
677 "123456789",
678 "Test Company",
679 TaxRegime::SimplesNacional,
680 "SP",
681 IbgeCode("3550308".to_string()),
682 "Sao Paulo",
683 "Av Paulista",
684 "1000",
685 "Bela Vista",
686 "01310100",
687 )
688 .trade_name("Test");
689
690 let item = InvoiceItemData::new(
691 1,
692 "1",
693 "Product A",
694 "84715010",
695 "5102",
696 "UN",
697 2.0,
698 Cents(1000),
699 Cents(2000),
700 "102",
701 Rate(0),
702 Cents(0),
703 "99",
704 "99",
705 );
706
707 let payment = PaymentData::new("01", Cents(2000));
708
709 let offset = br_offset();
710 let issued_at = chrono::NaiveDate::from_ymd_opt(2026, 1, 15)
711 .unwrap()
712 .and_hms_opt(10, 30, 0)
713 .unwrap()
714 .and_local_timezone(offset)
715 .unwrap();
716
717 InvoiceBuilder::new(issuer, SefazEnvironment::Homologation, InvoiceModel::Nfce)
718 .series(1)
719 .invoice_number(1)
720 .issued_at(issued_at)
721 .add_item(item)
722 .payments(vec![payment])
723 }
724
725 fn built_builder() -> InvoiceBuilder<Built> {
727 sample_builder().build().expect("build should succeed")
728 }
729
730 #[test]
731 fn sign_with_identity_fn() {
732 let built = built_builder();
733 let original_xml = built.xml().to_string();
734
735 let signed = built
736 .sign_with(|xml| Ok(xml.to_string()))
737 .expect("identity signer should not fail");
738
739 assert_eq!(signed.signed_xml(), original_xml);
740 }
741
742 #[test]
743 fn sign_with_failing_fn() {
744 let built = built_builder();
745
746 let result =
747 built.sign_with(|_xml| Err(FiscalError::Certificate("test signing failure".into())));
748
749 let err = match result {
750 Err(e) => e,
751 Ok(_) => panic!("expected sign_with to return Err"),
752 };
753 assert_eq!(err, FiscalError::Certificate("test signing failure".into()),);
754 }
755
756 #[test]
757 fn signed_accessors() {
758 let built = built_builder();
759 let original_xml = built.xml().to_string();
760 let original_key = built.access_key().to_string();
761
762 let signed = built
763 .sign_with(|xml| Ok(format!("{xml}<Signature/>")))
764 .expect("signer should succeed");
765
766 assert_eq!(signed.signed_xml(), format!("{original_xml}<Signature/>"),);
767 assert_eq!(signed.access_key(), original_key);
768 assert_eq!(signed.unsigned_xml(), original_xml);
769 }
770
771 #[test]
772 fn built_still_works() {
773 let built = built_builder();
774
775 let xml = built.xml();
777 assert!(xml.contains("<NFe"));
778 assert!(xml.contains("</NFe>"));
779 assert!(xml.contains("<infNFe"));
780
781 let key = built.access_key();
782 assert_eq!(key.len(), 44);
783 assert!(key.chars().all(|c| c.is_ascii_digit()));
784 }
785
786 fn nfe_builder() -> InvoiceBuilder<Draft> {
788 let issuer = IssuerData::new(
789 "12345678000199",
790 "123456789",
791 "Test Company",
792 TaxRegime::SimplesNacional,
793 "SP",
794 IbgeCode("3550308".to_string()),
795 "Sao Paulo",
796 "Av Paulista",
797 "1000",
798 "Bela Vista",
799 "01310100",
800 )
801 .trade_name("Test");
802
803 let item = InvoiceItemData::new(
804 1,
805 "1",
806 "Product A",
807 "84715010",
808 "5102",
809 "UN",
810 2.0,
811 Cents(1000),
812 Cents(2000),
813 "102",
814 Rate(0),
815 Cents(0),
816 "99",
817 "99",
818 );
819
820 let payment = PaymentData::new("01", Cents(2000));
821
822 let offset = br_offset();
823 let issued_at = chrono::NaiveDate::from_ymd_opt(2026, 1, 15)
824 .unwrap()
825 .and_hms_opt(10, 30, 0)
826 .unwrap()
827 .and_local_timezone(offset)
828 .unwrap();
829
830 InvoiceBuilder::new(issuer, SefazEnvironment::Homologation, InvoiceModel::Nfe)
831 .series(1)
832 .invoice_number(1)
833 .issued_at(issued_at)
834 .add_item(item)
835 .payments(vec![payment])
836 }
837
838 #[test]
839 fn dh_sai_ent_emitted_for_model_55() {
840 let offset = br_offset();
841 let exit = chrono::NaiveDate::from_ymd_opt(2026, 1, 15)
842 .unwrap()
843 .and_hms_opt(14, 0, 0)
844 .unwrap()
845 .and_local_timezone(offset)
846 .unwrap();
847
848 let built = nfe_builder()
849 .exit_at(exit)
850 .build()
851 .expect("build should succeed");
852
853 let xml = built.xml();
854 assert!(
855 xml.contains("<dhSaiEnt>2026-01-15T14:00:00-03:00</dhSaiEnt>"),
856 "NF-e (model 55) with exit_at must emit <dhSaiEnt>, got:\n{xml}"
857 );
858 let emi_pos = xml.find("<dhEmi>").expect("dhEmi must be present");
860 let sai_pos = xml.find("<dhSaiEnt>").expect("dhSaiEnt must be present");
861 let tp_nf_pos = xml.find("<tpNF>").expect("tpNF must be present");
862 assert!(
863 emi_pos < sai_pos && sai_pos < tp_nf_pos,
864 "dhSaiEnt must come after dhEmi and before tpNF"
865 );
866 }
867
868 #[test]
869 fn dh_sai_ent_omitted_for_model_65() {
870 let offset = br_offset();
871 let exit = chrono::NaiveDate::from_ymd_opt(2026, 1, 15)
872 .unwrap()
873 .and_hms_opt(14, 0, 0)
874 .unwrap()
875 .and_local_timezone(offset)
876 .unwrap();
877
878 let built = sample_builder()
879 .exit_at(exit)
880 .build()
881 .expect("build should succeed");
882
883 let xml = built.xml();
884 assert!(
885 !xml.contains("<dhSaiEnt>"),
886 "NFC-e (model 65) must NOT emit <dhSaiEnt>, got:\n{xml}"
887 );
888 }
889
890 #[test]
891 fn dh_sai_ent_omitted_when_not_set() {
892 let built = nfe_builder().build().expect("build should succeed");
893
894 let xml = built.xml();
895 assert!(
896 !xml.contains("<dhSaiEnt>"),
897 "NF-e without exit_at must NOT emit <dhSaiEnt>"
898 );
899 }
900
901 #[test]
902 fn dh_cont_and_x_just_emitted_in_contingency() {
903 use crate::types::{ContingencyData, ContingencyType};
904
905 let offset = br_offset();
906 let cont_at = chrono::NaiveDate::from_ymd_opt(2026, 1, 15)
907 .unwrap()
908 .and_hms_opt(9, 0, 0)
909 .unwrap()
910 .and_local_timezone(offset)
911 .unwrap();
912
913 let contingency = ContingencyData::new(
914 ContingencyType::SvcAn,
915 "SEFAZ fora do ar para manutencao programada",
916 cont_at,
917 );
918
919 let built = nfe_builder()
920 .contingency(contingency)
921 .build()
922 .expect("build should succeed");
923
924 let xml = built.xml();
925 assert!(
926 xml.contains("<dhCont>2026-01-15T09:00:00-03:00</dhCont>"),
927 "Contingency must emit <dhCont>, got:\n{xml}"
928 );
929 assert!(
930 xml.contains("<xJust>SEFAZ fora do ar para manutencao programada</xJust>"),
931 "Contingency must emit <xJust>, got:\n{xml}"
932 );
933 let ver_proc_pos = xml.find("<verProc>").expect("verProc must be present");
935 let dh_cont_pos = xml.find("<dhCont>").expect("dhCont must be present");
936 let x_just_pos = xml.find("<xJust>").expect("xJust must be present");
937 assert!(
938 ver_proc_pos < dh_cont_pos && dh_cont_pos < x_just_pos,
939 "dhCont must come after verProc, xJust must come after dhCont"
940 );
941 }
942
943 #[test]
944 fn dh_cont_omitted_without_contingency() {
945 let built = nfe_builder().build().expect("build should succeed");
946
947 let xml = built.xml();
948 assert!(
949 !xml.contains("<dhCont>"),
950 "Without contingency, <dhCont> must NOT be present"
951 );
952 assert!(
953 !xml.contains("<xJust>"),
954 "Without contingency, <xJust> must NOT be present"
955 );
956 }
957}