1use super::swift_utils::{
18 format_swift_amount_for_currency, parse_amount_with_currency, parse_currency_non_commodity,
19 parse_date_yymmdd,
20};
21use crate::errors::ParseError;
22use crate::traits::SwiftField;
23use chrono::NaiveDate;
24use serde::{Deserialize, Serialize};
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct Field32A {
32 #[serde(with = "date_string")]
34 pub value_date: NaiveDate,
35 pub currency: String,
37 pub amount: f64,
39}
40
41impl SwiftField for Field32A {
42 fn parse(input: &str) -> crate::Result<Self>
43 where
44 Self: Sized,
45 {
46 if input.len() < 10 {
48 return Err(ParseError::InvalidFormat {
50 message: format!(
51 "Field 32A must be at least 10 characters, found {}",
52 input.len()
53 ),
54 });
55 }
56
57 let value_date = parse_date_yymmdd(&input[0..6])?;
59
60 let currency = parse_currency_non_commodity(&input[6..9])?;
62
63 let amount_str = &input[9..];
65 if amount_str.is_empty() {
66 return Err(ParseError::InvalidFormat {
67 message: "Field 32A amount cannot be empty".to_string(),
68 });
69 }
70
71 let amount = parse_amount_with_currency(amount_str, ¤cy)?;
72
73 if amount <= 0.0 {
75 return Err(ParseError::InvalidFormat {
76 message: "Field 32A amount must be greater than zero".to_string(),
77 });
78 }
79
80 Ok(Field32A {
81 value_date,
82 currency,
83 amount,
84 })
85 }
86
87 fn to_swift_string(&self) -> String {
88 format!(
89 ":32A:{}{}{}",
90 self.value_date.format("%y%m%d"),
91 self.currency,
92 format_swift_amount_for_currency(self.amount, &self.currency)
93 )
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102pub struct Field32B {
103 pub currency: String,
105 pub amount: f64,
107}
108
109impl SwiftField for Field32B {
110 fn parse(input: &str) -> crate::Result<Self>
111 where
112 Self: Sized,
113 {
114 if input.len() < 4 {
116 return Err(ParseError::InvalidFormat {
118 message: format!(
119 "Field 32B must be at least 4 characters, found {}",
120 input.len()
121 ),
122 });
123 }
124
125 let currency = parse_currency_non_commodity(&input[0..3])?;
127
128 let amount_str = &input[3..];
130 if amount_str.is_empty() {
131 return Err(ParseError::InvalidFormat {
132 message: "Field 32B amount cannot be empty".to_string(),
133 });
134 }
135
136 let amount = parse_amount_with_currency(amount_str, ¤cy)?;
137
138 if amount <= 0.0 {
140 return Err(ParseError::InvalidFormat {
141 message: "Field 32B amount must be greater than zero".to_string(),
142 });
143 }
144
145 Ok(Field32B { currency, amount })
146 }
147
148 fn to_swift_string(&self) -> String {
149 format!(
150 ":32B:{}{}",
151 self.currency,
152 format_swift_amount_for_currency(self.amount, &self.currency)
153 )
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
162pub struct Field32C {
163 #[serde(with = "date_string")]
165 pub value_date: NaiveDate,
166 pub currency: String,
168 pub amount: f64,
170}
171
172mod date_string {
174 use chrono::NaiveDate;
175 use serde::{Deserialize, Deserializer, Serializer};
176
177 pub fn serialize<S>(date: &NaiveDate, serializer: S) -> Result<S::Ok, S::Error>
178 where
179 S: Serializer,
180 {
181 serializer.serialize_str(&date.format("%Y-%m-%d").to_string())
182 }
183
184 pub fn deserialize<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error>
185 where
186 D: Deserializer<'de>,
187 {
188 let s = String::deserialize(deserializer)?;
189 NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(serde::de::Error::custom)
190 }
191}
192
193impl SwiftField for Field32C {
194 fn parse(input: &str) -> crate::Result<Self>
195 where
196 Self: Sized,
197 {
198 if input.len() < 10 {
200 return Err(ParseError::InvalidFormat {
201 message: format!(
202 "Field 32C must be at least 10 characters, found {}",
203 input.len()
204 ),
205 });
206 }
207
208 let value_date = parse_date_yymmdd(&input[0..6])?;
209 let currency = parse_currency_non_commodity(&input[6..9])?;
210 let amount_str = &input[9..];
211
212 if amount_str.is_empty() {
213 return Err(ParseError::InvalidFormat {
214 message: "Field 32C amount cannot be empty".to_string(),
215 });
216 }
217
218 let amount = parse_amount_with_currency(amount_str, ¤cy)?;
219
220 if amount <= 0.0 {
221 return Err(ParseError::InvalidFormat {
222 message: "Field 32C amount must be greater than zero".to_string(),
223 });
224 }
225
226 Ok(Field32C {
227 value_date,
228 currency,
229 amount,
230 })
231 }
232
233 fn to_swift_string(&self) -> String {
234 format!(
235 ":32C:{}{}{}",
236 self.value_date.format("%y%m%d"),
237 self.currency,
238 format_swift_amount_for_currency(self.amount, &self.currency)
239 )
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
248pub struct Field32D {
249 #[serde(with = "date_string")]
251 pub value_date: NaiveDate,
252 pub currency: String,
254 pub amount: f64,
256}
257
258impl SwiftField for Field32D {
259 fn parse(input: &str) -> crate::Result<Self>
260 where
261 Self: Sized,
262 {
263 if input.len() < 10 {
265 return Err(ParseError::InvalidFormat {
266 message: format!(
267 "Field 32D must be at least 10 characters, found {}",
268 input.len()
269 ),
270 });
271 }
272
273 let value_date = parse_date_yymmdd(&input[0..6])?;
274 let currency = parse_currency_non_commodity(&input[6..9])?;
275 let amount_str = &input[9..];
276
277 if amount_str.is_empty() {
278 return Err(ParseError::InvalidFormat {
279 message: "Field 32D amount cannot be empty".to_string(),
280 });
281 }
282
283 let amount = parse_amount_with_currency(amount_str, ¤cy)?;
284
285 if amount <= 0.0 {
286 return Err(ParseError::InvalidFormat {
287 message: "Field 32D amount must be greater than zero".to_string(),
288 });
289 }
290
291 Ok(Field32D {
292 value_date,
293 currency,
294 amount,
295 })
296 }
297
298 fn to_swift_string(&self) -> String {
299 format!(
300 ":32D:{}{}{}",
301 self.value_date.format("%y%m%d"),
302 self.currency,
303 format_swift_amount_for_currency(self.amount, &self.currency)
304 )
305 }
306}
307
308#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
312pub enum Field32 {
313 #[serde(rename = "32A")]
314 A(Field32A),
315 #[serde(rename = "32B")]
316 B(Field32B),
317 #[serde(rename = "32C")]
318 C(Field32C),
319 #[serde(rename = "32D")]
320 D(Field32D),
321}
322
323impl SwiftField for Field32 {
324 fn parse(input: &str) -> crate::Result<Self>
325 where
326 Self: Sized,
327 {
328 if input.len() >= 6 {
332 if input[0..6].chars().all(|c| c.is_ascii_digit()) {
334 Ok(Field32::A(Field32A::parse(input)?))
336 } else if input.len() >= 3 && input[0..3].chars().all(|c| c.is_ascii_alphabetic()) {
337 Ok(Field32::B(Field32B::parse(input)?))
339 } else {
340 Err(ParseError::InvalidFormat {
341 message:
342 "Field 32 must start with either date (6 digits) or currency (3 letters)"
343 .to_string(),
344 })
345 }
346 } else {
347 Err(ParseError::InvalidFormat {
348 message: format!(
349 "Field 32 must be at least 6 characters, found {}",
350 input.len()
351 ),
352 })
353 }
354 }
355
356 fn to_swift_string(&self) -> String {
357 match self {
358 Field32::A(field) => field.to_swift_string(),
359 Field32::B(field) => field.to_swift_string(),
360 Field32::C(field) => field.to_swift_string(),
361 Field32::D(field) => field.to_swift_string(),
362 }
363 }
364}
365
366#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
371pub enum Field32AB {
372 #[serde(rename = "32A")]
373 A(Field32A),
374 #[serde(rename = "32B")]
375 B(Field32B),
376}
377
378impl SwiftField for Field32AB {
379 fn parse(input: &str) -> crate::Result<Self>
380 where
381 Self: Sized,
382 {
383 if let Ok(field) = Field32A::parse(input) {
385 return Ok(Field32AB::A(field));
386 }
387
388 if let Ok(field) = Field32B::parse(input) {
390 return Ok(Field32AB::B(field));
391 }
392
393 Err(ParseError::InvalidFormat {
394 message: "Field 32 must be either format 32A (YYMMDD + Currency + Amount) or 32B (Currency + Amount)".to_string(),
395 })
396 }
397
398 fn to_swift_string(&self) -> String {
399 match self {
400 Field32AB::A(field) => field.to_swift_string(),
401 Field32AB::B(field) => field.to_swift_string(),
402 }
403 }
404}
405
406#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
411pub enum Field32AmountCD {
412 #[serde(rename = "32C")]
413 C(Field32C),
414 #[serde(rename = "32D")]
415 D(Field32D),
416}
417
418impl SwiftField for Field32AmountCD {
419 fn parse(input: &str) -> crate::Result<Self>
420 where
421 Self: Sized,
422 {
423 if let Ok(field) = Field32C::parse(input) {
426 return Ok(Field32AmountCD::C(field));
427 }
428
429 if let Ok(field) = Field32D::parse(input) {
431 return Ok(Field32AmountCD::D(field));
432 }
433
434 Err(ParseError::InvalidFormat {
435 message: "Field 32 must be in format: YYMMDD + Currency + Amount".to_string(),
436 })
437 }
438
439 fn parse_with_variant(
440 value: &str,
441 variant: Option<&str>,
442 _field_tag: Option<&str>,
443 ) -> crate::Result<Self>
444 where
445 Self: Sized,
446 {
447 match variant {
449 Some("C") => {
450 let field = Field32C::parse(value)?;
451 Ok(Field32AmountCD::C(field))
452 }
453 Some("D") => {
454 let field = Field32D::parse(value)?;
455 Ok(Field32AmountCD::D(field))
456 }
457 _ => {
458 Self::parse(value)
460 }
461 }
462 }
463
464 fn to_swift_string(&self) -> String {
465 match self {
466 Field32AmountCD::C(field) => field.to_swift_string(),
467 Field32AmountCD::D(field) => field.to_swift_string(),
468 }
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475 use chrono::NaiveDate;
476
477 #[test]
478 fn test_field32a_valid() {
479 let field = Field32A::parse("240719EUR1250,50").unwrap();
480 assert_eq!(
481 field.value_date,
482 NaiveDate::from_ymd_opt(2024, 7, 19).unwrap()
483 );
484 assert_eq!(field.currency, "EUR");
485 assert_eq!(field.amount, 1250.50);
486 assert_eq!(
487 field.to_swift_string(),
488 ":32A:240719EUR1250.5".replace('.', ",")
489 );
490
491 let field = Field32A::parse("240720USD10000,00").unwrap();
492 assert_eq!(field.currency, "USD");
493 assert_eq!(field.amount, 10000.0);
494
495 let field = Field32A::parse("240721JPY1500000").unwrap();
496 assert_eq!(field.currency, "JPY");
497 assert_eq!(field.amount, 1500000.0);
498 }
499
500 #[test]
501 fn test_field32a_invalid() {
502 assert!(Field32A::parse("991332EUR100").is_err());
504
505 assert!(Field32A::parse("240719EU1100").is_err());
507 assert!(Field32A::parse("2407191UR100").is_err());
508
509 assert!(Field32A::parse("240719EUR0").is_err());
511
512 assert!(Field32A::parse("240719EUR-100").is_err());
514
515 assert!(Field32A::parse("240719EUR").is_err());
517 }
518
519 #[test]
520 fn test_field32b_valid() {
521 let field = Field32B::parse("EUR5000,00").unwrap();
522 assert_eq!(field.currency, "EUR");
523 assert_eq!(field.amount, 5000.0);
524 assert_eq!(field.to_swift_string(), ":32B:EUR5000");
525
526 let field = Field32B::parse("USD100").unwrap();
527 assert_eq!(field.currency, "USD");
528 assert_eq!(field.amount, 100.0);
529 }
530
531 #[test]
532 fn test_field32b_invalid() {
533 assert!(Field32B::parse("12A100").is_err());
535
536 assert!(Field32B::parse("EUR0").is_err());
538
539 assert!(Field32B::parse("EUR").is_err());
541 }
542
543 #[test]
544 fn test_field32c_valid() {
545 let field = Field32C::parse("240719EUR500,25").unwrap();
546 assert_eq!(
547 field.value_date,
548 NaiveDate::from_ymd_opt(2024, 7, 19).unwrap()
549 );
550 assert_eq!(field.currency, "EUR");
551 assert_eq!(field.amount, 500.25);
552 }
553
554 #[test]
555 fn test_field32d_valid() {
556 let field = Field32D::parse("240719USD750,50").unwrap();
557 assert_eq!(
558 field.value_date,
559 NaiveDate::from_ymd_opt(2024, 7, 19).unwrap()
560 );
561 assert_eq!(field.currency, "USD");
562 assert_eq!(field.amount, 750.50);
563 }
564
565 #[test]
566 fn test_field32_enum() {
567 let field = Field32::parse("240719EUR1000").unwrap();
569 match field {
570 Field32::A(f) => {
571 assert_eq!(f.currency, "EUR");
572 assert_eq!(f.amount, 1000.0);
573 }
574 _ => panic!("Expected Field32::A"),
575 }
576
577 let field = Field32::parse("EUR2000").unwrap();
579 match field {
580 Field32::B(f) => {
581 assert_eq!(f.currency, "EUR");
582 assert_eq!(f.amount, 2000.0);
583 }
584 _ => panic!("Expected Field32::B"),
585 }
586 }
587
588 #[test]
589 fn test_field32_ab() {
590 let field = Field32AB::parse("240719EUR500,25").unwrap();
592 match field {
593 Field32AB::A(f) => {
594 assert_eq!(f.value_date, NaiveDate::from_ymd_opt(2024, 7, 19).unwrap());
595 assert_eq!(f.currency, "EUR");
596 assert_eq!(f.amount, 500.25);
597 }
598 _ => panic!("Expected Field32AB::A"),
599 }
600
601 let field = Field32AB::parse("USD1000,00").unwrap();
603 match field {
604 Field32AB::B(f) => {
605 assert_eq!(f.currency, "USD");
606 assert_eq!(f.amount, 1000.00);
607 }
608 _ => panic!("Expected Field32AB::B"),
609 }
610
611 let field_a = Field32AB::A(Field32A {
613 value_date: NaiveDate::from_ymd_opt(2024, 7, 19).unwrap(),
614 currency: "EUR".to_string(),
615 amount: 500.25,
616 });
617 assert_eq!(field_a.to_swift_string(), ":32A:240719EUR500,25");
618
619 let field_b = Field32AB::B(Field32B {
621 currency: "USD".to_string(),
622 amount: 1000.00,
623 });
624 assert_eq!(field_b.to_swift_string(), ":32B:USD1000");
625 }
626
627 #[test]
628 fn test_field32_amount_cd() {
629 let field = Field32AmountCD::parse("240719EUR500,25").unwrap();
631 match field {
632 Field32AmountCD::C(f) => {
633 assert_eq!(f.value_date, NaiveDate::from_ymd_opt(2024, 7, 19).unwrap());
634 assert_eq!(f.currency, "EUR");
635 assert_eq!(f.amount, 500.25);
636 }
637 _ => panic!("Expected Field32AmountCD::C"),
638 }
639
640 let field = Field32AmountCD::parse("240720USD750,50").unwrap();
642 match field {
643 Field32AmountCD::C(f) => {
644 assert_eq!(f.value_date, NaiveDate::from_ymd_opt(2024, 7, 20).unwrap());
646 assert_eq!(f.currency, "USD");
647 assert_eq!(f.amount, 750.50);
648 }
649 _ => panic!("Expected Field32AmountCD::C"),
650 }
651
652 let credit_field = Field32AmountCD::C(Field32C {
654 value_date: NaiveDate::from_ymd_opt(2024, 7, 19).unwrap(),
655 currency: "EUR".to_string(),
656 amount: 500.25,
657 });
658 assert_eq!(credit_field.to_swift_string(), ":32C:240719EUR500,25");
659
660 let debit_field = Field32AmountCD::D(Field32D {
661 value_date: NaiveDate::from_ymd_opt(2024, 7, 20).unwrap(),
662 currency: "USD".to_string(),
663 amount: 750.50,
664 });
665 assert_eq!(debit_field.to_swift_string(), ":32D:240720USD750,5");
666 }
667
668 #[test]
669 fn test_field32a_c08_commodity_currency_rejection() {
670 assert!(Field32A::parse("240719XAU1000").is_err()); assert!(Field32A::parse("240719XAG500").is_err()); assert!(Field32A::parse("240719XPT250").is_err()); assert!(Field32A::parse("240719XPD100").is_err()); let err = Field32A::parse("240719XAU1000").unwrap_err();
678 let err_msg = format!("{}", err);
679 assert!(err_msg.contains("C08"));
680 }
681
682 #[test]
683 fn test_field32a_c03_decimal_precision_validation() {
684 assert!(Field32A::parse("240719USD100.50").is_ok());
686 assert!(Field32A::parse("240719USD100,50").is_ok());
687 assert!(Field32A::parse("240719USD100.505").is_err()); assert!(Field32A::parse("240719JPY1500000").is_ok());
691 assert!(Field32A::parse("240719JPY1500000.5").is_err()); assert!(Field32A::parse("240719BHD100.505").is_ok());
695 assert!(Field32A::parse("240719BHD100,505").is_ok());
696 assert!(Field32A::parse("240719BHD100.5055").is_err()); let err = Field32A::parse("240719USD100.505").unwrap_err();
700 let err_msg = format!("{}", err);
701 assert!(err_msg.contains("C03"));
702 }
703
704 #[test]
705 fn test_field32a_currency_specific_formatting() {
706 let field_usd = Field32A {
708 value_date: NaiveDate::from_ymd_opt(2024, 7, 19).unwrap(),
709 currency: "USD".to_string(),
710 amount: 1000.50,
711 };
712 assert_eq!(field_usd.to_swift_string(), ":32A:240719USD1000,5");
713
714 let field_jpy = Field32A {
715 value_date: NaiveDate::from_ymd_opt(2024, 7, 19).unwrap(),
716 currency: "JPY".to_string(),
717 amount: 1500000.0,
718 };
719 assert_eq!(field_jpy.to_swift_string(), ":32A:240719JPY1500000");
720
721 let field_bhd = Field32A {
722 value_date: NaiveDate::from_ymd_opt(2024, 7, 19).unwrap(),
723 currency: "BHD".to_string(),
724 amount: 123.456,
725 };
726 assert_eq!(field_bhd.to_swift_string(), ":32A:240719BHD123,456");
727 }
728}