swift_mt_message/fields/
field33b.rs

1use crate::{SwiftField, ValidationError, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// # Field 33B: Currency/Instructed Amount
5///
6/// ## Overview
7/// Field 33B specifies the original ordered amount in currency conversions and
8/// multi-currency transactions. This field is used when the instructed amount differs
9/// from the settlement amount, typically in foreign exchange transactions, currency
10/// conversions, or when fees are deducted from the principal amount. It provides
11/// transparency about the original instruction versus the actual settlement.
12///
13/// ## Format Specification
14/// **Format**: `3!a15d`
15/// - **3!a**: Currency code (3 alphabetic characters, ISO 4217)
16/// - **15d**: Amount with up to 15 digits including decimal places
17///
18/// ### Component Details
19/// 1. **Currency Code (3!a)**:
20///    - ISO 4217 standard currency codes
21///    - Exactly 3 alphabetic characters
22///    - Case-insensitive input, stored as uppercase
23///    - Must be valid and active currency code
24///    - Examples: USD, EUR, GBP, JPY, CHF
25///
26/// 2. **Amount (15d)**:
27///    - Up to 15 digits including decimal places
28///    - Decimal separator: comma (,) in SWIFT format
29///    - No thousands separators allowed
30///    - Must be non-negative (≥ 0)
31///    - Precision varies by currency (typically 2 decimal places)
32///
33/// ## Usage Context
34/// Field 33B appears in various SWIFT MT message types where currency conversion
35/// or amount differentiation is required:
36///
37/// ### Primary Usage
38/// - **MT103**: Single Customer Credit Transfer - when original amount differs from settlement
39/// - **MT202**: General Financial Institution Transfer - for currency conversion scenarios
40/// - **MT202COV**: Cover for customer credit transfer - original instructed amount
41/// - **MT205**: Financial Institution Transfer - when amounts differ due to charges
42///
43/// ### Secondary Usage
44/// - **MT400**: Advice of Payment - original payment instruction amount
45/// - **MT410**: Acknowledgement - acknowledged original amount
46/// - **MT420**: Tracer - original traced amount
47/// - **MT900/910**: Confirmation messages - original instruction amount
48///
49/// ## Business Applications
50/// - **Currency conversion**: Original amount before FX conversion
51/// - **Charge deduction**: Principal amount before fee deduction
52/// - **Multi-currency processing**: Cross-currency transaction handling
53/// - **Reconciliation**: Matching original instructions with settlements
54/// - **Audit trails**: Maintaining complete transaction history
55/// - **Compliance reporting**: Regulatory reporting of original amounts
56/// - **Customer transparency**: Showing original vs. settled amounts
57/// - **FX risk management**: Tracking exposure in original currency
58///
59/// ## Related Fields
60/// Field 33B works in conjunction with other amount fields:
61///
62/// ### Field 32A (Value Date/Currency/Amount)
63/// - **32A**: Settlement amount and currency
64/// - **33B**: Original instructed amount and currency
65/// - **Relationship**: 33B shows original, 32A shows final settlement
66///
67/// ### Field 71A (Details of Charges)
68/// - **71A**: Charge allocation (OUR/BEN/SHA)
69/// - **33B**: Amount before charge deduction
70/// - **Usage**: When charges affect the settlement amount
71///
72/// ### Field 36 (Exchange Rate)
73/// - **36**: Exchange rate applied
74/// - **33B**: Amount in original currency
75/// - **Usage**: FX transactions showing rate and original amount
76///
77/// ## Currency Conversion Scenarios
78/// 1. **Customer instructs**: EUR 100,000
79/// 2. **Bank converts to**: USD 108,500 (at rate 1.085)
80/// 3. **Field 33B**: EUR100000,00 (original instruction)
81/// 4. **Field 32A**: USD108500,00 (settlement amount)
82///
83/// ## Charge Deduction Scenarios
84/// 1. **Customer instructs**: USD 50,000
85/// 2. **Bank deducts charges**: USD 25 (wire fee)
86/// 3. **Field 33B**: USD50000,00 (original amount)
87/// 4. **Field 32A**: USD49975,00 (net settlement)
88///
89/// ## Validation Rules
90/// 1. **Currency format**: Must be exactly 3 alphabetic characters
91/// 2. **Currency validity**: Should be valid ISO 4217 code
92/// 3. **Amount format**: Must follow SWIFT decimal format (comma separator)
93/// 4. **Amount value**: Must be non-negative (zero allowed for certain scenarios)
94/// 5. **Precision**: Should match currency-specific decimal place rules
95/// 6. **Consistency**: Should be logically consistent with Field 32A
96///
97/// ## Network Validated Rules (SWIFT Standards)
98/// - Currency code must be exactly 3 characters (Error: T52)
99/// - Currency must be valid ISO 4217 code (Error: T52)
100/// - Amount must be properly formatted (Error: T40)
101/// - Amount cannot be negative (Error: T13)
102/// - Field should be present when amounts differ (Warning: recommended)
103/// - Currency should be actively traded (Warning: recommended)
104///
105///
106/// ## Examples
107/// ```text
108/// :33B:EUR100000,00
109/// └─── Original instruction: EUR 100,000.00
110///
111/// :33B:USD50000,00
112/// └─── Before charges: USD 50,000.00
113///
114/// :33B:GBP25000,50
115/// └─── Original amount: GBP 25,000.50
116/// ```
117///
118
119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
120pub struct Field33B {
121    /// ISO 4217 currency code (3 alphabetic characters)
122    ///
123    /// Specifies the currency of the original instructed amount using the
124    /// international standard ISO 4217 currency codes. This represents the
125    /// currency in which the customer originally instructed the transaction.
126    ///
127    /// **Format**: Exactly 3 uppercase alphabetic characters
128    /// **Standard**: ISO 4217 (International Organization for Standardization)
129    /// **Case handling**: Automatically converted to uppercase
130    /// **Validation**: Must be valid and preferably active currency code
131    ///
132    /// # Common Scenarios
133    /// - **FX conversion**: Original currency before conversion to settlement currency
134    /// - **Multi-currency**: Different from settlement currency in Field 32A
135    /// - **Charge scenarios**: Same as settlement currency but different amount
136    ///
137    /// # Examples
138    /// - `"EUR"` - Euro (original instruction currency)
139    /// - `"USD"` - US Dollar (before conversion to EUR settlement)
140    /// - `"GBP"` - British Pound (customer's account currency)
141    pub currency: String,
142
143    /// Original instructed amount as decimal value
144    ///
145    /// The monetary amount as originally instructed by the customer or
146    /// ordering party, before any currency conversion, charge deduction,
147    /// or other modifications that result in a different settlement amount.
148    ///
149    /// **Range**: Must be non-negative (≥ 0.0)
150    /// **Precision**: Should follow currency-specific decimal place rules
151    /// **Usage**: Represents the "gross" or "original" amount
152    ///
153    /// # Business Context
154    /// - **Before FX**: Amount before currency conversion
155    /// - **Before charges**: Amount before fee/charge deduction
156    /// - **Customer view**: Amount as seen by the ordering customer
157    /// - **Audit trail**: Original instruction for compliance purposes
158    ///
159    /// # Examples
160    /// - `100000.00` - EUR 100,000 before conversion to USD
161    /// - `50000.00` - USD 50,000 before $25 wire fee deduction
162    /// - `25000.50` - GBP 25,000.50 original instruction amount
163    pub amount: f64,
164
165    /// Raw amount string as received (preserves original formatting)
166    ///
167    /// Maintains the exact string representation of the amount as received
168    /// in the SWIFT message, preserving the original formatting including
169    /// decimal separator, precision, and any leading/trailing characters.
170    ///
171    /// **Format**: SWIFT standard with comma as decimal separator
172    /// **Preservation**: Exact reproduction of original message format
173    /// **Usage**: For message reconstruction and audit purposes
174    ///
175    /// # Format Examples
176    /// - `"100000,00"` - SWIFT format with comma separator
177    /// - `"50000,00"` - Two decimal places preserved
178    /// - `"25000,50"` - Original precision maintained
179    /// - `"0,01"` - Minimum amount with leading zero
180    pub raw_amount: String,
181}
182
183impl Field33B {
184    /// Create a new Field33B with validation
185    ///
186    /// Creates a new Field33B instance with comprehensive validation of both
187    /// currency code and amount. The currency is normalized to uppercase and
188    /// the amount is formatted according to SWIFT standards.
189    ///
190    /// # Arguments
191    /// * `currency` - ISO 4217 currency code (will be converted to uppercase)
192    /// * `amount` - Original instructed amount (must be non-negative)
193    ///
194    /// # Returns
195    /// Result containing the Field33B instance or validation error
196    ///
197    /// # Example
198    /// ```rust
199    /// # use swift_mt_message::fields::Field33B;
200    /// let field = Field33B::new("EUR", 100000.00).unwrap();
201    /// assert_eq!(field.currency(), "EUR");
202    /// assert_eq!(field.amount(), 100000.00);
203    /// ```
204    pub fn new(currency: impl Into<String>, amount: f64) -> Result<Self, crate::ParseError> {
205        let currency = currency.into().to_uppercase();
206
207        // Validate currency code
208        if currency.len() != 3 {
209            return Err(crate::ParseError::InvalidFieldFormat {
210                field_tag: "33B".to_string(),
211                message: "Currency code must be exactly 3 characters".to_string(),
212            });
213        }
214
215        if !currency.chars().all(|c| c.is_alphabetic() && c.is_ascii()) {
216            return Err(crate::ParseError::InvalidFieldFormat {
217                field_tag: "33B".to_string(),
218                message: "Currency code must contain only alphabetic characters".to_string(),
219            });
220        }
221
222        // Validate amount
223        if amount < 0.0 {
224            return Err(crate::ParseError::InvalidFieldFormat {
225                field_tag: "33B".to_string(),
226                message: "Amount cannot be negative".to_string(),
227            });
228        }
229
230        let raw_amount = Self::format_amount(amount);
231
232        Ok(Field33B {
233            currency,
234            amount,
235            raw_amount,
236        })
237    }
238
239    /// Create from raw amount string
240    ///
241    /// Creates a Field33B instance from a raw amount string, preserving
242    /// the original formatting while parsing the numeric value.
243    ///
244    /// # Arguments
245    /// * `currency` - ISO 4217 currency code
246    /// * `raw_amount` - Amount string in SWIFT format
247    ///
248    /// # Returns
249    /// Result containing the Field33B instance or parse error
250    ///
251    /// # Example
252    /// ```rust
253    /// # use swift_mt_message::fields::Field33B;
254    /// let field = Field33B::from_raw("USD", "50000,00").unwrap();
255    /// assert_eq!(field.amount(), 50000.00);
256    /// assert_eq!(field.raw_amount(), "50000,00");
257    /// ```
258    pub fn from_raw(
259        currency: impl Into<String>,
260        raw_amount: impl Into<String>,
261    ) -> Result<Self, crate::ParseError> {
262        let currency = currency.into().to_uppercase();
263        let raw_amount = raw_amount.into();
264
265        let amount = Self::parse_amount(&raw_amount)?;
266
267        Ok(Field33B {
268            currency,
269            amount,
270            raw_amount: raw_amount.to_string(),
271        })
272    }
273
274    /// Get the currency code
275    ///
276    /// Returns the ISO 4217 currency code for the original instructed amount.
277    ///
278    /// # Returns
279    /// Currency code as string slice
280    ///
281    /// # Example
282    /// ```rust
283    /// # use swift_mt_message::fields::Field33B;
284    /// let field = Field33B::new("GBP", 25000.50).unwrap();
285    /// assert_eq!(field.currency(), "GBP");
286    /// ```
287    pub fn currency(&self) -> &str {
288        &self.currency
289    }
290
291    /// Get the amount value
292    ///
293    /// Returns the original instructed amount as a floating-point number.
294    ///
295    /// # Returns
296    /// Amount as f64
297    ///
298    /// # Example
299    /// ```rust
300    /// # use swift_mt_message::fields::Field33B;
301    /// let field = Field33B::new("USD", 75000.25).unwrap();
302    /// assert_eq!(field.amount(), 75000.25);
303    /// ```
304    pub fn amount(&self) -> f64 {
305        self.amount
306    }
307
308    /// Get the raw amount string
309    ///
310    /// Returns the original amount string as received, preserving
311    /// the exact formatting from the SWIFT message.
312    ///
313    /// # Returns
314    /// Raw amount string
315    ///
316    /// # Example
317    /// ```rust
318    /// # use swift_mt_message::fields::Field33B;
319    /// let field = Field33B::from_raw("EUR", "1000,50").unwrap();
320    /// assert_eq!(field.raw_amount(), "1000,50");
321    /// ```
322    pub fn raw_amount(&self) -> &str {
323        &self.raw_amount
324    }
325
326    /// Format amount for SWIFT output (with comma as decimal separator)
327    ///
328    /// Formats a decimal amount according to SWIFT standards using
329    /// comma as the decimal separator.
330    ///
331    /// # Arguments
332    /// * `amount` - Amount to format
333    ///
334    /// # Returns
335    /// Formatted amount string
336    ///
337    /// # Example
338    /// ```rust
339    /// # use swift_mt_message::fields::Field33B;
340    /// let formatted = Field33B::format_amount(1234.56);
341    /// assert_eq!(formatted, "1234,56");
342    /// ```
343    pub fn format_amount(amount: f64) -> String {
344        format!("{:.2}", amount).replace('.', ",")
345    }
346
347    /// Parse amount from string (handles both comma and dot as decimal separator)
348    fn parse_amount(amount_str: &str) -> Result<f64, crate::ParseError> {
349        let normalized_amount = amount_str.replace(',', ".");
350
351        normalized_amount
352            .parse::<f64>()
353            .map_err(|_| crate::ParseError::InvalidFieldFormat {
354                field_tag: "33B".to_string(),
355                message: "Invalid amount format".to_string(),
356            })
357    }
358
359    /// Check if this is a valid ISO 4217 currency code (basic validation)
360    ///
361    /// Performs basic format validation to check if the currency code
362    /// follows ISO 4217 standards (3 alphabetic characters).
363    ///
364    /// # Returns
365    /// `true` if the currency code format is valid
366    ///
367    /// # Example
368    /// ```rust
369    /// # use swift_mt_message::fields::Field33B;
370    /// let field = Field33B::new("USD", 1000.00).unwrap();
371    /// assert!(field.is_valid_currency());
372    /// ```
373    pub fn is_valid_currency(&self) -> bool {
374        self.currency.len() == 3 && self.currency.chars().all(|c| c.is_alphabetic())
375    }
376
377    /// Check if the currency is a major currency
378    ///
379    /// Determines if the currency is one of the major internationally
380    /// traded currencies with high liquidity and frequent usage.
381    ///
382    /// # Returns
383    /// `true` if the currency is a major currency
384    ///
385    /// # Example
386    /// ```rust
387    /// # use swift_mt_message::fields::Field33B;
388    /// let usd_field = Field33B::new("USD", 1000.00).unwrap();
389    /// assert!(usd_field.is_major_currency());
390    ///
391    /// let exotic_field = Field33B::new("XYZ", 1000.00).unwrap();
392    /// assert!(!exotic_field.is_major_currency());
393    /// ```
394    pub fn is_major_currency(&self) -> bool {
395        matches!(
396            self.currency.as_str(),
397            "USD" | "EUR" | "GBP" | "JPY" | "CHF" | "CAD" | "AUD" | "NZD" | "SEK" | "NOK" | "DKK"
398        )
399    }
400
401    /// Check if the currency typically has decimal places
402    ///
403    /// Determines if the currency typically uses decimal places in
404    /// amount representation. Some currencies like JPY typically
405    /// don't use decimal places.
406    ///
407    /// # Returns
408    /// `true` if the currency typically uses decimal places
409    ///
410    /// # Example
411    /// ```rust
412    /// # use swift_mt_message::fields::Field33B;
413    /// let usd_field = Field33B::new("USD", 1000.00).unwrap();
414    /// assert!(usd_field.has_decimal_places());
415    ///
416    /// let jpy_field = Field33B::new("JPY", 1000.00).unwrap();
417    /// assert!(!jpy_field.has_decimal_places());
418    /// ```
419    pub fn has_decimal_places(&self) -> bool {
420        !matches!(
421            self.currency.as_str(),
422            "JPY" | "KRW" | "VND" | "IDR" | "CLP" | "PYG" | "UGX" | "RWF" | "GNF" | "MGA"
423        )
424    }
425
426    /// Get the typical decimal places for this currency
427    ///
428    /// Returns the number of decimal places typically used for
429    /// this currency in financial transactions.
430    ///
431    /// # Returns
432    /// Number of decimal places (0, 2, or 3)
433    ///
434    /// # Example
435    /// ```rust
436    /// # use swift_mt_message::fields::Field33B;
437    /// let usd_field = Field33B::new("USD", 1000.00).unwrap();
438    /// assert_eq!(usd_field.decimal_places(), 2);
439    ///
440    /// let jpy_field = Field33B::new("JPY", 1000.00).unwrap();
441    /// assert_eq!(jpy_field.decimal_places(), 0);
442    /// ```
443    pub fn decimal_places(&self) -> u8 {
444        match self.currency.as_str() {
445            // Currencies with no decimal places
446            "JPY" | "KRW" | "VND" | "IDR" | "CLP" | "PYG" | "UGX" | "RWF" | "GNF" | "MGA" => 0,
447            // Currencies with 3 decimal places
448            "BHD" | "IQD" | "JOD" | "KWD" | "LYD" | "OMR" | "TND" => 3,
449            // Most currencies use 2 decimal places
450            _ => 2,
451        }
452    }
453
454    /// Check if the amount is a high-value transaction
455    ///
456    /// Determines if the original instructed amount exceeds typical
457    /// high-value thresholds that may require special handling or reporting.
458    ///
459    /// # Returns
460    /// `true` if this is considered a high-value transaction
461    ///
462    /// # Example
463    /// ```rust
464    /// # use swift_mt_message::fields::Field33B;
465    /// let high_value = Field33B::new("USD", 1500000.00).unwrap();
466    /// assert!(high_value.is_high_value_transaction());
467    ///
468    /// let normal_value = Field33B::new("USD", 50000.00).unwrap();
469    /// assert!(!normal_value.is_high_value_transaction());
470    /// ```
471    pub fn is_high_value_transaction(&self) -> bool {
472        // High-value thresholds vary by currency
473        let threshold = match self.currency.as_str() {
474            "USD" | "EUR" | "GBP" | "CHF" | "CAD" | "AUD" => 1_000_000.0,
475            "JPY" => 100_000_000.0,
476            "SEK" | "NOK" | "DKK" => 10_000_000.0,
477            _ => 1_000_000.0, // Default threshold
478        };
479
480        self.amount >= threshold
481    }
482
483    /// Check if this represents a currency conversion scenario
484    ///
485    /// This method would typically be used in conjunction with Field 32A
486    /// to determine if the transaction involves currency conversion.
487    ///
488    /// # Arguments
489    /// * `settlement_currency` - Currency from Field 32A for comparison
490    ///
491    /// # Returns
492    /// `true` if currencies differ, indicating conversion
493    ///
494    /// # Example
495    /// ```rust
496    /// # use swift_mt_message::fields::Field33B;
497    /// let field = Field33B::new("EUR", 100000.00).unwrap();
498    /// assert!(field.is_currency_conversion("USD"));
499    /// assert!(!field.is_currency_conversion("EUR"));
500    /// ```
501    pub fn is_currency_conversion(&self, settlement_currency: &str) -> bool {
502        self.currency != settlement_currency.to_uppercase()
503    }
504
505    /// Calculate potential FX exposure
506    ///
507    /// Estimates the foreign exchange exposure based on the original
508    /// amount and currency. This is useful for risk management purposes.
509    ///
510    /// # Returns
511    /// Exposure category as string
512    ///
513    /// # Example
514    /// ```rust
515    /// # use swift_mt_message::fields::Field33B;
516    /// let field = Field33B::new("USD", 2000000.00).unwrap();
517    /// assert_eq!(field.fx_exposure_category(), "High");
518    /// ```
519    pub fn fx_exposure_category(&self) -> &'static str {
520        if self.is_high_value_transaction() {
521            if self.is_major_currency() {
522                "High"
523            } else {
524                "Very High" // High value in exotic currency
525            }
526        } else if self.is_major_currency() {
527            "Low"
528        } else {
529            "Medium" // Exotic currency but lower amount
530        }
531    }
532
533    /// Format amount with proper currency precision
534    ///
535    /// Formats the amount according to the typical precision
536    /// rules for the currency.
537    ///
538    /// # Returns
539    /// Formatted amount string
540    ///
541    /// # Example
542    /// ```rust
543    /// # use swift_mt_message::fields::Field33B;
544    /// let usd_field = Field33B::new("USD", 1234.56).unwrap();
545    /// assert_eq!(usd_field.formatted_amount(), "1234.56");
546    ///
547    /// let jpy_field = Field33B::new("JPY", 1234.00).unwrap();
548    /// assert_eq!(jpy_field.formatted_amount(), "1234");
549    /// ```
550    pub fn formatted_amount(&self) -> String {
551        let decimal_places = self.decimal_places();
552        match decimal_places {
553            0 => format!("{:.0}", self.amount),
554            2 => format!("{:.2}", self.amount),
555            3 => format!("{:.3}", self.amount),
556            _ => format!("{:.2}", self.amount), // Default to 2
557        }
558    }
559
560    /// Get transaction purpose classification
561    ///
562    /// Provides a classification of the likely transaction purpose
563    /// based on amount and currency characteristics.
564    ///
565    /// # Returns
566    /// Transaction purpose category
567    ///
568    /// # Example
569    /// ```rust
570    /// # use swift_mt_message::fields::Field33B;
571    /// let field = Field33B::new("USD", 50000.00).unwrap();
572    /// let purpose = field.transaction_purpose();
573    /// assert!(!purpose.is_empty());
574    /// ```
575    pub fn transaction_purpose(&self) -> &'static str {
576        if self.is_high_value_transaction() {
577            if self.is_major_currency() {
578                "Corporate/Institutional Transfer"
579            } else {
580                "High-Value Cross-Border Transfer"
581            }
582        } else if self.amount < 10000.0 {
583            "Personal/Retail Transfer"
584        } else if self.is_major_currency() {
585            "Commercial Transfer"
586        } else {
587            "Cross-Border Commercial Transfer"
588        }
589    }
590
591    /// Get human-readable description
592    ///
593    /// Returns a comprehensive description of the original instructed
594    /// amount including currency, amount, and transaction characteristics.
595    ///
596    /// # Returns
597    /// Formatted description string
598    ///
599    /// # Example
600    /// ```rust
601    /// # use swift_mt_message::fields::Field33B;
602    /// let field = Field33B::new("EUR", 100000.00).unwrap();
603    /// let desc = field.description();
604    /// assert!(desc.contains("EUR"));
605    /// assert!(desc.contains("100000.00"));
606    /// ```
607    pub fn description(&self) -> String {
608        format!(
609            "Currency/Instructed Amount: {} {:.2}",
610            self.currency, self.amount
611        )
612    }
613
614    /// Get comprehensive transaction analysis
615    ///
616    /// Returns a detailed analysis of the transaction including currency
617    /// characteristics, amount classification, and risk assessment.
618    ///
619    /// # Returns
620    /// Formatted analysis string
621    ///
622    /// # Example
623    /// ```rust
624    /// # use swift_mt_message::fields::Field33B;
625    /// let field = Field33B::new("USD", 1500000.00).unwrap();
626    /// let analysis = field.comprehensive_analysis();
627    /// assert!(analysis.contains("USD"));
628    /// assert!(analysis.contains("High-value"));
629    /// assert!(analysis.contains("Major currency"));
630    /// ```
631    pub fn comprehensive_analysis(&self) -> String {
632        let currency_type = if self.is_major_currency() {
633            "Major currency"
634        } else {
635            "Other currency"
636        };
637
638        let amount_category = if self.is_high_value_transaction() {
639            "High-value"
640        } else {
641            "Standard"
642        };
643
644        let fx_exposure = self.fx_exposure_category();
645        let purpose = self.transaction_purpose();
646
647        format!(
648            "Original Amount: {} {} ({}) | Category: {} | FX Exposure: {} | Purpose: {}",
649            self.formatted_amount(),
650            self.currency,
651            currency_type,
652            amount_category,
653            fx_exposure,
654            purpose
655        )
656    }
657}
658
659impl SwiftField for Field33B {
660    fn parse(value: &str) -> Result<Self, crate::ParseError> {
661        let content = if let Some(stripped) = value.strip_prefix(":33B:") {
662            stripped // Remove ":33B:" prefix
663        } else if let Some(stripped) = value.strip_prefix("33B:") {
664            stripped // Remove "33B:" prefix
665        } else {
666            value
667        };
668
669        let content = content.trim();
670
671        if content.len() < 4 {
672            return Err(crate::ParseError::InvalidFieldFormat {
673                field_tag: "33B".to_string(),
674                message: "Field content too short (minimum 4 characters: CCCAMOUNT)".to_string(),
675            });
676        }
677
678        // Parse components: first 3 characters are currency, rest is amount
679        let currency_str = &content[0..3];
680        let amount_str = &content[3..];
681
682        let currency = currency_str.to_uppercase();
683
684        // Validate currency
685        if !currency.chars().all(|c| c.is_alphabetic() && c.is_ascii()) {
686            return Err(crate::ParseError::InvalidFieldFormat {
687                field_tag: "33B".to_string(),
688                message: "Currency code must contain only alphabetic characters".to_string(),
689            });
690        }
691
692        let amount = Self::parse_amount(amount_str)?;
693
694        if amount < 0.0 {
695            return Err(crate::ParseError::InvalidFieldFormat {
696                field_tag: "33B".to_string(),
697                message: "Amount cannot be negative".to_string(),
698            });
699        }
700
701        Ok(Field33B {
702            currency,
703            amount,
704            raw_amount: amount_str.to_string(),
705        })
706    }
707
708    fn to_swift_string(&self) -> String {
709        format!(":33B:{}{}", self.currency, self.raw_amount)
710    }
711
712    fn validate(&self) -> ValidationResult {
713        let mut errors = Vec::new();
714
715        // Validate currency code
716        if self.currency.len() != 3 {
717            errors.push(ValidationError::LengthValidation {
718                field_tag: "33B".to_string(),
719                expected: "3 characters".to_string(),
720                actual: self.currency.len(),
721            });
722        }
723
724        if !self
725            .currency
726            .chars()
727            .all(|c| c.is_alphabetic() && c.is_ascii())
728        {
729            errors.push(ValidationError::FormatValidation {
730                field_tag: "33B".to_string(),
731                message: "Currency code must contain only alphabetic characters".to_string(),
732            });
733        }
734
735        // Validate amount
736        if self.amount < 0.0 {
737            errors.push(ValidationError::ValueValidation {
738                field_tag: "33B".to_string(),
739                message: "Amount cannot be negative".to_string(),
740            });
741        }
742
743        // Validate raw amount format
744        if self.raw_amount.is_empty() {
745            errors.push(ValidationError::ValueValidation {
746                field_tag: "33B".to_string(),
747                message: "Amount cannot be empty".to_string(),
748            });
749        }
750
751        ValidationResult {
752            is_valid: errors.is_empty(),
753            errors,
754            warnings: Vec::new(),
755        }
756    }
757
758    fn format_spec() -> &'static str {
759        "3!a15d"
760    }
761}
762
763impl std::fmt::Display for Field33B {
764    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
765        write!(f, "{} {}", self.currency, self.raw_amount)
766    }
767}
768
769#[cfg(test)]
770mod tests {
771    use super::*;
772
773    #[test]
774    fn test_field33b_creation() {
775        let field = Field33B::new("USD", 1234.56).unwrap();
776        assert_eq!(field.currency(), "USD");
777        assert_eq!(field.amount(), 1234.56);
778        assert_eq!(field.raw_amount(), "1234,56");
779    }
780
781    #[test]
782    fn test_field33b_from_raw() {
783        let field = Field33B::from_raw("EUR", "999,99").unwrap();
784        assert_eq!(field.currency(), "EUR");
785        assert_eq!(field.amount(), 999.99);
786        assert_eq!(field.raw_amount(), "999,99");
787    }
788
789    #[test]
790    fn test_field33b_parse() {
791        let field = Field33B::parse("USD1234,56").unwrap();
792        assert_eq!(field.currency(), "USD");
793        assert_eq!(field.amount(), 1234.56);
794        assert_eq!(field.raw_amount(), "1234,56");
795    }
796
797    #[test]
798    fn test_field33b_parse_with_prefix() {
799        let field = Field33B::parse(":33B:EUR500,00").unwrap();
800        assert_eq!(field.currency(), "EUR");
801        assert_eq!(field.amount(), 500.0);
802        assert_eq!(field.raw_amount(), "500,00");
803    }
804
805    #[test]
806    fn test_field33b_to_swift_string() {
807        let field = Field33B::new("GBP", 750.25).unwrap();
808        assert_eq!(field.to_swift_string(), ":33B:GBP750,25");
809    }
810
811    #[test]
812    fn test_field33b_invalid_currency_length() {
813        let result = Field33B::new("US", 100.0);
814        assert!(result.is_err());
815
816        let result = Field33B::new("USDD", 100.0);
817        assert!(result.is_err());
818    }
819
820    #[test]
821    fn test_field33b_invalid_currency_characters() {
822        let result = Field33B::new("U$D", 100.0);
823        assert!(result.is_err());
824
825        let result = Field33B::new("123", 100.0);
826        assert!(result.is_err());
827    }
828
829    #[test]
830    fn test_field33b_negative_amount() {
831        let result = Field33B::new("USD", -100.0);
832        assert!(result.is_err());
833    }
834
835    #[test]
836    fn test_field33b_parse_invalid_format() {
837        let result = Field33B::parse("USD");
838        assert!(result.is_err());
839
840        let result = Field33B::parse("US1234,56");
841        assert!(result.is_err());
842    }
843
844    #[test]
845    fn test_field33b_validation() {
846        let field = Field33B::new("USD", 1000.0).unwrap();
847        let validation = field.validate();
848        assert!(validation.is_valid);
849        assert!(validation.errors.is_empty());
850    }
851
852    #[test]
853    fn test_field33b_display() {
854        let field = Field33B::new("CHF", 2500.75).unwrap();
855        assert_eq!(format!("{}", field), "CHF 2500,75");
856    }
857
858    #[test]
859    fn test_field33b_is_valid_currency() {
860        let field = Field33B::new("USD", 100.0).unwrap();
861        assert!(field.is_valid_currency());
862    }
863
864    #[test]
865    fn test_field33b_description() {
866        let field = Field33B::new("EUR", 1500.0).unwrap();
867        assert_eq!(
868            field.description(),
869            "Currency/Instructed Amount: EUR 1500.00"
870        );
871    }
872
873    #[test]
874    fn test_field33b_parse_dot_decimal() {
875        let field = Field33B::parse("USD1234.56").unwrap();
876        assert_eq!(field.amount(), 1234.56);
877        assert_eq!(field.raw_amount(), "1234.56");
878    }
879
880    #[test]
881    fn test_field33b_major_currencies() {
882        let major_currencies = [
883            "USD", "EUR", "GBP", "JPY", "CHF", "CAD", "AUD", "NZD", "SEK", "NOK", "DKK",
884        ];
885
886        for currency in major_currencies {
887            let field = Field33B::new(currency, 1000.00).unwrap();
888            assert!(
889                field.is_major_currency(),
890                "Currency {} should be major currency",
891                currency
892            );
893        }
894
895        let exotic_currencies = ["XYZ", "ABC", "THB", "MXN"];
896        for currency in exotic_currencies {
897            let field = Field33B::new(currency, 1000.00).unwrap();
898            assert!(
899                !field.is_major_currency(),
900                "Currency {} should not be major currency",
901                currency
902            );
903        }
904    }
905
906    #[test]
907    fn test_field33b_decimal_places() {
908        // Currencies with no decimal places
909        let no_decimal_currencies = ["JPY", "KRW", "VND"];
910        for currency in no_decimal_currencies {
911            let field = Field33B::new(currency, 1000.00).unwrap();
912            assert_eq!(
913                field.decimal_places(),
914                0,
915                "Currency {} should have 0 decimal places",
916                currency
917            );
918            assert!(!field.has_decimal_places());
919        }
920
921        // Currencies with 3 decimal places
922        let three_decimal_currencies = ["BHD", "JOD", "KWD"];
923        for currency in three_decimal_currencies {
924            let field = Field33B::new(currency, 1000.00).unwrap();
925            assert_eq!(
926                field.decimal_places(),
927                3,
928                "Currency {} should have 3 decimal places",
929                currency
930            );
931            assert!(field.has_decimal_places());
932        }
933
934        // Currencies with 2 decimal places (most common)
935        let two_decimal_currencies = ["USD", "EUR", "GBP"];
936        for currency in two_decimal_currencies {
937            let field = Field33B::new(currency, 1000.00).unwrap();
938            assert_eq!(
939                field.decimal_places(),
940                2,
941                "Currency {} should have 2 decimal places",
942                currency
943            );
944            assert!(field.has_decimal_places());
945        }
946    }
947
948    #[test]
949    fn test_field33b_high_value_transactions() {
950        // High-value USD transaction
951        let high_usd = Field33B::new("USD", 1_500_000.00).unwrap();
952        assert!(high_usd.is_high_value_transaction());
953
954        let normal_usd = Field33B::new("USD", 500_000.00).unwrap();
955        assert!(!normal_usd.is_high_value_transaction());
956
957        // High-value JPY transaction (different threshold)
958        let high_jpy = Field33B::new("JPY", 150_000_000.00).unwrap();
959        assert!(high_jpy.is_high_value_transaction());
960
961        let normal_jpy = Field33B::new("JPY", 50_000_000.00).unwrap();
962        assert!(!normal_jpy.is_high_value_transaction());
963    }
964
965    #[test]
966    fn test_field33b_currency_conversion() {
967        let field = Field33B::new("EUR", 100000.00).unwrap();
968
969        // Different currencies indicate conversion
970        assert!(field.is_currency_conversion("USD"));
971        assert!(field.is_currency_conversion("GBP"));
972
973        // Same currency indicates no conversion
974        assert!(!field.is_currency_conversion("EUR"));
975        assert!(!field.is_currency_conversion("eur")); // Case insensitive
976    }
977
978    #[test]
979    fn test_field33b_fx_exposure_category() {
980        // High value, major currency
981        let high_major = Field33B::new("USD", 2_000_000.00).unwrap();
982        assert_eq!(high_major.fx_exposure_category(), "High");
983
984        // High value, exotic currency
985        let high_exotic = Field33B::new("THB", 2_000_000.00).unwrap();
986        assert_eq!(high_exotic.fx_exposure_category(), "Very High");
987
988        // Low value, major currency
989        let low_major = Field33B::new("EUR", 50_000.00).unwrap();
990        assert_eq!(low_major.fx_exposure_category(), "Low");
991
992        // Low value, exotic currency
993        let low_exotic = Field33B::new("MXN", 50_000.00).unwrap();
994        assert_eq!(low_exotic.fx_exposure_category(), "Medium");
995    }
996
997    #[test]
998    fn test_field33b_formatted_amount() {
999        // USD with 2 decimal places
1000        let usd_field = Field33B::new("USD", 1234.56).unwrap();
1001        assert_eq!(usd_field.formatted_amount(), "1234.56");
1002
1003        // JPY with 0 decimal places
1004        let jpy_field = Field33B::new("JPY", 1234.00).unwrap();
1005        assert_eq!(jpy_field.formatted_amount(), "1234");
1006
1007        // KWD with 3 decimal places
1008        let kwd_field = Field33B::new("KWD", 1234.567).unwrap();
1009        assert_eq!(kwd_field.formatted_amount(), "1234.567");
1010    }
1011
1012    #[test]
1013    fn test_field33b_transaction_purpose() {
1014        // High-value major currency
1015        let corporate = Field33B::new("USD", 2_000_000.00).unwrap();
1016        assert_eq!(
1017            corporate.transaction_purpose(),
1018            "Corporate/Institutional Transfer"
1019        );
1020
1021        // High-value exotic currency
1022        let cross_border_high = Field33B::new("THB", 2_000_000.00).unwrap();
1023        assert_eq!(
1024            cross_border_high.transaction_purpose(),
1025            "High-Value Cross-Border Transfer"
1026        );
1027
1028        // Small amount
1029        let personal = Field33B::new("USD", 5_000.00).unwrap();
1030        assert_eq!(personal.transaction_purpose(), "Personal/Retail Transfer");
1031
1032        // Medium amount, major currency
1033        let commercial = Field33B::new("EUR", 50_000.00).unwrap();
1034        assert_eq!(commercial.transaction_purpose(), "Commercial Transfer");
1035
1036        // Medium amount, exotic currency
1037        let cross_border_commercial = Field33B::new("MXN", 50_000.00).unwrap();
1038        assert_eq!(
1039            cross_border_commercial.transaction_purpose(),
1040            "Cross-Border Commercial Transfer"
1041        );
1042    }
1043
1044    #[test]
1045    fn test_field33b_comprehensive_analysis() {
1046        let field = Field33B::new("USD", 1_500_000.00).unwrap();
1047        let analysis = field.comprehensive_analysis();
1048
1049        assert!(analysis.contains("USD"));
1050        assert!(analysis.contains("1500000.00"));
1051        assert!(analysis.contains("Major currency"));
1052        assert!(analysis.contains("High-value"));
1053        assert!(analysis.contains("High")); // FX exposure
1054        assert!(analysis.contains("Corporate/Institutional Transfer"));
1055    }
1056
1057    #[test]
1058    fn test_field33b_format_amount_static() {
1059        assert_eq!(Field33B::format_amount(1234.56), "1234,56");
1060        assert_eq!(Field33B::format_amount(100.00), "100,00");
1061        assert_eq!(Field33B::format_amount(0.01), "0,01");
1062    }
1063
1064    #[test]
1065    fn test_field33b_serialization() {
1066        let field = Field33B::new("EUR", 1234.56).unwrap();
1067        let serialized = serde_json::to_string(&field).unwrap();
1068        let deserialized: Field33B = serde_json::from_str(&serialized).unwrap();
1069
1070        assert_eq!(field.currency(), deserialized.currency());
1071        assert_eq!(field.amount(), deserialized.amount());
1072        assert_eq!(field.raw_amount(), deserialized.raw_amount());
1073        assert_eq!(field.is_major_currency(), deserialized.is_major_currency());
1074    }
1075
1076    #[test]
1077    fn test_field33b_business_logic_combinations() {
1078        // Major currency, high-value transaction
1079        let field = Field33B::new("USD", 2_000_000.00).unwrap();
1080        assert!(field.is_major_currency());
1081        assert!(field.is_high_value_transaction());
1082        assert_eq!(field.fx_exposure_category(), "High");
1083        assert_eq!(
1084            field.transaction_purpose(),
1085            "Corporate/Institutional Transfer"
1086        );
1087        assert_eq!(field.decimal_places(), 2);
1088
1089        // Exotic currency, normal value transaction
1090        let field = Field33B::new("THB", 100_000.00).unwrap();
1091        assert!(!field.is_major_currency());
1092        assert!(!field.is_high_value_transaction());
1093        assert_eq!(field.fx_exposure_category(), "Medium");
1094        assert_eq!(
1095            field.transaction_purpose(),
1096            "Cross-Border Commercial Transfer"
1097        );
1098        assert_eq!(field.decimal_places(), 2);
1099    }
1100
1101    #[test]
1102    fn test_field33b_edge_cases() {
1103        // Zero amount (allowed)
1104        let zero_field = Field33B::new("USD", 0.0).unwrap();
1105        assert_eq!(zero_field.amount(), 0.0);
1106        assert!(!zero_field.is_high_value_transaction());
1107
1108        // Very small amount
1109        let small_field = Field33B::new("USD", 0.01).unwrap();
1110        assert_eq!(small_field.amount(), 0.01);
1111        assert_eq!(
1112            small_field.transaction_purpose(),
1113            "Personal/Retail Transfer"
1114        );
1115
1116        // Very large amount
1117        let large_field = Field33B::new("USD", 999_999_999.99).unwrap();
1118        assert!(large_field.is_high_value_transaction());
1119        assert_eq!(
1120            large_field.transaction_purpose(),
1121            "Corporate/Institutional Transfer"
1122        );
1123    }
1124
1125    #[test]
1126    fn test_field33b_real_world_scenarios() {
1127        // Scenario 1: FX conversion (EUR to USD)
1128        let fx_conversion = Field33B::new("EUR", 100_000.00).unwrap();
1129        assert!(fx_conversion.is_currency_conversion("USD"));
1130        assert!(fx_conversion.is_major_currency());
1131        assert!(!fx_conversion.is_high_value_transaction());
1132        assert_eq!(fx_conversion.transaction_purpose(), "Commercial Transfer");
1133
1134        // Scenario 2: High-value corporate transfer
1135        let corporate_transfer = Field33B::new("USD", 5_000_000.00).unwrap();
1136        assert!(corporate_transfer.is_high_value_transaction());
1137        assert_eq!(corporate_transfer.fx_exposure_category(), "High");
1138        assert_eq!(
1139            corporate_transfer.transaction_purpose(),
1140            "Corporate/Institutional Transfer"
1141        );
1142
1143        // Scenario 3: Personal remittance
1144        let remittance = Field33B::new("USD", 2_500.00).unwrap();
1145        assert_eq!(remittance.transaction_purpose(), "Personal/Retail Transfer");
1146        assert_eq!(remittance.fx_exposure_category(), "Low");
1147
1148        // Scenario 4: Exotic currency transaction
1149        let exotic_transfer = Field33B::new("THB", 3_000_000.00).unwrap();
1150        assert!(!exotic_transfer.is_major_currency());
1151        assert!(exotic_transfer.is_high_value_transaction());
1152        assert_eq!(exotic_transfer.fx_exposure_category(), "Very High");
1153        assert_eq!(
1154            exotic_transfer.transaction_purpose(),
1155            "High-Value Cross-Border Transfer"
1156        );
1157    }
1158
1159    #[test]
1160    fn test_field33b_currency_specific_behavior() {
1161        let test_cases = [
1162            ("USD", true, 2, 1_000_000.0),
1163            ("EUR", true, 2, 1_000_000.0),
1164            ("JPY", true, 0, 100_000_000.0),
1165            ("KWD", false, 3, 1_000_000.0),
1166            ("THB", false, 2, 1_000_000.0),
1167        ];
1168
1169        for (currency, is_major, decimal_places, high_value_threshold) in test_cases {
1170            let field = Field33B::new(currency, 1000.00).unwrap();
1171
1172            assert_eq!(
1173                field.is_major_currency(),
1174                is_major,
1175                "Major currency check failed for {}",
1176                currency
1177            );
1178            assert_eq!(
1179                field.decimal_places(),
1180                decimal_places,
1181                "Decimal places check failed for {}",
1182                currency
1183            );
1184
1185            // Test high-value threshold
1186            let high_value_field = Field33B::new(currency, high_value_threshold).unwrap();
1187            assert!(
1188                high_value_field.is_high_value_transaction(),
1189                "High value threshold check failed for {}",
1190                currency
1191            );
1192        }
1193    }
1194
1195    #[test]
1196    fn test_field33b_cross_field_integration() {
1197        // Test scenarios that would typically involve Field 32A integration
1198        let field33b = Field33B::new("EUR", 100_000.00).unwrap();
1199
1200        // Currency conversion scenario
1201        assert!(field33b.is_currency_conversion("USD"));
1202        assert!(!field33b.is_currency_conversion("EUR"));
1203
1204        // Charge deduction scenario (same currency, different amount)
1205        assert!(!field33b.is_currency_conversion("EUR"));
1206        // In real usage, amount comparison with Field 32A would show charge deduction
1207
1208        // Multi-currency analysis
1209        let analysis = field33b.comprehensive_analysis();
1210        assert!(analysis.contains("EUR"));
1211        assert!(analysis.contains("Major currency"));
1212        assert!(analysis.contains("Standard")); // Not high-value
1213        assert!(analysis.contains("Low")); // FX exposure for major currency, standard amount
1214    }
1215}