Skip to main content

yuki_client/client/
sales.rs

1use quick_xml::Reader;
2use quick_xml::events::Event;
3
4use crate::error::YukiError;
5
6use super::local_name;
7use super::soap_client::{SoapClient, SoapEnvelope};
8
9const BASE_URL: &str = "https://api.yukiworks.nl/ws/Sales.asmx";
10
11/// A Yuki sales item (product or service available for invoicing).
12#[derive(Debug, Clone)]
13pub struct SalesItem {
14    pub id: String,
15    pub description: String,
16}
17
18/// Client for the Yuki Sales SOAP service.
19pub struct SalesClient {
20    soap: SoapClient,
21}
22
23impl SalesClient {
24    pub fn new() -> Self {
25        Self {
26            soap: SoapClient::new(BASE_URL),
27        }
28    }
29
30    /// Build over a caller-provided HTTP client, so a long-running consumer can
31    /// share a single pooled client across all service clients.
32    pub fn with_client(http: reqwest::Client) -> Self {
33        Self {
34            soap: SoapClient::with_client(BASE_URL, http),
35        }
36    }
37
38    fn require_session(&self) -> Result<&str, YukiError> {
39        self.soap.session_id().ok_or_else(|| {
40            YukiError::AuthFailed("not authenticated — call authenticate() first".to_string())
41        })
42    }
43
44    /// Authenticate with the Yuki API and store the session ID.
45    pub async fn authenticate(&mut self, api_key: &str) -> Result<String, YukiError> {
46        self.soap.authenticate(api_key).await
47    }
48
49    /// Retrieve all sales items.
50    pub async fn get_sales_items(&self) -> Result<Vec<SalesItem>, YukiError> {
51        let session = self.require_session()?;
52        let envelope = SoapEnvelope::new("GetSalesItems").session(session).build();
53        let body = self.soap.call("GetSalesItems", envelope).await?;
54        Self::parse_sales_items(&body)
55    }
56
57    /// Parse a GetSalesItems SOAP response into a list of `SalesItem` values.
58    ///
59    /// Each `SalesItem` element carries child elements `id` and `description`.
60    pub fn parse_sales_items(xml: &str) -> Result<Vec<SalesItem>, YukiError> {
61        let mut reader = Reader::from_str(xml);
62        reader.config_mut().trim_text(true);
63
64        let mut items = Vec::new();
65        let mut in_item = false;
66        let mut field: Option<String> = None;
67        let mut current = SalesItem {
68            id: String::new(),
69            description: String::new(),
70        };
71        let mut buf = Vec::new();
72
73        loop {
74            match reader.read_event_into(&mut buf) {
75                Ok(Event::Start(ref e)) => {
76                    let local = local_name(e.name().as_ref()).to_string();
77                    match local.as_str() {
78                        "SalesItem" => {
79                            in_item = true;
80                            current = SalesItem {
81                                id: String::new(),
82                                description: String::new(),
83                            };
84                        }
85                        "id" | "description" if in_item => {
86                            field = Some(local);
87                        }
88                        _ => {}
89                    }
90                }
91                Ok(Event::Text(ref e)) => {
92                    if let Some(ref f) = field {
93                        let text = e
94                            .unescape()
95                            .map_err(|e| YukiError::Xml(e.to_string()))?
96                            .trim()
97                            .to_string();
98                        match f.as_str() {
99                            "id" => current.id = text,
100                            "description" => current.description = text,
101                            _ => {}
102                        }
103                    }
104                }
105                Ok(Event::End(ref e)) => {
106                    let local = local_name(e.name().as_ref()).to_string();
107                    match local.as_str() {
108                        "id" | "description" => {
109                            field = None;
110                        }
111                        "SalesItem" if in_item => {
112                            items.push(current.clone());
113                            in_item = false;
114                        }
115                        _ => {}
116                    }
117                }
118                Ok(Event::Eof) => break,
119                Err(e) => return Err(YukiError::Xml(e.to_string())),
120                _ => {}
121            }
122            buf.clear();
123        }
124
125        Ok(items)
126    }
127}
128
129impl Default for SalesClient {
130    fn default() -> Self {
131        Self::new()
132    }
133}