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