Skip to main content

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