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 schema_version: SchemaVersion,
54
55 series: u32,
57 invoice_number: u32,
58 emission_type: EmissionType,
59 issued_at: DateTime<FixedOffset>,
60 operation_nature: String,
61
62 items: Vec<InvoiceItemData>,
64 recipient: Option<RecipientData>,
65 payments: Vec<PaymentData>,
66 change_amount: Option<Cents>,
67 payment_card_details: Option<Vec<PaymentCardDetail>>,
68 contingency: Option<ContingencyData>,
69 exit_at: Option<DateTime<FixedOffset>>,
70
71 operation_type: Option<u8>,
73 purpose_code: Option<u8>,
74 destination_indicator: Option<String>,
75 intermediary_indicator: Option<String>,
76 emission_process: Option<String>,
77 consumer_type: Option<String>,
78 buyer_presence: Option<String>,
79 print_format: Option<String>,
80 ver_proc: Option<String>,
81 references: Option<Vec<ReferenceDoc>>,
82
83 transport: Option<TransportData>,
85 billing: Option<BillingData>,
86 withdrawal: Option<LocationData>,
87 delivery: Option<LocationData>,
88 authorized_xml: Option<Vec<AuthorizedXml>>,
89 additional_info: Option<AdditionalInfo>,
90 intermediary: Option<IntermediaryData>,
91 ret_trib: Option<RetTribData>,
92 tech_responsible: Option<TechResponsibleData>,
93 purchase: Option<PurchaseData>,
94 export: Option<ExportData>,
95 issqn_tot: Option<IssqnTotData>,
96 cana: Option<CanaData>,
97 agropecuario: Option<AgropecuarioData>,
98 compra_gov: Option<CompraGovData>,
99 pag_antecipado: Option<PagAntecipadoData>,
100 is_tot: Option<crate::tax_ibs_cbs::IsTotData>,
101 ibs_cbs_tot: Option<crate::tax_ibs_cbs::IbsCbsTotData>,
102
103 only_ascii: bool,
105 calculation_method: crate::types::CalculationMethod,
106
107 result_xml: Option<String>,
109 result_access_key: Option<String>,
110
111 result_signed_xml: Option<String>,
113
114 _state: PhantomData<State>,
115}
116
117impl InvoiceBuilder<Draft> {
120 pub fn new(issuer: IssuerData, environment: SefazEnvironment, model: InvoiceModel) -> Self {
125 let now = chrono::Utc::now()
126 .with_timezone(&FixedOffset::west_opt(3 * 3600).expect("valid offset"));
127
128 Self {
129 issuer,
130 environment,
131 model,
132 schema_version: SchemaVersion::default(),
133 series: 1,
134 invoice_number: 1,
135 emission_type: EmissionType::Normal,
136 issued_at: now,
137 operation_nature: "VENDA".to_string(),
138 items: Vec::new(),
139 recipient: None,
140 payments: Vec::new(),
141 change_amount: None,
142 payment_card_details: None,
143 contingency: None,
144 exit_at: None,
145 operation_type: None,
146 purpose_code: None,
147 destination_indicator: None,
148 intermediary_indicator: None,
149 emission_process: None,
150 consumer_type: None,
151 buyer_presence: None,
152 print_format: None,
153 ver_proc: None,
154 references: None,
155 transport: None,
156 billing: None,
157 withdrawal: None,
158 delivery: None,
159 authorized_xml: None,
160 additional_info: None,
161 intermediary: None,
162 ret_trib: None,
163 tech_responsible: None,
164 purchase: None,
165 export: None,
166 issqn_tot: None,
167 cana: None,
168 agropecuario: None,
169 compra_gov: None,
170 pag_antecipado: None,
171 is_tot: None,
172 ibs_cbs_tot: None,
173 only_ascii: false,
174 calculation_method: crate::types::CalculationMethod::V2,
175 result_xml: None,
176 result_access_key: None,
177 result_signed_xml: None,
178 _state: PhantomData,
179 }
180 }
181
182 pub fn series(mut self, s: u32) -> Self {
186 self.series = s;
187 self
188 }
189
190 pub fn invoice_number(mut self, n: u32) -> Self {
192 self.invoice_number = n;
193 self
194 }
195
196 pub fn emission_type(mut self, et: EmissionType) -> Self {
198 self.emission_type = et;
199 self
200 }
201
202 pub fn schema_version(mut self, sv: SchemaVersion) -> Self {
211 self.schema_version = sv;
212 self
213 }
214
215 pub fn issued_at(mut self, dt: DateTime<FixedOffset>) -> Self {
217 self.issued_at = dt;
218 self
219 }
220
221 pub fn operation_nature(mut self, n: impl Into<String>) -> Self {
223 self.operation_nature = n.into();
224 self
225 }
226
227 pub fn add_item(mut self, item: InvoiceItemData) -> Self {
229 self.items.push(item);
230 self
231 }
232
233 pub fn items(mut self, items: Vec<InvoiceItemData>) -> Self {
235 self.items = items;
236 self
237 }
238
239 pub fn recipient(mut self, r: RecipientData) -> Self {
241 self.recipient = Some(r);
242 self
243 }
244
245 pub fn payments(mut self, p: Vec<PaymentData>) -> Self {
247 self.payments = p;
248 self
249 }
250
251 pub fn change_amount(mut self, c: Cents) -> Self {
253 self.change_amount = Some(c);
254 self
255 }
256
257 pub fn payment_card_details(mut self, d: Vec<PaymentCardDetail>) -> Self {
259 self.payment_card_details = Some(d);
260 self
261 }
262
263 pub fn contingency(mut self, c: ContingencyData) -> Self {
265 self.contingency = Some(c);
266 self
267 }
268
269 pub fn exit_at(mut self, dt: DateTime<FixedOffset>) -> Self {
271 self.exit_at = Some(dt);
272 self
273 }
274
275 pub fn operation_type(mut self, v: u8) -> Self {
277 self.operation_type = Some(v);
278 self
279 }
280
281 pub fn purpose_code(mut self, v: u8) -> Self {
283 self.purpose_code = Some(v);
284 self
285 }
286
287 pub fn intermediary_indicator(mut self, v: impl Into<String>) -> Self {
289 self.intermediary_indicator = Some(v.into());
290 self
291 }
292
293 pub fn emission_process(mut self, v: impl Into<String>) -> Self {
295 self.emission_process = Some(v.into());
296 self
297 }
298
299 pub fn consumer_type(mut self, v: impl Into<String>) -> Self {
301 self.consumer_type = Some(v.into());
302 self
303 }
304
305 pub fn buyer_presence(mut self, v: impl Into<String>) -> Self {
307 self.buyer_presence = Some(v.into());
308 self
309 }
310
311 pub fn print_format(mut self, v: impl Into<String>) -> Self {
313 self.print_format = Some(v.into());
314 self
315 }
316
317 pub fn destination_indicator(mut self, v: impl Into<String>) -> Self {
319 self.destination_indicator = Some(v.into());
320 self
321 }
322
323 pub fn ver_proc(mut self, v: impl Into<String>) -> Self {
325 self.ver_proc = Some(v.into());
326 self
327 }
328
329 pub fn references(mut self, refs: Vec<ReferenceDoc>) -> Self {
331 self.references = Some(refs);
332 self
333 }
334
335 pub fn transport(mut self, t: TransportData) -> Self {
337 self.transport = Some(t);
338 self
339 }
340
341 pub fn billing(mut self, b: BillingData) -> Self {
343 self.billing = Some(b);
344 self
345 }
346
347 pub fn withdrawal(mut self, w: LocationData) -> Self {
349 self.withdrawal = Some(w);
350 self
351 }
352
353 pub fn delivery(mut self, d: LocationData) -> Self {
355 self.delivery = Some(d);
356 self
357 }
358
359 pub fn authorized_xml(mut self, a: Vec<AuthorizedXml>) -> Self {
361 self.authorized_xml = Some(a);
362 self
363 }
364
365 pub fn additional_info(mut self, a: AdditionalInfo) -> Self {
367 self.additional_info = Some(a);
368 self
369 }
370
371 pub fn intermediary(mut self, i: IntermediaryData) -> Self {
373 self.intermediary = Some(i);
374 self
375 }
376
377 pub fn ret_trib(mut self, r: RetTribData) -> Self {
379 self.ret_trib = Some(r);
380 self
381 }
382
383 pub fn tech_responsible(mut self, t: TechResponsibleData) -> Self {
385 self.tech_responsible = Some(t);
386 self
387 }
388
389 pub fn purchase(mut self, p: PurchaseData) -> Self {
391 self.purchase = Some(p);
392 self
393 }
394
395 pub fn export(mut self, e: ExportData) -> Self {
397 self.export = Some(e);
398 self
399 }
400
401 pub fn issqn_tot(mut self, t: IssqnTotData) -> Self {
403 self.issqn_tot = Some(t);
404 self
405 }
406
407 pub fn cana(mut self, c: CanaData) -> Self {
409 self.cana = Some(c);
410 self
411 }
412
413 pub fn agropecuario(mut self, a: AgropecuarioData) -> Self {
415 self.agropecuario = Some(a);
416 self
417 }
418
419 pub fn compra_gov(mut self, c: CompraGovData) -> Self {
421 self.compra_gov = Some(c);
422 self
423 }
424
425 pub fn pag_antecipado(mut self, p: PagAntecipadoData) -> Self {
427 self.pag_antecipado = Some(p);
428 self
429 }
430
431 pub fn is_tot(mut self, t: crate::tax_ibs_cbs::IsTotData) -> Self {
433 self.is_tot = Some(t);
434 self
435 }
436
437 pub fn ibs_cbs_tot(mut self, t: crate::tax_ibs_cbs::IbsCbsTotData) -> Self {
439 self.ibs_cbs_tot = Some(t);
440 self
441 }
442
443 pub fn only_ascii(mut self, enabled: bool) -> Self {
451 self.only_ascii = enabled;
452 self
453 }
454
455 pub fn calculation_method(mut self, m: crate::types::CalculationMethod) -> Self {
462 self.calculation_method = m;
463 self
464 }
465
466 pub fn build(self) -> Result<InvoiceBuilder<Built>, FiscalError> {
474 let data = InvoiceBuildData {
475 schema_version: self.schema_version,
476 model: self.model,
477 series: self.series,
478 number: self.invoice_number,
479 emission_type: self.emission_type,
480 environment: self.environment,
481 issued_at: self.issued_at,
482 operation_nature: self.operation_nature,
483 issuer: self.issuer,
484 recipient: self.recipient,
485 items: self.items,
486 payments: self.payments,
487 change_amount: self.change_amount,
488 payment_card_details: self.payment_card_details,
489 contingency: self.contingency,
490 exit_at: self.exit_at,
491 operation_type: self.operation_type,
492 purpose_code: self.purpose_code,
493 destination_indicator: self.destination_indicator,
494 intermediary_indicator: self.intermediary_indicator,
495 emission_process: self.emission_process,
496 consumer_type: self.consumer_type,
497 buyer_presence: self.buyer_presence,
498 print_format: self.print_format,
499 ver_proc: self.ver_proc,
500 references: self.references,
501 transport: self.transport,
502 billing: self.billing,
503 withdrawal: self.withdrawal,
504 delivery: self.delivery,
505 authorized_xml: self.authorized_xml,
506 additional_info: self.additional_info,
507 intermediary: self.intermediary,
508 ret_trib: self.ret_trib,
509 tech_responsible: self.tech_responsible,
510 purchase: self.purchase,
511 export: self.export,
512 issqn_tot: self.issqn_tot,
513 cana: self.cana,
514 agropecuario: self.agropecuario,
515 compra_gov: self.compra_gov,
516 pag_antecipado: self.pag_antecipado,
517 is_tot: self.is_tot,
518 ibs_cbs_tot: self.ibs_cbs_tot,
519 only_ascii: self.only_ascii,
520 calculation_method: self.calculation_method,
521 };
522
523 let result = super::generate_xml(&data)?;
524
525 Ok(InvoiceBuilder {
526 issuer: data.issuer,
527 environment: data.environment,
528 model: data.model,
529 schema_version: data.schema_version,
530 series: data.series,
531 invoice_number: data.number,
532 emission_type: data.emission_type,
533 issued_at: data.issued_at,
534 operation_nature: data.operation_nature,
535 items: data.items,
536 recipient: data.recipient,
537 payments: data.payments,
538 change_amount: data.change_amount,
539 payment_card_details: data.payment_card_details,
540 contingency: data.contingency,
541 exit_at: data.exit_at,
542 operation_type: data.operation_type,
543 purpose_code: data.purpose_code,
544 destination_indicator: data.destination_indicator,
545 intermediary_indicator: data.intermediary_indicator,
546 emission_process: data.emission_process,
547 consumer_type: data.consumer_type,
548 buyer_presence: data.buyer_presence,
549 print_format: data.print_format,
550 ver_proc: data.ver_proc,
551 references: data.references,
552 transport: data.transport,
553 billing: data.billing,
554 withdrawal: data.withdrawal,
555 delivery: data.delivery,
556 authorized_xml: data.authorized_xml,
557 additional_info: data.additional_info,
558 intermediary: data.intermediary,
559 ret_trib: data.ret_trib,
560 tech_responsible: data.tech_responsible,
561 purchase: data.purchase,
562 export: data.export,
563 issqn_tot: data.issqn_tot,
564 cana: data.cana,
565 agropecuario: data.agropecuario,
566 compra_gov: data.compra_gov,
567 pag_antecipado: data.pag_antecipado,
568 is_tot: data.is_tot,
569 ibs_cbs_tot: data.ibs_cbs_tot,
570 only_ascii: data.only_ascii,
571 calculation_method: data.calculation_method,
572 result_xml: Some(result.xml),
573 result_access_key: Some(result.access_key),
574 result_signed_xml: None,
575 _state: PhantomData,
576 })
577 }
578}
579
580impl InvoiceBuilder<Built> {
583 pub fn xml(&self) -> &str {
585 self.result_xml
586 .as_deref()
587 .expect("Built state always has XML")
588 }
589
590 pub fn access_key(&self) -> &str {
592 self.result_access_key
593 .as_deref()
594 .expect("Built state always has access key")
595 }
596
597 pub fn sign_with<F>(self, signer: F) -> Result<InvoiceBuilder<Signed>, FiscalError>
623 where
624 F: FnOnce(&str) -> Result<String, FiscalError>,
625 {
626 let unsigned_xml = self
627 .result_xml
628 .as_deref()
629 .expect("Built state always has XML");
630
631 let signed_xml = signer(unsigned_xml)?;
632
633 Ok(InvoiceBuilder {
634 issuer: self.issuer,
635 environment: self.environment,
636 model: self.model,
637 schema_version: self.schema_version,
638 series: self.series,
639 invoice_number: self.invoice_number,
640 emission_type: self.emission_type,
641 issued_at: self.issued_at,
642 operation_nature: self.operation_nature,
643 items: self.items,
644 recipient: self.recipient,
645 payments: self.payments,
646 change_amount: self.change_amount,
647 payment_card_details: self.payment_card_details,
648 contingency: self.contingency,
649 exit_at: self.exit_at,
650 operation_type: self.operation_type,
651 purpose_code: self.purpose_code,
652 destination_indicator: self.destination_indicator,
653 intermediary_indicator: self.intermediary_indicator,
654 emission_process: self.emission_process,
655 consumer_type: self.consumer_type,
656 buyer_presence: self.buyer_presence,
657 print_format: self.print_format,
658 ver_proc: self.ver_proc,
659 references: self.references,
660 transport: self.transport,
661 billing: self.billing,
662 withdrawal: self.withdrawal,
663 delivery: self.delivery,
664 authorized_xml: self.authorized_xml,
665 additional_info: self.additional_info,
666 intermediary: self.intermediary,
667 ret_trib: self.ret_trib,
668 tech_responsible: self.tech_responsible,
669 purchase: self.purchase,
670 export: self.export,
671 issqn_tot: self.issqn_tot,
672 cana: self.cana,
673 agropecuario: self.agropecuario,
674 compra_gov: self.compra_gov,
675 pag_antecipado: self.pag_antecipado,
676 is_tot: self.is_tot,
677 ibs_cbs_tot: self.ibs_cbs_tot,
678 only_ascii: self.only_ascii,
679 calculation_method: self.calculation_method,
680 result_xml: self.result_xml,
681 result_access_key: self.result_access_key,
682 result_signed_xml: Some(signed_xml),
683 _state: PhantomData,
684 })
685 }
686}
687
688impl InvoiceBuilder<Signed> {
691 pub fn signed_xml(&self) -> &str {
693 self.result_signed_xml
694 .as_deref()
695 .expect("Signed state always has signed XML")
696 }
697
698 pub fn access_key(&self) -> &str {
700 self.result_access_key
701 .as_deref()
702 .expect("Signed state always has access key")
703 }
704
705 pub fn unsigned_xml(&self) -> &str {
707 self.result_xml
708 .as_deref()
709 .expect("Signed state always has unsigned XML")
710 }
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716 use crate::newtypes::{Cents, IbgeCode, Rate};
717 use crate::types::{
718 InvoiceItemData, InvoiceModel, IssuerData, PaymentData, SefazEnvironment, TaxRegime,
719 };
720
721 fn br_offset() -> chrono::FixedOffset {
723 chrono::FixedOffset::west_opt(3 * 3600).unwrap()
724 }
725
726 fn sample_builder() -> InvoiceBuilder<Draft> {
728 let issuer = IssuerData::new(
729 "12345678000199",
730 "123456789",
731 "Test Company",
732 TaxRegime::SimplesNacional,
733 "SP",
734 IbgeCode("3550308".to_string()),
735 "Sao Paulo",
736 "Av Paulista",
737 "1000",
738 "Bela Vista",
739 "01310100",
740 )
741 .trade_name("Test");
742
743 let item = InvoiceItemData::new(
744 1,
745 "1",
746 "Product A",
747 "84715010",
748 "5102",
749 "UN",
750 2.0,
751 Cents(1000),
752 Cents(2000),
753 "102",
754 Rate(0),
755 Cents(0),
756 "99",
757 "99",
758 );
759
760 let payment = PaymentData::new("01", Cents(2000));
761
762 let offset = br_offset();
763 let issued_at = chrono::NaiveDate::from_ymd_opt(2026, 1, 15)
764 .unwrap()
765 .and_hms_opt(10, 30, 0)
766 .unwrap()
767 .and_local_timezone(offset)
768 .unwrap();
769
770 InvoiceBuilder::new(issuer, SefazEnvironment::Homologation, InvoiceModel::Nfce)
771 .series(1)
772 .invoice_number(1)
773 .issued_at(issued_at)
774 .add_item(item)
775 .payments(vec![payment])
776 }
777
778 fn built_builder() -> InvoiceBuilder<Built> {
780 sample_builder().build().expect("build should succeed")
781 }
782
783 #[test]
784 fn sign_with_identity_fn() {
785 let built = built_builder();
786 let original_xml = built.xml().to_string();
787
788 let signed = built
789 .sign_with(|xml| Ok(xml.to_string()))
790 .expect("identity signer should not fail");
791
792 assert_eq!(signed.signed_xml(), original_xml);
793 }
794
795 #[test]
796 fn sign_with_failing_fn() {
797 let built = built_builder();
798
799 let result =
800 built.sign_with(|_xml| Err(FiscalError::Certificate("test signing failure".into())));
801
802 let err = match result {
803 Err(e) => e,
804 Ok(_) => panic!("expected sign_with to return Err"),
805 };
806 assert_eq!(err, FiscalError::Certificate("test signing failure".into()),);
807 }
808
809 #[test]
810 fn signed_accessors() {
811 let built = built_builder();
812 let original_xml = built.xml().to_string();
813 let original_key = built.access_key().to_string();
814
815 let signed = built
816 .sign_with(|xml| Ok(format!("{xml}<Signature/>")))
817 .expect("signer should succeed");
818
819 assert_eq!(signed.signed_xml(), format!("{original_xml}<Signature/>"),);
820 assert_eq!(signed.access_key(), original_key);
821 assert_eq!(signed.unsigned_xml(), original_xml);
822 }
823
824 #[test]
825 fn built_still_works() {
826 let built = built_builder();
827
828 let xml = built.xml();
830 assert!(xml.contains("<NFe"));
831 assert!(xml.contains("</NFe>"));
832 assert!(xml.contains("<infNFe"));
833
834 let key = built.access_key();
835 assert_eq!(key.len(), 44);
836 assert!(key.chars().all(|c| c.is_ascii_digit()));
837 }
838
839 fn nfe_builder() -> InvoiceBuilder<Draft> {
841 let issuer = IssuerData::new(
842 "12345678000199",
843 "123456789",
844 "Test Company",
845 TaxRegime::SimplesNacional,
846 "SP",
847 IbgeCode("3550308".to_string()),
848 "Sao Paulo",
849 "Av Paulista",
850 "1000",
851 "Bela Vista",
852 "01310100",
853 )
854 .trade_name("Test");
855
856 let item = InvoiceItemData::new(
857 1,
858 "1",
859 "Product A",
860 "84715010",
861 "5102",
862 "UN",
863 2.0,
864 Cents(1000),
865 Cents(2000),
866 "102",
867 Rate(0),
868 Cents(0),
869 "99",
870 "99",
871 );
872
873 let payment = PaymentData::new("01", Cents(2000));
874
875 let offset = br_offset();
876 let issued_at = chrono::NaiveDate::from_ymd_opt(2026, 1, 15)
877 .unwrap()
878 .and_hms_opt(10, 30, 0)
879 .unwrap()
880 .and_local_timezone(offset)
881 .unwrap();
882
883 InvoiceBuilder::new(issuer, SefazEnvironment::Homologation, InvoiceModel::Nfe)
884 .series(1)
885 .invoice_number(1)
886 .issued_at(issued_at)
887 .add_item(item)
888 .payments(vec![payment])
889 }
890
891 #[test]
892 fn dh_sai_ent_emitted_for_model_55() {
893 let offset = br_offset();
894 let exit = chrono::NaiveDate::from_ymd_opt(2026, 1, 15)
895 .unwrap()
896 .and_hms_opt(14, 0, 0)
897 .unwrap()
898 .and_local_timezone(offset)
899 .unwrap();
900
901 let built = nfe_builder()
902 .exit_at(exit)
903 .build()
904 .expect("build should succeed");
905
906 let xml = built.xml();
907 assert!(
908 xml.contains("<dhSaiEnt>2026-01-15T14:00:00-03:00</dhSaiEnt>"),
909 "NF-e (model 55) with exit_at must emit <dhSaiEnt>, got:\n{xml}"
910 );
911 let emi_pos = xml.find("<dhEmi>").expect("dhEmi must be present");
913 let sai_pos = xml.find("<dhSaiEnt>").expect("dhSaiEnt must be present");
914 let tp_nf_pos = xml.find("<tpNF>").expect("tpNF must be present");
915 assert!(
916 emi_pos < sai_pos && sai_pos < tp_nf_pos,
917 "dhSaiEnt must come after dhEmi and before tpNF"
918 );
919 }
920
921 #[test]
922 fn dh_sai_ent_omitted_for_model_65() {
923 let offset = br_offset();
924 let exit = chrono::NaiveDate::from_ymd_opt(2026, 1, 15)
925 .unwrap()
926 .and_hms_opt(14, 0, 0)
927 .unwrap()
928 .and_local_timezone(offset)
929 .unwrap();
930
931 let built = sample_builder()
932 .exit_at(exit)
933 .build()
934 .expect("build should succeed");
935
936 let xml = built.xml();
937 assert!(
938 !xml.contains("<dhSaiEnt>"),
939 "NFC-e (model 65) must NOT emit <dhSaiEnt>, got:\n{xml}"
940 );
941 }
942
943 #[test]
944 fn dh_sai_ent_omitted_when_not_set() {
945 let built = nfe_builder().build().expect("build should succeed");
946
947 let xml = built.xml();
948 assert!(
949 !xml.contains("<dhSaiEnt>"),
950 "NF-e without exit_at must NOT emit <dhSaiEnt>"
951 );
952 }
953
954 #[test]
955 fn dh_cont_and_x_just_emitted_in_contingency() {
956 use crate::types::{ContingencyData, ContingencyType};
957
958 let offset = br_offset();
959 let cont_at = chrono::NaiveDate::from_ymd_opt(2026, 1, 15)
960 .unwrap()
961 .and_hms_opt(9, 0, 0)
962 .unwrap()
963 .and_local_timezone(offset)
964 .unwrap();
965
966 let contingency = ContingencyData::new(
967 ContingencyType::SvcAn,
968 "SEFAZ fora do ar para manutencao programada",
969 cont_at,
970 );
971
972 let built = nfe_builder()
973 .contingency(contingency)
974 .build()
975 .expect("build should succeed");
976
977 let xml = built.xml();
978 assert!(
979 xml.contains("<dhCont>2026-01-15T09:00:00-03:00</dhCont>"),
980 "Contingency must emit <dhCont>, got:\n{xml}"
981 );
982 assert!(
983 xml.contains("<xJust>SEFAZ fora do ar para manutencao programada</xJust>"),
984 "Contingency must emit <xJust>, got:\n{xml}"
985 );
986 let ver_proc_pos = xml.find("<verProc>").expect("verProc must be present");
988 let dh_cont_pos = xml.find("<dhCont>").expect("dhCont must be present");
989 let x_just_pos = xml.find("<xJust>").expect("xJust must be present");
990 assert!(
991 ver_proc_pos < dh_cont_pos && dh_cont_pos < x_just_pos,
992 "dhCont must come after verProc, xJust must come after dhCont"
993 );
994 }
995
996 #[test]
997 fn dh_cont_omitted_without_contingency() {
998 let built = nfe_builder().build().expect("build should succeed");
999
1000 let xml = built.xml();
1001 assert!(
1002 !xml.contains("<dhCont>"),
1003 "Without contingency, <dhCont> must NOT be present"
1004 );
1005 assert!(
1006 !xml.contains("<xJust>"),
1007 "Without contingency, <xJust> must NOT be present"
1008 );
1009 }
1010}