Extract price logic to PriceService

- Create PriceService with get_recent_prices() and fetch_and_store_price()
- Update routes/audit.py to use PriceService instead of direct queries
- Use PriceHistoryMapper consistently
- Update test to patch services.price.fetch_btc_eur_price
This commit is contained in:
counterweight 2025-12-25 18:30:26 +01:00
parent 168b67acee
commit badb45da59
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
4 changed files with 324 additions and 50 deletions

70
backend/services/price.py Normal file
View file

@ -0,0 +1,70 @@
"""Price service for fetching and managing price history."""
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from exceptions import ConflictError
from models import PriceHistory
from price_fetcher import PAIR_BTC_EUR, SOURCE_BITFINEX, fetch_btc_eur_price
from repositories.price import PriceRepository
PRICE_HISTORY_LIMIT = 20
class PriceService:
"""Service for price-related business logic."""
def __init__(self, db: AsyncSession):
self.db = db
self.price_repo = PriceRepository(db)
async def get_recent_prices(
self, limit: int = PRICE_HISTORY_LIMIT
) -> list[PriceHistory]:
"""
Get recent price history records.
Args:
limit: Maximum number of records to return (default: 20)
Returns:
List of PriceHistory records, most recent first
"""
return await self.price_repo.get_recent(limit)
async def fetch_and_store_price(self) -> PriceHistory:
"""
Fetch price from Bitfinex and store it in the database.
Handles duplicate timestamp conflicts by returning the existing record.
Returns:
PriceHistory record (newly created or existing if duplicate)
Raises:
ConflictError: If unable to fetch or store price after retries
"""
price_value, timestamp = await fetch_btc_eur_price()
record = PriceHistory(
source=SOURCE_BITFINEX,
pair=PAIR_BTC_EUR,
price=price_value,
timestamp=timestamp,
)
self.db.add(record)
try:
await self.db.commit()
await self.db.refresh(record)
return record
except IntegrityError:
# Duplicate timestamp - return the existing record
await self.db.rollback()
existing_record = await self.price_repo.get_by_timestamp(
timestamp, SOURCE_BITFINEX, PAIR_BTC_EUR
)
if existing_record:
return existing_record
# This should not happen, but handle gracefully
raise ConflictError("Failed to fetch or store price") from None