Skip to main content

quickbooks_types/
lib.rs

1//! QuickBooks Online type models and helpers for Rust.
2//!
3//! This crate defines strongly-typed data models for common QBO entities and reports,
4//! plus helper traits that validate local preconditions (for example, `can_create` and `can_full_update`).
5/*! It does not make HTTP requests; bring your own client. */
6//!
7//! Modules and exports:
8//! - Top-level entities: `Account`, `Attachable`, `Bill`, `BillPayment`, `CompanyInfo`, `Customer`, `Employee`, `Estimate`, `Invoice`, `Item`, `Payment`, `Preferences`, `SalesReceipt`, `Vendor`
9//! - `common`: supporting types like `NtRef`, `MetaData`, addresses, phones, taxes, etc.
10//! - `reports`: report models and strongly-typed parameter builders
11//!
12//! Features:
13//! - `builder`: derive builders and add an associated `new()` for most entities
14//! - `polars`: optional helpers for reports + Polars integration
15//!
16//! Quick start (entities):
17//! ```no_run
18//! use chrono::NaiveDate;
19//! use crate::{Invoice, Line, LineDetail, SalesItemLineDetail, QBCreatable};
20//! use crate::common::NtRef;
21//!
22//! let invoice = Invoice {
23//!     customer_ref: Some(NtRef::from(("John Doe", "CUST-123"))),
24//!     txn_date: NaiveDate::from_ymd_opt(2024, 10, 1),
25
26//!     line: Some(vec![
27
28//!         Line {
29//!             amount: Some(100.0),
30//!             line_detail: LineDetail::SalesItemLineDetail(SalesItemLineDetail {
31//!                 item_ref: Some(NtRef::from(("Widget A", "ITEM-001"))),
32//!                 qty: Some(1.0),
33//!                 unit_price: Some(100.0),
34//!                 ..Default::default()
35//!             }),
36//!             ..Default::default()
37//!         }
38//!     ]),
39//!     ..Default::default()
40//! };
41//! assert!(invoice.can_create());
42//! ```
43//!
44//! Reports parameters:
45//! ```no_run
46//! use chrono::NaiveDate;
47//! use crate::reports::types::*;
48//! use crate::reports::params::*;
49//!
50//! let params = BalanceSheetParams::new()
51//!     .accounting_method(AccountingMethod::Cash)
52//!     .start_date(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap())
53//!     .end_date(NaiveDate::from_ymd_opt(2024, 12, 31).unwrap())
54//!     .date_macro(DateMacro::ThisFiscalYear);
55//! let query = params.to_query_string();
56//! assert!(query.contains("accounting_method=Cash"));
57//! ```
58
59#[cfg(feature = "builder")]
60#[macro_use]
61extern crate derive_builder;
62
63mod error;
64mod models;
65pub mod reports;
66use std::fmt::{Debug, Display};
67
68pub use error::*;
69use models::common::{MetaData, NtRef};
70pub use models::*;
71use serde::{de::DeserializeOwned, Serialize};
72
73/// Core trait for all `QuickBooks` entities.
74///
75/// This trait defines the fundamental interface that all `QuickBooks` entities must implement.
76/// It provides access to common fields like ID, sync token, and metadata that are present
77/// on all `QuickBooks` objects, as well as type information for API operations.
78///
79/// # Required Methods
80///
81/// - `id()`: Returns the entity's unique identifier
82/// - `clone_id()`: Returns a cloned copy of the ID
83/// - `sync_token()`: Returns the synchronization token for updates
84/// - `meta_data()`: Returns metadata about the entity
85/// - `name()`: Returns the entity type name for API calls
86/// - `qb_id()`: Returns the lowercase entity identifier for URLs
87///
88/// # Default Methods
89///
90/// - `has_read()`: Returns true if the entity has both ID and sync token (indicates it was read from QB)
91///
92/// # Examples
93///
94/// ```no_run
95/// use quickbooks_types::{QBItem, Customer};
96///
97/// let customer = Customer::default();
98///
99/// // Check if entity has been read from QuickBooks
100/// if customer.has_read() {
101///     println!("Customer ID: {:?}", customer.id());
102///     println!("Sync Token: {:?}", customer.sync_token());
103/// }
104///
105/// // Get type information
106/// println!("Entity name: {}", Customer::name()); // "Customer"
107/// println!("API identifier: {}", Customer::qb_id()); // "customer"
108/// ```
109pub trait QBItem: Serialize + Default + Clone + Sized + DeserializeOwned + Debug + Send {
110    fn id(&self) -> Option<&String>;
111    fn clone_id(&self) -> Option<String>;
112    fn sync_token(&self) -> Option<&String>;
113    fn meta_data(&self) -> Option<&MetaData>;
114    fn name() -> &'static str;
115    fn qb_id() -> &'static str;
116    fn has_read(&self) -> bool {
117        self.id().is_some() && self.sync_token().is_some()
118    }
119}
120
121/// Macro to apply a given macro to each QuickBooks entity type.
122#[macro_export]
123macro_rules! for_each_qb_item {
124    ($func:ident) => {
125        $func!(Invoice);
126        $func!(Vendor);
127        $func!(Payment);
128        $func!(Item);
129        $func!(Estimate);
130        $func!(Employee);
131        $func!(Customer);
132        $func!(Class);
133        $func!(CompanyInfo);
134        $func!(Bill);
135        $func!(Attachable);
136        $func!(Account);
137        $func!(Preferences);
138        $func!(SalesReceipt);
139        $func!(BillPayment);
140        $func!(TaxCode);
141        $func!(TaxRate);
142        $func!(Term);
143    };
144}
145
146macro_rules! impl_qb_data {
147    ($x:ident) => {
148        #[cfg(feature = "builder")]
149        paste::paste! {
150            #[allow(clippy::new_ret_no_self)]
151            impl [<$x>] {
152                #[must_use] pub fn new() -> [<$x Builder>] {
153                    [<$x Builder>]::default()
154                }
155            }
156        }
157
158        impl QBItem for $x {
159            fn id(&self) -> Option<&String> {
160                self.id.as_ref()
161            }
162
163            fn clone_id(&self) -> Option<String> {
164                self.id.clone()
165            }
166
167            fn sync_token(&self) -> Option<&String> {
168                self.sync_token.as_ref()
169            }
170
171            fn meta_data(&self) -> Option<&MetaData> {
172                self.meta_data.as_ref()
173            }
174
175            #[inline]
176            fn name() -> &'static str {
177                stringify!($x)
178            }
179
180            #[inline]
181            fn qb_id() -> &'static str {
182                paste::paste! {
183                    stringify!([<$x:lower>])
184                }
185            }
186        }
187
188        impl Display for $x {
189            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190                write!(
191                    f,
192                    "{} : {}",
193                    Self::name(),
194                    serde_json::to_string_pretty(self)
195                        .expect("Could not serialize object for display!")
196                )
197            }
198        }
199    };
200}
201
202for_each_qb_item!(impl_qb_data);
203
204/// Trait for entities that can be created in `QuickBooks`.
205///
206/// This trait defines the validation logic for determining whether an entity
207/// has the required fields to be successfully created via the `QuickBooks` API.
208/// Each entity implements its own validation rules based on `QuickBooks` requirements.
209///
210/// # Required Methods
211///
212/// - `can_create()`: Returns true if the entity has all required fields for creation
213///
214/// # Examples
215///
216/// ```no_run
217/// use quickbooks_types::{Customer, QBCreatable};
218///
219/// let mut customer = Customer::default();
220///
221/// // Check if customer can be created (will be false - missing required fields)
222/// assert!(!customer.can_create());
223///
224/// // Add required field
225/// customer.display_name = Some("John Doe".to_string());
226///
227/// // Now it can be created
228/// assert!(customer.can_create());
229/// ```
230///
231/// # Implementation Notes
232///
233/// Different entities have different creation requirements:
234/// - **Customer/Vendor**: Requires `display_name` or individual name components
235/// - **Account**: Requires name and `account_type` or `account_sub_type`
236/// - **Invoice**: Requires customer reference and line items
237/// - **Item**: Requires name and type
238pub trait QBCreatable {
239    fn can_create(&self) -> bool;
240}
241
242/// Trait for entities that can be read from `QuickBooks` by ID.
243///
244/// This trait is automatically implemented for all [`QBItem`] types and provides
245/// the ability to read entities from `QuickBooks` using their unique identifier.
246///
247/// # Default Implementation
248///
249/// The default implementation checks if the entity has an ID field, which is
250/// required for read operations.
251///
252/// # Examples
253///
254/// ```no_run
255/// use quickbooks_types::{Customer, QBReadable};
256///
257/// let mut customer = Customer::default();
258/// customer.id = Some("123".to_string());
259///
260/// // Can read because it has an ID
261/// assert!(customer.can_read());
262/// ```
263pub trait QBReadable: QBItem {
264    fn can_read(&self) -> bool;
265}
266
267impl<T: QBItem> QBReadable for T {
268    fn can_read(&self) -> bool {
269        self.id().is_some()
270    }
271}
272
273/// Trait for entities that can be queried from `QuickBooks`.
274///
275/// This trait is automatically implemented for all [`QBItem`] types and indicates
276/// that the entity supports `QuickBooks` SQL-like query operations.
277///
278/// # Examples
279///
280/// ```no_run
281/// use quickbooks_types::{Customer, QBQueryable};
282///
283/// // All QBItem types automatically implement QBQueryable
284/// let customer = Customer::default();
285/// ```
286pub trait QBQueryable: QBItem {}
287impl<T: QBItem> QBQueryable for T {}
288
289/// Trait for entities that can be deleted from `QuickBooks`.
290///
291/// This trait is automatically implemented for all [`QBItem`] types and provides
292/// validation for delete operations. Entities must have been read from `QuickBooks`
293/// (have both ID and sync token) to be deletable.
294///
295/// # Default Implementation
296///
297/// The default implementation uses [`QBItem::has_read()`] to verify the entity
298/// has both an ID and sync token, which are required for delete operations.
299///
300/// # Examples
301///
302/// ```no_run
303/// use quickbooks_types::{Invoice, QBDeletable};
304///
305/// let mut invoice = Invoice::default();
306/// invoice.id = Some("123".to_string());
307/// invoice.sync_token = Some("2".to_string());
308///
309/// // Can delete because it has both ID and sync token
310/// assert!(invoice.can_delete());
311/// ```
312pub trait QBDeletable: QBItem {
313    fn can_delete(&self) -> bool {
314        self.has_read()
315    }
316}
317
318/// Trait for entities that can be voided in `QuickBooks`.
319///
320/// Voiding is a special operation in `QuickBooks` that marks transactions as void
321/// while preserving them for audit purposes. Only certain entities support voiding.
322///
323/// # Default Implementation
324///
325/// The default implementation requires that the entity has been read from `QuickBooks`
326/// (has both ID and sync token).
327///
328/// # Supported Entities
329///
330/// Typically includes: Invoice, Payment, Bill, Check, `SalesReceipt`, and other transactional entities.
331///
332/// # Examples
333///
334/// ```no_run
335/// use quickbooks_types::{Invoice, QBVoidable};
336///
337/// let mut invoice = Invoice::default();
338/// invoice.id = Some("123".to_string());
339/// invoice.sync_token = Some("2".to_string());
340///
341/// // Can void because it has been read from QuickBooks
342/// assert!(invoice.can_void());
343/// ```
344pub trait QBVoidable: QBItem {
345    fn can_void(&self) -> bool {
346        self.has_read()
347    }
348}
349
350/// Trait for entities that support full update operations.
351///
352/// Full updates require sending the complete entity data to `QuickBooks`,
353/// replacing all fields with the provided values. This is in contrast to
354/// sparse updates which only update specified fields.
355///
356/// # Required Methods
357///
358/// - `can_full_update()`: Returns true if the entity can be fully updated
359///
360/// # Implementation Notes
361///
362/// Typically requires:
363/// - Entity has been read from `QuickBooks` (has ID and sync token)
364/// - Entity meets creation requirements (has required fields)
365/// - Some entities may have additional validation rules
366///
367/// # Examples
368///
369/// ```no_run
370/// use quickbooks_types::{Customer, QBFullUpdatable};
371///
372/// let mut customer = Customer::default();
373/// customer.id = Some("123".to_string());
374/// customer.sync_token = Some("2".to_string());
375/// customer.display_name = Some("John Doe".to_string());
376///
377/// // Check if can be fully updated
378/// if customer.can_full_update() {
379///     // Proceed with full update
380/// }
381/// ```
382pub trait QBFullUpdatable {
383    fn can_full_update(&self) -> bool;
384}
385
386/// Trait for entities that support sparse update operations.
387///
388/// Sparse updates allow updating only specific fields of an entity without
389/// affecting other fields. This is more efficient and safer than full updates
390/// when you only need to change specific values.
391///
392/// # Required Methods
393///
394/// - `can_sparse_update()`: Returns true if the entity can be sparse updated
395///
396/// # Implementation Notes
397///
398/// Typically requires:
399/// - Entity can perform full updates
400/// - Entity has the `sparse` field set to `true`
401/// - `QuickBooks` API supports sparse updates for this entity type
402///
403/// # Examples
404///
405/// ```no_run
406/// use quickbooks_types::{Customer, QBSparseUpdateable};
407///
408/// let mut customer = Customer::default();
409/// customer.id = Some("123".to_string());
410/// customer.sync_token = Some("2".to_string());
411/// customer.display_name = Some("John Doe".to_string());
412/// customer.sparse = Some(true);
413///
414/// // Check if can be sparse updated
415/// if customer.can_sparse_update() {
416///     // Proceed with sparse update
417/// }
418/// ```
419pub trait QBSparseUpdateable {
420    fn can_sparse_update(&self) -> bool;
421}
422
423/// Trait for entities that can be sent via email from `QuickBooks`.
424///
425/// This trait marks entities that support `QuickBooks`' built-in email functionality,
426/// such as sending invoices or estimates to customers via email.
427///
428/// # Supported Entities
429///
430/// Typically includes: Invoice, Estimate, `SalesReceipt`, and other customer-facing documents.
431pub trait QBSendable {}
432
433/// Trait for entities that can be generated as PDF documents.
434///
435/// This trait marks entities that support `QuickBooks`' PDF generation functionality,
436/// allowing you to retrieve formatted PDF versions of documents.
437///
438/// # Supported Entities
439///
440/// Typically includes: Invoice, Estimate, `SalesReceipt`, Statement, and other printable documents.
441pub trait QBPDFable {}
442
443/// Trait for entities that can be converted to `QuickBooks` entity references.
444///
445/// Entity references (`NtRef`) are used throughout `QuickBooks` to link entities together.
446/// For example, an invoice has a customer reference that points to a specific customer.
447///
448/// # Required Methods
449///
450/// - `to_ref()`: Converts the entity to an `NtRef` for use in other entities
451///
452/// # Returns
453///
454/// Returns a `Result<NtRef, QBTypeError>` where:
455/// - `Ok(NtRef)` if the entity can be referenced (has ID and name field)
456/// - `Err(QBTypeError::QBToRefError)` if the entity cannot be referenced
457///
458/// # Examples
459///
460/// ```no_run
461/// use quickbooks_types::{Invoice, Customer, QBToRef};
462///
463/// let mut customer = Customer::default();
464/// customer.id = Some("123".to_string());
465/// customer.display_name = Some("John Doe".to_string());
466///
467/// // Convert to reference for use in other entities
468/// let customer_ref = customer.to_ref().unwrap();
469///
470/// // Use the reference in an invoice
471/// let mut invoice = Invoice::default();
472/// invoice.customer_ref = Some(customer_ref);
473/// ```
474pub trait QBToRef: QBItem {
475    fn to_ref(&self) -> Result<NtRef, QBTypeError>;
476}
477
478macro_rules! impl_qb_to_ref {
479  ($($struct:ident {$name_field:ident}),+) => {
480    $(
481      impl QBToRef for $struct {
482        fn to_ref(&self) -> Result<NtRef, $crate::QBTypeError> {
483          if self.id.is_some() {
484            Ok(NtRef {
485              entity_ref_type: Some(Self::name().into()),
486              name: self.$name_field.clone(),
487              value: self.id.clone()
488            })
489          } else {
490            Err($crate::QBTypeError::QBToRefError)
491          }
492        }
493      }
494    )+
495  }
496}
497
498impl_qb_to_ref!(
499    Account {
500        fully_qualified_name
501    },
502    Attachable { file_name },
503    Invoice { doc_number },
504    SalesReceipt { doc_number },
505    Item { name },
506    Customer { display_name },
507    Vendor { display_name }
508);
509
510/*
511Create: ✓
512- Account
513- Attachable
514- Bill
515- Customer
516- Employee
517- Estimate
518- Invoice
519- Item (Category)
520- Payment
521- Sales Receipt
522- Vendor
523Read: ✓
524- Attachable
525- Account
526- Bill
527- CompanyInfo
528- Customer
529- Employee
530- Estimate
531- Invoice
532- Item (Category, Bundle)
533- Preferences
534- Sales Receipt
535- Vendor
536Query: ✓
537- Attachable
538- Account
539- Bill
540- CompanyInfo
541- Customer
542- Employee
543- Estimate
544- Invoice
545- Item (Category, Bundle)
546- Payment
547- Preferences
548- Sales Receipt
549- Vendor
550Delete: ✓
551- Attachable
552- Bill
553- Estimate
554- Invoice
555- Payment
556- Sales Receipt
557Void: ✓
558- Invoice
559- Payment
560- Sales Receipt
561Full Update: ✓
562- Account
563- Attachable
564- Bill
565- CompanyInfo
566- Customer
567- Employee
568- Estimate
569- Invoice
570- Item (Category)
571- Payment
572- Preferences
573- Sales Receipt
574- Vendor
575Sparse Update: ✓
576- CompanyInfo
577- Customer
578- Estimate
579- Invoice
580- Sales Receipt
581Send: ✓
582- Estimate
583- Invoice
584- Payment
585- Sales Receipt
586Get as PDF: ✓
587- Estimate
588- Invoice
589- Payment
590- Sales Receipt
591
592- Attachment has three other actions that are unique
593- Upload ✓
594
595*/