swift_mt_message/
common.rs

1//! Common structures and field definitions for SWIFT MT messages
2
3use chrono::{NaiveDate, NaiveDateTime};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7use crate::error::{MTError, Result};
8
9/// Represents a SWIFT message tag
10#[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/// Represents a field in a SWIFT message
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Field {
38    pub tag: Tag,
39    pub value: String,
40    pub raw_value: String, // Original value before any processing
41}
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    /// Get the field value
54    pub fn value(&self) -> &str {
55        &self.value
56    }
57
58    /// Get the raw field value (before processing)
59    pub fn raw_value(&self) -> &str {
60        &self.raw_value
61    }
62
63    /// Get the field tag
64    pub fn tag(&self) -> &Tag {
65        &self.tag
66    }
67
68    /// Check if field is empty
69    pub fn is_empty(&self) -> bool {
70        self.value.trim().is_empty()
71    }
72}
73
74/// SWIFT message blocks
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub enum MessageBlock {
77    /// Block 1: Basic Header Block
78    BasicHeader {
79        application_id: String,
80        service_id: String,
81        logical_terminal: String,
82        session_number: String,
83        sequence_number: String,
84    },
85    /// Block 2: Application Header Block
86    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    /// Block 3: User Header Block (optional)
95    UserHeader {
96        fields: HashMap<String, String>,
97    },
98    /// Block 4: Text Block
99    TextBlock {
100        fields: Vec<Field>,
101    },
102    /// Block 5: Trailer Block (optional)
103    TrailerBlock {
104        fields: HashMap<String, String>,
105    },
106}
107
108/// Common field types used across MT messages
109#[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        // Parse amount in format like "EUR1234567,89" or "USD1000.00"
119        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        // Handle both comma and dot as decimal separators
129        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/// Date field representation
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct SwiftDate {
147    pub date: NaiveDate,
148    pub raw: String,
149}
150
151impl SwiftDate {
152    /// Parse SWIFT date format (YYMMDD)
153    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        // Handle Y2K: assume 00-49 is 20xx, 50-99 is 19xx
176        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    /// Parse SWIFT date format (YYYYMMDD)
190    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/// Time field representation
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct SwiftTime {
227    pub time: NaiveDateTime,
228    pub raw: String,
229}
230
231/// Currency code validation
232pub 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
248/// Common SWIFT field tags
249pub 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    // MT940 specific tags
267    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}