first steps towards sql connections to supabase
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Failing after 2m18s
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been skipped
Run linting on the backend code / Build (pull_request) Failing after 44s
Run testing on the backend code / Build (pull_request) Failing after 2m24s

This commit is contained in:
2025-11-16 10:16:11 +01:00
parent 40e5ba084b
commit 4404eb6f77
4 changed files with 143 additions and 57 deletions

View File

@@ -4,9 +4,11 @@ from typing import Literal
from datetime import datetime, timedelta from datetime import datetime, timedelta
import requests 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 fastapi.responses import RedirectResponse
from ..supabase.supabase import SupabaseClient
from ..structs.shop import Item, BasketItem
from ..configuration.environment import Environment from ..configuration.environment import Environment
from ..cache import CreditCache, make_credit_cache_key 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' 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): class OrderRequest(BaseModel):
""" """
@@ -89,6 +43,8 @@ class OrderRequest(BaseModel):
items: list[Item] = Field(default_factory=list) items: list[Item] = Field(default_factory=list)
total_price: float = None total_price: float = None
total_credits: int = None total_credits: int = None
supabase_client: SupabaseClient
@field_validator('basket') @field_validator('basket')
def validate_basket(cls, v): def validate_basket(cls, v):
@@ -103,10 +59,16 @@ class OrderRequest(BaseModel):
Returns: Returns:
list: The validated basket. list: The validated basket.
""" """
if not v or not all(isinstance(i, BasketItem) for i in v): if not v:
raise ValueError('Basket must contain BasketItem objects') raise ValueError("Basket cannot be empty")
return
# 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): def load_items_and_price(self):
# This should be automatic upon initialization of the class # This should be automatic upon initialization of the class
""" """
@@ -116,12 +78,17 @@ class OrderRequest(BaseModel):
self.total_price = 0 self.total_price = 0
self.total_credits = 0 self.total_credits = 0
for basket_item in self.basket: 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.items.append(item)
self.total_price += item.unit_price * basket_item.quantity # increment price self.total_price += item.unit_price * basket_item.quantity # increment price
self.total_credits += item.unit_credits * basket_item.quantity # increment credit balance 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): def to_paypal_items(self):
""" """
Converts items to the PayPal API item format. Converts items to the PayPal API item format.
@@ -187,8 +154,6 @@ class PaypalClient:
self.key = Environment.paypal_key_prod self.key = Environment.paypal_key_prod
self.base_url = BASE_URL_PROD self.base_url = BASE_URL_PROD
def _get_access_token(self) -> str | None: def _get_access_token(self) -> str | None:
""" """
Gets (and caches) a PayPal access token. Gets (and caches) a PayPal access token.

View File

@@ -44,7 +44,8 @@ def create_order(
order = OrderRequest( order = OrderRequest(
user_id = user_id, user_id = user_id,
basket=basket, basket=basket,
currency=currency currency=currency,
supabase_client=supabase
) )
# Process the order and return the details # Process the order and return the details

View File

@@ -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

View File

@@ -4,6 +4,7 @@ import yaml
from fastapi import HTTPException, status from fastapi import HTTPException, status
from supabase import create_client, Client, ClientOptions from supabase import create_client, Client, ClientOptions
from ..structs.shop import Item, BasketItem
from ..constants import PARAMETERS_DIR from ..constants import PARAMETERS_DIR
from ..configuration.environment import Environment from ..configuration.environment import Environment
@@ -166,3 +167,90 @@ class SupabaseClient:
return True return True
else: else:
raise Exception("Error incrementing credit balance.") 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