swift_mt_message/fields/field71g.rs
1use crate::{SwiftField, ValidationError, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// # Field 71G: Receiver's Charges
5///
6/// ## Overview
7/// Field 71G specifies the charges borne by the receiver in SWIFT payment messages. This field
8/// contains the currency and amount of charges that the beneficiary or receiving institution
9/// pays for processing the payment transaction. These charges are deducted from the payment
10/// amount or billed separately, providing transparency in fee allocation and supporting
11/// accurate payment reconciliation and regulatory compliance requirements.
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/// - **Decimal separator**: Comma (,) as per SWIFT standards
18/// - **Amount format**: No thousands separators, up to 2 decimal places
19///
20/// ## Structure
21/// ```text
22/// EUR12,75
23/// │││└──┘
24/// │││ └─ Amount (12.75)
25/// └┴┴─── Currency (EUR)
26/// ```
27///
28/// ## Field Components
29/// - **Currency Code**: ISO 4217 three-letter currency code
30/// - Must be valid and recognized currency
31/// - Alphabetic characters only
32/// - Case-insensitive but normalized to uppercase
33/// - **Charge Amount**: Monetary amount of receiver's charges
34/// - Maximum 15 digits including decimal places
35/// - Comma as decimal separator
36/// - Non-negative values only
37///
38/// ## Usage Context
39/// Field 71G is used in:
40/// - **MT103**: Single Customer Credit Transfer
41/// - **MT200**: Financial Institution Transfer
42/// - **MT202**: General Financial Institution Transfer
43/// - **MT202COV**: Cover for customer credit transfer
44/// - **MT205**: Financial Institution Transfer for its own account
45///
46/// ### Business Applications
47/// - **Charge transparency**: Detailed fee disclosure for receivers
48/// - **Payment reconciliation**: Accurate net amount calculation
49/// - **Correspondent banking**: Fee settlement with receiving banks
50/// - **Regulatory compliance**: Charge reporting and disclosure
51/// - **Customer communication**: Clear fee breakdown for beneficiaries
52/// - **Audit trails**: Complete transaction cost documentation
53///
54/// ## Examples
55/// ```text
56/// :71G:EUR12,75
57/// └─── EUR 12.75 in receiver's charges
58///
59/// :71G:USD20,00
60/// └─── USD 20.00 in beneficiary bank fees
61///
62/// :71G:GBP8,50
63/// └─── GBP 8.50 in receiving charges
64///
65/// :71G:CHF15,25
66/// └─── CHF 15.25 in processing fees
67///
68/// :71G:JPY1500,00
69/// └─── JPY 1,500.00 in local charges
70/// ```
71///
72/// ## Charge Types
73/// - **Receiving fees**: Basic charges for incoming payments
74/// - **Processing charges**: Fees for payment processing and crediting
75/// - **Correspondent fees**: Charges from correspondent banking arrangements
76/// - **Regulatory fees**: Compliance and reporting related charges
77/// - **Investigation fees**: Charges for payment inquiries or research
78/// - **Account maintenance**: Fees related to account services
79///
80/// ## Currency Guidelines
81/// - **ISO 4217 compliance**: Must use standard currency codes
82/// - **Local currency**: Often in receiving country's currency
83/// - **Payment currency**: May match main payment currency
84/// - **Active currencies**: Should use currently active currency codes
85/// - **Consistency**: Should align with local banking practices
86///
87/// ## Amount Calculation
88/// - **Deduction method**: Typically deducted from payment amount
89/// - **Separate billing**: May be billed separately to beneficiary
90/// - **Net amount**: Payment amount minus receiver's charges
91/// - **Currency conversion**: May involve currency conversion costs
92/// - **Rate application**: Applied at current exchange rates
93///
94/// ## Validation Rules
95/// 1. **Currency format**: Must be exactly 3 alphabetic characters
96/// 2. **Currency validity**: Must be valid ISO 4217 currency code
97/// 3. **Amount format**: Must follow SWIFT decimal format with comma
98/// 4. **Amount range**: Must be non-negative
99/// 5. **Length limits**: Total field length within SWIFT limits
100/// 6. **Character validation**: Only allowed characters in amount
101///
102/// ## Network Validated Rules (SWIFT Standards)
103/// - Currency must be valid ISO 4217 code (Error: T52)
104/// - Amount must be properly formatted (Error: T40)
105/// - Amount cannot be negative (Error: T13)
106/// - Decimal separator must be comma (Error: T41)
107/// - Maximum 15 digits in amount (Error: T50)
108/// - Currency must be alphabetic only (Error: T15)
109/// - Field format must comply with specification (Error: T26)
110///
111
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
113pub struct Field71G {
114 /// Currency code (3 letters, ISO 4217)
115 pub currency: String,
116 /// Charge amount
117 pub amount: f64,
118 /// Raw amount string as received (preserves original formatting)
119 pub raw_amount: String,
120}
121
122impl Field71G {
123 /// Create a new Field71G with validation
124 pub fn new(currency: impl Into<String>, amount: f64) -> Result<Self, crate::ParseError> {
125 let currency = currency.into().to_uppercase();
126
127 // Validate currency code
128 if currency.len() != 3 {
129 return Err(crate::ParseError::InvalidFieldFormat {
130 field_tag: "71G".to_string(),
131 message: "Currency code must be exactly 3 characters".to_string(),
132 });
133 }
134
135 if !currency.chars().all(|c| c.is_alphabetic() && c.is_ascii()) {
136 return Err(crate::ParseError::InvalidFieldFormat {
137 field_tag: "71G".to_string(),
138 message: "Currency code must contain only alphabetic characters".to_string(),
139 });
140 }
141
142 // Validate amount
143 if amount < 0.0 {
144 return Err(crate::ParseError::InvalidFieldFormat {
145 field_tag: "71G".to_string(),
146 message: "Charge amount cannot be negative".to_string(),
147 });
148 }
149
150 let raw_amount = Self::format_amount(amount);
151
152 Ok(Field71G {
153 currency,
154 amount,
155 raw_amount,
156 })
157 }
158
159 /// Create from raw amount string
160 pub fn from_raw(
161 currency: impl Into<String>,
162 raw_amount: impl Into<String>,
163 ) -> Result<Self, crate::ParseError> {
164 let currency = currency.into().to_uppercase();
165 let raw_amount = raw_amount.into();
166
167 let amount = Self::parse_amount(&raw_amount)?;
168
169 Ok(Field71G {
170 currency,
171 amount,
172 raw_amount: raw_amount.to_string(),
173 })
174 }
175
176 /// Get the currency code
177 pub fn currency(&self) -> &str {
178 &self.currency
179 }
180
181 /// Get the charge amount
182 pub fn amount(&self) -> f64 {
183 self.amount
184 }
185
186 /// Get the raw amount string
187 pub fn raw_amount(&self) -> &str {
188 &self.raw_amount
189 }
190
191 /// Format amount for SWIFT output (with comma as decimal separator)
192 pub fn format_amount(amount: f64) -> String {
193 format!("{:.2}", amount).replace('.', ",")
194 }
195
196 /// Parse amount from string (handles both comma and dot as decimal separator)
197 fn parse_amount(amount_str: &str) -> Result<f64, crate::ParseError> {
198 let normalized_amount = amount_str.replace(',', ".");
199
200 normalized_amount
201 .parse::<f64>()
202 .map_err(|_| crate::ParseError::InvalidFieldFormat {
203 field_tag: "71G".to_string(),
204 message: "Invalid charge amount format".to_string(),
205 })
206 }
207
208 /// Get human-readable description
209 pub fn description(&self) -> String {
210 format!("Receiver's Charges: {} {}", self.currency, self.raw_amount)
211 }
212}
213
214impl SwiftField for Field71G {
215 fn parse(value: &str) -> Result<Self, crate::ParseError> {
216 let content = if let Some(stripped) = value.strip_prefix(":71G:") {
217 stripped // Remove ":71G:" prefix
218 } else if let Some(stripped) = value.strip_prefix("71G:") {
219 stripped // Remove "71G:" prefix
220 } else {
221 value
222 };
223
224 let content = content.trim();
225
226 if content.len() < 4 {
227 return Err(crate::ParseError::InvalidFieldFormat {
228 field_tag: "71G".to_string(),
229 message: "Field content too short (minimum 4 characters: CCCAMOUNT)".to_string(),
230 });
231 }
232
233 // Parse components: first 3 characters are currency, rest is amount
234 let currency_str = &content[0..3];
235 let amount_str = &content[3..];
236
237 let currency = currency_str.to_uppercase();
238
239 // Validate currency
240 if !currency.chars().all(|c| c.is_alphabetic() && c.is_ascii()) {
241 return Err(crate::ParseError::InvalidFieldFormat {
242 field_tag: "71G".to_string(),
243 message: "Currency code must contain only alphabetic characters".to_string(),
244 });
245 }
246
247 let amount = Self::parse_amount(amount_str)?;
248
249 if amount < 0.0 {
250 return Err(crate::ParseError::InvalidFieldFormat {
251 field_tag: "71G".to_string(),
252 message: "Charge amount cannot be negative".to_string(),
253 });
254 }
255
256 Ok(Field71G {
257 currency,
258 amount,
259 raw_amount: amount_str.to_string(),
260 })
261 }
262
263 fn to_swift_string(&self) -> String {
264 format!(":71G:{}{}", self.currency, self.raw_amount)
265 }
266
267 fn validate(&self) -> ValidationResult {
268 let mut errors = Vec::new();
269
270 // Validate currency code
271 if self.currency.len() != 3 {
272 errors.push(ValidationError::LengthValidation {
273 field_tag: "71G".to_string(),
274 expected: "3 characters".to_string(),
275 actual: self.currency.len(),
276 });
277 }
278
279 if !self
280 .currency
281 .chars()
282 .all(|c| c.is_alphabetic() && c.is_ascii())
283 {
284 errors.push(ValidationError::FormatValidation {
285 field_tag: "71G".to_string(),
286 message: "Currency code must contain only alphabetic characters".to_string(),
287 });
288 }
289
290 // Validate amount
291 if self.amount < 0.0 {
292 errors.push(ValidationError::ValueValidation {
293 field_tag: "71G".to_string(),
294 message: "Charge amount cannot be negative".to_string(),
295 });
296 }
297
298 // Validate raw amount format
299 if self.raw_amount.is_empty() {
300 errors.push(ValidationError::ValueValidation {
301 field_tag: "71G".to_string(),
302 message: "Charge amount cannot be empty".to_string(),
303 });
304 }
305
306 ValidationResult {
307 is_valid: errors.is_empty(),
308 errors,
309 warnings: Vec::new(),
310 }
311 }
312
313 fn format_spec() -> &'static str {
314 "3!a15d"
315 }
316}
317
318impl std::fmt::Display for Field71G {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 write!(f, "{} {}", self.currency, self.raw_amount)
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn test_field71g_creation() {
330 let field = Field71G::new("USD", 10.50).unwrap();
331 assert_eq!(field.currency(), "USD");
332 assert_eq!(field.amount(), 10.50);
333 assert_eq!(field.raw_amount(), "10,50");
334 }
335
336 #[test]
337 fn test_field71g_from_raw() {
338 let field = Field71G::from_raw("EUR", "25,75").unwrap();
339 assert_eq!(field.currency(), "EUR");
340 assert_eq!(field.amount(), 25.75);
341 assert_eq!(field.raw_amount(), "25,75");
342 }
343
344 #[test]
345 fn test_field71g_parse() {
346 let field = Field71G::parse("USD15,00").unwrap();
347 assert_eq!(field.currency(), "USD");
348 assert_eq!(field.amount(), 15.0);
349 assert_eq!(field.raw_amount(), "15,00");
350 }
351
352 #[test]
353 fn test_field71g_parse_with_prefix() {
354 let field = Field71G::parse(":71G:GBP5,25").unwrap();
355 assert_eq!(field.currency(), "GBP");
356 assert_eq!(field.amount(), 5.25);
357 assert_eq!(field.raw_amount(), "5,25");
358 }
359
360 #[test]
361 fn test_field71g_to_swift_string() {
362 let field = Field71G::new("CHF", 100.0).unwrap();
363 assert_eq!(field.to_swift_string(), ":71G:CHF100,00");
364 }
365
366 #[test]
367 fn test_field71g_invalid_currency() {
368 let result = Field71G::new("US", 10.0);
369 assert!(result.is_err());
370
371 let result = Field71G::new("123", 10.0);
372 assert!(result.is_err());
373 }
374
375 #[test]
376 fn test_field71g_negative_amount() {
377 let result = Field71G::new("USD", -10.0);
378 assert!(result.is_err());
379 }
380
381 #[test]
382 fn test_field71g_validation() {
383 let field = Field71G::new("USD", 50.0).unwrap();
384 let validation = field.validate();
385 assert!(validation.is_valid);
386 assert!(validation.errors.is_empty());
387 }
388
389 #[test]
390 fn test_field71g_display() {
391 let field = Field71G::new("EUR", 75.50).unwrap();
392 assert_eq!(format!("{}", field), "EUR 75,50");
393 }
394
395 #[test]
396 fn test_field71g_description() {
397 let field = Field71G::new("USD", 20.0).unwrap();
398 assert_eq!(field.description(), "Receiver's Charges: USD 20,00");
399 }
400}