use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("unknown credit/debit indicator {0:?}: expected CRDT or DBIT")]
pub struct UnknownIndicator(
pub String,
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum CreditDebitIndicator {
#[cfg_attr(feature = "serde", serde(rename = "CRDT"))]
Credit,
#[cfg_attr(feature = "serde", serde(rename = "DBIT"))]
Debit,
}
impl CreditDebitIndicator {
#[inline]
#[must_use]
pub const fn as_code(self) -> &'static str {
match self {
Self::Credit => "CRDT",
Self::Debit => "DBIT",
}
}
}
impl std::fmt::Display for CreditDebitIndicator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_code())
}
}
impl FromStr for CreditDebitIndicator {
type Err = UnknownIndicator;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_uppercase().as_str() {
"CRDT" => Ok(Self::Credit),
"DBIT" => Ok(Self::Debit),
_ => Err(UnknownIndicator(s.to_owned())),
}
}
}
impl TryFrom<&str> for CreditDebitIndicator {
type Error = UnknownIndicator;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Camt054Notification {
pub notification_id: String,
pub account: crate::camt::AccountRef,
pub from_date_raw: Option<String>,
pub to_date_raw: Option<String>,
pub entries: Vec<crate::camt::CashEntry>,
}
impl Camt054Notification {
#[must_use]
pub fn from_date(&self) -> Option<crate::IsoDate> {
crate::IsoDate::parse_date_part(self.from_date_raw.as_deref()?).ok()
}
#[must_use]
pub fn to_date(&self) -> Option<crate::IsoDate> {
crate::IsoDate::parse_date_part(self.to_date_raw.as_deref()?).ok()
}
#[must_use]
pub fn net_movement_ct(&self) -> Option<i64> {
self.entries
.iter()
.try_fold(0i64, |acc, e| Some(acc.saturating_add(e.signed_ct()?)))
}
pub fn returns(&self) -> impl Iterator<Item = &crate::camt::CashEntry> {
self.entries.iter().filter(|e| e.is_return())
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Camt054Document {
pub msg_id: String,
pub created_at_raw: String,
pub namespace: Option<String>,
pub notifications: Vec<Camt054Notification>,
}
impl Camt054Document {
#[must_use]
pub fn created_at(&self) -> Option<crate::IsoDateTime> {
crate::IsoDateTime::parse(&self.created_at_raw).ok()
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum Camt054ParseError {
#[error(transparent)]
Xml(#[from] crate::xml::XmlError),
#[error("not a camt.054 document: root element <BkToCstmrDbtCdtNtfctn> not found")]
NotCamt054,
}
pub fn parse_camt054(xml: &str) -> Result<Camt054Document, Camt054ParseError> {
use crate::camt;
use crate::xml::{Document, Node};
fn parse_notification(n: &Node) -> Camt054Notification {
let (from_date, to_date) = camt::period(n);
Camt054Notification {
notification_id: n.text_of("Id").unwrap_or_default().to_owned(),
account: camt::account_of(n),
from_date_raw: from_date,
to_date_raw: to_date,
entries: camt::entries_of(n),
}
}
let doc = Document::parse(xml)?;
let root = doc
.root
.child("BkToCstmrDbtCdtNtfctn")
.ok_or(Camt054ParseError::NotCamt054)?;
let grp_hdr = root.child("GrpHdr");
let text = |tag: &str| {
grp_hdr
.and_then(|h| h.text_of(tag))
.unwrap_or_default()
.to_owned()
};
Ok(Camt054Document {
msg_id: text("MsgId"),
created_at_raw: text("CreDtTm"),
namespace: doc.namespace,
notifications: root
.children_named("Ntfctn")
.map(parse_notification)
.collect(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn indicator_display_and_parse() {
assert_eq!(CreditDebitIndicator::Credit.to_string(), "CRDT");
assert_eq!(CreditDebitIndicator::Debit.to_string(), "DBIT");
assert_eq!(
"CRDT".parse::<CreditDebitIndicator>().unwrap(),
CreditDebitIndicator::Credit
);
assert_eq!(
"dbit".parse::<CreditDebitIndicator>().unwrap(),
CreditDebitIndicator::Debit
);
assert!("INVALID".parse::<CreditDebitIndicator>().is_err());
}
#[test]
fn credit_debit_indicator_codes() {
assert_eq!(CreditDebitIndicator::Credit.as_code(), "CRDT");
assert_eq!(CreditDebitIndicator::Debit.as_code(), "DBIT");
}
}