Error Handling

Complete guide to handling errors in the ATH Móvil unofficial library.

Exception Hierarchy

All exceptions inherit from ATHMovilError:

ATHMovilError (base exception)
├── AuthenticationError          # Invalid tokens, auth failures
├── ValidationError              # Invalid amounts, phone, metadata
├── TransactionError             # Transaction state errors
├── TimeoutError                 # Network or polling timeout
├── RateLimitError               # Too many requests
├── NetworkError                 # Connection issues
└── InternalServerError          # ATH Móvil server errors

Basic Error Handling

Catch Specific Exceptions

from athm import (
    ATHMovilClient,
    AuthenticationError,
    ValidationError,
    TransactionError,
    TimeoutError,
    ATHMovilError
)

client = ATHMovilClient(public_token="...")

try:
    payment = client.create_payment(
        total="50.00",
        phone_number="7875551234",
        subtotal="50.00",
        tax="0.00"
    )

except ValidationError as e:
    # Invalid amount, phone, or metadata
    print(f"Validation failed: {e}")
    print(f"Error code: {e.error_code}")

except AuthenticationError as e:
    # Invalid or expired token
    print(f"Authentication failed: {e}")
    print("Check your PUBLIC_TOKEN")

except TransactionError as e:
    # Transaction state errors
    print(f"Transaction error: {e}")

except TimeoutError as e:
    # Network or polling timeout
    print(f"Request timed out: {e}")

except ATHMovilError as e:
    # Catch-all for any API error
    print(f"ATH Móvil error: {e}")

finally:
    client.close()

Exception Attributes

All exceptions include:

try:
    payment = client.create_payment(...)
except ATHMovilError as e:
    print(f"Message: {e}")
    print(f"Error code: {e.error_code}")  # May be None
    print(f"Status code: {e.status_code}")  # May be None
    print(f"Response: {e.response_data}")  # Full API response

Complete Error Code Reference

Authentication Errors

Authentication failures, invalid or expired tokens.

Error Code Description Solution
token.invalid.header No authorization header provided Ensure public_token is set when creating client
token.expired Authorization token has expired Generate new token in ATH Business portal
BTRA_0401 Authorization token issue Check token format and validity
BTRA_0402 Authorization token issue Check token format and validity
BTRA_0403 Authorization token issue Check token format and validity
BTRA_0017 Invalid authorization token Use correct public token from ATH Business

Recovery Strategy:

try:
    payment = client.create_payment(...)
except AuthenticationError as e:
    if "expired" in str(e).lower():
        # Token expired - user needs new one
        notify_admin("ATH token expired, please update")
    else:
        # Invalid token
        log_error(f"Check ATH_PUBLIC_TOKEN: {e}")
    raise  # Can't recover automatically

Validation Errors

Invalid input data (amounts, phone numbers, metadata).

Error Code Description Solution
BTRA_0001 Amount is below minimum ($1.00) Set total to at least "1.00"
BTRA_0004 Amount exceeds maximum ($1,500.00) Set total to maximum "1500.00" or split payment
BTRA_0006 Invalid format or required body missing Check all required fields are provided
BTRA_0013 Amount cannot be zero Set total to a positive value
BTRA_0038 Metadata exceeds 40 characters Truncate metadata1 or metadata2 to 40 chars
BTRA_0040 Message exceeds 50 characters Truncate refund message to 50 chars

Recovery Strategy:

try:
    payment = client.create_payment(
        total="50.00",
        phone_number="7875551234",
        subtotal="50.00",
        tax="0.00",
        metadata1=order_id  # Might be too long
    )
except ValidationError as e:
    if "BTRA_0038" in str(e):
        # Metadata too long, truncate and retry
        payment = client.create_payment(
            total="50.00",
            phone_number="7875551234",
            subtotal="50.00",
            tax="0.00",
            metadata1=order_id[:40]  # Max 40 characters
        )
    elif "BTRA_0004" in str(e):
        # Amount too high
        print("Payment exceeds $1500 limit, please split")
        raise
    else:
        raise

Transaction Errors

Payment state and lifecycle errors.

Error Code Description Solution
BTRA_0007 Transaction ID does not exist Check ecommerce_id is correct
BTRA_0031 Ecommerce ID does not exist Verify payment was created successfully
BTRA_0032 Transaction status is not confirmed Wait for customer to confirm before authorizing
BTRA_0037 Cannot confirm cancelled or failed transaction Payment was cancelled, create new one
BTRA_0039 Transaction timeout has expired Payment expired, create new one

Recovery Strategy:

try:
    result = client.authorize_payment(ecommerce_id)
except TransactionError as e:
    if "BTRA_0032" in str(e):
        # Not confirmed yet, wait for confirmation
        print("Waiting for customer confirmation...")
        client.wait_for_confirmation(ecommerce_id, timeout=60)
        result = client.authorize_payment(ecommerce_id)

    elif "BTRA_0037" in str(e) or "BTRA_0039" in str(e):
        # Cancelled or expired, restart
        print("Payment cancelled/expired, creating new one")
        new_payment = client.create_payment(...)
        return new_payment

    elif "BTRA_0031" in str(e):
        # ID doesn't exist - payment creation failed
        log_error(f"Invalid ecommerce_id: {ecommerce_id}")
        raise

    else:
        raise

Business Errors

Business account configuration issues.

Error Code Description Solution
BTRA_0003 Customer card cannot be same as business card Customer must use different payment method
BTRA_0009 Business is not active Contact ATH Business support
BTRA_0010 Business is not active Contact ATH Business support

Recovery Strategy:

try:
    payment = client.create_payment(...)
except ATHMovilError as e:
    if "BTRA_0003" in str(e):
        # Customer using same card as merchant
        return "Cannot use same card for payment, please use different account"

    elif "BTRA_0009" in str(e) or "BTRA_0010" in str(e):
        # Business account inactive
        notify_admin("ATH Business account is inactive")
        raise

    else:
        raise

Network Errors

Connection and communication issues.

Error Code Description Solution
BTRA_9998 Communication error with ATH Móvil services Retry request, check network connectivity

Recovery Strategy:

import time

max_retries = 3
for attempt in range(max_retries):
    try:
        payment = client.create_payment(...)
        break
    except NetworkError as e:
        if attempt < max_retries - 1:
            wait = 2 ** attempt  # Exponential backoff
            print(f"Network error, retrying in {wait}s...")
            time.sleep(wait)
        else:
            print("Network error persists after retries")
            raise

Internal Server Errors

ATH Móvil server issues.

Error Code Description Solution
BTRA_9999 Internal server error Wait and retry, contact ATH support if persists

Recovery Strategy:

try:
    payment = client.create_payment(...)
except InternalServerError as e:
    # ATH Móvil server issue, retry with backoff
    print("Server error, will retry shortly")
    time.sleep(5)
    payment = client.create_payment(...)  # Retry once

Complete Error Handling Pattern

Here's a production-ready error handling pattern:

from athm import (
    ATHMovilClient,
    AuthenticationError,
    ValidationError,
    TransactionError,
    TimeoutError,
    NetworkError,
    InternalServerError,
    ATHMovilError
)
import time
from typing import Optional


def create_payment_with_retry(
    client: ATHMovilClient,
    amount: str,
    phone_number: str,
    max_retries: int = 3
) -> Optional[str]:
    """
    Create payment with comprehensive error handling.

    Returns ecommerce_id on success, None on unrecoverable error.
    """
    for attempt in range(max_retries):
        try:
            payment = client.create_payment(
                total=amount,
                phone_number=phone_number,
                subtotal=amount,
                tax="0.00",
                items=[{"name": "Item", "description": "Description", "quantity": "1", "price": amount}]
            )
            return payment.data.ecommerce_id

        except ValidationError as e:
            # Invalid data - don't retry, fix input
            print(f"Validation error: {e}")
            if "BTRA_0001" in str(e):
                print("Amount too low (min $1.00)")
            elif "BTRA_0004" in str(e):
                print("Amount too high (max $1500.00)")
            elif "BTRA_0038" in str(e):
                print("Metadata too long (max 40 chars)")
            return None

        except AuthenticationError as e:
            # Invalid token - don't retry, need new token
            print(f"Authentication error: {e}")
            print("Check your PUBLIC_TOKEN configuration")
            return None

        except NetworkError as e:
            # Network issue - retry with backoff
            if attempt < max_retries - 1:
                wait = 2 ** attempt
                print(f"Network error, retry {attempt + 1}/{max_retries} in {wait}s")
                time.sleep(wait)
                continue
            print("Network error persists, giving up")
            return None

        except InternalServerError as e:
            # Server error - retry with backoff
            if attempt < max_retries - 1:
                wait = 5
                print(f"Server error, retry {attempt + 1}/{max_retries} in {wait}s")
                time.sleep(wait)
                continue
            print("Server error persists, giving up")
            return None

        except TimeoutError as e:
            # Timeout - retry
            if attempt < max_retries - 1:
                print(f"Timeout, retry {attempt + 1}/{max_retries}")
                continue
            print("Timeout persists, giving up")
            return None

        except ATHMovilError as e:
            # Other errors
            print(f"ATH Móvil error: {e}")
            print(f"Error code: {e.error_code}")
            return None

    return None


def complete_payment_flow(
    client: ATHMovilClient,
    amount: str,
    phone_number: str
) -> Optional[str]:
    """
    Complete payment flow with error handling.

    Returns reference_number on success.
    """
    # 1. Create payment
    ecommerce_id = create_payment_with_retry(client, amount, phone_number)
    if not ecommerce_id:
        return None

    try:
        # 2. Wait for confirmation
        client.wait_for_confirmation(ecommerce_id, timeout=300)

        # 3. Authorize payment
        result = client.authorize_payment(ecommerce_id)
        return result.data.reference_number

    except TimeoutError:
        print("Customer didn't confirm, cancelling")
        client.cancel_payment(ecommerce_id)
        return None
    except TransactionError as e:
        print(f"Transaction error: {e}")
        return None
    except Exception as e:
        # Unexpected error, try to clean up
        print(f"Unexpected error: {e}")
        try:
            client.cancel_payment(ecommerce_id)
        except:
            pass  # Best effort cleanup
        return None


# Usage
if __name__ == "__main__":
    client = ATHMovilClient(public_token="...")

    try:
        ref = complete_payment_flow(
            client,
            amount="25.00",
            phone_number="7875551234"
        )

        if ref:
            print(f"Payment successful: {ref}")
        else:
            print("Payment failed")

    finally:
        client.close()

Logging Errors

Safe Logging Pattern

Never log sensitive data (tokens, full phone numbers):

from athm import ATHMovilClient, ATHMovilError
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

client = ATHMovilClient(public_token="...")

try:
    payment = client.create_payment(
        total="50.00",
        phone_number="7875551234",
        subtotal="50.00",
        tax="0.00"
    )
    logger.info(f"Payment created: {payment.data.ecommerce_id}")

except ATHMovilError as e:
    # Log error safely
    logger.error(
        "Payment creation failed",
        extra={
            "error_code": e.error_code,
            "status_code": e.status_code,
            "error_message": str(e),
            # Don't log full response, may contain sensitive data
        }
    )

Structured Logging

import structlog

logger = structlog.get_logger()

try:
    payment = client.create_payment(...)
    logger.info(
        "payment_created",
        ecommerce_id=payment.data.ecommerce_id,
        amount="50.00"
    )
except ATHMovilError as e:
    logger.error(
        "payment_creation_failed",
        error_code=e.error_code,
        error_type=type(e).__name__,
        retryable=isinstance(e, (NetworkError, InternalServerError, TimeoutError))
    )

Testing Error Scenarios

Mock Errors for Testing

from unittest.mock import Mock, patch
from athm import ATHMovilClient, ValidationError

def test_amount_too_high():
    client = ATHMovilClient(public_token="test")

    with patch.object(client, '_make_request') as mock_request:
        # Mock API error response
        mock_request.side_effect = ValidationError(
            "Amount exceeds maximum limit",
            error_code="BTRA_0004",
            status_code=400
        )

        with pytest.raises(ValidationError) as exc_info:
            client.create_payment(
                total="2000.00",  # Over limit
                phone_number="7875551234",
                subtotal="2000.00",
                tax="0.00"
            )

        assert exc_info.value.error_code == "BTRA_0004"

Common Scenarios

Scenario: Amount Validation

from decimal import Decimal

def validate_amount(amount: str) -> str:
    """Validate and format amount before payment."""
    decimal_amount = Decimal(amount)

    if decimal_amount < Decimal("1.00"):
        raise ValueError("Amount must be at least $1.00")
    elif decimal_amount > Decimal("1500.00"):
        raise ValueError("Amount cannot exceed $1,500.00")

    # Format to 2 decimals
    return f"{decimal_amount:.2f}"

# Usage
try:
    validated = validate_amount("50")  # "50.00"
    payment = client.create_payment(
        total=validated,
        ...
    )
except ValueError as e:
    print(f"Invalid amount: {e}")

Scenario: Polling Timeout

from athm import TimeoutError

# Quick checkout: 2 minute timeout
try:
    client.wait_for_confirmation(ecommerce_id, timeout=120)
except TimeoutError:
    # Graceful handling - send reminder and extend timeout
    print("Payment timed out, sending reminder...")
    send_sms_reminder(phone_number)

    # Extended wait (extra 5 minutes)
    try:
        client.wait_for_confirmation(ecommerce_id, timeout=300)
    except TimeoutError:
        # Give up
        print("Extended timeout reached, cancelling payment")
        client.cancel_payment(ecommerce_id)

Scenario: Refund Errors

from athm import TransactionError

try:
    refund = client.refund_payment(
        reference_number="123456",
        amount="50.00",
        message="Refund for order #123"
    )
except TransactionError as e:
    if "not found" in str(e).lower():
        print("Invalid reference number")
    elif "already refunded" in str(e).lower():
        print("Payment was already refunded")
    else:
        print(f"Refund failed: {e}")

Next Steps