swift_mt_message/fields/
field32a.rs

1use crate::{SwiftField, ValidationResult};
2use chrono::{Datelike, NaiveDate};
3use serde::{Deserialize, Serialize};
4
5/// # Field 32A: Value Date, Currency Code, Amount
6///
7/// ## Overview
8/// Field 32A is a composite field that contains three critical components of a financial
9/// transaction: the value date (when the transaction becomes effective), the currency code
10/// (ISO 4217 standard), and the transaction amount. This field is fundamental to SWIFT MT
11/// messages and serves as the primary transaction specification in most payment messages.
12///
13/// ## Format Specification
14/// **Format**: `6!n3!a15d`
15/// - **6!n**: Value date in YYMMDD format (6 numeric characters)
16/// - **3!a**: Currency code (3 alphabetic characters, ISO 4217)
17/// - **15d**: Amount with up to 15 digits including decimal places
18///
19/// ### Component Details
20/// 1. **Value Date (YYMMDD)**:
21///    - Year: 2-digit year (YY) - assumes 20YY for years 00-99
22///    - Month: 2-digit month (01-12)
23///    - Day: 2-digit day (01-31, depending on month)
24///    - Must be a valid calendar date
25///
26/// 2. **Currency Code (3!a)**:
27///    - ISO 4217 standard currency codes
28///    - Exactly 3 alphabetic characters
29///    - Case-insensitive input, stored as uppercase
30///    - Examples: USD, EUR, GBP, JPY, CHF
31///
32/// 3. **Amount (15d)**:
33///    - Up to 15 digits including decimal places
34///    - Decimal separator: comma (,) in SWIFT format
35///    - No thousands separators
36///    - Must be positive (> 0)
37///    - Precision: typically 2 decimal places for most currencies
38///
39/// ## Usage Context
40/// Field 32A appears in numerous SWIFT MT message types:
41///
42/// ### Primary Usage
43/// - **MT103**: Single Customer Credit Transfer - transaction amount and value date
44/// - **MT202**: General Financial Institution Transfer - settlement amount
45/// - **MT202COV**: Cover for customer credit transfer - cover amount
46/// - **MT205**: Financial Institution Transfer for its Own Account
47///
48/// ### Secondary Usage
49/// - **MT400**: Advice of Payment - payment amount
50/// - **MT410**: Acknowledgement - acknowledged amount
51/// - **MT420**: Tracer - traced amount
52/// - **MT900**: Confirmation of Debit - debited amount
53/// - **MT910**: Confirmation of Credit - credited amount
54///
55/// ## Business Applications
56/// - **Payment processing**: Core transaction specification
57/// - **Settlement**: Value dating for settlement systems
58/// - **Accounting**: Transaction recording and reconciliation
59/// - **Compliance**: AML/KYC amount thresholds
60/// - **Risk management**: Exposure calculation and limits
61/// - **Reporting**: Regulatory and management reporting
62/// - **FX processing**: Currency conversion and hedging
63/// - **Liquidity management**: Cash flow planning
64///
65/// ## Value Dating Rules
66/// Value dates must follow specific business rules:
67///
68/// ### Standard Rules
69/// - **Same day value**: Value date = current business date
70/// - **Next day value**: Value date = next business date
71/// - **Forward value**: Value date > current date (up to 1 year typically)
72/// - **Back value**: Value date < current date (limited, usually same week)
73///
74/// ### Currency-Specific Rules
75/// - **USD**: T+0 or T+1 settlement
76/// - **EUR**: T+1 settlement (TARGET2)
77/// - **GBP**: T+0 settlement (CHAPS)
78/// - **JPY**: T+0 or T+1 settlement
79/// - **Exotic currencies**: May require T+2 or longer
80///
81/// ### Holiday Considerations
82/// - Value dates must be valid business days
83/// - Consider both sending and receiving country holidays
84/// - Weekend adjustments follow market conventions
85/// - Holiday calendars vary by currency and market
86///
87/// ## Amount Formatting Rules
88/// 1. **Decimal separator**: Always comma (,) in SWIFT format
89/// 2. **No thousands separators**: 1234567,89 not 1,234,567.89
90/// 3. **Leading zeros**: Not required (123,45 not 0000123,45)
91/// 4. **Trailing zeros**: Required for decimal places (100,00 not 100)
92/// 5. **Maximum precision**: Varies by currency (typically 2 decimal places)
93///
94/// ## Currency Code Validation
95/// - Must be valid ISO 4217 currency code
96/// - Active currencies only (not historical or test codes)
97/// - Some restricted currencies may require special handling
98/// - Cryptocurrency codes follow ISO 4217 digital currency standards
99///
100/// ## Network Validated Rules (SWIFT Standards)
101/// - Value date must be valid calendar date (Error: T50)
102/// - Currency code must be valid ISO 4217 (Error: T52)
103/// - Amount must be positive and properly formatted (Error: T40)
104/// - Value date should be reasonable business date (Warning: recommended)
105/// - Currency should be actively traded (Warning: recommended)
106///
107///
108/// ## Examples
109/// ```text
110/// :32A:240315USD1234567,89
111/// └─── Value: March 15, 2024, USD 1,234,567.89
112///
113/// :32A:240401EUR500000,00
114/// └─── Value: April 1, 2024, EUR 500,000.00
115///
116/// :32A:240228GBP75000,50
117/// └─── Value: February 28, 2024, GBP 75,000.50
118/// ```
119///
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
122pub struct Field32A {
123    /// Value date when the transaction becomes effective
124    ///
125    /// Specifies the date on which the transaction amount should be
126    /// credited or debited to the beneficiary's account. Must be a
127    /// valid calendar date and typically a business day.
128    ///
129    /// **Format**: YYMMDD (6 numeric characters)
130    /// **Range**: Valid calendar dates
131    /// **Business rules**: Should be valid business day for currency
132    ///
133    /// # Examples
134    /// - March 15, 2024 → `NaiveDate::from_ymd_opt(2024, 3, 15)`
135    /// - December 31, 2023 → `NaiveDate::from_ymd_opt(2023, 12, 31)`
136    pub value_date: NaiveDate,
137
138    /// ISO 4217 currency code (3 alphabetic characters)
139    ///
140    /// Specifies the currency of the transaction amount using the
141    /// international standard ISO 4217 currency codes.
142    ///
143    /// **Format**: Exactly 3 uppercase alphabetic characters
144    /// **Standard**: ISO 4217 (International Organization for Standardization)
145    /// **Case handling**: Automatically converted to uppercase
146    ///
147    /// # Common Currencies
148    /// - `"USD"` - United States Dollar
149    /// - `"EUR"` - Euro
150    /// - `"GBP"` - British Pound Sterling
151    /// - `"JPY"` - Japanese Yen
152    /// - `"CHF"` - Swiss Franc
153    /// - `"CAD"` - Canadian Dollar
154    /// - `"AUD"` - Australian Dollar
155    ///
156    /// # Examples
157    /// ```rust
158    /// # use swift_mt_message::fields::Field32A;
159    /// # use chrono::NaiveDate;
160    /// let field = Field32A::new(
161    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
162    ///     "USD".to_string(),
163    ///     1000.00
164    /// );
165    /// assert_eq!(field.currency, "USD");
166    /// ```
167    pub currency: String,
168
169    /// Transaction amount as decimal value
170    ///
171    /// The monetary amount of the transaction expressed as a floating-point
172    /// number. Must be positive and should respect the precision rules
173    /// for the specified currency.
174    ///
175    /// **Range**: Must be positive (> 0.0)
176    /// **Precision**: Typically 2 decimal places for most currencies
177    /// **Special cases**: JPY typically has 0 decimal places
178    ///
179    /// # Examples
180    /// - `1234567.89` - One million, two hundred thirty-four thousand, five hundred sixty-seven and 89 cents
181    /// - `100.00` - One hundred units
182    /// - `0.01` - One cent (minimum for most currencies)
183    pub amount: f64,
184
185    /// Raw amount string as received (preserves original formatting)
186    ///
187    /// Maintains the original string representation of the amount as
188    /// received in the SWIFT message, preserving the exact formatting
189    /// including decimal separator and precision.
190    ///
191    /// **Format**: SWIFT standard with comma as decimal separator
192    /// **Preservation**: Maintains original precision and formatting
193    /// **Usage**: For exact reproduction of original message format
194    ///
195    /// # Examples
196    /// - `"1234567,89"` - SWIFT format with comma separator
197    /// - `"100,00"` - Two decimal places preserved
198    /// - `"0,01"` - Leading zero preserved
199    pub raw_amount: String,
200}
201
202impl SwiftField for Field32A {
203    fn parse(value: &str) -> Result<Self, crate::ParseError> {
204        let content = if let Some(stripped) = value.strip_prefix(":32A:") {
205            stripped // Remove ":32A:" prefix
206        } else if let Some(stripped) = value.strip_prefix("32A:") {
207            stripped // Remove "32A:" prefix
208        } else {
209            value
210        };
211
212        let content = content.trim();
213
214        if content.len() < 9 {
215            return Err(crate::ParseError::InvalidFieldFormat {
216                field_tag: "32A".to_string(),
217                message: "Field too short (minimum 9 characters: YYMMDDCCCAMOUNT)".to_string(),
218            });
219        }
220
221        // Parse date (YYMMDD format)
222        let date_str = &content[0..6];
223        let year = format!("20{}", &date_str[0..2]);
224        let month = &date_str[2..4];
225        let day = &date_str[4..6];
226        let full_date_str = format!("{}-{}-{}", year, month, day);
227
228        let value_date = NaiveDate::parse_from_str(&full_date_str, "%Y-%m-%d").map_err(|_| {
229            crate::ParseError::InvalidFieldFormat {
230                field_tag: "32A".to_string(),
231                message: "Invalid date format".to_string(),
232            }
233        })?;
234
235        // Parse currency (3 characters)
236        let currency = content[6..9].to_string().to_uppercase();
237
238        // Parse amount (remaining characters)
239        let raw_amount = content[9..].to_string();
240        let amount = raw_amount.replace(',', ".").parse::<f64>().map_err(|_| {
241            crate::ParseError::InvalidFieldFormat {
242                field_tag: "32A".to_string(),
243                message: "Invalid amount format".to_string(),
244            }
245        })?;
246
247        Ok(Self {
248            value_date,
249            currency,
250            amount,
251            raw_amount,
252        })
253    }
254
255    fn to_swift_string(&self) -> String {
256        // Format date as YYMMDD
257        let date_str = format!(
258            "{:02}{:02}{:02}",
259            self.value_date.year() % 100,
260            self.value_date.month(),
261            self.value_date.day()
262        );
263
264        format!(":32A:{}{}{}", date_str, self.currency, self.raw_amount)
265    }
266
267    fn validate(&self) -> ValidationResult {
268        let mut errors = Vec::new();
269
270        // Validate currency length
271        if self.currency.len() != 3 {
272            errors.push(crate::ValidationError::LengthValidation {
273                field_tag: "32A".to_string(),
274                expected: "3 characters".to_string(),
275                actual: self.currency.len(),
276            });
277        }
278
279        // Validate currency contains only letters
280        if !self.currency.chars().all(|c| c.is_alphabetic()) {
281            errors.push(crate::ValidationError::ValueValidation {
282                field_tag: "32A".to_string(),
283                message: "Currency must contain only alphabetic characters".to_string(),
284            });
285        }
286
287        // Validate amount is positive
288        if self.amount <= 0.0 {
289            errors.push(crate::ValidationError::ValueValidation {
290                field_tag: "32A".to_string(),
291                message: "Amount must be positive".to_string(),
292            });
293        }
294
295        ValidationResult {
296            is_valid: errors.is_empty(),
297            errors,
298            warnings: Vec::new(),
299        }
300    }
301
302    fn format_spec() -> &'static str {
303        "6!n3!a15d"
304    }
305}
306
307impl Field32A {
308    /// Create a new Field32A
309    ///
310    /// Creates a new Field32A instance with the specified value date,
311    /// currency, and amount. The amount is automatically formatted
312    /// according to SWIFT standards.
313    ///
314    /// # Arguments
315    /// * `value_date` - The value date for the transaction
316    /// * `currency` - ISO 4217 currency code (will be converted to uppercase)
317    /// * `amount` - Transaction amount (must be positive)
318    ///
319    /// # Returns
320    /// A new Field32A instance
321    ///
322    /// # Example
323    /// ```rust
324    /// # use swift_mt_message::fields::Field32A;
325    /// # use chrono::NaiveDate;
326    /// let field = Field32A::new(
327    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
328    ///     "USD".to_string(),
329    ///     1234.56
330    /// );
331    /// assert_eq!(field.amount, 1234.56);
332    /// assert_eq!(field.currency, "USD");
333    /// ```
334    pub fn new(value_date: NaiveDate, currency: String, amount: f64) -> Self {
335        // Format amount with comma as decimal separator (SWIFT standard)
336        let raw_amount = format!("{:.2}", amount).replace('.', ",");
337        Self {
338            value_date,
339            currency: currency.to_uppercase(),
340            amount,
341            raw_amount,
342        }
343    }
344
345    /// Create from raw values
346    ///
347    /// Creates a Field32A instance from raw string amount, preserving
348    /// the original formatting while parsing the numeric value.
349    ///
350    /// # Arguments
351    /// * `value_date` - The value date for the transaction
352    /// * `currency` - ISO 4217 currency code
353    /// * `raw_amount` - Amount string in SWIFT format
354    ///
355    /// # Returns
356    /// Result containing the Field32A instance or parse error
357    ///
358    /// # Example
359    /// ```rust
360    /// # use swift_mt_message::fields::Field32A;
361    /// # use chrono::NaiveDate;
362    /// let field = Field32A::from_raw(
363    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
364    ///     "EUR".to_string(),
365    ///     "1000,50".to_string()
366    /// ).unwrap();
367    /// assert_eq!(field.amount, 1000.50);
368    /// assert_eq!(field.raw_amount, "1000,50");
369    /// ```
370    pub fn from_raw(
371        value_date: NaiveDate,
372        currency: String,
373        raw_amount: String,
374    ) -> Result<Self, std::num::ParseFloatError> {
375        let amount = raw_amount.replace(',', ".").parse::<f64>()?;
376        Ok(Self {
377            value_date,
378            currency: currency.to_uppercase(),
379            amount,
380            raw_amount,
381        })
382    }
383
384    /// Get the currency code
385    ///
386    /// Returns the ISO 4217 currency code for this transaction.
387    ///
388    /// # Returns
389    /// Currency code as string slice
390    ///
391    /// # Example
392    /// ```rust
393    /// # use swift_mt_message::fields::Field32A;
394    /// # use chrono::NaiveDate;
395    /// let field = Field32A::new(
396    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
397    ///     "GBP".to_string(),
398    ///     500.00
399    /// );
400    /// assert_eq!(field.currency_code(), "GBP");
401    /// ```
402    pub fn currency_code(&self) -> &str {
403        &self.currency
404    }
405
406    /// Get the amount as decimal
407    ///
408    /// Returns the transaction amount as a floating-point number.
409    ///
410    /// # Returns
411    /// Amount as f64
412    ///
413    /// # Example
414    /// ```rust
415    /// # use swift_mt_message::fields::Field32A;
416    /// # use chrono::NaiveDate;
417    /// let field = Field32A::new(
418    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
419    ///     "USD".to_string(),
420    ///     1234.56
421    /// );
422    /// assert_eq!(field.amount_decimal(), 1234.56);
423    /// ```
424    pub fn amount_decimal(&self) -> f64 {
425        self.amount
426    }
427
428    /// Format date as YYMMDD string
429    ///
430    /// Returns the value date formatted as a 6-character string
431    /// in YYMMDD format as used in SWIFT messages.
432    ///
433    /// # Returns
434    /// Date string in YYMMDD format
435    ///
436    /// # Example
437    /// ```rust
438    /// # use swift_mt_message::fields::Field32A;
439    /// # use chrono::NaiveDate;
440    /// let field = Field32A::new(
441    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
442    ///     "USD".to_string(),
443    ///     1000.00
444    /// );
445    /// assert_eq!(field.date_string(), "240315");
446    /// ```
447    pub fn date_string(&self) -> String {
448        format!(
449            "{:02}{:02}{:02}",
450            self.value_date.year() % 100,
451            self.value_date.month(),
452            self.value_date.day()
453        )
454    }
455
456    /// Check if the currency is a major currency
457    ///
458    /// Determines if the currency is one of the major internationally
459    /// traded currencies with high liquidity.
460    ///
461    /// # Returns
462    /// `true` if the currency is a major currency
463    ///
464    /// # Example
465    /// ```rust
466    /// # use swift_mt_message::fields::Field32A;
467    /// # use chrono::NaiveDate;
468    /// let usd_field = Field32A::new(
469    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
470    ///     "USD".to_string(),
471    ///     1000.00
472    /// );
473    /// assert!(usd_field.is_major_currency());
474    ///
475    /// let exotic_field = Field32A::new(
476    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
477    ///     "XYZ".to_string(),
478    ///     1000.00
479    /// );
480    /// assert!(!exotic_field.is_major_currency());
481    /// ```
482    pub fn is_major_currency(&self) -> bool {
483        matches!(
484            self.currency.as_str(),
485            "USD" | "EUR" | "GBP" | "JPY" | "CHF" | "CAD" | "AUD" | "NZD" | "SEK" | "NOK" | "DKK"
486        )
487    }
488
489    /// Check if the currency typically has decimal places
490    ///
491    /// Determines if the currency typically uses decimal places
492    /// in amount representation. Some currencies like JPY typically
493    /// don't use decimal places.
494    ///
495    /// # Returns
496    /// `true` if the currency typically uses decimal places
497    ///
498    /// # Example
499    /// ```rust
500    /// # use swift_mt_message::fields::Field32A;
501    /// # use chrono::NaiveDate;
502    /// let usd_field = Field32A::new(
503    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
504    ///     "USD".to_string(),
505    ///     1000.00
506    /// );
507    /// assert!(usd_field.has_decimal_places());
508    ///
509    /// let jpy_field = Field32A::new(
510    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
511    ///     "JPY".to_string(),
512    ///     1000.00
513    /// );
514    /// assert!(!jpy_field.has_decimal_places());
515    /// ```
516    pub fn has_decimal_places(&self) -> bool {
517        !matches!(
518            self.currency.as_str(),
519            "JPY" | "KRW" | "VND" | "IDR" | "CLP" | "PYG" | "UGX" | "RWF" | "GNF" | "MGA"
520        )
521    }
522
523    /// Get the typical decimal places for this currency
524    ///
525    /// Returns the number of decimal places typically used
526    /// for this currency in financial transactions.
527    ///
528    /// # Returns
529    /// Number of decimal places (0, 2, or 3)
530    ///
531    /// # Example
532    /// ```rust
533    /// # use swift_mt_message::fields::Field32A;
534    /// # use chrono::NaiveDate;
535    /// let usd_field = Field32A::new(
536    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
537    ///     "USD".to_string(),
538    ///     1000.00
539    /// );
540    /// assert_eq!(usd_field.decimal_places(), 2);
541    ///
542    /// let jpy_field = Field32A::new(
543    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
544    ///     "JPY".to_string(),
545    ///     1000.00
546    /// );
547    /// assert_eq!(jpy_field.decimal_places(), 0);
548    /// ```
549    pub fn decimal_places(&self) -> u8 {
550        match self.currency.as_str() {
551            // Currencies with no decimal places
552            "JPY" | "KRW" | "VND" | "IDR" | "CLP" | "PYG" | "UGX" | "RWF" | "GNF" | "MGA" => 0,
553            // Currencies with 3 decimal places
554            "BHD" | "IQD" | "JOD" | "KWD" | "LYD" | "OMR" | "TND" => 3,
555            // Most currencies use 2 decimal places
556            _ => 2,
557        }
558    }
559
560    /// Check if the amount is a high-value transaction
561    ///
562    /// Determines if the transaction amount exceeds typical
563    /// high-value thresholds that may require special handling.
564    ///
565    /// # Returns
566    /// `true` if this is considered a high-value transaction
567    ///
568    /// # Example
569    /// ```rust
570    /// # use swift_mt_message::fields::Field32A;
571    /// # use chrono::NaiveDate;
572    /// let high_value = Field32A::new(
573    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
574    ///     "USD".to_string(),
575    ///     1500000.00
576    /// );
577    /// assert!(high_value.is_high_value_transaction());
578    ///
579    /// let normal_value = Field32A::new(
580    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
581    ///     "USD".to_string(),
582    ///     50000.00
583    /// );
584    /// assert!(!normal_value.is_high_value_transaction());
585    /// ```
586    pub fn is_high_value_transaction(&self) -> bool {
587        // High-value thresholds vary by currency
588        let threshold = match self.currency.as_str() {
589            "USD" | "EUR" | "GBP" | "CHF" | "CAD" | "AUD" => 1_000_000.0,
590            "JPY" => 100_000_000.0,
591            "SEK" | "NOK" | "DKK" => 10_000_000.0,
592            _ => 1_000_000.0, // Default threshold
593        };
594
595        self.amount >= threshold
596    }
597
598    /// Get the settlement timing for this currency
599    ///
600    /// Returns the typical settlement timing for transactions
601    /// in this currency based on market conventions.
602    ///
603    /// # Returns
604    /// Settlement timing description
605    ///
606    /// # Example
607    /// ```rust
608    /// # use swift_mt_message::fields::Field32A;
609    /// # use chrono::NaiveDate;
610    /// let usd_field = Field32A::new(
611    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
612    ///     "USD".to_string(),
613    ///     1000.00
614    /// );
615    /// assert_eq!(usd_field.settlement_timing(), "T+0/T+1 (Same day or next day)");
616    /// ```
617    pub fn settlement_timing(&self) -> &'static str {
618        match self.currency.as_str() {
619            "USD" => "T+0/T+1 (Same day or next day)",
620            "EUR" => "T+1 (Next day via TARGET2)",
621            "GBP" => "T+0 (Same day via CHAPS)",
622            "JPY" => "T+0/T+1 (Same day or next day)",
623            "CHF" => "T+0 (Same day via SIC)",
624            "CAD" => "T+0/T+1 (Same day or next day)",
625            "AUD" => "T+1 (Next day via RITS)",
626            "SEK" => "T+1 (Next day via RIX)",
627            "NOK" => "T+0 (Same day via NBO)",
628            "DKK" => "T+1 (Next day via Kronos2)",
629            _ => "T+2 or longer (Depends on currency and market)",
630        }
631    }
632
633    /// Check if this is a same-day value transaction
634    ///
635    /// Determines if the value date is the same as today's date,
636    /// indicating same-day value requirements.
637    ///
638    /// # Returns
639    /// `true` if the value date is today
640    ///
641    /// # Example
642    /// ```rust
643    /// # use swift_mt_message::fields::Field32A;
644    /// # use chrono::{NaiveDate, Utc};
645    /// let today = Utc::now().date_naive();
646    /// let field = Field32A::new(today, "USD".to_string(), 1000.00);
647    /// assert!(field.is_same_day_value());
648    /// ```
649    pub fn is_same_day_value(&self) -> bool {
650        let today = chrono::Utc::now().date_naive();
651        self.value_date == today
652    }
653
654    /// Check if this is a forward-dated transaction
655    ///
656    /// Determines if the value date is in the future,
657    /// indicating a forward-dated transaction.
658    ///
659    /// # Returns
660    /// `true` if the value date is in the future
661    ///
662    /// # Example
663    /// ```rust
664    /// # use swift_mt_message::fields::Field32A;
665    /// # use chrono::{NaiveDate, Utc, Duration};
666    /// let future_date = Utc::now().date_naive() + Duration::days(5);
667    /// let field = Field32A::new(future_date, "USD".to_string(), 1000.00);
668    /// assert!(field.is_forward_dated());
669    /// ```
670    pub fn is_forward_dated(&self) -> bool {
671        let today = chrono::Utc::now().date_naive();
672        self.value_date > today
673    }
674
675    /// Check if this is a back-dated transaction
676    ///
677    /// Determines if the value date is in the past,
678    /// indicating a back-dated transaction.
679    ///
680    /// # Returns
681    /// `true` if the value date is in the past
682    ///
683    /// # Example
684    /// ```rust
685    /// # use swift_mt_message::fields::Field32A;
686    /// # use chrono::{NaiveDate, Utc, Duration};
687    /// let past_date = Utc::now().date_naive() - Duration::days(2);
688    /// let field = Field32A::new(past_date, "USD".to_string(), 1000.00);
689    /// assert!(field.is_back_dated());
690    /// ```
691    pub fn is_back_dated(&self) -> bool {
692        let today = chrono::Utc::now().date_naive();
693        self.value_date < today
694    }
695
696    /// Get days until value date
697    ///
698    /// Returns the number of days between today and the value date.
699    /// Positive values indicate future dates, negative values indicate past dates.
700    ///
701    /// # Returns
702    /// Number of days (positive for future, negative for past, 0 for today)
703    ///
704    /// # Example
705    /// ```rust
706    /// # use swift_mt_message::fields::Field32A;
707    /// # use chrono::{NaiveDate, Utc, Duration};
708    /// let future_date = Utc::now().date_naive() + Duration::days(3);
709    /// let field = Field32A::new(future_date, "USD".to_string(), 1000.00);
710    /// assert_eq!(field.days_until_value_date(), 3);
711    /// ```
712    pub fn days_until_value_date(&self) -> i64 {
713        let today = chrono::Utc::now().date_naive();
714        (self.value_date - today).num_days()
715    }
716
717    /// Format amount with proper currency precision
718    ///
719    /// Formats the amount according to the typical precision
720    /// rules for the currency.
721    ///
722    /// # Returns
723    /// Formatted amount string
724    ///
725    /// # Example
726    /// ```rust
727    /// # use swift_mt_message::fields::Field32A;
728    /// # use chrono::NaiveDate;
729    /// let usd_field = Field32A::new(
730    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
731    ///     "USD".to_string(),
732    ///     1234.56
733    /// );
734    /// assert_eq!(usd_field.formatted_amount(), "1234.56");
735    ///
736    /// let jpy_field = Field32A::new(
737    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
738    ///     "JPY".to_string(),
739    ///     1234.00
740    /// );
741    /// assert_eq!(jpy_field.formatted_amount(), "1234");
742    /// ```
743    pub fn formatted_amount(&self) -> String {
744        let decimal_places = self.decimal_places();
745        match decimal_places {
746            0 => format!("{:.0}", self.amount.round()),
747            2 => format!("{:.2}", self.amount),
748            3 => format!("{:.3}", self.amount),
749            _ => format!("{:.2}", self.amount), // Default to 2
750        }
751    }
752
753    /// Get comprehensive transaction description
754    ///
755    /// Returns a detailed description of the transaction including
756    /// value date, currency, amount, and transaction characteristics.
757    ///
758    /// # Returns
759    /// Formatted description string
760    ///
761    /// # Example
762    /// ```rust
763    /// # use swift_mt_message::fields::Field32A;
764    /// # use chrono::NaiveDate;
765    /// let field = Field32A::new(
766    ///     NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
767    ///     "USD".to_string(),
768    ///     1500000.00
769    /// );
770    /// let desc = field.comprehensive_description();
771    /// assert!(desc.contains("USD"));
772    /// assert!(desc.contains("1500000.00"));
773    /// assert!(desc.contains("2024-03-15"));
774    /// ```
775    pub fn comprehensive_description(&self) -> String {
776        let value_timing = if self.is_same_day_value() {
777            "Same-day value"
778        } else if self.is_forward_dated() {
779            "Forward-dated"
780        } else if self.is_back_dated() {
781            "Back-dated"
782        } else {
783            "Standard value"
784        };
785
786        let amount_category = if self.is_high_value_transaction() {
787            "High-value"
788        } else {
789            "Standard"
790        };
791
792        let currency_type = if self.is_major_currency() {
793            "Major currency"
794        } else {
795            "Other currency"
796        };
797
798        format!(
799            "Value Date: {} | Currency: {} ({}) | Amount: {} {} | Settlement: {} | Category: {} {}",
800            self.value_date,
801            self.currency,
802            currency_type,
803            self.formatted_amount(),
804            self.currency,
805            self.settlement_timing(),
806            amount_category,
807            value_timing
808        )
809    }
810}
811
812impl std::fmt::Display for Field32A {
813    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
814        write!(
815            f,
816            "{}{}{}",
817            self.date_string(),
818            self.currency,
819            self.raw_amount
820        )
821    }
822}
823
824#[cfg(test)]
825mod tests {
826    use super::*;
827    use chrono::Datelike;
828
829    #[test]
830    fn test_field32a_creation() {
831        let date = NaiveDate::from_ymd_opt(2021, 3, 15).unwrap();
832        let field = Field32A::new(date, "EUR".to_string(), 1234567.89);
833
834        assert_eq!(field.value_date.year(), 2021);
835        assert_eq!(field.value_date.month(), 3);
836        assert_eq!(field.value_date.day(), 15);
837        assert_eq!(field.currency_code(), "EUR");
838        assert_eq!(field.amount, 1234567.89);
839    }
840
841    #[test]
842    fn test_field32a_parse() {
843        let field = Field32A::parse("210315EUR1234567,89").unwrap();
844        assert_eq!(field.value_date.year(), 2021);
845        assert_eq!(field.value_date.month(), 3);
846        assert_eq!(field.value_date.day(), 15);
847        assert_eq!(field.currency_code(), "EUR");
848        assert_eq!(field.amount, 1234567.89);
849    }
850
851    #[test]
852    fn test_field32a_date_string() {
853        let date = NaiveDate::from_ymd_opt(2021, 3, 15).unwrap();
854        let field = Field32A::new(date, "EUR".to_string(), 1234567.89);
855
856        assert_eq!(field.date_string(), "210315");
857    }
858
859    #[test]
860    fn test_field32a_to_swift_string() {
861        let date = NaiveDate::from_ymd_opt(2021, 3, 15).unwrap();
862        let field = Field32A::new(date, "EUR".to_string(), 1234567.89);
863
864        assert_eq!(field.to_swift_string(), ":32A:210315EUR1234567,89");
865    }
866
867    #[test]
868    fn test_field32a_major_currencies() {
869        let major_currencies = [
870            "USD", "EUR", "GBP", "JPY", "CHF", "CAD", "AUD", "NZD", "SEK", "NOK", "DKK",
871        ];
872        let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
873
874        for currency in major_currencies {
875            let field = Field32A::new(date, currency.to_string(), 1000.00);
876            assert!(
877                field.is_major_currency(),
878                "Currency {} should be major currency",
879                currency
880            );
881        }
882
883        let exotic_currencies = ["XYZ", "ABC", "THB", "MXN"];
884        for currency in exotic_currencies {
885            let field = Field32A::new(date, currency.to_string(), 1000.00);
886            assert!(
887                !field.is_major_currency(),
888                "Currency {} should not be major currency",
889                currency
890            );
891        }
892    }
893
894    #[test]
895    fn test_field32a_decimal_places() {
896        let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
897
898        // Currencies with no decimal places
899        let no_decimal_currencies = ["JPY", "KRW", "VND"];
900        for currency in no_decimal_currencies {
901            let field = Field32A::new(date, currency.to_string(), 1000.00);
902            assert_eq!(
903                field.decimal_places(),
904                0,
905                "Currency {} should have 0 decimal places",
906                currency
907            );
908            assert!(!field.has_decimal_places());
909        }
910
911        // Currencies with 3 decimal places
912        let three_decimal_currencies = ["BHD", "JOD", "KWD"];
913        for currency in three_decimal_currencies {
914            let field = Field32A::new(date, currency.to_string(), 1000.00);
915            assert_eq!(
916                field.decimal_places(),
917                3,
918                "Currency {} should have 3 decimal places",
919                currency
920            );
921            assert!(field.has_decimal_places());
922        }
923
924        // Currencies with 2 decimal places (most common)
925        let two_decimal_currencies = ["USD", "EUR", "GBP"];
926        for currency in two_decimal_currencies {
927            let field = Field32A::new(date, currency.to_string(), 1000.00);
928            assert_eq!(
929                field.decimal_places(),
930                2,
931                "Currency {} should have 2 decimal places",
932                currency
933            );
934            assert!(field.has_decimal_places());
935        }
936    }
937
938    #[test]
939    fn test_field32a_high_value_transactions() {
940        let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
941
942        // High-value USD transaction
943        let high_usd = Field32A::new(date, "USD".to_string(), 1_500_000.00);
944        assert!(high_usd.is_high_value_transaction());
945
946        let normal_usd = Field32A::new(date, "USD".to_string(), 500_000.00);
947        assert!(!normal_usd.is_high_value_transaction());
948
949        // High-value JPY transaction (different threshold)
950        let high_jpy = Field32A::new(date, "JPY".to_string(), 150_000_000.00);
951        assert!(high_jpy.is_high_value_transaction());
952
953        let normal_jpy = Field32A::new(date, "JPY".to_string(), 50_000_000.00);
954        assert!(!normal_jpy.is_high_value_transaction());
955    }
956
957    #[test]
958    fn test_field32a_settlement_timing() {
959        let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
960
961        let test_cases = [
962            ("USD", "T+0/T+1 (Same day or next day)"),
963            ("EUR", "T+1 (Next day via TARGET2)"),
964            ("GBP", "T+0 (Same day via CHAPS)"),
965            ("JPY", "T+0/T+1 (Same day or next day)"),
966            ("CHF", "T+0 (Same day via SIC)"),
967            ("XYZ", "T+2 or longer (Depends on currency and market)"),
968        ];
969
970        for (currency, expected_timing) in test_cases {
971            let field = Field32A::new(date, currency.to_string(), 1000.00);
972            assert_eq!(
973                field.settlement_timing(),
974                expected_timing,
975                "Settlement timing mismatch for currency {}",
976                currency
977            );
978        }
979    }
980
981    #[test]
982    fn test_field32a_value_date_analysis() {
983        let today = chrono::Utc::now().date_naive();
984        let future_date = today + chrono::Duration::days(5);
985        let past_date = today - chrono::Duration::days(3);
986
987        // Same-day value
988        let same_day = Field32A::new(today, "USD".to_string(), 1000.00);
989        assert!(same_day.is_same_day_value());
990        assert!(!same_day.is_forward_dated());
991        assert!(!same_day.is_back_dated());
992        assert_eq!(same_day.days_until_value_date(), 0);
993
994        // Forward-dated
995        let forward = Field32A::new(future_date, "USD".to_string(), 1000.00);
996        assert!(!forward.is_same_day_value());
997        assert!(forward.is_forward_dated());
998        assert!(!forward.is_back_dated());
999        assert_eq!(forward.days_until_value_date(), 5);
1000
1001        // Back-dated
1002        let back = Field32A::new(past_date, "USD".to_string(), 1000.00);
1003        assert!(!back.is_same_day_value());
1004        assert!(!back.is_forward_dated());
1005        assert!(back.is_back_dated());
1006        assert_eq!(back.days_until_value_date(), -3);
1007    }
1008
1009    #[test]
1010    fn test_field32a_formatted_amount() {
1011        let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
1012
1013        // USD with 2 decimal places
1014        let usd_field = Field32A::new(date, "USD".to_string(), 1234.56);
1015        assert_eq!(usd_field.formatted_amount(), "1234.56");
1016
1017        // JPY with 0 decimal places
1018        let jpy_field = Field32A::new(date, "JPY".to_string(), 1234.00);
1019        assert_eq!(jpy_field.formatted_amount(), "1234");
1020
1021        // KWD with 3 decimal places
1022        let kwd_field = Field32A::new(date, "KWD".to_string(), 1234.567);
1023        assert_eq!(kwd_field.formatted_amount(), "1234.567");
1024    }
1025
1026    #[test]
1027    fn test_field32a_from_raw() {
1028        let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
1029
1030        let field = Field32A::from_raw(date, "EUR".to_string(), "1000,50".to_string()).unwrap();
1031        assert_eq!(field.amount, 1000.50);
1032        assert_eq!(field.raw_amount, "1000,50");
1033        assert_eq!(field.currency, "EUR");
1034
1035        // Test with dot separator
1036        let field = Field32A::from_raw(date, "USD".to_string(), "2500.75".to_string()).unwrap();
1037        assert_eq!(field.amount, 2500.75);
1038        assert_eq!(field.raw_amount, "2500.75");
1039    }
1040
1041    #[test]
1042    fn test_field32a_comprehensive_description() {
1043        let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
1044
1045        // High-value USD transaction
1046        let field = Field32A::new(date, "USD".to_string(), 1_500_000.00);
1047        let desc = field.comprehensive_description();
1048
1049        assert!(desc.contains("USD"));
1050        assert!(desc.contains("1500000.00"));
1051        assert!(desc.contains("2024-03-15"));
1052        assert!(desc.contains("Major currency"));
1053        assert!(desc.contains("High-value"));
1054        assert!(desc.contains("T+0/T+1"));
1055
1056        // Normal EUR transaction
1057        let field = Field32A::new(date, "EUR".to_string(), 50_000.00);
1058        let desc = field.comprehensive_description();
1059
1060        assert!(desc.contains("EUR"));
1061        assert!(desc.contains("50000.00"));
1062        assert!(desc.contains("Standard"));
1063        assert!(desc.contains("T+1"));
1064    }
1065
1066    #[test]
1067    fn test_field32a_validation() {
1068        let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
1069        let field = Field32A::new(date, "USD".to_string(), 1000.00);
1070
1071        let validation = field.validate();
1072        assert!(validation.is_valid);
1073        assert!(validation.errors.is_empty());
1074    }
1075
1076    #[test]
1077    fn test_field32a_serialization() {
1078        let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
1079        let field = Field32A::new(date, "USD".to_string(), 1234.56);
1080
1081        let serialized = serde_json::to_string(&field).unwrap();
1082        let deserialized: Field32A = serde_json::from_str(&serialized).unwrap();
1083
1084        assert_eq!(field.value_date, deserialized.value_date);
1085        assert_eq!(field.currency, deserialized.currency);
1086        assert_eq!(field.amount, deserialized.amount);
1087        assert_eq!(field.raw_amount, deserialized.raw_amount);
1088    }
1089
1090    #[test]
1091    fn test_field32a_business_logic_combinations() {
1092        let today = chrono::Utc::now().date_naive();
1093
1094        // Major currency, high-value, forward-dated
1095        let future_date = today + chrono::Duration::days(7);
1096        let field = Field32A::new(future_date, "USD".to_string(), 2_000_000.00);
1097
1098        assert!(field.is_major_currency());
1099        assert!(field.is_high_value_transaction());
1100        assert!(field.is_forward_dated());
1101        assert!(field.has_decimal_places());
1102        assert_eq!(field.decimal_places(), 2);
1103        assert_eq!(field.settlement_timing(), "T+0/T+1 (Same day or next day)");
1104
1105        // Exotic currency, normal value, back-dated
1106        let past_date = today - chrono::Duration::days(2);
1107        let field = Field32A::new(past_date, "THB".to_string(), 100_000.00);
1108
1109        assert!(!field.is_major_currency());
1110        assert!(!field.is_high_value_transaction());
1111        assert!(field.is_back_dated());
1112        assert!(field.has_decimal_places());
1113        assert_eq!(field.decimal_places(), 2);
1114        assert_eq!(
1115            field.settlement_timing(),
1116            "T+2 or longer (Depends on currency and market)"
1117        );
1118    }
1119
1120    #[test]
1121    fn test_field32a_edge_cases() {
1122        let date = NaiveDate::from_ymd_opt(2024, 2, 29).unwrap(); // Leap year
1123
1124        // Very small amount
1125        let small_field = Field32A::new(date, "USD".to_string(), 0.01);
1126        assert_eq!(small_field.amount, 0.01);
1127        assert!(!small_field.is_high_value_transaction());
1128
1129        // Very large amount
1130        let large_field = Field32A::new(date, "USD".to_string(), 999_999_999.99);
1131        assert!(large_field.is_high_value_transaction());
1132
1133        // JPY with fractional amount (should still work)
1134        let jpy_field = Field32A::new(date, "JPY".to_string(), 1000.50);
1135        assert_eq!(jpy_field.formatted_amount(), "1001"); // Rounded to 0 decimal places
1136    }
1137
1138    #[test]
1139    fn test_field32a_real_world_scenarios() {
1140        // Scenario 1: International wire transfer
1141        let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
1142        let wire = Field32A::new(date, "USD".to_string(), 50_000.00);
1143
1144        assert_eq!(wire.currency_code(), "USD");
1145        assert!(wire.is_major_currency());
1146        assert!(!wire.is_high_value_transaction());
1147        assert_eq!(wire.settlement_timing(), "T+0/T+1 (Same day or next day)");
1148        assert_eq!(wire.formatted_amount(), "50000.00");
1149
1150        // Scenario 2: High-value EUR payment
1151        let eur_payment = Field32A::new(date, "EUR".to_string(), 1_200_000.00);
1152
1153        assert!(eur_payment.is_high_value_transaction());
1154        assert_eq!(
1155            eur_payment.settlement_timing(),
1156            "T+1 (Next day via TARGET2)"
1157        );
1158
1159        // Scenario 3: JPY transaction
1160        let jpy_payment = Field32A::new(date, "JPY".to_string(), 5_000_000.00);
1161
1162        assert!(!jpy_payment.has_decimal_places());
1163        assert_eq!(jpy_payment.decimal_places(), 0);
1164        assert_eq!(jpy_payment.formatted_amount(), "5000000");
1165
1166        // Scenario 4: Same-day GBP payment
1167        let today = chrono::Utc::now().date_naive();
1168        let gbp_payment = Field32A::new(today, "GBP".to_string(), 75_000.00);
1169
1170        assert!(gbp_payment.is_same_day_value());
1171        assert_eq!(gbp_payment.settlement_timing(), "T+0 (Same day via CHAPS)");
1172        assert_eq!(gbp_payment.days_until_value_date(), 0);
1173    }
1174
1175    #[test]
1176    fn test_field32a_currency_specific_behavior() {
1177        let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
1178
1179        // Test specific currency behaviors
1180        let test_cases = [
1181            (
1182                "USD",
1183                true,
1184                2,
1185                1_000_000.0,
1186                "T+0/T+1 (Same day or next day)",
1187            ),
1188            ("EUR", true, 2, 1_000_000.0, "T+1 (Next day via TARGET2)"),
1189            (
1190                "JPY",
1191                true,
1192                0,
1193                100_000_000.0,
1194                "T+0/T+1 (Same day or next day)",
1195            ),
1196            (
1197                "KWD",
1198                false,
1199                3,
1200                1_000_000.0,
1201                "T+2 or longer (Depends on currency and market)",
1202            ),
1203            (
1204                "THB",
1205                false,
1206                2,
1207                1_000_000.0,
1208                "T+2 or longer (Depends on currency and market)",
1209            ),
1210        ];
1211
1212        for (currency, is_major, decimal_places, high_value_threshold, settlement) in test_cases {
1213            let field = Field32A::new(date, currency.to_string(), 1000.00);
1214
1215            assert_eq!(
1216                field.is_major_currency(),
1217                is_major,
1218                "Major currency check failed for {}",
1219                currency
1220            );
1221            assert_eq!(
1222                field.decimal_places(),
1223                decimal_places,
1224                "Decimal places check failed for {}",
1225                currency
1226            );
1227            assert_eq!(
1228                field.settlement_timing(),
1229                settlement,
1230                "Settlement timing check failed for {}",
1231                currency
1232            );
1233
1234            // Test high-value threshold
1235            let high_value_field = Field32A::new(date, currency.to_string(), high_value_threshold);
1236            assert!(
1237                high_value_field.is_high_value_transaction(),
1238                "High value threshold check failed for {}",
1239                currency
1240            );
1241        }
1242    }
1243}