use crate::error::{BRCodeError, Result};
use crate::types::{BRCode, MerchantAccountInfo, AdditionalData, EmvField};
use crate::validation::*;
use std::collections::HashMap;
pub fn parse_brcode(input: &str) -> Result<BRCode> {
if input.is_empty() {
return Err(BRCodeError::invalid_format("Empty BR Code string"));
}
if !validate_crc16(input)? {
return Err(BRCodeError::InvalidChecksum);
}
let fields = parse_tlv_fields(input)?;
build_brcode_from_fields(fields)
}
fn parse_tlv_fields(input: &str) -> Result<HashMap<String, EmvField>> {
let mut fields = HashMap::new();
let mut pos = 0;
while pos < input.len() {
if pos + 4 > input.len() {
return Err(BRCodeError::TlvParsingError("Insufficient data for TLV field".to_string()));
}
let tag = &input[pos..pos + 2];
pos += 2;
let length_str = &input[pos..pos + 2];
pos += 2;
let length: usize = length_str.parse()
.map_err(|_| BRCodeError::TlvParsingError(format!("Invalid length field: {}", length_str)))?;
if pos + length > input.len() {
return Err(BRCodeError::TlvParsingError(format!("Insufficient data for field value, tag: {}", tag)));
}
let value = &input[pos..pos + length];
pos += length;
let field = EmvField {
tag: tag.to_string(),
length,
value: value.to_string(),
};
fields.insert(tag.to_string(), field);
}
Ok(fields)
}
fn build_brcode_from_fields(fields: HashMap<String, EmvField>) -> Result<BRCode> {
let payload_format_indicator = get_required_field(&fields, "00")?;
validate_payload_format(&payload_format_indicator.value)?;
let merchant_category_code = get_required_field(&fields, "52")?;
let transaction_currency = get_required_field(&fields, "53")?;
validate_currency(&transaction_currency.value)?;
let country_code = get_required_field(&fields, "58")?;
validate_country_code(&country_code.value)?;
let merchant_name = get_required_field(&fields, "59")?;
validate_merchant_name(&merchant_name.value)?;
let merchant_city = get_required_field(&fields, "60")?;
validate_merchant_city(&merchant_city.value)?;
let crc16 = get_required_field(&fields, "63")?;
let point_of_initiation_method = fields.get("01").map(|f| f.value.clone());
let transaction_amount = fields.get("54").map(|f| f.value.clone());
if let Some(ref amount) = transaction_amount {
validate_transaction_amount(amount)?;
}
let merchant_account_info = parse_merchant_account_info(&fields)?;
let additional_data = if let Some(field) = fields.get("62") {
Some(parse_additional_data(&field.value)?)
} else {
None
};
Ok(BRCode {
payload_format_indicator: payload_format_indicator.value.clone(),
point_of_initiation_method,
merchant_account_info,
merchant_category_code: merchant_category_code.value.clone(),
transaction_currency: transaction_currency.value.clone(),
transaction_amount,
country_code: country_code.value.clone(),
merchant_name: merchant_name.value.clone(),
merchant_city: merchant_city.value.clone(),
additional_data,
crc16: crc16.value.clone(),
})
}
fn parse_merchant_account_info(fields: &HashMap<String, EmvField>) -> Result<MerchantAccountInfo> {
let field_26 = get_required_field(fields, "26")?;
let nested_fields = parse_tlv_fields(&field_26.value)?;
let gui_field = get_required_field(&nested_fields, "00")?;
validate_gui(&gui_field.value)?;
let pix_key_field = get_required_field(&nested_fields, "01")?;
validate_pix_key(&pix_key_field.value)?;
let description = nested_fields.get("02").map(|f| f.value.clone());
let url = nested_fields.get("25").map(|f| f.value.clone());
Ok(MerchantAccountInfo {
gui: gui_field.value.clone(),
pix_key: pix_key_field.value.clone(),
description,
url,
})
}
fn parse_additional_data(input: &str) -> Result<AdditionalData> {
let fields = parse_tlv_fields(input)?;
Ok(AdditionalData {
bill_number: fields.get("01").map(|f| f.value.clone()),
mobile_number: fields.get("02").map(|f| f.value.clone()),
store_label: fields.get("03").map(|f| f.value.clone()),
loyalty_number: fields.get("04").map(|f| f.value.clone()),
reference_label: fields.get("05").map(|f| f.value.clone()),
customer_label: fields.get("06").map(|f| f.value.clone()),
terminal_label: fields.get("07").map(|f| f.value.clone()),
purpose_of_transaction: fields.get("08").map(|f| f.value.clone()),
additional_consumer_data_request: fields.get("09").map(|f| f.value.clone()),
})
}
fn get_required_field<'a>(fields: &'a HashMap<String, EmvField>, tag: &str) -> Result<&'a EmvField> {
fields.get(tag).ok_or_else(|| BRCodeError::missing_field(format!("Required field with tag {}", tag)))
}
pub fn generate_brcode(brcode: &BRCode) -> Result<String> {
let mut result = String::new();
result.push_str(&format_field("00", &brcode.payload_format_indicator));
if let Some(ref pim) = brcode.point_of_initiation_method {
result.push_str(&format_field("01", pim));
}
let mai_content = format_merchant_account_info(&brcode.merchant_account_info);
result.push_str(&format_field("26", &mai_content));
result.push_str(&format_field("52", &brcode.merchant_category_code));
result.push_str(&format_field("53", &brcode.transaction_currency));
if let Some(ref amount) = brcode.transaction_amount {
result.push_str(&format_field("54", amount));
}
result.push_str(&format_field("58", &brcode.country_code));
result.push_str(&format_field("59", &brcode.merchant_name));
result.push_str(&format_field("60", &brcode.merchant_city));
if let Some(ref additional_data) = brcode.additional_data {
let ad_content = format_additional_data(additional_data);
if !ad_content.is_empty() {
result.push_str(&format_field("62", &ad_content));
}
}
let crc = calculate_crc16(&result);
result.push_str(&format_field("63", &crc));
Ok(result)
}
fn format_field(tag: &str, value: &str) -> String {
format!("{}{:02}{}", tag, value.len(), value)
}
fn format_merchant_account_info(mai: &MerchantAccountInfo) -> String {
let mut result = String::new();
result.push_str(&format_field("00", &mai.gui));
result.push_str(&format_field("01", &mai.pix_key));
if let Some(ref description) = mai.description {
result.push_str(&format_field("02", description));
}
if let Some(ref url) = mai.url {
result.push_str(&format_field("25", url));
}
result
}
fn format_additional_data(ad: &AdditionalData) -> String {
let mut result = String::new();
if let Some(ref bill_number) = ad.bill_number {
result.push_str(&format_field("01", bill_number));
}
if let Some(ref mobile_number) = ad.mobile_number {
result.push_str(&format_field("02", mobile_number));
}
if let Some(ref store_label) = ad.store_label {
result.push_str(&format_field("03", store_label));
}
if let Some(ref loyalty_number) = ad.loyalty_number {
result.push_str(&format_field("04", loyalty_number));
}
if let Some(ref reference_label) = ad.reference_label {
result.push_str(&format_field("05", reference_label));
}
if let Some(ref customer_label) = ad.customer_label {
result.push_str(&format_field("06", customer_label));
}
if let Some(ref terminal_label) = ad.terminal_label {
result.push_str(&format_field("07", terminal_label));
}
if let Some(ref purpose_of_transaction) = ad.purpose_of_transaction {
result.push_str(&format_field("08", purpose_of_transaction));
}
if let Some(ref additional_consumer_data_request) = ad.additional_consumer_data_request {
result.push_str(&format_field("09", additional_consumer_data_request));
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid_static_brcode() {
let brcode_str = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426614174000520400005303986540510.005802BR5913FULANO DE TAL6008BRASILIA62070503***630436D9";
let result = parse_brcode(brcode_str);
assert!(result.is_ok());
let brcode = result.unwrap();
assert_eq!(brcode.payload_format_indicator, "01");
assert_eq!(brcode.merchant_account_info.gui, "br.gov.bcb.pix");
assert_eq!(brcode.merchant_account_info.pix_key, "123e4567-e12b-12d1-a456-426614174000");
assert_eq!(brcode.transaction_currency, "986");
assert_eq!(brcode.country_code, "BR");
assert_eq!(brcode.merchant_name, "FULANO DE TAL");
assert_eq!(brcode.merchant_city, "BRASILIA");
assert_eq!(brcode.transaction_amount, Some("10.00".to_string()));
}
#[test]
fn test_generate_brcode() {
let brcode = BRCode {
payload_format_indicator: "01".to_string(),
point_of_initiation_method: Some("11".to_string()),
merchant_account_info: MerchantAccountInfo {
gui: "br.gov.bcb.pix".to_string(),
pix_key: "test@example.com".to_string(),
description: None,
url: None,
},
merchant_category_code: "0000".to_string(),
transaction_currency: "986".to_string(),
transaction_amount: None,
country_code: "BR".to_string(),
merchant_name: "TEST MERCHANT".to_string(),
merchant_city: "TEST CITY".to_string(),
additional_data: None,
crc16: "".to_string(), };
let result = generate_brcode(&brcode);
assert!(result.is_ok());
let generated = result.unwrap();
let parsed = parse_brcode(&generated);
assert!(parsed.is_ok());
}
#[test]
fn test_invalid_crc() {
let brcode_str = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426614174000520400005303986540510.005802BR5913FULANO DE TAL6008BRASILIA62070503***630457B9";
let result = parse_brcode(brcode_str);
assert!(matches!(result, Err(BRCodeError::InvalidChecksum)));
}
#[test]
fn test_missing_required_field() {
let brcode_str = "0002";
let result = parse_brcode(brcode_str);
assert!(result.is_err());
}
}