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
69 operation_type: Option<u8>,
71 purpose_code: Option<u8>,
72 intermediary_indicator: Option<String>,
73 emission_process: Option<String>,
74 consumer_type: Option<String>,
75 buyer_presence: Option<String>,
76 print_format: Option<String>,
77 references: Option<Vec<ReferenceDoc>>,
78
79 transport: Option<TransportData>,
81 billing: Option<BillingData>,
82 withdrawal: Option<LocationData>,
83 delivery: Option<LocationData>,
84 authorized_xml: Option<Vec<AuthorizedXml>>,
85 additional_info: Option<AdditionalInfo>,
86 intermediary: Option<IntermediaryData>,
87 ret_trib: Option<RetTribData>,
88 tech_responsible: Option<TechResponsibleData>,
89 purchase: Option<PurchaseData>,
90 export: Option<ExportData>,
91
92 result_xml: Option<String>,
94 result_access_key: Option<String>,
95
96 result_signed_xml: Option<String>,
98
99 _state: PhantomData<State>,
100}
101
102impl InvoiceBuilder<Draft> {
105 pub fn new(issuer: IssuerData, environment: SefazEnvironment, model: InvoiceModel) -> Self {
110 let now = chrono::Utc::now()
111 .with_timezone(&FixedOffset::west_opt(3 * 3600).expect("valid offset"));
112
113 Self {
114 issuer,
115 environment,
116 model,
117 series: 1,
118 invoice_number: 1,
119 emission_type: EmissionType::Normal,
120 issued_at: now,
121 operation_nature: "VENDA".to_string(),
122 items: Vec::new(),
123 recipient: None,
124 payments: Vec::new(),
125 change_amount: None,
126 payment_card_details: None,
127 contingency: None,
128 operation_type: None,
129 purpose_code: None,
130 intermediary_indicator: None,
131 emission_process: None,
132 consumer_type: None,
133 buyer_presence: None,
134 print_format: None,
135 references: None,
136 transport: None,
137 billing: None,
138 withdrawal: None,
139 delivery: None,
140 authorized_xml: None,
141 additional_info: None,
142 intermediary: None,
143 ret_trib: None,
144 tech_responsible: None,
145 purchase: None,
146 export: None,
147 result_xml: None,
148 result_access_key: None,
149 result_signed_xml: None,
150 _state: PhantomData,
151 }
152 }
153
154 pub fn series(mut self, s: u32) -> Self {
158 self.series = s;
159 self
160 }
161
162 pub fn invoice_number(mut self, n: u32) -> Self {
164 self.invoice_number = n;
165 self
166 }
167
168 pub fn emission_type(mut self, et: EmissionType) -> Self {
170 self.emission_type = et;
171 self
172 }
173
174 pub fn issued_at(mut self, dt: DateTime<FixedOffset>) -> Self {
176 self.issued_at = dt;
177 self
178 }
179
180 pub fn operation_nature(mut self, n: impl Into<String>) -> Self {
182 self.operation_nature = n.into();
183 self
184 }
185
186 pub fn add_item(mut self, item: InvoiceItemData) -> Self {
188 self.items.push(item);
189 self
190 }
191
192 pub fn items(mut self, items: Vec<InvoiceItemData>) -> Self {
194 self.items = items;
195 self
196 }
197
198 pub fn recipient(mut self, r: RecipientData) -> Self {
200 self.recipient = Some(r);
201 self
202 }
203
204 pub fn payments(mut self, p: Vec<PaymentData>) -> Self {
206 self.payments = p;
207 self
208 }
209
210 pub fn change_amount(mut self, c: Cents) -> Self {
212 self.change_amount = Some(c);
213 self
214 }
215
216 pub fn payment_card_details(mut self, d: Vec<PaymentCardDetail>) -> Self {
218 self.payment_card_details = Some(d);
219 self
220 }
221
222 pub fn contingency(mut self, c: ContingencyData) -> Self {
224 self.contingency = Some(c);
225 self
226 }
227
228 pub fn operation_type(mut self, v: u8) -> Self {
230 self.operation_type = Some(v);
231 self
232 }
233
234 pub fn purpose_code(mut self, v: u8) -> Self {
236 self.purpose_code = Some(v);
237 self
238 }
239
240 pub fn intermediary_indicator(mut self, v: impl Into<String>) -> Self {
242 self.intermediary_indicator = Some(v.into());
243 self
244 }
245
246 pub fn emission_process(mut self, v: impl Into<String>) -> Self {
248 self.emission_process = Some(v.into());
249 self
250 }
251
252 pub fn consumer_type(mut self, v: impl Into<String>) -> Self {
254 self.consumer_type = Some(v.into());
255 self
256 }
257
258 pub fn buyer_presence(mut self, v: impl Into<String>) -> Self {
260 self.buyer_presence = Some(v.into());
261 self
262 }
263
264 pub fn print_format(mut self, v: impl Into<String>) -> Self {
266 self.print_format = Some(v.into());
267 self
268 }
269
270 pub fn references(mut self, refs: Vec<ReferenceDoc>) -> Self {
272 self.references = Some(refs);
273 self
274 }
275
276 pub fn transport(mut self, t: TransportData) -> Self {
278 self.transport = Some(t);
279 self
280 }
281
282 pub fn billing(mut self, b: BillingData) -> Self {
284 self.billing = Some(b);
285 self
286 }
287
288 pub fn withdrawal(mut self, w: LocationData) -> Self {
290 self.withdrawal = Some(w);
291 self
292 }
293
294 pub fn delivery(mut self, d: LocationData) -> Self {
296 self.delivery = Some(d);
297 self
298 }
299
300 pub fn authorized_xml(mut self, a: Vec<AuthorizedXml>) -> Self {
302 self.authorized_xml = Some(a);
303 self
304 }
305
306 pub fn additional_info(mut self, a: AdditionalInfo) -> Self {
308 self.additional_info = Some(a);
309 self
310 }
311
312 pub fn intermediary(mut self, i: IntermediaryData) -> Self {
314 self.intermediary = Some(i);
315 self
316 }
317
318 pub fn ret_trib(mut self, r: RetTribData) -> Self {
320 self.ret_trib = Some(r);
321 self
322 }
323
324 pub fn tech_responsible(mut self, t: TechResponsibleData) -> Self {
326 self.tech_responsible = Some(t);
327 self
328 }
329
330 pub fn purchase(mut self, p: PurchaseData) -> Self {
332 self.purchase = Some(p);
333 self
334 }
335
336 pub fn export(mut self, e: ExportData) -> Self {
338 self.export = Some(e);
339 self
340 }
341
342 pub fn build(self) -> Result<InvoiceBuilder<Built>, FiscalError> {
350 let data = InvoiceBuildData {
351 model: self.model,
352 series: self.series,
353 number: self.invoice_number,
354 emission_type: self.emission_type,
355 environment: self.environment,
356 issued_at: self.issued_at,
357 operation_nature: self.operation_nature,
358 issuer: self.issuer,
359 recipient: self.recipient,
360 items: self.items,
361 payments: self.payments,
362 change_amount: self.change_amount,
363 payment_card_details: self.payment_card_details,
364 contingency: self.contingency,
365 operation_type: self.operation_type,
366 purpose_code: self.purpose_code,
367 intermediary_indicator: self.intermediary_indicator,
368 emission_process: self.emission_process,
369 consumer_type: self.consumer_type,
370 buyer_presence: self.buyer_presence,
371 print_format: self.print_format,
372 references: self.references,
373 transport: self.transport,
374 billing: self.billing,
375 withdrawal: self.withdrawal,
376 delivery: self.delivery,
377 authorized_xml: self.authorized_xml,
378 additional_info: self.additional_info,
379 intermediary: self.intermediary,
380 ret_trib: self.ret_trib,
381 tech_responsible: self.tech_responsible,
382 purchase: self.purchase,
383 export: self.export,
384 };
385
386 let result = super::generate_xml(&data)?;
387
388 Ok(InvoiceBuilder {
389 issuer: data.issuer,
390 environment: data.environment,
391 model: data.model,
392 series: data.series,
393 invoice_number: data.number,
394 emission_type: data.emission_type,
395 issued_at: data.issued_at,
396 operation_nature: data.operation_nature,
397 items: data.items,
398 recipient: data.recipient,
399 payments: data.payments,
400 change_amount: data.change_amount,
401 payment_card_details: data.payment_card_details,
402 contingency: data.contingency,
403 operation_type: data.operation_type,
404 purpose_code: data.purpose_code,
405 intermediary_indicator: data.intermediary_indicator,
406 emission_process: data.emission_process,
407 consumer_type: data.consumer_type,
408 buyer_presence: data.buyer_presence,
409 print_format: data.print_format,
410 references: data.references,
411 transport: data.transport,
412 billing: data.billing,
413 withdrawal: data.withdrawal,
414 delivery: data.delivery,
415 authorized_xml: data.authorized_xml,
416 additional_info: data.additional_info,
417 intermediary: data.intermediary,
418 ret_trib: data.ret_trib,
419 tech_responsible: data.tech_responsible,
420 purchase: data.purchase,
421 export: data.export,
422 result_xml: Some(result.xml),
423 result_access_key: Some(result.access_key),
424 result_signed_xml: None,
425 _state: PhantomData,
426 })
427 }
428}
429
430impl InvoiceBuilder<Built> {
433 pub fn xml(&self) -> &str {
435 self.result_xml
436 .as_deref()
437 .expect("Built state always has XML")
438 }
439
440 pub fn access_key(&self) -> &str {
442 self.result_access_key
443 .as_deref()
444 .expect("Built state always has access key")
445 }
446
447 pub fn sign_with<F>(self, signer: F) -> Result<InvoiceBuilder<Signed>, FiscalError>
473 where
474 F: FnOnce(&str) -> Result<String, FiscalError>,
475 {
476 let unsigned_xml = self
477 .result_xml
478 .as_deref()
479 .expect("Built state always has XML");
480
481 let signed_xml = signer(unsigned_xml)?;
482
483 Ok(InvoiceBuilder {
484 issuer: self.issuer,
485 environment: self.environment,
486 model: self.model,
487 series: self.series,
488 invoice_number: self.invoice_number,
489 emission_type: self.emission_type,
490 issued_at: self.issued_at,
491 operation_nature: self.operation_nature,
492 items: self.items,
493 recipient: self.recipient,
494 payments: self.payments,
495 change_amount: self.change_amount,
496 payment_card_details: self.payment_card_details,
497 contingency: self.contingency,
498 operation_type: self.operation_type,
499 purpose_code: self.purpose_code,
500 intermediary_indicator: self.intermediary_indicator,
501 emission_process: self.emission_process,
502 consumer_type: self.consumer_type,
503 buyer_presence: self.buyer_presence,
504 print_format: self.print_format,
505 references: self.references,
506 transport: self.transport,
507 billing: self.billing,
508 withdrawal: self.withdrawal,
509 delivery: self.delivery,
510 authorized_xml: self.authorized_xml,
511 additional_info: self.additional_info,
512 intermediary: self.intermediary,
513 ret_trib: self.ret_trib,
514 tech_responsible: self.tech_responsible,
515 purchase: self.purchase,
516 export: self.export,
517 result_xml: self.result_xml,
518 result_access_key: self.result_access_key,
519 result_signed_xml: Some(signed_xml),
520 _state: PhantomData,
521 })
522 }
523}
524
525impl InvoiceBuilder<Signed> {
528 pub fn signed_xml(&self) -> &str {
530 self.result_signed_xml
531 .as_deref()
532 .expect("Signed state always has signed XML")
533 }
534
535 pub fn access_key(&self) -> &str {
537 self.result_access_key
538 .as_deref()
539 .expect("Signed state always has access key")
540 }
541
542 pub fn unsigned_xml(&self) -> &str {
544 self.result_xml
545 .as_deref()
546 .expect("Signed state always has unsigned XML")
547 }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use crate::newtypes::{Cents, IbgeCode, Rate};
554 use crate::types::{
555 InvoiceItemData, InvoiceModel, IssuerData, PaymentData, SefazEnvironment, TaxRegime,
556 };
557
558 fn br_offset() -> chrono::FixedOffset {
560 chrono::FixedOffset::west_opt(3 * 3600).unwrap()
561 }
562
563 fn sample_builder() -> InvoiceBuilder<Draft> {
565 let issuer = IssuerData::new(
566 "12345678000199",
567 "123456789",
568 "Test Company",
569 TaxRegime::SimplesNacional,
570 "SP",
571 IbgeCode("3550308".to_string()),
572 "Sao Paulo",
573 "Av Paulista",
574 "1000",
575 "Bela Vista",
576 "01310100",
577 )
578 .trade_name("Test");
579
580 let item = InvoiceItemData::new(
581 1,
582 "1",
583 "Product A",
584 "84715010",
585 "5102",
586 "UN",
587 2.0,
588 Cents(1000),
589 Cents(2000),
590 "102",
591 Rate(0),
592 Cents(0),
593 "99",
594 "99",
595 );
596
597 let payment = PaymentData::new("01", Cents(2000));
598
599 let offset = br_offset();
600 let issued_at = chrono::NaiveDate::from_ymd_opt(2026, 1, 15)
601 .unwrap()
602 .and_hms_opt(10, 30, 0)
603 .unwrap()
604 .and_local_timezone(offset)
605 .unwrap();
606
607 InvoiceBuilder::new(issuer, SefazEnvironment::Homologation, InvoiceModel::Nfce)
608 .series(1)
609 .invoice_number(1)
610 .issued_at(issued_at)
611 .add_item(item)
612 .payments(vec![payment])
613 }
614
615 fn built_builder() -> InvoiceBuilder<Built> {
617 sample_builder().build().expect("build should succeed")
618 }
619
620 #[test]
621 fn sign_with_identity_fn() {
622 let built = built_builder();
623 let original_xml = built.xml().to_string();
624
625 let signed = built
626 .sign_with(|xml| Ok(xml.to_string()))
627 .expect("identity signer should not fail");
628
629 assert_eq!(signed.signed_xml(), original_xml);
630 }
631
632 #[test]
633 fn sign_with_failing_fn() {
634 let built = built_builder();
635
636 let result =
637 built.sign_with(|_xml| Err(FiscalError::Certificate("test signing failure".into())));
638
639 let err = match result {
640 Err(e) => e,
641 Ok(_) => panic!("expected sign_with to return Err"),
642 };
643 assert_eq!(err, FiscalError::Certificate("test signing failure".into()),);
644 }
645
646 #[test]
647 fn signed_accessors() {
648 let built = built_builder();
649 let original_xml = built.xml().to_string();
650 let original_key = built.access_key().to_string();
651
652 let signed = built
653 .sign_with(|xml| Ok(format!("{xml}<Signature/>")))
654 .expect("signer should succeed");
655
656 assert_eq!(signed.signed_xml(), format!("{original_xml}<Signature/>"),);
657 assert_eq!(signed.access_key(), original_key);
658 assert_eq!(signed.unsigned_xml(), original_xml);
659 }
660
661 #[test]
662 fn built_still_works() {
663 let built = built_builder();
664
665 let xml = built.xml();
667 assert!(xml.contains("<NFe"));
668 assert!(xml.contains("</NFe>"));
669 assert!(xml.contains("<infNFe"));
670
671 let key = built.access_key();
672 assert_eq!(key.len(), 44);
673 assert!(key.chars().all(|c| c.is_ascii_digit()));
674 }
675}