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/AccountingInfo.asmx";
10
11#[derive(Debug, Clone)]
13pub struct GlAccount {
14 pub code: String,
15 pub description: String,
16 pub account_type: String,
17}
18
19#[derive(Debug, Clone)]
21pub struct AccountStartBalance {
22 pub gl_account_code: String,
23 pub description: String,
24 pub balance: String,
25}
26
27#[derive(Debug, Clone)]
29pub struct Project {
30 pub id: String,
31 pub code: String,
32 pub description: String,
33}
34
35#[derive(Debug, Clone)]
37pub struct ProjectBalance {
38 pub project_code: String,
39 pub gl_account_code: String,
40 pub amount: String,
41}
42
43#[derive(Debug, Clone)]
45pub struct TransactionDetail {
46 pub id: String,
47 pub date: String,
48 pub description: String,
49 pub amount: String,
50 pub currency: String,
51 pub gl_account_code: String,
52}
53
54pub struct AccountingInfoClient {
56 soap: SoapClient,
57}
58
59impl AccountingInfoClient {
60 pub fn new() -> Self {
61 Self {
62 soap: SoapClient::new(BASE_URL),
63 }
64 }
65
66 pub fn with_client(http: reqwest::Client) -> Self {
69 Self {
70 soap: SoapClient::with_client(BASE_URL, http),
71 }
72 }
73
74 fn require_session(&self) -> Result<&str, YukiError> {
75 self.soap.session_id().ok_or_else(|| {
76 YukiError::AuthFailed("not authenticated — call authenticate() first".to_string())
77 })
78 }
79
80 pub async fn authenticate(&mut self, api_key: &str) -> Result<String, YukiError> {
82 self.soap.authenticate(api_key).await
83 }
84
85 pub async fn get_transaction_details(
87 &self,
88 transaction_id: &str,
89 ) -> Result<Vec<TransactionDetail>, YukiError> {
90 let session = self.require_session()?;
91 let envelope = SoapEnvelope::new("GetTransactionDetails")
92 .session(session)
93 .param("transactionId", transaction_id)
94 .build();
95 let body = self.soap.call("GetTransactionDetails", envelope).await?;
96 Self::parse_transaction_details(&body)
97 }
98
99 pub fn parse_transaction_details(xml: &str) -> Result<Vec<TransactionDetail>, YukiError> {
104 let mut reader = Reader::from_str(xml);
105 reader.config_mut().trim_text(true);
106
107 let mut details = Vec::new();
108 let mut in_info = false;
109 let mut field: Option<String> = None;
110 let mut current = TransactionDetail {
111 id: String::new(),
112 date: String::new(),
113 description: String::new(),
114 amount: String::new(),
115 currency: String::new(),
116 gl_account_code: String::new(),
117 };
118 let mut buf = Vec::new();
119
120 loop {
121 match reader.read_event_into(&mut buf) {
122 Ok(Event::Start(ref e)) => {
123 let local = local_name(e.name().as_ref()).to_string();
124 match local.as_str() {
125 "TransactionInfo" => {
126 in_info = true;
127 current = TransactionDetail {
128 id: String::new(),
129 date: String::new(),
130 description: String::new(),
131 amount: String::new(),
132 currency: String::new(),
133 gl_account_code: String::new(),
134 };
135 }
136 "id" | "transactionDate" | "description" | "transactionAmount"
137 | "currency" | "glAccountCode"
138 if in_info =>
139 {
140 field = Some(local);
141 }
142 _ => {}
143 }
144 }
145 Ok(Event::Text(ref e)) => {
146 if let Some(ref f) = field {
147 let text = e
148 .unescape()
149 .map_err(|e| YukiError::Xml(e.to_string()))?
150 .trim()
151 .to_string();
152 match f.as_str() {
153 "id" => current.id = text,
154 "transactionDate" => current.date = text,
155 "description" => current.description = text,
156 "transactionAmount" => current.amount = text,
157 "currency" => current.currency = text,
158 "glAccountCode" => current.gl_account_code = text,
159 _ => {}
160 }
161 }
162 }
163 Ok(Event::End(ref e)) => {
164 let local = local_name(e.name().as_ref()).to_string();
165 match local.as_str() {
166 "id" | "transactionDate" | "description" | "transactionAmount"
167 | "currency" | "glAccountCode" => {
168 field = None;
169 }
170 "TransactionInfo" if in_info => {
171 details.push(current.clone());
172 in_info = false;
173 }
174 _ => {}
175 }
176 }
177 Ok(Event::Eof) => break,
178 Err(e) => return Err(YukiError::Xml(e.to_string())),
179 _ => {}
180 }
181 buf.clear();
182 }
183
184 Ok(details)
185 }
186
187 pub async fn get_gl_account_scheme(
189 &self,
190 administration_id: &str,
191 ) -> Result<Vec<GlAccount>, YukiError> {
192 let session = self.require_session()?;
193 let envelope = SoapEnvelope::new("GetGLAccountScheme")
194 .session(session)
195 .param("administrationID", administration_id)
196 .build();
197 let body = self.soap.call("GetGLAccountScheme", envelope).await?;
198 Self::parse_gl_accounts(&body)
199 }
200
201 pub async fn get_transaction_document(
203 &self,
204 administration_id: &str,
205 transaction_id: &str,
206 ) -> Result<String, YukiError> {
207 let session = self.require_session()?;
208 let envelope = SoapEnvelope::new("GetTransactionDocument")
209 .session(session)
210 .param("administrationID", administration_id)
211 .param("transactionID", transaction_id)
212 .build();
213 self.soap.call("GetTransactionDocument", envelope).await
214 }
215
216 pub async fn get_period_date_table(&self, year: &str) -> Result<String, YukiError> {
218 let session = self.require_session()?;
219 let envelope = SoapEnvelope::new("GetPeriodDateTable")
220 .session(session)
221 .param("year", year)
222 .build();
223 self.soap.call("GetPeriodDateTable", envelope).await
224 }
225
226 pub async fn get_start_balance_by_gl_account(
228 &self,
229 administration_id: &str,
230 bookyear: &str,
231 ) -> Result<Vec<AccountStartBalance>, YukiError> {
232 let session = self.require_session()?;
233 let envelope = SoapEnvelope::new("GetStartBalanceByGlAccount")
234 .session(session)
235 .param("administrationID", administration_id)
236 .param("bookyear", bookyear)
237 .param("financialMode", "0")
238 .build();
239 let body = self
240 .soap
241 .call("GetStartBalanceByGlAccount", envelope)
242 .await?;
243 Self::parse_start_balances(&body)
244 }
245
246 pub async fn get_projects(&self, administration_id: &str) -> Result<Vec<Project>, YukiError> {
248 let session = self.require_session()?;
249 let envelope = SoapEnvelope::new("GetProjectsAndID")
250 .session(session)
251 .param("administrationID", administration_id)
252 .param("searchOption", "")
253 .param("searchValue", "")
254 .build();
255 let body = self.soap.call("GetProjectsAndID", envelope).await?;
256 Self::parse_projects(&body)
257 }
258
259 pub async fn get_project_balance(
261 &self,
262 administration_id: &str,
263 project_code: &str,
264 gl_account_code: &str,
265 start_date: &str,
266 end_date: &str,
267 ) -> Result<Vec<ProjectBalance>, YukiError> {
268 let session = self.require_session()?;
269 let envelope = SoapEnvelope::new("GetProjectBalance")
270 .session(session)
271 .param("administrationID", administration_id)
272 .param("GLAccountCode", gl_account_code)
273 .param("projectCode", project_code)
274 .param("StartDate", start_date)
275 .param("EndDate", end_date)
276 .build();
277 let body = self.soap.call("GetProjectBalance", envelope).await?;
278 Self::parse_project_balances(&body)
279 }
280
281 pub fn parse_gl_accounts(xml: &str) -> Result<Vec<GlAccount>, YukiError> {
285 let mut reader = Reader::from_str(xml);
286 reader.config_mut().trim_text(true);
287
288 let mut accounts = Vec::new();
289 let mut in_account = false;
290 let mut field: Option<String> = None;
291 let mut current = GlAccount {
292 code: String::new(),
293 description: String::new(),
294 account_type: String::new(),
295 };
296 let mut buf = Vec::new();
297
298 loop {
299 match reader.read_event_into(&mut buf) {
300 Ok(Event::Start(ref e)) => {
301 let local = local_name(e.name().as_ref()).to_string();
302 match local.as_str() {
303 "GlAccount" | "GLAccount" => {
304 in_account = true;
305 current = GlAccount {
306 code: String::new(),
307 description: String::new(),
308 account_type: String::new(),
309 };
310 }
311 "Code" | "code" | "Description" | "description" | "descripton" | "Type"
312 | "type"
313 if in_account =>
314 {
315 field = Some(local);
316 }
317 _ => {}
318 }
319 }
320 Ok(Event::Text(ref e)) => {
321 if let Some(ref f) = field {
322 let text = e
323 .unescape()
324 .map_err(|e| YukiError::Xml(e.to_string()))?
325 .trim()
326 .to_string();
327 match f.as_str() {
328 "Code" | "code" => current.code = text,
329 "Description" | "description" | "descripton" => {
330 current.description = text;
331 }
332 "Type" | "type" => current.account_type = text,
333 _ => {}
334 }
335 }
336 }
337 Ok(Event::End(ref e)) => {
338 let local = local_name(e.name().as_ref()).to_string();
339 match local.as_str() {
340 "Code" | "code" | "Description" | "description" | "descripton" | "Type"
341 | "type" => {
342 field = None;
343 }
344 "GlAccount" | "GLAccount" if in_account => {
345 accounts.push(current.clone());
346 in_account = false;
347 }
348 _ => {}
349 }
350 }
351 Ok(Event::Eof) => break,
352 Err(e) => return Err(YukiError::Xml(e.to_string())),
353 _ => {}
354 }
355 buf.clear();
356 }
357
358 Ok(accounts)
359 }
360
361 pub fn parse_start_balances(xml: &str) -> Result<Vec<AccountStartBalance>, YukiError> {
362 let mut reader = Reader::from_str(xml);
363 reader.config_mut().trim_text(true);
364
365 let mut balances = Vec::new();
366 let mut in_item = false;
367 let mut field: Option<String> = None;
368 let mut current = AccountStartBalance {
369 gl_account_code: String::new(),
370 description: String::new(),
371 balance: String::new(),
372 };
373 let mut buf = Vec::new();
374
375 loop {
376 match reader.read_event_into(&mut buf) {
377 Ok(Event::Start(ref e)) => {
378 let local = local_name(e.name().as_ref()).to_string();
379 match local.as_str() {
380 "AccountStartBalance" => {
381 in_item = true;
382 current = AccountStartBalance {
383 gl_account_code: String::new(),
384 description: String::new(),
385 balance: String::new(),
386 };
387 }
388 "GLAccountCode" | "glAccountCode" | "accountID" | "Description"
389 | "description" | "accountDescription" | "Balance" | "balance"
390 | "StartBalance" | "startBalance"
391 if in_item =>
392 {
393 field = Some(local);
394 }
395 _ => {}
396 }
397 }
398 Ok(Event::Text(ref e)) => {
399 if let Some(ref f) = field {
400 let text = e
401 .unescape()
402 .map_err(|e| YukiError::Xml(e.to_string()))?
403 .trim()
404 .to_string();
405 match f.as_str() {
406 "GLAccountCode" | "glAccountCode" | "accountID" => {
407 current.gl_account_code = text;
408 }
409 "Description" | "description" | "accountDescription" => {
410 current.description = text;
411 }
412 "Balance" | "balance" | "StartBalance" | "startBalance" => {
413 current.balance = text;
414 }
415 _ => {}
416 }
417 }
418 }
419 Ok(Event::End(ref e)) => {
420 let local = local_name(e.name().as_ref()).to_string();
421 match local.as_str() {
422 "GLAccountCode" | "glAccountCode" | "accountID" | "Description"
423 | "description" | "accountDescription" | "Balance" | "balance"
424 | "StartBalance" | "startBalance" => {
425 field = None;
426 }
427 "AccountStartBalance" if in_item => {
428 balances.push(current.clone());
429 in_item = false;
430 }
431 _ => {}
432 }
433 }
434 Ok(Event::Eof) => break,
435 Err(e) => return Err(YukiError::Xml(e.to_string())),
436 _ => {}
437 }
438 buf.clear();
439 }
440
441 Ok(balances)
442 }
443
444 fn parse_projects(xml: &str) -> Result<Vec<Project>, YukiError> {
445 let mut reader = Reader::from_str(xml);
446 reader.config_mut().trim_text(true);
447
448 let mut projects = Vec::new();
449 let mut in_item = false;
450 let mut field: Option<String> = None;
451 let mut current = Project {
452 id: String::new(),
453 code: String::new(),
454 description: String::new(),
455 };
456 let mut buf = Vec::new();
457
458 loop {
459 match reader.read_event_into(&mut buf) {
460 Ok(Event::Start(ref e)) => {
461 let local = local_name(e.name().as_ref()).to_string();
462 match local.as_str() {
463 "Project" => {
464 in_item = true;
465 current = Project {
466 id: String::new(),
467 code: String::new(),
468 description: String::new(),
469 };
470 for attr in e.attributes().flatten() {
471 if attr.key.as_ref() == b"ID" {
472 current.id = String::from_utf8_lossy(&attr.value).to_string();
473 }
474 }
475 }
476 "Code" | "code" | "Description" | "description" if in_item => {
477 field = Some(local);
478 }
479 _ => {}
480 }
481 }
482 Ok(Event::Text(ref e)) => {
483 if let Some(ref f) = field {
484 let text = e
485 .unescape()
486 .map_err(|e| YukiError::Xml(e.to_string()))?
487 .trim()
488 .to_string();
489 match f.as_str() {
490 "Code" | "code" => current.code = text,
491 "Description" | "description" => current.description = text,
492 _ => {}
493 }
494 }
495 }
496 Ok(Event::End(ref e)) => {
497 let local = local_name(e.name().as_ref()).to_string();
498 match local.as_str() {
499 "Code" | "code" | "Description" | "description" => {
500 field = None;
501 }
502 "Project" if in_item => {
503 projects.push(current.clone());
504 in_item = false;
505 }
506 _ => {}
507 }
508 }
509 Ok(Event::Eof) => break,
510 Err(e) => return Err(YukiError::Xml(e.to_string())),
511 _ => {}
512 }
513 buf.clear();
514 }
515
516 Ok(projects)
517 }
518
519 fn parse_project_balances(xml: &str) -> Result<Vec<ProjectBalance>, YukiError> {
520 let mut reader = Reader::from_str(xml);
521 reader.config_mut().trim_text(true);
522
523 let mut balances = Vec::new();
524 let mut in_item = false;
525 let mut field: Option<String> = None;
526 let mut current = ProjectBalance {
527 project_code: String::new(),
528 gl_account_code: String::new(),
529 amount: String::new(),
530 };
531 let mut buf = Vec::new();
532
533 loop {
534 match reader.read_event_into(&mut buf) {
535 Ok(Event::Start(ref e)) => {
536 let local = local_name(e.name().as_ref()).to_string();
537 match local.as_str() {
538 "ProjectBalance" => {
539 in_item = true;
540 current = ProjectBalance {
541 project_code: String::new(),
542 gl_account_code: String::new(),
543 amount: String::new(),
544 };
545 }
546 "ProjectCode" | "projectCode" | "GLAccountCode" | "glAccountCode"
547 | "Amount" | "amount"
548 if in_item =>
549 {
550 field = Some(local);
551 }
552 _ => {}
553 }
554 }
555 Ok(Event::Text(ref e)) => {
556 if let Some(ref f) = field {
557 let text = e
558 .unescape()
559 .map_err(|e| YukiError::Xml(e.to_string()))?
560 .trim()
561 .to_string();
562 match f.as_str() {
563 "ProjectCode" | "projectCode" => current.project_code = text,
564 "GLAccountCode" | "glAccountCode" => current.gl_account_code = text,
565 "Amount" | "amount" => current.amount = text,
566 _ => {}
567 }
568 }
569 }
570 Ok(Event::End(ref e)) => {
571 let local = local_name(e.name().as_ref()).to_string();
572 match local.as_str() {
573 "ProjectCode" | "projectCode" | "GLAccountCode" | "glAccountCode"
574 | "Amount" | "amount" => {
575 field = None;
576 }
577 "ProjectBalance" if in_item => {
578 balances.push(current.clone());
579 in_item = false;
580 }
581 _ => {}
582 }
583 }
584 Ok(Event::Eof) => break,
585 Err(e) => return Err(YukiError::Xml(e.to_string())),
586 _ => {}
587 }
588 buf.clear();
589 }
590
591 Ok(balances)
592 }
593}
594
595impl Default for AccountingInfoClient {
596 fn default() -> Self {
597 Self::new()
598 }
599}