Skip to main content

quickbooks_types/models/
attachable.rs

1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4use serde_with::skip_serializing_none;
5
6use super::common::{CustomField, MetaData, NtRef};
7use crate::{QBCreatable, QBDeletable, QBFullUpdatable, QBItem, QBToRef, QBTypeError};
8
9#[skip_serializing_none]
10#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
11#[serde(rename_all = "PascalCase", default)]
12#[cfg_attr(
13    feature = "builder",
14    derive(Builder),
15    builder(default, build_fn(error = "QBTypeError"), setter(into, strip_option))
16)]
17
18/// Attachable
19///
20/// Represents a file attachment or note that can be linked to other `QuickBooks` entities (for example: Invoice, Bill, Customer).
21///
22/// Notes:
23/// - With the "builder" feature enabled, `Attachable::new().file(path)` sets `file_path` for upload and derives `file_name` and `content_type` from the path.
24/// - This crate models metadata only; HTTP upload logic and file bytes handling should be implemented in your client.
25///
26/// API reference:
27/// <https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/attachable>
28pub struct Attachable {
29    /// The unique ID of the entity
30    pub id: Option<String>,
31    /// The unique sync token of the entity, used for concurrency control
32    pub sync_token: Option<String>,
33    /// Metadata about the entity
34    #[serde(skip_serializing)]
35    pub meta_data: Option<MetaData>,
36    /// File name of the attachment
37    #[cfg_attr(feature = "builder", builder(setter(custom)))]
38    pub file_name: Option<String>,
39    /// Filesystem path used during upload (builder feature only); not serialized.
40    #[cfg_attr(feature = "builder", builder(setter(custom)))]
41    #[serde(skip)]
42    pub file_path: Option<PathBuf>,
43    /// Private note for the attachment
44    pub note: Option<String>,
45    /// Category of the attachment
46    pub category: Option<AttachmentCategory>,
47    /// Content type of the attachment
48    #[cfg_attr(feature = "builder", builder(setter(custom)))]
49    pub content_type: Option<String>,
50    pub place_name: Option<String>,
51    /// References to the transaction object to which this attachable file is to be linked
52    #[cfg_attr(feature = "builder", builder(setter(strip_option)))]
53    pub attachable_ref: Option<Vec<AttachableRef>>,
54    /// Longitude of the place where the attachment was taken
55    pub long: Option<String>,
56    /// Tag for the attachment
57    pub tag: Option<String>,
58    /// Latitude of the place where the attachment was taken
59    pub lat: Option<String>,
60    /// URI for accessing the file
61    pub file_access_uri: Option<String>,
62    /// Size of the file in bytes
63    pub size: Option<f64>,
64    /// URI for accessing the thumbnail of the file
65    pub thumbnail_file_access_uri: Option<String>,
66    /// Temporary download URI for the file
67    pub temp_download_uri: Option<String>,
68}
69
70/// Derives the content type from a file extension
71#[must_use]
72pub fn content_type_from_ext(ext: &str) -> Option<&'static str> {
73    let out = match ext {
74        "ai" | "eps" => "application/postscript",
75        "csv" => "text/csv",
76        "doc" => "application/msword",
77        "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
78        "gif" => "image/gif",
79        "jpeg" => "image/jpeg",
80        "jpg" => "image/jpg",
81        "png" => "image/png",
82        "rtf" => "text/rtf",
83        "txt" => "text/plain",
84        "tif" => "image/tiff",
85        "ods" => "application/vnd.oasis.opendocument.spreadsheet",
86        "pdf" => "application/pdf",
87        "xls" => "application/vnd.ms-excel",
88        "xml" => "text/xml",
89        "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
90        _ => return None,
91    };
92    Some(out)
93}
94
95#[cfg(feature = "builder")]
96impl AttachableBuilder {
97    /// Sets `file_path` (for upload), and derives `file_name` and `content_type` from it.
98    /// On success, stores the original path for upload, sets `file_name` to the basename, and infers `content_type` from the file extension.
99    pub fn file(&mut self, path: &impl AsRef<Path>) -> Result<&mut Self, QBTypeError> {
100        let path = path.as_ref();
101
102        self.file_path = Some(Some(path.to_path_buf()));
103
104        self.file_name = Some(Some(
105            path.file_name()
106                .ok_or(QBTypeError::ValidationError(
107                    "No file name on file path".into(),
108                ))?
109                .to_str()
110                .ok_or(QBTypeError::ValidationError(
111                    "Could not turn file name into string".into(),
112                ))?
113                .to_owned(),
114        ));
115
116        self.content_type = Some(
117            content_type_from_ext(
118                path.extension()
119                    .ok_or(QBTypeError::ValidationError(
120                        "No extension on file/dir".into(),
121                    ))?
122                    .to_string_lossy()
123                    .as_ref(),
124            )
125            .map(|f| f.to_owned()),
126        );
127
128        Ok(self)
129    }
130}
131
132/// Trait for all entities that can be attached as files/notes.
133///
134/// Preconditions for upload:
135/// - `file_name` must be present;
136/// - `file_path` must be present.
137/// - `content_type` must be present.
138/// - `can_upload()` returns an error if required fields are missing.
139pub trait QBAttachable {
140    /// Returns Ok(()) when the instance has the fields required for upload.
141    fn can_upload(&self) -> Result<(), QBTypeError>;
142    fn file_path(&self) -> Option<&Path>;
143}
144impl QBAttachable for Attachable {
145    fn can_upload(&self) -> Result<(), QBTypeError> {
146        if self.file_name.is_none() {
147            return Err(QBTypeError::MissingField("file_name"));
148        }
149        if self.file_path.is_none() {
150            return Err(QBTypeError::MissingField("file_path"));
151        }
152        if self.content_type.is_none() {
153            return Err(QBTypeError::MissingField("content_type"));
154        }
155        Ok(())
156    }
157
158    fn file_path(&self) -> Option<&Path> {
159        self.file_path.as_deref()
160    }
161}
162
163/// `AttachmentCategory`
164///
165/// Enumerates the category of an attachment (for example: Document, Image, Receipt).
166#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
167pub enum AttachmentCategory {
168    ContactPhoto,
169    Document,
170    Image,
171    Receipt,
172    Signature,
173    Sound,
174    #[default]
175    Other,
176}
177
178/// AttachableRef
179///
180/// A reference that links an attachment to a target `QuickBooks` entity (such as an Invoice line or a Bill).
181///
182/// Most callers will construct this via `QBToAttachableRef::to_attach_ref()` on an entity that implements `QBToRef`.
183#[skip_serializing_none]
184#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
185#[serde(rename_all = "PascalCase", default)]
186#[cfg_attr(feature = "builder", derive(Builder), builder(default))]
187pub struct AttachableRef {
188    /// Indicates if the entity should be included on send
189    pub include_on_send: Option<bool>,
190    /// Line information for the entity
191    pub line_info: Option<String>,
192    /// Indicates if the entity is a reference only
193    pub no_ref_only: Option<bool>,
194    /// Custom fields for the entity
195    pub custom_field: Option<Vec<CustomField>>,
196    /// Type of the entity
197    #[serde(rename = "type")]
198    pub ref_type: Option<String>,
199    /// Indicates if the entity is inactive
200    pub inactive: Option<bool>,
201    /// The unique ID of the entity
202    pub entity_ref: Option<NtRef>,
203}
204
205impl From<NtRef> for AttachableRef {
206    fn from(value: NtRef) -> Self {
207        AttachableRef {
208            entity_ref: Some(value),
209            ..Default::default()
210        }
211    }
212}
213
214/// Trait for entities that can be converted to a reference for an attachment.
215pub trait QBToAttachableRef: QBToRef {
216    fn to_attach_ref(&self) -> Result<AttachableRef, QBTypeError> {
217        let value = self.to_ref()?;
218        Ok(value.into())
219    }
220}
221
222impl<T: QBToRef> QBToAttachableRef for T {}
223
224impl QBCreatable for Attachable {
225    fn can_create(&self) -> bool {
226        (self.file_name.is_some() || self.note.is_some())
227            && self.content_type.is_some()
228            && self.file_path.is_some()
229    }
230}
231impl QBDeletable for Attachable {}
232impl QBFullUpdatable for Attachable {
233    fn can_full_update(&self) -> bool {
234        self.has_read() && self.can_create()
235    }
236}