# Data validation

Collect and validate the data entered by your customers.

## Overview

By validating data prior to transaction initiation, you can:

* Reduce the risk of fraudulent payments.
* Offer a better user experience, by providing clear feedback that helps customers complete forms correctly.
* Ensure you meet PCI and regulatory requirements.


## Validation methods

There are three key validation methods:

* `getValue()`: Retrieves current field values synchronously.
* `validate()`: Validates field data and returns results.
* `validateWhenSubmit()`: Validates component data during submission.


Validation can happen:

* Automatically on user input (when field is touched and value changes).
* When `submit()` is called.
* When you call validation methods directly.


The following table shows the compatibility of these methods with the different components.

| Component name | `getValue()` | `validate()` | `validateWhenSubmit()` |
|  --- | --- | --- | --- |
| Address |  |  |  |
| Billing address |  |  |  |
| Card brand selector | Returns null |  |  |
| Card consent | Returns Boolean |  |  |
| Card CVC |  |  |  |
| Card expiry date |  |  |  |
| Cardholder name |  |  |  |
| Card number |  |  |  |
| Card-on-file | Returns null |  |  |
| Card submit | Returns null |  |  |
| Click-once | Returns null |  |  |
| Country selection |  |  |  |
| Dynamic card image | Returns null |  |  |
| New card | Returns null |  |  |
| Postcode |  |  |  |
| Pre-fill billing address checkbox | Returns Boolean |  |  |


## Validation result structure


```kotlin
data class ValidationResult(
    /**
     * Whether the validation passed
     */
    val isValid: Boolean,
    
    /**
     * Error message if validation failed
     */
    val errorMessage: String? = null,
    
    /**
     * Error code if validation failed (e.g., "CN01", "CN02")
     * Used for specific error handling and internationalisation
     */
    val errorCode: String? = null,
    
    /**
     * Component ID for tracking which component the validation result belongs to
     */
    val componentId: String? = null,
    
    /**
     * Detailed validation errors (for complex validations like Kount)
     * Maps field names to validation errors
     */
    val validationErrors: Map<String, ValidationFieldError>? = null
) {
    companion object {
        fun success(): ValidationResult {
            return ValidationResult(isValid = true)
        }
        
        fun error(errorMessage: String, componentId: String? = null): ValidationResult {
            return ValidationResult(
                isValid = false,
                errorMessage = errorMessage,
                componentId = componentId
            )
        }
        
        fun error(errorCode: String, errorMessage: String, componentId: String? = null): ValidationResult {
            return ValidationResult(
                isValid = false,
                errorMessage = errorMessage,
                errorCode = errorCode,
                componentId = componentId
            )
        }
    }
}
```

## Common validation scenarios

### Get values before payment processing

Use `getValue()` to collect form data before processing payments.


```kotlin
class PaymentValidationManager(
    private val cardNumberComponent: CardNumberComponent,
    private val expiryDateComponent: CardExpiryDateComponent,
    private val cvcComponent: CardCvcComponent,
    private val holderNameComponent: CardHolderNameComponent,
    private val billingAddressComponent: BillingAddressComponent?
) {
    
    suspend fun processPayment(): PaymentResult {
        // Get individual card field values
        val cardNumber = cardNumberComponent.getValue()
        val expiryDate = expiryDateComponent.getValue()
        val cvc = cvcComponent.getValue()
        val holderName = holderNameComponent.getValue()
        
        // Get complex component values
        val addressData = billingAddressComponent?.getValue()
        
        Log.d("Payment", "Payment data collected: cardNumber=${cardNumber?.let { "****${it.takeLast(4)}" }}, expiryDate=$expiryDate")
        
        // Validate all collected data
        if (!validateCollectedData(cardNumber, expiryDate, cvc, holderName, addressData)) {
            return PaymentResult.ValidationFailed("Invalid payment data")
        }
        
        // Submit payment with collected data
        return submitPayment(cardNumber, expiryDate, cvc, holderName, addressData)
    }
    
    private fun validateCollectedData(
        cardNumber: String?,
        expiryDate: String?,
        cvc: String?,
        holderName: String?,
        addressData: AddressData?
    ): Boolean {
        return cardNumber?.isNotEmpty() == true &&
               expiryDate?.isNotEmpty() == true &&
               cvc?.isNotEmpty() == true &&
               holderName?.isNotEmpty() == true
    }
}
```

### Validate all components before submission

Use `validate()` to check individual components and `validateWhenSubmit()` for submission validation.


```kotlin
class FormValidationExample {
    
    fun validateAllFieldsBeforeSubmission(
        cardNumber: CardNumberComponent,
        expiry: CardExpiryDateComponent,
        cvc: CardCvcComponent,
        holderName: CardHolderNameComponent
    ): Boolean {
        
        val results = mutableListOf<ValidationResult>()
        
        // Validate individual components
        results.addAll(cardNumber.validate())
        results.addAll(expiry.validate())
        results.addAll(cvc.validate())
        results.addAll(holderName.validate())
        
        // Check if all validations passed
        val isAllValid = results.all { it.isValid }
        
        if (!isAllValid) {
            Log.w("Validation", "Form validation failed:")
            results.filter { !it.isValid }.forEach { result ->
                Log.w("Validation", "- ${result.errorCode}: ${result.errorMessage}")
            }
        }
        
        return isAllValid
    }
    
    fun validateForSubmission(components: List<Component<*>>): List<ValidationResult> {
        return components.flatMap { component ->
            component.validateWhenSubmit()
        }
    }
}
```

## Card number validation

### Built-in validation rules

Card number validation follows this priority order:

- Required field validation (CN01): The field can't be empty.
- Format validation (CN02): The field must contain only numeric characters.
- Length validation (CN03): The field must meet card brand length requirements.
- Brand detection (CN04): The card brand must be recognisable.
- Brand acceptance (CN05): The card brand must be in accepted list.
- Luhn algorithm (CN06): The field must pass checksum validation.
- Card restrictions (CN07): The card must match the configured restrictions (owner type and funding source).



```kotlin
data class CardNumberValidations(
    /**
     * Error message when card number is required but not provided - REQUIRED (CN01)
     */
    var required: String = "Card number is required",
    
    /**
     * Error message when card number contains non-numeric characters - INVALID (CN02) 
     */
    var invalid: String = "Please enter a valid card number",
    
    /**
     * Error message when card number is too short for detected card brand - TOO_SHORT (CN03)
     */
    var tooShort: String = "Card number is too short",
    
    /**
     * Error message when card type is not recognised - TYPE_NOT_RECOGNIZED (CN04)
     */
    var typeNotRecognized: String = "Card type not recognised",
    
    /**
     * Error message when card brand is not supported - TYPE_NOT_SUPPORTED (CN05)
     */
    var typeNotSupported: String = "Card type is not supported",
    
    /**
     * Error message when card number fails the Luhn check algorithm - LUHN_FAILED (CN06)
     */
    var luhnFailed: String = "Please enter a valid card number",
    
    /**
     * Error message when card fails restrictions validation - RESTRICTIONS_NOT_MET (CN07)
     * Example: "Corporate, Credit cards are not accepted"
     */
    var restrictionsNotMet: String = "This card type is not accepted"
)
```

### Configuration example


```kotlin
val cardNumberConfig = CardNumberComponentConfig(
    label = "Card Number",
    placeholder = "1234 5678 9012 3456",
    validationEnabled = true,
    formatCardNumber = true,
    acceptedCardBrands = listOf(CardBrand.VISA, CardBrand.MASTERCARD, CardBrand.AMEX),
    validations = CardNumberValidations(
        required = "Please enter your card number",
        invalid = "Please enter a valid card number",
        tooShort = "Your card number is incomplete",
        typeNotRecognized = "Please enter a valid card number",
        typeNotSupported = "This card type is not accepted",
        luhnFailed = "Please check your card number",
        restrictionsNotMet = "This card type is not accepted for this transaction"
    ),
    
    // Callbacks
    onCardBrandChanged = { cardBrand ->
        Log.d("CardNumber", "Card brand detected: $cardBrand")
    },
    onCardBrandCannotRecognised = {
        Log.w("CardNumber", "Unable to recognise card brand")
    },
    
    // Validation callbacks  
    onValidationPassed = { results ->
        Log.d("CardNumber", "Validation passed")
    },
    onValidationFailed = { results ->
        results.forEach { result ->
            Log.w("CardNumber", "Validation failed: ${result.errorMessage}")
        }
    }
)
```

### Card restrictions vs accepted card brands

The SDK provides two ways to control what cards are accepted:

- **`acceptedCardBrands`** (component-level): Validates against specific card brands (Visa, Mastercard, etc.). This is configured in `CardNumberComponentConfig`.
- **`restrictions`** (SDK-level): Validates against card characteristics like owner type (Corporate/Consumer) and funding source (Credit/Debit/Prepaid). This is configured in `PxpSdkConfig` and/or returned from the session.


Both validations work together. If a card passes brand validation but fails restriction validation, it will be rejected with a validation error.


```kotlin
// SDK-level restrictions (in PxpSdkConfig)
val sdkConfig = PxpSdkConfig(
    // ... other config
    restrictions = Restrictions(
        card = RestrictionsCard(
            ownerTypes = listOf(RestrictionOwnerType.CONSUMER),
            fundingSources = listOf(RestrictionFundingSource.CREDIT, RestrictionFundingSource.DEBIT)
        )
    )
)

// Component-level brand acceptance (in CardNumberComponentConfig)
val cardNumberConfig = CardNumberComponentConfig(
    acceptedCardBrands = listOf(CardBrand.VISA, CardBrand.MASTERCARD, CardBrand.AMEX)
)
```

In this example:

- A corporate credit card would be rejected (fails owner type restriction with CN07 error).
- A consumer prepaid Visa would be rejected (fails funding source restriction with CN07 error).
- A consumer credit Discover card would be rejected (fails brand acceptance with CN05 error).
- A consumer credit Visa would be accepted (passes both validations).


#### Restriction validation errors

When a card fails restriction validation, the SDK returns a `CN07` error with a message indicating which restrictions weren't met. For example `"Corporate, Credit cards are not accepted"` if both owner type and funding source restrictions fail. The error message can be customised via the `restrictionsNotMet` property in `CardNumberValidations`

If restrictions are specified in both the session (via backend Sessions API) and the SDK config, they are merged using a union approach. Session restrictions are applied first, followed by SDK-only values.

### Card brand detection and validation

The SDK automatically detects card brands based on number prefixes:

| Card brand | Prefix patterns | Length range |
|  --- | --- | --- |
| Visa | `4` | 13-16 |
| Mastercard | `51-58`, `22-27` | 16 |
| American Express | `34`, `37` | 15 |
| Discover | `6011`, `644-649`, `65` | 16-19 |
| JCB | `3528` | 16-19 |
| Diners | `30`, `36`, `38`, `39` | 14-19 |
| China UnionPay | `62` | 16-19 |


### Luhn algorithm validation

The SDK uses the Luhn algorithm to validate card number checksums:


```kotlin
object LuhnAlgorithm {
    fun isValid(cardNumber: String): Boolean {
        if (cardNumber.isEmpty()) return false
        
        val digits = cardNumber.map { it.toString().toInt() }
        val checksum = digits.reversed()
            .mapIndexed { index, digit ->
                if (index % 2 == 1) {
                    val doubled = digit * 2
                    if (doubled > 9) doubled - 9 else doubled
                } else {
                    digit
                }
            }
            .sum()
        
        return checksum % 10 == 0
    }
}
```

### Using validation in your app


```kotlin
class PaymentValidator {
    
    fun validateCardData(
        cardNumber: String,
        expiryMonth: String,
        expiryYear: String,
        cvc: String
    ): ValidationResult {
        
        // Validate card number
        val cardValidation = validateCardNumber(cardNumber)
        if (!cardValidation.isValid) {
            return cardValidation
        }
        
        // Validate expiry date
        val expiryValidation = validateExpiryDate(expiryMonth, expiryYear)
        if (!expiryValidation.isValid) {
            return expiryValidation
        }
        
        // Validate CVC
        val cvcValidation = validateCvc(cvc)
        if (!cvcValidation.isValid) {
            return cvcValidation
        }
        
        return ValidationResult.success()
    }
    
    private fun validateCardNumber(cardNumber: String): ValidationResult {
        val digitsOnly = cardNumber.replace("\\s".toRegex(), "")
        
        // Check length
        if (!digitsOnly.matches("^[0-9]{13,19}$".toRegex())) {
            return ValidationResult.error(
                errorCode = "CN02",
                errorMessage = "Please enter a valid card number"
            )
        }
        
        // Luhn algorithm check
        if (!LuhnAlgorithm.isValid(digitsOnly)) {
            return ValidationResult.error(
                errorCode = "CN06",
                errorMessage = "Please check your card number"
            )
        }
        
        return ValidationResult.success()
    }
    
    private fun validateExpiryDate(month: String, year: String): ValidationResult {
        val monthInt = month.toIntOrNull() ?: return ValidationResult.error(
            errorCode = "ED01",
            errorMessage = "Invalid expiry month"
        )
        
        val yearInt = year.toIntOrNull() ?: return ValidationResult.error(
            errorCode = "ED02", 
            errorMessage = "Invalid expiry year"
        )
        
        if (monthInt < 1 || monthInt > 12) {
            return ValidationResult.error(
                errorCode = "ED01",
                errorMessage = "Expiry month must be between 1 and 12"
            )
        }
        
        val currentYear = Calendar.getInstance().get(Calendar.YEAR)
        val currentMonth = Calendar.getInstance().get(Calendar.MONTH) + 1
        val fullYear = if (yearInt < 100) 2000 + yearInt else yearInt
        
        if (fullYear < currentYear || (fullYear == currentYear && monthInt < currentMonth)) {
            return ValidationResult.error(
                errorCode = "ED03",
                errorMessage = "Card has expired"
            )
        }
        
        return ValidationResult.success()
    }
    
    private fun validateCvc(cvc: String): ValidationResult {
        if (cvc.isEmpty()) {
            return ValidationResult.error(
                errorCode = "CVC01",
                errorMessage = "CVC is required"
            )
        }
        
        if (!cvc.matches("^[0-9]{3,4}$".toRegex())) {
            return ValidationResult.error(
                errorCode = "CVC02",
                errorMessage = "CVC must be 3 or 4 digits"
            )
        }
        
        return ValidationResult.success()
    }
}
```

## Expiry date validation

### Built-in validation rules

Expiry date validation uses error codes ED01-ED05:

- Format validation (ED01): The field must contain only numeric characters.
- Month validation (ED02): The month must be between 01-12.
- Pattern validation (ED03): The field must match MM/YY format.
- Expiry check (ED04): The date can't be in the past.
- Required validation (ED05): The field can't be empty.



```kotlin
data class CardExpiryDateValidations(
    /** Invalid format - non-numeric characters (ED01) */
    var invalidFormat: String = "Expiry date must contain only digits",

    /** Invalid month - not between 01-12 (ED02) */
    var invalidMonth: String = "Invalid month. Please enter a month between 01 and 12",

    /** Invalid format pattern (ED03) */
    var invalidFormatPattern: String = "Invalid expiry date format. Please use ",

    /** Card expired (ED04) */
    var expired: String = "The card expiry date you entered has already passed. Please enter a valid expiration date",

    /** Required field empty (ED05) */
    var required: String = "Please enter expiry date"
)
```

### Configuration example


```kotlin
val expiryDateConfig = CardExpiryDateComponentConfig(
    label = "Expiry Date",
    formatOptions = ExpiryDateFormat.SHORT_SLASH, // MM/YY
    validations = CardExpiryDateValidations(
        required = "Please provide your card expiry date",
        invalidFormat = "Only numbers are allowed (no letters or symbols)",
        expired = "Your card expiry date has passed",
        invalidMonth = "Month must be between 01-12"
    ),
    
    // Validation callbacks
    onValidationPassed = { 
        Log.d("ExpiryDate", "Validation passed")
    },
    onValidationFailed = { results ->
        results.forEach { result ->
            Log.w("ExpiryDate", "Validation failed: ${result.errorMessage}")
        }
    }
)
```

## CVC validation

### Built-in validation rules

CVC validation includes these checks:

- Required validation: The field can't be empty.
- Format validation: The field must contain only numeric characters.
- Length validation: The field must be 3-4 digits depending on the card brand.



```kotlin
// CVC validation is handled automatically by the component
// The length depends on the detected card brand:
// - Visa, Mastercard, Discover: 3 digits
// - American Express: 4 digits
```

### Configuration example


```kotlin
val cvcConfig = CardCvcComponentConfig(
    label = "CVC",
    placeholder = "123",
    
    // Validation callbacks
    onValidationPassed = { 
        Log.d("CVC", "CVC validation passed")
    },
    onValidationFailed = { results ->
        results.forEach { result ->
            Log.w("CVC", "CVC validation failed: ${result.errorMessage}")
        }
    }
)
```

## Custom validation with onCustomValidation

The `onCustomValidation` callback enables you to implement custom business rule validation after standard SDK component validation passes. This allows you to validate business-specific requirements before payment submission proceeds.

### When to use custom validation

Use `onCustomValidation` to:

- Validate transaction amount limits or thresholds.
- Check customer eligibility for specific payment methods.
- Verify inventory availability before charging.
- Implement fraud prevention rules based on order data.
- Enforce payment policies based on customer account status.
- Validate promotional code or discount eligibility.


### Validation flow order

The SDK follows a specific validation order to ensure all checks are performed systematically:

1. **Standard SDK validation**: Card number, CVV, expiry date, billing address, and other component fields are validated.
2. **Custom validation**: Your `onCustomValidation` callback is invoked (if provided).
3. **Payment submission**: If both standard and custom validation pass, the payment submission proceeds.


The `onCustomValidation` callback is only called after all standard SDK component validations have passed successfully. If any standard validation fails, custom validation is not executed.

### Configuration example


```kotlin
val cardSubmitConfig = CardSubmitComponentConfig(
    // Component references
    cardNumberComponent = cardNumberComponent,
    cardExpiryDateComponent = cardExpiryComponent,
    cardCvcComponent = cardCvcComponent,
    cardHolderNameComponent = cardHolderNameComponent,
    
    // Custom validation callback
    onCustomValidation = {
        Log.d("Validation", "Running custom business rule validation")
        
        // All custom validation checks
        val validationPassed = validateCustomRules()
        
        if (!validationPassed) {
            Log.w("Validation", "Custom validation failed")
        }
        
        validationPassed // Return true to proceed, false to block
    },
    
    // Other callbacks
    onValidation = { results ->
        // Standard validation results
        val allValid = results.all { it.isValid }
        Log.d("Validation", "Standard validation: ${if (allValid) "Passed" else "Failed"}")
    },
    
    onSubmitError = { error ->
        Log.e("Payment", "Submission error: ${error.message}")
    }
)

private fun validateCustomRules(): Boolean {
    // Implement your custom validation logic
    return true
}
```

### Complete validation example

This example includes a comprehensive validation flow with multiple business rules:


```kotlin
class PaymentValidationManager(
    private val checkout: PxpCheckout,
    private val customerRepository: CustomerRepository,
    private val inventoryService: InventoryService
) {
    
    fun createCardSubmitConfig(): CardSubmitComponentConfig {
        return CardSubmitComponentConfig(
            // Component references
            cardNumberComponent = cardNumberComponent,
            cardExpiryDateComponent = expiryComponent,
            cardCvcComponent = cvcComponent,
            
            // Custom validation with comprehensive business rules
            onCustomValidation = {
                Log.d("Payment", "Starting custom validation checks...")
                
                try {
                    // Check 1: Validate transaction amount
                    if (!validateTransactionAmount()) {
                        showValidationError("Transaction amount validation failed")
                        return@CardSubmitComponentConfig false
                    }
                    
                    // Check 2: Verify customer eligibility
                    if (!validateCustomerEligibility()) {
                        showValidationError("Customer not eligible for this payment method")
                        return@CardSubmitComponentConfig false
                    }
                    
                    // Check 3: Validate inventory availability
                    if (!validateInventoryAvailability()) {
                        showValidationError("Items no longer available")
                        return@CardSubmitComponentConfig false
                    }
                    
                    // Check 4: Verify payment limits
                    if (!validatePaymentLimits()) {
                        showValidationError("Payment exceeds allowed limits")
                        return@CardSubmitComponentConfig false
                    }
                    
                    Log.d("Payment", "All custom validation checks passed")
                    return@CardSubmitComponentConfig true
                    
                } catch (e: Exception) {
                    Log.e("Payment", "Custom validation error: ${e.message}", e)
                    showValidationError("Validation error occurred")
                    return@CardSubmitComponentConfig false
                }
            },
            
            // Standard validation callback
            onValidation = { results ->
                val failedValidations = results.filter { !it.isValid }
                
                if (failedValidations.isNotEmpty()) {
                    Log.w("Payment", "Standard validation failed:")
                    failedValidations.forEach { result ->
                        Log.w("Payment", "  - ${result.componentId}: ${result.errorMessage}")
                    }
                } else {
                    Log.d("Payment", "Standard validation passed - proceeding to custom validation")
                }
            },
            
            onSubmitError = { error ->
                Log.e("Payment", "Payment submission error: ${error.message}")
                handlePaymentError(error)
            },
            
            onPostAuthorisation = { result ->
                when (result) {
                    is MerchantSubmitResult -> handlePaymentSuccess(result)
                    is FailedSubmitResult -> handlePaymentFailure(result)
                    else -> handleUnknownResult(result)
                }
            }
        )
    }
    
    private fun validateTransactionAmount(): Boolean {
        val amount = checkout.getSdkConfig().transactionData.amount
        val currency = checkout.getSdkConfig().transactionData.currency
        
        // Example: Check minimum and maximum amounts
        val minAmount = 1.0
        val maxAmount = 10000.0
        
        if (amount < minAmount) {
            Log.w("Payment", "Amount $amount $currency is below minimum $minAmount")
            return false
        }
        
        if (amount > maxAmount) {
            Log.w("Payment", "Amount $amount $currency exceeds maximum $maxAmount")
            return false
        }
        
        Log.d("Payment", "Transaction amount validation passed: $amount $currency")
        return true
    }
    
    private fun validateCustomerEligibility(): Boolean {
        val customer = customerRepository.getCurrentCustomer()
        
        // Example: Check customer account status
        if (customer.accountStatus == AccountStatus.SUSPENDED) {
            Log.w("Payment", "Customer account is suspended")
            return false
        }
        
        if (customer.accountStatus == AccountStatus.PAYMENT_RESTRICTED) {
            Log.w("Payment", "Customer has payment restrictions")
            return false
        }
        
        // Example: Check customer tier limits
        val amount = checkout.getSdkConfig().transactionData.amount
        val tierLimit = customer.tier.maxTransactionAmount
        
        if (amount > tierLimit) {
            Log.w("Payment", "Amount exceeds customer tier limit: $tierLimit")
            return false
        }
        
        Log.d("Payment", "Customer eligibility validation passed")
        return true
    }
    
    private fun validateInventoryAvailability(): Boolean {
        val cartItems = getCartItems()
        
        // Check if all items are still in stock
        val unavailableItems = cartItems.filter { item ->
            !inventoryService.isAvailable(item.productId, item.quantity)
        }
        
        if (unavailableItems.isNotEmpty()) {
            Log.w("Payment", "Items not available: ${unavailableItems.map { it.name }}")
            return false
        }
        
        Log.d("Payment", "Inventory availability validation passed")
        return true
    }
    
    private fun validatePaymentLimits(): Boolean {
        val customer = customerRepository.getCurrentCustomer()
        val amount = checkout.getSdkConfig().transactionData.amount
        
        // Example: Check daily transaction limit
        val todaysTotal = customerRepository.getTodaysTransactionTotal(customer.id)
        val dailyLimit = 50000.0
        
        if (todaysTotal + amount > dailyLimit) {
            Log.w("Payment", "Would exceed daily limit: ${todaysTotal + amount} > $dailyLimit")
            return false
        }
        
        Log.d("Payment", "Payment limits validation passed")
        return true
    }
    
    private fun showValidationError(message: String) {
        // Display error to user
        Log.e("Payment", "Validation error: $message")
        // Update UI to show error message
    }
    
    private fun handlePaymentError(error: BaseSdkException) {
        // Handle payment submission error
    }
    
    private fun handlePaymentSuccess(result: MerchantSubmitResult) {
        // Handle successful payment
    }
    
    private fun handlePaymentFailure(result: FailedSubmitResult) {
        // Handle failed payment
    }
    
    private fun handleUnknownResult(result: SubmitResult) {
        // Handle unknown result type
    }
    
    private fun getCartItems(): List<CartItem> {
        // Return cart items
        return emptyList()
    }
}

data class CartItem(
    val productId: String,
    val name: String,
    val quantity: Int
)

enum class AccountStatus {
    ACTIVE,
    SUSPENDED,
    PAYMENT_RESTRICTED
}
```

### Displaying custom validation errors

When custom validation fails, you should provide clear feedback to the user:


```kotlin
class ValidationErrorHandler {
    
    fun setupCustomValidation(
        cardSubmitConfig: CardSubmitComponentConfig,
        errorDisplay: ErrorDisplayView
    ) {
        cardSubmitConfig.onCustomValidation = {
            val validationResult = performCustomValidation()
            
            if (!validationResult.isValid) {
                // Show user-friendly error message
                errorDisplay.showError(
                    title = "Payment Validation Failed",
                    message = validationResult.userMessage,
                    actionButton = "Update Details"
                )
                
                // Log technical details for debugging
                Log.w("Payment", "Custom validation failed: ${validationResult.technicalMessage}")
                
                // Track validation failure for analytics
                trackValidationFailure(validationResult)
                
                false // Block submission
            } else {
                // Clear any previous errors
                errorDisplay.clearErrors()
                true // Allow submission
            }
        }
    }
    
    private fun performCustomValidation(): CustomValidationResult {
        // Implement validation logic
        return CustomValidationResult(
            isValid = true,
            userMessage = "",
            technicalMessage = ""
        )
    }
    
    private fun trackValidationFailure(result: CustomValidationResult) {
        // Track analytics event
    }
}

data class CustomValidationResult(
    val isValid: Boolean,
    val userMessage: String,
    val technicalMessage: String
)

interface ErrorDisplayView {
    fun showError(title: String, message: String, actionButton: String)
    fun clearErrors()
}
```

## Best practices

### Real-time validation


```kotlin
class FormValidationManager {
    
    fun setupRealTimeValidation(
        cardNumber: CardNumberComponent,
        expiry: CardExpiryDateComponent,
        cvc: CardCvcComponent
    ) {
        // Validate as user types
        cardNumber.config.onValidationFailed = { results ->
            showFieldError(cardNumber, results.firstOrNull()?.errorMessage)
        }
        
        cardNumber.config.onValidationPassed = {
            hideFieldError(cardNumber)
        }
        
        // Similar for other components
        expiry.config.onValidationFailed = { results ->
            showFieldError(expiry, results.firstOrNull()?.errorMessage)
        }
        
        cvc.config.onValidationFailed = { results ->
            showFieldError(cvc, results.firstOrNull()?.errorMessage)
        }
    }
    
    private fun showFieldError(component: Component<*>, message: String?) {
        // Update UI to show error state
        Log.w("Validation", "Field ${component.componentType}: $message")
    }
    
    private fun hideFieldError(component: Component<*>) {
        // Update UI to hide error state
        Log.d("Validation", "Field ${component.componentType} is valid")
    }
}
```

### Form submission validation


```kotlin
suspend fun validateAndSubmitPayment(
    cardNumber: CardNumberComponent,
    expiry: CardExpiryDateComponent,
    cvc: CardCvcComponent,
    holderName: CardHolderNameComponent
): PaymentResult {
    
    // Validate all components before submission
    val allResults = mutableListOf<ValidationResult>()
    allResults.addAll(cardNumber.validateWhenSubmit())
    allResults.addAll(expiry.validateWhenSubmit())
    allResults.addAll(cvc.validateWhenSubmit())
    allResults.addAll(holderName.validateWhenSubmit())
    
    val failedResults = allResults.filter { !it.isValid }
    
    if (failedResults.isNotEmpty()) {
        Log.w("Payment", "Form validation failed:")
        failedResults.forEach { result ->
            Log.w("Payment", "- ${result.errorCode}: ${result.errorMessage}")
        }
        return PaymentResult.ValidationFailed(failedResults)
    }
    
    // All validations passed, proceed with payment
    return processPayment()
}
```