1use crate::error::{BRCodeError, Result};
4use crate::types::{BRCode, MerchantAccountInfo, AdditionalData, EmvField};
5use crate::validation::*;
6use std::collections::HashMap;
7
8pub fn parse_brcode(input: &str) -> Result<BRCode> {
10 if input.is_empty() {
11 return Err(BRCodeError::invalid_format("Empty BR Code string"));
12 }
13
14 if !validate_crc16(input)? {
16 return Err(BRCodeError::InvalidChecksum);
17 }
18
19 let fields = parse_tlv_fields(input)?;
21
22 build_brcode_from_fields(fields)
24}
25
26fn parse_tlv_fields(input: &str) -> Result<HashMap<String, EmvField>> {
28 let mut fields = HashMap::new();
29 let mut pos = 0;
30
31 while pos < input.len() {
32 if pos + 4 > input.len() {
33 return Err(BRCodeError::TlvParsingError("Insufficient data for TLV field".to_string()));
34 }
35
36 let tag = &input[pos..pos + 2];
38 pos += 2;
39
40 let length_str = &input[pos..pos + 2];
42 pos += 2;
43
44 let length: usize = length_str.parse()
45 .map_err(|_| BRCodeError::TlvParsingError(format!("Invalid length field: {}", length_str)))?;
46
47 if pos + length > input.len() {
49 return Err(BRCodeError::TlvParsingError(format!("Insufficient data for field value, tag: {}", tag)));
50 }
51
52 let value = &input[pos..pos + length];
54 pos += length;
55
56 let field = EmvField {
57 tag: tag.to_string(),
58 length,
59 value: value.to_string(),
60 };
61
62 fields.insert(tag.to_string(), field);
63 }
64
65 Ok(fields)
66}
67
68fn build_brcode_from_fields(fields: HashMap<String, EmvField>) -> Result<BRCode> {
70 let payload_format_indicator = get_required_field(&fields, "00")?;
72 validate_payload_format(&payload_format_indicator.value)?;
73
74 let merchant_category_code = get_required_field(&fields, "52")?;
75 let transaction_currency = get_required_field(&fields, "53")?;
76 validate_currency(&transaction_currency.value)?;
77
78 let country_code = get_required_field(&fields, "58")?;
79 validate_country_code(&country_code.value)?;
80
81 let merchant_name = get_required_field(&fields, "59")?;
82 validate_merchant_name(&merchant_name.value)?;
83
84 let merchant_city = get_required_field(&fields, "60")?;
85 validate_merchant_city(&merchant_city.value)?;
86
87 let crc16 = get_required_field(&fields, "63")?;
88
89 let point_of_initiation_method = fields.get("01").map(|f| f.value.clone());
91 let transaction_amount = fields.get("54").map(|f| f.value.clone());
92
93 if let Some(ref amount) = transaction_amount {
95 validate_transaction_amount(amount)?;
96 }
97
98 let merchant_account_info = parse_merchant_account_info(&fields)?;
100
101 let additional_data = if let Some(field) = fields.get("62") {
103 Some(parse_additional_data(&field.value)?)
104 } else {
105 None
106 };
107
108 Ok(BRCode {
109 payload_format_indicator: payload_format_indicator.value.clone(),
110 point_of_initiation_method,
111 merchant_account_info,
112 merchant_category_code: merchant_category_code.value.clone(),
113 transaction_currency: transaction_currency.value.clone(),
114 transaction_amount,
115 country_code: country_code.value.clone(),
116 merchant_name: merchant_name.value.clone(),
117 merchant_city: merchant_city.value.clone(),
118 additional_data,
119 crc16: crc16.value.clone(),
120 })
121}
122
123fn parse_merchant_account_info(fields: &HashMap<String, EmvField>) -> Result<MerchantAccountInfo> {
125 let field_26 = get_required_field(fields, "26")?;
126
127 let nested_fields = parse_tlv_fields(&field_26.value)?;
129
130 let gui_field = get_required_field(&nested_fields, "00")?;
132 validate_gui(&gui_field.value)?;
133
134 let pix_key_field = get_required_field(&nested_fields, "01")?;
136 validate_pix_key(&pix_key_field.value)?;
137
138 let description = nested_fields.get("02").map(|f| f.value.clone());
140
141 let url = nested_fields.get("25").map(|f| f.value.clone());
143
144 Ok(MerchantAccountInfo {
145 gui: gui_field.value.clone(),
146 pix_key: pix_key_field.value.clone(),
147 description,
148 url,
149 })
150}
151
152fn parse_additional_data(input: &str) -> Result<AdditionalData> {
154 let fields = parse_tlv_fields(input)?;
155
156 Ok(AdditionalData {
157 bill_number: fields.get("01").map(|f| f.value.clone()),
158 mobile_number: fields.get("02").map(|f| f.value.clone()),
159 store_label: fields.get("03").map(|f| f.value.clone()),
160 loyalty_number: fields.get("04").map(|f| f.value.clone()),
161 reference_label: fields.get("05").map(|f| f.value.clone()),
162 customer_label: fields.get("06").map(|f| f.value.clone()),
163 terminal_label: fields.get("07").map(|f| f.value.clone()),
164 purpose_of_transaction: fields.get("08").map(|f| f.value.clone()),
165 additional_consumer_data_request: fields.get("09").map(|f| f.value.clone()),
166 })
167}
168
169fn get_required_field<'a>(fields: &'a HashMap<String, EmvField>, tag: &str) -> Result<&'a EmvField> {
171 fields.get(tag).ok_or_else(|| BRCodeError::missing_field(format!("Required field with tag {}", tag)))
172}
173
174pub fn generate_brcode(brcode: &BRCode) -> Result<String> {
176 let mut result = String::new();
177
178 result.push_str(&format_field("00", &brcode.payload_format_indicator));
180
181 if let Some(ref pim) = brcode.point_of_initiation_method {
183 result.push_str(&format_field("01", pim));
184 }
185
186 let mai_content = format_merchant_account_info(&brcode.merchant_account_info);
188 result.push_str(&format_field("26", &mai_content));
189
190 result.push_str(&format_field("52", &brcode.merchant_category_code));
192
193 result.push_str(&format_field("53", &brcode.transaction_currency));
195
196 if let Some(ref amount) = brcode.transaction_amount {
198 result.push_str(&format_field("54", amount));
199 }
200
201 result.push_str(&format_field("58", &brcode.country_code));
203
204 result.push_str(&format_field("59", &brcode.merchant_name));
206
207 result.push_str(&format_field("60", &brcode.merchant_city));
209
210 if let Some(ref additional_data) = brcode.additional_data {
212 let ad_content = format_additional_data(additional_data);
213 if !ad_content.is_empty() {
214 result.push_str(&format_field("62", &ad_content));
215 }
216 }
217
218 let crc = calculate_crc16(&result);
220 result.push_str(&format_field("63", &crc));
221
222 Ok(result)
223}
224
225fn format_field(tag: &str, value: &str) -> String {
227 format!("{}{:02}{}", tag, value.len(), value)
228}
229
230fn format_merchant_account_info(mai: &MerchantAccountInfo) -> String {
232 let mut result = String::new();
233
234 result.push_str(&format_field("00", &mai.gui));
235 result.push_str(&format_field("01", &mai.pix_key));
236
237 if let Some(ref description) = mai.description {
238 result.push_str(&format_field("02", description));
239 }
240
241 if let Some(ref url) = mai.url {
242 result.push_str(&format_field("25", url));
243 }
244
245 result
246}
247
248fn format_additional_data(ad: &AdditionalData) -> String {
250 let mut result = String::new();
251
252 if let Some(ref bill_number) = ad.bill_number {
253 result.push_str(&format_field("01", bill_number));
254 }
255 if let Some(ref mobile_number) = ad.mobile_number {
256 result.push_str(&format_field("02", mobile_number));
257 }
258 if let Some(ref store_label) = ad.store_label {
259 result.push_str(&format_field("03", store_label));
260 }
261 if let Some(ref loyalty_number) = ad.loyalty_number {
262 result.push_str(&format_field("04", loyalty_number));
263 }
264 if let Some(ref reference_label) = ad.reference_label {
265 result.push_str(&format_field("05", reference_label));
266 }
267 if let Some(ref customer_label) = ad.customer_label {
268 result.push_str(&format_field("06", customer_label));
269 }
270 if let Some(ref terminal_label) = ad.terminal_label {
271 result.push_str(&format_field("07", terminal_label));
272 }
273 if let Some(ref purpose_of_transaction) = ad.purpose_of_transaction {
274 result.push_str(&format_field("08", purpose_of_transaction));
275 }
276 if let Some(ref additional_consumer_data_request) = ad.additional_consumer_data_request {
277 result.push_str(&format_field("09", additional_consumer_data_request));
278 }
279
280 result
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_parse_valid_static_brcode() {
289 let brcode_str = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426614174000520400005303986540510.005802BR5913FULANO DE TAL6008BRASILIA62070503***630436D9";
291
292 let result = parse_brcode(brcode_str);
293 assert!(result.is_ok());
294
295 let brcode = result.unwrap();
296 assert_eq!(brcode.payload_format_indicator, "01");
297 assert_eq!(brcode.merchant_account_info.gui, "br.gov.bcb.pix");
298 assert_eq!(brcode.merchant_account_info.pix_key, "123e4567-e12b-12d1-a456-426614174000");
299 assert_eq!(brcode.transaction_currency, "986");
300 assert_eq!(brcode.country_code, "BR");
301 assert_eq!(brcode.merchant_name, "FULANO DE TAL");
302 assert_eq!(brcode.merchant_city, "BRASILIA");
303 assert_eq!(brcode.transaction_amount, Some("10.00".to_string()));
304 }
307
308 #[test]
311 fn test_generate_brcode() {
312 let brcode = BRCode {
313 payload_format_indicator: "01".to_string(),
314 point_of_initiation_method: Some("11".to_string()),
315 merchant_account_info: MerchantAccountInfo {
316 gui: "br.gov.bcb.pix".to_string(),
317 pix_key: "test@example.com".to_string(),
318 description: None,
319 url: None,
320 },
321 merchant_category_code: "0000".to_string(),
322 transaction_currency: "986".to_string(),
323 transaction_amount: None,
324 country_code: "BR".to_string(),
325 merchant_name: "TEST MERCHANT".to_string(),
326 merchant_city: "TEST CITY".to_string(),
327 additional_data: None,
328 crc16: "".to_string(), };
330
331 let result = generate_brcode(&brcode);
332 assert!(result.is_ok());
333
334 let generated = result.unwrap();
335 let parsed = parse_brcode(&generated);
337 assert!(parsed.is_ok());
338 }
339
340 #[test]
341 fn test_invalid_crc() {
342 let brcode_str = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426614174000520400005303986540510.005802BR5913FULANO DE TAL6008BRASILIA62070503***630457B9";
343
344 let result = parse_brcode(brcode_str);
345 assert!(matches!(result, Err(BRCodeError::InvalidChecksum)));
346 }
347
348 #[test]
349 fn test_missing_required_field() {
350 let brcode_str = "0002"; let result = parse_brcode(brcode_str);
353 assert!(result.is_err());
354 }
355}