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 { fields: HashMap<String, String> },
96 TextBlock { fields: Vec<Field> },
98 TrailerBlock { fields: HashMap<String, String> },
100}
101
102#[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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct SwiftDate {
142 pub date: NaiveDate,
143 pub raw: String,
144}
145
146impl SwiftDate {
147 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct SwiftTime {
217 pub time: NaiveDateTime,
218 pub raw: String,
219}
220
221pub 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
238pub 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 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}