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
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,9 +59,15 @@ 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.

View File

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

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