Skip to content

Abstraction

"""
Abstraction hides complex implementation details while exposing only the essential features and functionality.
It provides a clear separation between what an object does and how it does it.

Types of Abstraction:
    Interface Abstraction: Defining contracts without implementation
    Implementation Hiding: Concealing complex internal workings
    Data Abstraction: Representing essential features without background details
"""

from abc import ABC, abstractmethod
from typing import List, Optional, Protocol
from enum import Enum
import uuid
from datetime import datetime


# Enum for order status
class OrderStatus(Enum):
    PENDING = "pending"
    CONFIRMED = "confirmed"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"


# Abstract base class defining payment interface
class PaymentProcessor(ABC):
    """
    Abstract base class for payment processing.
    Defines the contract that all payment processors must follow.
    """

    @abstractmethod
    def validate_payment_details(self, payment_details: dict) -> bool:
        """Validate payment information before processing."""
        pass

    @abstractmethod
    def process_payment(self, amount: float, payment_details: dict) -> dict:
        """Process the payment and return transaction details."""
        pass

    @abstractmethod
    def refund_payment(self, transaction_id: str, amount: float) -> dict:
        """Process a refund for a previous transaction."""
        pass

    @property
    def supported_currencies(self) -> List[str]:
        """List of supported currency codes."""
        pass

    @property
    def processor_name(self) -> str:
        """Name of the payment processor."""
        pass

    def generate_transaction_id(self) -> str:
        """Generate a unique transaction ID."""
        return f"TXN_{uuid.uuid4().hex[:12].upper()}"

    def validate_amount(self, amount: float) -> bool:
        """Common validation for payment amounts."""
        return amount > 0 and amount <= 10000  # Max $10,000 per transaction


# Concrete implementation - Credit Card Processor
class CreditCardProcessor(PaymentProcessor):
    """Credit card payment processor implementation."""

    def __init__(self):
        self._supported_currencies = ["USD", "EUR", "GBP", "CAD"]
        self._processor_name = "CreditCard Gateway v2.1"

    @property
    def supported_currencies(self) -> List[str]:
        return self._supported_currencies.copy()

    @property
    def processor_name(self) -> str:
        return self._processor_name

    def validate_payment_details(self, payment_details: dict) -> bool:
        """Validate credit card details."""
        required_fields = [
            "card_number",
            "expiry_month",
            "expiry_year",
            "cvv",
            "cardholder_name",
        ]

        # Check all required fields are present
        if not all(field in payment_details for field in required_fields):
            return False

        # Validate card number (simplified - just check length)
        card_number = payment_details["card_number"].replace(" ", "").replace("-", "")
        if not (13 <= len(card_number) <= 19 and card_number.isdigit()):
            return False

        # Validate expiry date
        try:
            month = int(payment_details["expiry_month"])
            year = int(payment_details["expiry_year"])
            if not (1 <= month <= 12) or year < datetime.now().year:
                return False
        except ValueError:
            return False

        # Validate CVV
        cvv = payment_details["cvv"]
        if not (3 <= len(cvv) <= 4 and cvv.isdigit()):
            return False

        return True

    def process_payment(self, amount: float, payment_details: dict) -> dict:
        """Process credit card payment."""
        if not self.validate_amount(amount):
            raise ValueError("Invalid payment amount")

        if not self.validate_payment_details(payment_details):
            raise ValueError("Invalid payment details")

        # Simulate payment processing
        transaction_id = self.generate_transaction_id()

        # Simulated processing logic (in reality, this would call external APIs)
        success = True  # Simplified - assume success

        return {
            "transaction_id": transaction_id,
            "status": "completed" if success else "failed",
            "amount": amount,
            "currency": "USD",
            "processor": self.processor_name,
            "timestamp": datetime.now().isoformat(),
            "card_last_four": payment_details["card_number"][-4:],
        }

    def refund_payment(self, transaction_id: str, amount: float) -> dict:
        """Process credit card refund."""
        return {
            "refund_id": self.generate_transaction_id(),
            "original_transaction": transaction_id,
            "refund_amount": amount,
            "status": "completed",
            "processor": self.processor_name,
            "timestamp": datetime.now().isoformat(),
        }


# Another concrete implementation - Digital Wallet Processor
class DigitalWalletProcessor(PaymentProcessor):
    """Digital wallet payment processor implementation."""

    def __init__(self):
        self._supported_currencies = ["USD", "EUR", "BTC", "ETH"]
        self._processor_name = "DigitalWallet Pro"

    @property
    def supported_currencies(self) -> List[str]:
        return self._supported_currencies.copy()

    @property
    def processor_name(self) -> str:
        return self._processor_name

    def validate_payment_details(self, payment_details: dict) -> bool:
        """Validate digital wallet details."""
        required_fields = ["wallet_address", "wallet_type", "pin"]

        if not all(field in payment_details for field in required_fields):
            return False

        # Validate wallet address format (simplified)
        wallet_address = payment_details["wallet_address"]
        if len(wallet_address) < 26 or len(wallet_address) > 62:
            return False

        # Validate PIN
        pin = payment_details["pin"]
        if not (4 <= len(pin) <= 8 and pin.isdigit()):
            return False

        return True

    def process_payment(self, amount: float, payment_details: dict) -> dict:
        """Process digital wallet payment."""
        if not self.validate_amount(amount):
            raise ValueError("Invalid payment amount")

        if not self.validate_payment_details(payment_details):
            raise ValueError("Invalid payment details")

        transaction_id = self.generate_transaction_id()

        return {
            "transaction_id": transaction_id,
            "status": "completed",
            "amount": amount,
            "currency": "USD",
            "processor": self.processor_name,
            "timestamp": datetime.now().isoformat(),
            "wallet_type": payment_details["wallet_type"],
        }

    def refund_payment(self, transaction_id: str, amount: float) -> dict:
        """Process digital wallet refund."""
        return {
            "refund_id": self.generate_transaction_id(),
            "original_transaction": transaction_id,
            "refund_amount": amount,
            "status": "completed",
            "processor": self.processor_name,
            "timestamp": datetime.now().isoformat(),
        }


# Protocol for type hinting (Python 3.8+) - another form of abstraction
class Discountable(Protocol):
    """Protocol defining interface for objects that can have discounts applied."""

    def apply_discount(self, percentage: float) -> float:
        """Apply discount and return new price."""
        ...

    def get_original_price(self) -> float:
        """Get the original price before discount."""
        ...


# High-level abstraction - Order Management System
class Order:
    """
    High-level abstraction for order management.
    Hides complex payment processing and inventory management details.
    """

    def __init__(self, customer_id: str, items: List[dict]):
        self.order_id = f"ORD_{uuid.uuid4().hex[:8].upper()}"
        self.customer_id = customer_id
        self.items = items
        self.status = OrderStatus.PENDING
        self.created_at = datetime.now()
        self.total_amount = self._calculate_total()
        self.payment_processor: Optional[PaymentProcessor] = None
        self.transaction_details: Optional[dict] = None

    def _calculate_total(self) -> float:
        """Private method to calculate order total - implementation detail hidden."""
        total = sum(item["price"] * item["quantity"] for item in self.items)
        tax = total * 0.08  # 8% tax
        return round(total + tax, 2)

    def set_payment_processor(self, processor: PaymentProcessor) -> None:
        """Set the payment processor for this order."""
        self.payment_processor = processor

    def process_payment(self, payment_details: dict) -> bool:
        """
        High-level payment processing - abstracts away processor-specific details.
        Client code doesn't need to know how different payment types work.
        """
        if not self.payment_processor:
            raise ValueError("No payment processor configured")

        if self.status != OrderStatus.PENDING:
            raise ValueError(
                f"Cannot process payment for order with status: {self.status.value}"
            )

        try:
            # Abstract payment processing - details hidden from client
            self.transaction_details = self.payment_processor.process_payment(
                self.total_amount, payment_details
            )

            if self.transaction_details["status"] == "completed":
                self.status = OrderStatus.CONFIRMED
                self._update_inventory()  # Hidden implementation detail
                self._send_confirmation_email()  # Hidden implementation detail
                return True
            else:
                return False

        except Exception as e:
            print(f"Payment processing failed: {e}")
            return False

    def _update_inventory(self) -> None:
        """Private method - inventory management abstracted away."""
        print(f"Updating inventory for order {self.order_id}")
        # Complex inventory management logic hidden here

    def _send_confirmation_email(self) -> None:
        """Private method - email sending abstracted away."""
        print(f"Sending confirmation email for order {self.order_id}")
        # Complex email sending logic hidden here

    def ship_order(self) -> bool:
        """Ship the order - abstracts shipping complexity."""
        if self.status != OrderStatus.CONFIRMED:
            return False

        # Abstract shipping process
        self._generate_shipping_label()
        self._schedule_pickup()
        self.status = OrderStatus.SHIPPED
        return True

    def _generate_shipping_label(self) -> None:
        """Hidden shipping implementation detail."""
        print(f"Generating shipping label for order {self.order_id}")

    def _schedule_pickup(self) -> None:
        """Hidden shipping implementation detail."""
        print(f"Scheduling pickup for order {self.order_id}")

    def cancel_order(self) -> bool:
        """Cancel order and process refund if necessary."""
        if self.status == OrderStatus.DELIVERED:
            return False

        if self.status in [OrderStatus.CONFIRMED, OrderStatus.SHIPPED]:
            # Process refund if payment was taken
            if self.transaction_details and self.payment_processor:
                refund_result = self.payment_processor.refund_payment(
                    self.transaction_details["transaction_id"], self.total_amount
                )
                print(f"Refund processed: {refund_result['refund_id']}")

        self.status = OrderStatus.CANCELLED
        return True

    def get_order_summary(self) -> dict:
        """Get high-level order information - implementation details abstracted."""
        return {
            "order_id": self.order_id,
            "customer_id": self.customer_id,
            "status": self.status.value,
            "total_amount": self.total_amount,
            "item_count": len(self.items),
            "created_at": self.created_at.isoformat(),
            "payment_processor": self.payment_processor.processor_name
            if self.payment_processor
            else None,
        }


# Factory pattern for creating payment processors (another abstraction)
class PaymentProcessorFactory:
    """
    Factory class that abstracts the creation of payment processors.
    Client code doesn't need to know about specific processor classes.
    """

    _processors = {
        "credit_card": CreditCardProcessor,
        "digital_wallet": DigitalWalletProcessor,
    }

    @classmethod
    def create_processor(cls, processor_type: str) -> PaymentProcessor:
        """Create a payment processor instance."""
        if processor_type not in cls._processors:
            raise ValueError(f"Unknown processor type: {processor_type}")

        return cls._processors[processor_type]()

    @classmethod
    def get_available_processors(cls) -> List[str]:
        """Get list of available processor types."""
        return list(cls._processors.keys())


# Usage example demonstrating abstraction
if __name__ == "__main__":
    print("=== Abstraction Demonstration ===\n")

    # 1. High-level order creation - complexity abstracted away
    order_items = [
        {"name": "Laptop", "price": 999.99, "quantity": 1},
        {"name": "Mouse", "price": 29.99, "quantity": 2},
        {"name": "Keyboard", "price": 79.99, "quantity": 1},
    ]

    order = Order("CUST_12345", order_items)
    print(f"Order created: {order.get_order_summary()}")

    print("\n" + "-" * 50 + "\n")

    # 2. Abstract payment processing - client doesn't need to know processor details
    print("Available payment processors:")
    for processor_type in PaymentProcessorFactory.get_available_processors():
        print(f"  - {processor_type}")

    # Create payment processor using factory (abstraction)
    cc_processor = PaymentProcessorFactory.create_processor("credit_card")
    order.set_payment_processor(cc_processor)

    # Process payment - implementation details hidden
    credit_card_details = {
        "card_number": "4532 1234 5678 9012",
        "expiry_month": "12",
        "expiry_year": "2025",
        "cvv": "123",
        "cardholder_name": "John Doe",
    }

    print("\nProcessing credit card payment...")
    if order.process_payment(credit_card_details):
        print("✅ Payment successful!")
        print(f"Order status: {order.status.value}")
    else:
        print("❌ Payment failed!")

    print("\n" + "-" * 50 + "\n")

    # 3. Demonstrate polymorphic behavior with different processors
    order2 = Order("CUST_67890", [{"name": "Book", "price": 19.99, "quantity": 3}])

    # Use different payment processor
    wallet_processor = PaymentProcessorFactory.create_processor("digital_wallet")
    order2.set_payment_processor(wallet_processor)

    wallet_details = {
        "wallet_address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
        "wallet_type": "Bitcoin",
        "pin": "1234",
    }

    print("Processing digital wallet payment...")
    if order2.process_payment(wallet_details):
        print("✅ Payment successful!")
        print(f"Order status: {order2.status.value}")

    print("\n" + "-" * 50 + "\n")

    # 4. High-level operations - complexity abstracted
    print("Shipping first order...")
    if order.ship_order():
        print("✅ Order shipped!")
        print(f"Order status: {order.status.value}")

    print("\nCancelling second order...")
    if order2.cancel_order():
        print("✅ Order cancelled and refund processed!")
        print(f"Order status: {order2.status.value}")

    print("\n" + "-" * 50 + "\n")

    # 5. Demonstrate abstraction levels
    print("Final order summaries:")
    print("Order 1:", order.get_order_summary())
    print("Order 2:", order2.get_order_summary())

    print("\n=== Abstraction Benefits Demonstrated ===")
    print("✅ Client code doesn't need to know payment processor internals")
    print("✅ Order management complexity is hidden behind simple methods")
    print("✅ Different payment processors can be used interchangeably")
    print("✅ Implementation details can change without affecting client code")
    print("✅ Factory pattern abstracts object creation complexity")