swift_mt_message/fields/
field71.rs

1use super::swift_utils::{
2    format_swift_amount_for_currency, parse_amount_with_currency, parse_currency_non_commodity,
3    parse_exact_length, parse_uppercase,
4};
5use crate::errors::ParseError;
6use crate::traits::SwiftField;
7use serde::{Deserialize, Serialize};
8
9/// **Field 71A: Details of Charges**
10///
11/// Charge allocation code specifying which party bears transaction costs.
12///
13/// **Format:** `3!a`
14/// **Values:** `BEN` (beneficiary pays), `OUR` (sender pays), `SHA` (shared)
15///
16/// **Example:**
17/// ```text
18/// :71A:SHA
19/// ```
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct Field71A {
22    /// Charge code: BEN, OUR, or SHA
23    pub code: String,
24}
25
26impl SwiftField for Field71A {
27    fn parse(input: &str) -> crate::Result<Self>
28    where
29        Self: Sized,
30    {
31        // Must be exactly 3 characters
32        let code = parse_exact_length(input, 3, "Field 71A code")?;
33
34        // Must be uppercase alphabetic
35        parse_uppercase(&code, "Field 71A code")?;
36
37        // Validate against known codes
38        const VALID_CODES: &[&str] = &["BEN", "OUR", "SHA"];
39        if !VALID_CODES.contains(&code.as_str()) {
40            return Err(ParseError::InvalidFormat {
41                message: format!(
42                    "Field 71A code must be one of {:?}, found {}",
43                    VALID_CODES, code
44                ),
45            });
46        }
47
48        Ok(Field71A { code })
49    }
50
51    fn to_swift_string(&self) -> String {
52        format!(":71A:{}", self.code)
53    }
54}
55
56/// **Field 71F: Sender's Charges**
57///
58/// Currency and amount of charges borne by sender.
59///
60/// **Format:** `3!a15d` (currency + amount)
61///
62/// **Example:**
63/// ```text
64/// :71F:USD25,00
65/// ```
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub struct Field71F {
68    /// ISO 4217 currency code
69    pub currency: String,
70    /// Charge amount
71    pub amount: f64,
72}
73
74impl SwiftField for Field71F {
75    fn parse(input: &str) -> crate::Result<Self>
76    where
77        Self: Sized,
78    {
79        if input.len() < 4 {
80            return Err(ParseError::InvalidFormat {
81                message: format!(
82                    "Field 71F must be at least 4 characters, found {}",
83                    input.len()
84                ),
85            });
86        }
87
88        // Parse currency (first 3 characters) - T52 + C08 validation
89        let currency = parse_currency_non_commodity(&input[0..3])?;
90
91        // Parse amount (remaining characters, max 15 digits) - T40/T43 + C03 validation
92        let amount_str = &input[3..];
93        if amount_str.is_empty() {
94            return Err(ParseError::InvalidFormat {
95                message: "Field 71F amount is required".to_string(),
96            });
97        }
98
99        let amount = parse_amount_with_currency(amount_str, &currency)?;
100
101        Ok(Field71F { currency, amount })
102    }
103
104    fn to_swift_string(&self) -> String {
105        format!(
106            ":71F:{}{}",
107            self.currency,
108            format_swift_amount_for_currency(self.amount, &self.currency)
109        )
110    }
111}
112
113/// **Field 71G: Receiver's Charges**
114///
115/// Currency and amount of charges borne by receiver.
116///
117/// **Format:** `3!a15d` (currency + amount)
118///
119/// **Example:**
120/// ```text
121/// :71G:EUR10,50
122/// ```
123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124pub struct Field71G {
125    /// ISO 4217 currency code
126    pub currency: String,
127    /// Charge amount
128    pub amount: f64,
129}
130
131impl SwiftField for Field71G {
132    fn parse(input: &str) -> crate::Result<Self>
133    where
134        Self: Sized,
135    {
136        if input.len() < 4 {
137            return Err(ParseError::InvalidFormat {
138                message: format!(
139                    "Field 71G must be at least 4 characters, found {}",
140                    input.len()
141                ),
142            });
143        }
144
145        // Parse currency (first 3 characters) - T52 + C08 validation
146        let currency = parse_currency_non_commodity(&input[0..3])?;
147
148        // Parse amount (remaining characters, max 15 digits) - T40/T43 + C03 validation
149        let amount_str = &input[3..];
150        if amount_str.is_empty() {
151            return Err(ParseError::InvalidFormat {
152                message: "Field 71G amount is required".to_string(),
153            });
154        }
155
156        let amount = parse_amount_with_currency(amount_str, &currency)?;
157
158        Ok(Field71G { currency, amount })
159    }
160
161    fn to_swift_string(&self) -> String {
162        format!(
163            ":71G:{}{}",
164            self.currency,
165            format_swift_amount_for_currency(self.amount, &self.currency)
166        )
167    }
168}
169
170/// **Field 71B: Details of Charges**
171///
172/// Detailed charge breakdown including interest and adjustments.
173/// Used in MT n90 messages (MT190, MT290, etc.).
174///
175/// **Format:** `6*35x` (max 6 lines, 35 chars each)
176///
177/// **Example:**
178/// ```text
179/// :71B:COMMISSION USD 25.00
180/// INTEREST USD 10.50
181/// ```
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct Field71B {
184    /// Charge details (max 6 lines, 35 chars each)
185    pub details: Vec<String>,
186}
187
188impl SwiftField for Field71B {
189    fn parse(input: &str) -> crate::Result<Self>
190    where
191        Self: Sized,
192    {
193        use super::field_utils::parse_multiline_text;
194
195        // Parse as multiline text (up to 6 lines, 35 chars each)
196        let details = parse_multiline_text(input, 6, 35)?;
197
198        if details.is_empty() {
199            return Err(ParseError::InvalidFormat {
200                message: "Field 71B must have at least one line of details".to_string(),
201            });
202        }
203
204        Ok(Field71B { details })
205    }
206
207    fn to_swift_string(&self) -> String {
208        format!(":71B:{}", self.details.join("\n"))
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_field71a() {
218        // Test valid codes
219        let field = Field71A::parse("BEN").unwrap();
220        assert_eq!(field.code, "BEN");
221
222        let field = Field71A::parse("OUR").unwrap();
223        assert_eq!(field.code, "OUR");
224
225        let field = Field71A::parse("SHA").unwrap();
226        assert_eq!(field.code, "SHA");
227
228        // Test invalid length
229        assert!(Field71A::parse("BE").is_err());
230        assert!(Field71A::parse("BENE").is_err());
231
232        // Test invalid code
233        assert!(Field71A::parse("XXX").is_err());
234
235        // Test lowercase (should fail)
236        assert!(Field71A::parse("ben").is_err());
237
238        // Test to_swift_string
239        let field = Field71A {
240            code: "SHA".to_string(),
241        };
242        assert_eq!(field.to_swift_string(), ":71A:SHA");
243    }
244
245    #[test]
246    fn test_field71f() {
247        // Test basic parsing
248        let field = Field71F::parse("USD100.50").unwrap();
249        assert_eq!(field.currency, "USD");
250        assert_eq!(field.amount, 100.50);
251
252        // Test with comma (European format)
253        let field = Field71F::parse("EUR1234,56").unwrap();
254        assert_eq!(field.currency, "EUR");
255        assert_eq!(field.amount, 1234.56);
256
257        // Test integer amount
258        let field = Field71F::parse("GBP500").unwrap();
259        assert_eq!(field.currency, "GBP");
260        assert_eq!(field.amount, 500.0);
261
262        // Test to_swift_string
263        let field = Field71F {
264            currency: "USD".to_string(),
265            amount: 250.75,
266        };
267        assert_eq!(field.to_swift_string(), ":71F:USD250,75");
268
269        // Test invalid currency
270        assert!(Field71F::parse("US100").is_err());
271        assert!(Field71F::parse("123100").is_err());
272
273        // Test missing amount
274        assert!(Field71F::parse("USD").is_err());
275    }
276
277    #[test]
278    fn test_field71b() {
279        // Test single line
280        let field = Field71B::parse("COMMISSION USD 25.00").unwrap();
281        assert_eq!(field.details.len(), 1);
282        assert_eq!(field.details[0], "COMMISSION USD 25.00");
283
284        // Test multiline
285        let input = "COMMISSION USD 25.00\nINTEREST USD 10.50\nSERVICE FEE USD 5.00";
286        let field = Field71B::parse(input).unwrap();
287        assert_eq!(field.details.len(), 3);
288        assert_eq!(field.details[0], "COMMISSION USD 25.00");
289        assert_eq!(field.details[1], "INTEREST USD 10.50");
290        assert_eq!(field.details[2], "SERVICE FEE USD 5.00");
291
292        // Test to_swift_string
293        assert_eq!(field.to_swift_string(), format!(":71B:{}", input));
294
295        // Test format spec
296    }
297
298    #[test]
299    fn test_field71g() {
300        // Test basic parsing
301        let field = Field71G::parse("CHF75.25").unwrap();
302        assert_eq!(field.currency, "CHF");
303        assert_eq!(field.amount, 75.25);
304
305        // Test large amount
306        let field = Field71G::parse("JPY1000000").unwrap();
307        assert_eq!(field.currency, "JPY");
308        assert_eq!(field.amount, 1000000.0);
309
310        // Test to_swift_string
311        let field = Field71G {
312            currency: "CAD".to_string(),
313            amount: 99.99,
314        };
315        assert_eq!(field.to_swift_string(), ":71G:CAD99,99");
316
317        // Test format spec
318    }
319}