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
|
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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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 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
|
||||||
Reference in New Issue
Block a user