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*/