1use crate::{SwiftField, ValidationError, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
113pub struct Field71F {
114 pub currency: String,
116 pub amount: f64,
118 pub raw_amount: String,
120}
121
122impl Field71F {
123 pub fn new(currency: impl Into<String>, amount: f64) -> Result<Self, crate::ParseError> {
125 let currency = currency.into().to_uppercase();
126
127 if currency.len() != 3 {
129 return Err(crate::ParseError::InvalidFieldFormat {
130 field_tag: "71F".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: "71F".to_string(),
138 message: "Currency code must contain only alphabetic characters".to_string(),
139 });
140 }
141
142 if amount < 0.0 {
144 return Err(crate::ParseError::InvalidFieldFormat {
145 field_tag: "71F".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(Field71F {
153 currency,
154 amount,
155 raw_amount,
156 })
157 }
158
159 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(Field71F {
170 currency,
171 amount,
172 raw_amount: raw_amount.to_string(),
173 })
174 }
175
176 pub fn currency(&self) -> &str {
178 &self.currency
179 }
180
181 pub fn amount(&self) -> f64 {
183 self.amount
184 }
185
186 pub fn raw_amount(&self) -> &str {
188 &self.raw_amount
189 }
190
191 pub fn format_amount(amount: f64) -> String {
193 format!("{:.2}", amount).replace('.', ",")
194 }
195
196 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: "71F".to_string(),
204 message: "Invalid charge amount format".to_string(),
205 })
206 }
207
208 pub fn description(&self) -> String {
210 format!("Sender's Charges: {} {}", self.currency, self.raw_amount)
211 }
212}
213
214impl SwiftField for Field71F {
215 fn parse(value: &str) -> Result<Self, crate::ParseError> {
216 let content = if let Some(stripped) = value.strip_prefix(":71F:") {
217 stripped } else if let Some(stripped) = value.strip_prefix("71F:") {
219 stripped } 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: "71F".to_string(),
229 message: "Field content too short (minimum 4 characters: CCCAMOUNT)".to_string(),
230 });
231 }
232
233 let currency_str = &content[0..3];
235 let amount_str = &content[3..];
236
237 let currency = currency_str.to_uppercase();
238
239 if !currency.chars().all(|c| c.is_alphabetic() && c.is_ascii()) {
241 return Err(crate::ParseError::InvalidFieldFormat {
242 field_tag: "71F".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: "71F".to_string(),
252 message: "Charge amount cannot be negative".to_string(),
253 });
254 }
255
256 Ok(Field71F {
257 currency,
258 amount,
259 raw_amount: amount_str.to_string(),
260 })
261 }
262
263 fn to_swift_string(&self) -> String {
264 format!(":71F:{}{}", self.currency, self.raw_amount)
265 }
266
267 fn validate(&self) -> ValidationResult {
268 let mut errors = Vec::new();
269
270 if self.currency.len() != 3 {
272 errors.push(ValidationError::LengthValidation {
273 field_tag: "71F".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: "71F".to_string(),
286 message: "Currency code must contain only alphabetic characters".to_string(),
287 });
288 }
289
290 if self.amount < 0.0 {
292 errors.push(ValidationError::ValueValidation {
293 field_tag: "71F".to_string(),
294 message: "Charge amount cannot be negative".to_string(),
295 });
296 }
297
298 if self.raw_amount.is_empty() {
300 errors.push(ValidationError::ValueValidation {
301 field_tag: "71F".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 Field71F {
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_field71f_creation() {
330 let field = Field71F::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_field71f_from_raw() {
338 let field = Field71F::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_field71f_parse() {
346 let field = Field71F::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_field71f_parse_with_prefix() {
354 let field = Field71F::parse(":71F: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_field71f_to_swift_string() {
362 let field = Field71F::new("CHF", 100.0).unwrap();
363 assert_eq!(field.to_swift_string(), ":71F:CHF100,00");
364 }
365
366 #[test]
367 fn test_field71f_invalid_currency() {
368 let result = Field71F::new("US", 10.0);
369 assert!(result.is_err());
370
371 let result = Field71F::new("123", 10.0);
372 assert!(result.is_err());
373 }
374
375 #[test]
376 fn test_field71f_negative_amount() {
377 let result = Field71F::new("USD", -10.0);
378 assert!(result.is_err());
379 }
380
381 #[test]
382 fn test_field71f_validation() {
383 let field = Field71F::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_field71f_display() {
391 let field = Field71F::new("EUR", 75.50).unwrap();
392 assert_eq!(format!("{}", field), "EUR 75,50");
393 }
394
395 #[test]
396 fn test_field71f_description() {
397 let field = Field71F::new("USD", 20.0).unwrap();
398 assert_eq!(field.description(), "Sender's Charges: USD 20,00");
399 }
400}