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