Skip to main content

yuki_client/client/
vat.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/Vat.asmx";
10
11/// A VAT return record for a specific period.
12#[derive(Debug, Clone)]
13pub struct VatReturn {
14    pub period: String,
15    pub status: String,
16    pub start_date: String,
17    pub end_date: String,
18}
19
20/// An active VAT code definition.
21#[derive(Debug, Clone)]
22pub struct VatCode {
23    pub code: String,
24    pub description: String,
25}
26
27/// Client for the Yuki VAT SOAP service.
28pub struct VatClient {
29    soap: SoapClient,
30}
31
32impl VatClient {
33    pub fn new() -> Self {
34        Self {
35            soap: SoapClient::new(BASE_URL),
36        }
37    }
38
39    /// Build over a caller-provided HTTP client, so a long-running consumer can
40    /// share a single pooled client across all service clients.
41    pub fn with_client(http: reqwest::Client) -> Self {
42        Self {
43            soap: SoapClient::with_client(BASE_URL, http),
44        }
45    }
46
47    fn require_session(&self) -> Result<&str, YukiError> {
48        self.soap.session_id().ok_or_else(|| {
49            YukiError::AuthFailed("not authenticated — call authenticate() first".to_string())
50        })
51    }
52
53    /// Authenticate with the Yuki API and store the session ID.
54    pub async fn authenticate(&mut self, api_key: &str) -> Result<String, YukiError> {
55        self.soap.authenticate(api_key).await
56    }
57
58    /// Retrieve the list of VAT returns for an administration.
59    pub async fn vat_return_list(
60        &self,
61        administration_id: &str,
62    ) -> Result<Vec<VatReturn>, YukiError> {
63        let session = self.require_session()?;
64        let envelope = SoapEnvelope::new("VATReturnList")
65            .session(session)
66            .param("administrationID", administration_id)
67            .build();
68        let body = self.soap.call("VATReturnList", envelope).await?;
69        Self::parse_vat_returns(&body)
70    }
71
72    /// Retrieve all active VAT codes for an administration.
73    pub async fn active_vat_codes(
74        &self,
75        administration_id: &str,
76    ) -> Result<Vec<VatCode>, YukiError> {
77        let session = self.require_session()?;
78        let envelope = SoapEnvelope::new("ActiveVATCodesList")
79            .session(session)
80            .param("administrationID", administration_id)
81            .build();
82        let body = self.soap.call("ActiveVATCodesList", envelope).await?;
83        Self::parse_vat_codes(&body)
84    }
85
86    /// Parse a VatReturnList SOAP response into a list of `VatReturn` values.
87    pub fn parse_vat_returns(xml: &str) -> Result<Vec<VatReturn>, YukiError> {
88        let mut reader = Reader::from_str(xml);
89        reader.config_mut().trim_text(true);
90
91        let mut returns = Vec::new();
92        let mut in_item = false;
93        let mut field: Option<String> = None;
94        let mut current = VatReturn {
95            period: String::new(),
96            status: String::new(),
97            start_date: String::new(),
98            end_date: String::new(),
99        };
100        let mut buf = Vec::new();
101
102        loop {
103            match reader.read_event_into(&mut buf) {
104                Ok(Event::Start(ref e)) => {
105                    let local = local_name(e.name().as_ref()).to_string();
106                    match local.as_str() {
107                        "VATReturnInfo" => {
108                            in_item = true;
109                            current = VatReturn {
110                                period: String::new(),
111                                status: String::new(),
112                                start_date: String::new(),
113                                end_date: String::new(),
114                            };
115                        }
116                        "startDate" | "endDate" | "status" if in_item => {
117                            field = Some(local);
118                        }
119                        _ => {}
120                    }
121                }
122                Ok(Event::Text(ref e)) => {
123                    if let Some(ref f) = field {
124                        let text = e
125                            .unescape()
126                            .map_err(|e| YukiError::Xml(e.to_string()))?
127                            .trim()
128                            .to_string();
129                        match f.as_str() {
130                            "startDate" => current.start_date = text,
131                            "endDate" => current.end_date = text,
132                            "status" => current.status = text,
133                            _ => {}
134                        }
135                    }
136                }
137                Ok(Event::End(ref e)) => {
138                    let local = local_name(e.name().as_ref()).to_string();
139                    match local.as_str() {
140                        "startDate" | "endDate" | "status" => field = None,
141                        "VATReturnInfo" if in_item => {
142                            // Derive period from start/end dates
143                            current.period = format!(
144                                "{} - {}",
145                                current
146                                    .start_date
147                                    .split('T')
148                                    .next()
149                                    .unwrap_or(&current.start_date),
150                                current
151                                    .end_date
152                                    .split('T')
153                                    .next()
154                                    .unwrap_or(&current.end_date),
155                            );
156                            returns.push(current.clone());
157                            in_item = false;
158                        }
159                        _ => {}
160                    }
161                }
162                Ok(Event::Eof) => break,
163                Err(e) => return Err(YukiError::Xml(e.to_string())),
164                _ => {}
165            }
166            buf.clear();
167        }
168
169        Ok(returns)
170    }
171
172    /// Parse an ActiveVatCodes SOAP response into a list of `VatCode` values.
173    pub fn parse_vat_codes(xml: &str) -> Result<Vec<VatCode>, YukiError> {
174        let mut reader = Reader::from_str(xml);
175        reader.config_mut().trim_text(true);
176
177        let mut codes = Vec::new();
178        let mut in_item = false;
179        let mut field: Option<String> = None;
180        let mut current = VatCode {
181            code: String::new(),
182            description: String::new(),
183        };
184        let mut buf = Vec::new();
185
186        loop {
187            match reader.read_event_into(&mut buf) {
188                Ok(Event::Start(ref e)) => {
189                    let local = local_name(e.name().as_ref()).to_string();
190                    match local.as_str() {
191                        "VATCode" => {
192                            in_item = true;
193                            current = VatCode {
194                                code: String::new(),
195                                description: String::new(),
196                            };
197                        }
198                        "type" | "description" if in_item => {
199                            field = Some(local);
200                        }
201                        _ => {}
202                    }
203                }
204                Ok(Event::Text(ref e)) => {
205                    if let Some(ref f) = field {
206                        let text = e
207                            .unescape()
208                            .map_err(|e| YukiError::Xml(e.to_string()))?
209                            .trim()
210                            .to_string();
211                        match f.as_str() {
212                            "type" => current.code = text,
213                            "description" => current.description = text,
214                            _ => {}
215                        }
216                    }
217                }
218                Ok(Event::End(ref e)) => {
219                    let local = local_name(e.name().as_ref()).to_string();
220                    match local.as_str() {
221                        "type" | "description" => field = None,
222                        "VATCode" if in_item => {
223                            codes.push(current.clone());
224                            in_item = false;
225                        }
226                        _ => {}
227                    }
228                }
229                Ok(Event::Eof) => break,
230                Err(e) => return Err(YukiError::Xml(e.to_string())),
231                _ => {}
232            }
233            buf.clear();
234        }
235
236        Ok(codes)
237    }
238}
239
240impl Default for VatClient {
241    fn default() -> Self {
242        Self::new()
243    }
244}