use crate::{SimpleStatement, SimpleTransaction};
use chrono::NaiveDate;
use rust_decimal::Decimal;
impl SimpleStatement {
pub fn to_mt940(&self) -> String {
let mut stmt_940 = String::default();
stmt_940 += "{1:F01SNSBNL2AXXXX0000000000}{2:O940SNSBNL2AXXXXN}{3:}{4:\r\n";
stmt_940 += ":20:0000000000\r\n";
stmt_940 += &format!(":25:{}\r\n", self.account);
stmt_940 += &format!(
"{}\r\n",
balance_tag_940("60F", self.opening_date, self.opening_amount)
);
let mut amount = self.opening_amount;
for transaction in &self.transactions {
amount += transaction.amount;
stmt_940 += &format!("{}\r\n", transaction.entry_940_61());
stmt_940 += &format!("{}\r\n", transaction.entry_940_86());
}
stmt_940 += &format!("{}\r\n", balance_tag_940("62F", self.closing_date, amount));
stmt_940 += "-}{5:}\r\n";
stmt_940
}
pub fn filename_940(&self) -> String {
format!(
"{}_{}_{}.940",
self.account,
self.opening_date.format("%Y-%m-%d"),
self.closing_date.format("%Y-%m-%d"),
)
}
}
impl SimpleTransaction {
fn entry_940_61(&self) -> String {
let amount = if self.amount.is_sign_negative() {
format!("D{:.2}", -self.amount)
} else {
format!("C{:.2}", self.amount)
};
let amount = amount.replace('.', ",");
format!(
":61:{}{}{}NDIV",
self.value_date.format("%y%m%d"),
self.book_date.format("%m%d"),
amount
)
}
fn entry_940_86(&self) -> String {
format!(":86:{}", self.description)
}
}
fn balance_tag_940(tag: &str, date: NaiveDate, amount: Decimal) -> String {
let date = date.format("%y%m%d");
let res = if amount.is_sign_negative() {
format!(":{tag}:D{date}EUR{:.2}", -amount)
} else {
format!(":{tag}:C{date}EUR{:.2}", amount)
};
res.replace('.', ",")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Document, simple};
use rust_decimal_macros::dec;
fn test_statement() -> SimpleStatement {
let doc = Document::from_reader(simple::TEST_XML.as_bytes()).expect("valid test fixture");
SimpleStatement::from_document(&doc)
.expect("valid statement")
.remove(0)
}
#[test]
fn balance_tag_940_formats_credit_and_debit() {
let date = NaiveDate::from_ymd_opt(2026, 1, 23).unwrap();
assert_eq!(
balance_tag_940("60F", date, dec!(203.69)),
":60F:C260123EUR203,69"
);
assert_eq!(
balance_tag_940("60F", date, dec!(-203.69)),
":60F:D260123EUR203,69"
);
}
#[test]
fn entry_940_61_formats_debit_and_credit_entries() {
let statement = test_statement();
assert_eq!(
statement.transactions[0].entry_940_61(),
":61:2603050305D100,00NDIV"
);
assert_eq!(
statement.transactions[1].entry_940_61(),
":61:2604100410C50,00NDIV"
);
}
#[test]
fn entry_940_86_uses_raw_description() {
let statement = test_statement();
assert_eq!(
statement.transactions[0].entry_940_86(),
":86:Payment for invoice 123"
);
}
#[test]
fn to_mt940_produces_expected_header_and_balances() {
let statement = test_statement();
let mt940 = statement.to_mt940();
assert!(mt940.starts_with("{1:F01SNSBNL2AXXXX0000000000}{2:O940SNSBNL2AXXXXN}{3:}{4:\r\n"));
assert!(mt940.contains(":25:NL00SNSB0000000000\r\n"));
assert!(mt940.contains(":60F:C260101EUR1000,00\r\n"));
assert!(mt940.contains(":62F:C260721EUR950,00\r\n"));
assert!(mt940.ends_with("-}{5:}\r\n"));
}
#[test]
fn filename_940_uses_opening_and_closing_dates() {
let statement = test_statement();
assert_eq!(
statement.filename_940(),
"NL00SNSB0000000000_2026-01-01_2026-07-21.940"
);
}
}