import math from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.user import User from app.models.credit_record import CreditRecord from app.models.system_config import SystemConfig from app.models.credit_ratio import CreditRatio from app.utils.id_gen import generate_id from app.utils.exceptions import InsufficientCreditsError async def calc_text_credits(db: AsyncSession, input_tokens: int, output_tokens: int) -> float: """Calculate text credits based on actual token usage and configurable rate.""" result = await db.execute( select(SystemConfig).where(SystemConfig.key == "text_credits_per_1000_tokens") ) config = result.scalar_one_or_none() rate = float(config.value) if config else 1.0 total_tokens = input_tokens + output_tokens return round(total_tokens * rate / 1000, 2) async def calc_video_credits(db: AsyncSession, duration: int, resolution: str) -> float: """Calculate video credits using CreditRatio table, with fallback to hardcoded.""" result = await db.execute( select(CreditRatio).where(CreditRatio.resolution == resolution).limit(1) ) ratio = result.scalar_one_or_none() if ratio: return round((ratio.base_credits + ratio.per_second_credits * duration) * ratio.ratio, 2) # Fallback base = 60.0 duration_cost = duration * 2.0 multiplier = {"4K": 2.5, "1080p": 1.5, "720p": 1.0}.get(resolution, 1.0) return round((base + duration_cost) * multiplier, 2) def calc_credits(duration: int, resolution: str) -> float: """Legacy: hardcoded credit calculation. Prefer calc_video_credits for new code.""" base = 60.0 duration_cost = duration * 2.0 multiplier = {"4K": 2.5, "1080p": 1.5, "720p": 1.0}.get(resolution, 1.0) return round((base + duration_cost) * multiplier, 2) async def calc_image_credits(db: AsyncSession, image_size: str) -> float: """Calculate image credits using CreditRatio table, with fallback to hardcoded.""" result = await db.execute( select(CreditRatio).where(CreditRatio.gen_type == "image").where(CreditRatio.resolution == image_size).limit(1) ) ratio = result.scalar_one_or_none() if ratio: return round(ratio.base_credits * ratio.ratio, 2) # Fallback multiplier = {"4K": 2.0, "2K": 1.0}.get(image_size, 1.0) base_cost = 4.0 return round(base_cost * multiplier, 2) async def deduct_credits( db: AsyncSession, user_id: str, amount: float, description: str, related_id: str | None = None, ) -> User: """Atomically deduct credits from user. Raises InsufficientCreditsError.""" result = await db.execute( select(User).where(User.id == user_id) ) user = result.scalar_one_or_none() if not user or user.credits < amount: raise InsufficientCreditsError() user.credits = round(user.credits - amount, 2) record = CreditRecord( id=generate_id(), user_id=user_id, type="consume", amount=-round(amount, 2), balance_after=user.credits, description=description, related_id=related_id, ) db.add(record) await db.flush() return user async def add_credits( db: AsyncSession, user_id: str, amount: float, description: str, related_id: str | None = None, ) -> User: """Add credits to user.""" result = await db.execute( select(User).where(User.id == user_id) ) user = result.scalar_one_or_none() if not user: raise ValueError("User not found") user.credits = round(user.credits + amount, 2) record = CreditRecord( id=generate_id(), user_id=user_id, type="recharge", amount=round(amount, 2), balance_after=user.credits, description=description, related_id=related_id, ) db.add(record) await db.flush() return user async def get_records(db: AsyncSession, user_id: str) -> list[CreditRecord]: result = await db.execute( select(CreditRecord) .where(CreditRecord.user_id == user_id) .order_by(CreditRecord.created_at.desc()) ) return list(result.scalars().all())