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 { fields: HashMap<String, String> },
96    /// Block 4: Text Block
97    TextBlock { fields: Vec<Field> },
98    /// Block 5: Trailer Block (optional)
99    TrailerBlock { fields: HashMap<String, String> },
100}
101
102/// Common field types used across MT messages
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct Amount {
105    pub value: f64,
106    pub currency: String,
107    pub raw: String,
108}
109
110impl Amount {
111    pub fn parse(input: &str) -> Result<Self> {
112        // Parse amount in format like "EUR1234567,89" or "USD1000.00"
113        if input.len() < 4 {
114            return Err(MTError::AmountParseError {
115                message: "Amount string too short".to_string(),
116            });
117        }
118
119        let currency = &input[0..3];
120        let amount_str = &input[3..];
121
122        // Handle both comma and dot as decimal separators
123        let normalized_amount = amount_str.replace(',', ".");
124
125        let value = normalized_amount
126            .parse::<f64>()
127            .map_err(|_| MTError::AmountParseError {
128                message: format!("Invalid amount format: {}", amount_str),
129            })?;
130
131        Ok(Amount {
132            value,
133            currency: currency.to_string(),
134            raw: input.to_string(),
135        })
136    }
137}
138
139/// Date field representation
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct SwiftDate {
142    pub date: NaiveDate,
143    pub raw: String,
144}
145
146impl SwiftDate {
147    /// Parse SWIFT date format (YYMMDD)
148    pub fn parse_yymmdd(input: &str) -> Result<Self> {
149        if input.len() != 6 {
150            return Err(MTError::DateParseError {
151                message: format!("Invalid date format, expected YYMMDD, got: {}", input),
152            });
153        }
154
155        let year: i32 = input[0..2].parse().map_err(|_| MTError::DateParseError {
156            message: format!("Invalid year in date: {}", input),
157        })?;
158
159        let month: u32 = input[2..4].parse().map_err(|_| MTError::DateParseError {
160            message: format!("Invalid month in date: {}", input),
161        })?;
162
163        let day: u32 = input[4..6].parse().map_err(|_| MTError::DateParseError {
164            message: format!("Invalid day in date: {}", input),
165        })?;
166
167        // Handle Y2K: assume 00-49 is 20xx, 50-99 is 19xx
168        let full_year = if year <= 49 { 2000 + year } else { 1900 + year };
169
170        let date = NaiveDate::from_ymd_opt(full_year, month, day).ok_or_else(|| {
171            MTError::DateParseError {
172                message: format!("Invalid date: {}-{:02}-{:02}", full_year, month, day),
173            }
174        })?;
175
176        Ok(SwiftDate {
177            date,
178            raw: input.to_string(),
179        })
180    }
181
182    /// Parse SWIFT date format (YYYYMMDD)
183    pub fn parse_yyyymmdd(input: &str) -> Result<Self> {
184        if input.len() != 8 {
185            return Err(MTError::DateParseError {
186                message: format!("Invalid date format, expected YYYYMMDD, got: {}", input),
187            });
188        }
189
190        let year: i32 = input[0..4].parse().map_err(|_| MTError::DateParseError {
191            message: format!("Invalid year in date: {}", input),
192        })?;
193
194        let month: u32 = input[4..6].parse().map_err(|_| MTError::DateParseError {
195            message: format!("Invalid month in date: {}", input),
196        })?;
197
198        let day: u32 = input[6..8].parse().map_err(|_| MTError::DateParseError {
199            message: format!("Invalid day in date: {}", input),
200        })?;
201
202        let date =
203            NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| MTError::DateParseError {
204                message: format!("Invalid date: {}-{:02}-{:02}", year, month, day),
205            })?;
206
207        Ok(SwiftDate {
208            date,
209            raw: input.to_string(),
210        })
211    }
212}
213
214/// Time field representation
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct SwiftTime {
217    pub time: NaiveDateTime,
218    pub raw: String,
219}
220
221/// Currency code validation
222pub fn validate_currency_code(code: &str) -> Result<()> {
223    if code.len() != 3 {
224        return Err(MTError::CurrencyError {
225            message: format!("Currency code must be 3 characters, got: {}", code),
226        });
227    }
228
229    if !code.chars().all(|c| c.is_ascii_uppercase()) {
230        return Err(MTError::CurrencyError {
231            message: format!("Currency code must be uppercase letters, got: {}", code),
232        });
233    }
234
235    Ok(())
236}
237
238/// Common SWIFT field tags
239pub mod tags {
240    pub const SENDER_REFERENCE: &str = "20";
241    pub const BANK_OPERATION_CODE: &str = "23B";
242    pub const VALUE_DATE_CURRENCY_AMOUNT: &str = "32A";
243    pub const ORDERING_CUSTOMER: &str = "50K";
244    pub const ORDERING_INSTITUTION: &str = "52A";
245    pub const SENDERS_CORRESPONDENT: &str = "53A";
246    pub const RECEIVERS_CORRESPONDENT: &str = "54A";
247    pub const THIRD_REIMBURSEMENT_INSTITUTION: &str = "55A";
248    pub const INTERMEDIARY_INSTITUTION: &str = "56A";
249    pub const ACCOUNT_WITH_INSTITUTION: &str = "57A";
250    pub const BENEFICIARY_CUSTOMER: &str = "59";
251    pub const REMITTANCE_INFORMATION: &str = "70";
252    pub const DETAILS_OF_CHARGES: &str = "71A";
253    pub const SENDERS_CHARGES: &str = "71F";
254    pub const RECEIVERS_CHARGES: &str = "71G";
255
256    // MT940 specific tags
257    pub const TRANSACTION_REFERENCE: &str = "20";
258    pub const ACCOUNT_IDENTIFICATION: &str = "25";
259    pub const STATEMENT_NUMBER: &str = "28C";
260    pub const OPENING_BALANCE: &str = "60F";
261    pub const STATEMENT_LINE: &str = "61";
262    pub const INFORMATION_TO_ACCOUNT_OWNER: &str = "86";
263    pub const CLOSING_BALANCE: &str = "62F";
264    pub const CLOSING_AVAILABLE_BALANCE: &str = "64";
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use chrono::Datelike;
271
272    #[test]
273    fn test_amount_parsing() {
274        let amount = Amount::parse("EUR1234567,89").unwrap();
275        assert_eq!(amount.currency, "EUR");
276        assert_eq!(amount.value, 1234567.89);
277
278        let amount = Amount::parse("USD1000.50").unwrap();
279        assert_eq!(amount.currency, "USD");
280        assert_eq!(amount.value, 1000.50);
281    }
282
283    #[test]
284    fn test_date_parsing() {
285        let date = SwiftDate::parse_yymmdd("210315").unwrap();
286        assert_eq!(date.date.year(), 2021);
287        assert_eq!(date.date.month(), 3);
288        assert_eq!(date.date.day(), 15);
289
290        let date = SwiftDate::parse_yymmdd("991231").unwrap();
291        assert_eq!(date.date.year(), 1999);
292        assert_eq!(date.date.month(), 12);
293        assert_eq!(date.date.day(), 31);
294    }
295
296    #[test]
297    fn test_currency_validation() {
298        assert!(validate_currency_code("EUR").is_ok());
299        assert!(validate_currency_code("USD").is_ok());
300        assert!(validate_currency_code("eur").is_err());
301        assert!(validate_currency_code("EURO").is_err());
302        assert!(validate_currency_code("EU").is_err());
303    }
304}