# Data validation

Learn about built-in validation and implement additional scenarios for Android.

## Overview

The PayPal component includes comprehensive validation to ensure data integrity and compliance with PayPal's requirements. All built-in validation is performed before creating orders. If it fails, the SDK will trigger the `onSubmitError` callback with detailed error information.

You can also easily build custom validation, depending on your business needs.

## Built-in validation

By default, the PayPal component validates that:

* Fields marked as required are provided.
* Maximum length constraints are respected.
* All email, currency code, country code, and date fields are formatted properly.
* Valid values are provided for every enum.


### Error codes

The PayPal component returns structured error codes for different validation failures:

| Error code  | Description  |
|  --- | --- |
| `REQUIRED_FIELD` | At least one required field is missing. |
| `MAX_LENGTH_EXCEEDED` | At least one field exceeds its maximum length. |
| `INVALID_LENGTH` | At least one field has the incorrect length. |
| `INVALID_EMAIL_FORMAT` | The email format is invalid. |
| `INVALID_DATE_FORMAT` | The date format is invalid. It should be in ISO 8601 date format. |
| `INVALID_ENUM_VALUE` | The enum value is invalid. |
| `INVALID_SHIPPING_PREFERENCE` | There's a shipping preference mismatch. |
| `NO_SELECTION_MADE` | No shipping option is selected. |
| `MULTIPLE_SELECTIONS_NOT_ALLOWED` | Multiple shipping options are selected. |
| `CURRENCY_CODE_INVALID` | There's a currency code inconsistency. For example, due to different currencies in the same request. |


#### Validation example

```kotlin
import android.util.Log

val paypalConfig = PayPalComponentConfig(
    // ... configuration
    onSubmitError = { error ->
        Log.e("PayPal", "Validation error: $error")
        
        // Handle specific validation errors
        when {
            error.contains("payeeEmailAddress") -> {
                showError("Invalid email address format")
            }
            error.contains("currencyCode") -> {
                showError("Invalid currency code")
            }
            error.contains("REQUIRED_FIELD") -> {
                showError("Please fill in all required fields")
            }
            error.contains("MAX_LENGTH_EXCEEDED") -> {
                showError("Some fields exceed maximum length")
            }
            else -> {
                showError("Validation failed: $error")
            }
        }
        
        // Log for debugging
        crashlytics.log("PayPal validation error: $error")
    }
)
```

## Custom validation

### Pre-authorisation validation

Run a validation before the PayPal authorisation to catch issues early and prevent failed payments.

```kotlin
import kotlinx.coroutines.launch

val paypalConfig = PayPalComponentConfig(
    onSuccess = { data ->
        viewModelScope.launch {
            try {
                // 1. Business logic validation
                val orderValidation = validateOrder(
                    cartItems = getCartItems(),
                    customerLocation = getCustomerLocation(),
                    paymentAmount = getOrderTotal()
                )
                
                if (!orderValidation.isValid) {
                    throw ValidationException(orderValidation.reason)
                }
                
                // 2. Security validation
                val securityCheck = performSecurityValidation(
                    customerIP = getClientIP(),
                    deviceFingerprint = getDeviceFingerprint(),
                    paymentHistory = getCustomerPaymentHistory()
                )
                
                if (securityCheck.riskLevel == RiskLevel.HIGH) {
                    // Require additional verification
                    val verified = requestAdditionalVerification()
                    if (!verified) {
                        throw SecurityException("Additional verification required")
                    }
                }
                
                // 3. Inventory validation
                val inventoryCheck = validateInventory(getCartItems())
                if (!inventoryCheck.allAvailable) {
                    throw InventoryException("Some items are no longer available")
                }
                
                // 4. Regulatory compliance
                val complianceCheck = validateCompliance(
                    customerCountry = getCustomerCountry(),
                    orderAmount = getOrderTotal(),
                    productTypes = getProductTypes()
                )
                
                if (!complianceCheck.compliant) {
                    throw ComplianceException("Order does not meet regulatory requirements")
                }
                
                // If all validations pass, proceed with payment processing
                processPayPalPayment(data)
                
            } catch (e: Exception) {
                Log.e("PayPal", "Validation failed", e)
                showError(e.message ?: "Validation failed")
            }
        }
    }
)
```

### Confirmation screen validation

Run validation on the confirmation screen before capturing funds, to ensure order integrity and prevent capture failures.

```kotlin
suspend fun confirmAndCaptureOrder(orderID: String) {
    try {
        // 1. Re-validate order (things might have changed)
        val currentOrderState = validateCurrentOrderState(orderID)
        if (!currentOrderState.isValid) {
            throw ValidationException("Order state has changed since authorisation")
        }
        
        // 2. Final inventory check
        val finalInventoryCheck = reserveInventory(orderID)
        if (!finalInventoryCheck.success) {
            throw InventoryException("Inventory no longer available")
        }
        
        // 3. Calculate final amounts (shipping, taxes, fees)
        val finalCalculation = calculateFinalAmounts(orderID)
        
        // 4. Validate amount changes are within acceptable limits
        val authData = getAuthorizationData(orderID)
        val amountDifference = Math.abs(finalCalculation.total - authData.authorizedAmount)
        val maxAllowedDifference = authData.authorizedAmount * 0.15 // 15% tolerance
        
        if (amountDifference > maxAllowedDifference) {
            throw ValidationException("Final amount differs too much from authorised amount")
        }
        
        // 5. Capture the payment
        captureAuthorizedPayment(orderID, finalCalculation.total)
        
    } catch (e: Exception) {
        handleCaptureValidationError(e)
    }
}
```

### Validate geographic restrictions

Check in real-time if any cart items are restricted in the customer's country to prevent compliance violations.

```kotlin
data class GeographicValidationResult(
    val isValid: Boolean,
    val restrictedItems: List<CartItem>
)

fun validateGeographicRestrictions(
    customerCountry: String,
    cartItems: List<CartItem>
): GeographicValidationResult {
    val restrictedItems = cartItems.filter { item -> 
        item.restrictions?.countries?.contains(customerCountry) == true
    }
    
    return GeographicValidationResult(
        isValid = restrictedItems.isEmpty(),
        restrictedItems = restrictedItems
    )
}

// Usage in PayPal component
val paypalConfig = PayPalComponentConfig(
    onSuccess = { data ->
        viewModelScope.launch {
            val validation = validateGeographicRestrictions(
                customerCountry = getCustomerCountry(),
                cartItems = getCartItems()
            )
            
            if (!validation.isValid) {
                showError(
                    "The following items cannot be shipped to your country: " +
                    validation.restrictedItems.joinToString(", ") { it.name }
                )
                return@launch
            }
            
            processPayPalPayment(data)
        }
    }
)
```

### Calculate a risk score

Calculate a comprehensive fraud risk score based on multiple behavioural and historical factors.

```kotlin
data class RiskFactors(
    val velocityScore: Double,
    val locationScore: Double,
    val deviceScore: Double,
    val historyScore: Double
)

data class FraudRiskResult(
    val score: Double,
    val riskLevel: RiskLevel,
    val factors: RiskFactors
)

enum class RiskLevel {
    LOW, MEDIUM, HIGH
}

suspend fun calculateFraudScore(transactionData: TransactionData): FraudRiskResult {
    val factors = RiskFactors(
        velocityScore = checkPaymentVelocity(transactionData.customerEmail),
        locationScore = validateLocation(transactionData.ipAddress),
        deviceScore = analyzeDeviceFingerprint(transactionData.deviceData),
        historyScore = analyzePaymentHistory(transactionData.customerId)
    )
    
    val totalScore = (factors.velocityScore + factors.locationScore + 
                     factors.deviceScore + factors.historyScore) / 4.0
    
    val riskLevel = when {
        totalScore > 0.8 -> RiskLevel.HIGH
        totalScore > 0.5 -> RiskLevel.MEDIUM
        else -> RiskLevel.LOW
    }
    
    return FraudRiskResult(
        score = totalScore,
        riskLevel = riskLevel,
        factors = factors
    )
}

// Usage in PayPal component
val paypalConfig = PayPalComponentConfig(
    onSuccess = { data ->
        viewModelScope.launch {
            val riskResult = calculateFraudScore(
                TransactionData(
                    customerEmail = getCurrentCustomerEmail(),
                    ipAddress = getClientIP(),
                    deviceData = getDeviceData(),
                    customerId = getCurrentCustomerId()
                )
            )
            
            when (riskResult.riskLevel) {
                RiskLevel.HIGH -> {
                    Log.w("PayPal", "High risk transaction detected")
                    // Require additional verification
                    if (!requestAdditionalVerification()) {
                        showError("Additional verification required")
                        return@launch
                    }
                }
                RiskLevel.MEDIUM -> {
                    Log.i("PayPal", "Medium risk transaction")
                    // Optional: Add extra monitoring
                    flagTransactionForReview(data)
                }
                RiskLevel.LOW -> {
                    Log.d("PayPal", "Low risk transaction")
                }
            }
            
            processPayPalPayment(data)
        }
    }
)
```

### Shipping address validation

Validate shipping addresses in the `onShippingAddressChange` callback.

```kotlin
import org.json.JSONObject

val paypalConfig = PayPalComponentConfig(
    onShippingAddressChange = { data ->
        try {
            val jsonObject = JSONObject(data)
            val shippingAddress = jsonObject.optJSONObject("shippingAddress")
            
            if (shippingAddress != null) {
                val countryCode = shippingAddress.optString("countryCode", "")
                val postalCode = shippingAddress.optString("postalCode", "")
                val city = shippingAddress.optString("city", "")
                val state = shippingAddress.optString("state", "")
                
                // Validate country
                if (!isSupportedCountry(countryCode)) {
                    return@PayPalComponentConfig "reject:COUNTRY_NOT_SUPPORTED"
                }
                
                // Validate postal code format
                if (!isValidPostalCode(postalCode, countryCode)) {
                    return@PayPalComponentConfig "reject:INVALID_POSTAL_CODE"
                }
                
                // Check PO Box restrictions
                val addressLine1 = shippingAddress.optString("addressLine1", "")
                if (containsPOBox(addressLine1) && !allowPOBox()) {
                    return@PayPalComponentConfig "reject:PO_BOX_NOT_ALLOWED"
                }
                
                // Validate state for US addresses
                if (countryCode == "US" && !isValidUSState(state)) {
                    return@PayPalComponentConfig "reject:INVALID_STATE"
                }
                
                Log.d("PayPal", "Address validation passed")
                null // Allow the change
            } else {
                null
            }
        } catch (e: Exception) {
            Log.e("PayPal", "Address validation error", e)
            null // Allow on error
        }
    }
)

// Helper functions
fun isSupportedCountry(countryCode: String): Boolean {
    val supportedCountries = setOf("US", "GB", "CA", "AU", "DE", "FR", "IT", "ES")
    return countryCode in supportedCountries
}

fun isValidPostalCode(postalCode: String, countryCode: String): Boolean {
    return when (countryCode) {
        "US" -> postalCode.matches(Regex("^\\d{5}(-\\d{4})?$"))
        "GB" -> postalCode.matches(Regex("^[A-Z]{1,2}\\d[A-Z\\d]? ?\\d[A-Z]{2}$"))
        "CA" -> postalCode.matches(Regex("^[A-Z]\\d[A-Z] ?\\d[A-Z]\\d$"))
        else -> postalCode.isNotEmpty()
    }
}

fun containsPOBox(address: String): Boolean {
    val poBoxPatterns = listOf(
        "P\\.?O\\.? Box",
        "PO Box",
        "Post Office Box"
    )
    return poBoxPatterns.any { pattern ->
        address.contains(Regex(pattern, RegexOption.IGNORE_CASE))
    }
}

fun isValidUSState(state: String): Boolean {
    val validStates = setOf(
        "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA",
        "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD",
        "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ",
        "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC",
        "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY"
    )
    return state.uppercase() in validStates
}
```

### Amount validation

Validate transaction amounts to prevent errors and fraud.

```kotlin
data class AmountValidationResult(
    val isValid: Boolean,
    val errors: List<String>
)

fun validateTransactionAmount(
    amount: Int,
    currency: String,
    customerType: String
): AmountValidationResult {
    val errors = mutableListOf<String>()
    
    // Minimum amount check
    val minimumAmount = when (currency) {
        "USD", "CAD", "AUD" -> 100 // $1.00
        "EUR", "GBP" -> 50  // €0.50 or £0.50
        else -> 100
    }
    
    if (amount < minimumAmount) {
        errors.add("Amount is below minimum of ${minimumAmount / 100.0} $currency")
    }
    
    // Maximum amount check
    val maximumAmount = when (customerType) {
        "new" -> 50000 // $500.00 for new customers
        "verified" -> 100000 // $1000.00 for verified
        "vip" -> Int.MAX_VALUE // No limit for VIP
        else -> 50000
    }
    
    if (amount > maximumAmount) {
        errors.add("Amount exceeds maximum of ${maximumAmount / 100.0} $currency for $customerType customers")
    }
    
    // Check for suspicious amounts (e.g., exactly $9999.99)
    if (amount == 999999) {
        errors.add("Suspicious transaction amount detected")
    }
    
    return AmountValidationResult(
        isValid = errors.isEmpty(),
        errors = errors
    )
}

// Usage
val paypalConfig = PayPalComponentConfig(
    onSuccess = { data ->
        viewModelScope.launch {
            val amountValidation = validateTransactionAmount(
                amount = transactionData.amount,
                currency = transactionData.currency.toString(),
                customerType = getCustomerType()
            )
            
            if (!amountValidation.isValid) {
                showError(amountValidation.errors.joinToString(", "))
                return@launch
            }
            
            processPayPalPayment(data)
        }
    }
)
```

## Validation best practices

1. **Validate early:** Perform validation before showing the PayPal button to prevent user frustration.
2. **Provide clear feedback:** Show specific, actionable error messages to help users correct issues.
3. **Log validation errors:** Track validation failures for debugging and improvement.


```kotlin
fun logValidationError(field: String, error: String, value: Any?) {
    val errorData = mapOf(
        "field" to field,
        "error" to error,
        "value" to value.toString(),
        "timestamp" to System.currentTimeMillis()
    )
    
    analytics.track("validation_error", errorData)
    crashlytics.log("Validation error: $field - $error")
}
```

1. **Handle edge cases:** Consider unusual but valid inputs (e.g., international address formats).
2. **Test thoroughly:** Test validation with various valid and invalid inputs across different devices and Android versions.