Skip to main content

yuki_client/client/
contact.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/Contact.asmx";
10
11/// A Yuki contact (customer or supplier).
12#[derive(Debug, Clone)]
13pub struct Contact {
14    pub id: String,
15    pub name: String,
16    pub contact_type: String,
17    pub country: String,
18    pub is_supplier: bool,
19    pub is_customer: bool,
20}
21
22/// Client for the Yuki Contact SOAP service.
23pub struct ContactClient {
24    soap: SoapClient,
25}
26
27impl ContactClient {
28    pub fn new() -> Self {
29        Self {
30            soap: SoapClient::new(BASE_URL),
31        }
32    }
33
34    /// Build over a caller-provided HTTP client, so a long-running consumer can
35    /// share a single pooled client across all service clients.
36    pub fn with_client(http: reqwest::Client) -> Self {
37        Self {
38            soap: SoapClient::with_client(BASE_URL, http),
39        }
40    }
41
42    fn require_session(&self) -> Result<&str, YukiError> {
43        self.soap.session_id().ok_or_else(|| {
44            YukiError::AuthFailed("not authenticated — call authenticate() first".to_string())
45        })
46    }
47
48    /// Authenticate with the Yuki API and store the session ID.
49    pub async fn authenticate(&mut self, api_key: &str) -> Result<String, YukiError> {
50        self.soap.authenticate(api_key).await
51    }
52
53    /// Search for contacts matching a query string.
54    pub async fn search_contacts(&self, query: &str) -> Result<Vec<Contact>, YukiError> {
55        let session = self.require_session()?;
56        let envelope = SoapEnvelope::new("SearchContacts")
57            .session(session)
58            .param("searchQuery", query)
59            .build();
60        let body = self.soap.call("SearchContacts", envelope).await?;
61        parse_contacts(&body)
62    }
63
64    /// Retrieve suppliers and customers filtered by contact type.
65    pub async fn get_suppliers_and_customers(
66        &self,
67        contact_type: &str,
68    ) -> Result<Vec<Contact>, YukiError> {
69        let session = self.require_session()?;
70        let envelope = SoapEnvelope::new("GetSuppliersAndCustomers")
71            .session(session)
72            .param("contactType", contact_type)
73            .build();
74        let body = self.soap.call("GetSuppliersAndCustomers", envelope).await?;
75        parse_contacts(&body)
76    }
77}
78
79/// Parse a SearchContacts or GetSuppliersAndCustomers SOAP response into a list of contacts.
80///
81/// Each `<Contact ID="uuid">` element carries child elements for each field.
82/// The contact ID is an XML attribute; all other fields are child text nodes.
83pub fn parse_contacts(xml: &str) -> Result<Vec<Contact>, YukiError> {
84    let mut reader = Reader::from_str(xml);
85    reader.config_mut().trim_text(true);
86
87    let mut contacts = Vec::new();
88    let mut in_contact = false;
89    let mut current_field = String::new();
90    let mut contact = Contact {
91        id: String::new(),
92        name: String::new(),
93        contact_type: String::new(),
94        country: String::new(),
95        is_supplier: false,
96        is_customer: false,
97    };
98    let mut buf = Vec::new();
99
100    loop {
101        match reader.read_event_into(&mut buf) {
102            Ok(Event::Start(ref e)) => {
103                let local = local_name(e.name().as_ref()).to_string();
104                match local.as_str() {
105                    "Contact" => {
106                        in_contact = true;
107                        contact = Contact {
108                            id: String::new(),
109                            name: String::new(),
110                            contact_type: String::new(),
111                            country: String::new(),
112                            is_supplier: false,
113                            is_customer: false,
114                        };
115                        for attr in e.attributes().flatten() {
116                            if attr.key.as_ref() == b"ID" {
117                                contact.id = String::from_utf8_lossy(&attr.value).to_string();
118                            }
119                        }
120                    }
121                    "Type" | "Name" | "Country" | "IsSupplier" | "IsCustomer" if in_contact => {
122                        current_field = local;
123                    }
124                    _ => {}
125                }
126            }
127            Ok(Event::Text(ref e)) if in_contact && !current_field.is_empty() => {
128                let text = e
129                    .unescape()
130                    .map_err(|e| YukiError::Xml(e.to_string()))?
131                    .trim()
132                    .to_string();
133                match current_field.as_str() {
134                    "Type" => contact.contact_type = text,
135                    "Name" => contact.name = text,
136                    "Country" => contact.country = text,
137                    "IsSupplier" => contact.is_supplier = text.eq_ignore_ascii_case("true"),
138                    "IsCustomer" => contact.is_customer = text.eq_ignore_ascii_case("true"),
139                    _ => {}
140                }
141            }
142            Ok(Event::End(ref e)) => {
143                let name = e.name();
144                let local = local_name(name.as_ref());
145                match local {
146                    "Type" | "Name" | "Country" | "IsSupplier" | "IsCustomer" => {
147                        current_field.clear();
148                    }
149                    "Contact" => {
150                        if !contact.id.is_empty() {
151                            contacts.push(contact.clone());
152                        }
153                        in_contact = false;
154                    }
155                    _ => {}
156                }
157            }
158            Ok(Event::Eof) => break,
159            Err(e) => return Err(YukiError::Xml(e.to_string())),
160            _ => {}
161        }
162        buf.clear();
163    }
164
165    Ok(contacts)
166}
167
168impl Default for ContactClient {
169    fn default() -> Self {
170        Self::new()
171    }
172}