1use quick_xml::events::Event;
2
3use crate::aggregates::{
4 AvailableBalance, Balance, BankAccount, CcStatementResponse, CreditCardAccount, CurrencyInfo,
5 InvStatementResponse, InvestmentAccount, LedgerBalance, Payee, StatementResponse,
6 StatementTransactionBuilder, Status, TransactionList, TransactionWrapper,
7};
8use crate::document::{
9 BankingMessageSet, CreditCardMessageSet, InvestmentMessageSet, OfxDocument, SignonResponse,
10};
11use crate::error::OfxError;
12use crate::header::{self, OfxHeader};
13use crate::xml::{OfxReader, XmlError};
14
15pub fn parse(input: &str) -> Result<OfxDocument, OfxError> {
25 let (ofx_header, xml_body) = header::parse_header(input)?;
26 let xml_body = xml_body.trim();
27
28 if ofx_header.version().major() < 2 {
31 let normalized = crate::sgml::normalize_sgml_to_xml(xml_body);
32 parse_ofx_body(&normalized, ofx_header)
33 } else {
34 parse_ofx_body(xml_body, ofx_header)
35 }
36}
37
38fn tag_name(e: &quick_xml::events::BytesStart<'_>) -> String {
43 String::from_utf8_lossy(e.name().as_ref()).into_owned()
44}
45
46fn end_tag_name(e: &quick_xml::events::BytesEnd<'_>) -> String {
47 String::from_utf8_lossy(e.name().as_ref()).into_owned()
48}
49
50fn read_text(reader: &mut OfxReader<'_>, tag: &str) -> Result<String, XmlError> {
52 reader.read_text(tag)
53}
54
55fn parse_text_as<T: core::str::FromStr>(
58 text: &str,
59 element: &str,
60) -> Result<T, XmlError> {
61 text.parse::<T>().map_err(|_| XmlError::InvalidContent {
62 element: element.to_owned(),
63 value: text.to_owned(),
64 reason: format!("failed to parse as {}", core::any::type_name::<T>()),
65 })
66}
67
68fn parse_ofx_body(xml: &str, header: OfxHeader) -> Result<OfxDocument, OfxError> {
73 let mut reader = OfxReader::new(xml);
74 let mut signon: Option<SignonResponse> = None;
75 let mut banking: Option<BankingMessageSet> = None;
76 let mut credit_card: Option<CreditCardMessageSet> = None;
77 let mut investment: Option<InvestmentMessageSet> = None;
78
79 loop {
81 match reader.next_event()? {
82 Event::Start(e) if tag_name(&e) == "OFX" => break,
83 Event::Eof => {
84 return Err(XmlError::MissingElement {
85 parent: "document".to_owned(),
86 element: "OFX".to_owned(),
87 }
88 .into());
89 }
90 _ => {}
91 }
92 }
93
94 loop {
96 match reader.next_event()? {
97 Event::Start(e) => {
98 let name = tag_name(&e);
99 match name.as_str() {
100 "SIGNONMSGSRSV1" => {
101 signon = Some(parse_signon_message_set(&mut reader)?);
102 }
103 "BANKMSGSRSV1" => {
104 banking = Some(parse_banking_message_set(&mut reader)?);
105 }
106 "CREDITCARDMSGSRSV1" => {
107 credit_card = Some(parse_cc_message_set(&mut reader)?);
108 }
109 "INVSTMTMSGSRSV1" => {
110 investment = Some(parse_investment_message_set(&mut reader)?);
111 }
112 _ => {
113 reader.skip_element(&name)?;
114 }
115 }
116 }
117 Event::End(e) if end_tag_name(&e) == "OFX" => break,
118 Event::Eof => break,
119 _ => {}
120 }
121 }
122
123 let signon = signon.ok_or_else(|| XmlError::MissingElement {
124 parent: "OFX".to_owned(),
125 element: "SIGNONMSGSRSV1".to_owned(),
126 })?;
127
128 let mut doc = OfxDocument::new(header, signon);
129 if let Some(b) = banking {
130 doc = doc.with_banking(b);
131 }
132 if let Some(cc) = credit_card {
133 doc = doc.with_credit_card(cc);
134 }
135 if let Some(inv) = investment {
136 doc = doc.with_investment(inv);
137 }
138
139 Ok(doc)
140}
141
142fn parse_signon_message_set(reader: &mut OfxReader<'_>) -> Result<SignonResponse, OfxError> {
147 loop {
148 match reader.next_event()? {
149 Event::Start(e) => {
150 let name = tag_name(&e);
151 if name == "SONRS" {
152 let sonrs = parse_sonrs(reader)?;
153 reader.skip_to_end("SIGNONMSGSRSV1")?;
154 return Ok(sonrs);
155 }
156 reader.skip_element(&name)?;
157 }
158 Event::End(e) if end_tag_name(&e) == "SIGNONMSGSRSV1" => {
159 return Err(XmlError::MissingElement {
160 parent: "SIGNONMSGSRSV1".to_owned(),
161 element: "SONRS".to_owned(),
162 }
163 .into());
164 }
165 Event::Eof => {
166 return Err(XmlError::MalformedXml {
167 message: "unexpected EOF in SIGNONMSGSRSV1".to_owned(),
168 }
169 .into());
170 }
171 _ => {}
172 }
173 }
174}
175
176fn parse_sonrs(reader: &mut OfxReader<'_>) -> Result<SignonResponse, OfxError> {
177 let mut status: Option<Status> = None;
178 let mut dtserver: Option<crate::types::OfxDateTime> = None;
179 let mut language: Option<String> = None;
180 let mut fi_org: Option<String> = None;
181 let mut fi_id: Option<String> = None;
182 let mut session_cookie: Option<String> = None;
183 let mut access_key: Option<String> = None;
184
185 loop {
186 match reader.next_event()? {
187 Event::Start(e) => {
188 let name = tag_name(&e);
189 match name.as_str() {
190 "STATUS" => status = Some(parse_status(reader)?),
191 "DTSERVER" => {
192 let text = read_text(reader, "DTSERVER")?;
193 dtserver = Some(parse_text_as(&text, "DTSERVER")?);
194 }
195 "LANGUAGE" => language = Some(read_text(reader, "LANGUAGE")?),
196 "FI" => {
197 let (org, id) = parse_fi(reader)?;
198 fi_org = org;
199 fi_id = id;
200 }
201 "SESSCOOKIE" => session_cookie = Some(read_text(reader, "SESSCOOKIE")?),
202 "ACCESSKEY" => access_key = Some(read_text(reader, "ACCESSKEY")?),
203 _ => reader.skip_element(&name)?,
204 }
205 }
206 Event::End(e) if end_tag_name(&e) == "SONRS" => break,
207 Event::Eof => {
208 return Err(XmlError::MalformedXml {
209 message: "unexpected EOF in SONRS".to_owned(),
210 }
211 .into());
212 }
213 _ => {}
214 }
215 }
216
217 let status = status.ok_or_else(|| XmlError::MissingElement {
218 parent: "SONRS".to_owned(),
219 element: "STATUS".to_owned(),
220 })?;
221 let dtserver = dtserver.ok_or_else(|| XmlError::MissingElement {
222 parent: "SONRS".to_owned(),
223 element: "DTSERVER".to_owned(),
224 })?;
225 let language = language.unwrap_or_else(|| "ENG".to_owned());
226
227 let mut sonrs = SignonResponse::new(status, dtserver, language);
228 if let Some(org) = fi_org {
229 sonrs = sonrs.with_fi_org(org);
230 }
231 if let Some(id) = fi_id {
232 sonrs = sonrs.with_fi_id(id);
233 }
234 if let Some(cookie) = session_cookie {
235 sonrs = sonrs.with_session_cookie(cookie);
236 }
237 if let Some(key) = access_key {
238 sonrs = sonrs.with_access_key(key);
239 }
240 Ok(sonrs)
241}
242
243fn parse_fi(reader: &mut OfxReader<'_>) -> Result<(Option<String>, Option<String>), OfxError> {
244 let mut org: Option<String> = None;
245 let mut fid: Option<String> = None;
246
247 loop {
248 match reader.next_event()? {
249 Event::Start(e) => {
250 let name = tag_name(&e);
251 match name.as_str() {
252 "ORG" => org = Some(read_text(reader, "ORG")?),
253 "FID" => fid = Some(read_text(reader, "FID")?),
254 _ => reader.skip_element(&name)?,
255 }
256 }
257 Event::End(e) if end_tag_name(&e) == "FI" => break,
258 Event::Eof => {
259 return Err(XmlError::MalformedXml {
260 message: "unexpected EOF in FI".to_owned(),
261 }
262 .into());
263 }
264 _ => {}
265 }
266 }
267 Ok((org, fid))
268}
269
270fn parse_status(reader: &mut OfxReader<'_>) -> Result<Status, OfxError> {
275 let mut code: Option<u32> = None;
276 let mut severity: Option<crate::types::Severity> = None;
277 let mut message: Option<String> = None;
278
279 loop {
280 match reader.next_event()? {
281 Event::Start(e) => {
282 let name = tag_name(&e);
283 match name.as_str() {
284 "CODE" => {
285 let text = read_text(reader, "CODE")?;
286 code = Some(parse_text_as(&text, "CODE")?);
287 }
288 "SEVERITY" => {
289 let text = read_text(reader, "SEVERITY")?;
290 severity = Some(parse_text_as(&text, "SEVERITY")?);
291 }
292 "MESSAGE" => message = Some(read_text(reader, "MESSAGE")?),
293 _ => reader.skip_element(&name)?,
294 }
295 }
296 Event::End(e) if end_tag_name(&e) == "STATUS" => break,
297 Event::Eof => {
298 return Err(XmlError::MalformedXml {
299 message: "unexpected EOF in STATUS".to_owned(),
300 }
301 .into());
302 }
303 _ => {}
304 }
305 }
306
307 let code = code.ok_or_else(|| XmlError::MissingElement {
308 parent: "STATUS".to_owned(),
309 element: "CODE".to_owned(),
310 })?;
311 let severity = severity.ok_or_else(|| XmlError::MissingElement {
312 parent: "STATUS".to_owned(),
313 element: "SEVERITY".to_owned(),
314 })?;
315
316 Ok(Status::new(code, severity, message))
317}
318
319fn parse_banking_message_set(reader: &mut OfxReader<'_>) -> Result<BankingMessageSet, OfxError> {
324 let mut responses = Vec::new();
325
326 loop {
327 match reader.next_event()? {
328 Event::Start(e) => {
329 let name = tag_name(&e);
330 if name == "STMTTRNRS" {
331 responses.push(parse_stmttrnrs(reader)?);
332 } else {
333 reader.skip_element(&name)?;
334 }
335 }
336 Event::End(e) if end_tag_name(&e) == "BANKMSGSRSV1" => break,
337 Event::Eof => {
338 return Err(XmlError::MalformedXml {
339 message: "unexpected EOF in BANKMSGSRSV1".to_owned(),
340 }
341 .into());
342 }
343 _ => {}
344 }
345 }
346
347 Ok(BankingMessageSet::new(responses))
348}
349
350fn parse_stmttrnrs(
351 reader: &mut OfxReader<'_>,
352) -> Result<TransactionWrapper<StatementResponse>, OfxError> {
353 let mut trnuid: Option<String> = None;
354 let mut status: Option<Status> = None;
355 let mut client_cookie: Option<String> = None;
356 let mut stmtrs: Option<StatementResponse> = None;
357
358 loop {
359 match reader.next_event()? {
360 Event::Start(e) => {
361 let name = tag_name(&e);
362 match name.as_str() {
363 "TRNUID" => trnuid = Some(read_text(reader, "TRNUID")?),
364 "STATUS" => status = Some(parse_status(reader)?),
365 "CLTCOOKIE" => client_cookie = Some(read_text(reader, "CLTCOOKIE")?),
366 "STMTRS" => stmtrs = Some(parse_stmtrs(reader)?),
367 _ => reader.skip_element(&name)?,
368 }
369 }
370 Event::End(e) if end_tag_name(&e) == "STMTTRNRS" => break,
371 Event::Eof => {
372 return Err(XmlError::MalformedXml {
373 message: "unexpected EOF in STMTTRNRS".to_owned(),
374 }
375 .into());
376 }
377 _ => {}
378 }
379 }
380
381 let trnuid = trnuid.ok_or_else(|| XmlError::MissingElement {
382 parent: "STMTTRNRS".to_owned(),
383 element: "TRNUID".to_owned(),
384 })?;
385 let status = status.ok_or_else(|| XmlError::MissingElement {
386 parent: "STMTTRNRS".to_owned(),
387 element: "STATUS".to_owned(),
388 })?;
389
390 let mut wrapper = TransactionWrapper::new(trnuid, status, stmtrs);
391 if let Some(cookie) = client_cookie {
392 wrapper = wrapper.with_client_cookie(cookie);
393 }
394 Ok(wrapper)
395}
396
397fn parse_stmtrs(reader: &mut OfxReader<'_>) -> Result<StatementResponse, OfxError> {
398 let mut curdef: Option<crate::types::CurrencyCode> = None;
399 let mut bank_acct: Option<BankAccount> = None;
400 let mut transaction_list: Option<TransactionList> = None;
401 let mut ledger_balance: Option<LedgerBalance> = None;
402 let mut available_balance: Option<AvailableBalance> = None;
403 let mut balance_list: Vec<Balance> = Vec::new();
404 let mut marketing_info: Option<String> = None;
405
406 loop {
407 match reader.next_event()? {
408 Event::Start(e) => {
409 let name = tag_name(&e);
410 match name.as_str() {
411 "CURDEF" => {
412 let text = read_text(reader, "CURDEF")?;
413 curdef = Some(parse_text_as(&text, "CURDEF")?);
414 }
415 "BANKACCTFROM" => bank_acct = Some(parse_bank_account(reader, "BANKACCTFROM")?),
416 "BANKTRANLIST" => transaction_list = Some(parse_bank_transaction_list(reader)?),
417 "LEDGERBAL" => ledger_balance = Some(parse_ledger_balance(reader)?),
418 "AVAILBAL" => available_balance = Some(parse_available_balance(reader)?),
419 "BALLIST" => balance_list = parse_balance_list(reader)?,
420 "MKTGINFO" => marketing_info = Some(read_text(reader, "MKTGINFO")?),
421 _ => reader.skip_element(&name)?,
422 }
423 }
424 Event::End(e) if end_tag_name(&e) == "STMTRS" => break,
425 Event::Eof => {
426 return Err(XmlError::MalformedXml {
427 message: "unexpected EOF in STMTRS".to_owned(),
428 }
429 .into());
430 }
431 _ => {}
432 }
433 }
434
435 let curdef = curdef.ok_or_else(|| XmlError::MissingElement {
436 parent: "STMTRS".to_owned(),
437 element: "CURDEF".to_owned(),
438 })?;
439 let bank_acct = bank_acct.ok_or_else(|| XmlError::MissingElement {
440 parent: "STMTRS".to_owned(),
441 element: "BANKACCTFROM".to_owned(),
442 })?;
443
444 let mut stmt = StatementResponse::new(curdef, bank_acct);
445 if let Some(tl) = transaction_list {
446 stmt = stmt.with_transaction_list(tl);
447 }
448 if let Some(lb) = ledger_balance {
449 stmt = stmt.with_ledger_balance(lb);
450 }
451 if let Some(ab) = available_balance {
452 stmt = stmt.with_available_balance(ab);
453 }
454 if !balance_list.is_empty() {
455 stmt = stmt.with_balance_list(balance_list);
456 }
457 if let Some(info) = marketing_info {
458 stmt = stmt.with_marketing_info(info);
459 }
460
461 Ok(stmt)
462}
463
464fn parse_bank_transaction_list(
469 reader: &mut OfxReader<'_>,
470) -> Result<TransactionList, OfxError> {
471 let mut dtstart: Option<crate::types::OfxDateTime> = None;
472 let mut dtend: Option<crate::types::OfxDateTime> = None;
473 let mut transactions = Vec::new();
474
475 loop {
476 match reader.next_event()? {
477 Event::Start(e) => {
478 let name = tag_name(&e);
479 match name.as_str() {
480 "DTSTART" => {
481 let text = read_text(reader, "DTSTART")?;
482 dtstart = Some(parse_text_as(&text, "DTSTART")?);
483 }
484 "DTEND" => {
485 let text = read_text(reader, "DTEND")?;
486 dtend = Some(parse_text_as(&text, "DTEND")?);
487 }
488 "STMTTRN" => {
489 transactions.push(parse_statement_transaction(reader)?);
490 }
491 _ => reader.skip_element(&name)?,
492 }
493 }
494 Event::End(e) if end_tag_name(&e) == "BANKTRANLIST" => break,
495 Event::Eof => {
496 return Err(XmlError::MalformedXml {
497 message: "unexpected EOF in BANKTRANLIST".to_owned(),
498 }
499 .into());
500 }
501 _ => {}
502 }
503 }
504
505 let dtstart = dtstart.ok_or_else(|| XmlError::MissingElement {
506 parent: "BANKTRANLIST".to_owned(),
507 element: "DTSTART".to_owned(),
508 })?;
509 let dtend = dtend.ok_or_else(|| XmlError::MissingElement {
510 parent: "BANKTRANLIST".to_owned(),
511 element: "DTEND".to_owned(),
512 })?;
513
514 Ok(TransactionList::new(dtstart, dtend, transactions))
515}
516
517fn apply_stmttrn_field(
522 builder: StatementTransactionBuilder,
523 reader: &mut OfxReader<'_>,
524 name: &str,
525) -> Result<StatementTransactionBuilder, OfxError> {
526 Ok(match name {
527 "TRNTYPE" => builder.transaction_type(parse_text_as(&read_text(reader, name)?, name)?),
528 "DTPOSTED" => builder.date_posted(parse_text_as(&read_text(reader, name)?, name)?),
529 "DTUSER" => builder.date_user(parse_text_as(&read_text(reader, name)?, name)?),
530 "DTAVAIL" => builder.date_available(parse_text_as(&read_text(reader, name)?, name)?),
531 "TRNAMT" => builder.amount(parse_text_as(&read_text(reader, name)?, name)?),
532 "FITID" => builder.fit_id(parse_text_as(&read_text(reader, name)?, name)?),
533 "CORRECTFITID" => builder.correction_id(parse_text_as(&read_text(reader, name)?, name)?),
534 "CORRECTACTION" => builder.correction_action(parse_text_as(&read_text(reader, name)?, name)?),
535 "SRVRTID" => builder.server_transaction_id(parse_text_as(&read_text(reader, name)?, name)?),
536 "CHECKNUM" => builder.check_number(parse_text_as(&read_text(reader, name)?, name)?),
537 "REFNUM" => builder.reference_number(read_text(reader, name)?),
538 "SIC" => builder.sic(parse_text_as(&read_text(reader, name)?, name)?),
539 "PAYEEID" => builder.payee_id(read_text(reader, name)?),
540 "NAME" => builder.name(read_text(reader, name)?),
541 "PAYEE" => builder.payee(parse_payee(reader)?),
542 "BANKACCTTO" => builder.bank_account_to(parse_bank_account(reader, name)?),
543 "CCACCTTO" => builder.cc_account_to(parse_cc_account(reader, name)?),
544 "MEMO" => builder.memo(read_text(reader, name)?),
545 "CURRENCY" | "ORIGCURRENCY" => builder.currency(parse_currency(reader, name)?),
546 "INV401KSOURCE" => builder.inv401k_source(parse_text_as(&read_text(reader, name)?, name)?),
547 _ => {
548 reader.skip_element(name)?;
549 builder
550 }
551 })
552}
553
554fn parse_statement_transaction(
555 reader: &mut OfxReader<'_>,
556) -> Result<crate::aggregates::StatementTransaction, OfxError> {
557 let mut builder = StatementTransactionBuilder::new();
558
559 loop {
560 match reader.next_event()? {
561 Event::Start(e) => {
562 let name = tag_name(&e);
563 builder = apply_stmttrn_field(builder, reader, &name)?;
564 }
565 Event::End(e) if end_tag_name(&e) == "STMTTRN" => break,
566 Event::Eof => {
567 return Err(XmlError::MalformedXml {
568 message: "unexpected EOF in STMTTRN".to_owned(),
569 }
570 .into());
571 }
572 _ => {}
573 }
574 }
575
576 builder.build().map_err(|msg| {
577 OfxError::Xml(XmlError::MalformedXml {
578 message: format!("failed to build STMTTRN: {msg}"),
579 })
580 })
581}
582
583fn parse_ledger_balance(reader: &mut OfxReader<'_>) -> Result<LedgerBalance, OfxError> {
588 let mut balamt: Option<crate::types::OfxAmount> = None;
589 let mut dtasof: Option<crate::types::OfxDateTime> = None;
590
591 loop {
592 match reader.next_event()? {
593 Event::Start(e) => {
594 let name = tag_name(&e);
595 match name.as_str() {
596 "BALAMT" => {
597 let text = read_text(reader, "BALAMT")?;
598 balamt = Some(parse_text_as(&text, "BALAMT")?);
599 }
600 "DTASOF" => {
601 let text = read_text(reader, "DTASOF")?;
602 dtasof = Some(parse_text_as(&text, "DTASOF")?);
603 }
604 _ => reader.skip_element(&name)?,
605 }
606 }
607 Event::End(e) if end_tag_name(&e) == "LEDGERBAL" => break,
608 Event::Eof => {
609 return Err(XmlError::MalformedXml {
610 message: "unexpected EOF in LEDGERBAL".to_owned(),
611 }
612 .into());
613 }
614 _ => {}
615 }
616 }
617
618 let balamt = balamt.ok_or_else(|| XmlError::MissingElement {
619 parent: "LEDGERBAL".to_owned(),
620 element: "BALAMT".to_owned(),
621 })?;
622 let dtasof = dtasof.ok_or_else(|| XmlError::MissingElement {
623 parent: "LEDGERBAL".to_owned(),
624 element: "DTASOF".to_owned(),
625 })?;
626
627 Ok(LedgerBalance::new(balamt, dtasof))
628}
629
630fn parse_available_balance(reader: &mut OfxReader<'_>) -> Result<AvailableBalance, OfxError> {
631 let mut balamt: Option<crate::types::OfxAmount> = None;
632 let mut dtasof: Option<crate::types::OfxDateTime> = None;
633
634 loop {
635 match reader.next_event()? {
636 Event::Start(e) => {
637 let name = tag_name(&e);
638 match name.as_str() {
639 "BALAMT" => {
640 let text = read_text(reader, "BALAMT")?;
641 balamt = Some(parse_text_as(&text, "BALAMT")?);
642 }
643 "DTASOF" => {
644 let text = read_text(reader, "DTASOF")?;
645 dtasof = Some(parse_text_as(&text, "DTASOF")?);
646 }
647 _ => reader.skip_element(&name)?,
648 }
649 }
650 Event::End(e) if end_tag_name(&e) == "AVAILBAL" => break,
651 Event::Eof => {
652 return Err(XmlError::MalformedXml {
653 message: "unexpected EOF in AVAILBAL".to_owned(),
654 }
655 .into());
656 }
657 _ => {}
658 }
659 }
660
661 let balamt = balamt.ok_or_else(|| XmlError::MissingElement {
662 parent: "AVAILBAL".to_owned(),
663 element: "BALAMT".to_owned(),
664 })?;
665 let dtasof = dtasof.ok_or_else(|| XmlError::MissingElement {
666 parent: "AVAILBAL".to_owned(),
667 element: "DTASOF".to_owned(),
668 })?;
669
670 Ok(AvailableBalance::new(balamt, dtasof))
671}
672
673fn parse_balance_list(reader: &mut OfxReader<'_>) -> Result<Vec<Balance>, OfxError> {
674 let mut balances = Vec::new();
675
676 loop {
677 match reader.next_event()? {
678 Event::Start(e) => {
679 let name = tag_name(&e);
680 if name == "BAL" {
681 balances.push(parse_bal(reader)?);
682 } else {
683 reader.skip_element(&name)?;
684 }
685 }
686 Event::End(e) if end_tag_name(&e) == "BALLIST" => break,
687 Event::Eof => {
688 return Err(XmlError::MalformedXml {
689 message: "unexpected EOF in BALLIST".to_owned(),
690 }
691 .into());
692 }
693 _ => {}
694 }
695 }
696
697 Ok(balances)
698}
699
700fn parse_bal(reader: &mut OfxReader<'_>) -> Result<Balance, OfxError> {
701 let mut name: Option<String> = None;
702 let mut desc: Option<String> = None;
703 let mut bal_type: Option<crate::types::BalanceType> = None;
704 let mut value: Option<crate::types::OfxAmount> = None;
705 let mut dtasof: Option<crate::types::OfxDateTime> = None;
706 let mut currency: Option<crate::types::CurrencyCode> = None;
707
708 loop {
709 match reader.next_event()? {
710 Event::Start(e) => {
711 let tag = tag_name(&e);
712 match tag.as_str() {
713 "NAME" => name = Some(read_text(reader, "NAME")?),
714 "DESC" => desc = Some(read_text(reader, "DESC")?),
715 "BALTYPE" => {
716 let text = read_text(reader, "BALTYPE")?;
717 bal_type = Some(parse_text_as(&text, "BALTYPE")?);
718 }
719 "VALUE" => {
720 let text = read_text(reader, "VALUE")?;
721 value = Some(parse_text_as(&text, "VALUE")?);
722 }
723 "DTASOF" => {
724 let text = read_text(reader, "DTASOF")?;
725 dtasof = Some(parse_text_as(&text, "DTASOF")?);
726 }
727 "CURRENCY" => {
728 let text = read_text(reader, "CURRENCY")?;
729 currency = Some(parse_text_as(&text, "CURRENCY")?);
730 }
731 _ => reader.skip_element(&tag)?,
732 }
733 }
734 Event::End(e) if end_tag_name(&e) == "BAL" => break,
735 Event::Eof => {
736 return Err(XmlError::MalformedXml {
737 message: "unexpected EOF in BAL".to_owned(),
738 }
739 .into());
740 }
741 _ => {}
742 }
743 }
744
745 let name = name.ok_or_else(|| XmlError::MissingElement {
746 parent: "BAL".to_owned(),
747 element: "NAME".to_owned(),
748 })?;
749 let desc = desc.ok_or_else(|| XmlError::MissingElement {
750 parent: "BAL".to_owned(),
751 element: "DESC".to_owned(),
752 })?;
753 let bal_type = bal_type.ok_or_else(|| XmlError::MissingElement {
754 parent: "BAL".to_owned(),
755 element: "BALTYPE".to_owned(),
756 })?;
757 let value = value.ok_or_else(|| XmlError::MissingElement {
758 parent: "BAL".to_owned(),
759 element: "VALUE".to_owned(),
760 })?;
761
762 Ok(Balance::new(name, desc, bal_type, value, dtasof, currency))
763}
764
765fn parse_bank_account(
770 reader: &mut OfxReader<'_>,
771 closing_tag: &str,
772) -> Result<BankAccount, OfxError> {
773 let mut bank_id: Option<crate::types::BankId> = None;
774 let mut branch_id: Option<crate::types::BranchId> = None;
775 let mut acct_id: Option<crate::types::AccountId> = None;
776 let mut acct_type: Option<crate::types::AccountType> = None;
777 let mut acct_key: Option<String> = None;
778
779 loop {
780 match reader.next_event()? {
781 Event::Start(e) => {
782 let name = tag_name(&e);
783 match name.as_str() {
784 "BANKID" => {
785 let text = read_text(reader, "BANKID")?;
786 bank_id = Some(parse_text_as(&text, "BANKID")?);
787 }
788 "BRANCHID" => {
789 let text = read_text(reader, "BRANCHID")?;
790 branch_id = Some(parse_text_as(&text, "BRANCHID")?);
791 }
792 "ACCTID" => {
793 let text = read_text(reader, "ACCTID")?;
794 acct_id = Some(parse_text_as(&text, "ACCTID")?);
795 }
796 "ACCTTYPE" => {
797 let text = read_text(reader, "ACCTTYPE")?;
798 acct_type = Some(parse_text_as(&text, "ACCTTYPE")?);
799 }
800 "ACCTKEY" => acct_key = Some(read_text(reader, "ACCTKEY")?),
801 _ => reader.skip_element(&name)?,
802 }
803 }
804 Event::End(e) if end_tag_name(&e) == closing_tag => break,
805 Event::Eof => {
806 return Err(XmlError::MalformedXml {
807 message: format!("unexpected EOF in {closing_tag}"),
808 }
809 .into());
810 }
811 _ => {}
812 }
813 }
814
815 let bank_id = bank_id.ok_or_else(|| XmlError::MissingElement {
816 parent: closing_tag.to_owned(),
817 element: "BANKID".to_owned(),
818 })?;
819 let acct_id = acct_id.ok_or_else(|| XmlError::MissingElement {
820 parent: closing_tag.to_owned(),
821 element: "ACCTID".to_owned(),
822 })?;
823 let acct_type = acct_type.ok_or_else(|| XmlError::MissingElement {
824 parent: closing_tag.to_owned(),
825 element: "ACCTTYPE".to_owned(),
826 })?;
827
828 Ok(BankAccount::new(
829 bank_id, branch_id, acct_id, acct_type, acct_key,
830 ))
831}
832
833fn parse_cc_account(
834 reader: &mut OfxReader<'_>,
835 closing_tag: &str,
836) -> Result<CreditCardAccount, OfxError> {
837 let mut acct_id: Option<crate::types::AccountId> = None;
838 let mut acct_key: Option<String> = None;
839
840 loop {
841 match reader.next_event()? {
842 Event::Start(e) => {
843 let name = tag_name(&e);
844 match name.as_str() {
845 "ACCTID" => {
846 let text = read_text(reader, "ACCTID")?;
847 acct_id = Some(parse_text_as(&text, "ACCTID")?);
848 }
849 "ACCTKEY" => acct_key = Some(read_text(reader, "ACCTKEY")?),
850 _ => reader.skip_element(&name)?,
851 }
852 }
853 Event::End(e) if end_tag_name(&e) == closing_tag => break,
854 Event::Eof => {
855 return Err(XmlError::MalformedXml {
856 message: format!("unexpected EOF in {closing_tag}"),
857 }
858 .into());
859 }
860 _ => {}
861 }
862 }
863
864 let acct_id = acct_id.ok_or_else(|| XmlError::MissingElement {
865 parent: closing_tag.to_owned(),
866 element: "ACCTID".to_owned(),
867 })?;
868
869 Ok(CreditCardAccount::new(acct_id, acct_key))
870}
871
872fn parse_payee(reader: &mut OfxReader<'_>) -> Result<Payee, OfxError> {
877 let mut name: Option<String> = None;
878 let mut addr1: Option<String> = None;
879 let mut addr2: Option<String> = None;
880 let mut addr3: Option<String> = None;
881 let mut city: Option<String> = None;
882 let mut state: Option<String> = None;
883 let mut postalcode: Option<String> = None;
884 let mut country: Option<String> = None;
885 let mut phone: Option<String> = None;
886
887 loop {
888 match reader.next_event()? {
889 Event::Start(e) => {
890 let tag = tag_name(&e);
891 match tag.as_str() {
892 "NAME" => name = Some(read_text(reader, "NAME")?),
893 "ADDR1" => addr1 = Some(read_text(reader, "ADDR1")?),
894 "ADDR2" => addr2 = Some(read_text(reader, "ADDR2")?),
895 "ADDR3" => addr3 = Some(read_text(reader, "ADDR3")?),
896 "CITY" => city = Some(read_text(reader, "CITY")?),
897 "STATE" => state = Some(read_text(reader, "STATE")?),
898 "POSTALCODE" => postalcode = Some(read_text(reader, "POSTALCODE")?),
899 "COUNTRY" => country = Some(read_text(reader, "COUNTRY")?),
900 "PHONE" => phone = Some(read_text(reader, "PHONE")?),
901 _ => reader.skip_element(&tag)?,
902 }
903 }
904 Event::End(e) if end_tag_name(&e) == "PAYEE" => break,
905 Event::Eof => {
906 return Err(XmlError::MalformedXml {
907 message: "unexpected EOF in PAYEE".to_owned(),
908 }
909 .into());
910 }
911 _ => {}
912 }
913 }
914
915 let name = name.ok_or_else(|| XmlError::MissingElement {
916 parent: "PAYEE".to_owned(),
917 element: "NAME".to_owned(),
918 })?;
919 let addr1 = addr1.ok_or_else(|| XmlError::MissingElement {
920 parent: "PAYEE".to_owned(),
921 element: "ADDR1".to_owned(),
922 })?;
923 let city = city.ok_or_else(|| XmlError::MissingElement {
924 parent: "PAYEE".to_owned(),
925 element: "CITY".to_owned(),
926 })?;
927 let state = state.ok_or_else(|| XmlError::MissingElement {
928 parent: "PAYEE".to_owned(),
929 element: "STATE".to_owned(),
930 })?;
931 let postalcode = postalcode.ok_or_else(|| XmlError::MissingElement {
932 parent: "PAYEE".to_owned(),
933 element: "POSTALCODE".to_owned(),
934 })?;
935 let phone = phone.ok_or_else(|| XmlError::MissingElement {
936 parent: "PAYEE".to_owned(),
937 element: "PHONE".to_owned(),
938 })?;
939
940 let mut payee = Payee::new(name, addr1, city, state, postalcode, phone);
941 if let Some(a2) = addr2 {
942 payee = payee.with_address2(a2);
943 }
944 if let Some(a3) = addr3 {
945 payee = payee.with_address3(a3);
946 }
947 if let Some(c) = country {
948 payee = payee.with_country(c);
949 }
950 Ok(payee)
951}
952
953fn parse_currency(reader: &mut OfxReader<'_>, closing_tag: &str) -> Result<CurrencyInfo, OfxError> {
958 let mut currate: Option<crate::types::OfxAmount> = None;
959 let mut cursym: Option<crate::types::CurrencyCode> = None;
960
961 loop {
962 match reader.next_event()? {
963 Event::Start(e) => {
964 let name = tag_name(&e);
965 match name.as_str() {
966 "CURRATE" => {
967 let text = read_text(reader, "CURRATE")?;
968 currate = Some(parse_text_as(&text, "CURRATE")?);
969 }
970 "CURSYM" => {
971 let text = read_text(reader, "CURSYM")?;
972 cursym = Some(parse_text_as(&text, "CURSYM")?);
973 }
974 _ => reader.skip_element(&name)?,
975 }
976 }
977 Event::End(e) if end_tag_name(&e) == closing_tag => break,
978 Event::Eof => {
979 return Err(XmlError::MalformedXml {
980 message: format!("unexpected EOF in {closing_tag}"),
981 }
982 .into());
983 }
984 _ => {}
985 }
986 }
987
988 let currate = currate.ok_or_else(|| XmlError::MissingElement {
989 parent: closing_tag.to_owned(),
990 element: "CURRATE".to_owned(),
991 })?;
992 let cursym = cursym.ok_or_else(|| XmlError::MissingElement {
993 parent: closing_tag.to_owned(),
994 element: "CURSYM".to_owned(),
995 })?;
996
997 Ok(match closing_tag {
998 "ORIGCURRENCY" => CurrencyInfo::OrigCurrency {
999 code: cursym,
1000 rate: currate,
1001 },
1002 _ => CurrencyInfo::Currency {
1003 code: cursym,
1004 rate: currate,
1005 },
1006 })
1007}
1008
1009fn parse_cc_message_set(reader: &mut OfxReader<'_>) -> Result<CreditCardMessageSet, OfxError> {
1014 let mut responses = Vec::new();
1015
1016 loop {
1017 match reader.next_event()? {
1018 Event::Start(e) => {
1019 let name = tag_name(&e);
1020 if name == "CCSTMTTRNRS" {
1021 responses.push(parse_ccstmttrnrs(reader)?);
1022 } else {
1023 reader.skip_element(&name)?;
1024 }
1025 }
1026 Event::End(e) if end_tag_name(&e) == "CREDITCARDMSGSRSV1" => break,
1027 Event::Eof => {
1028 return Err(XmlError::MalformedXml {
1029 message: "unexpected EOF in CREDITCARDMSGSRSV1".to_owned(),
1030 }
1031 .into());
1032 }
1033 _ => {}
1034 }
1035 }
1036
1037 Ok(CreditCardMessageSet::new(responses))
1038}
1039
1040fn parse_ccstmttrnrs(
1041 reader: &mut OfxReader<'_>,
1042) -> Result<TransactionWrapper<CcStatementResponse>, OfxError> {
1043 let mut trnuid: Option<String> = None;
1044 let mut status: Option<Status> = None;
1045 let mut client_cookie: Option<String> = None;
1046 let mut ccstmtrs: Option<CcStatementResponse> = None;
1047
1048 loop {
1049 match reader.next_event()? {
1050 Event::Start(e) => {
1051 let name = tag_name(&e);
1052 match name.as_str() {
1053 "TRNUID" => trnuid = Some(read_text(reader, "TRNUID")?),
1054 "STATUS" => status = Some(parse_status(reader)?),
1055 "CLTCOOKIE" => client_cookie = Some(read_text(reader, "CLTCOOKIE")?),
1056 "CCSTMTRS" => ccstmtrs = Some(parse_ccstmtrs(reader)?),
1057 _ => reader.skip_element(&name)?,
1058 }
1059 }
1060 Event::End(e) if end_tag_name(&e) == "CCSTMTTRNRS" => break,
1061 Event::Eof => {
1062 return Err(XmlError::MalformedXml {
1063 message: "unexpected EOF in CCSTMTTRNRS".to_owned(),
1064 }
1065 .into());
1066 }
1067 _ => {}
1068 }
1069 }
1070
1071 let trnuid = trnuid.ok_or_else(|| XmlError::MissingElement {
1072 parent: "CCSTMTTRNRS".to_owned(),
1073 element: "TRNUID".to_owned(),
1074 })?;
1075 let status = status.ok_or_else(|| XmlError::MissingElement {
1076 parent: "CCSTMTTRNRS".to_owned(),
1077 element: "STATUS".to_owned(),
1078 })?;
1079
1080 let mut wrapper = TransactionWrapper::new(trnuid, status, ccstmtrs);
1081 if let Some(cookie) = client_cookie {
1082 wrapper = wrapper.with_client_cookie(cookie);
1083 }
1084 Ok(wrapper)
1085}
1086
1087fn parse_ccstmtrs(reader: &mut OfxReader<'_>) -> Result<CcStatementResponse, OfxError> {
1088 let mut curdef: Option<crate::types::CurrencyCode> = None;
1089 let mut cc_acct: Option<CreditCardAccount> = None;
1090 let mut transaction_list: Option<TransactionList> = None;
1091 let mut ledger_balance: Option<LedgerBalance> = None;
1092 let mut available_balance: Option<AvailableBalance> = None;
1093 let mut balance_list: Vec<Balance> = Vec::new();
1094 let mut marketing_info: Option<String> = None;
1095
1096 loop {
1097 match reader.next_event()? {
1098 Event::Start(e) => {
1099 let name = tag_name(&e);
1100 match name.as_str() {
1101 "CURDEF" => {
1102 let text = read_text(reader, "CURDEF")?;
1103 curdef = Some(parse_text_as(&text, "CURDEF")?);
1104 }
1105 "CCACCTFROM" => cc_acct = Some(parse_cc_account(reader, "CCACCTFROM")?),
1106 "BANKTRANLIST" => transaction_list = Some(parse_bank_transaction_list(reader)?),
1107 "LEDGERBAL" => ledger_balance = Some(parse_ledger_balance(reader)?),
1108 "AVAILBAL" => available_balance = Some(parse_available_balance(reader)?),
1109 "BALLIST" => balance_list = parse_balance_list(reader)?,
1110 "MKTGINFO" => marketing_info = Some(read_text(reader, "MKTGINFO")?),
1111 _ => reader.skip_element(&name)?,
1112 }
1113 }
1114 Event::End(e) if end_tag_name(&e) == "CCSTMTRS" => break,
1115 Event::Eof => {
1116 return Err(XmlError::MalformedXml {
1117 message: "unexpected EOF in CCSTMTRS".to_owned(),
1118 }
1119 .into());
1120 }
1121 _ => {}
1122 }
1123 }
1124
1125 let curdef = curdef.ok_or_else(|| XmlError::MissingElement {
1126 parent: "CCSTMTRS".to_owned(),
1127 element: "CURDEF".to_owned(),
1128 })?;
1129 let cc_acct = cc_acct.ok_or_else(|| XmlError::MissingElement {
1130 parent: "CCSTMTRS".to_owned(),
1131 element: "CCACCTFROM".to_owned(),
1132 })?;
1133
1134 let mut stmt = CcStatementResponse::new(curdef, cc_acct);
1135 if let Some(tl) = transaction_list {
1136 stmt = stmt.with_transaction_list(tl);
1137 }
1138 if let Some(lb) = ledger_balance {
1139 stmt = stmt.with_ledger_balance(lb);
1140 }
1141 if let Some(ab) = available_balance {
1142 stmt = stmt.with_available_balance(ab);
1143 }
1144 if !balance_list.is_empty() {
1145 stmt = stmt.with_balance_list(balance_list);
1146 }
1147 if let Some(info) = marketing_info {
1148 stmt = stmt.with_marketing_info(info);
1149 }
1150
1151 Ok(stmt)
1152}
1153
1154fn parse_investment_message_set(
1159 reader: &mut OfxReader<'_>,
1160) -> Result<InvestmentMessageSet, OfxError> {
1161 let mut responses = Vec::new();
1162
1163 loop {
1164 match reader.next_event()? {
1165 Event::Start(e) => {
1166 let name = tag_name(&e);
1167 if name == "INVSTMTTRNRS" {
1168 responses.push(parse_invstmttrnrs(reader)?);
1169 } else {
1170 reader.skip_element(&name)?;
1171 }
1172 }
1173 Event::End(e) if end_tag_name(&e) == "INVSTMTMSGSRSV1" => break,
1174 Event::Eof => {
1175 return Err(XmlError::MalformedXml {
1176 message: "unexpected EOF in INVSTMTMSGSRSV1".to_owned(),
1177 }
1178 .into());
1179 }
1180 _ => {}
1181 }
1182 }
1183
1184 Ok(InvestmentMessageSet::new(responses))
1185}
1186
1187fn parse_invstmttrnrs(
1188 reader: &mut OfxReader<'_>,
1189) -> Result<TransactionWrapper<InvStatementResponse>, OfxError> {
1190 let mut trnuid: Option<String> = None;
1191 let mut status: Option<Status> = None;
1192 let mut client_cookie: Option<String> = None;
1193 let mut invstmtrs: Option<InvStatementResponse> = None;
1194
1195 loop {
1196 match reader.next_event()? {
1197 Event::Start(e) => {
1198 let name = tag_name(&e);
1199 match name.as_str() {
1200 "TRNUID" => trnuid = Some(read_text(reader, "TRNUID")?),
1201 "STATUS" => status = Some(parse_status(reader)?),
1202 "CLTCOOKIE" => client_cookie = Some(read_text(reader, "CLTCOOKIE")?),
1203 "INVSTMTRS" => invstmtrs = Some(parse_invstmtrs(reader)?),
1204 _ => reader.skip_element(&name)?,
1205 }
1206 }
1207 Event::End(e) if end_tag_name(&e) == "INVSTMTTRNRS" => break,
1208 Event::Eof => {
1209 return Err(XmlError::MalformedXml {
1210 message: "unexpected EOF in INVSTMTTRNRS".to_owned(),
1211 }
1212 .into());
1213 }
1214 _ => {}
1215 }
1216 }
1217
1218 let trnuid = trnuid.ok_or_else(|| XmlError::MissingElement {
1219 parent: "INVSTMTTRNRS".to_owned(),
1220 element: "TRNUID".to_owned(),
1221 })?;
1222 let status = status.ok_or_else(|| XmlError::MissingElement {
1223 parent: "INVSTMTTRNRS".to_owned(),
1224 element: "STATUS".to_owned(),
1225 })?;
1226
1227 let mut wrapper = TransactionWrapper::new(trnuid, status, invstmtrs);
1228 if let Some(cookie) = client_cookie {
1229 wrapper = wrapper.with_client_cookie(cookie);
1230 }
1231 Ok(wrapper)
1232}
1233
1234fn parse_invstmtrs(reader: &mut OfxReader<'_>) -> Result<InvStatementResponse, OfxError> {
1235 let mut curdef: Option<crate::types::CurrencyCode> = None;
1236 let mut inv_acct: Option<InvestmentAccount> = None;
1237 let mut transaction_list: Option<TransactionList> = None;
1238
1239 loop {
1240 match reader.next_event()? {
1241 Event::Start(e) => {
1242 let name = tag_name(&e);
1243 match name.as_str() {
1244 "CURDEF" => {
1245 let text = read_text(reader, "CURDEF")?;
1246 curdef = Some(parse_text_as(&text, "CURDEF")?);
1247 }
1248 "INVACCTFROM" => {
1249 inv_acct = Some(parse_investment_account(reader)?);
1250 }
1251 "INVTRANLIST" => {
1252 transaction_list = Some(parse_inv_transaction_list(reader)?);
1253 }
1254 _ => reader.skip_element(&name)?,
1255 }
1256 }
1257 Event::End(e) if end_tag_name(&e) == "INVSTMTRS" => break,
1258 Event::Eof => {
1259 return Err(XmlError::MalformedXml {
1260 message: "unexpected EOF in INVSTMTRS".to_owned(),
1261 }
1262 .into());
1263 }
1264 _ => {}
1265 }
1266 }
1267
1268 let curdef = curdef.ok_or_else(|| XmlError::MissingElement {
1269 parent: "INVSTMTRS".to_owned(),
1270 element: "CURDEF".to_owned(),
1271 })?;
1272 let inv_acct = inv_acct.ok_or_else(|| XmlError::MissingElement {
1273 parent: "INVSTMTRS".to_owned(),
1274 element: "INVACCTFROM".to_owned(),
1275 })?;
1276
1277 let mut stmt = InvStatementResponse::new(curdef, inv_acct);
1278 if let Some(tl) = transaction_list {
1279 stmt = stmt.with_transaction_list(tl);
1280 }
1281
1282 Ok(stmt)
1283}
1284
1285fn parse_investment_account(reader: &mut OfxReader<'_>) -> Result<InvestmentAccount, OfxError> {
1286 let mut broker_id: Option<String> = None;
1287 let mut acct_id: Option<crate::types::AccountId> = None;
1288
1289 loop {
1290 match reader.next_event()? {
1291 Event::Start(e) => {
1292 let name = tag_name(&e);
1293 match name.as_str() {
1294 "BROKERID" => broker_id = Some(read_text(reader, "BROKERID")?),
1295 "ACCTID" => {
1296 let text = read_text(reader, "ACCTID")?;
1297 acct_id = Some(parse_text_as(&text, "ACCTID")?);
1298 }
1299 _ => reader.skip_element(&name)?,
1300 }
1301 }
1302 Event::End(e) if end_tag_name(&e) == "INVACCTFROM" => break,
1303 Event::Eof => {
1304 return Err(XmlError::MalformedXml {
1305 message: "unexpected EOF in INVACCTFROM".to_owned(),
1306 }
1307 .into());
1308 }
1309 _ => {}
1310 }
1311 }
1312
1313 let broker_id = broker_id.ok_or_else(|| XmlError::MissingElement {
1314 parent: "INVACCTFROM".to_owned(),
1315 element: "BROKERID".to_owned(),
1316 })?;
1317 let acct_id = acct_id.ok_or_else(|| XmlError::MissingElement {
1318 parent: "INVACCTFROM".to_owned(),
1319 element: "ACCTID".to_owned(),
1320 })?;
1321
1322 Ok(InvestmentAccount::new(broker_id, acct_id))
1323}
1324
1325fn parse_inv_transaction_list(reader: &mut OfxReader<'_>) -> Result<TransactionList, OfxError> {
1328 let mut dtstart: Option<crate::types::OfxDateTime> = None;
1329 let mut dtend: Option<crate::types::OfxDateTime> = None;
1330 let mut transactions = Vec::new();
1331
1332 loop {
1333 match reader.next_event()? {
1334 Event::Start(e) => {
1335 let name = tag_name(&e);
1336 match name.as_str() {
1337 "DTSTART" => {
1338 let text = read_text(reader, "DTSTART")?;
1339 dtstart = Some(parse_text_as(&text, "DTSTART")?);
1340 }
1341 "DTEND" => {
1342 let text = read_text(reader, "DTEND")?;
1343 dtend = Some(parse_text_as(&text, "DTEND")?);
1344 }
1345 "INVBANKTRAN" => {
1346 if let Some(txn) = parse_invbanktran(reader)? {
1347 transactions.push(txn);
1348 }
1349 }
1350 _ => reader.skip_element(&name)?,
1351 }
1352 }
1353 Event::End(e) if end_tag_name(&e) == "INVTRANLIST" => break,
1354 Event::Eof => {
1355 return Err(XmlError::MalformedXml {
1356 message: "unexpected EOF in INVTRANLIST".to_owned(),
1357 }
1358 .into());
1359 }
1360 _ => {}
1361 }
1362 }
1363
1364 let dtstart = dtstart.ok_or_else(|| XmlError::MissingElement {
1365 parent: "INVTRANLIST".to_owned(),
1366 element: "DTSTART".to_owned(),
1367 })?;
1368 let dtend = dtend.ok_or_else(|| XmlError::MissingElement {
1369 parent: "INVTRANLIST".to_owned(),
1370 element: "DTEND".to_owned(),
1371 })?;
1372
1373 Ok(TransactionList::new(dtstart, dtend, transactions))
1374}
1375
1376fn parse_invbanktran(
1380 reader: &mut OfxReader<'_>,
1381) -> Result<Option<crate::aggregates::StatementTransaction>, OfxError> {
1382 let mut transaction = None;
1383
1384 loop {
1385 match reader.next_event()? {
1386 Event::Start(e) => {
1387 let name = tag_name(&e);
1388 if name == "STMTTRN" {
1389 transaction = Some(parse_statement_transaction(reader)?);
1390 } else {
1391 reader.skip_element(&name)?;
1392 }
1393 }
1394 Event::End(e) if end_tag_name(&e) == "INVBANKTRAN" => break,
1395 Event::Eof => {
1396 return Err(XmlError::MalformedXml {
1397 message: "unexpected EOF in INVBANKTRAN".to_owned(),
1398 }
1399 .into());
1400 }
1401 _ => {}
1402 }
1403 }
1404
1405 Ok(transaction)
1406}
1407
1408#[cfg(test)]
1409mod tests {
1410 use super::*;
1411
1412 const SIMPLE_BANK_OFX: &str = r#"<?OFX OFXHEADER="200" VERSION="220" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>
1413<OFX>
1414<SIGNONMSGSRSV1>
1415<SONRS>
1416<STATUS>
1417<CODE>0</CODE>
1418<SEVERITY>INFO</SEVERITY>
1419</STATUS>
1420<DTSERVER>20230115120000</DTSERVER>
1421<LANGUAGE>ENG</LANGUAGE>
1422<FI>
1423<ORG>MyBank</ORG>
1424<FID>1234</FID>
1425</FI>
1426</SONRS>
1427</SIGNONMSGSRSV1>
1428<BANKMSGSRSV1>
1429<STMTTRNRS>
1430<TRNUID>1001</TRNUID>
1431<STATUS>
1432<CODE>0</CODE>
1433<SEVERITY>INFO</SEVERITY>
1434</STATUS>
1435<STMTRS>
1436<CURDEF>USD</CURDEF>
1437<BANKACCTFROM>
1438<BANKID>123456789</BANKID>
1439<ACCTID>987654321</ACCTID>
1440<ACCTTYPE>CHECKING</ACCTTYPE>
1441</BANKACCTFROM>
1442<BANKTRANLIST>
1443<DTSTART>20230101</DTSTART>
1444<DTEND>20230131</DTEND>
1445<STMTTRN>
1446<TRNTYPE>DEBIT</TRNTYPE>
1447<DTPOSTED>20230115</DTPOSTED>
1448<TRNAMT>-50.00</TRNAMT>
1449<FITID>20230115001</FITID>
1450<NAME>GROCERY STORE</NAME>
1451<MEMO>Weekly groceries</MEMO>
1452</STMTTRN>
1453</BANKTRANLIST>
1454<LEDGERBAL>
1455<BALAMT>1500.00</BALAMT>
1456<DTASOF>20230115120000</DTASOF>
1457</LEDGERBAL>
1458</STMTRS>
1459</STMTTRNRS>
1460</BANKMSGSRSV1>
1461</OFX>"#;
1462
1463 #[test]
1464 fn parse_simple_bank_statement() {
1465 let doc = parse(SIMPLE_BANK_OFX).unwrap();
1466
1467 assert_eq!(doc.header().version(), "220".parse().unwrap());
1468 assert!(doc.signon().status().is_success());
1469 assert_eq!(doc.signon().language(), "ENG");
1470
1471 let banking = doc.banking().expect("banking should be present");
1472 assert_eq!(banking.statement_responses().len(), 1);
1473
1474 let wrapper = &banking.statement_responses()[0];
1475 assert!(wrapper.status().is_success());
1476 assert_eq!(wrapper.transaction_uid(), "1001");
1477
1478 let stmt = wrapper.response().expect("response should be present");
1479 assert_eq!(stmt.currency_default().as_str(), "USD");
1480 assert_eq!(stmt.bank_account().bank_id().as_str(), "123456789");
1481 assert_eq!(stmt.bank_account().account_id().as_str(), "987654321");
1482
1483 let txn_list = stmt
1484 .transaction_list()
1485 .expect("transaction list should be present");
1486 assert_eq!(txn_list.len(), 1);
1487
1488 let txn = &txn_list.transactions()[0];
1489 assert_eq!(txn.transaction_type(), crate::types::TransactionType::Debit);
1490 assert_eq!(txn.name(), Some("GROCERY STORE"));
1491 assert_eq!(txn.memo(), Some("Weekly groceries"));
1492
1493 let ledger = stmt
1494 .ledger_balance()
1495 .expect("ledger balance should be present");
1496 assert_eq!(
1497 ledger.amount().as_decimal(),
1498 rust_decimal::Decimal::new(150000, 2)
1499 );
1500 }
1501
1502 #[test]
1503 fn parse_multiple_transactions() {
1504 let input = r#"<?OFX OFXHEADER="200" VERSION="220" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>
1505<OFX>
1506<SIGNONMSGSRSV1>
1507<SONRS>
1508<STATUS><CODE>0</CODE><SEVERITY>INFO</SEVERITY></STATUS>
1509<DTSERVER>20230131120000</DTSERVER>
1510<LANGUAGE>ENG</LANGUAGE>
1511</SONRS>
1512</SIGNONMSGSRSV1>
1513<BANKMSGSRSV1>
1514<STMTTRNRS>
1515<TRNUID>2001</TRNUID>
1516<STATUS><CODE>0</CODE><SEVERITY>INFO</SEVERITY></STATUS>
1517<STMTRS>
1518<CURDEF>USD</CURDEF>
1519<BANKACCTFROM>
1520<BANKID>111222333</BANKID>
1521<ACCTID>444555666</ACCTID>
1522<ACCTTYPE>CHECKING</ACCTTYPE>
1523</BANKACCTFROM>
1524<BANKTRANLIST>
1525<DTSTART>20230101</DTSTART>
1526<DTEND>20230131</DTEND>
1527<STMTTRN>
1528<TRNTYPE>DEBIT</TRNTYPE>
1529<DTPOSTED>20230105</DTPOSTED>
1530<TRNAMT>-25.00</TRNAMT>
1531<FITID>TXN001</FITID>
1532<NAME>COFFEE SHOP</NAME>
1533</STMTTRN>
1534<STMTTRN>
1535<TRNTYPE>CREDIT</TRNTYPE>
1536<DTPOSTED>20230110</DTPOSTED>
1537<TRNAMT>3000.00</TRNAMT>
1538<FITID>TXN002</FITID>
1539<NAME>PAYROLL</NAME>
1540<MEMO>Direct deposit</MEMO>
1541</STMTTRN>
1542<STMTTRN>
1543<TRNTYPE>DEBIT</TRNTYPE>
1544<DTPOSTED>20230115</DTPOSTED>
1545<TRNAMT>-1200.00</TRNAMT>
1546<FITID>TXN003</FITID>
1547<NAME>RENT PAYMENT</NAME>
1548<CHECKNUM>1042</CHECKNUM>
1549</STMTTRN>
1550<STMTTRN>
1551<TRNTYPE>DEBIT</TRNTYPE>
1552<DTPOSTED>20230120</DTPOSTED>
1553<TRNAMT>-89.50</TRNAMT>
1554<FITID>TXN004</FITID>
1555<NAME>ELECTRIC COMPANY</NAME>
1556<MEMO>Monthly bill</MEMO>
1557</STMTTRN>
1558<STMTTRN>
1559<TRNTYPE>DEBIT</TRNTYPE>
1560<DTPOSTED>20230125</DTPOSTED>
1561<TRNAMT>-42.99</TRNAMT>
1562<FITID>TXN005</FITID>
1563<NAME>STREAMING SERVICE</NAME>
1564<SIC>7812</SIC>
1565</STMTTRN>
1566</BANKTRANLIST>
1567<LEDGERBAL>
1568<BALAMT>1642.51</BALAMT>
1569<DTASOF>20230131120000</DTASOF>
1570</LEDGERBAL>
1571<AVAILBAL>
1572<BALAMT>1542.51</BALAMT>
1573<DTASOF>20230131120000</DTASOF>
1574</AVAILBAL>
1575</STMTRS>
1576</STMTTRNRS>
1577</BANKMSGSRSV1>
1578</OFX>"#;
1579
1580 let doc = parse(input).unwrap();
1581 let banking = doc.banking().unwrap();
1582 let stmt = banking.statement_responses()[0].response().unwrap();
1583
1584 let txn_list = stmt.transaction_list().unwrap();
1586 assert_eq!(txn_list.len(), 5);
1587
1588 assert_eq!(txn_list.transactions()[0].fit_id().as_str(), "TXN001");
1590 assert_eq!(txn_list.transactions()[0].name(), Some("COFFEE SHOP"));
1591 assert_eq!(
1592 txn_list.transactions()[0].amount().as_decimal(),
1593 rust_decimal::Decimal::new(-2500, 2)
1594 );
1595
1596 assert_eq!(txn_list.transactions()[1].fit_id().as_str(), "TXN002");
1597 assert_eq!(
1598 txn_list.transactions()[1].transaction_type(),
1599 crate::types::TransactionType::Credit
1600 );
1601 assert_eq!(txn_list.transactions()[1].memo(), Some("Direct deposit"));
1602
1603 assert_eq!(txn_list.transactions()[2].fit_id().as_str(), "TXN003");
1604 assert_eq!(
1605 txn_list.transactions()[2].check_number().unwrap().as_str(),
1606 "1042"
1607 );
1608
1609 assert_eq!(txn_list.transactions()[3].fit_id().as_str(), "TXN004");
1610 assert_eq!(txn_list.transactions()[3].memo(), Some("Monthly bill"));
1611
1612 assert_eq!(txn_list.transactions()[4].fit_id().as_str(), "TXN005");
1613 assert_eq!(txn_list.transactions()[4].sic(), Some(7812));
1614
1615 let ledger = stmt.ledger_balance().unwrap();
1617 assert_eq!(
1618 ledger.amount().as_decimal(),
1619 rust_decimal::Decimal::new(164251, 2)
1620 );
1621
1622 let avail = stmt.available_balance().unwrap();
1623 assert_eq!(
1624 avail.amount().as_decimal(),
1625 rust_decimal::Decimal::new(154251, 2)
1626 );
1627 }
1628
1629 #[test]
1630 fn parse_missing_header_returns_error() {
1631 let result = parse("<OFX></OFX>");
1632 assert!(result.is_err());
1633 }
1634
1635 #[test]
1636 fn parse_missing_signon_returns_error() {
1637 let input = r#"<?OFX OFXHEADER="200" VERSION="220" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?><OFX></OFX>"#;
1638 let result = parse(input);
1639 assert!(result.is_err());
1640 }
1641}