1use super::swift_utils::parse_swift_chars;
8use crate::errors::ParseError;
9
10#[derive(Debug, Clone, PartialEq)]
12pub enum PaymentMethodCode {
13 FW,
15 RT,
17 AU,
19 IN,
21 SW,
23 CH,
25 CP,
27 RU,
29}
30
31impl PaymentMethodCode {
32 pub fn parse(code: &str) -> Option<Self> {
34 match code {
35 "FW" => Some(PaymentMethodCode::FW),
36 "RT" => Some(PaymentMethodCode::RT),
37 "AU" => Some(PaymentMethodCode::AU),
38 "IN" => Some(PaymentMethodCode::IN),
39 "SW" => Some(PaymentMethodCode::SW),
40 "CH" => Some(PaymentMethodCode::CH),
41 "CP" => Some(PaymentMethodCode::CP),
42 "RU" => Some(PaymentMethodCode::RU),
43 _ => None,
44 }
45 }
46
47 pub fn as_str(&self) -> &'static str {
49 match self {
50 PaymentMethodCode::FW => "FW",
51 PaymentMethodCode::RT => "RT",
52 PaymentMethodCode::AU => "AU",
53 PaymentMethodCode::IN => "IN",
54 PaymentMethodCode::SW => "SW",
55 PaymentMethodCode::CH => "CH",
56 PaymentMethodCode::CP => "CP",
57 PaymentMethodCode::RU => "RU",
58 }
59 }
60}
61
62pub fn parse_payment_method(input: &str) -> Option<PaymentMethodCode> {
64 if input.starts_with("//") && input.len() == 4 {
65 PaymentMethodCode::parse(&input[2..])
66 } else {
67 None
68 }
69}
70
71#[derive(Debug, Clone, PartialEq)]
73pub enum TransactionTypeCode {
74 BOK,
76 MSC,
78 RTR,
80 CHK,
82 DFT,
84 STO,
86 LDP,
88 FEX,
90 COL,
92 LBX,
94 TCK,
96 DCR,
98 CSH,
100 CHG,
102 INT,
104 DIV,
106}
107
108impl TransactionTypeCode {
109 pub fn parse(code: &str) -> Option<Self> {
111 match code {
112 "BOK" => Some(TransactionTypeCode::BOK),
113 "MSC" => Some(TransactionTypeCode::MSC),
114 "RTR" => Some(TransactionTypeCode::RTR),
115 "CHK" => Some(TransactionTypeCode::CHK),
116 "DFT" => Some(TransactionTypeCode::DFT),
117 "STO" => Some(TransactionTypeCode::STO),
118 "LDP" => Some(TransactionTypeCode::LDP),
119 "FEX" => Some(TransactionTypeCode::FEX),
120 "COL" => Some(TransactionTypeCode::COL),
121 "LBX" => Some(TransactionTypeCode::LBX),
122 "TCK" => Some(TransactionTypeCode::TCK),
123 "DCR" => Some(TransactionTypeCode::DCR),
124 "CSH" => Some(TransactionTypeCode::CSH),
125 "CHG" => Some(TransactionTypeCode::CHG),
126 "INT" => Some(TransactionTypeCode::INT),
127 "DIV" => Some(TransactionTypeCode::DIV),
128 _ => None,
129 }
130 }
131
132 pub fn as_str(&self) -> &'static str {
134 match self {
135 TransactionTypeCode::BOK => "BOK",
136 TransactionTypeCode::MSC => "MSC",
137 TransactionTypeCode::RTR => "RTR",
138 TransactionTypeCode::CHK => "CHK",
139 TransactionTypeCode::DFT => "DFT",
140 TransactionTypeCode::STO => "STO",
141 TransactionTypeCode::LDP => "LDP",
142 TransactionTypeCode::FEX => "FEX",
143 TransactionTypeCode::COL => "COL",
144 TransactionTypeCode::LBX => "LBX",
145 TransactionTypeCode::TCK => "TCK",
146 TransactionTypeCode::DCR => "DCR",
147 TransactionTypeCode::CSH => "CSH",
148 TransactionTypeCode::CHG => "CHG",
149 TransactionTypeCode::INT => "INT",
150 TransactionTypeCode::DIV => "DIV",
151 }
152 }
153}
154
155#[derive(Debug, Clone, PartialEq)]
157pub enum BankOperationCode {
158 CRED,
160 CRTS,
162 SPAY,
164 SSTD,
166 SPRI,
168 CHQB,
170}
171
172impl BankOperationCode {
173 pub fn parse(code: &str) -> Option<Self> {
175 match code {
176 "CRED" => Some(BankOperationCode::CRED),
177 "CRTS" => Some(BankOperationCode::CRTS),
178 "SPAY" => Some(BankOperationCode::SPAY),
179 "SSTD" => Some(BankOperationCode::SSTD),
180 "SPRI" => Some(BankOperationCode::SPRI),
181 "CHQB" => Some(BankOperationCode::CHQB),
182 _ => None,
183 }
184 }
185
186 pub fn as_str(&self) -> &'static str {
188 match self {
189 BankOperationCode::CRED => "CRED",
190 BankOperationCode::CRTS => "CRTS",
191 BankOperationCode::SPAY => "SPAY",
192 BankOperationCode::SSTD => "SSTD",
193 BankOperationCode::SPRI => "SPRI",
194 BankOperationCode::CHQB => "CHQB",
195 }
196 }
197}
198
199pub fn validate_field_option(
201 field_number: &str,
202 option: Option<char>,
203 allowed_options: &[char],
204) -> Result<(), ParseError> {
205 if let Some(opt) = option
206 && !allowed_options.contains(&opt)
207 {
208 return Err(ParseError::InvalidFormat {
209 message: format!("Field {} does not support option {}", field_number, opt),
210 });
211 }
212 Ok(())
213}
214
215pub fn parse_field_tag(tag: &str) -> (String, Option<char>) {
217 if tag.len() >= 2 {
218 let last_char = tag.chars().last().unwrap();
219 if last_char.is_ascii_alphabetic()
220 && tag[..tag.len() - 1].chars().all(|c| c.is_ascii_digit())
221 {
222 return (tag[..tag.len() - 1].to_string(), Some(last_char));
223 }
224 }
225 (tag.to_string(), None)
226}
227
228pub fn is_numbered_line(line: &str) -> bool {
230 if line.len() >= 2 {
231 let mut chars = line.chars();
232 if let Some(first) = chars.next()
233 && let Some(second) = chars.next()
234 {
235 return first.is_ascii_digit() && second == '/';
236 }
237 }
238 false
239}
240
241pub fn parse_numbered_lines(lines: &[&str]) -> Result<Vec<(u8, String)>, ParseError> {
243 let mut result = Vec::new();
244
245 for line in lines {
246 if !is_numbered_line(line) {
247 return Err(ParseError::InvalidFormat {
248 message: format!("Expected numbered line format (n/text), found: {}", line),
249 });
250 }
251
252 let number = line.chars().next().unwrap().to_digit(10).unwrap() as u8;
253 let content = &line[2..]; result.push((number, content.to_string()));
256 }
257
258 Ok(result)
259}
260
261pub fn extract_field_number(field_tag: &str) -> String {
263 field_tag
264 .chars()
265 .take_while(|c| c.is_ascii_digit())
266 .collect()
267}
268
269pub fn parse_party_identifier(input: &str) -> Result<Option<String>, ParseError> {
272 if !input.starts_with('/') {
273 return Ok(None);
274 }
275
276 let remaining = &input[1..];
277
278 if let Some(special_code) = remaining.strip_prefix('/') {
280 if !special_code.is_empty() && special_code.len() <= 34 {
281 parse_swift_chars(special_code, "party identifier")?;
282 return Ok(Some(format!("/{}", special_code)));
283 }
284 return Err(ParseError::InvalidFormat {
285 message: format!("Invalid special party identifier format: {}", input),
286 });
287 }
288
289 if let Some(slash_pos) = remaining.find('/') {
291 let code = &remaining[..slash_pos];
292 let id = &remaining[slash_pos + 1..];
293
294 if code.len() == 1 && code.chars().all(|c| c.is_ascii_alphabetic()) {
296 if id.len() > 34 {
297 return Err(ParseError::InvalidFormat {
298 message: format!("Party identifier exceeds 34 characters: {}", id.len()),
299 });
300 }
301 parse_swift_chars(id, "party identifier")?;
302 return Ok(Some(format!("{}/{}", code, id)));
303 }
304
305 if (1..=2).contains(&code.len())
307 && code
308 .chars()
309 .all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit())
310 {
311 if id.len() > 34 {
312 return Err(ParseError::InvalidFormat {
313 message: format!("Party identifier exceeds 34 characters: {}", id.len()),
314 });
315 }
316 parse_swift_chars(id, "party identifier")?;
317 return Ok(Some(format!("{}/{}", code, id)));
318 }
319 } else if remaining.len() <= 34 {
320 parse_swift_chars(remaining, "party identifier")?;
322 return Ok(Some(remaining.to_string()));
323 }
324
325 Err(ParseError::InvalidFormat {
326 message: format!("Invalid party identifier format: {}", input),
327 })
328}
329
330pub fn parse_debit_credit_mark(input: char) -> Result<String, ParseError> {
332 if input != 'D' && input != 'C' {
333 return Err(ParseError::InvalidFormat {
334 message: format!("Debit/credit mark must be 'D' or 'C', found: '{}'", input),
335 });
336 }
337 Ok(input.to_string())
338}
339
340pub fn validate_multiline_text(
343 lines: &[&str],
344 max_lines: usize,
345 max_line_length: usize,
346 field_name: &str,
347) -> Result<Vec<String>, ParseError> {
348 if lines.is_empty() {
349 return Err(ParseError::InvalidFormat {
350 message: format!("{} must have at least one line", field_name),
351 });
352 }
353
354 if lines.len() > max_lines {
355 return Err(ParseError::InvalidFormat {
356 message: format!(
357 "{} cannot have more than {} lines, found {}",
358 field_name,
359 max_lines,
360 lines.len()
361 ),
362 });
363 }
364
365 let mut result = Vec::new();
366 for (i, line) in lines.iter().enumerate() {
367 if line.len() > max_line_length {
368 return Err(ParseError::InvalidFormat {
369 message: format!(
370 "{} line {} exceeds {} characters",
371 field_name,
372 i + 1,
373 max_line_length
374 ),
375 });
376 }
377 parse_swift_chars(line, &format!("{} line {}", field_name, i + 1))?;
378 result.push(line.to_string());
379 }
380
381 Ok(result)
382}
383
384pub fn parse_name_and_address(
387 lines: &[&str],
388 start_idx: usize,
389 field_name: &str,
390) -> Result<Vec<String>, ParseError> {
391 let mut name_and_address = Vec::new();
392
393 for (i, line) in lines.iter().enumerate().skip(start_idx) {
394 if line.len() > 35 {
395 return Err(ParseError::InvalidFormat {
396 message: format!(
397 "{} line {} exceeds 35 characters",
398 field_name,
399 i - start_idx + 1
400 ),
401 });
402 }
403 parse_swift_chars(line, &format!("{} line {}", field_name, i - start_idx + 1))?;
404 name_and_address.push(line.to_string());
405 }
406
407 if name_and_address.is_empty() {
408 return Err(ParseError::InvalidFormat {
409 message: format!("{} must have at least one name/address line", field_name),
410 });
411 }
412
413 if name_and_address.len() > 4 {
414 return Err(ParseError::InvalidFormat {
415 message: format!(
416 "{} cannot have more than 4 name/address lines, found {}",
417 field_name,
418 name_and_address.len()
419 ),
420 });
421 }
422
423 Ok(name_and_address)
424}
425
426pub fn parse_multiline_text(
428 input: &str,
429 max_lines: usize,
430 max_line_length: usize,
431) -> Result<Vec<String>, ParseError> {
432 let lines: Vec<String> = input
433 .lines()
434 .map(|s| s.to_string())
435 .filter(|s| !s.is_empty())
436 .collect();
437
438 if lines.len() > max_lines {
439 return Err(ParseError::InvalidFormat {
440 message: format!(
441 "Text exceeds maximum of {} lines, found {}",
442 max_lines,
443 lines.len()
444 ),
445 });
446 }
447
448 for (i, line) in lines.iter().enumerate() {
449 if line.len() > max_line_length {
450 return Err(ParseError::InvalidFormat {
451 message: format!(
452 "Line {} exceeds maximum length of {} characters",
453 i + 1,
454 max_line_length
455 ),
456 });
457 }
458 }
459
460 Ok(lines)
461}
462
463pub fn extract_field_option(tag: &str) -> Option<char> {
465 if tag.len() >= 5 && tag.starts_with(':') && tag.ends_with(':') {
467 let inner = &tag[1..tag.len() - 1];
468 if inner.len() == 3 && inner[0..2].chars().all(|c| c.is_numeric()) {
469 return inner.chars().nth(2);
470 }
471 }
472 None
473}
474
475pub fn parse_field_with_suffix(input: &str) -> (String, Option<char>) {
477 if let Some(last_char) = input.chars().last()
478 && last_char.is_alphabetic()
479 && input[..input.len() - 1].chars().all(|c| c.is_numeric())
480 {
481 return (input[..input.len() - 1].to_string(), Some(last_char));
482 }
483 (input.to_string(), None)
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489
490 #[test]
491 fn test_payment_method_code() {
492 assert_eq!(PaymentMethodCode::parse("FW"), Some(PaymentMethodCode::FW));
493 assert_eq!(PaymentMethodCode::parse("RT"), Some(PaymentMethodCode::RT));
494 assert_eq!(PaymentMethodCode::parse("XX"), None);
495
496 assert_eq!(PaymentMethodCode::FW.as_str(), "FW");
497 }
498
499 #[test]
500 fn test_parse_payment_method() {
501 assert_eq!(parse_payment_method("//FW"), Some(PaymentMethodCode::FW));
502 assert_eq!(parse_payment_method("//RT"), Some(PaymentMethodCode::RT));
503 assert_eq!(parse_payment_method("FW"), None);
504 assert_eq!(parse_payment_method("//XXX"), None);
505 }
506
507 #[test]
508 fn test_transaction_type_code() {
509 assert_eq!(
510 TransactionTypeCode::parse("MSC"),
511 Some(TransactionTypeCode::MSC)
512 );
513 assert_eq!(
514 TransactionTypeCode::parse("CHK"),
515 Some(TransactionTypeCode::CHK)
516 );
517 assert_eq!(TransactionTypeCode::parse("XXX"), None);
518
519 assert_eq!(TransactionTypeCode::MSC.as_str(), "MSC");
520 }
521
522 #[test]
523 fn test_validate_field_option() {
524 assert!(validate_field_option("50", Some('A'), &['A', 'K', 'F']).is_ok());
525 assert!(validate_field_option("50", Some('X'), &['A', 'K', 'F']).is_err());
526 assert!(validate_field_option("50", None, &['A', 'K', 'F']).is_ok());
527 }
528
529 #[test]
530 fn test_parse_field_tag() {
531 assert_eq!(parse_field_tag("50A"), ("50".to_string(), Some('A')));
532 assert_eq!(parse_field_tag("50"), ("50".to_string(), None));
533 assert_eq!(parse_field_tag("32A"), ("32".to_string(), Some('A')));
534 assert_eq!(parse_field_tag("ABC"), ("ABC".to_string(), None));
535 }
536
537 #[test]
538 fn test_is_numbered_line() {
539 assert!(is_numbered_line("1/ACCOUNT"));
540 assert!(is_numbered_line("2/NAME"));
541 assert!(is_numbered_line("9/TEXT"));
542 assert!(!is_numbered_line("ACCOUNT"));
543 assert!(!is_numbered_line("/ACCOUNT"));
544 assert!(!is_numbered_line("A/ACCOUNT"));
545 }
546
547 #[test]
548 fn test_parse_numbered_lines() {
549 let lines = vec!["1/ACCOUNT", "2/NAME", "3/ADDRESS"];
550 let result = parse_numbered_lines(&lines).unwrap();
551 assert_eq!(result.len(), 3);
552 assert_eq!(result[0], (1, "ACCOUNT".to_string()));
553 assert_eq!(result[1], (2, "NAME".to_string()));
554 assert_eq!(result[2], (3, "ADDRESS".to_string()));
555
556 let bad_lines = vec!["1/ACCOUNT", "NAME"];
557 assert!(parse_numbered_lines(&bad_lines).is_err());
558 }
559
560 #[test]
561 fn test_extract_field_number() {
562 assert_eq!(extract_field_number("50A"), "50");
563 assert_eq!(extract_field_number("32"), "32");
564 assert_eq!(extract_field_number("103STP"), "103");
565 assert_eq!(extract_field_number("ABC"), "");
566 }
567
568 #[test]
569 fn test_parse_party_identifier() {
570 let result = parse_party_identifier("/D/12345678").unwrap();
572 assert_eq!(result, Some("D/12345678".to_string()));
573
574 let result = parse_party_identifier("/CH/123456").unwrap();
576 assert_eq!(result, Some("CH/123456".to_string()));
577
578 let result = parse_party_identifier("/ACCOUNT123").unwrap();
580 assert_eq!(result, Some("ACCOUNT123".to_string()));
581
582 let result = parse_party_identifier("//FW123456").unwrap();
584 assert_eq!(result, Some("/FW123456".to_string()));
585
586 let result = parse_party_identifier("//RT").unwrap();
587 assert_eq!(result, Some("/RT".to_string()));
588
589 let result = parse_party_identifier("NOTPARTY").unwrap();
591 assert_eq!(result, None);
592
593 assert!(parse_party_identifier("/D/12345678901234567890123456789012345").is_err());
595 }
596
597 #[test]
598 fn test_parse_debit_credit_mark() {
599 assert_eq!(parse_debit_credit_mark('D').unwrap(), "D");
600 assert_eq!(parse_debit_credit_mark('C').unwrap(), "C");
601 assert!(parse_debit_credit_mark('X').is_err());
602 assert!(parse_debit_credit_mark('1').is_err());
603 }
604
605 #[test]
606 fn test_validate_multiline_text() {
607 let lines = vec!["LINE 1", "LINE 2", "LINE 3"];
608 let result = validate_multiline_text(&lines, 4, 35, "Test Field").unwrap();
609 assert_eq!(result.len(), 3);
610 assert_eq!(result[0], "LINE 1");
611
612 let lines = vec!["L1", "L2", "L3", "L4", "L5"];
614 assert!(validate_multiline_text(&lines, 4, 35, "Test Field").is_err());
615
616 let lines = vec!["THIS LINE IS TOO LONG AND EXCEEDS THE 35 CHARACTER LIMIT"];
618 assert!(validate_multiline_text(&lines, 4, 35, "Test Field").is_err());
619
620 let lines: Vec<&str> = vec![];
622 assert!(validate_multiline_text(&lines, 4, 35, "Test Field").is_err());
623 }
624
625 #[test]
626 fn test_parse_name_and_address() {
627 let lines = vec!["PARTY ID", "JOHN DOE", "123 MAIN ST", "NEW YORK"];
628 let result = parse_name_and_address(&lines, 1, "Test Field").unwrap();
629 assert_eq!(result.len(), 3);
630 assert_eq!(result[0], "JOHN DOE");
631 assert_eq!(result[1], "123 MAIN ST");
632 assert_eq!(result[2], "NEW YORK");
633
634 let lines = vec!["ID", "L1", "L2", "L3", "L4", "L5"];
636 assert!(parse_name_and_address(&lines, 1, "Test Field").is_err());
637
638 let lines = vec![
640 "ID",
641 "THIS LINE IS TOO LONG AND EXCEEDS THE 35 CHARACTER LIMIT",
642 ];
643 assert!(parse_name_and_address(&lines, 1, "Test Field").is_err());
644 }
645
646 #[test]
647 fn test_extract_field_option() {
648 assert_eq!(extract_field_option(":50A:"), Some('A'));
649 assert_eq!(extract_field_option(":50K:"), Some('K'));
650 assert_eq!(extract_field_option(":20:"), None);
651 assert_eq!(extract_field_option("50A"), None);
652 }
653
654 #[test]
655 fn test_parse_field_with_suffix() {
656 assert_eq!(
657 parse_field_with_suffix("20C"),
658 ("20".to_string(), Some('C'))
659 );
660 assert_eq!(parse_field_with_suffix("20"), ("20".to_string(), None));
661 assert_eq!(parse_field_with_suffix("ABC"), ("ABC".to_string(), None));
662 }
663}