swift_mt_message/fields/
field90.rs

1//! **Field 90: Number & Sum**
2//!
3//! Number of transactions and total sum for control and reconciliation in statements.
4//!
5//! **Variants:**
6//! - **Field 90C**: Credit entries (number and sum)
7//! - **Field 90D**: Debit entries (number and sum)
8//!
9//! **Format:** `5n3!a15d` (number, currency, amount)
10//! **Used in:** MT 940, MT 942 (statement messages)
11
12use super::swift_utils::{parse_amount, parse_currency, parse_swift_digits};
13use crate::errors::ParseError;
14use crate::traits::SwiftField;
15use serde::{Deserialize, Serialize};
16
17/// **Field 90D: Number & Sum of Debit Entries**
18///
19/// Number and total sum of debit transactions for control purposes.
20///
21/// **Format:** `5n3!a15d` (number, currency, amount)
22///
23/// **Example:**
24/// ```text
25/// :90D:2GBP250050,00
26/// ```
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct Field90D {
29    /// Number of debit transactions (max 5 digits)
30    pub number: u32,
31
32    /// Currency code (ISO 4217)
33    pub currency: String,
34
35    /// Total sum of debit amounts
36    pub amount: f64,
37}
38
39impl SwiftField for Field90D {
40    fn parse(input: &str) -> crate::Result<Self>
41    where
42        Self: Sized,
43    {
44        let mut remaining = input;
45
46        // Parse number of transactions (5n)
47        if remaining.len() < 8 {
48            return Err(ParseError::InvalidFormat {
49                message: "Field90D requires at least 8 characters (5n + 3!a)".to_string(),
50            });
51        }
52
53        // Find where number ends by looking for non-digit
54        let mut number_end = 0;
55        for (i, c) in remaining.char_indices() {
56            if !c.is_ascii_digit() {
57                number_end = i;
58                break;
59            }
60            if i >= 4 {
61                // Max 5 digits
62                number_end = i + 1;
63                break;
64            }
65        }
66
67        if number_end == 0 {
68            return Err(ParseError::InvalidFormat {
69                message: "Field90D number part not found".to_string(),
70            });
71        }
72
73        let number_str = &remaining[..number_end];
74        if number_str.len() > 5 {
75            return Err(ParseError::InvalidFormat {
76                message: "Field90D number cannot exceed 5 digits".to_string(),
77            });
78        }
79
80        parse_swift_digits(number_str, "Field90D number")?;
81        let number: u32 = number_str.parse().map_err(|_| ParseError::InvalidFormat {
82            message: "Invalid number in Field90D".to_string(),
83        })?;
84
85        remaining = &remaining[number_end..];
86
87        // Parse currency (3!a)
88        if remaining.len() < 3 {
89            return Err(ParseError::InvalidFormat {
90                message: "Field90D requires currency code".to_string(),
91            });
92        }
93
94        let currency = parse_currency(&remaining[..3])?;
95        remaining = &remaining[3..];
96
97        // Parse amount (15d)
98        if remaining.is_empty() {
99            return Err(ParseError::InvalidFormat {
100                message: "Field90D requires amount".to_string(),
101            });
102        }
103
104        let amount = parse_amount(remaining)?;
105
106        Ok(Field90D {
107            number,
108            currency,
109            amount,
110        })
111    }
112
113    fn to_swift_string(&self) -> String {
114        let amount_str = format!("{:.2}", self.amount).replace('.', ",");
115        format!(":90D:{}{}{}", self.number, self.currency, amount_str)
116    }
117}
118
119/// **Field 90C: Number & Sum of Credit Entries**
120///
121/// Number and total sum of credit transactions for control purposes.
122///
123/// **Format:** `5n3!a15d` (number, currency, amount)
124///
125/// **Example:**
126/// ```text
127/// :90C:5USD12500,50
128/// ```
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130pub struct Field90C {
131    /// Number of credit transactions (max 5 digits)
132    pub number: u32,
133
134    /// Currency code (ISO 4217)
135    pub currency: String,
136
137    /// Total sum of credit amounts
138    pub amount: f64,
139}
140
141impl SwiftField for Field90C {
142    fn parse(input: &str) -> crate::Result<Self>
143    where
144        Self: Sized,
145    {
146        let mut remaining = input;
147
148        // Parse number of transactions (5n)
149        if remaining.len() < 8 {
150            return Err(ParseError::InvalidFormat {
151                message: "Field90C requires at least 8 characters (5n + 3!a)".to_string(),
152            });
153        }
154
155        // Find where number ends by looking for non-digit
156        let mut number_end = 0;
157        for (i, c) in remaining.char_indices() {
158            if !c.is_ascii_digit() {
159                number_end = i;
160                break;
161            }
162            if i >= 4 {
163                // Max 5 digits
164                number_end = i + 1;
165                break;
166            }
167        }
168
169        if number_end == 0 {
170            return Err(ParseError::InvalidFormat {
171                message: "Field90C number part not found".to_string(),
172            });
173        }
174
175        let number_str = &remaining[..number_end];
176        if number_str.len() > 5 {
177            return Err(ParseError::InvalidFormat {
178                message: "Field90C number cannot exceed 5 digits".to_string(),
179            });
180        }
181
182        parse_swift_digits(number_str, "Field90C number")?;
183        let number: u32 = number_str.parse().map_err(|_| ParseError::InvalidFormat {
184            message: "Invalid number in Field90C".to_string(),
185        })?;
186
187        remaining = &remaining[number_end..];
188
189        // Parse currency (3!a)
190        if remaining.len() < 3 {
191            return Err(ParseError::InvalidFormat {
192                message: "Field90C requires currency code".to_string(),
193            });
194        }
195
196        let currency = parse_currency(&remaining[..3])?;
197        remaining = &remaining[3..];
198
199        // Parse amount (15d)
200        if remaining.is_empty() {
201            return Err(ParseError::InvalidFormat {
202                message: "Field90C requires amount".to_string(),
203            });
204        }
205
206        let amount = parse_amount(remaining)?;
207
208        Ok(Field90C {
209            number,
210            currency,
211            amount,
212        })
213    }
214
215    fn to_swift_string(&self) -> String {
216        let amount_str = format!("{:.2}", self.amount).replace('.', ",");
217        format!(":90C:{}{}{}", self.number, self.currency, amount_str)
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_field90d_parsing_basic() {
227        let value = "2GBP250050";
228        match Field90D::parse(value) {
229            Ok(field) => {
230                assert_eq!(field.number, 2);
231                assert_eq!(field.currency, "GBP");
232                assert_eq!(field.amount, 250050.0);
233            }
234            Err(e) => {
235                panic!("Failed to parse Field90D '{}': {:?}", value, e);
236            }
237        }
238    }
239}