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
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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
32
backend/src/structs/shop.py
Normal file
32
backend/src/structs/shop.py
Normal 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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user