pix-brcode-parser 0.1.0

A Rust library for parsing and validating Brazilian PIX QR codes (BR Code) following EMV QRCPS standard
Documentation
//! Type definitions for PIX BR Code data structures

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// Main BR Code structure representing a PIX QR code
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BRCode {
    /// Payload format indicator (always "01" for EMV QRCPS)
    pub payload_format_indicator: String,
    
    /// Point of initiation method ("11" for static, "12" for dynamic)
    pub point_of_initiation_method: Option<String>,
    
    /// Merchant account information containing PIX key
    pub merchant_account_info: MerchantAccountInfo,
    
    /// Merchant category code (MCC)
    pub merchant_category_code: String,
    
    /// Transaction currency code ("986" for BRL)
    pub transaction_currency: String,
    
    /// Transaction amount (optional for static QR codes)
    pub transaction_amount: Option<String>,
    
    /// Country code ("BR" for Brazil)
    pub country_code: String,
    
    /// Merchant name (max 25 characters)
    pub merchant_name: String,
    
    /// Merchant city (max 15 characters)
    pub merchant_city: String,
    
    /// Additional data field template
    pub additional_data: Option<AdditionalData>,
    
    /// CRC16 checksum for validation
    pub crc16: String,
}

/// Merchant account information specific to PIX
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MerchantAccountInfo {
    /// Globally Unique Identifier ("br.gov.bcb.pix")
    pub gui: String,
    
    /// PIX key (UUID, email, phone, CPF, CNPJ, or random key)
    pub pix_key: String,
    
    /// Optional description for the payment
    pub description: Option<String>,
    
    /// URL for dynamic QR codes
    pub url: Option<String>,
}

/// Additional data field template
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AdditionalData {
    /// Bill number
    pub bill_number: Option<String>,
    
    /// Mobile number
    pub mobile_number: Option<String>,
    
    /// Store label
    pub store_label: Option<String>,
    
    /// Loyalty number
    pub loyalty_number: Option<String>,
    
    /// Reference label
    pub reference_label: Option<String>,
    
    /// Customer label
    pub customer_label: Option<String>,
    
    /// Terminal label
    pub terminal_label: Option<String>,
    
    /// Purpose of transaction
    pub purpose_of_transaction: Option<String>,
    
    /// Additional consumer data request
    pub additional_consumer_data_request: Option<String>,
}

/// PIX key types
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum PixKeyType {
    /// UUID format key
    Uuid,
    /// Email address
    Email,
    /// Phone number (+5511999999999)
    Phone,
    /// CPF (individual taxpayer number)
    Cpf,
    /// CNPJ (company taxpayer number)  
    Cnpj,
    /// Random key (alphanumeric)
    Random,
}

/// EMV data field
#[derive(Debug, Clone, PartialEq)]
pub struct EmvField {
    /// Field tag identifier
    pub tag: String,
    /// Field length
    pub length: usize,
    /// Field value
    pub value: String,
}

impl BRCode {
    /// Check if this is a static QR code
    pub fn is_static(&self) -> bool {
        self.point_of_initiation_method.as_deref() == Some("11")
    }
    
    /// Check if this is a dynamic QR code
    pub fn is_dynamic(&self) -> bool {
        self.point_of_initiation_method.as_deref() == Some("12")
    }
    
    /// Get the PIX key type
    pub fn pix_key_type(&self) -> PixKeyType {
        classify_pix_key(&self.merchant_account_info.pix_key)
    }
    
    /// Check if amount is specified
    pub fn has_amount(&self) -> bool {
        self.transaction_amount.is_some()
    }
}

/// Classify a PIX key by its format
pub fn classify_pix_key(key: &str) -> PixKeyType {
    // UUID format: 8-4-4-4-12 hex digits
    if key.len() == 36 && key.chars().nth(8) == Some('-') && key.chars().nth(13) == Some('-') {
        return PixKeyType::Uuid;
    }
    
    // Email format
    if key.contains('@') && key.contains('.') {
        return PixKeyType::Email;
    }
    
    // Phone format: +5511999999999
    if key.starts_with("+55") && key.len() >= 13 {
        return PixKeyType::Phone;
    }
    
    // CPF: 11 digits
    if key.len() == 11 && key.chars().all(|c| c.is_ascii_digit()) {
        return PixKeyType::Cpf;
    }
    
    // CNPJ: 14 digits
    if key.len() == 14 && key.chars().all(|c| c.is_ascii_digit()) {
        return PixKeyType::Cnpj;
    }
    
    // Default to random key
    PixKeyType::Random
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_pix_key_classification() {
        assert_eq!(classify_pix_key("123e4567-e89b-12d3-a456-426614174000"), PixKeyType::Uuid);
        assert_eq!(classify_pix_key("user@example.com"), PixKeyType::Email);
        assert_eq!(classify_pix_key("+5511999999999"), PixKeyType::Phone);
        assert_eq!(classify_pix_key("12345678901"), PixKeyType::Cpf);
        assert_eq!(classify_pix_key("12345678000195"), PixKeyType::Cnpj);
        assert_eq!(classify_pix_key("randomkey123"), PixKeyType::Random);
    }
}