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/Accounting.asmx";
10
11#[derive(Debug, Clone)]
13pub struct Administration {
14 pub id: String,
15 pub name: String,
16 pub domain_id: String,
17}
18
19#[derive(Debug, Clone)]
21pub struct OutstandingItem {
22 pub contact_name: String,
23 pub description: String,
24 pub date: String,
25 pub amount: String,
26 pub open_amount: String,
27}
28
29#[derive(Debug, Clone)]
31pub struct GlTransaction {
32 pub id: String,
33 pub date: String,
34 pub description: String,
35 pub gl_account: String,
36 pub amount: String,
37}
38
39#[derive(Debug, Clone)]
41pub struct GlTransactionWithContact {
42 pub id: String,
43 pub date: String,
44 pub description: String,
45 pub gl_account: String,
46 pub amount: String,
47 pub contact_name: String,
48}
49
50pub struct AccountingClient {
52 soap: SoapClient,
53}
54
55impl AccountingClient {
56 pub fn new() -> Self {
57 Self {
58 soap: SoapClient::new(BASE_URL),
59 }
60 }
61
62 fn require_session(&self) -> Result<&str, YukiError> {
63 self.soap.session_id().ok_or_else(|| {
64 YukiError::AuthFailed("not authenticated — call authenticate() first".to_string())
65 })
66 }
67
68 pub async fn authenticate(&mut self, api_key: &str) -> Result<String, YukiError> {
70 self.soap.authenticate(api_key).await
71 }
72
73 pub async fn administrations(&self) -> Result<Vec<Administration>, YukiError> {
75 let session = self.require_session()?;
76 let envelope = SoapEnvelope::new("Administrations")
77 .session(session)
78 .build();
79 let body = self.soap.call("Administrations", envelope).await?;
80 Self::parse_administrations(&body)
81 }
82
83 pub async fn set_current_domain(&mut self, domain_id: &str) -> Result<(), YukiError> {
85 let session = self.require_session()?;
86 let envelope = SoapEnvelope::new("SetCurrentDomain")
87 .session(session)
88 .param("domainID", domain_id)
89 .build();
90 self.soap.call("SetCurrentDomain", envelope).await?;
91 Ok(())
92 }
93
94 pub async fn gl_account_balance(
96 &self,
97 administration_id: &str,
98 gl_account_code: &str,
99 transaction_date: &str,
100 ) -> Result<String, YukiError> {
101 let session = self.require_session()?;
102 let envelope = SoapEnvelope::new("GLAccountBalance")
103 .session(session)
104 .param("administrationID", administration_id)
105 .param("GLAccountCode", gl_account_code)
106 .param("transactionDate", transaction_date)
107 .build();
108 self.soap.call("GLAccountBalance", envelope).await
109 }
110
111 pub async fn gl_account_transactions(
113 &self,
114 administration_id: &str,
115 gl_account_code: &str,
116 start_date: &str,
117 end_date: &str,
118 ) -> Result<String, YukiError> {
119 let session = self.require_session()?;
120 let envelope = SoapEnvelope::new("GLAccountTransactions")
121 .session(session)
122 .param("administrationID", administration_id)
123 .param("GLAccountCode", gl_account_code)
124 .param("StartDate", start_date)
125 .param("EndDate", end_date)
126 .build();
127 self.soap.call("GLAccountTransactions", envelope).await
128 }
129
130 pub async fn outstanding_debtor_items(
132 &self,
133 administration_id: &str,
134 ) -> Result<Vec<OutstandingItem>, YukiError> {
135 let session = self.require_session()?;
136 let envelope = SoapEnvelope::new("OutstandingDebtorItems")
137 .session(session)
138 .param("administrationID", administration_id)
139 .build();
140 let body = self.soap.call("OutstandingDebtorItems", envelope).await?;
141 Self::parse_outstanding_items(&body, "OutstandingDebtorItemsResult")
142 }
143
144 pub async fn outstanding_debtor_items_by_date(
146 &self,
147 administration_id: &str,
148 start_date: &str,
149 end_date: &str,
150 ) -> Result<Vec<OutstandingItem>, YukiError> {
151 let session = self.require_session()?;
152 let envelope = SoapEnvelope::new("OutstandingDebtorItemsByDate")
153 .session(session)
154 .param("administrationID", administration_id)
155 .param("startDate", start_date)
156 .param("endDate", end_date)
157 .build();
158 let body = self
159 .soap
160 .call("OutstandingDebtorItemsByDate", envelope)
161 .await?;
162 Self::parse_outstanding_items(&body, "OutstandingDebtorItemsByDateResult")
163 }
164
165 pub async fn outstanding_creditor_items(
167 &self,
168 administration_id: &str,
169 ) -> Result<Vec<OutstandingItem>, YukiError> {
170 let session = self.require_session()?;
171 let envelope = SoapEnvelope::new("OutstandingCreditorItems")
172 .session(session)
173 .param("administrationID", administration_id)
174 .build();
175 let body = self.soap.call("OutstandingCreditorItems", envelope).await?;
176 Self::parse_outstanding_items(&body, "OutstandingCreditorItemsResult")
177 }
178
179 pub async fn outstanding_creditor_items_by_date(
181 &self,
182 administration_id: &str,
183 start_date: &str,
184 end_date: &str,
185 ) -> Result<Vec<OutstandingItem>, YukiError> {
186 let session = self.require_session()?;
187 let envelope = SoapEnvelope::new("OutstandingCreditorItemsByDate")
188 .session(session)
189 .param("administrationID", administration_id)
190 .param("startDate", start_date)
191 .param("endDate", end_date)
192 .build();
193 let body = self
194 .soap
195 .call("OutstandingCreditorItemsByDate", envelope)
196 .await?;
197 Self::parse_outstanding_items(&body, "OutstandingCreditorItemsByDateResult")
198 }
199
200 pub async fn gl_account_transactions_and_contact(
202 &self,
203 administration_id: &str,
204 gl_account_code: &str,
205 start_date: &str,
206 end_date: &str,
207 ) -> Result<Vec<GlTransactionWithContact>, YukiError> {
208 let session = self.require_session()?;
209 let envelope = SoapEnvelope::new("GLAccountTransactionsAndContact")
210 .session(session)
211 .param("administrationID", administration_id)
212 .param("GLAccountCode", gl_account_code)
213 .param("StartDate", start_date)
214 .param("EndDate", end_date)
215 .build();
216 let body = self
217 .soap
218 .call("GLAccountTransactionsAndContact", envelope)
219 .await?;
220 Self::parse_gl_transactions_with_contact(&body)
221 }
222
223 pub async fn net_revenue(
225 &self,
226 administration_id: &str,
227 start_date: &str,
228 end_date: &str,
229 ) -> Result<String, YukiError> {
230 let session = self.require_session()?;
231 let envelope = SoapEnvelope::new("NetRevenue")
232 .session(session)
233 .param("administrationID", administration_id)
234 .param("StartDate", start_date)
235 .param("EndDate", end_date)
236 .build();
237 let body = self.soap.call("NetRevenue", envelope).await?;
238 SoapClient::parse_single_result(&body, "NetRevenueResult")
239 }
240
241 pub async fn check_outstanding_item_admin(
243 &self,
244 administration_id: &str,
245 reference: &str,
246 ) -> Result<String, YukiError> {
247 let session = self.require_session()?;
248 let envelope = SoapEnvelope::new("CheckOutstandingItemAdmin")
249 .session(session)
250 .param("administrationID", administration_id)
251 .param("Reference", reference)
252 .build();
253 self.soap.call("CheckOutstandingItemAdmin", envelope).await
254 }
255
256 pub fn parse_gl_transactions(xml: &str) -> Result<Vec<GlTransaction>, YukiError> {
261 let mut reader = Reader::from_str(xml);
262 reader.config_mut().trim_text(true);
263
264 let mut transactions = Vec::new();
265 let mut in_transaction = false;
266 let mut field: Option<String> = None;
267 let mut current = GlTransaction {
268 id: String::new(),
269 date: String::new(),
270 description: String::new(),
271 gl_account: String::new(),
272 amount: String::new(),
273 };
274 let mut buf = Vec::new();
275
276 loop {
277 match reader.read_event_into(&mut buf) {
278 Ok(Event::Start(ref e)) => {
279 let local = local_name(e.name().as_ref()).to_string();
280 match local.as_str() {
281 "GLAccountTransaction" => {
282 in_transaction = true;
283 current = GlTransaction {
284 id: String::new(),
285 date: String::new(),
286 description: String::new(),
287 gl_account: String::new(),
288 amount: String::new(),
289 };
290 for attr in e.attributes().flatten() {
291 if attr.key.as_ref() == b"ID" {
292 current.id = String::from_utf8_lossy(&attr.value).to_string();
293 }
294 }
295 }
296 "Date" | "Description" | "Amount" | "GLAccountCode" if in_transaction => {
297 field = Some(local);
298 }
299 _ => {}
300 }
301 }
302 Ok(Event::Text(ref e)) => {
303 if let Some(ref f) = field {
304 let text = e
305 .unescape()
306 .map_err(|e| YukiError::Xml(e.to_string()))?
307 .trim()
308 .to_string();
309 match f.as_str() {
310 "Date" => current.date = text,
311 "Description" => current.description = text,
312 "Amount" => current.amount = text,
313 "GLAccountCode" => current.gl_account = text,
314 _ => {}
315 }
316 }
317 }
318 Ok(Event::End(ref e)) => {
319 let local = local_name(e.name().as_ref()).to_string();
320 match local.as_str() {
321 "Date" | "Description" | "Amount" | "GLAccountCode" => {
322 field = None;
323 }
324 "GLAccountTransaction" if in_transaction => {
325 transactions.push(current.clone());
326 in_transaction = false;
327 }
328 _ => {}
329 }
330 }
331 Ok(Event::Eof) => break,
332 Err(e) => return Err(YukiError::Xml(e.to_string())),
333 _ => {}
334 }
335 buf.clear();
336 }
337
338 Ok(transactions)
339 }
340
341 pub fn parse_gl_transactions_with_contact(
345 xml: &str,
346 ) -> Result<Vec<GlTransactionWithContact>, YukiError> {
347 let mut reader = Reader::from_str(xml);
348 reader.config_mut().trim_text(true);
349
350 let mut transactions = Vec::new();
351 let mut in_transaction = false;
352 let mut field: Option<String> = None;
353 let mut current = GlTransactionWithContact {
354 id: String::new(),
355 date: String::new(),
356 description: String::new(),
357 gl_account: String::new(),
358 amount: String::new(),
359 contact_name: String::new(),
360 };
361 let mut buf = Vec::new();
362
363 loop {
364 match reader.read_event_into(&mut buf) {
365 Ok(Event::Start(ref e)) => {
366 let local = local_name(e.name().as_ref()).to_string();
367 match local.as_str() {
368 "GLAccountTransaction" => {
369 in_transaction = true;
370 current = GlTransactionWithContact {
371 id: String::new(),
372 date: String::new(),
373 description: String::new(),
374 gl_account: String::new(),
375 amount: String::new(),
376 contact_name: String::new(),
377 };
378 for attr in e.attributes().flatten() {
379 if attr.key.as_ref() == b"ID" {
380 current.id = String::from_utf8_lossy(&attr.value).to_string();
381 }
382 }
383 }
384 "Date" | "Description" | "Amount" | "GLAccountCode" | "Contact"
385 | "ContactName"
386 if in_transaction =>
387 {
388 field = Some(local);
389 }
390 _ => {}
391 }
392 }
393 Ok(Event::Text(ref e)) => {
394 if let Some(ref f) = field {
395 let text = e
396 .unescape()
397 .map_err(|e| YukiError::Xml(e.to_string()))?
398 .trim()
399 .to_string();
400 match f.as_str() {
401 "Date" => current.date = text,
402 "Description" => current.description = text,
403 "Amount" => current.amount = text,
404 "GLAccountCode" => current.gl_account = text,
405 "Contact" | "ContactName" => current.contact_name = text,
406 _ => {}
407 }
408 }
409 }
410 Ok(Event::End(ref e)) => {
411 let local = local_name(e.name().as_ref()).to_string();
412 match local.as_str() {
413 "Date" | "Description" | "Amount" | "GLAccountCode" | "Contact"
414 | "ContactName" => {
415 field = None;
416 }
417 "GLAccountTransaction" if in_transaction => {
418 transactions.push(current.clone());
419 in_transaction = false;
420 }
421 _ => {}
422 }
423 }
424 Ok(Event::Eof) => break,
425 Err(e) => return Err(YukiError::Xml(e.to_string())),
426 _ => {}
427 }
428 buf.clear();
429 }
430
431 Ok(transactions)
432 }
433
434 pub fn parse_administrations(xml: &str) -> Result<Vec<Administration>, YukiError> {
440 let mut reader = Reader::from_str(xml);
441 reader.config_mut().trim_text(true);
442
443 let mut administrations = Vec::new();
444 let mut current_id = String::new();
445 let mut current_name = String::new();
446 let mut current_domain_id = String::new();
447 let mut in_administration = false;
448 let mut in_name = false;
449 let mut in_domain_id = false;
450 let mut buf = Vec::new();
451
452 loop {
453 match reader.read_event_into(&mut buf) {
454 Ok(Event::Start(ref e)) => {
455 let local = local_name(e.name().as_ref()).to_string();
456 match local.as_str() {
457 "Administration" => {
458 in_administration = true;
459 current_id.clear();
460 current_name.clear();
461 current_domain_id.clear();
462 for attr in e.attributes().flatten() {
464 if attr.key.as_ref() == b"ID" {
465 current_id = String::from_utf8_lossy(&attr.value).to_string();
466 }
467 }
468 }
469 "Name" if in_administration => in_name = true,
470 "DomainID" if in_administration => in_domain_id = true,
471 _ => {}
472 }
473 }
474 Ok(Event::Text(ref e)) => {
475 let text = e
476 .unescape()
477 .map_err(|e| YukiError::Xml(e.to_string()))?
478 .trim()
479 .to_string();
480 if in_name {
481 current_name = text;
482 } else if in_domain_id {
483 current_domain_id = text;
484 }
485 }
486 Ok(Event::End(ref e)) => {
487 let local = local_name(e.name().as_ref()).to_string();
488 match local.as_str() {
489 "Name" => in_name = false,
490 "DomainID" => in_domain_id = false,
491 "Administration" => {
492 if !current_id.is_empty() {
493 administrations.push(Administration {
494 id: current_id.clone(),
495 name: current_name.clone(),
496 domain_id: current_domain_id.clone(),
497 });
498 }
499 in_administration = false;
500 }
501 _ => {}
502 }
503 }
504 Ok(Event::Eof) => break,
505 Err(e) => return Err(YukiError::Xml(e.to_string())),
506 _ => {}
507 }
508 buf.clear();
509 }
510
511 Ok(administrations)
512 }
513
514 pub fn parse_outstanding_items(
519 xml: &str,
520 result_tag: &str,
521 ) -> Result<Vec<OutstandingItem>, YukiError> {
522 let mut reader = Reader::from_str(xml);
523 reader.config_mut().trim_text(true);
524
525 let mut items = Vec::new();
526 let mut in_result = false;
527 let mut in_item = false;
528 let mut field: Option<String> = None;
529 let mut current = OutstandingItem {
530 contact_name: String::new(),
531 description: String::new(),
532 date: String::new(),
533 amount: String::new(),
534 open_amount: String::new(),
535 };
536 let mut buf = Vec::new();
537
538 loop {
539 match reader.read_event_into(&mut buf) {
540 Ok(Event::Start(ref e)) => {
541 let local = local_name(e.name().as_ref()).to_string();
542 match local.as_str() {
543 tag if tag == result_tag => in_result = true,
544 "Item" if in_result => {
545 in_item = true;
546 current = OutstandingItem {
547 contact_name: String::new(),
548 description: String::new(),
549 date: String::new(),
550 amount: String::new(),
551 open_amount: String::new(),
552 };
553 }
554 "Contact" | "ContactName" | "Description" | "Date" | "Amount"
555 | "OriginalAmount" | "OpenAmount"
556 if in_item =>
557 {
558 field = Some(local);
559 }
560 _ => {}
561 }
562 }
563 Ok(Event::Text(ref e)) => {
564 if let Some(ref f) = field {
565 let text = e
566 .unescape()
567 .map_err(|e| YukiError::Xml(e.to_string()))?
568 .trim()
569 .to_string();
570 match f.as_str() {
571 "Contact" | "ContactName" => current.contact_name = text,
572 "Description" => current.description = text,
573 "Date" => current.date = text,
574 "Amount" | "OriginalAmount" => current.amount = text,
575 "OpenAmount" => current.open_amount = text,
576 _ => {}
577 }
578 }
579 }
580 Ok(Event::End(ref e)) => {
581 let local = local_name(e.name().as_ref()).to_string();
582 match local.as_str() {
583 "Contact" | "ContactName" | "Description" | "Date" | "Amount"
584 | "OriginalAmount" | "OpenAmount" => {
585 field = None;
586 }
587 "Item" if in_item => {
588 items.push(current.clone());
589 in_item = false;
590 }
591 tag if tag == result_tag => {
592 in_result = false;
593 }
594 _ => {}
595 }
596 }
597 Ok(Event::Eof) => break,
598 Err(e) => return Err(YukiError::Xml(e.to_string())),
599 _ => {}
600 }
601 buf.clear();
602 }
603
604 Ok(items)
605 }
606}
607
608impl Default for AccountingClient {
609 fn default() -> Self {
610 Self::new()
611 }
612}