Skip to main content

fatoora_core/
lib.rs

1//! Rust toolkit for ZATCA Phase 1/2 e-invoicing (CSR, signing, validation, QR, and API).
2//!
3//! # Examples
4//! ```rust
5//! use fatoora_core::config::{Config, EnvironmentType};
6//!
7//! let config = Config::new(EnvironmentType::NonProduction);
8//! # let _ = config;
9//! ```
10pub mod api;
11pub mod config;
12pub mod csr;
13pub mod invoice;
14
15/// Stable error kinds used across the core and FFI layers.
16#[repr(i32)]
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum ErrorKind {
19    InvalidInput = 1,
20    Validation = 2,
21    Parse = 3,
22    Xml = 4,
23    Crypto = 5,
24    Io = 6,
25    Network = 7,
26    Unauthorized = 8,
27    Internal = 9,
28    Api = 10,
29}
30
31/// Unified error type for the core library.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Error {
34    kind: ErrorKind,
35    message: String,
36}
37
38impl Error {
39    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
40        Self {
41            kind,
42            message: message.into(),
43        }
44    }
45
46    pub fn kind(&self) -> ErrorKind {
47        self.kind
48    }
49
50    pub fn message(&self) -> &str {
51        &self.message
52    }
53}
54
55impl std::fmt::Display for Error {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        write!(f, "{}", self.message)
58    }
59}
60
61impl std::error::Error for Error {}
62
63impl From<config::EnvironmentParseError> for Error {
64    fn from(err: config::EnvironmentParseError) -> Self {
65        Error::new(ErrorKind::InvalidInput, err.to_string())
66    }
67}
68
69impl From<csr::CsrError> for Error {
70    fn from(err: csr::CsrError) -> Self {
71        let kind = match err {
72            csr::CsrError::Io { .. } => ErrorKind::Io,
73            csr::CsrError::PropertiesRead { .. } => ErrorKind::Parse,
74            csr::CsrError::MissingProperty { .. } => ErrorKind::InvalidInput,
75            csr::CsrError::InvalidSubject { .. } => ErrorKind::InvalidInput,
76            csr::CsrError::InvalidSan { .. } => ErrorKind::InvalidInput,
77            csr::CsrError::RequestBuild { .. } => ErrorKind::Crypto,
78            csr::CsrError::AddExtension { .. } => ErrorKind::Crypto,
79            csr::CsrError::CsrBuild { .. } => ErrorKind::Crypto,
80            csr::CsrError::DerEncode { .. } => ErrorKind::Crypto,
81            csr::CsrError::KeyDecode { .. } => ErrorKind::InvalidInput,
82            csr::CsrError::KeyEncode { .. } => ErrorKind::Crypto,
83            csr::CsrError::Validation { .. } => ErrorKind::Validation,
84        };
85        Error::new(kind, err.to_string())
86    }
87}
88
89impl From<invoice::InvoiceError> for Error {
90    fn from(err: invoice::InvoiceError) -> Self {
91        let kind = match err {
92            invoice::InvoiceError::Validation(_) => ErrorKind::Validation,
93            invoice::InvoiceError::InvalidCountryCode(_)
94            | invoice::InvoiceError::InvalidCurrencyCode(_)
95            | invoice::InvoiceError::InvalidTimestamp(_)
96            | invoice::InvoiceError::InvalidIssueDate(_)
97            | invoice::InvoiceError::MissingVatForSeller
98            | invoice::InvoiceError::MissingBuyerId
99            | invoice::InvoiceError::InvalidVatFormat => ErrorKind::InvalidInput,
100        };
101        Error::new(kind, err.to_string())
102    }
103}
104
105impl From<invoice::sign::SigningError> for Error {
106    fn from(err: invoice::sign::SigningError) -> Self {
107        Error::new(ErrorKind::Crypto, err.to_string())
108    }
109}
110
111impl From<invoice::QrCodeError> for Error {
112    fn from(err: invoice::QrCodeError) -> Self {
113        let kind = match err {
114            invoice::QrCodeError::MissingSellerName
115            | invoice::QrCodeError::MissingSellerVat
116            | invoice::QrCodeError::ValueTooLong { .. }
117            | invoice::QrCodeError::EncodedTooLong { .. } => ErrorKind::InvalidInput,
118            invoice::QrCodeError::Xml(_) => ErrorKind::Xml,
119        };
120        Error::new(kind, err.to_string())
121    }
122}
123
124impl From<invoice::xml::InvoiceXmlError> for Error {
125    fn from(err: invoice::xml::InvoiceXmlError) -> Self {
126        Error::new(ErrorKind::Xml, err.to_string())
127    }
128}
129
130impl From<invoice::xml::parse::ParseError> for Error {
131    fn from(err: invoice::xml::parse::ParseError) -> Self {
132        let kind = match err {
133            invoice::xml::parse::ParseError::XmlParse(_) => ErrorKind::Xml,
134            invoice::xml::parse::ParseError::XPath(_) => ErrorKind::Parse,
135            invoice::xml::parse::ParseError::MissingField(_)
136            | invoice::xml::parse::ParseError::InvalidValue { .. } => ErrorKind::InvalidInput,
137        };
138        Error::new(kind, err.to_string())
139    }
140}
141
142impl From<invoice::validation::XmlValidationError> for Error {
143    fn from(err: invoice::validation::XmlValidationError) -> Self {
144        let kind = match err {
145            invoice::validation::XmlValidationError::InvalidXsdPath { .. } => ErrorKind::InvalidInput,
146            invoice::validation::XmlValidationError::SchemaParse { .. } => ErrorKind::Parse,
147            invoice::validation::XmlValidationError::XmlParse { .. } => ErrorKind::Xml,
148            invoice::validation::XmlValidationError::SchemaValidation { .. } => ErrorKind::Validation,
149        };
150        Error::new(kind, err.to_string())
151    }
152}
153
154impl From<api::ZatcaError> for Error {
155    fn from(err: api::ZatcaError) -> Self {
156        let kind = match err {
157            api::ZatcaError::NetworkError(_) => ErrorKind::Network,
158            api::ZatcaError::InvalidResponse(_) => ErrorKind::Parse,
159            api::ZatcaError::Unauthorized(_) => ErrorKind::Unauthorized,
160            api::ZatcaError::ServerError(_) => ErrorKind::Api,
161            api::ZatcaError::Http(_) => ErrorKind::Network,
162            api::ZatcaError::ClientState(_) => ErrorKind::Internal,
163        };
164        Error::new(kind, err.to_string())
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::{Error, ErrorKind};
171    use crate::{
172        api::ZatcaError,
173        csr::CsrError,
174        invoice::{
175            InvoiceError, QrCodeError, ValidationError, ValidationIssue, ValidationKind, InvoiceField,
176        },
177    };
178    use crate::invoice::sign::SigningError;
179    use crate::invoice::xml::InvoiceXmlError;
180    use crate::invoice::xml::parse::ParseError;
181    use crate::invoice::validation::XmlValidationError;
182    use quick_xml::se::SeError;
183
184    #[test]
185    fn error_conversions_cover_variants() {
186        let invoice_err = InvoiceError::Validation(ValidationError::new(vec![
187            ValidationIssue::new(InvoiceField::Id, ValidationKind::Missing, None),
188        ]));
189        let err: Error = invoice_err.into();
190        assert_eq!(err.kind(), ErrorKind::Validation);
191
192        let err: Error = SigningError::SigningError("sign".into()).into();
193        assert_eq!(err.kind(), ErrorKind::Crypto);
194
195        let err: Error = QrCodeError::MissingSellerName.into();
196        assert_eq!(err.kind(), ErrorKind::InvalidInput);
197
198        let xml_err = InvoiceXmlError::Serialize {
199            source: SeError::Custom("xml".into()),
200        };
201        let err: Error = xml_err.into();
202        assert_eq!(err.kind(), ErrorKind::Xml);
203
204        let err: Error = ParseError::MissingField("uuid").into();
205        assert_eq!(err.kind(), ErrorKind::InvalidInput);
206
207        let err: Error = XmlValidationError::XmlParse {
208            message: "bad".into(),
209        }
210        .into();
211        assert_eq!(err.kind(), ErrorKind::Xml);
212
213        let err: Error = ZatcaError::ClientState("state".into()).into();
214        assert_eq!(err.kind(), ErrorKind::Internal);
215
216        let err: Error = CsrError::Validation {
217            message: "csr".into(),
218        }
219        .into();
220        assert_eq!(err.kind(), ErrorKind::Validation);
221    }
222}
223
224#[cfg(test)]
225mod fixture_hash_sign;