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:
counterweight 2025-12-26 20:08:35 +01:00
parent 82c4d0168e
commit 32ce27180d
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
5 changed files with 291 additions and 0 deletions

View file

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

View 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),
)

View file

@ -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",
] ]

View 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

View 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