Step 1: Add PricingConfig model and PricingRepository
- Create PricingConfig model with all required fields (premium settings, trade limits) - Implement PricingRepository with singleton pattern (get_current, create_or_update) - Add comprehensive tests for repository functionality - Export model and repository in __init__.py files
This commit is contained in:
parent
82c4d0168e
commit
32ce27180d
5 changed files with 291 additions and 0 deletions
|
|
@ -14,6 +14,7 @@ from .enums import (
|
||||||
from .exchange import Exchange
|
from .exchange import Exchange
|
||||||
from .invite import Invite
|
from .invite import Invite
|
||||||
from .price_history import PriceHistory
|
from .price_history import PriceHistory
|
||||||
|
from .pricing_config import PricingConfig
|
||||||
|
|
||||||
# Export role configuration
|
# Export role configuration
|
||||||
from .role_config import ROLE_ADMIN, ROLE_DEFINITIONS, ROLE_REGULAR
|
from .role_config import ROLE_ADMIN, ROLE_DEFINITIONS, ROLE_REGULAR
|
||||||
|
|
@ -34,6 +35,7 @@ __all__ = [
|
||||||
"InviteStatus",
|
"InviteStatus",
|
||||||
"Permission",
|
"Permission",
|
||||||
"PriceHistory",
|
"PriceHistory",
|
||||||
|
"PricingConfig",
|
||||||
"Role",
|
"Role",
|
||||||
"RoleConfig",
|
"RoleConfig",
|
||||||
"TradeDirection",
|
"TradeDirection",
|
||||||
|
|
|
||||||
44
backend/models/pricing_config.py
Normal file
44
backend/models/pricing_config.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, Integer
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class PricingConfig(Base):
|
||||||
|
"""Pricing configuration for exchange trades.
|
||||||
|
|
||||||
|
Singleton pattern: Only one record should exist in the database.
|
||||||
|
Updates modify the existing record rather than creating new ones.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "pricing_config"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
|
||||||
|
# Premium settings
|
||||||
|
premium_buy: Mapped[int] = mapped_column(Integer, nullable=False) # -100 to 100
|
||||||
|
premium_sell: Mapped[int] = mapped_column(Integer, nullable=False) # -100 to 100
|
||||||
|
small_trade_threshold_eur: Mapped[int] = mapped_column(
|
||||||
|
Integer, nullable=False
|
||||||
|
) # EUR cents, e.g., 20000 = €200
|
||||||
|
small_trade_extra_premium: Mapped[int] = mapped_column(
|
||||||
|
Integer, nullable=False
|
||||||
|
) # -100 to 100
|
||||||
|
|
||||||
|
# Trade amount limits (EUR cents)
|
||||||
|
eur_min_buy: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
eur_max_buy: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
eur_min_sell: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
eur_max_sell: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
|
||||||
|
# Timestamps
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), default=lambda: datetime.now(UTC)
|
||||||
|
)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
default=lambda: datetime.now(UTC),
|
||||||
|
onupdate=lambda: datetime.now(UTC),
|
||||||
|
)
|
||||||
|
|
@ -4,6 +4,7 @@ from repositories.availability import AvailabilityRepository
|
||||||
from repositories.exchange import ExchangeRepository
|
from repositories.exchange import ExchangeRepository
|
||||||
from repositories.invite import InviteRepository
|
from repositories.invite import InviteRepository
|
||||||
from repositories.price import PriceRepository
|
from repositories.price import PriceRepository
|
||||||
|
from repositories.pricing import PricingRepository
|
||||||
from repositories.role import RoleRepository
|
from repositories.role import RoleRepository
|
||||||
from repositories.user import UserRepository
|
from repositories.user import UserRepository
|
||||||
|
|
||||||
|
|
@ -12,6 +13,7 @@ __all__ = [
|
||||||
"ExchangeRepository",
|
"ExchangeRepository",
|
||||||
"InviteRepository",
|
"InviteRepository",
|
||||||
"PriceRepository",
|
"PriceRepository",
|
||||||
|
"PricingRepository",
|
||||||
"RoleRepository",
|
"RoleRepository",
|
||||||
"UserRepository",
|
"UserRepository",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
83
backend/repositories/pricing.py
Normal file
83
backend/repositories/pricing.py
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
"""Pricing repository for database queries."""
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from models import PricingConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PricingRepository:
|
||||||
|
"""Repository for pricing configuration database queries."""
|
||||||
|
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get_current(self) -> PricingConfig | None:
|
||||||
|
"""Get the current pricing configuration (singleton).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PricingConfig if exists, None otherwise
|
||||||
|
"""
|
||||||
|
result = await self.db.execute(select(PricingConfig).limit(1))
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def create_or_update(
|
||||||
|
self,
|
||||||
|
premium_buy: int,
|
||||||
|
premium_sell: int,
|
||||||
|
small_trade_threshold_eur: int,
|
||||||
|
small_trade_extra_premium: int,
|
||||||
|
eur_min_buy: int,
|
||||||
|
eur_max_buy: int,
|
||||||
|
eur_min_sell: int,
|
||||||
|
eur_max_sell: int,
|
||||||
|
) -> PricingConfig:
|
||||||
|
"""Create or update pricing configuration (singleton pattern).
|
||||||
|
|
||||||
|
If no config exists, creates a new one.
|
||||||
|
If config exists, updates the existing record.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
premium_buy: Premium percentage for BUY direction (-100 to 100)
|
||||||
|
premium_sell: Premium percentage for SELL direction (-100 to 100)
|
||||||
|
small_trade_threshold_eur: Threshold in EUR cents for small trade
|
||||||
|
extra premium
|
||||||
|
small_trade_extra_premium: Extra premium percentage for small trades
|
||||||
|
(-100 to 100)
|
||||||
|
eur_min_buy: Minimum trade amount in EUR cents for BUY
|
||||||
|
eur_max_buy: Maximum trade amount in EUR cents for BUY
|
||||||
|
eur_min_sell: Minimum trade amount in EUR cents for SELL
|
||||||
|
eur_max_sell: Maximum trade amount in EUR cents for SELL
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Created or updated PricingConfig record
|
||||||
|
"""
|
||||||
|
config = await self.get_current()
|
||||||
|
|
||||||
|
if config is None:
|
||||||
|
# Create new config
|
||||||
|
config = PricingConfig(
|
||||||
|
premium_buy=premium_buy,
|
||||||
|
premium_sell=premium_sell,
|
||||||
|
small_trade_threshold_eur=small_trade_threshold_eur,
|
||||||
|
small_trade_extra_premium=small_trade_extra_premium,
|
||||||
|
eur_min_buy=eur_min_buy,
|
||||||
|
eur_max_buy=eur_max_buy,
|
||||||
|
eur_min_sell=eur_min_sell,
|
||||||
|
eur_max_sell=eur_max_sell,
|
||||||
|
)
|
||||||
|
self.db.add(config)
|
||||||
|
else:
|
||||||
|
# Update existing config
|
||||||
|
config.premium_buy = premium_buy
|
||||||
|
config.premium_sell = premium_sell
|
||||||
|
config.small_trade_threshold_eur = small_trade_threshold_eur
|
||||||
|
config.small_trade_extra_premium = small_trade_extra_premium
|
||||||
|
config.eur_min_buy = eur_min_buy
|
||||||
|
config.eur_max_buy = eur_max_buy
|
||||||
|
config.eur_min_sell = eur_min_sell
|
||||||
|
config.eur_max_sell = eur_max_sell
|
||||||
|
|
||||||
|
await self.db.commit()
|
||||||
|
await self.db.refresh(config)
|
||||||
|
return config
|
||||||
160
backend/tests/test_pricing_repository.py
Normal file
160
backend/tests/test_pricing_repository.py
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
"""
|
||||||
|
Pricing Repository Tests
|
||||||
|
|
||||||
|
Tests for the pricing configuration repository.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from models import PricingConfig
|
||||||
|
from repositories.pricing import PricingRepository
|
||||||
|
|
||||||
|
|
||||||
|
class TestPricingRepository:
|
||||||
|
"""Test the PricingRepository class."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_current_returns_none_when_no_config(self, client_factory):
|
||||||
|
"""get_current() returns None when no config exists."""
|
||||||
|
async with client_factory.get_db_session() as db:
|
||||||
|
repo = PricingRepository(db)
|
||||||
|
config = await repo.get_current()
|
||||||
|
assert config is None
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_or_update_creates_first_config(self, client_factory):
|
||||||
|
"""create_or_update() creates a new config when none exists."""
|
||||||
|
async with client_factory.get_db_session() as db:
|
||||||
|
repo = PricingRepository(db)
|
||||||
|
config = await repo.create_or_update(
|
||||||
|
premium_buy=5,
|
||||||
|
premium_sell=5,
|
||||||
|
small_trade_threshold_eur=20000, # €200
|
||||||
|
small_trade_extra_premium=2,
|
||||||
|
eur_min_buy=10000, # €100
|
||||||
|
eur_max_buy=300000, # €3000
|
||||||
|
eur_min_sell=10000,
|
||||||
|
eur_max_sell=300000,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert config is not None
|
||||||
|
assert config.id is not None
|
||||||
|
assert config.premium_buy == 5
|
||||||
|
assert config.premium_sell == 5
|
||||||
|
assert config.small_trade_threshold_eur == 20000
|
||||||
|
assert config.small_trade_extra_premium == 2
|
||||||
|
assert config.eur_min_buy == 10000
|
||||||
|
assert config.eur_max_buy == 300000
|
||||||
|
assert config.eur_min_sell == 10000
|
||||||
|
assert config.eur_max_sell == 300000
|
||||||
|
assert config.created_at is not None
|
||||||
|
assert config.updated_at is not None
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_or_update_updates_existing_config(self, client_factory):
|
||||||
|
"""create_or_update() updates existing config (singleton behavior)."""
|
||||||
|
async with client_factory.get_db_session() as db:
|
||||||
|
repo = PricingRepository(db)
|
||||||
|
|
||||||
|
# Create initial config
|
||||||
|
config1 = await repo.create_or_update(
|
||||||
|
premium_buy=5,
|
||||||
|
premium_sell=5,
|
||||||
|
small_trade_threshold_eur=20000,
|
||||||
|
small_trade_extra_premium=2,
|
||||||
|
eur_min_buy=10000,
|
||||||
|
eur_max_buy=300000,
|
||||||
|
eur_min_sell=10000,
|
||||||
|
eur_max_sell=300000,
|
||||||
|
)
|
||||||
|
original_id = config1.id
|
||||||
|
original_created_at = config1.created_at
|
||||||
|
|
||||||
|
# Update config
|
||||||
|
config2 = await repo.create_or_update(
|
||||||
|
premium_buy=7,
|
||||||
|
premium_sell=6,
|
||||||
|
small_trade_threshold_eur=25000,
|
||||||
|
small_trade_extra_premium=3,
|
||||||
|
eur_min_buy=15000,
|
||||||
|
eur_max_buy=350000,
|
||||||
|
eur_min_sell=12000,
|
||||||
|
eur_max_sell=320000,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should be the same record (singleton)
|
||||||
|
assert config2.id == original_id
|
||||||
|
assert config2.created_at == original_created_at
|
||||||
|
assert config2.premium_buy == 7
|
||||||
|
assert config2.premium_sell == 6
|
||||||
|
assert config2.small_trade_threshold_eur == 25000
|
||||||
|
assert config2.small_trade_extra_premium == 3
|
||||||
|
assert config2.eur_min_buy == 15000
|
||||||
|
assert config2.eur_max_buy == 350000
|
||||||
|
assert config2.eur_min_sell == 12000
|
||||||
|
assert config2.eur_max_sell == 320000
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_current_returns_existing_config(self, client_factory):
|
||||||
|
"""get_current() returns existing config."""
|
||||||
|
async with client_factory.get_db_session() as db:
|
||||||
|
repo = PricingRepository(db)
|
||||||
|
|
||||||
|
# Create config
|
||||||
|
created_config = await repo.create_or_update(
|
||||||
|
premium_buy=5,
|
||||||
|
premium_sell=5,
|
||||||
|
small_trade_threshold_eur=20000,
|
||||||
|
small_trade_extra_premium=2,
|
||||||
|
eur_min_buy=10000,
|
||||||
|
eur_max_buy=300000,
|
||||||
|
eur_min_sell=10000,
|
||||||
|
eur_max_sell=300000,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Retrieve config
|
||||||
|
retrieved_config = await repo.get_current()
|
||||||
|
|
||||||
|
assert retrieved_config is not None
|
||||||
|
assert retrieved_config.id == created_config.id
|
||||||
|
assert retrieved_config.premium_buy == 5
|
||||||
|
assert retrieved_config.premium_sell == 5
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_singleton_pattern_only_one_config_exists(self, client_factory):
|
||||||
|
"""Only one config record can exist (singleton pattern)."""
|
||||||
|
async with client_factory.get_db_session() as db:
|
||||||
|
repo = PricingRepository(db)
|
||||||
|
|
||||||
|
# Create config multiple times
|
||||||
|
config1 = await repo.create_or_update(
|
||||||
|
premium_buy=5,
|
||||||
|
premium_sell=5,
|
||||||
|
small_trade_threshold_eur=20000,
|
||||||
|
small_trade_extra_premium=2,
|
||||||
|
eur_min_buy=10000,
|
||||||
|
eur_max_buy=300000,
|
||||||
|
eur_min_sell=10000,
|
||||||
|
eur_max_sell=300000,
|
||||||
|
)
|
||||||
|
|
||||||
|
config2 = await repo.create_or_update(
|
||||||
|
premium_buy=10,
|
||||||
|
premium_sell=10,
|
||||||
|
small_trade_threshold_eur=30000,
|
||||||
|
small_trade_extra_premium=5,
|
||||||
|
eur_min_buy=20000,
|
||||||
|
eur_max_buy=400000,
|
||||||
|
eur_min_sell=20000,
|
||||||
|
eur_max_sell=400000,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should be same record
|
||||||
|
assert config1.id == config2.id
|
||||||
|
|
||||||
|
# Verify only one record exists
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
result = await db.execute(select(PricingConfig))
|
||||||
|
all_configs = list(result.scalars().all())
|
||||||
|
assert len(all_configs) == 1
|
||||||
Loading…
Add table
Add a link
Reference in a new issue