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
50#[derive(Debug, Clone)]
56pub struct GlAccountBalance {
57 pub code: String,
58 pub description: String,
59 pub balance_type: String,
60 pub amount: String,
61}
62
63pub struct AccountingClient {
65 soap: SoapClient,
66}
67
68impl AccountingClient {
69 pub fn new() -> Self {
70 Self {
71 soap: SoapClient::new(BASE_URL),
72 }
73 }
74
75 pub fn with_client(http: reqwest::Client) -> Self {
78 Self {
79 soap: SoapClient::with_client(BASE_URL, http),
80 }
81 }
82
83 fn require_session(&self) -> Result<&str, YukiError> {
84 self.soap.session_id().ok_or_else(|| {
85 YukiError::AuthFailed("not authenticated — call authenticate() first".to_string())
86 })
87 }
88
89 pub async fn authenticate(&mut self, api_key: &str) -> Result<String, YukiError> {
91 self.soap.authenticate(api_key).await
92 }
93
94 pub async fn administrations(&self) -> Result<Vec<Administration>, YukiError> {
96 let session = self.require_session()?;
97 let envelope = SoapEnvelope::new("Administrations")
98 .session(session)
99 .build();
100 let body = self.soap.call("Administrations", envelope).await?;
101 Self::parse_administrations(&body)
102 }
103
104 pub async fn set_current_domain(&mut self, domain_id: &str) -> Result<(), YukiError> {
106 let session = self.require_session()?;
107 let envelope = SoapEnvelope::new("SetCurrentDomain")
108 .session(session)
109 .param("domainID", domain_id)
110 .build();
111 self.soap.call("SetCurrentDomain", envelope).await?;
112 Ok(())
113 }
114
115 pub async fn gl_account_balances(
122 &self,
123 administration_id: &str,
124 transaction_date: &str,
125 ) -> Result<Vec<GlAccountBalance>, YukiError> {
126 let session = self.require_session()?;
127 let envelope = SoapEnvelope::new("GLAccountBalance")
128 .session(session)
129 .param("administrationID", administration_id)
130 .param("transactionDate", transaction_date)
131 .build();
132 let body = self.soap.call("GLAccountBalance", envelope).await?;
133 Self::parse_gl_account_balances(&body)
134 }
135
136 pub async fn gl_account_transactions(
138 &self,
139 administration_id: &str,
140 gl_account_code: &str,
141 start_date: &str,
142 end_date: &str,
143 ) -> Result<String, YukiError> {
144 let session = self.require_session()?;
145 let envelope = SoapEnvelope::new("GLAccountTransactions")
146 .session(session)
147 .param("administrationID", administration_id)
148 .param("GLAccountCode", gl_account_code)
149 .param("StartDate", start_date)
150 .param("EndDate", end_date)
151 .build();
152 self.soap.call("GLAccountTransactions", envelope).await
153 }
154
155 pub async fn outstanding_debtor_items(
157 &self,
158 administration_id: &str,
159 ) -> Result<Vec<OutstandingItem>, YukiError> {
160 let session = self.require_session()?;
161 let envelope = SoapEnvelope::new("OutstandingDebtorItems")
162 .session(session)
163 .param("administrationID", administration_id)
164 .build();
165 let body = self.soap.call("OutstandingDebtorItems", envelope).await?;
166 Self::parse_outstanding_items(&body, "OutstandingDebtorItemsResult")
167 }
168
169 pub async fn outstanding_debtor_items_by_date(
171 &self,
172 administration_id: &str,
173 start_date: &str,
174 end_date: &str,
175 ) -> Result<Vec<OutstandingItem>, YukiError> {
176 let session = self.require_session()?;
177 let envelope = SoapEnvelope::new("OutstandingDebtorItemsByDate")
178 .session(session)
179 .param("administrationID", administration_id)
180 .param("startDate", start_date)
181 .param("endDate", end_date)
182 .build();
183 let body = self
184 .soap
185 .call("OutstandingDebtorItemsByDate", envelope)
186 .await?;
187 Self::parse_outstanding_items(&body, "OutstandingDebtorItemsByDateResult")
188 }
189
190 pub async fn outstanding_creditor_items(
192 &self,
193 administration_id: &str,
194 ) -> Result<Vec<OutstandingItem>, YukiError> {
195 let session = self.require_session()?;
196 let envelope = SoapEnvelope::new("OutstandingCreditorItems")
197 .session(session)
198 .param("administrationID", administration_id)
199 .build();
200 let body = self.soap.call("OutstandingCreditorItems", envelope).await?;
201 Self::parse_outstanding_items(&body, "OutstandingCreditorItemsResult")
202 }
203
204 pub async fn outstanding_creditor_items_by_date(
206 &self,
207 administration_id: &str,
208 start_date: &str,
209 end_date: &str,
210 ) -> Result<Vec<OutstandingItem>, YukiError> {
211 let session = self.require_session()?;
212 let envelope = SoapEnvelope::new("OutstandingCreditorItemsByDate")
213 .session(session)
214 .param("administrationID", administration_id)
215 .param("startDate", start_date)
216 .param("endDate", end_date)
217 .build();
218 let body = self
219 .soap
220 .call("OutstandingCreditorItemsByDate", envelope)
221 .await?;
222 Self::parse_outstanding_items(&body, "OutstandingCreditorItemsByDateResult")
223 }
224
225 pub async fn gl_account_transactions_and_contact(
227 &self,
228 administration_id: &str,
229 gl_account_code: &str,
230 start_date: &str,
231 end_date: &str,
232 ) -> Result<Vec<GlTransactionWithContact>, YukiError> {
233 let session = self.require_session()?;
234 let envelope = SoapEnvelope::new("GLAccountTransactionsAndContact")
235 .session(session)
236 .param("administrationID", administration_id)
237 .param("GLAccountCode", gl_account_code)
238 .param("StartDate", start_date)
239 .param("EndDate", end_date)
240 .build();
241 let body = self
242 .soap
243 .call("GLAccountTransactionsAndContact", envelope)
244 .await?;
245 Self::parse_gl_transactions_with_contact(&body)
246 }
247
248 pub async fn net_revenue(
250 &self,
251 administration_id: &str,
252 start_date: &str,
253 end_date: &str,
254 ) -> Result<String, YukiError> {
255 let session = self.require_session()?;
256 let envelope = SoapEnvelope::new("NetRevenue")
257 .session(session)
258 .param("administrationID", administration_id)
259 .param("StartDate", start_date)
260 .param("EndDate", end_date)
261 .build();
262 let body = self.soap.call("NetRevenue", envelope).await?;
263 SoapClient::parse_single_result(&body, "NetRevenueResult")
264 }
265
266 pub async fn check_outstanding_item_admin(
268 &self,
269 administration_id: &str,
270 reference: &str,
271 ) -> Result<String, YukiError> {
272 let session = self.require_session()?;
273 let envelope = SoapEnvelope::new("CheckOutstandingItemAdmin")
274 .session(session)
275 .param("administrationID", administration_id)
276 .param("Reference", reference)
277 .build();
278 self.soap.call("CheckOutstandingItemAdmin", envelope).await
279 }
280
281 pub fn parse_gl_account_balances(xml: &str) -> Result<Vec<GlAccountBalance>, YukiError> {
287 if let Some(err) = SoapClient::parse_soap_fault(xml) {
288 return Err(err);
289 }
290 let mut reader = Reader::from_str(xml);
291 reader.config_mut().trim_text(true);
292
293 let mut balances = Vec::new();
294 let mut in_account = false;
295 let mut field: Option<String> = None;
296 let mut current = GlAccountBalance {
297 code: String::new(),
298 description: String::new(),
299 balance_type: String::new(),
300 amount: String::new(),
301 };
302 let mut buf = Vec::new();
303
304 loop {
305 match reader.read_event_into(&mut buf) {
306 Ok(Event::Start(ref e)) => {
307 let local = local_name(e.name().as_ref()).to_string();
308 match local.as_str() {
309 "GLAccount" => {
310 in_account = true;
311 current = GlAccountBalance {
312 code: String::new(),
313 description: String::new(),
314 balance_type: String::new(),
315 amount: String::new(),
316 };
317 for attr in e.attributes().flatten() {
318 match attr.key.as_ref() {
319 b"Code" => {
320 current.code =
321 String::from_utf8_lossy(&attr.value).to_string();
322 }
323 b"BalanceType" => {
324 current.balance_type =
325 String::from_utf8_lossy(&attr.value).to_string();
326 }
327 _ => {}
328 }
329 }
330 }
331 "Description" | "Amount" if in_account => {
332 field = Some(local);
333 }
334 _ => {}
335 }
336 }
337 Ok(Event::Text(ref e)) => {
338 if let Some(ref f) = field {
339 let text = e
340 .unescape()
341 .map_err(|e| YukiError::Xml(e.to_string()))?
342 .trim()
343 .to_string();
344 match f.as_str() {
345 "Description" => current.description = text,
346 "Amount" => current.amount = text,
347 _ => {}
348 }
349 }
350 }
351 Ok(Event::End(ref e)) => {
352 let local = local_name(e.name().as_ref()).to_string();
353 match local.as_str() {
354 "Description" | "Amount" => field = None,
355 "GLAccount" if in_account => {
356 balances.push(current.clone());
357 in_account = false;
358 }
359 _ => {}
360 }
361 }
362 Ok(Event::Eof) => break,
363 Err(e) => return Err(YukiError::Xml(e.to_string())),
364 _ => {}
365 }
366 buf.clear();
367 }
368
369 Ok(balances)
370 }
371
372 pub fn parse_gl_transactions(xml: &str) -> Result<Vec<GlTransaction>, YukiError> {
377 let mut reader = Reader::from_str(xml);
378 reader.config_mut().trim_text(true);
379
380 let mut transactions = Vec::new();
381 let mut in_transaction = false;
382 let mut field: Option<String> = None;
383 let mut current = GlTransaction {
384 id: String::new(),
385 date: String::new(),
386 description: String::new(),
387 gl_account: String::new(),
388 amount: String::new(),
389 };
390 let mut buf = Vec::new();
391
392 loop {
393 match reader.read_event_into(&mut buf) {
394 Ok(Event::Start(ref e)) => {
395 let local = local_name(e.name().as_ref()).to_string();
396 match local.as_str() {
397 "GLAccountTransaction" => {
398 in_transaction = true;
399 current = GlTransaction {
400 id: String::new(),
401 date: String::new(),
402 description: String::new(),
403 gl_account: String::new(),
404 amount: String::new(),
405 };
406 for attr in e.attributes().flatten() {
407 if attr.key.as_ref() == b"ID" {
408 current.id = String::from_utf8_lossy(&attr.value).to_string();
409 }
410 }
411 }
412 "Date" | "Description" | "Amount" | "GLAccountCode" if in_transaction => {
413 field = Some(local);
414 }
415 _ => {}
416 }
417 }
418 Ok(Event::Text(ref e)) => {
419 if let Some(ref f) = field {
420 let text = e
421 .unescape()
422 .map_err(|e| YukiError::Xml(e.to_string()))?
423 .trim()
424 .to_string();
425 match f.as_str() {
426 "Date" => current.date = text,
427 "Description" => current.description = text,
428 "Amount" => current.amount = text,
429 "GLAccountCode" => current.gl_account = text,
430 _ => {}
431 }
432 }
433 }
434 Ok(Event::End(ref e)) => {
435 let local = local_name(e.name().as_ref()).to_string();
436 match local.as_str() {
437 "Date" | "Description" | "Amount" | "GLAccountCode" => {
438 field = None;
439 }
440 "GLAccountTransaction" if in_transaction => {
441 transactions.push(current.clone());
442 in_transaction = false;
443 }
444 _ => {}
445 }
446 }
447 Ok(Event::Eof) => break,
448 Err(e) => return Err(YukiError::Xml(e.to_string())),
449 _ => {}
450 }
451 buf.clear();
452 }
453
454 Ok(transactions)
455 }
456
457 pub fn parse_gl_transactions_with_contact(
461 xml: &str,
462 ) -> Result<Vec<GlTransactionWithContact>, YukiError> {
463 let mut reader = Reader::from_str(xml);
464 reader.config_mut().trim_text(true);
465
466 let mut transactions = Vec::new();
467 let mut in_transaction = false;
468 let mut field: Option<String> = None;
469 let mut current = GlTransactionWithContact {
470 id: String::new(),
471 date: String::new(),
472 description: String::new(),
473 gl_account: String::new(),
474 amount: String::new(),
475 contact_name: String::new(),
476 };
477 let mut buf = Vec::new();
478
479 loop {
480 match reader.read_event_into(&mut buf) {
481 Ok(Event::Start(ref e)) => {
482 let local = local_name(e.name().as_ref()).to_string();
483 match local.as_str() {
484 "GLAccountTransaction" => {
485 in_transaction = true;
486 current = GlTransactionWithContact {
487 id: String::new(),
488 date: String::new(),
489 description: String::new(),
490 gl_account: String::new(),
491 amount: String::new(),
492 contact_name: String::new(),
493 };
494 for attr in e.attributes().flatten() {
495 if attr.key.as_ref() == b"ID" {
496 current.id = String::from_utf8_lossy(&attr.value).to_string();
497 }
498 }
499 }
500 "Date" | "Description" | "Amount" | "GLAccountCode" | "Contact"
501 | "ContactName"
502 if in_transaction =>
503 {
504 field = Some(local);
505 }
506 _ => {}
507 }
508 }
509 Ok(Event::Text(ref e)) => {
510 if let Some(ref f) = field {
511 let text = e
512 .unescape()
513 .map_err(|e| YukiError::Xml(e.to_string()))?
514 .trim()
515 .to_string();
516 match f.as_str() {
517 "Date" => current.date = text,
518 "Description" => current.description = text,
519 "Amount" => current.amount = text,
520 "GLAccountCode" => current.gl_account = text,
521 "Contact" | "ContactName" => current.contact_name = text,
522 _ => {}
523 }
524 }
525 }
526 Ok(Event::End(ref e)) => {
527 let local = local_name(e.name().as_ref()).to_string();
528 match local.as_str() {
529 "Date" | "Description" | "Amount" | "GLAccountCode" | "Contact"
530 | "ContactName" => {
531 field = None;
532 }
533 "GLAccountTransaction" if in_transaction => {
534 transactions.push(current.clone());
535 in_transaction = false;
536 }
537 _ => {}
538 }
539 }
540 Ok(Event::Eof) => break,
541 Err(e) => return Err(YukiError::Xml(e.to_string())),
542 _ => {}
543 }
544 buf.clear();
545 }
546
547 Ok(transactions)
548 }
549
550 pub fn parse_administrations(xml: &str) -> Result<Vec<Administration>, YukiError> {
556 let mut reader = Reader::from_str(xml);
557 reader.config_mut().trim_text(true);
558
559 let mut administrations = Vec::new();
560 let mut current_id = String::new();
561 let mut current_name = String::new();
562 let mut current_domain_id = String::new();
563 let mut in_administration = false;
564 let mut in_name = false;
565 let mut in_domain_id = false;
566 let mut buf = Vec::new();
567
568 loop {
569 match reader.read_event_into(&mut buf) {
570 Ok(Event::Start(ref e)) => {
571 let local = local_name(e.name().as_ref()).to_string();
572 match local.as_str() {
573 "Administration" => {
574 in_administration = true;
575 current_id.clear();
576 current_name.clear();
577 current_domain_id.clear();
578 for attr in e.attributes().flatten() {
580 if attr.key.as_ref() == b"ID" {
581 current_id = String::from_utf8_lossy(&attr.value).to_string();
582 }
583 }
584 }
585 "Name" if in_administration => in_name = true,
586 "DomainID" if in_administration => in_domain_id = true,
587 _ => {}
588 }
589 }
590 Ok(Event::Text(ref e)) => {
591 let text = e
592 .unescape()
593 .map_err(|e| YukiError::Xml(e.to_string()))?
594 .trim()
595 .to_string();
596 if in_name {
597 current_name = text;
598 } else if in_domain_id {
599 current_domain_id = text;
600 }
601 }
602 Ok(Event::End(ref e)) => {
603 let local = local_name(e.name().as_ref()).to_string();
604 match local.as_str() {
605 "Name" => in_name = false,
606 "DomainID" => in_domain_id = false,
607 "Administration" => {
608 if !current_id.is_empty() {
609 administrations.push(Administration {
610 id: current_id.clone(),
611 name: current_name.clone(),
612 domain_id: current_domain_id.clone(),
613 });
614 }
615 in_administration = false;
616 }
617 _ => {}
618 }
619 }
620 Ok(Event::Eof) => break,
621 Err(e) => return Err(YukiError::Xml(e.to_string())),
622 _ => {}
623 }
624 buf.clear();
625 }
626
627 Ok(administrations)
628 }
629
630 pub fn parse_outstanding_items(
635 xml: &str,
636 result_tag: &str,
637 ) -> Result<Vec<OutstandingItem>, YukiError> {
638 let mut reader = Reader::from_str(xml);
639 reader.config_mut().trim_text(true);
640
641 let mut items = Vec::new();
642 let mut in_result = false;
643 let mut in_item = false;
644 let mut field: Option<String> = None;
645 let mut current = OutstandingItem {
646 contact_name: String::new(),
647 description: String::new(),
648 date: String::new(),
649 amount: String::new(),
650 open_amount: String::new(),
651 };
652 let mut buf = Vec::new();
653
654 loop {
655 match reader.read_event_into(&mut buf) {
656 Ok(Event::Start(ref e)) => {
657 let local = local_name(e.name().as_ref()).to_string();
658 match local.as_str() {
659 tag if tag == result_tag => in_result = true,
660 "Item" if in_result => {
661 in_item = true;
662 current = OutstandingItem {
663 contact_name: String::new(),
664 description: String::new(),
665 date: String::new(),
666 amount: String::new(),
667 open_amount: String::new(),
668 };
669 }
670 "Contact" | "ContactName" | "Description" | "Date" | "Amount"
671 | "OriginalAmount" | "OpenAmount"
672 if in_item =>
673 {
674 field = Some(local);
675 }
676 _ => {}
677 }
678 }
679 Ok(Event::Text(ref e)) => {
680 if let Some(ref f) = field {
681 let text = e
682 .unescape()
683 .map_err(|e| YukiError::Xml(e.to_string()))?
684 .trim()
685 .to_string();
686 match f.as_str() {
687 "Contact" | "ContactName" => current.contact_name = text,
688 "Description" => current.description = text,
689 "Date" => current.date = text,
690 "Amount" | "OriginalAmount" => current.amount = text,
691 "OpenAmount" => current.open_amount = text,
692 _ => {}
693 }
694 }
695 }
696 Ok(Event::End(ref e)) => {
697 let local = local_name(e.name().as_ref()).to_string();
698 match local.as_str() {
699 "Contact" | "ContactName" | "Description" | "Date" | "Amount"
700 | "OriginalAmount" | "OpenAmount" => {
701 field = None;
702 }
703 "Item" if in_item => {
704 items.push(current.clone());
705 in_item = false;
706 }
707 tag if tag == result_tag => {
708 in_result = false;
709 }
710 _ => {}
711 }
712 }
713 Ok(Event::Eof) => break,
714 Err(e) => return Err(YukiError::Xml(e.to_string())),
715 _ => {}
716 }
717 buf.clear();
718 }
719
720 Ok(items)
721 }
722}
723
724impl Default for AccountingClient {
725 fn default() -> Self {
726 Self::new()
727 }
728}