1use crate::{SwiftField, ValidationError, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
132pub struct Field77B {
133 pub information: Vec<String>,
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub ordering_country: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub beneficiary_country: Option<String>,
141}
142
143impl Field77B {
144 pub fn new(information: Vec<String>) -> Result<Self, crate::ParseError> {
146 if information.is_empty() {
147 return Err(crate::ParseError::InvalidFieldFormat {
148 field_tag: "77B".to_string(),
149 message: "Regulatory reporting information cannot be empty".to_string(),
150 });
151 }
152
153 if information.len() > 3 {
154 return Err(crate::ParseError::InvalidFieldFormat {
155 field_tag: "77B".to_string(),
156 message: "Too many lines (max 3)".to_string(),
157 });
158 }
159
160 for (i, line) in information.iter().enumerate() {
161 if line.len() > 35 {
162 return Err(crate::ParseError::InvalidFieldFormat {
163 field_tag: "77B".to_string(),
164 message: format!("Line {} too long (max 35 characters)", i + 1),
165 });
166 }
167
168 if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
170 return Err(crate::ParseError::InvalidFieldFormat {
171 field_tag: "77B".to_string(),
172 message: format!("Line {} contains invalid characters", i + 1),
173 });
174 }
175 }
176
177 let mut ordering_country = None;
179 let mut beneficiary_country = None;
180
181 for line in &information {
182 if line.starts_with("/ORDERRES/") {
183 if let Some(country_part) = line.strip_prefix("/ORDERRES/") {
185 let country = country_part.split('/').next().unwrap_or("").to_string();
187 if !country.is_empty() {
188 ordering_country = Some(country);
189 }
190 }
191 }
192 if line.starts_with("/BENEFRES/") {
193 if let Some(country_part) = line.strip_prefix("/BENEFRES/") {
195 let country = country_part.split('/').next().unwrap_or("").to_string();
197 if !country.is_empty() {
198 beneficiary_country = Some(country);
199 }
200 }
201 }
202 }
203
204 Ok(Field77B {
205 information,
206 ordering_country,
207 beneficiary_country,
208 })
209 }
210
211 pub fn from_string(content: impl Into<String>) -> Result<Self, crate::ParseError> {
213 let content = content.into();
214 let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
215 Self::new(lines)
216 }
217
218 pub fn information(&self) -> &[String] {
220 &self.information
221 }
222
223 pub fn line_count(&self) -> usize {
225 self.information.len()
226 }
227
228 pub fn line(&self, index: usize) -> Option<&str> {
230 self.information.get(index).map(|s| s.as_str())
231 }
232
233 pub fn add_line(&mut self, line: String) -> Result<(), crate::ParseError> {
235 if self.information.len() >= 3 {
236 return Err(crate::ParseError::InvalidFieldFormat {
237 field_tag: "77B".to_string(),
238 message: "Cannot add more lines (max 3)".to_string(),
239 });
240 }
241
242 if line.len() > 35 {
243 return Err(crate::ParseError::InvalidFieldFormat {
244 field_tag: "77B".to_string(),
245 message: "Line too long (max 35 characters)".to_string(),
246 });
247 }
248
249 if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
250 return Err(crate::ParseError::InvalidFieldFormat {
251 field_tag: "77B".to_string(),
252 message: "Line contains invalid characters".to_string(),
253 });
254 }
255
256 self.information.push(line);
257 Ok(())
258 }
259
260 pub fn has_ordering_country(&self) -> bool {
262 self.ordering_country.is_some()
263 }
264
265 pub fn has_beneficiary_country(&self) -> bool {
267 self.beneficiary_country.is_some()
268 }
269
270 pub fn ordering_country(&self) -> Option<&str> {
272 self.ordering_country.as_deref()
273 }
274
275 pub fn beneficiary_country(&self) -> Option<&str> {
277 self.beneficiary_country.as_deref()
278 }
279
280 pub fn description(&self) -> String {
282 format!("Regulatory Reporting ({} lines)", self.line_count())
283 }
284}
285
286impl SwiftField for Field77B {
287 fn parse(value: &str) -> Result<Self, crate::ParseError> {
288 let content = if let Some(stripped) = value.strip_prefix(":77B:") {
289 stripped } else if let Some(stripped) = value.strip_prefix("77B:") {
291 stripped } else {
293 value
294 };
295
296 let content = content.trim();
297
298 if content.is_empty() {
299 return Err(crate::ParseError::InvalidFieldFormat {
300 field_tag: "77B".to_string(),
301 message: "Field content cannot be empty".to_string(),
302 });
303 }
304
305 Self::from_string(content)
306 }
307
308 fn to_swift_string(&self) -> String {
309 format!(":77B:{}", self.information.join("\n"))
310 }
311
312 fn validate(&self) -> ValidationResult {
313 let mut errors = Vec::new();
314
315 if self.information.is_empty() {
317 errors.push(ValidationError::ValueValidation {
318 field_tag: "77B".to_string(),
319 message: "Information cannot be empty".to_string(),
320 });
321 }
322
323 if self.information.len() > 3 {
324 errors.push(ValidationError::LengthValidation {
325 field_tag: "77B".to_string(),
326 expected: "max 3 lines".to_string(),
327 actual: self.information.len(),
328 });
329 }
330
331 for (i, line) in self.information.iter().enumerate() {
333 if line.len() > 35 {
334 errors.push(ValidationError::LengthValidation {
335 field_tag: "77B".to_string(),
336 expected: format!("max 35 characters for line {}", i + 1),
337 actual: line.len(),
338 });
339 }
340
341 if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
342 errors.push(ValidationError::FormatValidation {
343 field_tag: "77B".to_string(),
344 message: format!("Line {} contains invalid characters", i + 1),
345 });
346 }
347 }
348
349 ValidationResult {
350 is_valid: errors.is_empty(),
351 errors,
352 warnings: Vec::new(),
353 }
354 }
355
356 fn format_spec() -> &'static str {
357 "3*35x"
358 }
359}
360
361impl std::fmt::Display for Field77B {
362 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363 write!(f, "{}", self.information.join("\n"))
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[test]
372 fn test_field77b_creation() {
373 let lines = vec!["/ORDERRES/DE".to_string(), "/BENEFRES/BE".to_string()];
374 let field = Field77B::new(lines.clone()).unwrap();
375 assert_eq!(field.information(), &lines);
376 assert_eq!(field.line_count(), 2);
377 }
378
379 #[test]
380 fn test_field77b_from_string() {
381 let content = "/ORDERRES/DE\n/BENEFRES/BE\nREGPORT123";
382 let field = Field77B::from_string(content).unwrap();
383 assert_eq!(field.line_count(), 3);
384 assert_eq!(field.line(0), Some("/ORDERRES/DE"));
385 assert_eq!(field.line(1), Some("/BENEFRES/BE"));
386 assert_eq!(field.line(2), Some("REGPORT123"));
387 }
388
389 #[test]
390 fn test_field77b_parse() {
391 let field = Field77B::parse("/ORDERRES/DE\n/BENEFRES/BE").unwrap();
392 assert_eq!(field.line_count(), 2);
393 assert_eq!(field.line(0), Some("/ORDERRES/DE"));
394 assert_eq!(field.line(1), Some("/BENEFRES/BE"));
395 }
396
397 #[test]
398 fn test_field77b_parse_with_prefix() {
399 let field = Field77B::parse(":77B:/ORDERRES/DE\n/BENEFRES/BE").unwrap();
400 assert_eq!(field.line_count(), 2);
401 assert_eq!(field.line(0), Some("/ORDERRES/DE"));
402 }
403
404 #[test]
405 fn test_field77b_to_swift_string() {
406 let lines = vec!["/ORDERRES/DE".to_string(), "/BENEFRES/BE".to_string()];
407 let field = Field77B::new(lines).unwrap();
408 assert_eq!(field.to_swift_string(), ":77B:/ORDERRES/DE\n/BENEFRES/BE");
409 }
410
411 #[test]
412 fn test_field77b_add_line() {
413 let mut field = Field77B::new(vec!["/ORDERRES/DE".to_string()]).unwrap();
414 field.add_line("/BENEFRES/BE".to_string()).unwrap();
415 assert_eq!(field.line_count(), 2);
416 assert_eq!(field.line(1), Some("/BENEFRES/BE"));
417 }
418
419 #[test]
420 fn test_field77b_country_extraction() {
421 let field =
422 Field77B::new(vec!["/ORDERRES/DE".to_string(), "/BENEFRES/BE".to_string()]).unwrap();
423
424 assert!(field.has_ordering_country());
425 assert!(field.has_beneficiary_country());
426 assert_eq!(field.ordering_country(), Some("DE"));
427 assert_eq!(field.beneficiary_country(), Some("BE"));
428 }
429
430 #[test]
431 fn test_field77b_country_extraction_with_additional_info() {
432 let field = Field77B::new(vec![
434 "/ORDERRES/DE//REGULATORY INFO".to_string(),
435 "SOFTWARE LICENSE COMPLIANCE".to_string(),
436 "TRADE RELATED TRANSACTION".to_string(),
437 ])
438 .unwrap();
439
440 assert!(field.has_ordering_country());
441 assert!(!field.has_beneficiary_country());
442 assert_eq!(field.ordering_country(), Some("DE"));
443 assert_eq!(field.beneficiary_country(), None);
444 }
445
446 #[test]
447 fn test_field77b_no_country_codes() {
448 let field = Field77B::new(vec![
449 "REGULATORY INFORMATION".to_string(),
450 "NO COUNTRY CODES HERE".to_string(),
451 ])
452 .unwrap();
453
454 assert!(!field.has_ordering_country());
455 assert!(!field.has_beneficiary_country());
456 assert_eq!(field.ordering_country(), None);
457 assert_eq!(field.beneficiary_country(), None);
458 }
459
460 #[test]
461 fn test_field77b_too_many_lines() {
462 let lines = vec![
463 "Line 1".to_string(),
464 "Line 2".to_string(),
465 "Line 3".to_string(),
466 "Line 4".to_string(), ];
468 let result = Field77B::new(lines);
469 assert!(result.is_err());
470 }
471
472 #[test]
473 fn test_field77b_line_too_long() {
474 let lines = vec!["A".repeat(36)]; let result = Field77B::new(lines);
476 assert!(result.is_err());
477 }
478
479 #[test]
480 fn test_field77b_empty() {
481 let result = Field77B::new(vec![]);
482 assert!(result.is_err());
483 }
484
485 #[test]
486 fn test_field77b_validation() {
487 let field = Field77B::new(vec!["/ORDERRES/DE".to_string()]).unwrap();
488 let validation = field.validate();
489 assert!(validation.is_valid);
490 assert!(validation.errors.is_empty());
491 }
492
493 #[test]
494 fn test_field77b_display() {
495 let field =
496 Field77B::new(vec!["/ORDERRES/DE".to_string(), "/BENEFRES/BE".to_string()]).unwrap();
497 assert_eq!(format!("{}", field), "/ORDERRES/DE\n/BENEFRES/BE");
498 }
499
500 #[test]
501 fn test_field77b_description() {
502 let field =
503 Field77B::new(vec!["/ORDERRES/DE".to_string(), "/BENEFRES/BE".to_string()]).unwrap();
504 assert_eq!(field.description(), "Regulatory Reporting (2 lines)");
505 }
506
507 #[test]
508 fn test_field77b_add_line_max_reached() {
509 let mut field = Field77B::new(vec![
510 "Line 1".to_string(),
511 "Line 2".to_string(),
512 "Line 3".to_string(),
513 ])
514 .unwrap();
515
516 let result = field.add_line("Line 4".to_string());
517 assert!(result.is_err());
518 }
519}