1use chrono::{NaiveDate, NaiveDateTime};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7use crate::error::{MTError, Result};
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct Tag(pub String);
12
13impl Tag {
14 pub fn new(tag: impl Into<String>) -> Self {
15 Self(tag.into())
16 }
17
18 pub fn as_str(&self) -> &str {
19 &self.0
20 }
21}
22
23impl From<&str> for Tag {
24 fn from(s: &str) -> Self {
25 Self(s.to_string())
26 }
27}
28
29impl From<String> for Tag {
30 fn from(s: String) -> Self {
31 Self(s)
32 }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Field {
38 pub tag: Tag,
39 pub value: String,
40 pub raw_value: String, }
42
43impl Field {
44 pub fn new(tag: impl Into<Tag>, value: impl Into<String>) -> Self {
45 let value = value.into();
46 Self {
47 tag: tag.into(),
48 raw_value: value.clone(),
49 value,
50 }
51 }
52
53 pub fn value(&self) -> &str {
55 &self.value
56 }
57
58 pub fn raw_value(&self) -> &str {
60 &self.raw_value
61 }
62
63 pub fn tag(&self) -> &Tag {
65 &self.tag
66 }
67
68 pub fn is_empty(&self) -> bool {
70 self.value.trim().is_empty()
71 }
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub enum MessageBlock {
77 BasicHeader {
79 application_id: String,
80 service_id: String,
81 logical_terminal: String,
82 session_number: String,
83 sequence_number: String,
84 },
85 ApplicationHeader {
87 input_output_identifier: String,
88 message_type: String,
89 destination_address: String,
90 priority: String,
91 delivery_monitoring: Option<String>,
92 obsolescence_period: Option<String>,
93 },
94 UserHeader {
96 fields: HashMap<String, String>,
97 },
98 TextBlock {
100 fields: Vec<Field>,
101 },
102 TrailerBlock {
104 fields: HashMap<String, String>,
105 },
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct Amount {
111 pub value: f64,
112 pub currency: String,
113 pub raw: String,
114}
115
116impl Amount {
117 pub fn parse(input: &str) -> Result<Self> {
118 if input.len() < 4 {
120 return Err(MTError::AmountParseError {
121 message: "Amount string too short".to_string(),
122 });
123 }
124
125 let currency = &input[0..3];
126 let amount_str = &input[3..];
127
128 let normalized_amount = amount_str.replace(',', ".");
130
131 let value = normalized_amount.parse::<f64>()
132 .map_err(|_| MTError::AmountParseError {
133 message: format!("Invalid amount format: {}", amount_str),
134 })?;
135
136 Ok(Amount {
137 value,
138 currency: currency.to_string(),
139 raw: input.to_string(),
140 })
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct SwiftDate {
147 pub date: NaiveDate,
148 pub raw: String,
149}
150
151impl SwiftDate {
152 pub fn parse_yymmdd(input: &str) -> Result<Self> {
154 if input.len() != 6 {
155 return Err(MTError::DateParseError {
156 message: format!("Invalid date format, expected YYMMDD, got: {}", input),
157 });
158 }
159
160 let year: i32 = input[0..2].parse()
161 .map_err(|_| MTError::DateParseError {
162 message: format!("Invalid year in date: {}", input),
163 })?;
164
165 let month: u32 = input[2..4].parse()
166 .map_err(|_| MTError::DateParseError {
167 message: format!("Invalid month in date: {}", input),
168 })?;
169
170 let day: u32 = input[4..6].parse()
171 .map_err(|_| MTError::DateParseError {
172 message: format!("Invalid day in date: {}", input),
173 })?;
174
175 let full_year = if year <= 49 { 2000 + year } else { 1900 + year };
177
178 let date = NaiveDate::from_ymd_opt(full_year, month, day)
179 .ok_or_else(|| MTError::DateParseError {
180 message: format!("Invalid date: {}-{:02}-{:02}", full_year, month, day),
181 })?;
182
183 Ok(SwiftDate {
184 date,
185 raw: input.to_string(),
186 })
187 }
188
189 pub fn parse_yyyymmdd(input: &str) -> Result<Self> {
191 if input.len() != 8 {
192 return Err(MTError::DateParseError {
193 message: format!("Invalid date format, expected YYYYMMDD, got: {}", input),
194 });
195 }
196
197 let year: i32 = input[0..4].parse()
198 .map_err(|_| MTError::DateParseError {
199 message: format!("Invalid year in date: {}", input),
200 })?;
201
202 let month: u32 = input[4..6].parse()
203 .map_err(|_| MTError::DateParseError {
204 message: format!("Invalid month in date: {}", input),
205 })?;
206
207 let day: u32 = input[6..8].parse()
208 .map_err(|_| MTError::DateParseError {
209 message: format!("Invalid day in date: {}", input),
210 })?;
211
212 let date = NaiveDate::from_ymd_opt(year, month, day)
213 .ok_or_else(|| MTError::DateParseError {
214 message: format!("Invalid date: {}-{:02}-{:02}", year, month, day),
215 })?;
216
217 Ok(SwiftDate {
218 date,
219 raw: input.to_string(),
220 })
221 }
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct SwiftTime {
227 pub time: NaiveDateTime,
228 pub raw: String,
229}
230
231pub fn validate_currency_code(code: &str) -> Result<()> {
233 if code.len() != 3 {
234 return Err(MTError::CurrencyError {
235 message: format!("Currency code must be 3 characters, got: {}", code),
236 });
237 }
238
239 if !code.chars().all(|c| c.is_ascii_uppercase()) {
240 return Err(MTError::CurrencyError {
241 message: format!("Currency code must be uppercase letters, got: {}", code),
242 });
243 }
244
245 Ok(())
246}
247
248pub mod tags {
250 pub const SENDER_REFERENCE: &str = "20";
251 pub const BANK_OPERATION_CODE: &str = "23B";
252 pub const VALUE_DATE_CURRENCY_AMOUNT: &str = "32A";
253 pub const ORDERING_CUSTOMER: &str = "50K";
254 pub const ORDERING_INSTITUTION: &str = "52A";
255 pub const SENDERS_CORRESPONDENT: &str = "53A";
256 pub const RECEIVERS_CORRESPONDENT: &str = "54A";
257 pub const THIRD_REIMBURSEMENT_INSTITUTION: &str = "55A";
258 pub const INTERMEDIARY_INSTITUTION: &str = "56A";
259 pub const ACCOUNT_WITH_INSTITUTION: &str = "57A";
260 pub const BENEFICIARY_CUSTOMER: &str = "59";
261 pub const REMITTANCE_INFORMATION: &str = "70";
262 pub const DETAILS_OF_CHARGES: &str = "71A";
263 pub const SENDERS_CHARGES: &str = "71F";
264 pub const RECEIVERS_CHARGES: &str = "71G";
265
266 pub const TRANSACTION_REFERENCE: &str = "20";
268 pub const ACCOUNT_IDENTIFICATION: &str = "25";
269 pub const STATEMENT_NUMBER: &str = "28C";
270 pub const OPENING_BALANCE: &str = "60F";
271 pub const STATEMENT_LINE: &str = "61";
272 pub const INFORMATION_TO_ACCOUNT_OWNER: &str = "86";
273 pub const CLOSING_BALANCE: &str = "62F";
274 pub const CLOSING_AVAILABLE_BALANCE: &str = "64";
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use chrono::Datelike;
281
282 #[test]
283 fn test_amount_parsing() {
284 let amount = Amount::parse("EUR1234567,89").unwrap();
285 assert_eq!(amount.currency, "EUR");
286 assert_eq!(amount.value, 1234567.89);
287
288 let amount = Amount::parse("USD1000.50").unwrap();
289 assert_eq!(amount.currency, "USD");
290 assert_eq!(amount.value, 1000.50);
291 }
292
293 #[test]
294 fn test_date_parsing() {
295 let date = SwiftDate::parse_yymmdd("210315").unwrap();
296 assert_eq!(date.date.year(), 2021);
297 assert_eq!(date.date.month(), 3);
298 assert_eq!(date.date.day(), 15);
299
300 let date = SwiftDate::parse_yymmdd("991231").unwrap();
301 assert_eq!(date.date.year(), 1999);
302 assert_eq!(date.date.month(), 12);
303 assert_eq!(date.date.day(), 31);
304 }
305
306 #[test]
307 fn test_currency_validation() {
308 assert!(validate_currency_code("EUR").is_ok());
309 assert!(validate_currency_code("USD").is_ok());
310 assert!(validate_currency_code("eur").is_err());
311 assert!(validate_currency_code("EURO").is_err());
312 assert!(validate_currency_code("EU").is_err());
313 }
314}