quick_oxibooks/
error.rs

1//! Error types for `QuickBooks` API operations.
2
3use quickbooks_types::QBTypeError;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use crate::batch::{QBBatchOperation, QBBatchResponseData};
8
9/// The main error type for all `QuickBooks` API operations.
10///
11/// `APIError` is a wrapper around [`APIErrorInner`] that provides a consistent
12/// error interface for all operations in the library. It implements standard
13/// error traits and can be converted from various underlying error types.
14///
15/// # Examples
16///
17/// ```no_run
18/// use quick_oxibooks::error::APIError;
19/// use quick_oxibooks::{QBContext, Environment};
20/// use ureq::Agent;
21///
22/// fn create_context() -> Result<QBContext, APIError> {
23///     let client = Agent::new_with_defaults();
24///     QBContext::new_from_env(Environment::SANDBOX, &client)
25/// }
26///
27/// let _ = match create_context() {
28///     Ok(_context) => { println!("Context created successfully"); Ok(()) }
29///     Err(e) => { eprintln!("Error: {}", e); Err(e) }
30/// };
31/// ```
32///
33/// # Error Conversion
34///
35/// Many error types automatically convert to `APIError`:
36/// - Network errors from `ureq`
37/// - JSON parsing errors from `serde_json`
38/// - QuickBooks-specific validation errors
39/// - Environment variable errors
40///
41/// # Error Handling Patterns
42///
43/// ```no_run
44/// use quick_oxibooks::error::{APIError, APIErrorInner};
45/// use quick_oxibooks::functions::create::QBCreate;
46/// use quickbooks_types::{Customer, QBItem};
47///
48/// fn handle_customer_creation(customer: &Customer, qb_context: &quick_oxibooks::QBContext, client: &ureq::Agent) {
49///     match customer.create(qb_context, client) {
50///         Ok(created) => println!("Created: {:?}", created.id()),
51///         Err(e) => {
52///             match &*e {
53///                 APIErrorInner::CreateMissingItems => {
54///                     eprintln!("Customer missing required fields");
55///                 }
56///                 APIErrorInner::BadRequest(_qb_error) => {
57///                     eprintln!("QuickBooks rejected the request");
58///                 }
59///                 _ => eprintln!("Other error: {}", e),
60///             }
61///         }
62///     }
63/// }
64/// ```
65#[derive(Debug)]
66pub struct APIError(Box<APIErrorInner>);
67
68impl std::fmt::Display for APIError {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        write!(f, "{}", self.0)
71    }
72}
73
74impl std::error::Error for APIError {
75    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
76        self.0.source()
77    }
78}
79
80impl std::ops::Deref for APIError {
81    type Target = APIErrorInner;
82    fn deref(&self) -> &Self::Target {
83        &self.0
84    }
85}
86
87impl<T> From<T> for APIError
88where
89    T: Into<APIErrorInner>,
90{
91    fn from(err: T) -> Self {
92        APIError(Box::new(err.into()))
93    }
94}
95
96/// Detailed error types for `QuickBooks` API operations.
97///
98/// This enum contains all the specific error conditions that can occur when
99/// interacting with the `QuickBooks` API. Each variant represents a different
100/// category of failure with appropriate context information.
101///
102/// # Error Categories
103///
104/// ## Network and HTTP Errors
105/// - [`UreqError`](APIErrorInner::UreqError): HTTP client errors (network, timeout, etc.)
106/// - [`HttpError`](APIErrorInner::HttpError): HTTP protocol errors (malformed requests, etc.)
107/// - [`IoError`](APIErrorInner::IoError): I/O errors (file operations, etc.)
108///
109/// ## API Response Errors
110/// - [`BadRequest`](APIErrorInner::BadRequest): `QuickBooks` API returned an error response
111/// - [`InvalidClient`](APIErrorInner::InvalidClient): Authentication/authorization failures
112/// - [`ThrottleLimitReached`](APIErrorInner::ThrottleLimitReached): Rate limit exceeded
113///
114/// ## Data Validation Errors
115/// - [`QBTypeError`](APIErrorInner::QBTypeError): Entity validation failures
116/// - [`CreateMissingItems`](APIErrorInner::CreateMissingItems): Required fields missing for creation
117/// - [`DeleteMissingItems`](APIErrorInner::DeleteMissingItems): ID/sync token missing for deletion
118/// - [`NoIdOnRead`](APIErrorInner::NoIdOnRead): Entity missing ID for read operation
119///
120/// ## Query and Operation Errors
121/// - [`BatchRequestMissingItems`](APIErrorInner::BatchRequestMissingItems): Batch operation failures
122/// - [`BatchLimitExceeded`](APIErrorInner::BatchLimitExceeded): Too many items in batch request
123///
124/// ## File and Attachment Errors
125/// - [`NoAttachableObjects`](APIErrorInner::NoAttachableObjects): No attachments in upload response
126/// - [`InvalidFile`](APIErrorInner::InvalidFile): Invalid file name or extension
127/// - [`ByteLengthMismatch`](APIErrorInner::ByteLengthMismatch): File write operation incomplete
128///
129/// ## PDF Generation Errors
130/// - [`NoIdOnGetPDF`](APIErrorInner::NoIdOnGetPDF): Entity missing ID for PDF generation
131/// - [`NoIdOnSend`](APIErrorInner::NoIdOnSend): Entity missing ID for email send operation
132///
133/// ## Configuration Errors
134/// - [`EnvVarError`](APIErrorInner::EnvVarError): Missing or invalid environment variables
135/// - [`JsonError`](APIErrorInner::JsonError): JSON parsing/serialization errors
136///
137/// # Examples
138///
139/// ```rust
140/// use quick_oxibooks::error::{APIError, APIErrorInner};
141/// use quickbooks_types::Customer;
142///
143/// fn handle_specific_errors(result: Result<Customer, APIError>) {
144///     match result {
145///         Ok(customer) => println!("Success: {:?}", customer.id),
146///         Err(e) => {
147///             match &*e {
148///                 APIErrorInner::CreateMissingItems => {
149///                     eprintln!("Please provide required fields like display_name");
150///                 }
151///                 APIErrorInner::ThrottleLimitReached => {
152///                     eprintln!("Rate limit hit, please wait before retrying");
153///                 }
154///                 APIErrorInner::BadRequest(qb_error) => {
155///                     eprintln!("QuickBooks error: {:?}", qb_error);
156///                 }
157///                 _ => eprintln!("Other error: {}", e),
158///             }
159///         }
160///     }
161/// }
162/// ```
163// TODO Split this into multiple error types, currently all errors are lumped into one enum
164#[derive(Debug, thiserror::Error)]
165pub enum APIErrorInner {
166    // #[cfg(any(feature = "attachments", feature = "pdf"))]
167    // #[error(transparent)]
168    // TokioIoError(#[from] tokio::io::Error),
169    #[error("Error on Ureq Request: {0}")]
170    UreqError(#[from] ureq::Error),
171    #[error("HTTP Error: {0}")]
172    HttpError(#[from] ureq::http::Error),
173    #[error(transparent)]
174    IoError(#[from] std::io::Error),
175    #[error("Bad request: {0}")]
176    BadRequest(QBErrorResponse),
177    #[error(transparent)]
178    JsonError(#[from] serde_json::Error),
179    #[error(transparent)]
180    QBTypeError(#[from] QBTypeError),
181    #[error("Invalid Client! Try re-authenticating")]
182    InvalidClient,
183    #[error("Trying to update an object when it doesn't have an ID set")]
184    NoIdOnRead,
185    #[error("Trying to send object email when it doesn't have an ID set")]
186    NoIdOnSend,
187    #[error("Missing objects when trying to create item")]
188    CreateMissingItems,
189    #[error("Can't delete objects without ID or SyncToken")]
190    DeleteMissingItems,
191    #[error("Missing ID when trying to get PDF of object")]
192    NoIdOnGetPDF,
193    #[error("Couldn't write all the bytes of file")]
194    ByteLengthMismatch,
195    #[error("Missing Attachable object on upload response")]
196    NoAttachableObjects,
197    #[error("Throttle limit reached")]
198    ThrottleLimitReached,
199    #[error("Batch limit exceeded")]
200    BatchLimitExceeded,
201    #[error("Env Var error : {0}")]
202    EnvVarError(#[from] std::env::VarError),
203    #[error("Invalid Batch Response, Missing items for : {0}")]
204    BatchRequestMissingItems(BatchMissingItemsError),
205    #[error("Invalid File name or extenstion : {0}")]
206    InvalidFile(String),
207}
208
209/// Error type for missing items in batch requests.
210#[derive(Debug, thiserror::Error)]
211pub struct BatchMissingItemsError {
212    pub items: std::collections::HashMap<String, crate::batch::QBBatchOperation>,
213    pub results: Vec<(QBBatchOperation, QBBatchResponseData)>,
214}
215
216impl std::fmt::Display for BatchMissingItemsError {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        write!(
219            f,
220            "BatchRequestMissingItems : {{ Missing Items: {:#?}, \n Results : {:#?} }}",
221            self.items, self.results
222        )
223    }
224}
225
226impl Serialize for APIError {
227    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
228    where
229        S: serde::Serializer,
230    {
231        serializer.serialize_str(self.to_string().as_str())
232    }
233}
234
235/// Represents a single error returned by the `QuickBooks` API.
236///
237/// This is currently not a strongly typed structure, as the `QuickBooks` API
238/// does not provide detailed documentation on the error fields.
239#[derive(Serialize, Deserialize, Debug)]
240pub struct QBError {
241    #[serde(alias = "Message")]
242    pub message: String,
243    pub code: String,
244    #[serde(alias = "Detail")]
245    pub detail: Option<String>,
246    pub element: Option<String>,
247}
248
249/// Represents the type of fault returned by the `QuickBooks` API.
250#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
251pub enum FaultType {
252    #[serde(alias = "AUTHENTICATION")]
253    Authentication,
254    #[serde(rename = "ValidationFault")]
255    Validation,
256    #[serde(rename = "SystemFault")]
257    System,
258    // TODO Add the rest of the fault types, if found or needed
259    #[serde(untagged)]
260    Other(String),
261}
262
263/// Represents a fault returned by the `QuickBooks` API.
264#[derive(Serialize, Deserialize, Debug)]
265pub struct Fault {
266    pub r#type: FaultType,
267    #[serde(alias = "Error")]
268    pub error: Vec<QBError>,
269}
270
271/// Represents an error response returned by the `QuickBooks` API.
272// TODO Make the fields more strongly typed, currently no documentation on the error types that I can find
273#[derive(Serialize, Deserialize, Debug, Default)]
274#[serde(rename_all = "camelCase")]
275pub struct QBErrorResponse {
276    pub warnings: Option<Value>,
277    pub intuit_object: Option<Value>,
278    #[serde(alias = "Fault")]
279    pub fault: Option<Fault>,
280    pub report: Option<Value>,
281    pub sync_error_response: Option<Value>,
282    pub query_response: Option<Vec<Value>>,
283    pub batch_item_response: Option<Vec<Value>>,
284    pub request_id: Option<String>,
285    pub status: Option<String>,
286    #[serde(rename = "cdcresponse")]
287    pub cdc_response: Option<Vec<Value>>,
288}
289
290impl std::fmt::Display for QBErrorResponse {
291    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292        write!(
293            f,
294            "{}",
295            serde_json::to_string_pretty(self)
296                .expect("Could not serialize QBErrorResponse for display!")
297        )
298    }
299}