Skip to main content

yuki_cli/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    fn require_session(&self) -> Result<&str, YukiError> {
31        self.soap.session_id().ok_or_else(|| {
32            YukiError::AuthFailed("not authenticated — call authenticate() first".to_string())
33        })
34    }
35
36    /// Authenticate with the Yuki API and store the session ID.
37    pub async fn authenticate(&mut self, api_key: &str) -> Result<String, YukiError> {
38        self.soap.authenticate(api_key).await
39    }
40
41    /// Retrieve all sales items.
42    pub async fn get_sales_items(&self) -> Result<Vec<SalesItem>, YukiError> {
43        let session = self.require_session()?;
44        let envelope = SoapEnvelope::new("GetSalesItems").session(session).build();
45        let body = self.soap.call("GetSalesItems", envelope).await?;
46        Self::parse_sales_items(&body)
47    }
48
49    /// Parse a GetSalesItems SOAP response into a list of `SalesItem` values.
50    ///
51    /// Each `SalesItem` element carries child elements `id` and `description`.
52    pub fn parse_sales_items(xml: &str) -> Result<Vec<SalesItem>, YukiError> {
53        let mut reader = Reader::from_str(xml);
54        reader.config_mut().trim_text(true);
55
56        let mut items = Vec::new();
57        let mut in_item = false;
58        let mut field: Option<String> = None;
59        let mut current = SalesItem {
60            id: String::new(),
61            description: String::new(),
62        };
63        let mut buf = Vec::new();
64
65        loop {
66            match reader.read_event_into(&mut buf) {
67                Ok(Event::Start(ref e)) => {
68                    let local = local_name(e.name().as_ref()).to_string();
69                    match local.as_str() {
70                        "SalesItem" => {
71                            in_item = true;
72                            current = SalesItem {
73                                id: String::new(),
74                                description: String::new(),
75                            };
76                        }
77                        "id" | "description" if in_item => {
78                            field = Some(local);
79                        }
80                        _ => {}
81                    }
82                }
83                Ok(Event::Text(ref e)) => {
84                    if let Some(ref f) = field {
85                        let text = e
86                            .unescape()
87                            .map_err(|e| YukiError::Xml(e.to_string()))?
88                            .trim()
89                            .to_string();
90                        match f.as_str() {
91                            "id" => current.id = text,
92                            "description" => current.description = text,
93                            _ => {}
94                        }
95                    }
96                }
97                Ok(Event::End(ref e)) => {
98                    let local = local_name(e.name().as_ref()).to_string();
99                    match local.as_str() {
100                        "id" | "description" => {
101                            field = None;
102                        }
103                        "SalesItem" if in_item => {
104                            items.push(current.clone());
105                            in_item = false;
106                        }
107                        _ => {}
108                    }
109                }
110                Ok(Event::Eof) => break,
111                Err(e) => return Err(YukiError::Xml(e.to_string())),
112                _ => {}
113            }
114            buf.clear();
115        }
116
117        Ok(items)
118    }
119}
120
121impl Default for SalesClient {
122    fn default() -> Self {
123        Self::new()
124    }
125}