swift_mt_message/fields/
field60.rs

1use super::swift_utils::{parse_amount, parse_currency, parse_date_yymmdd, parse_exact_length};
2use crate::errors::ParseError;
3use crate::traits::SwiftField;
4use chrono::NaiveDate;
5use serde::{Deserialize, Serialize};
6
7/// **Field 60: Opening Balance**
8///
9/// Opening balance for account statements (MT 940).
10///
11/// **Format:** `1!a6!n3!a15d` (D/C mark + YYMMDD + currency + amount)
12/// **Variants:** F (first opening balance), M (intermediate opening balance)
13///
14/// **Example:**
15/// ```text
16/// :60F:C231225USD1234,56
17/// ```
18///
19/// **Field 60F: First Opening Balance**
20///
21/// Initial balance at statement start.
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct Field60F {
24    /// Debit/Credit mark (D or C)
25    pub debit_credit_mark: String,
26
27    /// Value date (YYMMDD)
28    pub value_date: NaiveDate,
29
30    /// ISO 4217 currency code
31    pub currency: String,
32
33    /// Opening balance amount
34    pub amount: f64,
35}
36
37/// **Field 60M: Intermediate Opening Balance**
38///
39/// Balance at sequence break within statement.
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub struct Field60M {
42    /// Debit/Credit mark (D or C)
43    pub debit_credit_mark: String,
44
45    /// Value date (YYMMDD)
46    pub value_date: NaiveDate,
47
48    /// ISO 4217 currency code
49    pub currency: String,
50
51    /// Intermediate opening balance amount
52    pub amount: f64,
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub enum Field60 {
57    F(Field60F),
58    M(Field60M),
59}
60
61impl SwiftField for Field60F {
62    fn parse(input: &str) -> crate::Result<Self>
63    where
64        Self: Sized,
65    {
66        // Format: 1!a6!n3!a15d - DebitCredit + Date + Currency + Amount
67        if input.len() < 10 {
68            return Err(ParseError::InvalidFormat {
69                message: "Field 60F must be at least 10 characters long".to_string(),
70            });
71        }
72
73        // Parse debit/credit mark (1 character)
74        let debit_credit_mark = parse_exact_length(&input[0..1], 1, "Field 60F debit/credit mark")?;
75        if debit_credit_mark != "D" && debit_credit_mark != "C" {
76            return Err(ParseError::InvalidFormat {
77                message: "Field 60F debit/credit mark must be 'D' or 'C'".to_string(),
78            });
79        }
80
81        // Parse value date (6 digits)
82        let date_str = parse_exact_length(&input[1..7], 6, "Field 60F value date")?;
83        let value_date = parse_date_yymmdd(&date_str)?;
84
85        // Parse currency (3 characters)
86        let currency = parse_exact_length(&input[7..10], 3, "Field 60F currency")?;
87        let currency = parse_currency(&currency)?;
88
89        // Parse amount (remaining characters)
90        let amount_str = &input[10..];
91        let amount = parse_amount(amount_str)?;
92
93        Ok(Field60F {
94            debit_credit_mark,
95            value_date,
96            currency,
97            amount,
98        })
99    }
100
101    fn to_swift_string(&self) -> String {
102        format!(
103            ":60F:{}{}{}{}",
104            self.debit_credit_mark,
105            self.value_date.format("%y%m%d"),
106            self.currency,
107            format!("{:.2}", self.amount).replace('.', ",")
108        )
109    }
110}
111
112impl SwiftField for Field60M {
113    fn parse(input: &str) -> crate::Result<Self>
114    where
115        Self: Sized,
116    {
117        // Format: 1!a6!n3!a15d - DebitCredit + Date + Currency + Amount
118        if input.len() < 10 {
119            return Err(ParseError::InvalidFormat {
120                message: "Field 60M must be at least 10 characters long".to_string(),
121            });
122        }
123
124        // Parse debit/credit mark (1 character)
125        let debit_credit_mark = parse_exact_length(&input[0..1], 1, "Field 60M debit/credit mark")?;
126        if debit_credit_mark != "D" && debit_credit_mark != "C" {
127            return Err(ParseError::InvalidFormat {
128                message: "Field 60M debit/credit mark must be 'D' or 'C'".to_string(),
129            });
130        }
131
132        // Parse value date (6 digits)
133        let date_str = parse_exact_length(&input[1..7], 6, "Field 60M value date")?;
134        let value_date = parse_date_yymmdd(&date_str)?;
135
136        // Parse currency (3 characters)
137        let currency = parse_exact_length(&input[7..10], 3, "Field 60M currency")?;
138        let currency = parse_currency(&currency)?;
139
140        // Parse amount (remaining characters)
141        let amount_str = &input[10..];
142        let amount = parse_amount(amount_str)?;
143
144        Ok(Field60M {
145            debit_credit_mark,
146            value_date,
147            currency,
148            amount,
149        })
150    }
151
152    fn to_swift_string(&self) -> String {
153        format!(
154            ":60M:{}{}{}{}",
155            self.debit_credit_mark,
156            self.value_date.format("%y%m%d"),
157            self.currency,
158            format!("{:.2}", self.amount).replace('.', ",")
159        )
160    }
161}
162
163impl SwiftField for Field60 {
164    fn parse(_input: &str) -> crate::Result<Self>
165    where
166        Self: Sized,
167    {
168        // This should not be called directly - parsing is handled by the message parser
169        // which determines the variant (F or M) from the field tag
170        Err(ParseError::InvalidFormat {
171            message: "Field60 enum should not be parsed directly".to_string(),
172        })
173    }
174
175    fn parse_with_variant(
176        value: &str,
177        variant: Option<&str>,
178        _field_tag: Option<&str>,
179    ) -> crate::Result<Self>
180    where
181        Self: Sized,
182    {
183        match variant {
184            Some("F") => {
185                let field = Field60F::parse(value)?;
186                Ok(Field60::F(field))
187            }
188            Some("M") => {
189                let field = Field60M::parse(value)?;
190                Ok(Field60::M(field))
191            }
192            _ => {
193                // No variant specified or unknown variant
194                Err(ParseError::InvalidFormat {
195                    message: "Field60 requires variant F or M".to_string(),
196                })
197            }
198        }
199    }
200
201    fn to_swift_string(&self) -> String {
202        match self {
203            Field60::F(field) => field.to_swift_string(),
204            Field60::M(field) => field.to_swift_string(),
205        }
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use chrono::NaiveDate;
213
214    #[test]
215    fn test_field60f_parse_valid() {
216        let field = Field60F::parse("C231225USD1234,56").unwrap();
217        assert_eq!(field.debit_credit_mark, "C");
218        assert_eq!(
219            field.value_date,
220            NaiveDate::from_ymd_opt(2023, 12, 25).unwrap()
221        );
222        assert_eq!(field.currency, "USD");
223        assert_eq!(field.amount, 1234.56);
224    }
225
226    #[test]
227    fn test_field60m_parse_valid() {
228        let field = Field60M::parse("D991231EUR500,00").unwrap();
229        assert_eq!(field.debit_credit_mark, "D");
230        assert_eq!(
231            field.value_date,
232            NaiveDate::from_ymd_opt(1999, 12, 31).unwrap()
233        );
234        assert_eq!(field.currency, "EUR");
235        assert_eq!(field.amount, 500.00);
236    }
237
238    #[test]
239    fn test_field60f_invalid_debit_credit_mark() {
240        assert!(Field60F::parse("X231225USD1234,56").is_err());
241    }
242
243    #[test]
244    fn test_field60f_to_swift_string() {
245        let field = Field60F {
246            debit_credit_mark: "C".to_string(),
247            value_date: NaiveDate::from_ymd_opt(2023, 12, 25).unwrap(),
248            currency: "USD".to_string(),
249            amount: 1234.56,
250        };
251        assert_eq!(field.to_swift_string(), ":60F:C231225USD1234,56");
252    }
253
254    #[test]
255    fn test_field60_enum_to_swift_string() {
256        let field_f = Field60::F(Field60F {
257            debit_credit_mark: "C".to_string(),
258            value_date: NaiveDate::from_ymd_opt(2023, 12, 25).unwrap(),
259            currency: "USD".to_string(),
260            amount: 1234.56,
261        });
262        assert_eq!(field_f.to_swift_string(), ":60F:C231225USD1234,56");
263
264        let field_m = Field60::M(Field60M {
265            debit_credit_mark: "D".to_string(),
266            value_date: NaiveDate::from_ymd_opt(2023, 12, 25).unwrap(),
267            currency: "EUR".to_string(),
268            amount: 500.00,
269        });
270        assert_eq!(field_m.to_swift_string(), ":60M:D231225EUR500,00");
271    }
272}