1use super::swift_utils::{parse_amount, parse_date_yymmdd, parse_swift_chars};
2use crate::errors::ParseError;
3use crate::traits::SwiftField;
4use chrono::NaiveDate;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct Field61 {
20 pub value_date: NaiveDate,
22
23 pub entry_date: Option<String>,
25
26 pub debit_credit_mark: String,
28
29 pub funds_code: Option<char>,
31
32 pub amount: f64,
34
35 pub transaction_type: String,
37
38 pub customer_reference: String,
40
41 pub bank_reference: Option<String>,
43
44 pub supplementary_details: Option<String>,
46}
47
48impl SwiftField for Field61 {
49 fn parse(input: &str) -> crate::Result<Self>
50 where
51 Self: Sized,
52 {
53 if input.len() < 15 {
55 return Err(ParseError::InvalidFormat {
56 message: "Field 61 must be at least 15 characters long".to_string(),
57 });
58 }
59
60 let mut pos = 0;
61
62 if input.len() < pos + 6 {
64 return Err(ParseError::InvalidFormat {
65 message: "Field 61 missing value date".to_string(),
66 });
67 }
68 let value_date_str = &input[pos..pos + 6];
69 let value_date = parse_date_yymmdd(value_date_str)?;
70 pos += 6;
71
72 let mut entry_date = None;
74 if pos + 4 <= input.len() && input[pos..pos + 4].chars().all(|c| c.is_ascii_digit()) {
75 entry_date = Some(input[pos..pos + 4].to_string());
76 pos += 4;
77 }
78
79 if pos >= input.len() {
81 return Err(ParseError::InvalidFormat {
82 message: "Field 61 missing debit/credit mark".to_string(),
83 });
84 }
85
86 let mut dc_mark_len = 1;
87 if pos + 1 < input.len() {
88 let two_char = &input[pos..pos + 2];
89 if two_char == "RD" || two_char == "RC" {
90 dc_mark_len = 2;
91 }
92 }
93
94 let debit_credit_mark = input[pos..pos + dc_mark_len].to_string();
95 if !["D", "C", "RD", "RC"].contains(&debit_credit_mark.as_str()) {
96 return Err(ParseError::InvalidFormat {
97 message: format!("Field 61 invalid debit/credit mark: {}", debit_credit_mark),
98 });
99 }
100 pos += dc_mark_len;
101
102 let mut funds_code = None;
104 if pos < input.len() && input.chars().nth(pos).unwrap().is_alphabetic() {
105 funds_code = Some(input.chars().nth(pos).unwrap());
106 pos += 1;
107 }
108
109 let amount_start = pos;
111 while pos < input.len()
112 && (input.chars().nth(pos).unwrap().is_ascii_digit()
113 || input.chars().nth(pos).unwrap() == ','
114 || input.chars().nth(pos).unwrap() == '.')
115 {
116 pos += 1;
117 }
118
119 if pos == amount_start {
120 return Err(ParseError::InvalidFormat {
121 message: "Field 61 missing amount".to_string(),
122 });
123 }
124
125 let amount_str = &input[amount_start..pos];
126 let amount = parse_amount(amount_str)?;
127
128 if pos + 4 > input.len() {
130 return Err(ParseError::InvalidFormat {
131 message: "Field 61 missing transaction type".to_string(),
132 });
133 }
134
135 let transaction_type = input[pos..pos + 4].to_string();
136 parse_swift_chars(&transaction_type, "Field 61 transaction type")?;
137 pos += 4;
138
139 let remaining = &input[pos..];
141 let (customer_ref_part, after_customer_ref) =
142 if let Some(double_slash_pos) = remaining.find("//") {
143 (
144 remaining[..double_slash_pos].to_string(),
145 Some(&remaining[double_slash_pos + 2..]),
146 )
147 } else {
148 (remaining.to_string(), None)
149 };
150
151 let customer_reference;
153 let mut supplementary_details = None;
154
155 if customer_ref_part.len() <= 16 {
156 customer_reference = customer_ref_part;
157 } else {
158 customer_reference = customer_ref_part[..16].to_string();
159 if after_customer_ref.is_none() && customer_ref_part.len() > 16 {
161 supplementary_details = Some(customer_ref_part[16..].to_string());
162 }
163 }
164
165 let bank_reference = if let Some(bank_ref_str) = after_customer_ref {
169 if let Some(newline_pos) = bank_ref_str.find('\n') {
171 let bank_ref = bank_ref_str[..newline_pos].to_string();
173 if newline_pos + 1 < bank_ref_str.len() {
174 supplementary_details = Some(bank_ref_str[newline_pos + 1..].to_string());
175 }
176 Some(bank_ref)
177 } else if bank_ref_str.len() > 16 {
178 supplementary_details = Some(bank_ref_str[16..].to_string());
181 Some(bank_ref_str[..16].to_string())
182 } else if !bank_ref_str.is_empty() {
183 Some(bank_ref_str.to_string())
184 } else {
185 None
186 }
187 } else {
188 None
189 };
190
191 if customer_reference.len() > 16 {
193 return Err(ParseError::InvalidFormat {
194 message: "Field 61 customer reference exceeds 16 characters".to_string(),
195 });
196 }
197
198 parse_swift_chars(&customer_reference, "Field 61 customer reference")?;
199
200 if let Some(ref bank_ref) = bank_reference {
201 parse_swift_chars(bank_ref, "Field 61 bank reference")?;
202 }
203
204 if let Some(ref supp_details) = supplementary_details {
205 if supp_details.len() > 34 {
206 return Err(ParseError::InvalidFormat {
207 message: "Field 61 supplementary details exceed 34 characters".to_string(),
208 });
209 }
210 parse_swift_chars(supp_details, "Field 61 supplementary details")?;
211 }
212
213 Ok(Field61 {
214 value_date,
215 entry_date,
216 debit_credit_mark,
217 funds_code,
218 amount,
219 transaction_type,
220 customer_reference,
221 bank_reference,
222 supplementary_details,
223 })
224 }
225
226 fn to_swift_string(&self) -> String {
227 let mut result = format!(":61:{}", self.value_date.format("%y%m%d"));
228
229 if let Some(ref entry_date) = self.entry_date {
230 result.push_str(entry_date);
231 }
232
233 result.push_str(&self.debit_credit_mark);
234
235 if let Some(funds_code) = self.funds_code {
236 result.push(funds_code);
237 }
238
239 result.push_str(&format!("{:.2}", self.amount).replace('.', ","));
240 result.push_str(&self.transaction_type);
241 result.push_str(&self.customer_reference);
242
243 if let Some(ref bank_reference) = self.bank_reference {
244 result.push_str("//");
245 result.push_str(bank_reference);
246
247 if let Some(ref supplementary_details) = self.supplementary_details {
249 result.push('\n');
250 result.push_str(supplementary_details);
251 }
252 } else if let Some(ref supplementary_details) = self.supplementary_details {
253 result.push_str(supplementary_details);
255 }
256
257 result
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use chrono::NaiveDate;
265
266 #[test]
267 fn test_field61_parse_basic() {
268 let field = Field61::parse("231225D1234,56NTRFREF123456").unwrap();
269 assert_eq!(
270 field.value_date,
271 NaiveDate::from_ymd_opt(2023, 12, 25).unwrap()
272 );
273 assert_eq!(field.entry_date, None);
274 assert_eq!(field.debit_credit_mark, "D");
275 assert_eq!(field.funds_code, None);
276 assert_eq!(field.amount, 1234.56);
277 assert_eq!(field.transaction_type, "NTRF");
278 assert_eq!(field.customer_reference, "REF123456");
279 assert_eq!(field.bank_reference, None);
280 assert_eq!(field.supplementary_details, None);
281 }
282
283 #[test]
284 fn test_field61_parse_with_entry_date() {
285 let field = Field61::parse("2312251226C500,00NTRFREF789//BANK456").unwrap();
286 assert_eq!(
287 field.value_date,
288 NaiveDate::from_ymd_opt(2023, 12, 25).unwrap()
289 );
290 assert_eq!(field.entry_date, Some("1226".to_string()));
291 assert_eq!(field.debit_credit_mark, "C");
292 assert_eq!(field.funds_code, None);
293 assert_eq!(field.amount, 500.00);
294 assert_eq!(field.transaction_type, "NTRF");
295 assert_eq!(field.customer_reference, "REF789");
296 assert_eq!(field.bank_reference, Some("BANK456".to_string()));
297 }
298
299 #[test]
300 fn test_field61_parse_with_funds_code() {
301 let field = Field61::parse("231225DF100,00NTRFCUSTREF").unwrap();
302 assert_eq!(field.debit_credit_mark, "D");
303 assert_eq!(field.funds_code, Some('F'));
304 assert_eq!(field.amount, 100.00);
305 }
306
307 #[test]
308 fn test_field61_parse_reversal() {
309 let field = Field61::parse("231225RD1000,00NTRFREVREF123").unwrap();
310 assert_eq!(field.debit_credit_mark, "RD");
311 assert_eq!(field.amount, 1000.00);
312 }
313
314 #[test]
315 fn test_field61_to_swift_string() {
316 let field = Field61 {
317 value_date: NaiveDate::from_ymd_opt(2023, 12, 25).unwrap(),
318 entry_date: Some("1226".to_string()),
319 debit_credit_mark: "C".to_string(),
320 funds_code: Some('F'),
321 amount: 1234.56,
322 transaction_type: "NTRF".to_string(),
323 customer_reference: "REF123456".to_string(),
324 bank_reference: Some("BANK789".to_string()),
325 supplementary_details: None,
326 };
327
328 assert_eq!(
329 field.to_swift_string(),
330 ":61:2312251226CF1234,56NTRFREF123456//BANK789"
331 );
332 }
333
334 #[test]
335 fn test_field61_invalid_debit_credit_mark() {
336 assert!(Field61::parse("231225X1234,56NTRFREF123").is_err());
337 }
338
339 #[test]
340 fn test_field61_too_short() {
341 assert!(Field61::parse("23122").is_err());
342 }
343
344 #[test]
345 fn test_field61_with_supplementary_details() {
346 let field =
348 Field61::parse("2412201220C10000,00NMSCREF100000//BA1-1234567890\nDUPLICATE-SEQ-1")
349 .unwrap();
350 assert_eq!(field.customer_reference, "REF100000");
351 assert_eq!(field.bank_reference, Some("BA1-1234567890".to_string()));
352 assert_eq!(
353 field.supplementary_details,
354 Some("DUPLICATE-SEQ-1".to_string())
355 );
356
357 let swift_str = field.to_swift_string();
359 let reparsed = Field61::parse(&swift_str.replace(":61:", "")).unwrap();
360 assert_eq!(reparsed.customer_reference, field.customer_reference);
361 assert_eq!(reparsed.bank_reference, field.bank_reference);
362 assert_eq!(reparsed.supplementary_details, field.supplementary_details);
363 }
364}