diff --git a/backend/src/payments/payment_handler.py b/backend/src/payments/payment_handler.py index 26e4057..b01a38f 100644 --- a/backend/src/payments/payment_handler.py +++ b/backend/src/payments/payment_handler.py @@ -4,9 +4,11 @@ from typing import Literal from datetime import datetime, timedelta import requests -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, Field, field_validator, model_post_init from fastapi.responses import RedirectResponse +from ..supabase.supabase import SupabaseClient +from ..structs.shop import Item, BasketItem from ..configuration.environment import Environment from ..cache import CreditCache, make_credit_cache_key @@ -19,54 +21,6 @@ BASE_URL_PROD = 'https://api-m.paypal.com' BASE_URL_SANDBOX = 'https://api-m.sandbox.paypal.com' -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 (float): The unit price of the item. - """ - id: str - name: str - description: str - unit_price: float - unit_credits: int - - -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 = 0.1, - unit_credits = 5 - ) - class OrderRequest(BaseModel): """ @@ -89,6 +43,8 @@ class OrderRequest(BaseModel): items: list[Item] = Field(default_factory=list) total_price: float = None total_credits: int = None + supabase_client: SupabaseClient + @field_validator('basket') def validate_basket(cls, v): @@ -103,10 +59,16 @@ class OrderRequest(BaseModel): 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 - + if not v: + raise ValueError("Basket cannot be empty") + + # Pydantic already converts dict -> BasketItem, so isinstance works + if not all(isinstance(i, BasketItem) for i in v): + raise ValueError("Basket must contain BasketItem objects") + + return v + + def load_items_and_price(self): # This should be automatic upon initialization of the class """ @@ -116,12 +78,17 @@ class OrderRequest(BaseModel): self.total_price = 0 self.total_credits = 0 for basket_item in self.basket: - item = item_from_sql(basket_item.id) + item = self.supabase_client.get_item(basket_item.id, self.currency) self.items.append(item) self.total_price += item.unit_price * basket_item.quantity # increment price self.total_credits += item.unit_credits * basket_item.quantity # increment credit balance + @model_post_init + def auto_load_items(self, _): + self.load_items_and_price() + + def to_paypal_items(self): """ Converts items to the PayPal API item format. @@ -187,8 +154,6 @@ class PaypalClient: self.key = Environment.paypal_key_prod self.base_url = BASE_URL_PROD - - def _get_access_token(self) -> str | None: """ Gets (and caches) a PayPal access token. diff --git a/backend/src/payments/payment_router.py b/backend/src/payments/payment_router.py index 5c9108c..8c2de92 100644 --- a/backend/src/payments/payment_router.py +++ b/backend/src/payments/payment_router.py @@ -44,7 +44,8 @@ def create_order( order = OrderRequest( user_id = user_id, basket=basket, - currency=currency + currency=currency, + supabase_client=supabase ) # Process the order and return the details diff --git a/backend/src/structs/shop.py b/backend/src/structs/shop.py new file mode 100644 index 0000000..bac75cb --- /dev/null +++ b/backend/src/structs/shop.py @@ -0,0 +1,32 @@ +"""Module to handle classes related to online shop""" +from pydantic import BaseModel + + +class BasketItem(BaseModel): + """ + Represents a single item in the user's basket. + + Attributes: + item_id (str): The unique identifier for the item. + quantity (int): The number of units of the item. + """ + item_id: str + quantity: int + + +class Item(BaseModel): + """ + Represents an item available in the shop. + + Attributes: + item_id (str): The unique identifier for the item. + name (str): The name of the item. + description (str): The description of the item. + unit_price (float): The unit price of the item. + """ + item_id: str + name: str + description: str + unit_price: float + unit_credits: int + currency: str diff --git a/backend/src/supabase/supabase.py b/backend/src/supabase/supabase.py index 8d29a8a..242d926 100644 --- a/backend/src/supabase/supabase.py +++ b/backend/src/supabase/supabase.py @@ -4,6 +4,7 @@ import yaml from fastapi import HTTPException, status from supabase import create_client, Client, ClientOptions +from ..structs.shop import Item, BasketItem from ..constants import PARAMETERS_DIR from ..configuration.environment import Environment @@ -166,3 +167,90 @@ class SupabaseClient: return True else: raise Exception("Error incrementing credit balance.") + + + def get_item(self, item_id: int, currency: str) -> Item | None: + """ + Fetch full item info (name, description) and price/credits for a given currency. + Returns an Item pydantic model. + """ + # First, validate the currency + try: + ok = self.validate_currency(currency=currency) + except Exception as e: + self.logger.error(e) + raise Exception from e + + # Fetch from items table + item_res = ( + self.supabase + .table("items") + .select("*") + .eq("id", item_id) + .single() + .execute() + ) + + if item_res.data is None: + raise ValueError(f"Item {item_id} does not exist.") + + base_item = item_res.data + + # Fetch price for this currency and item_id + price_res = ( + self.supabase + .table("item_prices") + .select("*") + .eq("item_id", item_id) + .eq("currency", currency.upper()) + .single() + .execute() + ) + + if price_res.data is None: + raise ValueError(f"Price for item {item_id} in {currency} does not exist.") + + price = price_res.data + + # Return Item model + return Item( + id=base_item["id"], + name=base_item["name"], + description=base_item["description"], + unit_price=price["unit_price"], + unit_credits=price["unit_credits"], + currency=price["currency"] + ) + + + def validate_currency(self, currency: str) -> dict: + """ + Validates that a currency exists in the available_currencies table + and is active. + + Args: + currency (str): Currency code (e.g. 'EUR', 'USD', 'CHF'). + + Returns: + dict: The currency row. + + Raises: + ValueError: If currency does not exist or is inactive. + """ + + result = ( + self.supabase + .table("available_currencies") + .select("*") + .eq("currency", currency.upper()) + .single() + .execute() + ) + + if result.data is None: + raise ValueError(f"Currency '{currency}' is not supported yet.") + + if result.data.get("active") is not True: + raise ValueError(f"Currency '{currency}' is currently not supported.") + + return True \ No newline at end of file