1use crate::{SimpleStatement, SimpleTransaction};
5use chrono::NaiveDate;
6use rust_decimal::Decimal;
7
8impl SimpleStatement {
9 pub fn to_mt940(&self) -> String {
11 let mut stmt_940 = String::default();
12
13 stmt_940 += "{1:F01SNSBNL2AXXXX0000000000}{2:O940SNSBNL2AXXXXN}{3:}{4:\r\n";
14 stmt_940 += ":20:0000000000\r\n";
15 stmt_940 += &format!(":25:{}\r\n", self.account);
16 stmt_940 += &format!(
17 "{}\r\n",
18 balance_tag_940("60F", self.opening_date, self.opening_amount)
19 );
20
21 let mut amount = self.opening_amount;
22 for transaction in &self.transactions {
23 amount += transaction.amount;
24 stmt_940 += &format!("{}\r\n", transaction.entry_940_61());
25 stmt_940 += &format!("{}\r\n", transaction.entry_940_86());
26 }
27
28 stmt_940 += &format!("{}\r\n", balance_tag_940("62F", self.closing_date, amount));
29 stmt_940 += "-}{5:}\r\n";
30 stmt_940
31 }
32
33 pub fn filename_940(&self) -> String {
36 format!(
37 "{}_{}_{}.940",
38 self.account,
39 self.opening_date.format("%Y-%m-%d"),
40 self.closing_date.format("%Y-%m-%d"),
41 )
42 }
43}
44
45impl SimpleTransaction {
46 fn entry_940_61(&self) -> String {
47 let amount = if self.amount.is_sign_negative() {
48 format!("D{:.2}", -self.amount)
49 } else {
50 format!("C{:.2}", self.amount)
51 };
52 let amount = amount.replace('.', ",");
53
54 format!(
55 ":61:{}{}{}NDIV",
56 self.value_date.format("%y%m%d"),
57 self.book_date.format("%m%d"),
58 amount
59 )
60 }
61
62 fn entry_940_86(&self) -> String {
63 format!(":86:{}", self.description)
64 }
65}
66
67fn balance_tag_940(tag: &str, date: NaiveDate, amount: Decimal) -> String {
69 let date = date.format("%y%m%d");
70 let res = if amount.is_sign_negative() {
71 format!(":{tag}:D{date}EUR{:.2}", -amount)
72 } else {
73 format!(":{tag}:C{date}EUR{:.2}", amount)
74 };
75 res.replace('.', ",")
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::{Document, simple};
82 use rust_decimal_macros::dec;
83
84 fn test_statement() -> SimpleStatement {
85 let doc = Document::from_reader(simple::TEST_XML.as_bytes()).expect("valid test fixture");
86 SimpleStatement::from_document(&doc)
87 .expect("valid statement")
88 .remove(0)
89 }
90
91 #[test]
92 fn balance_tag_940_formats_credit_and_debit() {
93 let date = NaiveDate::from_ymd_opt(2026, 1, 23).unwrap();
94 assert_eq!(
95 balance_tag_940("60F", date, dec!(203.69)),
96 ":60F:C260123EUR203,69"
97 );
98 assert_eq!(
99 balance_tag_940("60F", date, dec!(-203.69)),
100 ":60F:D260123EUR203,69"
101 );
102 }
103
104 #[test]
105 fn entry_940_61_formats_debit_and_credit_entries() {
106 let statement = test_statement();
107 assert_eq!(
108 statement.transactions[0].entry_940_61(),
109 ":61:2603050305D100,00NDIV"
110 );
111 assert_eq!(
112 statement.transactions[1].entry_940_61(),
113 ":61:2604100410C50,00NDIV"
114 );
115 }
116
117 #[test]
118 fn entry_940_86_uses_raw_description() {
119 let statement = test_statement();
120 assert_eq!(
121 statement.transactions[0].entry_940_86(),
122 ":86:Payment for invoice 123"
123 );
124 }
125
126 #[test]
127 fn to_mt940_produces_expected_header_and_balances() {
128 let statement = test_statement();
129 let mt940 = statement.to_mt940();
130 assert!(mt940.starts_with("{1:F01SNSBNL2AXXXX0000000000}{2:O940SNSBNL2AXXXXN}{3:}{4:\r\n"));
131 assert!(mt940.contains(":25:NL00SNSB0000000000\r\n"));
132 assert!(mt940.contains(":60F:C260101EUR1000,00\r\n"));
133 assert!(mt940.contains(":62F:C260721EUR950,00\r\n"));
135 assert!(mt940.ends_with("-}{5:}\r\n"));
136 }
137
138 #[test]
139 fn filename_940_uses_opening_and_closing_dates() {
140 let statement = test_statement();
141 assert_eq!(
142 statement.filename_940(),
143 "NL00SNSB0000000000_2026-01-01_2026-07-21.940"
144 );
145 }
146}