Merge modifications for more separate backend functions #69
@@ -1,149 +1,336 @@
 | 
				
			|||||||
from typing import Literal
 | 
					from typing import Literal
 | 
				
			||||||
 | 
					from datetime import datetime, timedelta
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
import json
 | 
					import json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from pydantic import BaseModel
 | 
					from pydantic import BaseModel, Field, field_validator
 | 
				
			||||||
from fastapi import HTTPException
 | 
					 | 
				
			||||||
import requests
 | 
					import requests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..configuration.environment import Environment
 | 
					from ..configuration.environment import Environment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Model for payment request body
 | 
					# Define the base URL, might move that to toml file
 | 
				
			||||||
class OrderDetails():
 | 
					BASE_URL = 'https://api-m.sandbox.paypal.com'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Intialize the logger
 | 
				
			||||||
 | 
					logger = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BasketItem(BaseModel):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Represents a single item in the user's basket.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Attributes:
 | 
				
			||||||
 | 
					        id (str): The unique identifier for the item.
 | 
				
			||||||
 | 
					        quantity (int): The number of units of the item.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    id: str
 | 
				
			||||||
 | 
					    quantity: int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Item(BaseModel):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Represents an item available in the shop.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Attributes:
 | 
				
			||||||
 | 
					        id (str): The unique identifier for the item.
 | 
				
			||||||
 | 
					        name (str): The name of the item.
 | 
				
			||||||
 | 
					        description (str): The description of the item.
 | 
				
			||||||
 | 
					        unit_price (str): The unit price of the item.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    id: str
 | 
				
			||||||
 | 
					    name: str
 | 
				
			||||||
 | 
					    description: str
 | 
				
			||||||
 | 
					    unit_price: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def item_from_sql(item_id: str):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Fetches an item from the database by its ID.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        item_id (str): The unique identifier for the item.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        Item: The item object retrieved from the database.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    # TODO: Replace with actual SQL fetch logic
 | 
				
			||||||
 | 
					    return Item(
 | 
				
			||||||
 | 
					        id = '12345678',
 | 
				
			||||||
 | 
					        name = 'test_item',
 | 
				
			||||||
 | 
					        description = 'lorem ipsum',
 | 
				
			||||||
 | 
					        unit_price = 420
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OrderRequest(BaseModel):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Represents an order request from the frontend.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Attributes:
 | 
				
			||||||
 | 
					        user_id (str): The ID of the user placing the order.
 | 
				
			||||||
 | 
					        basket (list[BasketItem]): List of basket items.
 | 
				
			||||||
 | 
					        currency (str): The currency code for the order.
 | 
				
			||||||
 | 
					        created_at (datetime): Timestamp when the order was created.
 | 
				
			||||||
 | 
					        updated_at (datetime): Timestamp when the order was last updated.
 | 
				
			||||||
 | 
					        items (list[Item]): List of item details loaded from the database.
 | 
				
			||||||
 | 
					        total_price (float): Total price of the order.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
    user_id: str
 | 
					    user_id: str
 | 
				
			||||||
    number_of_credits: Literal[10, 50, 100]
 | 
					    basket: list[BasketItem]
 | 
				
			||||||
    unit_price: float
 | 
					    currency: Literal['CHF', 'EUR', 'USD']
 | 
				
			||||||
    amount: int
 | 
					    created_at: datetime = Field(default_factory=datetime.now)
 | 
				
			||||||
    currency: Literal['USD', 'EUR', 'CHF']
 | 
					    updated_at: datetime = Field(default_factory=datetime.now)
 | 
				
			||||||
 | 
					    items: list[Item] = Field(default_factory=list)
 | 
				
			||||||
 | 
					    total_price: float = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @field_validator('basket')
 | 
				
			||||||
 | 
					    def validate_basket(cls, v):
 | 
				
			||||||
 | 
					        """Validates the basket items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            v (list): List of basket items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Raises:
 | 
				
			||||||
 | 
					            ValueError: If basket does not contain valid BasketItem objects.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            list: The validated basket.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if not v or not all(isinstance(i, BasketItem) for i in v):
 | 
				
			||||||
 | 
					            raise ValueError('Basket must contain BasketItem objects')
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def load_items_and_price(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Loads item details from database and calculates the total price.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.items = []
 | 
				
			||||||
 | 
					        self.total_price = 0
 | 
				
			||||||
 | 
					        for basket_item in self.basket:
 | 
				
			||||||
 | 
					            item = item_from_sql(basket_item.id)
 | 
				
			||||||
 | 
					            self.items.append(item)
 | 
				
			||||||
 | 
					            self.total_price += item.unit_price * basket_item.quantity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_paypal_items(self): 
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Converts items to the PayPal API item format.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            list: List of items formatted for PayPal API.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        item_list = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for basket_item, item in zip(self.basket, self.items):
 | 
				
			||||||
 | 
					            item_list.append({
 | 
				
			||||||
 | 
					                'id': item.id,
 | 
				
			||||||
 | 
					                'name': item.name,
 | 
				
			||||||
 | 
					                'description': item.description,
 | 
				
			||||||
 | 
					                'quantity': str(basket_item.quantity),
 | 
				
			||||||
 | 
					                'unit_amount': {
 | 
				
			||||||
 | 
					                    'currency_code': self.currency,
 | 
				
			||||||
 | 
					                    'value': str(item.unit_price)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        return item_list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
					 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Payment handler class for managing PayPal payments
 | 
					# Payment handler class for managing PayPal payments
 | 
				
			||||||
class PaypalHandler:
 | 
					class PaypalHandler:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Handles PayPal payment operations.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # PayPal secrets
 | 
					    Attributes:
 | 
				
			||||||
    username = Environment.paypal_id_sandbox
 | 
					        sandbox (bool): Whether to use the sandbox environment.
 | 
				
			||||||
    password = Environment.paypal_key_sandbox
 | 
					        id (str): PayPal client ID.
 | 
				
			||||||
 | 
					        key (str): PayPal client secret.
 | 
				
			||||||
 | 
					        base_url (str): Base URL for PayPal API.
 | 
				
			||||||
 | 
					        _token_cache (dict): Cache for the PayPal OAuth access token.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _token_cache = {
 | 
				
			||||||
 | 
					        "access_token": None,
 | 
				
			||||||
 | 
					        "expires_at": 0
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    order_request = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(
 | 
					    def __init__(
 | 
				
			||||||
            self,
 | 
					            self,
 | 
				
			||||||
            order_details: OrderDetails,
 | 
					 | 
				
			||||||
            sandbox_mode: bool = False
 | 
					            sandbox_mode: bool = False
 | 
				
			||||||
            ):
 | 
					            ):
 | 
				
			||||||
        # Initialize the logger
 | 
					        """
 | 
				
			||||||
        self.logger = logging.getLogger(__name__)
 | 
					        Initializes the handler.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Payment request parameters
 | 
					        Args:
 | 
				
			||||||
        self.order_details = order_details
 | 
					            sandbox_mode (bool): Whether to use sandbox credentials.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.logger = logging.getLogger(__name__)
 | 
				
			||||||
        self.sandbox = sandbox_mode
 | 
					        self.sandbox = sandbox_mode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Only support purchase of credit 'bundles': 10, 50 or 100 credits worth of trip generation
 | 
					        # PayPal keys
 | 
				
			||||||
    # def fetch_price(self) -> float:
 | 
					        if sandbox_mode :
 | 
				
			||||||
    #     '''
 | 
					            self.id = Environment.paypal_id_sandbox
 | 
				
			||||||
    #     Fetches the price of credits in the specified currency.
 | 
					            self.key = Environment.paypal_key_sandbox
 | 
				
			||||||
    #     '''
 | 
					            self.base_url = BASE_URL
 | 
				
			||||||
    #     result = self.supabase.table('prices').select('credit_amount').eq('currency', self.details.currency).single().execute()
 | 
					 | 
				
			||||||
    #     if result.data:
 | 
					 | 
				
			||||||
    #         return result.data.get('price')
 | 
					 | 
				
			||||||
    #     else:
 | 
					 | 
				
			||||||
    #         self.logger.error(f'Unsupported currency: {self.details.currency}')
 | 
					 | 
				
			||||||
    #         return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def validate(self):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if self.sandbox :
 | 
					 | 
				
			||||||
            validation_url = 'https://api-m.sandbox.paypal.com/v1/oauth2/token'
 | 
					 | 
				
			||||||
        else :
 | 
					        else :
 | 
				
			||||||
            validation_url = 'https://api-m.paypal.com/v1/oauth2/token'
 | 
					            self.id = Environment.paypal_id_prod
 | 
				
			||||||
 | 
					            self.key = Environment.paypal_key_prod
 | 
				
			||||||
 | 
					            self.base_url = 'https://api-m.paypal.com'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # payload for the validation request
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _get_access_token(self) -> str | None:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Gets (and caches) a PayPal access token.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            str | None: The access token if successful, None otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        now = datetime.now()
 | 
				
			||||||
 | 
					        # Check if token is still valid
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            self._token_cache["access_token"] is not None
 | 
				
			||||||
 | 
					            and self._token_cache["expires_at"] > now
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            self.logger.info('Returning (cached) access token.')
 | 
				
			||||||
 | 
					            return self._token_cache["access_token"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Request new token
 | 
				
			||||||
        validation_data = {'grant_type': 'client_credentials'}
 | 
					        validation_data = {'grant_type': 'client_credentials'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try: 
 | 
					        try: 
 | 
				
			||||||
            # pass the request
 | 
					            # pass the request
 | 
				
			||||||
            validation_response = requests.post(
 | 
					            validation_response = requests.post(
 | 
				
			||||||
                url=validation_url,
 | 
					                url  = f'{self.base_url}/v1/oauth2/token',
 | 
				
			||||||
                data=validation_data,
 | 
					                data = validation_data,
 | 
				
			||||||
                auth=(self.username, self.password)
 | 
					                auth =(self.id, self.key)
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        except Exception as exc:
 | 
					        except Exception as exc:
 | 
				
			||||||
            self.logger.error(f'Error while requesting access token: {exc}')
 | 
					            self.logger.error(f'Error while requesting access token: {exc}')
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
        if validation_response.status_code == 201 :
 | 
					        data = validation_response.json()
 | 
				
			||||||
            access_token = json.loads(validation_response.text)['access_token']
 | 
					        access_token = data.get("access_token")
 | 
				
			||||||
            self.logger.info('Validation step successful. Returning access token.')
 | 
					        expires_in = int(data.get("expires_in", 3600)) # seconds, default 1 hour
 | 
				
			||||||
            return access_token
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.logger.error(f'Error {validation_response.status_code} while requesting access token: {validation_response.text}')
 | 
					        # Cache the token and its expiry
 | 
				
			||||||
        return None        
 | 
					        self._token_cache["access_token"] = access_token
 | 
				
			||||||
 | 
					        self._token_cache["expires_at"] = now + timedelta(seconds=expires_in - 60)  # buffer 1 min
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.logger.info('Returning (new) access token.')
 | 
				
			||||||
 | 
					        return access_token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def order(
 | 
					    def order(self, order_request: OrderRequest):
 | 
				
			||||||
        self,
 | 
					        """
 | 
				
			||||||
        access_token: int
 | 
					        Creates a new PayPal order.
 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.sandbox :
 | 
					        Args:
 | 
				
			||||||
            order_url = 'https://api-m.sandbox.paypal.com/v2/checkout/orders'
 | 
					            order_request (OrderRequest): The order request.
 | 
				
			||||||
        else :
 | 
					 | 
				
			||||||
            order_url = 'https://api-m.paypal.com/v2/checkout/orders'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # payload for the order equest
 | 
					        Returns:
 | 
				
			||||||
 | 
					            dict | None: PayPal order response JSON, or None if failed.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
        order_data = {
 | 
					        order_data = {
 | 
				
			||||||
            'intent': 'CAPTURE',
 | 
					            'intent': 'CAPTURE',
 | 
				
			||||||
            'purchase_units': [
 | 
					            'purchase_units': [
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    'items': [
 | 
					                    'items': order_request.to_paypal_items(),
 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            'name': f'{self.order_details.number_of_credits} Credits Pack',
 | 
					 | 
				
			||||||
                            'description': f'Credits for {self.order_details.number_of_credits} trip generations on AnyWay.',
 | 
					 | 
				
			||||||
                            'quantity': self.order_details.amount,
 | 
					 | 
				
			||||||
                            'unit_amount': {
 | 
					 | 
				
			||||||
                                'currency_code': self.order_details.currency,
 | 
					 | 
				
			||||||
                                'value': self.order_details.unit_price
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    ],
 | 
					 | 
				
			||||||
                    'amount': {
 | 
					                    'amount': {
 | 
				
			||||||
                        'currency_code': self.order_details.currency,
 | 
					                        'currency_code': order_request.currency,
 | 
				
			||||||
                        'value': self.order_details.amount*self.order_details.unit_price,
 | 
					                        'value': str(order_request.total_price),
 | 
				
			||||||
                        # 'breakdown': {
 | 
					                        'breakdown': {
 | 
				
			||||||
                        #     'item_total': {
 | 
					                            'item_total': {
 | 
				
			||||||
                        #         'currency_code': 'CHF',
 | 
					                                'currency_code': order_request.currency,
 | 
				
			||||||
                        #         'value': '5.00'
 | 
					                                'value': str(order_request.total_price)
 | 
				
			||||||
                        #     }
 | 
					                            }
 | 
				
			||||||
                        # }
 | 
					                        }
 | 
				
			||||||
                        ## what is that for ?
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            # TODO: add these to anydev website somehow
 | 
					            # TODO: add these to anydev website
 | 
				
			||||||
            'application_context': {
 | 
					            'application_context': {
 | 
				
			||||||
                'return_url': 'https://anydev.info',
 | 
					                'return_url': 'https://anydev.info',
 | 
				
			||||||
                'cancel_url': 'https://anydev.info'
 | 
					                'cancel_url': 'https://anydev.info'
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # TODO continue here
 | 
					        # Get the access_token:
 | 
				
			||||||
 | 
					        access_token = self._get_access_token()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try: 
 | 
				
			||||||
 | 
					            order_response = requests.post(
 | 
				
			||||||
 | 
					                url     = f'{self.base_url}/v2/checkout/orders',
 | 
				
			||||||
 | 
					                headers = {'Authorization': f'Bearer {access_token}'},
 | 
				
			||||||
 | 
					                json    = order_data,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Raise HTTP Exception if request was unsuccessful.
 | 
				
			||||||
 | 
					        except Exception as exc:
 | 
				
			||||||
 | 
					            self.logger.error(f'Error creating PayPal order: {exc}')
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        order_response.raise_for_status()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # TODO Now that we have the order ID, we can inscribe the details in sql database using the order id given by paypal
 | 
				
			||||||
 | 
					        # DB for storing the transactions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # order_id (key): json.loads(order_response.text)["id"]
 | 
				
			||||||
 | 
					        # user_id       : order_request.user_id
 | 
				
			||||||
 | 
					        # created_at    : order_request.created_at
 | 
				
			||||||
 | 
					        # status        : order_request.status
 | 
				
			||||||
 | 
					        # basket (json) : OrderDetails.jsonify()
 | 
				
			||||||
 | 
					        # total_price   : order_request.total_price
 | 
				
			||||||
 | 
					        # currency      : order_request.currency
 | 
				
			||||||
 | 
					        # updated_at    : order_request.created_at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return order_response.json()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        pass
 | 
					    # Standalone function to capture a payment
 | 
				
			||||||
 | 
					    def capture(self, order_id: str):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Captures payment for a PayPal order.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def capture(self):
 | 
					        Args:
 | 
				
			||||||
 | 
					            order_id (str): The PayPal order ID.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            dict | None: PayPal capture response JSON, or None if failed.
 | 
				
			||||||
        pass
 | 
					        """
 | 
				
			||||||
 | 
					        # Get the access_token:
 | 
				
			||||||
 | 
					        access_token = self._get_access_token()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try: 
 | 
				
			||||||
 | 
					            capture_response = requests.post(
 | 
				
			||||||
 | 
					                url  = f'{BASE_URL}/v2/checkout/orders/{order_id}/capture',
 | 
				
			||||||
 | 
					                headers = {'Authorization': f'Bearer {access_token}'},
 | 
				
			||||||
 | 
					                json = {},
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        except Exception as exc:
 | 
				
			||||||
 | 
					            logger.error(f'Error while requesting access token: {exc}')
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        capture_response.raise_for_status()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # todo check status code + try except
 | 
				
			||||||
 | 
					        print(capture_response.text)
 | 
				
			||||||
 | 
					        # order_id = json.loads(response.text)["id"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # TODO: update status to PAID in sql database
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # where order_id (key) = order_id
 | 
				
			||||||
 | 
					        # status        : 'PAID'
 | 
				
			||||||
 | 
					        # updated_at    : datetime.now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Not sure yet if/how to implement that
 | 
				
			||||||
    def cancel(self):
 | 
					    def cancel(self):
 | 
				
			||||||
               
 | 
					               
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,79 +1,136 @@
 | 
				
			|||||||
import logging
 | 
					from typing import Literal
 | 
				
			||||||
import paypalrestsdk
 | 
					 | 
				
			||||||
from fastapi import HTTPException, APIRouter
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..supabase.supabase import SupabaseClient
 | 
					from fastapi import FastAPI, HTTPException
 | 
				
			||||||
from .payment_handler import PaymentRequest, PaymentHandler
 | 
					from ..payments import PaypalHandler, OrderRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Set up logging and supabase
 | 
					app = FastAPI()
 | 
				
			||||||
logger = logging.getLogger(__name__)
 | 
					 | 
				
			||||||
supabase = SupabaseClient()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Configure PayPal SDK
 | 
					# Initialize PayPal handler
 | 
				
			||||||
paypalrestsdk.configure({
 | 
					paypal_handler = PaypalHandler(sandbox_mode=True)
 | 
				
			||||||
    "mode": "sandbox",  # Use 'live' for production
 | 
					 | 
				
			||||||
    "client_id": "YOUR_PAYPAL_CLIENT_ID",
 | 
					 | 
				
			||||||
    "client_secret": "YOUR_PAYPAL_SECRET"
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.post("/orders/new")
 | 
				
			||||||
# Define the API router
 | 
					def create_order(
 | 
				
			||||||
router = APIRouter()
 | 
					        user_id: str,
 | 
				
			||||||
 | 
					        basket: list,
 | 
				
			||||||
@router.post("/purchase/credits")
 | 
					        currency: Literal['CHF', 'EUR', 'USD']
 | 
				
			||||||
def purchase_credits(payment_request: PaymentRequest):
 | 
					    ):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Handles token purchases. Calculates the number of tokens based on the amount paid,
 | 
					    Creates a new PayPal order.
 | 
				
			||||||
    updates the user's balance, and processes PayPal payment.
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        user_id (str): The ID of the user placing the order.
 | 
				
			||||||
 | 
					        basket (list): The basket items.
 | 
				
			||||||
 | 
					        currency (str): The currency code.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        dict: The PayPal order details.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    payment_handler = PaymentHandler(payment_request)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Create PayPal payment and get the approval URL
 | 
					    # Create order :
 | 
				
			||||||
    approval_url = payment_handler.create_paypal_payment()
 | 
					    order = OrderRequest(
 | 
				
			||||||
 | 
					        user_id = user_id,
 | 
				
			||||||
 | 
					        basket=basket,
 | 
				
			||||||
 | 
					        currency=currency
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    # Process the order and return the details
 | 
				
			||||||
        "message": "Purchase initiated successfully",
 | 
					    return paypal_handler.order(order_request = order)
 | 
				
			||||||
        "payment_id": payment_handler.payment_id,
 | 
					 | 
				
			||||||
        "credits": payment_request.credit_amount,
 | 
					 | 
				
			||||||
        "approval_url": approval_url,
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@router.get("/payment/success")
 | 
					
 | 
				
			||||||
def payment_success(paymentId: str, PayerID: str):
 | 
					@app.post("/orders/{order_id}/capture")
 | 
				
			||||||
 | 
					def capture_order(order_id: str):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Handles successful PayPal payment.
 | 
					    Captures payment for an existing PayPal order.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        order_id (str): The PayPal order ID.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        dict: The PayPal capture response.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    payment = paypalrestsdk.Payment.find(paymentId)
 | 
					    result = paypal_handler.capture(order_id)
 | 
				
			||||||
 | 
					    return result
 | 
				
			||||||
    if payment.execute({"payer_id": PayerID}):
 | 
					 | 
				
			||||||
        logger.info("Payment executed successfully")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Retrieve transaction details from the database
 | 
					 | 
				
			||||||
        result = supabase.table("pending_payments").select("*").eq("payment_id", paymentId).single().execute()
 | 
					 | 
				
			||||||
        if not result.data:
 | 
					 | 
				
			||||||
            raise HTTPException(status_code=404, detail="Transaction not found")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Extract the necessary information
 | 
					 | 
				
			||||||
        user_id = result.data["user_id"]
 | 
					 | 
				
			||||||
        credit_amount = result.data["credit_amount"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Update the user's balance
 | 
					 | 
				
			||||||
        supabase.increment_credit_balance(user_id, amount=credit_amount)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Optionally, delete the pending payment entry since the transaction is completed
 | 
					 | 
				
			||||||
        supabase.table("pending_payments").delete().eq("payment_id", paymentId).execute()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return {"message": "Payment completed successfully"}
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        logger.error(f"Payment execution failed: {payment.error}")
 | 
					 | 
				
			||||||
        raise HTTPException(status_code=500, detail="Payment execution failed")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@router.get("/payment/cancel")
 | 
					
 | 
				
			||||||
def payment_cancel():
 | 
					
 | 
				
			||||||
    """
 | 
					# import logging
 | 
				
			||||||
    Handles PayPal payment cancellation.
 | 
					# import paypalrestsdk
 | 
				
			||||||
    """
 | 
					# from fastapi import HTTPException, APIRouter
 | 
				
			||||||
    return {"message": "Payment was cancelled"}
 | 
					
 | 
				
			||||||
 | 
					# from ..supabase.supabase import SupabaseClient
 | 
				
			||||||
 | 
					# from .payment_handler import PaymentRequest, PaymentHandler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# # Set up logging and supabase
 | 
				
			||||||
 | 
					# logger = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					# supabase = SupabaseClient()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# # Configure PayPal SDK
 | 
				
			||||||
 | 
					# paypalrestsdk.configure({
 | 
				
			||||||
 | 
					#     "mode": "sandbox",  # Use 'live' for production
 | 
				
			||||||
 | 
					#     "client_id": "YOUR_PAYPAL_CLIENT_ID",
 | 
				
			||||||
 | 
					#     "client_secret": "YOUR_PAYPAL_SECRET"
 | 
				
			||||||
 | 
					# })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# # Define the API router
 | 
				
			||||||
 | 
					# router = APIRouter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# @router.post("/purchase/credits")
 | 
				
			||||||
 | 
					# def purchase_credits(payment_request: PaymentRequest):
 | 
				
			||||||
 | 
					#     """
 | 
				
			||||||
 | 
					#     Handles token purchases. Calculates the number of tokens based on the amount paid,
 | 
				
			||||||
 | 
					#     updates the user's balance, and processes PayPal payment.
 | 
				
			||||||
 | 
					#     """
 | 
				
			||||||
 | 
					#     payment_handler = PaymentHandler(payment_request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#     # Create PayPal payment and get the approval URL
 | 
				
			||||||
 | 
					#     approval_url = payment_handler.create_paypal_payment()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#     return {
 | 
				
			||||||
 | 
					#         "message": "Purchase initiated successfully",
 | 
				
			||||||
 | 
					#         "payment_id": payment_handler.payment_id,
 | 
				
			||||||
 | 
					#         "credits": payment_request.credit_amount,
 | 
				
			||||||
 | 
					#         "approval_url": approval_url,
 | 
				
			||||||
 | 
					#     }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# @router.get("/payment/success")
 | 
				
			||||||
 | 
					# def payment_success(paymentId: str, PayerID: str):
 | 
				
			||||||
 | 
					#     """
 | 
				
			||||||
 | 
					#     Handles successful PayPal payment.
 | 
				
			||||||
 | 
					#     """
 | 
				
			||||||
 | 
					#     payment = paypalrestsdk.Payment.find(paymentId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#     if payment.execute({"payer_id": PayerID}):
 | 
				
			||||||
 | 
					#         logger.info("Payment executed successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#         # Retrieve transaction details from the database
 | 
				
			||||||
 | 
					#         result = supabase.table("pending_payments").select("*").eq("payment_id", paymentId).single().execute()
 | 
				
			||||||
 | 
					#         if not result.data:
 | 
				
			||||||
 | 
					#             raise HTTPException(status_code=404, detail="Transaction not found")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#         # Extract the necessary information
 | 
				
			||||||
 | 
					#         user_id = result.data["user_id"]
 | 
				
			||||||
 | 
					#         credit_amount = result.data["credit_amount"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#         # Update the user's balance
 | 
				
			||||||
 | 
					#         supabase.increment_credit_balance(user_id, amount=credit_amount)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#         # Optionally, delete the pending payment entry since the transaction is completed
 | 
				
			||||||
 | 
					#         supabase.table("pending_payments").delete().eq("payment_id", paymentId).execute()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#         return {"message": "Payment completed successfully"}
 | 
				
			||||||
 | 
					#     else:
 | 
				
			||||||
 | 
					#         logger.error(f"Payment execution failed: {payment.error}")
 | 
				
			||||||
 | 
					#         raise HTTPException(status_code=500, detail="Payment execution failed")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# @router.get("/payment/cancel")
 | 
				
			||||||
 | 
					# def payment_cancel():
 | 
				
			||||||
 | 
					#     """
 | 
				
			||||||
 | 
					#     Handles PayPal payment cancellation.
 | 
				
			||||||
 | 
					#     """
 | 
				
			||||||
 | 
					#     return {"message": "Payment was cancelled"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -74,6 +74,7 @@ order_data = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
order_response = requests.post(
 | 
					order_response = requests.post(
 | 
				
			||||||
    url=order_url,
 | 
					    url=order_url,
 | 
				
			||||||
 | 
					    headers={"Authorization": f"Bearer {access_token}"},        ## need access token here? 
 | 
				
			||||||
    json=order_data,
 | 
					    json=order_data,
 | 
				
			||||||
    auth=(username, password)
 | 
					    auth=(username, password)
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user