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}