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}