use super::swift_utils::{parse_amount, parse_date_yymmdd, parse_swift_chars};
use crate::errors::ParseError;
use crate::traits::SwiftField;
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct Field61 {
#[cfg_attr(feature = "jsonschema", schemars(with = "String"))]
pub value_date: NaiveDate,
pub entry_date: Option<String>,
pub debit_credit_mark: String,
pub funds_code: Option<char>,
pub amount: f64,
pub transaction_type: String,
pub customer_reference: String,
pub bank_reference: Option<String>,
pub supplementary_details: Option<String>,
}
impl SwiftField for Field61 {
fn parse(input: &str) -> crate::Result<Self>
where
Self: Sized,
{
if input.len() < 15 {
return Err(ParseError::InvalidFormat {
message: "Field 61 must be at least 15 characters long".to_string(),
});
}
let mut pos = 0;
if input.len() < pos + 6 {
return Err(ParseError::InvalidFormat {
message: "Field 61 missing value date".to_string(),
});
}
let value_date_str = &input[pos..pos + 6];
let value_date = parse_date_yymmdd(value_date_str)?;
pos += 6;
let mut entry_date = None;
if pos + 4 <= input.len() && input[pos..pos + 4].chars().all(|c| c.is_ascii_digit()) {
entry_date = Some(input[pos..pos + 4].to_string());
pos += 4;
}
if pos >= input.len() {
return Err(ParseError::InvalidFormat {
message: "Field 61 missing debit/credit mark".to_string(),
});
}
let mut dc_mark_len = 1;
if pos + 1 < input.len() {
let two_char = &input[pos..pos + 2];
if two_char == "RD" || two_char == "RC" {
dc_mark_len = 2;
}
}
let debit_credit_mark = input[pos..pos + dc_mark_len].to_string();
if !["D", "C", "RD", "RC"].contains(&debit_credit_mark.as_str()) {
return Err(ParseError::InvalidFormat {
message: format!("Field 61 invalid debit/credit mark: {}", debit_credit_mark),
});
}
pos += dc_mark_len;
let mut funds_code = None;
if pos < input.len() && input.chars().nth(pos).unwrap().is_alphabetic() {
funds_code = Some(input.chars().nth(pos).unwrap());
pos += 1;
}
let amount_start = pos;
while pos < input.len()
&& (input.chars().nth(pos).unwrap().is_ascii_digit()
|| input.chars().nth(pos).unwrap() == ','
|| input.chars().nth(pos).unwrap() == '.')
{
pos += 1;
}
if pos == amount_start {
return Err(ParseError::InvalidFormat {
message: "Field 61 missing amount".to_string(),
});
}
let amount_str = &input[amount_start..pos];
let amount = parse_amount(amount_str)?;
if pos + 4 > input.len() {
return Err(ParseError::InvalidFormat {
message: "Field 61 missing transaction type".to_string(),
});
}
let transaction_type = input[pos..pos + 4].to_string();
parse_swift_chars(&transaction_type, "Field 61 transaction type")?;
pos += 4;
let remaining = &input[pos..];
let (customer_ref_part, after_customer_ref) =
if let Some(double_slash_pos) = remaining.find("//") {
(
remaining[..double_slash_pos].to_string(),
Some(&remaining[double_slash_pos + 2..]),
)
} else {
(remaining.to_string(), None)
};
let customer_reference;
let mut supplementary_details = None;
if customer_ref_part.len() <= 16 {
customer_reference = customer_ref_part;
} else {
customer_reference = customer_ref_part[..16].to_string();
if after_customer_ref.is_none() && customer_ref_part.len() > 16 {
supplementary_details = Some(customer_ref_part[16..].to_string());
}
}
let bank_reference = if let Some(bank_ref_str) = after_customer_ref {
if let Some(newline_pos) = bank_ref_str.find('\n') {
let bank_ref = bank_ref_str[..newline_pos].to_string();
if newline_pos + 1 < bank_ref_str.len() {
supplementary_details = Some(bank_ref_str[newline_pos + 1..].to_string());
}
Some(bank_ref)
} else if bank_ref_str.len() > 16 {
supplementary_details = Some(bank_ref_str[16..].to_string());
Some(bank_ref_str[..16].to_string())
} else if !bank_ref_str.is_empty() {
Some(bank_ref_str.to_string())
} else {
None
}
} else {
None
};
if customer_reference.len() > 16 {
return Err(ParseError::InvalidFormat {
message: "Field 61 customer reference exceeds 16 characters".to_string(),
});
}
parse_swift_chars(&customer_reference, "Field 61 customer reference")?;
if let Some(ref bank_ref) = bank_reference {
parse_swift_chars(bank_ref, "Field 61 bank reference")?;
}
if let Some(ref supp_details) = supplementary_details {
if supp_details.len() > 34 {
return Err(ParseError::InvalidFormat {
message: "Field 61 supplementary details exceed 34 characters".to_string(),
});
}
parse_swift_chars(supp_details, "Field 61 supplementary details")?;
}
Ok(Field61 {
value_date,
entry_date,
debit_credit_mark,
funds_code,
amount,
transaction_type,
customer_reference,
bank_reference,
supplementary_details,
})
}
fn to_swift_string(&self) -> String {
let mut result = format!(":61:{}", self.value_date.format("%y%m%d"));
if let Some(ref entry_date) = self.entry_date {
result.push_str(entry_date);
}
result.push_str(&self.debit_credit_mark);
if let Some(funds_code) = self.funds_code {
result.push(funds_code);
}
result.push_str(&format!("{:.2}", self.amount).replace('.', ","));
result.push_str(&self.transaction_type);
result.push_str(&self.customer_reference);
if let Some(ref bank_reference) = self.bank_reference {
result.push_str("//");
result.push_str(bank_reference);
if let Some(ref supplementary_details) = self.supplementary_details {
result.push('\n');
result.push_str(supplementary_details);
}
} else if let Some(ref supplementary_details) = self.supplementary_details {
result.push_str(supplementary_details);
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDate;
#[test]
fn test_field61_parse_basic() {
let field = Field61::parse("231225D1234,56NTRFREF123456").unwrap();
assert_eq!(
field.value_date,
NaiveDate::from_ymd_opt(2023, 12, 25).unwrap()
);
assert_eq!(field.entry_date, None);
assert_eq!(field.debit_credit_mark, "D");
assert_eq!(field.funds_code, None);
assert_eq!(field.amount, 1234.56);
assert_eq!(field.transaction_type, "NTRF");
assert_eq!(field.customer_reference, "REF123456");
assert_eq!(field.bank_reference, None);
assert_eq!(field.supplementary_details, None);
}
#[test]
fn test_field61_parse_with_entry_date() {
let field = Field61::parse("2312251226C500,00NTRFREF789//BANK456").unwrap();
assert_eq!(
field.value_date,
NaiveDate::from_ymd_opt(2023, 12, 25).unwrap()
);
assert_eq!(field.entry_date, Some("1226".to_string()));
assert_eq!(field.debit_credit_mark, "C");
assert_eq!(field.funds_code, None);
assert_eq!(field.amount, 500.00);
assert_eq!(field.transaction_type, "NTRF");
assert_eq!(field.customer_reference, "REF789");
assert_eq!(field.bank_reference, Some("BANK456".to_string()));
}
#[test]
fn test_field61_parse_with_funds_code() {
let field = Field61::parse("231225DF100,00NTRFCUSTREF").unwrap();
assert_eq!(field.debit_credit_mark, "D");
assert_eq!(field.funds_code, Some('F'));
assert_eq!(field.amount, 100.00);
}
#[test]
fn test_field61_parse_reversal() {
let field = Field61::parse("231225RD1000,00NTRFREVREF123").unwrap();
assert_eq!(field.debit_credit_mark, "RD");
assert_eq!(field.amount, 1000.00);
}
#[test]
fn test_field61_to_swift_string() {
let field = Field61 {
value_date: NaiveDate::from_ymd_opt(2023, 12, 25).unwrap(),
entry_date: Some("1226".to_string()),
debit_credit_mark: "C".to_string(),
funds_code: Some('F'),
amount: 1234.56,
transaction_type: "NTRF".to_string(),
customer_reference: "REF123456".to_string(),
bank_reference: Some("BANK789".to_string()),
supplementary_details: None,
};
assert_eq!(
field.to_swift_string(),
":61:2312251226CF1234,56NTRFREF123456//BANK789"
);
}
#[test]
fn test_field61_invalid_debit_credit_mark() {
assert!(Field61::parse("231225X1234,56NTRFREF123").is_err());
}
#[test]
fn test_field61_too_short() {
assert!(Field61::parse("23122").is_err());
}
#[test]
fn test_field61_with_supplementary_details() {
let field =
Field61::parse("2412201220C10000,00NMSCREF100000//BA1-1234567890\nDUPLICATE-SEQ-1")
.unwrap();
assert_eq!(field.customer_reference, "REF100000");
assert_eq!(field.bank_reference, Some("BA1-1234567890".to_string()));
assert_eq!(
field.supplementary_details,
Some("DUPLICATE-SEQ-1".to_string())
);
let swift_str = field.to_swift_string();
let reparsed = Field61::parse(&swift_str.replace(":61:", "")).unwrap();
assert_eq!(reparsed.customer_reference, field.customer_reference);
assert_eq!(reparsed.bank_reference, field.bank_reference);
assert_eq!(reparsed.supplementary_details, field.supplementary_details);
}
}