Skip to main content

fatoora_core/
invoice.rs

1//! Invoice domain types and builders.
2mod builder;
3mod qr;
4pub mod sign;
5pub mod validation;
6pub mod xml;
7pub use builder::{FinalizedInvoice, InvoiceBuilder, InvoiceView, SignedInvoice};
8pub use qr::{QrCodeError, QrPayload, QrResult};
9
10#[allow(unused_imports)]
11use bitflags::bitflags;
12use chrono::{NaiveDate, NaiveDateTime};
13use iso_currency::Currency as IsoCurrency;
14use isocountry::CountryCode as IsoCountryCode;
15use std::marker::PhantomData;
16use std::str::FromStr;
17use thiserror::Error;
18use serde::{Deserialize, Serialize};
19
20type Result<T> = std::result::Result<T, InvoiceError>;
21
22/// Invoice-related errors.
23#[derive(Debug, Error)]
24pub enum InvoiceError {
25    #[error(transparent)]
26    Validation(#[from] ValidationError),
27    #[error("Invalid country code: {0}")]
28    InvalidCountryCode(String),
29    #[error("Invalid currency code: {0}")]
30    InvalidCurrencyCode(String),
31    #[error("Invalid invoice timestamp: {0}")]
32    InvalidTimestamp(String),
33    #[error("Invalid invoice date: {0}")]
34    InvalidIssueDate(String),
35    #[error("Missing VAT ID for seller")]
36    MissingVatForSeller,
37    #[error("Missing Buyer ID for buyer")]
38    MissingBuyerId,
39    #[error("Invalid VAT ID format")]
40    InvalidVatFormat,
41}
42
43/// Structured validation error with field-level issues.
44#[derive(Debug, Clone, PartialEq, Eq, Hash, Error)]
45#[error("invoice validation failed")]
46pub struct ValidationError {
47    issues: Vec<ValidationIssue>,
48}
49
50impl ValidationError {
51    pub fn new(issues: Vec<ValidationIssue>) -> Self {
52        Self { issues }
53    }
54
55    pub fn issues(&self) -> &[ValidationIssue] {
56        &self.issues
57    }
58}
59
60/// Single validation issue.
61#[derive(Debug, Clone, PartialEq, Eq, Hash)]
62pub struct ValidationIssue {
63    field: InvoiceField,
64    kind: ValidationKind,
65    line_item_index: Option<usize>,
66}
67
68impl ValidationIssue {
69    pub fn new(
70        field: InvoiceField,
71        kind: ValidationKind,
72        line_item_index: Option<usize>,
73    ) -> Self {
74        Self {
75            field,
76            kind,
77            line_item_index,
78        }
79    }
80
81    pub fn field(&self) -> InvoiceField {
82        self.field
83    }
84
85    pub fn kind(&self) -> ValidationKind {
86        self.kind
87    }
88
89    pub fn line_item_index(&self) -> Option<usize> {
90        self.line_item_index
91    }
92}
93
94#[non_exhaustive]
95/// Field associated with a validation issue.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
97pub enum InvoiceField {
98    Id,
99    Uuid,
100    IssueDateTime,
101    Currency,
102    PreviousInvoiceHash,
103    InvoiceCounter,
104    Seller,
105    LineItems,
106    PaymentMeansCode,
107    VatCategory,
108    LineItemDescription,
109    LineItemUnitCode,
110    LineItemQuantity,
111    LineItemUnitPrice,
112    LineItemTotalAmount,
113    LineItemVatRate,
114    LineItemVatAmount,
115}
116
117#[non_exhaustive]
118/// Classification of validation issues.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
120pub enum ValidationKind {
121    Missing,
122    Empty,
123    InvalidFormat,
124    OutOfRange,
125    Mismatch,
126}
127
128/// Country code wrapper with ISO validation.
129#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
130pub struct CountryCode(String);
131
132impl CountryCode {
133    pub fn parse<S: Into<String>>(s: S) -> Result<Self> {
134        let value = s.into().trim().to_uppercase();
135        let normalized = match value.len() {
136            2 => IsoCountryCode::for_alpha2(&value)
137                .map_err(|_| InvoiceError::InvalidCountryCode(value.clone()))?
138                .alpha3()
139                .to_string(),
140            3 => IsoCountryCode::for_alpha3(&value)
141                .map_err(|_| InvoiceError::InvalidCountryCode(value.clone()))?
142                .alpha3()
143                .to_string(),
144            _ => return Err(InvoiceError::InvalidCountryCode(value)),
145        };
146        Ok(Self(normalized))
147    }
148
149    pub fn as_str(&self) -> &str {
150        &self.0
151    }
152
153    pub(crate) fn alpha2(&self) -> String {
154        IsoCountryCode::for_alpha3(self.as_str())
155            .expect("validated country code")
156            .alpha2()
157            .to_string()
158    }
159}
160
161impl AsRef<str> for CountryCode {
162    fn as_ref(&self) -> &str {
163        self.as_str()
164    }
165}
166
167impl FromStr for CountryCode {
168    type Err = InvoiceError;
169    fn from_str(s: &str) -> Result<Self> {
170        CountryCode::parse(s)
171    }
172}
173
174impl TryFrom<String> for CountryCode {
175    type Error = InvoiceError;
176    fn try_from(value: String) -> Result<Self> {
177        CountryCode::parse(value)
178    }
179}
180
181impl TryFrom<&str> for CountryCode {
182    type Error = InvoiceError;
183    fn try_from(value: &str) -> Result<Self> {
184        CountryCode::parse(value)
185    }
186}
187
188/// Currency code wrapper with ISO validation.
189#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
190pub struct CurrencyCode(String);
191
192impl CurrencyCode {
193    pub fn parse<S: Into<String>>(s: S) -> Result<Self> {
194        let value = s.into().trim().to_uppercase();
195        IsoCurrency::from_code(&value)
196            .ok_or_else(|| InvoiceError::InvalidCurrencyCode(value.clone()))?;
197        Ok(Self(value))
198    }
199
200    pub fn as_str(&self) -> &str {
201        &self.0
202    }
203}
204
205impl AsRef<str> for CurrencyCode {
206    fn as_ref(&self) -> &str {
207        self.as_str()
208    }
209}
210
211impl FromStr for CurrencyCode {
212    type Err = InvoiceError;
213    fn from_str(s: &str) -> Result<Self> {
214        CurrencyCode::parse(s)
215    }
216}
217
218impl TryFrom<String> for CurrencyCode {
219    type Error = InvoiceError;
220    fn try_from(value: String) -> Result<Self> {
221        CurrencyCode::parse(value)
222    }
223}
224
225impl TryFrom<&str> for CurrencyCode {
226    type Error = InvoiceError;
227    fn try_from(value: &str) -> Result<Self> {
228        CurrencyCode::parse(value)
229    }
230}
231
232/// Invoice timestamp in ZATCA ISO format (UTC `YYYY-MM-DDTHH:MM:SSZ`).
233#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
234pub struct InvoiceTimestamp(String);
235
236impl InvoiceTimestamp {
237    pub fn parse<S: Into<String>>(s: S) -> Result<Self> {
238        let value = s.into().trim().to_string();
239        let parsed = NaiveDateTime::parse_from_str(&value, "%Y-%m-%dT%H:%M:%SZ")
240            .map_err(|_| InvoiceError::InvalidTimestamp(value.clone()))?;
241        Ok(Self(parsed.format("%Y-%m-%dT%H:%M:%SZ").to_string()))
242    }
243
244    pub fn as_str(&self) -> &str {
245        &self.0
246    }
247
248    pub(crate) fn date_str(&self) -> &str {
249        &self.0[..10]
250    }
251
252    pub(crate) fn time_str(&self) -> &str {
253        &self.0[11..19]
254    }
255}
256
257impl AsRef<str> for InvoiceTimestamp {
258    fn as_ref(&self) -> &str {
259        self.as_str()
260    }
261}
262
263impl FromStr for InvoiceTimestamp {
264    type Err = InvoiceError;
265    fn from_str(s: &str) -> Result<Self> {
266        InvoiceTimestamp::parse(s)
267    }
268}
269
270impl TryFrom<String> for InvoiceTimestamp {
271    type Error = InvoiceError;
272    fn try_from(value: String) -> Result<Self> {
273        InvoiceTimestamp::parse(value)
274    }
275}
276
277impl TryFrom<&str> for InvoiceTimestamp {
278    type Error = InvoiceError;
279    fn try_from(value: &str) -> Result<Self> {
280        InvoiceTimestamp::parse(value)
281    }
282}
283
284/// Invoice date in `YYYY-MM-DD` format.
285#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
286pub struct InvoiceDate(String);
287
288impl InvoiceDate {
289    pub fn parse<S: Into<String>>(s: S) -> Result<Self> {
290        let value = s.into().trim().to_string();
291        let parsed = NaiveDate::parse_from_str(&value, "%Y-%m-%d")
292            .map_err(|_| InvoiceError::InvalidIssueDate(value.clone()))?;
293        Ok(Self(parsed.format("%Y-%m-%d").to_string()))
294    }
295
296    pub fn as_str(&self) -> &str {
297        &self.0
298    }
299}
300
301impl AsRef<str> for InvoiceDate {
302    fn as_ref(&self) -> &str {
303        self.as_str()
304    }
305}
306
307impl FromStr for InvoiceDate {
308    type Err = InvoiceError;
309    fn from_str(s: &str) -> Result<Self> {
310        InvoiceDate::parse(s)
311    }
312}
313
314impl TryFrom<String> for InvoiceDate {
315    type Error = InvoiceError;
316    fn try_from(value: String) -> Result<Self> {
317        InvoiceDate::parse(value)
318    }
319}
320
321impl TryFrom<&str> for InvoiceDate {
322    type Error = InvoiceError;
323    fn try_from(value: &str) -> Result<Self> {
324        InvoiceDate::parse(value)
325    }
326}
327
328/// Postal address for parties.
329#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
330pub struct Address {
331    pub country_code: CountryCode,
332    pub city: String,
333    pub street: String,
334    pub additional_street: Option<String>,
335    pub building_number: String,
336    pub additional_number: Option<String>,
337    pub postal_code: String, //fix 5 digits if country is KSA
338    pub subdivision: Option<String>,
339    pub district: Option<String>,
340}
341
342impl Address {
343    pub fn country_code(&self) -> &CountryCode {
344        &self.country_code
345    }
346
347    pub fn city(&self) -> &str {
348        &self.city
349    }
350
351    pub fn street(&self) -> &str {
352        &self.street
353    }
354
355    pub fn additional_street(&self) -> Option<&str> {
356        self.additional_street.as_deref()
357    }
358
359    pub fn building_number(&self) -> &str {
360        &self.building_number
361    }
362
363    pub fn additional_number(&self) -> Option<&str> {
364        self.additional_number.as_deref()
365    }
366
367    pub fn postal_code(&self) -> &str {
368        &self.postal_code
369    }
370
371    pub fn subdivision(&self) -> Option<&str> {
372        self.subdivision.as_deref()
373    }
374
375    pub fn district(&self) -> Option<&str> {
376        self.district.as_deref()
377    }
378}
379
380/// VAT identifier wrapper with validation helpers.
381///
382/// # Examples
383/// ```rust
384/// use fatoora_core::invoice::{VatId, InvoiceError};
385///
386/// let vat = VatId::parse("399999999900003")?;
387/// assert_eq!(vat.as_str(), "399999999900003");
388/// # Ok::<(), InvoiceError>(())
389/// ```
390///
391/// # Errors
392/// Returns [`InvoiceError::InvalidVatFormat`] if the input is empty.
393#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
394pub struct VatId(String);
395impl VatId {
396    pub fn parse<S: Into<String>>(s: S) -> Result<Self> {
397        let s = s.into().trim().to_string();
398        if s.is_empty() {
399            return Err(InvoiceError::InvalidVatFormat);
400        }
401        // TODO: tighten validation (e.g., KSA = 15 digits)
402        Ok(VatId(s))
403    }
404    pub fn as_str(&self) -> &str {
405        &self.0
406    }
407}
408impl AsRef<str> for VatId {
409    fn as_ref(&self) -> &str {
410        self.as_str()
411    }
412}
413impl FromStr for VatId {
414    type Err = InvoiceError;
415    fn from_str(s: &str) -> Result<Self> {
416        VatId::parse(s)
417    }
418}
419impl TryFrom<String> for VatId {
420    type Error = InvoiceError;
421    fn try_from(value: String) -> Result<Self> {
422        VatId::parse(value)
423    }
424}
425impl TryFrom<&str> for VatId {
426    type Error = InvoiceError;
427    fn try_from(value: &str) -> Result<Self> {
428        VatId::parse(value)
429    }
430}
431
432/// Additional party identifier.
433///
434/// # Examples
435/// ```rust
436/// use fatoora_core::invoice::OtherId;
437///
438/// let id = OtherId::with_scheme("7003339333", "CRN");
439/// assert_eq!(id.as_str(), "7003339333");
440/// assert_eq!(id.scheme_id(), Some("CRN"));
441/// ```
442#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
443pub struct OtherId {
444    value: String,
445    scheme_id: Option<String>,
446}
447impl OtherId {
448    pub fn new<S: Into<String>>(value: S) -> Self {
449        OtherId {
450            value: value.into(),
451            scheme_id: None,
452        }
453    }
454
455    pub fn with_scheme<V: Into<String>, S: Into<String>>(value: V, scheme_id: S) -> Self {
456        OtherId {
457            value: value.into(),
458            scheme_id: Some(scheme_id.into()),
459        }
460    }
461
462    pub fn as_str(&self) -> &str {
463        &self.value
464    }
465
466    pub fn scheme_id(&self) -> Option<&str> {
467        self.scheme_id.as_deref()
468    }
469}
470impl AsRef<str> for OtherId {
471    fn as_ref(&self) -> &str {
472        self.as_str()
473    }
474}
475
476/// Invoice note with language metadata.
477///
478/// # Examples
479/// ```rust
480/// use fatoora_core::invoice::InvoiceNote;
481///
482/// let note = InvoiceNote::new("en", "Thank you");
483/// assert_eq!(note.language(), "en");
484/// assert_eq!(note.text(), "Thank you");
485/// ```
486#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
487pub struct InvoiceNote {
488    language: String,
489    text: String,
490}
491
492impl InvoiceNote {
493    pub fn new(language: impl Into<String>, text: impl Into<String>) -> Self {
494        Self {
495            language: language.into(),
496            text: text.into(),
497        }
498    }
499
500    pub fn language(&self) -> &str {
501        &self.language
502    }
503
504    pub fn text(&self) -> &str {
505        &self.text
506    }
507}
508
509// Marker roles
510/// Marker trait for party role types.
511pub trait PartyRole {}
512
513/// Seller role marker.
514#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
515pub struct SellerRole;
516impl PartyRole for SellerRole {}
517/// Buyer role marker.
518#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
519pub struct BuyerRole;
520impl PartyRole for BuyerRole {}
521
522/// Party wrapper with role-specific typing.
523///
524/// # Examples
525/// ```rust
526/// use fatoora_core::invoice::{Party, SellerRole, Address, OtherId};
527/// use fatoora_core::invoice::CountryCode;
528///
529/// let seller = Party::<SellerRole>::new(
530///     "Acme Inc".into(),
531///     Address {
532///         country_code: CountryCode::parse("SAU")?,
533///         city: "Riyadh".into(),
534///         street: "King Fahd".into(),
535///         additional_street: None,
536///         building_number: "1234".into(),
537///         additional_number: Some("5678".into()),
538///         postal_code: "12222".into(),
539///         subdivision: None,
540///         district: None,
541///     },
542///     "399999999900003",
543///     Some(OtherId::with_scheme("7003339333", "CRN")),
544/// )?;
545/// # let _ = seller;
546/// use fatoora_core::invoice::InvoiceError;
547/// # Ok::<(), InvoiceError>(())
548/// ```
549#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
550#[allow(dead_code)]
551pub struct Party<R: PartyRole> {
552    _marker: PhantomData<R>,
553    name: String,
554    address: Address,
555    vat_id: Option<VatId>,
556    other_id: Option<OtherId>,
557}
558
559pub type Seller = Party<SellerRole>;
560pub type Buyer = Party<BuyerRole>;
561
562impl Party<SellerRole> {
563    /// Create a seller party from validated inputs.
564    ///
565    /// # Errors
566    /// Returns an error if the VAT ID is invalid.
567    pub fn new(
568        name: String,
569        address: Address,
570        vat_id: impl Into<String>, // required
571        other_id: Option<OtherId>, // optional
572    ) -> Result<Self> {
573        let vat = VatId::parse(vat_id.into())?;
574        Ok(Party {
575            _marker: PhantomData,
576            name,
577            address,
578            vat_id: Some(vat),
579            other_id,
580        })
581    }
582}
583
584impl Party<BuyerRole> {
585    /// Create a buyer party from validated inputs.
586    ///
587    /// # Errors
588    /// Returns an error if the VAT ID is invalid or no identifier is provided.
589    pub fn new(
590        name: String,
591        address: Address,
592        vat_id: Option<String>,    // optional
593        other_id: Option<OtherId>, // required if vat_id is None
594    ) -> Result<Self> {
595        let vat = match vat_id {
596            Some(v) => Some(VatId::parse(v)?),
597            None => None,
598        };
599        if vat.is_none() && other_id.is_none() {
600            return Err(InvoiceError::MissingBuyerId);
601        }
602        Ok(Party {
603            _marker: PhantomData,
604            name,
605            address,
606            vat_id: vat,
607            other_id,
608        })
609    }
610}
611
612impl<R: PartyRole> Party<R> {
613    pub fn name(&self) -> &str {
614        &self.name
615    }
616
617    pub fn address(&self) -> &Address {
618        &self.address
619    }
620
621    pub fn vat_id(&self) -> Option<&VatId> {
622        self.vat_id.as_ref()
623    }
624
625    pub fn other_id(&self) -> Option<&OtherId> {
626        self.other_id.as_ref()
627    }
628}
629
630/// Invoice subtype used for tax invoices and notes.
631///
632/// # Examples
633/// ```rust
634/// use fatoora_core::invoice::{InvoiceSubType, InvoiceType};
635///
636/// let invoice_type = InvoiceType::Tax(InvoiceSubType::Simplified);
637/// assert!(invoice_type.is_simplified());
638/// ```
639#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
640pub enum InvoiceSubType {
641    Simplified,
642    Standard,
643}
644
645/// Reference to an original invoice for credit/debit notes.
646///
647/// # Examples
648/// ```rust
649/// use fatoora_core::invoice::OriginalInvoiceRef;
650///
651/// let original = OriginalInvoiceRef::new("INV-ORIG")
652///     .with_uuid("uuid-orig");
653/// assert_eq!(original.id(), "INV-ORIG");
654/// assert_eq!(original.uuid(), Some("uuid-orig"));
655/// ```
656#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
657pub struct OriginalInvoiceRef {
658    id: String,
659    uuid: Option<String>,
660    issue_date: Option<InvoiceDate>,
661}
662
663impl OriginalInvoiceRef {
664    pub fn new(id: impl Into<String>) -> Self {
665        Self {
666            id: id.into(),
667            uuid: None,
668            issue_date: None,
669        }
670    }
671
672    pub fn with_uuid(mut self, uuid: impl Into<String>) -> Self {
673        self.uuid = Some(uuid.into());
674        self
675    }
676
677    pub fn with_issue_date(mut self, issue_date: InvoiceDate) -> Self {
678        self.issue_date = Some(issue_date);
679        self
680    }
681
682    pub fn with_issue_date_str(mut self, issue_date: impl Into<String>) -> Result<Self> {
683        self.issue_date = Some(InvoiceDate::parse(issue_date)?);
684        Ok(self)
685    }
686
687    pub fn id(&self) -> &str {
688        &self.id
689    }
690
691    pub fn uuid(&self) -> Option<&str> {
692        self.uuid.as_deref()
693    }
694
695    pub fn issue_date(&self) -> Option<&InvoiceDate> {
696        self.issue_date.as_ref()
697    }
698}
699
700/// Invoice type and required metadata.
701///
702/// # Examples
703/// ```rust
704/// use fatoora_core::invoice::{InvoiceSubType, InvoiceType};
705///
706/// let invoice_type = InvoiceType::Prepayment(InvoiceSubType::Standard);
707/// assert!(!invoice_type.is_simplified());
708/// ```
709#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
710pub enum InvoiceType {
711    Tax(InvoiceSubType),
712    Prepayment(InvoiceSubType),
713    CreditNote(InvoiceSubType, OriginalInvoiceRef, String), // original invoice ref + reason
714    DebitNote(InvoiceSubType, OriginalInvoiceRef, String),  // original invoice ref + reason
715}
716
717impl InvoiceType {
718    pub fn is_simplified(&self) -> bool {
719        matches!(
720            self,
721            InvoiceType::Tax(InvoiceSubType::Simplified)
722                | InvoiceType::Prepayment(InvoiceSubType::Simplified)
723                | InvoiceType::CreditNote(InvoiceSubType::Simplified, ..)
724                | InvoiceType::DebitNote(InvoiceSubType::Simplified, ..)
725        )
726    }
727}
728
729/// VAT category for line items.
730///
731/// # Examples
732/// ```rust
733/// use fatoora_core::invoice::VatCategory;
734///
735/// let cat = VatCategory::Standard;
736/// assert!(matches!(cat, VatCategory::Standard));
737/// ```
738#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
739pub enum VatCategory {
740    Exempt,
741    Standard,
742    Zero,
743    OutOfScope,
744}
745/// Single invoice line item.
746///
747/// # Examples
748/// ```rust
749/// use fatoora_core::invoice::{LineItem, VatCategory};
750///
751/// let item = LineItem::new("Item", 2.0, "PCE", 50.0, 15.0, VatCategory::Standard);
752/// assert_eq!(item.total_amount(), 100.0);
753/// ```
754#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
755pub struct LineItem {
756    description: String,
757    quantity: f64,
758    unit_code: String,
759    unit_price: f64,
760    total_amount: f64,
761    vat_rate: f64,
762    vat_amount: f64,
763    vat_category: VatCategory,
764}
765
766impl LineItem {
767    pub fn new(
768        description: impl Into<String>,
769        quantity: f64,
770        unit_code: impl Into<String>,
771        unit_price: f64,
772        vat_rate: f64,
773        vat_category: VatCategory,
774    ) -> Self {
775        let total_amount = Self::calculate_total_amount(quantity, unit_price);
776        let vat_amount = Self::calculate_vat_amount(total_amount, vat_rate);
777        Self {
778            description: description.into(),
779            quantity,
780            unit_code: unit_code.into(),
781            unit_price,
782            total_amount,
783            vat_rate,
784            vat_amount,
785            vat_category,
786        }
787    }
788
789    pub fn from_totals(
790        description: impl Into<String>,
791        quantity: f64,
792        unit_code: impl Into<String>,
793        unit_price: f64,
794        total_amount: f64,
795        vat_rate: f64,
796        vat_category: VatCategory,
797    ) -> Self {
798        let vat_amount = Self::calculate_vat_amount(total_amount, vat_rate);
799        Self {
800            description: description.into(),
801            quantity,
802            unit_code: unit_code.into(),
803            unit_price,
804            total_amount,
805            vat_rate,
806            vat_amount,
807            vat_category,
808        }
809    }
810
811    /// Create a line item from fully specified amounts.
812    ///
813    /// # Errors
814    /// Returns [`ValidationError`] if totals do not match computed values.
815    pub fn try_from_parts(
816        description: impl Into<String>,
817        quantity: f64,
818        unit_code: impl Into<String>,
819        unit_price: f64,
820        total_amount: f64,
821        vat_rate: f64,
822        vat_amount: f64,
823        vat_category: VatCategory,
824    ) -> std::result::Result<Self, ValidationError> {
825        const EPSILON: f64 = 0.01;
826        let expected_total = Self::calculate_total_amount(quantity, unit_price);
827        let expected_vat = Self::calculate_vat_amount(total_amount, vat_rate);
828
829        let mut issues = Vec::new();
830        if (expected_total - total_amount).abs() > EPSILON {
831            issues.push(ValidationIssue::new(
832                InvoiceField::LineItemTotalAmount,
833                ValidationKind::Mismatch,
834                None,
835            ));
836        }
837        if (expected_vat - vat_amount).abs() > EPSILON {
838            issues.push(ValidationIssue::new(
839                InvoiceField::LineItemVatAmount,
840                ValidationKind::Mismatch,
841                None,
842            ));
843        }
844        if !issues.is_empty() {
845            return Err(ValidationError::new(issues));
846        }
847
848        Ok(Self {
849            description: description.into(),
850            quantity,
851            unit_code: unit_code.into(),
852            unit_price,
853            total_amount,
854            vat_rate,
855            vat_amount,
856            vat_category,
857        })
858    }
859
860    pub fn description(&self) -> &str {
861        &self.description
862    }
863
864    pub fn quantity(&self) -> f64 {
865        self.quantity
866    }
867
868    pub fn unit_code(&self) -> &str {
869        &self.unit_code
870    }
871
872    pub fn unit_price(&self) -> f64 {
873        self.unit_price
874    }
875
876    pub fn total_amount(&self) -> f64 {
877        self.total_amount
878    }
879
880    pub fn vat_rate(&self) -> f64 {
881        self.vat_rate
882    }
883
884    pub fn vat_amount(&self) -> f64 {
885        self.vat_amount
886    }
887
888    pub fn vat_category(&self) -> VatCategory {
889        self.vat_category
890    }
891
892    fn calculate_total_amount(quantity: f64, unit_price: f64) -> f64 {
893        quantity * unit_price
894    }
895
896    fn calculate_vat_amount(total_amount: f64, vat_rate: f64) -> f64 {
897        total_amount * (vat_rate / 100.0)
898    }
899}
900
901/// Collection of line items.
902///
903/// # Examples
904/// ```rust
905/// use fatoora_core::invoice::{LineItem, LineItems, VatCategory};
906///
907/// let items: LineItems = vec![LineItem::new(
908///     "Item",
909///     1.0,
910///     "PCE",
911///     100.0,
912///     15.0,
913///     VatCategory::Standard,
914/// )];
915/// assert_eq!(items.len(), 1);
916/// ```
917pub type LineItems = Vec<LineItem>;
918
919bitflags! {
920    /// Invoice boolean flags packed into a bitset.
921    ///
922    /// # Examples
923    /// ```rust
924    /// use fatoora_core::invoice::InvoiceFlags;
925    ///
926    /// let flags = InvoiceFlags::EXPORT | InvoiceFlags::SELF_BILLED;
927    /// assert!(flags.contains(InvoiceFlags::EXPORT));
928    /// ```
929    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
930    pub struct InvoiceFlags: u8 {
931        const THIRD_PARTY = 0b00001;
932        const NOMINAL = 0b00010;
933        const EXPORT = 0b00100;
934        const SUMMARY = 0b01000;
935        const SELF_BILLED = 0b10000;
936    }
937}
938
939/// Core invoice data model.
940///
941/// Instances are produced by the builder and exposed via views.
942///
943/// # Examples
944/// ```rust,ignore
945/// use fatoora_core::invoice::InvoiceData;
946///
947/// let data: InvoiceData = unimplemented!();
948/// # let _ = data;
949/// ```
950#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
951pub struct InvoiceData {
952    invoice_type: InvoiceType,
953    id: String,
954    uuid: String,
955    issue_datetime: InvoiceTimestamp,
956    currency: CurrencyCode, // currently no separate tax/invoice currency
957    previous_invoice_hash: String,
958    invoice_counter: u64,
959    note: Option<InvoiceNote>,
960    seller: Seller,
961    buyer: Option<Buyer>,
962    line_items: LineItems,
963    payment_means_code: String,
964    vat_category: VatCategory,
965
966    flags: InvoiceFlags,
967
968    invoice_level_charge: f64,
969    invoice_level_discount: f64,
970    allowance_reason: Option<String>,
971}
972
973impl InvoiceData {
974    pub fn invoice_type(&self) -> &InvoiceType {
975        &self.invoice_type
976    }
977
978    pub fn id(&self) -> &str {
979        &self.id
980    }
981
982    pub fn uuid(&self) -> &str {
983        &self.uuid
984    }
985
986    pub fn issue_datetime(&self) -> &InvoiceTimestamp {
987        &self.issue_datetime
988    }
989
990    pub fn currency(&self) -> &CurrencyCode {
991        &self.currency
992    }
993
994    pub fn previous_invoice_hash(&self) -> &str {
995        &self.previous_invoice_hash
996    }
997
998    pub fn invoice_counter(&self) -> u64 {
999        self.invoice_counter
1000    }
1001
1002    pub fn note(&self) -> Option<&InvoiceNote> {
1003        self.note.as_ref()
1004    }
1005
1006    pub fn seller(&self) -> &Seller {
1007        &self.seller
1008    }
1009
1010    pub fn buyer(&self) -> Option<&Buyer> {
1011        self.buyer.as_ref()
1012    }
1013
1014    pub fn line_items(&self) -> &[LineItem] {
1015        &self.line_items
1016    }
1017
1018    pub fn payment_means_code(&self) -> &str {
1019        &self.payment_means_code
1020    }
1021
1022    pub fn vat_category(&self) -> VatCategory {
1023        self.vat_category
1024    }
1025
1026    pub fn flags(&self) -> InvoiceFlags {
1027        self.flags
1028    }
1029
1030    pub fn is_third_party(&self) -> bool {
1031        self.flags.contains(InvoiceFlags::THIRD_PARTY)
1032    }
1033
1034    pub fn is_nominal(&self) -> bool {
1035        self.flags.contains(InvoiceFlags::NOMINAL)
1036    }
1037
1038    pub fn is_export(&self) -> bool {
1039        self.flags.contains(InvoiceFlags::EXPORT)
1040    }
1041
1042    pub fn is_summary(&self) -> bool {
1043        self.flags.contains(InvoiceFlags::SUMMARY)
1044    }
1045
1046    pub fn is_self_billed(&self) -> bool {
1047        self.flags.contains(InvoiceFlags::SELF_BILLED)
1048    }
1049
1050    pub fn invoice_level_charge(&self) -> f64 {
1051        self.invoice_level_charge
1052    }
1053
1054    pub fn invoice_level_discount(&self) -> f64 {
1055        self.invoice_level_discount
1056    }
1057
1058    pub fn allowance_reason(&self) -> Option<&str> {
1059        self.allowance_reason.as_deref()
1060    }
1061
1062    pub(crate) fn seller_name(&self) -> QrResult<&str> {
1063        let name = self.seller.name.trim();
1064        if name.is_empty() {
1065            return Err(QrCodeError::MissingSellerName);
1066        }
1067        Ok(name)
1068    }
1069
1070    pub(crate) fn seller_vat(&self) -> QrResult<&str> {
1071        let vat = self
1072            .seller
1073            .vat_id
1074            .as_ref()
1075            .ok_or(QrCodeError::MissingSellerVat)?
1076            .as_str()
1077            .trim();
1078        if vat.is_empty() {
1079            return Err(QrCodeError::MissingSellerVat);
1080        }
1081        Ok(vat)
1082    }
1083
1084    pub(crate) fn issue_date_string(&self) -> String {
1085        self.issue_datetime.date_str().to_string()
1086    }
1087
1088    pub(crate) fn issue_time_string(&self) -> String {
1089        self.issue_datetime.time_str().to_string()
1090    }
1091
1092    pub(crate) fn format_amount(amount: f64) -> String {
1093        format!("{:.2}", amount)
1094    }
1095}
1096/// Computed invoice totals.
1097///
1098/// # Examples
1099/// ```rust,ignore
1100/// use fatoora_core::invoice::InvoiceTotalsData;
1101///
1102/// let totals: InvoiceTotalsData = unimplemented!();
1103/// let _ = totals.tax_inclusive_amount();
1104/// ```
1105#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1106pub struct InvoiceTotalsData {
1107    line_extension: f64,
1108    tax_amount: f64,
1109    allowance_total: f64,
1110    charge_total: f64,
1111}
1112
1113impl InvoiceTotalsData {
1114    pub(crate) fn from_data(data: &InvoiceData) -> Self {
1115        let line_extension: f64 = data.line_items.iter().map(|li| li.total_amount).sum();
1116        let tax_amount: f64 = data.line_items.iter().map(|li| li.vat_amount).sum();
1117
1118        Self {
1119            line_extension,
1120            tax_amount,
1121            allowance_total: data.invoice_level_discount,
1122            charge_total: data.invoice_level_charge,
1123        }
1124    }
1125
1126    pub fn line_extension(&self) -> f64 {
1127        self.line_extension
1128    }
1129
1130    pub fn tax_amount(&self) -> f64 {
1131        self.tax_amount
1132    }
1133
1134    pub fn allowance_total(&self) -> f64 {
1135        self.allowance_total
1136    }
1137
1138    pub fn charge_total(&self) -> f64 {
1139        self.charge_total
1140    }
1141
1142    pub fn taxable_amount(&self) -> f64 {
1143        self.line_extension - self.allowance_total + self.charge_total
1144    }
1145
1146    pub fn tax_inclusive_amount(&self) -> f64 {
1147        self.taxable_amount() + self.tax_amount
1148    }
1149}