use crate::camt054::CreditDebitIndicator;
use crate::xml::Node;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BalanceType {
OpeningBooked,
ClosingBooked,
IntradayBooked,
ClosingAvailable,
OpeningAvailable,
ForwardAvailable,
Other(String),
}
impl BalanceType {
#[must_use]
pub fn as_code(&self) -> &str {
match self {
Self::OpeningBooked => "OPBD",
Self::ClosingBooked => "CLBD",
Self::IntradayBooked => "ITBD",
Self::ClosingAvailable => "CLAV",
Self::OpeningAvailable => "OPAV",
Self::ForwardAvailable => "FWAV",
Self::Other(s) => s,
}
}
pub(crate) fn from_code(s: &str) -> Self {
match s.trim().to_ascii_uppercase().as_str() {
"OPBD" => Self::OpeningBooked,
"CLBD" => Self::ClosingBooked,
"ITBD" => Self::IntradayBooked,
"CLAV" => Self::ClosingAvailable,
"OPAV" => Self::OpeningAvailable,
"FWAV" => Self::ForwardAvailable,
other => Self::Other(other.to_owned()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EntryStatus {
Booked,
Pending,
Info,
Future,
Other(String),
}
impl EntryStatus {
pub(crate) fn from_code(s: &str) -> Self {
match s.trim().to_ascii_uppercase().as_str() {
"BOOK" => Self::Booked,
"PDNG" => Self::Pending,
"INFO" => Self::Info,
"FUTR" => Self::Future,
other => Self::Other(other.to_owned()),
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StatementBalance {
pub balance_type: BalanceType,
pub amount_ct: i64,
pub currency: String,
pub indicator: CreditDebitIndicator,
pub date: String,
}
impl StatementBalance {
#[inline]
#[must_use]
pub fn signed_ct(&self) -> i64 {
match self.indicator {
CreditDebitIndicator::Credit => self.amount_ct,
CreditDebitIndicator::Debit => -self.amount_ct,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EntryDetail {
pub amount_ct: Option<i64>,
pub end_to_end_id: Option<String>,
pub mandate_id: Option<String>,
pub creditor_id: Option<String>,
pub reference: Option<String>,
pub counterparty_name: Option<String>,
pub counterparty_iban: Option<String>,
pub return_reason_code: Option<String>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CashEntry {
pub amount_ct: i64,
pub currency: String,
pub indicator: CreditDebitIndicator,
pub status: EntryStatus,
pub batch_booked: bool,
pub booking_date: Option<String>,
pub value_date: Option<String>,
pub account_servicer_ref: Option<String>,
pub bank_tx_code: Option<String>,
pub details: Vec<EntryDetail>,
}
impl CashEntry {
#[inline]
#[must_use]
pub fn signed_ct(&self) -> i64 {
match self.indicator {
CreditDebitIndicator::Credit => self.amount_ct,
CreditDebitIndicator::Debit => -self.amount_ct,
}
}
#[inline]
#[must_use]
pub fn first_detail(&self) -> Option<&EntryDetail> {
self.details.first()
}
#[must_use]
pub fn end_to_end_id(&self) -> Option<&str> {
self.first_detail()?.end_to_end_id.as_deref()
}
#[must_use]
pub fn mandate_id(&self) -> Option<&str> {
self.first_detail()?.mandate_id.as_deref()
}
#[must_use]
pub fn creditor_id(&self) -> Option<&str> {
self.first_detail()?.creditor_id.as_deref()
}
#[must_use]
pub fn reference(&self) -> Option<&str> {
self.first_detail()?.reference.as_deref()
}
#[must_use]
pub fn counterparty_name(&self) -> Option<&str> {
self.first_detail()?.counterparty_name.as_deref()
}
#[must_use]
pub fn counterparty_iban(&self) -> Option<&str> {
self.first_detail()?.counterparty_iban.as_deref()
}
#[must_use]
pub fn return_reason_code(&self) -> Option<&str> {
self.first_detail()?.return_reason_code.as_deref()
}
#[must_use]
pub fn is_return(&self) -> bool {
self.details.iter().any(|d| d.return_reason_code.is_some())
}
}
pub(crate) fn amount_of(node: &Node, tag: &str) -> Option<(i64, String)> {
let amt = node.child(tag)?;
let ct = crate::ct_from_eur_str(&amt.text)?;
Some((ct, amt.attr("Ccy").unwrap_or("EUR").to_owned()))
}
pub(crate) fn indicator_of(node: &Node) -> CreditDebitIndicator {
node.text_of("CdtDbtInd")
.and_then(|s| s.parse().ok())
.unwrap_or(CreditDebitIndicator::Credit)
}
pub(crate) fn party_name(parties: Option<&Node>, tag: &str) -> Option<String> {
let party = parties?.child(tag)?;
party
.text_of("Nm")
.or_else(|| party.text_at(&["Pty", "Nm"]))
.map(str::to_owned)
}
pub(crate) fn parse_balance(b: &Node) -> Option<StatementBalance> {
let balance_type = b
.path(&["Tp", "CdOrPrtry"])
.and_then(Node::code)
.map_or_else(|| BalanceType::Other(String::new()), BalanceType::from_code);
let (amount_ct, currency) = amount_of(b, "Amt")?;
let date = b
.child("Dt")
.and_then(|d| d.text_of("Dt").or_else(|| d.text_of("DtTm")))
.unwrap_or_default()
.to_owned();
Some(StatementBalance {
balance_type,
amount_ct,
currency,
indicator: indicator_of(b),
date,
})
}
pub(crate) fn parse_entry(e: &Node) -> Option<CashEntry> {
let (amount_ct, currency) = amount_of(e, "Amt")?;
let indicator = indicator_of(e);
let status = e
.child("Sts")
.and_then(Node::code)
.map_or(EntryStatus::Booked, EntryStatus::from_code);
let date_of = |tag: &str| {
e.child(tag)
.and_then(|d| d.text_of("Dt").or_else(|| d.text_of("DtTm")))
.map(str::to_owned)
};
let bank_tx_code = e.child("BkTxCd").and_then(|c| {
c.text_at(&["Domn", "Cd"])
.or_else(|| c.text_at(&["Prtry", "Cd"]))
.map(str::to_owned)
});
let details_parent = e.child("NtryDtls").unwrap_or(e);
let batch_booked = details_parent.children_named("TxDtls").count() > 1;
let details = details_parent
.children_named("TxDtls")
.map(|td| parse_detail(td, indicator))
.collect();
Some(CashEntry {
amount_ct,
currency,
indicator,
status,
batch_booked,
booking_date: date_of("BookgDt"),
value_date: date_of("ValDt"),
account_servicer_ref: e.text_of("AcctSvcrRef").map(str::to_owned),
bank_tx_code,
details,
})
}
pub(crate) fn parse_detail(td: &Node, indicator: CreditDebitIndicator) -> EntryDetail {
let refs = td.child("Refs");
let ref_of = |tag: &str| refs.and_then(|r| r.text_of(tag)).map(str::to_owned);
let parties = td.child("RltdPties");
let (name_tag, acct_tag) = match indicator {
CreditDebitIndicator::Credit => ("Dbtr", "DbtrAcct"),
CreditDebitIndicator::Debit => ("Cdtr", "CdtrAcct"),
};
EntryDetail {
amount_ct: amount_of(td, "Amt").map(|(ct, _)| ct),
end_to_end_id: ref_of("EndToEndId"),
mandate_id: ref_of("MndtId"),
creditor_id: ref_of("CdtrId"),
reference: td.text_at(&["RmtInf", "Ustrd"]).map(str::to_owned),
counterparty_name: party_name(parties, name_tag),
counterparty_iban: parties
.and_then(|p| p.text_at(&[acct_tag, "Id", "IBAN"]))
.map(str::to_owned),
return_reason_code: td
.path(&["RtrInf", "Rsn"])
.and_then(Node::code)
.map(str::to_owned),
}
}
pub(crate) fn account_iban(node: &Node) -> String {
node.path(&["Acct", "Id", "IBAN"])
.map(|n| n.text.clone())
.unwrap_or_default()
}
pub(crate) fn agent_bic(agent: &Node) -> Option<&str> {
let fin = agent.child("FinInstnId")?;
fin.text_of("BIC").or_else(|| fin.text_of("BICFI"))
}
pub(crate) fn period(node: &Node) -> (Option<String>, Option<String>) {
let range = node.child("FrToDt");
let at = |dt_tm: &str, dt: &str| {
range
.and_then(|r| r.text_of(dt_tm).or_else(|| r.text_of(dt)))
.map(str::to_owned)
};
(at("FrDtTm", "FrDt"), at("ToDtTm", "ToDt"))
}
pub(crate) fn entries_of(node: &Node) -> Vec<CashEntry> {
node.children_named("Ntry")
.filter_map(parse_entry)
.collect()
}
pub(crate) fn balances_of(node: &Node) -> Vec<StatementBalance> {
node.children_named("Bal")
.filter_map(parse_balance)
.collect()
}