swift_mt_message/messages/
mt202cov.rs

1//! MT202COV: General Financial Institution Transfer (Cover)
2
3use chrono::NaiveDate;
4use serde::{Deserialize, Serialize};
5
6use crate::common::{Amount, Field, MessageBlock, SwiftDate, tags};
7use crate::error::{MTError, Result};
8use crate::messages::{
9    MTMessageType, extract_text_block, find_field, find_fields, get_optional_field_value,
10    get_required_field_value,
11};
12
13/// MT202COV: General Financial Institution Transfer (Cover)
14/// This message is used in correspondent banking to provide cover for an underlying customer credit transfer
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct MT202COV {
17    /// All fields from the text block
18    fields: Vec<Field>,
19}
20
21impl MT202COV {
22    /// Get transaction reference number (Field 20)
23    pub fn transaction_reference(&self) -> Result<String> {
24        get_required_field_value(&self.fields, tags::SENDER_REFERENCE)
25    }
26
27    /// Get related reference (Field 21) - Related reference
28    pub fn related_reference(&self) -> Option<String> {
29        get_optional_field_value(&self.fields, "21")
30    }
31
32    /// Get value date, currency and amount (Field 32A)
33    pub fn value_date_currency_amount(&self) -> Result<String> {
34        get_required_field_value(&self.fields, tags::VALUE_DATE_CURRENCY_AMOUNT)
35    }
36
37    /// Get parsed amount from field 32A
38    pub fn amount(&self) -> Result<Amount> {
39        let field_32a = get_required_field_value(&self.fields, tags::VALUE_DATE_CURRENCY_AMOUNT)?;
40
41        // Format: YYMMDDCCCNNNNN,NN (date + currency + amount)
42        if field_32a.len() < 9 {
43            return Err(MTError::InvalidFieldFormat {
44                field: "32A".to_string(),
45                message: "Field 32A too short".to_string(),
46            });
47        }
48
49        // Skip the date part (first 6 characters) and parse the currency+amount
50        let currency_amount = &field_32a[6..];
51        Amount::parse(currency_amount)
52    }
53
54    /// Get currency from field 32A
55    pub fn currency(&self) -> Result<String> {
56        let amount = self.amount()?;
57        Ok(amount.currency)
58    }
59
60    /// Get value date from field 32A
61    pub fn value_date(&self) -> Result<NaiveDate> {
62        let field_32a = get_required_field_value(&self.fields, tags::VALUE_DATE_CURRENCY_AMOUNT)?;
63
64        if field_32a.len() < 6 {
65            return Err(MTError::InvalidFieldFormat {
66                field: "32A".to_string(),
67                message: "Field 32A too short for date".to_string(),
68            });
69        }
70
71        let date_str = &field_32a[0..6];
72        let swift_date = SwiftDate::parse_yymmdd(date_str)?;
73        Ok(swift_date.date)
74    }
75
76    /// Get ordering customer (Field 50A/50F/50K) - Customer ordering the transfer
77    pub fn ordering_customer(&self) -> Result<String> {
78        // Try different variants in order of preference
79        if let Some(customer) = get_optional_field_value(&self.fields, tags::ORDERING_CUSTOMER) {
80            Ok(customer)
81        } else if let Some(customer) = get_optional_field_value(&self.fields, "50A") {
82            Ok(customer)
83        } else if let Some(customer) = get_optional_field_value(&self.fields, "50F") {
84            Ok(customer)
85        } else {
86            Err(MTError::missing_required_field("50K/50A/50F"))
87        }
88    }
89
90    /// Get ordering institution (Field 52A/52D) - Institution placing the order
91    pub fn ordering_institution(&self) -> Option<String> {
92        get_optional_field_value(&self.fields, tags::ORDERING_INSTITUTION)
93    }
94
95    /// Get ordering institution (Field 52D) - Alternative format
96    pub fn ordering_institution_d(&self) -> Option<String> {
97        get_optional_field_value(&self.fields, "52D")
98    }
99
100    /// Get sender's correspondent (Field 53A/53B/53D) - Sender's correspondent
101    pub fn senders_correspondent(&self) -> Option<String> {
102        get_optional_field_value(&self.fields, tags::SENDERS_CORRESPONDENT)
103    }
104
105    /// Get sender's correspondent (Field 53B) - Alternative format
106    pub fn senders_correspondent_b(&self) -> Option<String> {
107        get_optional_field_value(&self.fields, "53B")
108    }
109
110    /// Get sender's correspondent (Field 53D) - Alternative format
111    pub fn senders_correspondent_d(&self) -> Option<String> {
112        get_optional_field_value(&self.fields, "53D")
113    }
114
115    /// Get receiver's correspondent (Field 54A/54B/54D) - Receiver's correspondent
116    pub fn receivers_correspondent(&self) -> Option<String> {
117        get_optional_field_value(&self.fields, tags::RECEIVERS_CORRESPONDENT)
118    }
119
120    /// Get receiver's correspondent (Field 54B) - Alternative format
121    pub fn receivers_correspondent_b(&self) -> Option<String> {
122        get_optional_field_value(&self.fields, "54B")
123    }
124
125    /// Get receiver's correspondent (Field 54D) - Alternative format
126    pub fn receivers_correspondent_d(&self) -> Option<String> {
127        get_optional_field_value(&self.fields, "54D")
128    }
129
130    /// Get third reimbursement institution (Field 55A/55D) - Third reimbursement institution
131    pub fn third_reimbursement_institution(&self) -> Option<String> {
132        get_optional_field_value(&self.fields, tags::THIRD_REIMBURSEMENT_INSTITUTION)
133    }
134
135    /// Get third reimbursement institution (Field 55D) - Alternative format
136    pub fn third_reimbursement_institution_d(&self) -> Option<String> {
137        get_optional_field_value(&self.fields, "55D")
138    }
139
140    /// Get intermediary institution (Field 56A/56C/56D) - Intermediary institution
141    pub fn intermediary_institution(&self) -> Option<String> {
142        get_optional_field_value(&self.fields, tags::INTERMEDIARY_INSTITUTION)
143    }
144
145    /// Get intermediary institution (Field 56C) - Alternative format
146    pub fn intermediary_institution_c(&self) -> Option<String> {
147        get_optional_field_value(&self.fields, "56C")
148    }
149
150    /// Get intermediary institution (Field 56D) - Alternative format
151    pub fn intermediary_institution_d(&self) -> Option<String> {
152        get_optional_field_value(&self.fields, "56D")
153    }
154
155    /// Get account with institution (Field 57A/57B/57C/57D) - Account with institution
156    pub fn account_with_institution(&self) -> Option<String> {
157        get_optional_field_value(&self.fields, tags::ACCOUNT_WITH_INSTITUTION)
158    }
159
160    /// Get account with institution (Field 57B) - Alternative format
161    pub fn account_with_institution_b(&self) -> Option<String> {
162        get_optional_field_value(&self.fields, "57B")
163    }
164
165    /// Get account with institution (Field 57C) - Alternative format
166    pub fn account_with_institution_c(&self) -> Option<String> {
167        get_optional_field_value(&self.fields, "57C")
168    }
169
170    /// Get account with institution (Field 57D) - Alternative format
171    pub fn account_with_institution_d(&self) -> Option<String> {
172        get_optional_field_value(&self.fields, "57D")
173    }
174
175    /// Get beneficiary institution (Field 58A/58D) - Beneficiary institution
176    pub fn beneficiary_institution(&self) -> Result<String> {
177        if let Some(beneficiary) = get_optional_field_value(&self.fields, "58A") {
178            Ok(beneficiary)
179        } else if let Some(beneficiary) = get_optional_field_value(&self.fields, "58D") {
180            Ok(beneficiary)
181        } else {
182            Err(MTError::missing_required_field("58A or 58D"))
183        }
184    }
185
186    /// Get beneficiary institution (Field 58D) - Alternative format
187    pub fn beneficiary_institution_d(&self) -> Option<String> {
188        get_optional_field_value(&self.fields, "58D")
189    }
190
191    /// Get beneficiary customer (Field 59/59A/59F) - Customer receiving the transfer
192    pub fn beneficiary_customer(&self) -> Result<String> {
193        if let Some(customer) = get_optional_field_value(&self.fields, tags::BENEFICIARY_CUSTOMER) {
194            Ok(customer)
195        } else if let Some(customer) = get_optional_field_value(&self.fields, "59A") {
196            Ok(customer)
197        } else if let Some(customer) = get_optional_field_value(&self.fields, "59F") {
198            Ok(customer)
199        } else {
200            Err(MTError::missing_required_field("59/59A/59F"))
201        }
202    }
203
204    /// Get remittance information (Field 70) - Details of payment
205    pub fn remittance_information(&self) -> Option<String> {
206        get_optional_field_value(&self.fields, tags::REMITTANCE_INFORMATION)
207    }
208
209    /// Get details of charges (Field 71A) - Details of charges
210    pub fn details_of_charges(&self) -> Option<String> {
211        get_optional_field_value(&self.fields, tags::DETAILS_OF_CHARGES)
212    }
213
214    /// Get sender's charges (Field 71F) - Sender's charges
215    pub fn senders_charges(&self) -> Option<String> {
216        get_optional_field_value(&self.fields, tags::SENDERS_CHARGES)
217    }
218
219    /// Get receiver's charges (Field 71G) - Receiver's charges
220    pub fn receivers_charges(&self) -> Option<String> {
221        get_optional_field_value(&self.fields, tags::RECEIVERS_CHARGES)
222    }
223
224    /// Get regulatory reporting (Field 77B) - Regulatory reporting
225    pub fn regulatory_reporting(&self) -> Option<String> {
226        get_optional_field_value(&self.fields, "77B")
227    }
228
229    /// Get instructions to paying/receiving/cover bank (Field 72) - Instructions
230    pub fn instructions(&self) -> Option<String> {
231        get_optional_field_value(&self.fields, "72")
232    }
233
234    /// Get all instructions (Field 72) - can have multiple
235    pub fn all_instructions(&self) -> Vec<String> {
236        find_fields(&self.fields, "72")
237            .into_iter()
238            .map(|field| field.value().to_string())
239            .collect()
240    }
241
242    /// Get underlying customer credit transfer reference (Field 21) - Reference to underlying MT103
243    pub fn underlying_customer_credit_transfer(&self) -> Option<String> {
244        get_optional_field_value(&self.fields, "21")
245    }
246
247    /// Check if this is a cover message for an underlying customer transfer
248    pub fn is_cover_message(&self) -> bool {
249        // MT202COV is always a cover message if it has customer fields (50K and 59)
250        self.get_field("50K").is_some() || self.get_field("59").is_some()
251    }
252}
253
254impl MTMessageType for MT202COV {
255    fn from_blocks(blocks: Vec<MessageBlock>) -> Result<Self> {
256        let fields = extract_text_block(&blocks)?;
257
258        // Validate required fields are present
259        let required_fields = [
260            tags::SENDER_REFERENCE,           // Field 20
261            tags::VALUE_DATE_CURRENCY_AMOUNT, // Field 32A
262        ];
263
264        // Check for required fields
265        for &field_tag in &required_fields {
266            if !fields.iter().any(|f| f.tag.as_str() == field_tag) {
267                return Err(MTError::missing_required_field(field_tag));
268            }
269        }
270
271        // Check for either 58A or 58D (beneficiary institution)
272        if !fields
273            .iter()
274            .any(|f| f.tag.as_str() == "58A" || f.tag.as_str() == "58D")
275        {
276            return Err(MTError::missing_required_field("58A or 58D"));
277        }
278
279        // For COV messages, we also need ordering customer and beneficiary customer
280        if !fields
281            .iter()
282            .any(|f| f.tag.as_str() == "50K" || f.tag.as_str() == "50A" || f.tag.as_str() == "50F")
283        {
284            return Err(MTError::missing_required_field("50K/50A/50F"));
285        }
286
287        if !fields
288            .iter()
289            .any(|f| f.tag.as_str() == "59" || f.tag.as_str() == "59A" || f.tag.as_str() == "59F")
290        {
291            return Err(MTError::missing_required_field("59/59A/59F"));
292        }
293
294        Ok(MT202COV { fields })
295    }
296
297    fn get_field(&self, tag: &str) -> Option<&Field> {
298        find_field(&self.fields, tag)
299    }
300
301    fn get_fields(&self, tag: &str) -> Vec<&Field> {
302        find_fields(&self.fields, tag)
303    }
304
305    fn get_all_fields(&self) -> Vec<&Field> {
306        self.fields.iter().collect()
307    }
308
309    fn text_fields(&self) -> &[Field] {
310        &self.fields
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use crate::common::Field;
318    use chrono::Datelike;
319
320    fn create_test_mt202cov() -> MT202COV {
321        let fields = vec![
322            Field::new("20", "COV123456789"),
323            Field::new("21", "NOTPROVIDED"),
324            Field::new("32A", "210315USD10000000,00"),
325            Field::new("50K", "ORDERING CUSTOMER\nCOMPANY ABC\nNEW YORK NY"),
326            Field::new("52A", "ORDBANK33XXX"),
327            Field::new("53A", "SNDCOR33XXX"),
328            Field::new("54A", "RCVCOR33XXX"),
329            Field::new("57A", "ACWITH33XXX"),
330            Field::new("58A", "BENBANK44XXX"),
331            Field::new("59", "BENEFICIARY CUSTOMER\nCOMPANY XYZ\nLONDON GB"),
332            Field::new("70", "INVOICE PAYMENT INV-2021-001"),
333            Field::new("71A", "OUR"),
334            Field::new("72", "COVER FOR UNDERLYING MT103"),
335        ];
336        MT202COV { fields }
337    }
338
339    #[test]
340    fn test_transaction_reference() {
341        let mt202cov = create_test_mt202cov();
342        assert_eq!(mt202cov.transaction_reference().unwrap(), "COV123456789");
343    }
344
345    #[test]
346    fn test_related_reference() {
347        let mt202cov = create_test_mt202cov();
348        assert_eq!(mt202cov.related_reference().unwrap(), "NOTPROVIDED");
349    }
350
351    #[test]
352    fn test_amount_parsing() {
353        let mt202cov = create_test_mt202cov();
354        let amount = mt202cov.amount().unwrap();
355        assert_eq!(amount.value, 10000000.0);
356        assert_eq!(amount.currency, "USD");
357    }
358
359    #[test]
360    fn test_currency() {
361        let mt202cov = create_test_mt202cov();
362        assert_eq!(mt202cov.currency().unwrap(), "USD");
363    }
364
365    #[test]
366    fn test_value_date() {
367        let mt202cov = create_test_mt202cov();
368        let date = mt202cov.value_date().unwrap();
369        assert_eq!(date.year(), 2021);
370        assert_eq!(date.month(), 3);
371        assert_eq!(date.day(), 15);
372    }
373
374    #[test]
375    fn test_ordering_customer() {
376        let mt202cov = create_test_mt202cov();
377        assert_eq!(
378            mt202cov.ordering_customer().unwrap(),
379            "ORDERING CUSTOMER\nCOMPANY ABC\nNEW YORK NY"
380        );
381    }
382
383    #[test]
384    fn test_beneficiary_customer() {
385        let mt202cov = create_test_mt202cov();
386        assert_eq!(
387            mt202cov.beneficiary_customer().unwrap(),
388            "BENEFICIARY CUSTOMER\nCOMPANY XYZ\nLONDON GB"
389        );
390    }
391
392    #[test]
393    fn test_beneficiary_institution() {
394        let mt202cov = create_test_mt202cov();
395        assert_eq!(mt202cov.beneficiary_institution().unwrap(), "BENBANK44XXX");
396    }
397
398    #[test]
399    fn test_ordering_institution() {
400        let mt202cov = create_test_mt202cov();
401        assert_eq!(mt202cov.ordering_institution().unwrap(), "ORDBANK33XXX");
402    }
403
404    #[test]
405    fn test_remittance_information() {
406        let mt202cov = create_test_mt202cov();
407        assert_eq!(
408            mt202cov.remittance_information().unwrap(),
409            "INVOICE PAYMENT INV-2021-001"
410        );
411    }
412
413    #[test]
414    fn test_details_of_charges() {
415        let mt202cov = create_test_mt202cov();
416        assert_eq!(mt202cov.details_of_charges().unwrap(), "OUR");
417    }
418
419    #[test]
420    fn test_instructions() {
421        let mt202cov = create_test_mt202cov();
422        assert_eq!(
423            mt202cov.instructions().unwrap(),
424            "COVER FOR UNDERLYING MT103"
425        );
426    }
427
428    #[test]
429    fn test_is_cover_message() {
430        let mt202cov = create_test_mt202cov();
431        assert!(mt202cov.is_cover_message());
432    }
433
434    #[test]
435    fn test_get_field() {
436        let mt202cov = create_test_mt202cov();
437        let field_20 = mt202cov.get_field("20").unwrap();
438        assert_eq!(field_20.value(), "COV123456789");
439    }
440
441    #[test]
442    fn test_get_all_fields() {
443        let mt202cov = create_test_mt202cov();
444        let all_fields = mt202cov.get_all_fields();
445        assert_eq!(all_fields.len(), 13);
446    }
447}