Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8cb80c5f3 | |||
|
|
6016afb999 | ||
| f95c857363 | |||
|
|
174ce4474d | ||
|
|
2091302b8a | ||
| 27a272b813 | |||
|
|
95010bfd5d | ||
| deb40476a4 | |||
|
|
65d3099a7c | ||
| 8e02889fd4 | |||
|
|
b872a05397 |
@ -26,6 +26,7 @@ from services.trade_builder import (
|
|||||||
clear_trade_builder,
|
clear_trade_builder,
|
||||||
clear_trade_builder_by_team,
|
clear_trade_builder_by_team,
|
||||||
)
|
)
|
||||||
|
from services.league_service import league_service
|
||||||
from services.player_service import player_service
|
from services.player_service import player_service
|
||||||
from services.team_service import team_service
|
from services.team_service import team_service
|
||||||
from models.team import RosterType
|
from models.team import RosterType
|
||||||
@ -130,6 +131,22 @@ class TradeCommands(commands.Cog):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check trade deadline
|
||||||
|
current = await league_service.get_current_state()
|
||||||
|
if not current:
|
||||||
|
await interaction.followup.send(
|
||||||
|
"❌ Could not retrieve league state. Please try again later.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if current.is_past_trade_deadline:
|
||||||
|
await interaction.followup.send(
|
||||||
|
f"❌ **The trade deadline has passed.** The deadline was Week {current.trade_deadline} "
|
||||||
|
f"and we are currently in Week {current.week}. No new trades can be initiated.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Clear any existing trade and create new one
|
# Clear any existing trade and create new one
|
||||||
clear_trade_builder(interaction.user.id)
|
clear_trade_builder(interaction.user.id)
|
||||||
trade_builder = get_trade_builder(interaction.user.id, user_team)
|
trade_builder = get_trade_builder(interaction.user.id, user_team)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ Current league state model
|
|||||||
|
|
||||||
Represents the current state of the league including week, season, and settings.
|
Represents the current state of the league including week, season, and settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import Field, field_validator
|
from pydantic import Field, field_validator
|
||||||
|
|
||||||
from models.base import SBABaseModel
|
from models.base import SBABaseModel
|
||||||
@ -10,38 +11,45 @@ from models.base import SBABaseModel
|
|||||||
|
|
||||||
class Current(SBABaseModel):
|
class Current(SBABaseModel):
|
||||||
"""Model representing current league state and settings."""
|
"""Model representing current league state and settings."""
|
||||||
|
|
||||||
week: int = Field(69, description="Current week number")
|
week: int = Field(69, description="Current week number")
|
||||||
season: int = Field(69, description="Current season number")
|
season: int = Field(69, description="Current season number")
|
||||||
freeze: bool = Field(True, description="Whether league is frozen")
|
freeze: bool = Field(True, description="Whether league is frozen")
|
||||||
bet_week: str = Field('sheets', description="Betting week identifier")
|
bet_week: str = Field("sheets", description="Betting week identifier")
|
||||||
trade_deadline: int = Field(1, description="Trade deadline week")
|
trade_deadline: int = Field(1, description="Trade deadline week")
|
||||||
pick_trade_start: int = Field(69, description="Draft pick trading start week")
|
pick_trade_start: int = Field(69, description="Draft pick trading start week")
|
||||||
pick_trade_end: int = Field(420, description="Draft pick trading end week")
|
pick_trade_end: int = Field(420, description="Draft pick trading end week")
|
||||||
playoffs_begin: int = Field(420, description="Week when playoffs begin")
|
playoffs_begin: int = Field(420, description="Week when playoffs begin")
|
||||||
|
|
||||||
@field_validator("bet_week", mode="before")
|
@field_validator("bet_week", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def cast_bet_week_to_string(cls, v):
|
def cast_bet_week_to_string(cls, v):
|
||||||
"""Ensure bet_week is always a string."""
|
"""Ensure bet_week is always a string."""
|
||||||
return str(v) if v is not None else 'sheets'
|
return str(v) if v is not None else "sheets"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_offseason(self) -> bool:
|
def is_offseason(self) -> bool:
|
||||||
"""Check if league is currently in offseason."""
|
"""Check if league is currently in offseason."""
|
||||||
return self.week > 18
|
return self.week > 18
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_playoffs(self) -> bool:
|
def is_playoffs(self) -> bool:
|
||||||
"""Check if league is currently in playoffs."""
|
"""Check if league is currently in playoffs."""
|
||||||
return self.week >= self.playoffs_begin
|
return self.week >= self.playoffs_begin
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def can_trade_picks(self) -> bool:
|
def can_trade_picks(self) -> bool:
|
||||||
"""Check if draft pick trading is currently allowed."""
|
"""Check if draft pick trading is currently allowed."""
|
||||||
return self.pick_trade_start <= self.week <= self.pick_trade_end
|
return self.pick_trade_start <= self.week <= self.pick_trade_end
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ever_trade_picks(self) -> bool:
|
def ever_trade_picks(self) -> bool:
|
||||||
"""Check if draft pick trading is allowed this season at all"""
|
"""Check if draft pick trading is allowed this season at all"""
|
||||||
return self.pick_trade_start <= self.playoffs_begin + 4
|
return self.pick_trade_start <= self.playoffs_begin + 4
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_past_trade_deadline(self) -> bool:
|
||||||
|
"""Check if the trade deadline has passed."""
|
||||||
|
if self.is_offseason:
|
||||||
|
return False
|
||||||
|
return self.week > self.trade_deadline
|
||||||
|
|||||||
@ -4,6 +4,7 @@ Custom Commands Service for Discord Bot v2.0
|
|||||||
Modern async service layer for managing custom commands with full type safety.
|
Modern async service layer for managing custom commands with full type safety.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import math
|
import math
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Optional, List, Any, Tuple
|
from typing import Optional, List, Any, Tuple
|
||||||
@ -119,8 +120,8 @@ class CustomCommandsService(BaseService[CustomCommand]):
|
|||||||
content_length=len(content),
|
content_length=len(content),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return full command with creator info
|
# Return command with creator info (use POST response directly)
|
||||||
return await self.get_command_by_name(name)
|
return result.model_copy(update={"creator": creator})
|
||||||
|
|
||||||
async def get_command_by_name(self, name: str) -> CustomCommand:
|
async def get_command_by_name(self, name: str) -> CustomCommand:
|
||||||
"""
|
"""
|
||||||
@ -217,7 +218,8 @@ class CustomCommandsService(BaseService[CustomCommand]):
|
|||||||
new_content_length=len(new_content),
|
new_content_length=len(new_content),
|
||||||
)
|
)
|
||||||
|
|
||||||
return await self.get_command_by_name(name)
|
# Return updated command with creator info (use PUT response directly)
|
||||||
|
return result.model_copy(update={"creator": command.creator})
|
||||||
|
|
||||||
async def delete_command(
|
async def delete_command(
|
||||||
self, name: str, deleter_discord_id: int, force: bool = False
|
self, name: str, deleter_discord_id: int, force: bool = False
|
||||||
@ -466,21 +468,28 @@ class CustomCommandsService(BaseService[CustomCommand]):
|
|||||||
|
|
||||||
commands_data = await self.get_items_with_params(params)
|
commands_data = await self.get_items_with_params(params)
|
||||||
|
|
||||||
|
creators = await asyncio.gather(
|
||||||
|
*[
|
||||||
|
self.get_creator_by_id(cmd_data.creator_id)
|
||||||
|
for cmd_data in commands_data
|
||||||
|
],
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
|
|
||||||
commands = []
|
commands = []
|
||||||
for cmd_data in commands_data:
|
for cmd_data, creator in zip(commands_data, creators):
|
||||||
try:
|
if isinstance(creator, BotException):
|
||||||
creator = await self.get_creator_by_id(cmd_data.creator_id)
|
|
||||||
commands.append(CustomCommand(**cmd_data.model_dump(), creator=creator))
|
|
||||||
except BotException as e:
|
|
||||||
# Handle missing creator gracefully
|
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
"Skipping popular command with missing creator",
|
"Skipping popular command with missing creator",
|
||||||
command_id=cmd_data.id,
|
command_id=cmd_data.id,
|
||||||
command_name=cmd_data.name,
|
command_name=cmd_data.name,
|
||||||
creator_id=cmd_data.creator_id,
|
creator_id=cmd_data.creator_id,
|
||||||
error=str(e),
|
error=str(creator),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
if isinstance(creator, BaseException):
|
||||||
|
raise creator
|
||||||
|
commands.append(CustomCommand(**cmd_data.model_dump(), creator=creator))
|
||||||
|
|
||||||
return commands
|
return commands
|
||||||
|
|
||||||
@ -536,7 +545,9 @@ class CustomCommandsService(BaseService[CustomCommand]):
|
|||||||
# Update username if it changed
|
# Update username if it changed
|
||||||
if creator.username != username or creator.display_name != display_name:
|
if creator.username != username or creator.display_name != display_name:
|
||||||
await self._update_creator_info(creator.id, username, display_name)
|
await self._update_creator_info(creator.id, username, display_name)
|
||||||
creator = await self.get_creator_by_discord_id(discord_id)
|
creator = creator.model_copy(
|
||||||
|
update={"username": username, "display_name": display_name}
|
||||||
|
)
|
||||||
return creator
|
return creator
|
||||||
except BotException:
|
except BotException:
|
||||||
# Creator doesn't exist, create new one
|
# Creator doesn't exist, create new one
|
||||||
@ -557,7 +568,8 @@ class CustomCommandsService(BaseService[CustomCommand]):
|
|||||||
if not result:
|
if not result:
|
||||||
raise BotException("Failed to create command creator")
|
raise BotException("Failed to create command creator")
|
||||||
|
|
||||||
return await self.get_creator_by_discord_id(discord_id)
|
# Return created creator directly from POST response
|
||||||
|
return CustomCommandCreator(**result)
|
||||||
|
|
||||||
async def get_creator_by_discord_id(self, discord_id: int) -> CustomCommandCreator:
|
async def get_creator_by_discord_id(self, discord_id: int) -> CustomCommandCreator:
|
||||||
"""Get creator by Discord ID.
|
"""Get creator by Discord ID.
|
||||||
@ -610,31 +622,34 @@ class CustomCommandsService(BaseService[CustomCommand]):
|
|||||||
|
|
||||||
async def get_statistics(self) -> CustomCommandStats:
|
async def get_statistics(self) -> CustomCommandStats:
|
||||||
"""Get comprehensive statistics about custom commands."""
|
"""Get comprehensive statistics about custom commands."""
|
||||||
# Get basic counts
|
|
||||||
total_commands = await self._get_search_count([])
|
|
||||||
active_commands = await self._get_search_count([("is_active", True)])
|
|
||||||
total_creators = await self._get_creator_count()
|
|
||||||
|
|
||||||
# Get total uses
|
|
||||||
all_commands = await self.get_items_with_params([("is_active", True)])
|
|
||||||
total_uses = sum(cmd.use_count for cmd in all_commands)
|
|
||||||
|
|
||||||
# Get most popular command
|
|
||||||
popular_commands = await self.get_popular_commands(limit=1)
|
|
||||||
most_popular = popular_commands[0] if popular_commands else None
|
|
||||||
|
|
||||||
# Get most active creator
|
|
||||||
most_active_creator = await self._get_most_active_creator()
|
|
||||||
|
|
||||||
# Get recent commands count
|
|
||||||
week_ago = datetime.now(UTC) - timedelta(days=7)
|
week_ago = datetime.now(UTC) - timedelta(days=7)
|
||||||
recent_count = await self._get_search_count(
|
|
||||||
[("created_at__gte", week_ago.isoformat()), ("is_active", True)]
|
(
|
||||||
|
total_commands,
|
||||||
|
active_commands,
|
||||||
|
total_creators,
|
||||||
|
all_commands,
|
||||||
|
popular_commands,
|
||||||
|
most_active_creator,
|
||||||
|
recent_count,
|
||||||
|
warning_count,
|
||||||
|
deletion_count,
|
||||||
|
) = await asyncio.gather(
|
||||||
|
self._get_search_count([]),
|
||||||
|
self._get_search_count([("is_active", True)]),
|
||||||
|
self._get_creator_count(),
|
||||||
|
self.get_items_with_params([("is_active", True)]),
|
||||||
|
self.get_popular_commands(limit=1),
|
||||||
|
self._get_most_active_creator(),
|
||||||
|
self._get_search_count(
|
||||||
|
[("created_at__gte", week_ago.isoformat()), ("is_active", True)]
|
||||||
|
),
|
||||||
|
self._get_commands_needing_warning_count(),
|
||||||
|
self._get_commands_eligible_for_deletion_count(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get cleanup statistics
|
total_uses = sum(cmd.use_count for cmd in all_commands)
|
||||||
warning_count = await self._get_commands_needing_warning_count()
|
most_popular = popular_commands[0] if popular_commands else None
|
||||||
deletion_count = await self._get_commands_eligible_for_deletion_count()
|
|
||||||
|
|
||||||
return CustomCommandStats(
|
return CustomCommandStats(
|
||||||
total_commands=total_commands,
|
total_commands=total_commands,
|
||||||
@ -662,21 +677,28 @@ class CustomCommandsService(BaseService[CustomCommand]):
|
|||||||
|
|
||||||
commands_data = await self.get_items_with_params(params)
|
commands_data = await self.get_items_with_params(params)
|
||||||
|
|
||||||
|
creators = await asyncio.gather(
|
||||||
|
*[
|
||||||
|
self.get_creator_by_id(cmd_data.creator_id)
|
||||||
|
for cmd_data in commands_data
|
||||||
|
],
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
|
|
||||||
commands = []
|
commands = []
|
||||||
for cmd_data in commands_data:
|
for cmd_data, creator in zip(commands_data, creators):
|
||||||
try:
|
if isinstance(creator, BotException):
|
||||||
creator = await self.get_creator_by_id(cmd_data.creator_id)
|
|
||||||
commands.append(CustomCommand(**cmd_data.model_dump(), creator=creator))
|
|
||||||
except BotException as e:
|
|
||||||
# Handle missing creator gracefully
|
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
"Skipping command with missing creator",
|
"Skipping command with missing creator",
|
||||||
command_id=cmd_data.id,
|
command_id=cmd_data.id,
|
||||||
command_name=cmd_data.name,
|
command_name=cmd_data.name,
|
||||||
creator_id=cmd_data.creator_id,
|
creator_id=cmd_data.creator_id,
|
||||||
error=str(e),
|
error=str(creator),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
if isinstance(creator, BaseException):
|
||||||
|
raise creator
|
||||||
|
commands.append(CustomCommand(**cmd_data.model_dump(), creator=creator))
|
||||||
|
|
||||||
return commands
|
return commands
|
||||||
|
|
||||||
@ -688,21 +710,28 @@ class CustomCommandsService(BaseService[CustomCommand]):
|
|||||||
|
|
||||||
commands_data = await self.get_items_with_params(params)
|
commands_data = await self.get_items_with_params(params)
|
||||||
|
|
||||||
|
creators = await asyncio.gather(
|
||||||
|
*[
|
||||||
|
self.get_creator_by_id(cmd_data.creator_id)
|
||||||
|
for cmd_data in commands_data
|
||||||
|
],
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
|
|
||||||
commands = []
|
commands = []
|
||||||
for cmd_data in commands_data:
|
for cmd_data, creator in zip(commands_data, creators):
|
||||||
try:
|
if isinstance(creator, BotException):
|
||||||
creator = await self.get_creator_by_id(cmd_data.creator_id)
|
|
||||||
commands.append(CustomCommand(**cmd_data.model_dump(), creator=creator))
|
|
||||||
except BotException as e:
|
|
||||||
# Handle missing creator gracefully
|
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
"Skipping command with missing creator",
|
"Skipping command with missing creator",
|
||||||
command_id=cmd_data.id,
|
command_id=cmd_data.id,
|
||||||
command_name=cmd_data.name,
|
command_name=cmd_data.name,
|
||||||
creator_id=cmd_data.creator_id,
|
creator_id=cmd_data.creator_id,
|
||||||
error=str(e),
|
error=str(creator),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
if isinstance(creator, BaseException):
|
||||||
|
raise creator
|
||||||
|
commands.append(CustomCommand(**cmd_data.model_dump(), creator=creator))
|
||||||
|
|
||||||
return commands
|
return commands
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ Decision Service
|
|||||||
Manages pitching decision operations for game submission.
|
Manages pitching decision operations for game submission.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from typing import List, Dict, Any, Optional, Tuple
|
from typing import List, Dict, Any, Optional, Tuple
|
||||||
|
|
||||||
from utils.logging import get_contextual_logger
|
from utils.logging import get_contextual_logger
|
||||||
@ -124,22 +125,19 @@ class DecisionService:
|
|||||||
if int(decision.get("b_save", 0)) == 1:
|
if int(decision.get("b_save", 0)) == 1:
|
||||||
bsv_ids.append(pitcher_id)
|
bsv_ids.append(pitcher_id)
|
||||||
|
|
||||||
# Second pass: Fetch Player objects
|
# Second pass: Fetch all Player objects in parallel
|
||||||
wp = await player_service.get_player(wp_id) if wp_id else None
|
# Order: [wp_id, lp_id, sv_id, *hold_ids, *bsv_ids]; None IDs resolve immediately
|
||||||
lp = await player_service.get_player(lp_id) if lp_id else None
|
ordered_ids = [wp_id, lp_id, sv_id] + hold_ids + bsv_ids
|
||||||
sv = await player_service.get_player(sv_id) if sv_id else None
|
results = await asyncio.gather(
|
||||||
|
*[
|
||||||
|
player_service.get_player(pid) if pid else asyncio.sleep(0, result=None)
|
||||||
|
for pid in ordered_ids
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
holders = []
|
wp, lp, sv = results[0], results[1], results[2]
|
||||||
for hold_id in hold_ids:
|
holders = [p for p in results[3 : 3 + len(hold_ids)] if p]
|
||||||
holder = await player_service.get_player(hold_id)
|
blown_saves = [p for p in results[3 + len(hold_ids) :] if p]
|
||||||
if holder:
|
|
||||||
holders.append(holder)
|
|
||||||
|
|
||||||
blown_saves = []
|
|
||||||
for bsv_id in bsv_ids:
|
|
||||||
bsv = await player_service.get_player(bsv_id)
|
|
||||||
if bsv:
|
|
||||||
blown_saves.append(bsv)
|
|
||||||
|
|
||||||
return wp, lp, sv, holders, blown_saves
|
return wp, lp, sv, holders, blown_saves
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ Modern async service layer for managing help commands with full type safety.
|
|||||||
Allows admins and help editors to create custom help topics for league documentation,
|
Allows admins and help editors to create custom help topics for league documentation,
|
||||||
resources, FAQs, links, and guides.
|
resources, FAQs, links, and guides.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from utils.logging import get_contextual_logger
|
from utils.logging import get_contextual_logger
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ from models.help_command import (
|
|||||||
HelpCommand,
|
HelpCommand,
|
||||||
HelpCommandSearchFilters,
|
HelpCommandSearchFilters,
|
||||||
HelpCommandSearchResult,
|
HelpCommandSearchResult,
|
||||||
HelpCommandStats
|
HelpCommandStats,
|
||||||
)
|
)
|
||||||
from services.base_service import BaseService
|
from services.base_service import BaseService
|
||||||
from exceptions import BotException
|
from exceptions import BotException
|
||||||
@ -20,16 +21,19 @@ from exceptions import BotException
|
|||||||
|
|
||||||
class HelpCommandNotFoundError(BotException):
|
class HelpCommandNotFoundError(BotException):
|
||||||
"""Raised when a help command is not found."""
|
"""Raised when a help command is not found."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class HelpCommandExistsError(BotException):
|
class HelpCommandExistsError(BotException):
|
||||||
"""Raised when trying to create a help command that already exists."""
|
"""Raised when trying to create a help command that already exists."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class HelpCommandPermissionError(BotException):
|
class HelpCommandPermissionError(BotException):
|
||||||
"""Raised when user lacks permission for help command operation."""
|
"""Raised when user lacks permission for help command operation."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -37,8 +41,8 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
"""Service for managing help commands."""
|
"""Service for managing help commands."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(HelpCommand, 'help_commands')
|
super().__init__(HelpCommand, "help_commands")
|
||||||
self.logger = get_contextual_logger(f'{__name__}.HelpCommandsService')
|
self.logger = get_contextual_logger(f"{__name__}.HelpCommandsService")
|
||||||
self.logger.info("HelpCommandsService initialized")
|
self.logger.info("HelpCommandsService initialized")
|
||||||
|
|
||||||
# === Command CRUD Operations ===
|
# === Command CRUD Operations ===
|
||||||
@ -50,7 +54,7 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
content: str,
|
content: str,
|
||||||
creator_discord_id: str,
|
creator_discord_id: str,
|
||||||
category: Optional[str] = None,
|
category: Optional[str] = None,
|
||||||
display_order: int = 0
|
display_order: int = 0,
|
||||||
) -> HelpCommand:
|
) -> HelpCommand:
|
||||||
"""
|
"""
|
||||||
Create a new help command.
|
Create a new help command.
|
||||||
@ -80,14 +84,16 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
|
|
||||||
# Create help command data
|
# Create help command data
|
||||||
help_data = {
|
help_data = {
|
||||||
'name': name.lower().strip(),
|
"name": name.lower().strip(),
|
||||||
'title': title.strip(),
|
"title": title.strip(),
|
||||||
'content': content.strip(),
|
"content": content.strip(),
|
||||||
'category': category.lower().strip() if category else None,
|
"category": category.lower().strip() if category else None,
|
||||||
'created_by_discord_id': str(creator_discord_id), # Convert to string for safe storage
|
"created_by_discord_id": str(
|
||||||
'display_order': display_order,
|
creator_discord_id
|
||||||
'is_active': True,
|
), # Convert to string for safe storage
|
||||||
'view_count': 0
|
"display_order": display_order,
|
||||||
|
"is_active": True,
|
||||||
|
"view_count": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create via API
|
# Create via API
|
||||||
@ -95,18 +101,18 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
if not result:
|
if not result:
|
||||||
raise BotException("Failed to create help command")
|
raise BotException("Failed to create help command")
|
||||||
|
|
||||||
self.logger.info("Help command created",
|
self.logger.info(
|
||||||
help_name=name,
|
"Help command created",
|
||||||
creator_id=creator_discord_id,
|
help_name=name,
|
||||||
category=category)
|
creator_id=creator_discord_id,
|
||||||
|
category=category,
|
||||||
|
)
|
||||||
|
|
||||||
# Return full help command
|
# Return help command directly from POST response
|
||||||
return await self.get_help_by_name(name)
|
return result
|
||||||
|
|
||||||
async def get_help_by_name(
|
async def get_help_by_name(
|
||||||
self,
|
self, name: str, include_inactive: bool = False
|
||||||
name: str,
|
|
||||||
include_inactive: bool = False
|
|
||||||
) -> HelpCommand:
|
) -> HelpCommand:
|
||||||
"""
|
"""
|
||||||
Get a help command by name.
|
Get a help command by name.
|
||||||
@ -126,8 +132,12 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
try:
|
try:
|
||||||
# Use the dedicated by_name endpoint for exact lookup
|
# Use the dedicated by_name endpoint for exact lookup
|
||||||
client = await self.get_client()
|
client = await self.get_client()
|
||||||
params = [('include_inactive', include_inactive)] if include_inactive else []
|
params = (
|
||||||
data = await client.get(f'help_commands/by_name/{normalized_name}', params=params)
|
[("include_inactive", include_inactive)] if include_inactive else []
|
||||||
|
)
|
||||||
|
data = await client.get(
|
||||||
|
f"help_commands/by_name/{normalized_name}", params=params
|
||||||
|
)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
raise HelpCommandNotFoundError(f"Help topic '{name}' not found")
|
raise HelpCommandNotFoundError(f"Help topic '{name}' not found")
|
||||||
@ -139,9 +149,9 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
if "404" in str(e) or "not found" in str(e).lower():
|
if "404" in str(e) or "not found" in str(e).lower():
|
||||||
raise HelpCommandNotFoundError(f"Help topic '{name}' not found")
|
raise HelpCommandNotFoundError(f"Help topic '{name}' not found")
|
||||||
else:
|
else:
|
||||||
self.logger.error("Failed to get help command by name",
|
self.logger.error(
|
||||||
help_name=name,
|
"Failed to get help command by name", help_name=name, error=e
|
||||||
error=e)
|
)
|
||||||
raise BotException(f"Failed to retrieve help topic '{name}': {e}")
|
raise BotException(f"Failed to retrieve help topic '{name}': {e}")
|
||||||
|
|
||||||
async def update_help(
|
async def update_help(
|
||||||
@ -151,7 +161,7 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
new_content: Optional[str] = None,
|
new_content: Optional[str] = None,
|
||||||
updater_discord_id: Optional[str] = None,
|
updater_discord_id: Optional[str] = None,
|
||||||
new_category: Optional[str] = None,
|
new_category: Optional[str] = None,
|
||||||
new_display_order: Optional[int] = None
|
new_display_order: Optional[int] = None,
|
||||||
) -> HelpCommand:
|
) -> HelpCommand:
|
||||||
"""
|
"""
|
||||||
Update an existing help command.
|
Update an existing help command.
|
||||||
@ -176,35 +186,42 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
update_data = {}
|
update_data = {}
|
||||||
|
|
||||||
if new_title is not None:
|
if new_title is not None:
|
||||||
update_data['title'] = new_title.strip()
|
update_data["title"] = new_title.strip()
|
||||||
|
|
||||||
if new_content is not None:
|
if new_content is not None:
|
||||||
update_data['content'] = new_content.strip()
|
update_data["content"] = new_content.strip()
|
||||||
|
|
||||||
if new_category is not None:
|
if new_category is not None:
|
||||||
update_data['category'] = new_category.lower().strip() if new_category else None
|
update_data["category"] = (
|
||||||
|
new_category.lower().strip() if new_category else None
|
||||||
|
)
|
||||||
|
|
||||||
if new_display_order is not None:
|
if new_display_order is not None:
|
||||||
update_data['display_order'] = new_display_order
|
update_data["display_order"] = new_display_order
|
||||||
|
|
||||||
if updater_discord_id is not None:
|
if updater_discord_id is not None:
|
||||||
update_data['last_modified_by'] = str(updater_discord_id) # Convert to string for safe storage
|
update_data["last_modified_by"] = str(
|
||||||
|
updater_discord_id
|
||||||
|
) # Convert to string for safe storage
|
||||||
|
|
||||||
if not update_data:
|
if not update_data:
|
||||||
raise BotException("No fields to update")
|
raise BotException("No fields to update")
|
||||||
|
|
||||||
# Update via API
|
# Update via API
|
||||||
client = await self.get_client()
|
client = await self.get_client()
|
||||||
result = await client.put(f'help_commands/{help_cmd.id}', update_data)
|
result = await client.put(f"help_commands/{help_cmd.id}", update_data)
|
||||||
if not result:
|
if not result:
|
||||||
raise BotException("Failed to update help command")
|
raise BotException("Failed to update help command")
|
||||||
|
|
||||||
self.logger.info("Help command updated",
|
self.logger.info(
|
||||||
help_name=name,
|
"Help command updated",
|
||||||
updater_id=updater_discord_id,
|
help_name=name,
|
||||||
fields_updated=list(update_data.keys()))
|
updater_id=updater_discord_id,
|
||||||
|
fields_updated=list(update_data.keys()),
|
||||||
|
)
|
||||||
|
|
||||||
return await self.get_help_by_name(name)
|
# Return updated help command directly from PUT response
|
||||||
|
return self.model_class.from_api_data(result)
|
||||||
|
|
||||||
async def delete_help(self, name: str) -> bool:
|
async def delete_help(self, name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -223,11 +240,11 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
|
|
||||||
# Soft delete via API
|
# Soft delete via API
|
||||||
client = await self.get_client()
|
client = await self.get_client()
|
||||||
await client.delete(f'help_commands/{help_cmd.id}')
|
await client.delete(f"help_commands/{help_cmd.id}")
|
||||||
|
|
||||||
self.logger.info("Help command soft deleted",
|
self.logger.info(
|
||||||
help_name=name,
|
"Help command soft deleted", help_name=name, help_id=help_cmd.id
|
||||||
help_id=help_cmd.id)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -252,13 +269,11 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
|
|
||||||
# Restore via API
|
# Restore via API
|
||||||
client = await self.get_client()
|
client = await self.get_client()
|
||||||
result = await client.patch(f'help_commands/{help_cmd.id}/restore')
|
result = await client.patch(f"help_commands/{help_cmd.id}/restore")
|
||||||
if not result:
|
if not result:
|
||||||
raise BotException("Failed to restore help command")
|
raise BotException("Failed to restore help command")
|
||||||
|
|
||||||
self.logger.info("Help command restored",
|
self.logger.info("Help command restored", help_name=name, help_id=help_cmd.id)
|
||||||
help_name=name,
|
|
||||||
help_id=help_cmd.id)
|
|
||||||
|
|
||||||
return self.model_class.from_api_data(result)
|
return self.model_class.from_api_data(result)
|
||||||
|
|
||||||
@ -279,10 +294,9 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
client = await self.get_client()
|
client = await self.get_client()
|
||||||
await client.patch(f'help_commands/by_name/{normalized_name}/view')
|
await client.patch(f"help_commands/by_name/{normalized_name}/view")
|
||||||
|
|
||||||
self.logger.debug("Help command view count incremented",
|
self.logger.debug("Help command view count incremented", help_name=name)
|
||||||
help_name=name)
|
|
||||||
|
|
||||||
# Return updated command
|
# Return updated command
|
||||||
return await self.get_help_by_name(name)
|
return await self.get_help_by_name(name)
|
||||||
@ -291,16 +305,15 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
if "404" in str(e) or "not found" in str(e).lower():
|
if "404" in str(e) or "not found" in str(e).lower():
|
||||||
raise HelpCommandNotFoundError(f"Help topic '{name}' not found")
|
raise HelpCommandNotFoundError(f"Help topic '{name}' not found")
|
||||||
else:
|
else:
|
||||||
self.logger.error("Failed to increment view count",
|
self.logger.error(
|
||||||
help_name=name,
|
"Failed to increment view count", help_name=name, error=e
|
||||||
error=e)
|
)
|
||||||
raise BotException(f"Failed to increment view count for '{name}': {e}")
|
raise BotException(f"Failed to increment view count for '{name}': {e}")
|
||||||
|
|
||||||
# === Search and Listing ===
|
# === Search and Listing ===
|
||||||
|
|
||||||
async def search_help_commands(
|
async def search_help_commands(
|
||||||
self,
|
self, filters: HelpCommandSearchFilters
|
||||||
filters: HelpCommandSearchFilters
|
|
||||||
) -> HelpCommandSearchResult:
|
) -> HelpCommandSearchResult:
|
||||||
"""
|
"""
|
||||||
Search for help commands with filtering and pagination.
|
Search for help commands with filtering and pagination.
|
||||||
@ -316,23 +329,23 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
|
|
||||||
# Apply filters
|
# Apply filters
|
||||||
if filters.name_contains:
|
if filters.name_contains:
|
||||||
params.append(('name', filters.name_contains)) # API will do ILIKE search
|
params.append(("name", filters.name_contains)) # API will do ILIKE search
|
||||||
|
|
||||||
if filters.category:
|
if filters.category:
|
||||||
params.append(('category', filters.category))
|
params.append(("category", filters.category))
|
||||||
|
|
||||||
params.append(('is_active', filters.is_active))
|
params.append(("is_active", filters.is_active))
|
||||||
|
|
||||||
# Add sorting
|
# Add sorting
|
||||||
params.append(('sort', filters.sort_by))
|
params.append(("sort", filters.sort_by))
|
||||||
|
|
||||||
# Add pagination
|
# Add pagination
|
||||||
params.append(('page', filters.page))
|
params.append(("page", filters.page))
|
||||||
params.append(('page_size', filters.page_size))
|
params.append(("page_size", filters.page_size))
|
||||||
|
|
||||||
# Execute search via API
|
# Execute search via API
|
||||||
client = await self.get_client()
|
client = await self.get_client()
|
||||||
data = await client.get('help_commands', params=params)
|
data = await client.get("help_commands", params=params)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return HelpCommandSearchResult(
|
return HelpCommandSearchResult(
|
||||||
@ -341,14 +354,14 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
page=filters.page,
|
page=filters.page,
|
||||||
page_size=filters.page_size,
|
page_size=filters.page_size,
|
||||||
total_pages=0,
|
total_pages=0,
|
||||||
has_more=False
|
has_more=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract response data
|
# Extract response data
|
||||||
help_commands_data = data.get('help_commands', [])
|
help_commands_data = data.get("help_commands", [])
|
||||||
total_count = data.get('total_count', 0)
|
total_count = data.get("total_count", 0)
|
||||||
total_pages = data.get('total_pages', 0)
|
total_pages = data.get("total_pages", 0)
|
||||||
has_more = data.get('has_more', False)
|
has_more = data.get("has_more", False)
|
||||||
|
|
||||||
# Convert to HelpCommand objects
|
# Convert to HelpCommand objects
|
||||||
help_commands = []
|
help_commands = []
|
||||||
@ -356,15 +369,21 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
try:
|
try:
|
||||||
help_commands.append(self.model_class.from_api_data(cmd_data))
|
help_commands.append(self.model_class.from_api_data(cmd_data))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning("Failed to create HelpCommand from API data",
|
self.logger.warning(
|
||||||
help_id=cmd_data.get('id'),
|
"Failed to create HelpCommand from API data",
|
||||||
error=e)
|
help_id=cmd_data.get("id"),
|
||||||
|
error=e,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.logger.debug("Help commands search completed",
|
self.logger.debug(
|
||||||
total_results=total_count,
|
"Help commands search completed",
|
||||||
page=filters.page,
|
total_results=total_count,
|
||||||
filters_applied=len([p for p in params if p[0] not in ['sort', 'page', 'page_size']]))
|
page=filters.page,
|
||||||
|
filters_applied=len(
|
||||||
|
[p for p in params if p[0] not in ["sort", "page", "page_size"]]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
return HelpCommandSearchResult(
|
return HelpCommandSearchResult(
|
||||||
help_commands=help_commands,
|
help_commands=help_commands,
|
||||||
@ -372,13 +391,11 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
page=filters.page,
|
page=filters.page,
|
||||||
page_size=filters.page_size,
|
page_size=filters.page_size,
|
||||||
total_pages=total_pages,
|
total_pages=total_pages,
|
||||||
has_more=has_more
|
has_more=has_more,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_all_help_topics(
|
async def get_all_help_topics(
|
||||||
self,
|
self, category: Optional[str] = None, include_inactive: bool = False
|
||||||
category: Optional[str] = None,
|
|
||||||
include_inactive: bool = False
|
|
||||||
) -> List[HelpCommand]:
|
) -> List[HelpCommand]:
|
||||||
"""
|
"""
|
||||||
Get all help topics, optionally filtered by category.
|
Get all help topics, optionally filtered by category.
|
||||||
@ -393,37 +410,36 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
params = []
|
params = []
|
||||||
|
|
||||||
if category:
|
if category:
|
||||||
params.append(('category', category))
|
params.append(("category", category))
|
||||||
|
|
||||||
params.append(('is_active', not include_inactive))
|
params.append(("is_active", not include_inactive))
|
||||||
params.append(('sort', 'display_order'))
|
params.append(("sort", "display_order"))
|
||||||
params.append(('page_size', 100)) # Get all
|
params.append(("page_size", 100)) # Get all
|
||||||
|
|
||||||
client = await self.get_client()
|
client = await self.get_client()
|
||||||
data = await client.get('help_commands', params=params)
|
data = await client.get("help_commands", params=params)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
help_commands_data = data.get('help_commands', [])
|
help_commands_data = data.get("help_commands", [])
|
||||||
|
|
||||||
help_commands = []
|
help_commands = []
|
||||||
for cmd_data in help_commands_data:
|
for cmd_data in help_commands_data:
|
||||||
try:
|
try:
|
||||||
help_commands.append(self.model_class.from_api_data(cmd_data))
|
help_commands.append(self.model_class.from_api_data(cmd_data))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning("Failed to create HelpCommand from API data",
|
self.logger.warning(
|
||||||
help_id=cmd_data.get('id'),
|
"Failed to create HelpCommand from API data",
|
||||||
error=e)
|
help_id=cmd_data.get("id"),
|
||||||
|
error=e,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return help_commands
|
return help_commands
|
||||||
|
|
||||||
async def get_help_names_for_autocomplete(
|
async def get_help_names_for_autocomplete(
|
||||||
self,
|
self, partial_name: str = "", limit: int = 25, include_inactive: bool = False
|
||||||
partial_name: str = "",
|
|
||||||
limit: int = 25,
|
|
||||||
include_inactive: bool = False
|
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Get help command names for Discord autocomplete.
|
Get help command names for Discord autocomplete.
|
||||||
@ -439,25 +455,28 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
try:
|
try:
|
||||||
# Use the dedicated autocomplete endpoint
|
# Use the dedicated autocomplete endpoint
|
||||||
client = await self.get_client()
|
client = await self.get_client()
|
||||||
params = [('limit', limit)]
|
params = [("limit", limit)]
|
||||||
|
|
||||||
if partial_name:
|
if partial_name:
|
||||||
params.append(('q', partial_name.lower()))
|
params.append(("q", partial_name.lower()))
|
||||||
|
|
||||||
result = await client.get('help_commands/autocomplete', params=params)
|
result = await client.get("help_commands/autocomplete", params=params)
|
||||||
|
|
||||||
# The autocomplete endpoint returns results with name, title, category
|
# The autocomplete endpoint returns results with name, title, category
|
||||||
if isinstance(result, dict) and 'results' in result:
|
if isinstance(result, dict) and "results" in result:
|
||||||
return [item['name'] for item in result['results']]
|
return [item["name"] for item in result["results"]]
|
||||||
else:
|
else:
|
||||||
self.logger.warning("Unexpected autocomplete response format",
|
self.logger.warning(
|
||||||
response=result)
|
"Unexpected autocomplete response format", response=result
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error("Failed to get help names for autocomplete",
|
self.logger.error(
|
||||||
partial_name=partial_name,
|
"Failed to get help names for autocomplete",
|
||||||
error=e)
|
partial_name=partial_name,
|
||||||
|
error=e,
|
||||||
|
)
|
||||||
# Return empty list on error to not break Discord autocomplete
|
# Return empty list on error to not break Discord autocomplete
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@ -467,7 +486,7 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
"""Get comprehensive statistics about help commands."""
|
"""Get comprehensive statistics about help commands."""
|
||||||
try:
|
try:
|
||||||
client = await self.get_client()
|
client = await self.get_client()
|
||||||
data = await client.get('help_commands/stats')
|
data = await client.get("help_commands/stats")
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return HelpCommandStats(
|
return HelpCommandStats(
|
||||||
@ -475,23 +494,25 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
active_commands=0,
|
active_commands=0,
|
||||||
total_views=0,
|
total_views=0,
|
||||||
most_viewed_command=None,
|
most_viewed_command=None,
|
||||||
recent_commands_count=0
|
recent_commands_count=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Convert most_viewed_command if present
|
# Convert most_viewed_command if present
|
||||||
most_viewed = None
|
most_viewed = None
|
||||||
if data.get('most_viewed_command'):
|
if data.get("most_viewed_command"):
|
||||||
try:
|
try:
|
||||||
most_viewed = self.model_class.from_api_data(data['most_viewed_command'])
|
most_viewed = self.model_class.from_api_data(
|
||||||
|
data["most_viewed_command"]
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning("Failed to parse most viewed command", error=e)
|
self.logger.warning("Failed to parse most viewed command", error=e)
|
||||||
|
|
||||||
return HelpCommandStats(
|
return HelpCommandStats(
|
||||||
total_commands=data.get('total_commands', 0),
|
total_commands=data.get("total_commands", 0),
|
||||||
active_commands=data.get('active_commands', 0),
|
active_commands=data.get("active_commands", 0),
|
||||||
total_views=data.get('total_views', 0),
|
total_views=data.get("total_views", 0),
|
||||||
most_viewed_command=most_viewed,
|
most_viewed_command=most_viewed,
|
||||||
recent_commands_count=data.get('recent_commands_count', 0)
|
recent_commands_count=data.get("recent_commands_count", 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -502,7 +523,7 @@ class HelpCommandsService(BaseService[HelpCommand]):
|
|||||||
active_commands=0,
|
active_commands=0,
|
||||||
total_views=0,
|
total_views=0,
|
||||||
most_viewed_command=None,
|
most_viewed_command=None,
|
||||||
recent_commands_count=0
|
recent_commands_count=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -277,6 +277,35 @@ class TransactionBuilder:
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple of (success: bool, error_message: str). If success is True, error_message is empty.
|
Tuple of (success: bool, error_message: str). If success is True, error_message is empty.
|
||||||
"""
|
"""
|
||||||
|
# Fetch current state once if needed by FA lock or pending-transaction check
|
||||||
|
is_fa_pickup = (
|
||||||
|
move.from_roster == RosterType.FREE_AGENCY
|
||||||
|
and move.to_roster != RosterType.FREE_AGENCY
|
||||||
|
)
|
||||||
|
needs_current_state = is_fa_pickup or (
|
||||||
|
check_pending_transactions and next_week is None
|
||||||
|
)
|
||||||
|
|
||||||
|
current_week: Optional[int] = None
|
||||||
|
if needs_current_state:
|
||||||
|
try:
|
||||||
|
current_state = await league_service.get_current_state()
|
||||||
|
current_week = current_state.week if current_state else 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not get current week: {e}")
|
||||||
|
current_week = 1
|
||||||
|
|
||||||
|
# Block adding players FROM Free Agency after the FA lock deadline
|
||||||
|
if is_fa_pickup and current_week is not None:
|
||||||
|
config = get_config()
|
||||||
|
if current_week >= config.fa_lock_week:
|
||||||
|
error_msg = (
|
||||||
|
f"Free agency is closed (week {current_week}, deadline was week {config.fa_lock_week}). "
|
||||||
|
f"Cannot add {move.player.name} from FA."
|
||||||
|
)
|
||||||
|
logger.warning(error_msg)
|
||||||
|
return False, error_msg
|
||||||
|
|
||||||
# Check if player is already in a move in this transaction builder
|
# Check if player is already in a move in this transaction builder
|
||||||
existing_move = self.get_move_for_player(move.player.id)
|
existing_move = self.get_move_for_player(move.player.id)
|
||||||
if existing_move:
|
if existing_move:
|
||||||
@ -299,23 +328,15 @@ class TransactionBuilder:
|
|||||||
return False, error_msg
|
return False, error_msg
|
||||||
|
|
||||||
# Check if player is already in another team's pending transaction for next week
|
# Check if player is already in another team's pending transaction for next week
|
||||||
# This prevents duplicate claims that would need to be resolved at freeze time
|
|
||||||
# Only applies to /dropadd (scheduled moves), not /ilmove (immediate moves)
|
|
||||||
if check_pending_transactions:
|
if check_pending_transactions:
|
||||||
if next_week is None:
|
if next_week is None:
|
||||||
try:
|
next_week = (current_week + 1) if current_week else 1
|
||||||
current_state = await league_service.get_current_state()
|
|
||||||
next_week = (current_state.week + 1) if current_state else 1
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Could not get current week for pending transaction check: {e}"
|
|
||||||
)
|
|
||||||
next_week = 1
|
|
||||||
|
|
||||||
is_pending, claiming_team = (
|
(
|
||||||
await transaction_service.is_player_in_pending_transaction(
|
is_pending,
|
||||||
player_id=move.player.id, week=next_week, season=self.season
|
claiming_team,
|
||||||
)
|
) = await transaction_service.is_player_in_pending_transaction(
|
||||||
|
player_id=move.player.id, week=next_week, season=self.season
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_pending:
|
if is_pending:
|
||||||
|
|||||||
143
tests/test_commands_trade_deadline.py
Normal file
143
tests/test_commands_trade_deadline.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
"""
|
||||||
|
Tests for trade deadline enforcement in /trade commands.
|
||||||
|
|
||||||
|
Validates that trades are blocked after the trade deadline and allowed during/before it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
from tests.factories import CurrentFactory, TeamFactory
|
||||||
|
|
||||||
|
|
||||||
|
class TestTradeInitiateDeadlineGuard:
|
||||||
|
"""Test trade deadline enforcement in /trade initiate command."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_interaction(self):
|
||||||
|
"""Create mock Discord interaction with deferred response."""
|
||||||
|
interaction = AsyncMock()
|
||||||
|
interaction.user = MagicMock()
|
||||||
|
interaction.user.id = 258104532423147520
|
||||||
|
interaction.response = AsyncMock()
|
||||||
|
interaction.followup = AsyncMock()
|
||||||
|
interaction.guild = MagicMock()
|
||||||
|
interaction.guild.id = 669356687294988350
|
||||||
|
return interaction
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_trade_initiate_blocked_past_deadline(self, mock_interaction):
|
||||||
|
"""After the trade deadline, /trade initiate should return a friendly error."""
|
||||||
|
user_team = TeamFactory.west_virginia()
|
||||||
|
other_team = TeamFactory.new_york()
|
||||||
|
past_deadline = CurrentFactory.create(week=15, trade_deadline=14)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"commands.transactions.trade.validate_user_has_team",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=user_team,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"commands.transactions.trade.get_team_by_abbrev_with_validation",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=other_team,
|
||||||
|
),
|
||||||
|
patch("commands.transactions.trade.league_service") as mock_league,
|
||||||
|
):
|
||||||
|
mock_league.get_current_state = AsyncMock(return_value=past_deadline)
|
||||||
|
|
||||||
|
from commands.transactions.trade import TradeCommands
|
||||||
|
|
||||||
|
bot = MagicMock()
|
||||||
|
cog = TradeCommands(bot)
|
||||||
|
await cog.trade_initiate.callback(cog, mock_interaction, "NY")
|
||||||
|
|
||||||
|
mock_interaction.followup.send.assert_called_once()
|
||||||
|
call_kwargs = mock_interaction.followup.send.call_args
|
||||||
|
msg = (
|
||||||
|
call_kwargs[0][0]
|
||||||
|
if call_kwargs[0]
|
||||||
|
else call_kwargs[1].get("content", "")
|
||||||
|
)
|
||||||
|
assert "trade deadline has passed" in msg.lower()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_trade_initiate_allowed_at_deadline_week(self, mock_interaction):
|
||||||
|
"""During the deadline week itself, /trade initiate should proceed."""
|
||||||
|
user_team = TeamFactory.west_virginia()
|
||||||
|
other_team = TeamFactory.new_york()
|
||||||
|
at_deadline = CurrentFactory.create(week=14, trade_deadline=14)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"commands.transactions.trade.validate_user_has_team",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=user_team,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"commands.transactions.trade.get_team_by_abbrev_with_validation",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=other_team,
|
||||||
|
),
|
||||||
|
patch("commands.transactions.trade.league_service") as mock_league,
|
||||||
|
patch("commands.transactions.trade.clear_trade_builder") as mock_clear,
|
||||||
|
patch("commands.transactions.trade.get_trade_builder") as mock_get_builder,
|
||||||
|
patch(
|
||||||
|
"commands.transactions.trade.create_trade_embed",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=MagicMock(),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
mock_league.get_current_state = AsyncMock(return_value=at_deadline)
|
||||||
|
mock_builder = MagicMock()
|
||||||
|
mock_builder.add_team = AsyncMock(return_value=(True, None))
|
||||||
|
mock_builder.trade_id = "test-123"
|
||||||
|
mock_get_builder.return_value = mock_builder
|
||||||
|
|
||||||
|
from commands.transactions.trade import TradeCommands
|
||||||
|
|
||||||
|
bot = MagicMock()
|
||||||
|
cog = TradeCommands(bot)
|
||||||
|
cog.channel_manager = MagicMock()
|
||||||
|
cog.channel_manager.create_trade_channel = AsyncMock(return_value=None)
|
||||||
|
await cog.trade_initiate.callback(cog, mock_interaction, "NY")
|
||||||
|
|
||||||
|
# Should have proceeded past deadline check to clear/create trade
|
||||||
|
mock_clear.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_trade_initiate_blocked_when_current_none(self, mock_interaction):
|
||||||
|
"""When league state can't be fetched, /trade initiate should fail closed."""
|
||||||
|
user_team = TeamFactory.west_virginia()
|
||||||
|
other_team = TeamFactory.new_york()
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"commands.transactions.trade.validate_user_has_team",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=user_team,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"commands.transactions.trade.get_team_by_abbrev_with_validation",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=other_team,
|
||||||
|
),
|
||||||
|
patch("commands.transactions.trade.league_service") as mock_league,
|
||||||
|
):
|
||||||
|
mock_league.get_current_state = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
from commands.transactions.trade import TradeCommands
|
||||||
|
|
||||||
|
bot = MagicMock()
|
||||||
|
cog = TradeCommands(bot)
|
||||||
|
await cog.trade_initiate.callback(cog, mock_interaction, "NY")
|
||||||
|
|
||||||
|
mock_interaction.followup.send.assert_called_once()
|
||||||
|
call_kwargs = mock_interaction.followup.send.call_args
|
||||||
|
msg = (
|
||||||
|
call_kwargs[0][0]
|
||||||
|
if call_kwargs[0]
|
||||||
|
else call_kwargs[1].get("content", "")
|
||||||
|
)
|
||||||
|
assert "could not retrieve league state" in msg.lower()
|
||||||
@ -3,6 +3,7 @@ Tests for SBA data models
|
|||||||
|
|
||||||
Validates model creation, validation, and business logic.
|
Validates model creation, validation, and business logic.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from models import Team, Player, Current, DraftPick, DraftData, DraftList
|
from models import Team, Player, Current, DraftPick, DraftData, DraftList
|
||||||
@ -10,94 +11,102 @@ from models import Team, Player, Current, DraftPick, DraftData, DraftList
|
|||||||
|
|
||||||
class TestSBABaseModel:
|
class TestSBABaseModel:
|
||||||
"""Test base model functionality."""
|
"""Test base model functionality."""
|
||||||
|
|
||||||
def test_model_creation_with_api_data(self):
|
def test_model_creation_with_api_data(self):
|
||||||
"""Test creating models from API data."""
|
"""Test creating models from API data."""
|
||||||
team_data = {
|
team_data = {
|
||||||
'id': 1,
|
"id": 1,
|
||||||
'abbrev': 'NYY',
|
"abbrev": "NYY",
|
||||||
'sname': 'Yankees',
|
"sname": "Yankees",
|
||||||
'lname': 'New York Yankees',
|
"lname": "New York Yankees",
|
||||||
'season': 12
|
"season": 12,
|
||||||
}
|
}
|
||||||
|
|
||||||
team = Team.from_api_data(team_data)
|
team = Team.from_api_data(team_data)
|
||||||
assert team.id == 1
|
assert team.id == 1
|
||||||
assert team.abbrev == 'NYY'
|
assert team.abbrev == "NYY"
|
||||||
assert team.lname == 'New York Yankees'
|
assert team.lname == "New York Yankees"
|
||||||
|
|
||||||
def test_to_dict_functionality(self):
|
def test_to_dict_functionality(self):
|
||||||
"""Test model to dictionary conversion."""
|
"""Test model to dictionary conversion."""
|
||||||
team = Team(id=1, abbrev='LAA', sname='Angels', lname='Los Angeles Angels', season=12)
|
team = Team(
|
||||||
|
id=1, abbrev="LAA", sname="Angels", lname="Los Angeles Angels", season=12
|
||||||
|
)
|
||||||
|
|
||||||
team_dict = team.to_dict()
|
team_dict = team.to_dict()
|
||||||
assert 'abbrev' in team_dict
|
assert "abbrev" in team_dict
|
||||||
assert team_dict['abbrev'] == 'LAA'
|
assert team_dict["abbrev"] == "LAA"
|
||||||
assert team_dict['lname'] == 'Los Angeles Angels'
|
assert team_dict["lname"] == "Los Angeles Angels"
|
||||||
|
|
||||||
def test_model_repr(self):
|
def test_model_repr(self):
|
||||||
"""Test model string representation."""
|
"""Test model string representation."""
|
||||||
team = Team(id=2, abbrev='BOS', sname='Red Sox', lname='Boston Red Sox', season=12)
|
team = Team(
|
||||||
|
id=2, abbrev="BOS", sname="Red Sox", lname="Boston Red Sox", season=12
|
||||||
|
)
|
||||||
repr_str = repr(team)
|
repr_str = repr(team)
|
||||||
assert 'Team(' in repr_str
|
assert "Team(" in repr_str
|
||||||
assert 'abbrev=BOS' in repr_str
|
assert "abbrev=BOS" in repr_str
|
||||||
|
|
||||||
|
|
||||||
class TestTeamModel:
|
class TestTeamModel:
|
||||||
"""Test Team model functionality."""
|
"""Test Team model functionality."""
|
||||||
|
|
||||||
def test_team_creation_minimal(self):
|
def test_team_creation_minimal(self):
|
||||||
"""Test team creation with minimal required fields."""
|
"""Test team creation with minimal required fields."""
|
||||||
team = Team(
|
team = Team(
|
||||||
id=4,
|
id=4, abbrev="HOU", sname="Astros", lname="Houston Astros", season=12
|
||||||
abbrev='HOU',
|
|
||||||
sname='Astros',
|
|
||||||
lname='Houston Astros',
|
|
||||||
season=12
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert team.abbrev == 'HOU'
|
assert team.abbrev == "HOU"
|
||||||
assert team.sname == 'Astros'
|
assert team.sname == "Astros"
|
||||||
assert team.lname == 'Houston Astros'
|
assert team.lname == "Houston Astros"
|
||||||
assert team.season == 12
|
assert team.season == 12
|
||||||
|
|
||||||
def test_team_creation_with_optional_fields(self):
|
def test_team_creation_with_optional_fields(self):
|
||||||
"""Test team creation with optional fields."""
|
"""Test team creation with optional fields."""
|
||||||
team = Team(
|
team = Team(
|
||||||
id=5,
|
id=5,
|
||||||
abbrev='SF',
|
abbrev="SF",
|
||||||
sname='Giants',
|
sname="Giants",
|
||||||
lname='San Francisco Giants',
|
lname="San Francisco Giants",
|
||||||
season=12,
|
season=12,
|
||||||
gmid=100,
|
gmid=100,
|
||||||
division_id=1,
|
division_id=1,
|
||||||
stadium='Oracle Park',
|
stadium="Oracle Park",
|
||||||
color='FF8C00'
|
color="FF8C00",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert team.gmid == 100
|
assert team.gmid == 100
|
||||||
assert team.division_id == 1
|
assert team.division_id == 1
|
||||||
assert team.stadium == 'Oracle Park'
|
assert team.stadium == "Oracle Park"
|
||||||
assert team.color == 'FF8C00'
|
assert team.color == "FF8C00"
|
||||||
|
|
||||||
def test_team_str_representation(self):
|
def test_team_str_representation(self):
|
||||||
"""Test team string representation."""
|
"""Test team string representation."""
|
||||||
team = Team(id=3, abbrev='SD', sname='Padres', lname='San Diego Padres', season=12)
|
team = Team(
|
||||||
assert str(team) == 'SD - San Diego Padres'
|
id=3, abbrev="SD", sname="Padres", lname="San Diego Padres", season=12
|
||||||
|
)
|
||||||
|
assert str(team) == "SD - San Diego Padres"
|
||||||
|
|
||||||
def test_team_roster_type_major_league(self):
|
def test_team_roster_type_major_league(self):
|
||||||
"""Test roster type detection for Major League teams."""
|
"""Test roster type detection for Major League teams."""
|
||||||
from models.team import RosterType
|
from models.team import RosterType
|
||||||
|
|
||||||
# 3 chars or less → Major League
|
# 3 chars or less → Major League
|
||||||
team = Team(id=1, abbrev='NYY', sname='Yankees', lname='New York Yankees', season=12)
|
team = Team(
|
||||||
|
id=1, abbrev="NYY", sname="Yankees", lname="New York Yankees", season=12
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MAJOR_LEAGUE
|
assert team.roster_type() == RosterType.MAJOR_LEAGUE
|
||||||
|
|
||||||
team = Team(id=2, abbrev='BOS', sname='Red Sox', lname='Boston Red Sox', season=12)
|
team = Team(
|
||||||
|
id=2, abbrev="BOS", sname="Red Sox", lname="Boston Red Sox", season=12
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MAJOR_LEAGUE
|
assert team.roster_type() == RosterType.MAJOR_LEAGUE
|
||||||
|
|
||||||
# Even "BHM" (ends in M) should be Major League
|
# Even "BHM" (ends in M) should be Major League
|
||||||
team = Team(id=3, abbrev='BHM', sname='Iron', lname='Birmingham Iron', season=12)
|
team = Team(
|
||||||
|
id=3, abbrev="BHM", sname="Iron", lname="Birmingham Iron", season=12
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MAJOR_LEAGUE
|
assert team.roster_type() == RosterType.MAJOR_LEAGUE
|
||||||
|
|
||||||
def test_team_roster_type_minor_league(self):
|
def test_team_roster_type_minor_league(self):
|
||||||
@ -105,14 +114,28 @@ class TestTeamModel:
|
|||||||
from models.team import RosterType
|
from models.team import RosterType
|
||||||
|
|
||||||
# Standard Minor League: [Team] + "MIL"
|
# Standard Minor League: [Team] + "MIL"
|
||||||
team = Team(id=4, abbrev='NYYMIL', sname='RailRiders', lname='Staten Island RailRiders', season=12)
|
team = Team(
|
||||||
|
id=4,
|
||||||
|
abbrev="NYYMIL",
|
||||||
|
sname="RailRiders",
|
||||||
|
lname="Staten Island RailRiders",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
||||||
|
|
||||||
team = Team(id=5, abbrev='PORMIL', sname='Portland MiL', lname='Portland Minor League', season=12)
|
team = Team(
|
||||||
|
id=5,
|
||||||
|
abbrev="PORMIL",
|
||||||
|
sname="Portland MiL",
|
||||||
|
lname="Portland Minor League",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
||||||
|
|
||||||
# Case insensitive
|
# Case insensitive
|
||||||
team = Team(id=6, abbrev='LAAmil', sname='Bees', lname='Salt Lake Bees', season=12)
|
team = Team(
|
||||||
|
id=6, abbrev="LAAmil", sname="Bees", lname="Salt Lake Bees", season=12
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
||||||
|
|
||||||
def test_team_roster_type_injured_list(self):
|
def test_team_roster_type_injured_list(self):
|
||||||
@ -120,14 +143,32 @@ class TestTeamModel:
|
|||||||
from models.team import RosterType
|
from models.team import RosterType
|
||||||
|
|
||||||
# Standard Injured List: [Team] + "IL"
|
# Standard Injured List: [Team] + "IL"
|
||||||
team = Team(id=7, abbrev='NYYIL', sname='Yankees IL', lname='New York Yankees IL', season=12)
|
team = Team(
|
||||||
|
id=7,
|
||||||
|
abbrev="NYYIL",
|
||||||
|
sname="Yankees IL",
|
||||||
|
lname="New York Yankees IL",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.INJURED_LIST
|
assert team.roster_type() == RosterType.INJURED_LIST
|
||||||
|
|
||||||
team = Team(id=8, abbrev='PORIL', sname='Loggers IL', lname='Portland Loggers IL', season=12)
|
team = Team(
|
||||||
|
id=8,
|
||||||
|
abbrev="PORIL",
|
||||||
|
sname="Loggers IL",
|
||||||
|
lname="Portland Loggers IL",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.INJURED_LIST
|
assert team.roster_type() == RosterType.INJURED_LIST
|
||||||
|
|
||||||
# Case insensitive
|
# Case insensitive
|
||||||
team = Team(id=9, abbrev='LAAil', sname='Angels IL', lname='Los Angeles Angels IL', season=12)
|
team = Team(
|
||||||
|
id=9,
|
||||||
|
abbrev="LAAil",
|
||||||
|
sname="Angels IL",
|
||||||
|
lname="Los Angeles Angels IL",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.INJURED_LIST
|
assert team.roster_type() == RosterType.INJURED_LIST
|
||||||
|
|
||||||
def test_team_roster_type_edge_case_bhmil(self):
|
def test_team_roster_type_edge_case_bhmil(self):
|
||||||
@ -143,16 +184,30 @@ class TestTeamModel:
|
|||||||
from models.team import RosterType
|
from models.team import RosterType
|
||||||
|
|
||||||
# "BHMIL" = "BHM" + "IL" → sname contains "IL" → INJURED_LIST
|
# "BHMIL" = "BHM" + "IL" → sname contains "IL" → INJURED_LIST
|
||||||
team = Team(id=10, abbrev='BHMIL', sname='Iron IL', lname='Birmingham Iron IL', season=12)
|
team = Team(
|
||||||
|
id=10,
|
||||||
|
abbrev="BHMIL",
|
||||||
|
sname="Iron IL",
|
||||||
|
lname="Birmingham Iron IL",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.INJURED_LIST
|
assert team.roster_type() == RosterType.INJURED_LIST
|
||||||
|
|
||||||
# Compare with a real Minor League team that has "Island" in name
|
# Compare with a real Minor League team that has "Island" in name
|
||||||
# "NYYMIL" = "NYY" + "MIL", even though sname has "Island" → MINOR_LEAGUE
|
# "NYYMIL" = "NYY" + "MIL", even though sname has "Island" → MINOR_LEAGUE
|
||||||
team = Team(id=11, abbrev='NYYMIL', sname='Staten Island RailRiders', lname='Staten Island RailRiders', season=12)
|
team = Team(
|
||||||
|
id=11,
|
||||||
|
abbrev="NYYMIL",
|
||||||
|
sname="Staten Island RailRiders",
|
||||||
|
lname="Staten Island RailRiders",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
||||||
|
|
||||||
# Another IL edge case with sname containing "IL" at word boundary
|
# Another IL edge case with sname containing "IL" at word boundary
|
||||||
team = Team(id=12, abbrev='WVMIL', sname='WV IL', lname='West Virginia IL', season=12)
|
team = Team(
|
||||||
|
id=12, abbrev="WVMIL", sname="WV IL", lname="West Virginia IL", season=12
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.INJURED_LIST
|
assert team.roster_type() == RosterType.INJURED_LIST
|
||||||
|
|
||||||
def test_team_roster_type_sname_disambiguation(self):
|
def test_team_roster_type_sname_disambiguation(self):
|
||||||
@ -160,221 +215,231 @@ class TestTeamModel:
|
|||||||
from models.team import RosterType
|
from models.team import RosterType
|
||||||
|
|
||||||
# MiL team - sname does NOT have "IL" as a word
|
# MiL team - sname does NOT have "IL" as a word
|
||||||
team = Team(id=13, abbrev='WVMIL', sname='Miners', lname='West Virginia Miners', season=12)
|
team = Team(
|
||||||
|
id=13,
|
||||||
|
abbrev="WVMIL",
|
||||||
|
sname="Miners",
|
||||||
|
lname="West Virginia Miners",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
||||||
|
|
||||||
# IL team - sname has "IL" at word boundary
|
# IL team - sname has "IL" at word boundary
|
||||||
team = Team(id=14, abbrev='WVMIL', sname='Miners IL', lname='West Virginia Miners IL', season=12)
|
team = Team(
|
||||||
|
id=14,
|
||||||
|
abbrev="WVMIL",
|
||||||
|
sname="Miners IL",
|
||||||
|
lname="West Virginia Miners IL",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.INJURED_LIST
|
assert team.roster_type() == RosterType.INJURED_LIST
|
||||||
|
|
||||||
# MiL team - sname has "IL" but only in "Island" (substring, not word boundary)
|
# MiL team - sname has "IL" but only in "Island" (substring, not word boundary)
|
||||||
team = Team(id=15, abbrev='CHIMIL', sname='Island Hoppers', lname='Chicago Island Hoppers', season=12)
|
team = Team(
|
||||||
|
id=15,
|
||||||
|
abbrev="CHIMIL",
|
||||||
|
sname="Island Hoppers",
|
||||||
|
lname="Chicago Island Hoppers",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
||||||
|
|
||||||
|
|
||||||
class TestPlayerModel:
|
class TestPlayerModel:
|
||||||
"""Test Player model functionality."""
|
"""Test Player model functionality."""
|
||||||
|
|
||||||
def test_player_creation(self):
|
def test_player_creation(self):
|
||||||
"""Test player creation with required fields."""
|
"""Test player creation with required fields."""
|
||||||
player = Player(
|
player = Player(
|
||||||
id=101,
|
id=101,
|
||||||
name='Mike Trout',
|
name="Mike Trout",
|
||||||
wara=8.5,
|
wara=8.5,
|
||||||
season=12,
|
season=12,
|
||||||
team_id=1,
|
team_id=1,
|
||||||
image='trout.jpg',
|
image="trout.jpg",
|
||||||
pos_1='CF'
|
pos_1="CF",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert player.name == 'Mike Trout'
|
assert player.name == "Mike Trout"
|
||||||
assert player.wara == 8.5
|
assert player.wara == 8.5
|
||||||
assert player.team_id == 1
|
assert player.team_id == 1
|
||||||
assert player.pos_1 == 'CF'
|
assert player.pos_1 == "CF"
|
||||||
|
|
||||||
def test_player_positions_property(self):
|
def test_player_positions_property(self):
|
||||||
"""Test player positions property."""
|
"""Test player positions property."""
|
||||||
player = Player(
|
player = Player(
|
||||||
id=102,
|
id=102,
|
||||||
name='Shohei Ohtani',
|
name="Shohei Ohtani",
|
||||||
wara=9.0,
|
wara=9.0,
|
||||||
season=12,
|
season=12,
|
||||||
team_id=1,
|
team_id=1,
|
||||||
image='ohtani.jpg',
|
image="ohtani.jpg",
|
||||||
pos_1='SP',
|
pos_1="SP",
|
||||||
pos_2='DH',
|
pos_2="DH",
|
||||||
pos_3='RF'
|
pos_3="RF",
|
||||||
)
|
)
|
||||||
|
|
||||||
positions = player.positions
|
positions = player.positions
|
||||||
assert len(positions) == 3
|
assert len(positions) == 3
|
||||||
assert 'SP' in positions
|
assert "SP" in positions
|
||||||
assert 'DH' in positions
|
assert "DH" in positions
|
||||||
assert 'RF' in positions
|
assert "RF" in positions
|
||||||
|
|
||||||
def test_player_primary_position(self):
|
def test_player_primary_position(self):
|
||||||
"""Test primary position property."""
|
"""Test primary position property."""
|
||||||
player = Player(
|
player = Player(
|
||||||
id=103,
|
id=103,
|
||||||
name='Mookie Betts',
|
name="Mookie Betts",
|
||||||
wara=7.2,
|
wara=7.2,
|
||||||
season=12,
|
season=12,
|
||||||
team_id=1,
|
team_id=1,
|
||||||
image='betts.jpg',
|
image="betts.jpg",
|
||||||
pos_1='RF',
|
pos_1="RF",
|
||||||
pos_2='2B'
|
pos_2="2B",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert player.primary_position == 'RF'
|
assert player.primary_position == "RF"
|
||||||
|
|
||||||
def test_player_is_pitcher(self):
|
def test_player_is_pitcher(self):
|
||||||
"""Test is_pitcher property."""
|
"""Test is_pitcher property."""
|
||||||
pitcher = Player(
|
pitcher = Player(
|
||||||
id=104,
|
id=104,
|
||||||
name='Gerrit Cole',
|
name="Gerrit Cole",
|
||||||
wara=6.8,
|
wara=6.8,
|
||||||
season=12,
|
season=12,
|
||||||
team_id=1,
|
team_id=1,
|
||||||
image='cole.jpg',
|
image="cole.jpg",
|
||||||
pos_1='SP'
|
pos_1="SP",
|
||||||
)
|
)
|
||||||
|
|
||||||
position_player = Player(
|
position_player = Player(
|
||||||
id=105,
|
id=105,
|
||||||
name='Aaron Judge',
|
name="Aaron Judge",
|
||||||
wara=8.1,
|
wara=8.1,
|
||||||
season=12,
|
season=12,
|
||||||
team_id=1,
|
team_id=1,
|
||||||
image='judge.jpg',
|
image="judge.jpg",
|
||||||
pos_1='RF'
|
pos_1="RF",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert pitcher.is_pitcher is True
|
assert pitcher.is_pitcher is True
|
||||||
assert position_player.is_pitcher is False
|
assert position_player.is_pitcher is False
|
||||||
|
|
||||||
def test_player_str_representation(self):
|
def test_player_str_representation(self):
|
||||||
"""Test player string representation."""
|
"""Test player string representation."""
|
||||||
player = Player(
|
player = Player(
|
||||||
id=106,
|
id=106,
|
||||||
name='Ronald Acuna Jr.',
|
name="Ronald Acuna Jr.",
|
||||||
wara=8.8,
|
wara=8.8,
|
||||||
season=12,
|
season=12,
|
||||||
team_id=1,
|
team_id=1,
|
||||||
image='acuna.jpg',
|
image="acuna.jpg",
|
||||||
pos_1='OF'
|
pos_1="OF",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert str(player) == 'Ronald Acuna Jr. (OF)'
|
assert str(player) == "Ronald Acuna Jr. (OF)"
|
||||||
|
|
||||||
|
|
||||||
class TestCurrentModel:
|
class TestCurrentModel:
|
||||||
"""Test Current league state model."""
|
"""Test Current league state model."""
|
||||||
|
|
||||||
def test_current_default_values(self):
|
def test_current_default_values(self):
|
||||||
"""Test current model with default values."""
|
"""Test current model with default values."""
|
||||||
current = Current()
|
current = Current()
|
||||||
|
|
||||||
assert current.week == 69
|
assert current.week == 69
|
||||||
assert current.season == 69
|
assert current.season == 69
|
||||||
assert current.freeze is True
|
assert current.freeze is True
|
||||||
assert current.bet_week == 'sheets'
|
assert current.bet_week == "sheets"
|
||||||
|
|
||||||
def test_current_with_custom_values(self):
|
def test_current_with_custom_values(self):
|
||||||
"""Test current model with custom values."""
|
"""Test current model with custom values."""
|
||||||
current = Current(
|
current = Current(
|
||||||
week=15,
|
week=15, season=12, freeze=False, trade_deadline=14, playoffs_begin=19
|
||||||
season=12,
|
|
||||||
freeze=False,
|
|
||||||
trade_deadline=14,
|
|
||||||
playoffs_begin=19
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert current.week == 15
|
assert current.week == 15
|
||||||
assert current.season == 12
|
assert current.season == 12
|
||||||
assert current.freeze is False
|
assert current.freeze is False
|
||||||
|
|
||||||
def test_current_properties(self):
|
def test_current_properties(self):
|
||||||
"""Test current model properties."""
|
"""Test current model properties."""
|
||||||
# Regular season
|
# Regular season
|
||||||
current = Current(week=10, playoffs_begin=19)
|
current = Current(week=10, playoffs_begin=19)
|
||||||
assert current.is_offseason is False
|
assert current.is_offseason is False
|
||||||
assert current.is_playoffs is False
|
assert current.is_playoffs is False
|
||||||
|
|
||||||
# Playoffs
|
# Playoffs
|
||||||
current = Current(week=20, playoffs_begin=19)
|
current = Current(week=20, playoffs_begin=19)
|
||||||
assert current.is_offseason is True
|
assert current.is_offseason is True
|
||||||
assert current.is_playoffs is True
|
assert current.is_playoffs is True
|
||||||
|
|
||||||
# Pick trading
|
# Pick trading
|
||||||
current = Current(week=15, pick_trade_start=10, pick_trade_end=20)
|
current = Current(week=15, pick_trade_start=10, pick_trade_end=20)
|
||||||
assert current.can_trade_picks is True
|
assert current.can_trade_picks is True
|
||||||
|
|
||||||
|
def test_is_past_trade_deadline(self):
|
||||||
|
"""Test trade deadline property — trades allowed during deadline week, blocked after."""
|
||||||
|
# Before deadline
|
||||||
|
current = Current(week=10, trade_deadline=14)
|
||||||
|
assert current.is_past_trade_deadline is False
|
||||||
|
|
||||||
|
# At deadline week (still allowed)
|
||||||
|
current = Current(week=14, trade_deadline=14)
|
||||||
|
assert current.is_past_trade_deadline is False
|
||||||
|
|
||||||
|
# One week past deadline
|
||||||
|
current = Current(week=15, trade_deadline=14)
|
||||||
|
assert current.is_past_trade_deadline is True
|
||||||
|
|
||||||
|
# Offseason bypasses deadline (week > 18)
|
||||||
|
current = Current(week=20, trade_deadline=14)
|
||||||
|
assert current.is_offseason is True
|
||||||
|
assert current.is_past_trade_deadline is False
|
||||||
|
|
||||||
|
|
||||||
class TestDraftPickModel:
|
class TestDraftPickModel:
|
||||||
"""Test DraftPick model functionality."""
|
"""Test DraftPick model functionality."""
|
||||||
|
|
||||||
def test_draft_pick_creation(self):
|
def test_draft_pick_creation(self):
|
||||||
"""Test draft pick creation."""
|
"""Test draft pick creation."""
|
||||||
pick = DraftPick(
|
pick = DraftPick(season=12, overall=1, round=1, origowner_id=1, owner_id=1)
|
||||||
season=12,
|
|
||||||
overall=1,
|
|
||||||
round=1,
|
|
||||||
origowner_id=1,
|
|
||||||
owner_id=1
|
|
||||||
)
|
|
||||||
|
|
||||||
assert pick.season == 12
|
assert pick.season == 12
|
||||||
assert pick.overall == 1
|
assert pick.overall == 1
|
||||||
assert pick.origowner_id == 1
|
assert pick.origowner_id == 1
|
||||||
assert pick.owner_id == 1
|
assert pick.owner_id == 1
|
||||||
|
|
||||||
def test_draft_pick_properties(self):
|
def test_draft_pick_properties(self):
|
||||||
"""Test draft pick properties."""
|
"""Test draft pick properties."""
|
||||||
# Not traded, not selected
|
# Not traded, not selected
|
||||||
pick = DraftPick(
|
pick = DraftPick(season=12, overall=5, round=1, origowner_id=1, owner_id=1)
|
||||||
season=12,
|
|
||||||
overall=5,
|
|
||||||
round=1,
|
|
||||||
origowner_id=1,
|
|
||||||
owner_id=1
|
|
||||||
)
|
|
||||||
|
|
||||||
assert pick.is_traded is False
|
assert pick.is_traded is False
|
||||||
assert pick.is_selected is False
|
assert pick.is_selected is False
|
||||||
|
|
||||||
# Traded pick
|
# Traded pick
|
||||||
traded_pick = DraftPick(
|
traded_pick = DraftPick(
|
||||||
season=12,
|
season=12, overall=10, round=1, origowner_id=1, owner_id=2
|
||||||
overall=10,
|
|
||||||
round=1,
|
|
||||||
origowner_id=1,
|
|
||||||
owner_id=2
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert traded_pick.is_traded is True
|
assert traded_pick.is_traded is True
|
||||||
|
|
||||||
# Selected pick
|
# Selected pick
|
||||||
selected_pick = DraftPick(
|
selected_pick = DraftPick(
|
||||||
season=12,
|
season=12, overall=15, round=1, origowner_id=1, owner_id=1, player_id=100
|
||||||
overall=15,
|
|
||||||
round=1,
|
|
||||||
origowner_id=1,
|
|
||||||
owner_id=1,
|
|
||||||
player_id=100
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert selected_pick.is_selected is True
|
assert selected_pick.is_selected is True
|
||||||
|
|
||||||
|
|
||||||
class TestDraftDataModel:
|
class TestDraftDataModel:
|
||||||
"""Test DraftData model functionality."""
|
"""Test DraftData model functionality."""
|
||||||
|
|
||||||
def test_draft_data_creation(self):
|
def test_draft_data_creation(self):
|
||||||
"""Test draft data creation."""
|
"""Test draft data creation."""
|
||||||
draft_data = DraftData(
|
draft_data = DraftData(
|
||||||
result_channel=123456789,
|
result_channel=123456789, ping_channel=987654321, pick_minutes=10
|
||||||
ping_channel=987654321,
|
|
||||||
pick_minutes=10
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert draft_data.result_channel == 123456789
|
assert draft_data.result_channel == 123456789
|
||||||
@ -384,20 +449,12 @@ class TestDraftDataModel:
|
|||||||
def test_draft_data_properties(self):
|
def test_draft_data_properties(self):
|
||||||
"""Test draft data properties."""
|
"""Test draft data properties."""
|
||||||
# Inactive draft
|
# Inactive draft
|
||||||
draft_data = DraftData(
|
draft_data = DraftData(result_channel=123, ping_channel=456, timer=False)
|
||||||
result_channel=123,
|
|
||||||
ping_channel=456,
|
|
||||||
timer=False
|
|
||||||
)
|
|
||||||
|
|
||||||
assert draft_data.is_draft_active is False
|
assert draft_data.is_draft_active is False
|
||||||
|
|
||||||
# Active draft
|
# Active draft
|
||||||
active_draft = DraftData(
|
active_draft = DraftData(result_channel=123, ping_channel=456, timer=True)
|
||||||
result_channel=123,
|
|
||||||
ping_channel=456,
|
|
||||||
timer=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert active_draft.is_draft_active is True
|
assert active_draft.is_draft_active is True
|
||||||
|
|
||||||
@ -409,17 +466,13 @@ class TestDraftListModel:
|
|||||||
not just IDs. The API returns these objects populated.
|
not just IDs. The API returns these objects populated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _create_mock_team(self, team_id: int = 1) -> 'Team':
|
def _create_mock_team(self, team_id: int = 1) -> "Team":
|
||||||
"""Create a mock team for testing."""
|
"""Create a mock team for testing."""
|
||||||
return Team(
|
return Team(
|
||||||
id=team_id,
|
id=team_id, abbrev="TST", sname="Test", lname="Test Team", season=12
|
||||||
abbrev="TST",
|
|
||||||
sname="Test",
|
|
||||||
lname="Test Team",
|
|
||||||
season=12
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _create_mock_player(self, player_id: int = 100) -> 'Player':
|
def _create_mock_player(self, player_id: int = 100) -> "Player":
|
||||||
"""Create a mock player for testing."""
|
"""Create a mock player for testing."""
|
||||||
return Player(
|
return Player(
|
||||||
id=player_id,
|
id=player_id,
|
||||||
@ -430,7 +483,7 @@ class TestDraftListModel:
|
|||||||
team_id=1,
|
team_id=1,
|
||||||
season=12,
|
season=12,
|
||||||
wara=2.5,
|
wara=2.5,
|
||||||
image="https://example.com/test.jpg"
|
image="https://example.com/test.jpg",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_draft_list_creation(self):
|
def test_draft_list_creation(self):
|
||||||
@ -438,12 +491,7 @@ class TestDraftListModel:
|
|||||||
mock_team = self._create_mock_team(team_id=1)
|
mock_team = self._create_mock_team(team_id=1)
|
||||||
mock_player = self._create_mock_player(player_id=100)
|
mock_player = self._create_mock_player(player_id=100)
|
||||||
|
|
||||||
draft_entry = DraftList(
|
draft_entry = DraftList(season=12, team=mock_team, rank=1, player=mock_player)
|
||||||
season=12,
|
|
||||||
team=mock_team,
|
|
||||||
rank=1,
|
|
||||||
player=mock_player
|
|
||||||
)
|
|
||||||
|
|
||||||
assert draft_entry.season == 12
|
assert draft_entry.season == 12
|
||||||
assert draft_entry.team_id == 1
|
assert draft_entry.team_id == 1
|
||||||
@ -456,18 +504,10 @@ class TestDraftListModel:
|
|||||||
mock_player_top = self._create_mock_player(player_id=100)
|
mock_player_top = self._create_mock_player(player_id=100)
|
||||||
mock_player_lower = self._create_mock_player(player_id=200)
|
mock_player_lower = self._create_mock_player(player_id=200)
|
||||||
|
|
||||||
top_pick = DraftList(
|
top_pick = DraftList(season=12, team=mock_team, rank=1, player=mock_player_top)
|
||||||
season=12,
|
|
||||||
team=mock_team,
|
|
||||||
rank=1,
|
|
||||||
player=mock_player_top
|
|
||||||
)
|
|
||||||
|
|
||||||
lower_pick = DraftList(
|
lower_pick = DraftList(
|
||||||
season=12,
|
season=12, team=mock_team, rank=5, player=mock_player_lower
|
||||||
team=mock_team,
|
|
||||||
rank=5,
|
|
||||||
player=mock_player_lower
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert top_pick.is_top_ranked is True
|
assert top_pick.is_top_ranked is True
|
||||||
@ -486,32 +526,32 @@ class TestDraftListModel:
|
|||||||
"""
|
"""
|
||||||
# Simulate API response format - nested objects, NOT flat IDs
|
# Simulate API response format - nested objects, NOT flat IDs
|
||||||
api_response = {
|
api_response = {
|
||||||
'id': 303,
|
"id": 303,
|
||||||
'season': 13,
|
"season": 13,
|
||||||
'rank': 1,
|
"rank": 1,
|
||||||
'team': {
|
"team": {
|
||||||
'id': 548,
|
"id": 548,
|
||||||
'abbrev': 'WV',
|
"abbrev": "WV",
|
||||||
'sname': 'Black Bears',
|
"sname": "Black Bears",
|
||||||
'lname': 'West Virginia Black Bears',
|
"lname": "West Virginia Black Bears",
|
||||||
'season': 13
|
"season": 13,
|
||||||
},
|
},
|
||||||
'player': {
|
"player": {
|
||||||
'id': 12843,
|
"id": 12843,
|
||||||
'name': 'George Springer',
|
"name": "George Springer",
|
||||||
'wara': 0.31,
|
"wara": 0.31,
|
||||||
'image': 'https://example.com/springer.png',
|
"image": "https://example.com/springer.png",
|
||||||
'season': 13,
|
"season": 13,
|
||||||
'pos_1': 'CF',
|
"pos_1": "CF",
|
||||||
# Note: NO flat team_id here - it's nested in 'team' below
|
# Note: NO flat team_id here - it's nested in 'team' below
|
||||||
'team': {
|
"team": {
|
||||||
'id': 547, # Free Agent team
|
"id": 547, # Free Agent team
|
||||||
'abbrev': 'FA',
|
"abbrev": "FA",
|
||||||
'sname': 'Free Agents',
|
"sname": "Free Agents",
|
||||||
'lname': 'Free Agents',
|
"lname": "Free Agents",
|
||||||
'season': 13
|
"season": 13,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create DraftList using from_api_data (what BaseService calls)
|
# Create DraftList using from_api_data (what BaseService calls)
|
||||||
@ -522,87 +562,94 @@ class TestDraftListModel:
|
|||||||
assert draft_entry.player is not None
|
assert draft_entry.player is not None
|
||||||
|
|
||||||
# CRITICAL: player.team_id must be extracted from nested team object
|
# CRITICAL: player.team_id must be extracted from nested team object
|
||||||
assert draft_entry.player.team_id == 547, \
|
assert draft_entry.player.team_id == 547, (
|
||||||
f"player.team_id should be 547 (FA), got {draft_entry.player.team_id}"
|
f"player.team_id should be 547 (FA), got {draft_entry.player.team_id}"
|
||||||
|
)
|
||||||
|
|
||||||
# Verify the nested team object is also populated
|
# Verify the nested team object is also populated
|
||||||
assert draft_entry.player.team is not None
|
assert draft_entry.player.team is not None
|
||||||
assert draft_entry.player.team.id == 547
|
assert draft_entry.player.team.id == 547
|
||||||
assert draft_entry.player.team.abbrev == 'FA'
|
assert draft_entry.player.team.abbrev == "FA"
|
||||||
|
|
||||||
# Verify DraftList's own team data
|
# Verify DraftList's own team data
|
||||||
assert draft_entry.team.id == 548
|
assert draft_entry.team.id == 548
|
||||||
assert draft_entry.team.abbrev == 'WV'
|
assert draft_entry.team.abbrev == "WV"
|
||||||
assert draft_entry.team_id == 548 # Property from nested team
|
assert draft_entry.team_id == 548 # Property from nested team
|
||||||
|
|
||||||
|
|
||||||
class TestModelCoverageExtras:
|
class TestModelCoverageExtras:
|
||||||
"""Additional model coverage tests."""
|
"""Additional model coverage tests."""
|
||||||
|
|
||||||
def test_base_model_from_api_data_validation(self):
|
def test_base_model_from_api_data_validation(self):
|
||||||
"""Test from_api_data with various edge cases."""
|
"""Test from_api_data with various edge cases."""
|
||||||
from models.base import SBABaseModel
|
from models.base import SBABaseModel
|
||||||
|
|
||||||
# Test with empty data raises ValueError
|
# Test with empty data raises ValueError
|
||||||
with pytest.raises(ValueError, match="Cannot create SBABaseModel from empty data"):
|
with pytest.raises(
|
||||||
|
ValueError, match="Cannot create SBABaseModel from empty data"
|
||||||
|
):
|
||||||
SBABaseModel.from_api_data({})
|
SBABaseModel.from_api_data({})
|
||||||
|
|
||||||
# Test with None raises ValueError
|
# Test with None raises ValueError
|
||||||
with pytest.raises(ValueError, match="Cannot create SBABaseModel from empty data"):
|
with pytest.raises(
|
||||||
|
ValueError, match="Cannot create SBABaseModel from empty data"
|
||||||
|
):
|
||||||
SBABaseModel.from_api_data(None)
|
SBABaseModel.from_api_data(None)
|
||||||
|
|
||||||
def test_player_positions_comprehensive(self):
|
def test_player_positions_comprehensive(self):
|
||||||
"""Test player positions property with all position variations."""
|
"""Test player positions property with all position variations."""
|
||||||
player_data = {
|
player_data = {
|
||||||
'id': 201,
|
"id": 201,
|
||||||
'name': 'Multi-Position Player',
|
"name": "Multi-Position Player",
|
||||||
'wara': 3.0,
|
"wara": 3.0,
|
||||||
'season': 12,
|
"season": 12,
|
||||||
'team_id': 5,
|
"team_id": 5,
|
||||||
'image': 'https://example.com/player.jpg',
|
"image": "https://example.com/player.jpg",
|
||||||
'pos_1': 'C',
|
"pos_1": "C",
|
||||||
'pos_2': '1B',
|
"pos_2": "1B",
|
||||||
'pos_3': '3B',
|
"pos_3": "3B",
|
||||||
'pos_4': None, # Test None handling
|
"pos_4": None, # Test None handling
|
||||||
'pos_5': 'DH',
|
"pos_5": "DH",
|
||||||
'pos_6': 'OF',
|
"pos_6": "OF",
|
||||||
'pos_7': None, # Another None
|
"pos_7": None, # Another None
|
||||||
'pos_8': 'SS'
|
"pos_8": "SS",
|
||||||
}
|
}
|
||||||
player = Player.from_api_data(player_data)
|
player = Player.from_api_data(player_data)
|
||||||
|
|
||||||
positions = player.positions
|
positions = player.positions
|
||||||
assert 'C' in positions
|
assert "C" in positions
|
||||||
assert '1B' in positions
|
assert "1B" in positions
|
||||||
assert '3B' in positions
|
assert "3B" in positions
|
||||||
assert 'DH' in positions
|
assert "DH" in positions
|
||||||
assert 'OF' in positions
|
assert "OF" in positions
|
||||||
assert 'SS' in positions
|
assert "SS" in positions
|
||||||
assert len(positions) == 6 # Should exclude None values
|
assert len(positions) == 6 # Should exclude None values
|
||||||
assert None not in positions
|
assert None not in positions
|
||||||
|
|
||||||
def test_player_is_pitcher_variations(self):
|
def test_player_is_pitcher_variations(self):
|
||||||
"""Test is_pitcher property with different positions."""
|
"""Test is_pitcher property with different positions."""
|
||||||
test_cases = [
|
test_cases = [
|
||||||
('SP', True), # Starting pitcher
|
("SP", True), # Starting pitcher
|
||||||
('RP', True), # Relief pitcher
|
("RP", True), # Relief pitcher
|
||||||
('P', True), # Generic pitcher
|
("P", True), # Generic pitcher
|
||||||
('C', False), # Catcher
|
("C", False), # Catcher
|
||||||
('1B', False), # First base
|
("1B", False), # First base
|
||||||
('OF', False), # Outfield
|
("OF", False), # Outfield
|
||||||
('DH', False), # Designated hitter
|
("DH", False), # Designated hitter
|
||||||
]
|
]
|
||||||
|
|
||||||
for position, expected in test_cases:
|
for position, expected in test_cases:
|
||||||
player_data = {
|
player_data = {
|
||||||
'id': 300 + ord(position[0]), # Generate unique IDs based on position
|
"id": 300 + ord(position[0]), # Generate unique IDs based on position
|
||||||
'name': f'Test {position}',
|
"name": f"Test {position}",
|
||||||
'wara': 2.0,
|
"wara": 2.0,
|
||||||
'season': 12,
|
"season": 12,
|
||||||
'team_id': 5,
|
"team_id": 5,
|
||||||
'image': 'https://example.com/player.jpg',
|
"image": "https://example.com/player.jpg",
|
||||||
'pos_1': position,
|
"pos_1": position,
|
||||||
}
|
}
|
||||||
player = Player.from_api_data(player_data)
|
player = Player.from_api_data(player_data)
|
||||||
assert player.is_pitcher == expected, f"Position {position} should return {expected}"
|
assert player.is_pitcher == expected, (
|
||||||
assert player.primary_position == position
|
f"Position {position} should return {expected}"
|
||||||
|
)
|
||||||
|
assert player.primary_position == position
|
||||||
|
|||||||
@ -3,6 +3,7 @@ Tests for Help Commands Service in Discord Bot v2.0
|
|||||||
|
|
||||||
Comprehensive tests for help commands CRUD operations and business logic.
|
Comprehensive tests for help commands CRUD operations and business logic.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
@ -10,13 +11,13 @@ from unittest.mock import AsyncMock
|
|||||||
from services.help_commands_service import (
|
from services.help_commands_service import (
|
||||||
HelpCommandsService,
|
HelpCommandsService,
|
||||||
HelpCommandNotFoundError,
|
HelpCommandNotFoundError,
|
||||||
HelpCommandExistsError
|
HelpCommandExistsError,
|
||||||
)
|
)
|
||||||
from models.help_command import (
|
from models.help_command import (
|
||||||
HelpCommand,
|
HelpCommand,
|
||||||
HelpCommandSearchFilters,
|
HelpCommandSearchFilters,
|
||||||
HelpCommandSearchResult,
|
HelpCommandSearchResult,
|
||||||
HelpCommandStats
|
HelpCommandStats,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -26,17 +27,17 @@ def sample_help_command() -> HelpCommand:
|
|||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
return HelpCommand(
|
return HelpCommand(
|
||||||
id=1,
|
id=1,
|
||||||
name='trading-rules',
|
name="trading-rules",
|
||||||
title='Trading Rules & Guidelines',
|
title="Trading Rules & Guidelines",
|
||||||
content='Complete trading rules for the league...',
|
content="Complete trading rules for the league...",
|
||||||
category='rules',
|
category="rules",
|
||||||
created_by_discord_id='123456789',
|
created_by_discord_id="123456789",
|
||||||
created_at=now,
|
created_at=now,
|
||||||
updated_at=None,
|
updated_at=None,
|
||||||
last_modified_by=None,
|
last_modified_by=None,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
view_count=100,
|
view_count=100,
|
||||||
display_order=10
|
display_order=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -64,6 +65,7 @@ class TestHelpCommandsServiceInit:
|
|||||||
|
|
||||||
# Multiple imports should return the same instance
|
# Multiple imports should return the same instance
|
||||||
from services.help_commands_service import help_commands_service as service2
|
from services.help_commands_service import help_commands_service as service2
|
||||||
|
|
||||||
assert help_commands_service is service2
|
assert help_commands_service is service2
|
||||||
|
|
||||||
def test_service_has_required_methods(self):
|
def test_service_has_required_methods(self):
|
||||||
@ -71,22 +73,22 @@ class TestHelpCommandsServiceInit:
|
|||||||
from services.help_commands_service import help_commands_service
|
from services.help_commands_service import help_commands_service
|
||||||
|
|
||||||
# Core CRUD operations
|
# Core CRUD operations
|
||||||
assert hasattr(help_commands_service, 'create_help')
|
assert hasattr(help_commands_service, "create_help")
|
||||||
assert hasattr(help_commands_service, 'get_help_by_name')
|
assert hasattr(help_commands_service, "get_help_by_name")
|
||||||
assert hasattr(help_commands_service, 'update_help')
|
assert hasattr(help_commands_service, "update_help")
|
||||||
assert hasattr(help_commands_service, 'delete_help')
|
assert hasattr(help_commands_service, "delete_help")
|
||||||
assert hasattr(help_commands_service, 'restore_help')
|
assert hasattr(help_commands_service, "restore_help")
|
||||||
|
|
||||||
# Search and listing
|
# Search and listing
|
||||||
assert hasattr(help_commands_service, 'search_help_commands')
|
assert hasattr(help_commands_service, "search_help_commands")
|
||||||
assert hasattr(help_commands_service, 'get_all_help_topics')
|
assert hasattr(help_commands_service, "get_all_help_topics")
|
||||||
assert hasattr(help_commands_service, 'get_help_names_for_autocomplete')
|
assert hasattr(help_commands_service, "get_help_names_for_autocomplete")
|
||||||
|
|
||||||
# View tracking
|
# View tracking
|
||||||
assert hasattr(help_commands_service, 'increment_view_count')
|
assert hasattr(help_commands_service, "increment_view_count")
|
||||||
|
|
||||||
# Statistics
|
# Statistics
|
||||||
assert hasattr(help_commands_service, 'get_statistics')
|
assert hasattr(help_commands_service, "get_statistics")
|
||||||
|
|
||||||
|
|
||||||
class TestHelpCommandsServiceCRUD:
|
class TestHelpCommandsServiceCRUD:
|
||||||
@ -118,7 +120,7 @@ class TestHelpCommandsServiceCRUD:
|
|||||||
last_modified_by=None,
|
last_modified_by=None,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
view_count=0,
|
view_count=0,
|
||||||
display_order=data.get("display_order", 0)
|
display_order=data.get("display_order", 0),
|
||||||
)
|
)
|
||||||
return created_help
|
return created_help
|
||||||
|
|
||||||
@ -130,8 +132,8 @@ class TestHelpCommandsServiceCRUD:
|
|||||||
name="test-topic",
|
name="test-topic",
|
||||||
title="Test Topic",
|
title="Test Topic",
|
||||||
content="This is test content for the help topic.",
|
content="This is test content for the help topic.",
|
||||||
creator_discord_id='123456789',
|
creator_discord_id="123456789",
|
||||||
category="info"
|
category="info",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert isinstance(result, HelpCommand)
|
assert isinstance(result, HelpCommand)
|
||||||
@ -141,39 +143,48 @@ class TestHelpCommandsServiceCRUD:
|
|||||||
assert result.view_count == 0
|
assert result.view_count == 0
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_help_already_exists(self, help_commands_service_instance, sample_help_command):
|
async def test_create_help_already_exists(
|
||||||
|
self, help_commands_service_instance, sample_help_command
|
||||||
|
):
|
||||||
"""Test help command creation when topic already exists."""
|
"""Test help command creation when topic already exists."""
|
||||||
|
|
||||||
# Mock topic already exists
|
# Mock topic already exists
|
||||||
async def mock_get_help_by_name(*args, **kwargs):
|
async def mock_get_help_by_name(*args, **kwargs):
|
||||||
return sample_help_command
|
return sample_help_command
|
||||||
|
|
||||||
help_commands_service_instance.get_help_by_name = mock_get_help_by_name
|
help_commands_service_instance.get_help_by_name = mock_get_help_by_name
|
||||||
|
|
||||||
with pytest.raises(HelpCommandExistsError, match="Help topic 'trading-rules' already exists"):
|
with pytest.raises(
|
||||||
|
HelpCommandExistsError, match="Help topic 'trading-rules' already exists"
|
||||||
|
):
|
||||||
await help_commands_service_instance.create_help(
|
await help_commands_service_instance.create_help(
|
||||||
name="trading-rules",
|
name="trading-rules",
|
||||||
title="Trading Rules",
|
title="Trading Rules",
|
||||||
content="Rules content",
|
content="Rules content",
|
||||||
creator_discord_id='123456789'
|
creator_discord_id="123456789",
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_help_by_name_success(self, help_commands_service_instance, sample_help_command):
|
async def test_get_help_by_name_success(
|
||||||
|
self, help_commands_service_instance, sample_help_command
|
||||||
|
):
|
||||||
"""Test successful help command retrieval."""
|
"""Test successful help command retrieval."""
|
||||||
# Mock the API client to return proper data structure
|
# Mock the API client to return proper data structure
|
||||||
help_data = {
|
help_data = {
|
||||||
'id': sample_help_command.id,
|
"id": sample_help_command.id,
|
||||||
'name': sample_help_command.name,
|
"name": sample_help_command.name,
|
||||||
'title': sample_help_command.title,
|
"title": sample_help_command.title,
|
||||||
'content': sample_help_command.content,
|
"content": sample_help_command.content,
|
||||||
'category': sample_help_command.category,
|
"category": sample_help_command.category,
|
||||||
'created_by_discord_id': sample_help_command.created_by_discord_id,
|
"created_by_discord_id": sample_help_command.created_by_discord_id,
|
||||||
'created_at': sample_help_command.created_at.isoformat(),
|
"created_at": sample_help_command.created_at.isoformat(),
|
||||||
'updated_at': sample_help_command.updated_at.isoformat() if sample_help_command.updated_at else None,
|
"updated_at": sample_help_command.updated_at.isoformat()
|
||||||
'last_modified_by': sample_help_command.last_modified_by,
|
if sample_help_command.updated_at
|
||||||
'is_active': sample_help_command.is_active,
|
else None,
|
||||||
'view_count': sample_help_command.view_count,
|
"last_modified_by": sample_help_command.last_modified_by,
|
||||||
'display_order': sample_help_command.display_order
|
"is_active": sample_help_command.is_active,
|
||||||
|
"view_count": sample_help_command.view_count,
|
||||||
|
"display_order": sample_help_command.display_order,
|
||||||
}
|
}
|
||||||
|
|
||||||
help_commands_service_instance._client.get.return_value = help_data
|
help_commands_service_instance._client.get.return_value = help_data
|
||||||
@ -191,66 +202,61 @@ class TestHelpCommandsServiceCRUD:
|
|||||||
# Mock the API client to return None (not found)
|
# Mock the API client to return None (not found)
|
||||||
help_commands_service_instance._client.get.return_value = None
|
help_commands_service_instance._client.get.return_value = None
|
||||||
|
|
||||||
with pytest.raises(HelpCommandNotFoundError, match="Help topic 'nonexistent' not found"):
|
with pytest.raises(
|
||||||
|
HelpCommandNotFoundError, match="Help topic 'nonexistent' not found"
|
||||||
|
):
|
||||||
await help_commands_service_instance.get_help_by_name("nonexistent")
|
await help_commands_service_instance.get_help_by_name("nonexistent")
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_help_success(self, help_commands_service_instance, sample_help_command):
|
async def test_update_help_success(
|
||||||
|
self, help_commands_service_instance, sample_help_command
|
||||||
|
):
|
||||||
"""Test successful help command update."""
|
"""Test successful help command update."""
|
||||||
|
|
||||||
# Mock getting the existing help command
|
# Mock getting the existing help command
|
||||||
async def mock_get_help_by_name(name, include_inactive=False):
|
async def mock_get_help_by_name(name, include_inactive=False):
|
||||||
if name == "trading-rules":
|
if name == "trading-rules":
|
||||||
return sample_help_command
|
return sample_help_command
|
||||||
raise HelpCommandNotFoundError(f"Help topic '{name}' not found")
|
raise HelpCommandNotFoundError(f"Help topic '{name}' not found")
|
||||||
|
|
||||||
# Mock the API update call
|
# Mock the API update call returning the updated help command data directly
|
||||||
|
updated_data = {
|
||||||
|
"id": sample_help_command.id,
|
||||||
|
"name": sample_help_command.name,
|
||||||
|
"title": "Updated Trading Rules",
|
||||||
|
"content": "Updated content",
|
||||||
|
"category": sample_help_command.category,
|
||||||
|
"created_by_discord_id": sample_help_command.created_by_discord_id,
|
||||||
|
"created_at": sample_help_command.created_at.isoformat(),
|
||||||
|
"updated_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"last_modified_by": "987654321",
|
||||||
|
"is_active": sample_help_command.is_active,
|
||||||
|
"view_count": sample_help_command.view_count,
|
||||||
|
"display_order": sample_help_command.display_order,
|
||||||
|
}
|
||||||
|
|
||||||
async def mock_put(*args, **kwargs):
|
async def mock_put(*args, **kwargs):
|
||||||
return True
|
return updated_data
|
||||||
|
|
||||||
help_commands_service_instance.get_help_by_name = mock_get_help_by_name
|
help_commands_service_instance.get_help_by_name = mock_get_help_by_name
|
||||||
help_commands_service_instance._client.put = mock_put
|
help_commands_service_instance._client.put = mock_put
|
||||||
|
|
||||||
# Update should call get_help_by_name again at the end, so mock it to return updated version
|
|
||||||
updated_help = HelpCommand(
|
|
||||||
id=sample_help_command.id,
|
|
||||||
name=sample_help_command.name,
|
|
||||||
title="Updated Trading Rules",
|
|
||||||
content="Updated content",
|
|
||||||
category=sample_help_command.category,
|
|
||||||
created_by_discord_id=sample_help_command.created_by_discord_id,
|
|
||||||
created_at=sample_help_command.created_at,
|
|
||||||
updated_at=datetime.now(timezone.utc),
|
|
||||||
last_modified_by='987654321',
|
|
||||||
is_active=sample_help_command.is_active,
|
|
||||||
view_count=sample_help_command.view_count,
|
|
||||||
display_order=sample_help_command.display_order
|
|
||||||
)
|
|
||||||
|
|
||||||
call_count = 0
|
|
||||||
|
|
||||||
async def mock_get_with_counter(name, include_inactive=False):
|
|
||||||
nonlocal call_count
|
|
||||||
call_count += 1
|
|
||||||
if call_count == 1:
|
|
||||||
return sample_help_command
|
|
||||||
else:
|
|
||||||
return updated_help
|
|
||||||
|
|
||||||
help_commands_service_instance.get_help_by_name = mock_get_with_counter
|
|
||||||
|
|
||||||
result = await help_commands_service_instance.update_help(
|
result = await help_commands_service_instance.update_help(
|
||||||
name="trading-rules",
|
name="trading-rules",
|
||||||
new_title="Updated Trading Rules",
|
new_title="Updated Trading Rules",
|
||||||
new_content="Updated content",
|
new_content="Updated content",
|
||||||
updater_discord_id='987654321'
|
updater_discord_id="987654321",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert isinstance(result, HelpCommand)
|
assert isinstance(result, HelpCommand)
|
||||||
assert result.title == "Updated Trading Rules"
|
assert result.title == "Updated Trading Rules"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_delete_help_success(self, help_commands_service_instance, sample_help_command):
|
async def test_delete_help_success(
|
||||||
|
self, help_commands_service_instance, sample_help_command
|
||||||
|
):
|
||||||
"""Test successful help command deletion (soft delete)."""
|
"""Test successful help command deletion (soft delete)."""
|
||||||
|
|
||||||
# Mock getting the help command
|
# Mock getting the help command
|
||||||
async def mock_get_help_by_name(name, include_inactive=False):
|
async def mock_get_help_by_name(name, include_inactive=False):
|
||||||
return sample_help_command
|
return sample_help_command
|
||||||
@ -272,12 +278,12 @@ class TestHelpCommandsServiceCRUD:
|
|||||||
# Mock getting a deleted help command
|
# Mock getting a deleted help command
|
||||||
deleted_help = HelpCommand(
|
deleted_help = HelpCommand(
|
||||||
id=1,
|
id=1,
|
||||||
name='deleted-topic',
|
name="deleted-topic",
|
||||||
title='Deleted Topic',
|
title="Deleted Topic",
|
||||||
content='Content',
|
content="Content",
|
||||||
created_by_discord_id='123456789',
|
created_by_discord_id="123456789",
|
||||||
created_at=datetime.now(timezone.utc),
|
created_at=datetime.now(timezone.utc),
|
||||||
is_active=False
|
is_active=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def mock_get_help_by_name(name, include_inactive=False):
|
async def mock_get_help_by_name(name, include_inactive=False):
|
||||||
@ -285,15 +291,15 @@ class TestHelpCommandsServiceCRUD:
|
|||||||
|
|
||||||
# Mock the API restore call
|
# Mock the API restore call
|
||||||
restored_data = {
|
restored_data = {
|
||||||
'id': deleted_help.id,
|
"id": deleted_help.id,
|
||||||
'name': deleted_help.name,
|
"name": deleted_help.name,
|
||||||
'title': deleted_help.title,
|
"title": deleted_help.title,
|
||||||
'content': deleted_help.content,
|
"content": deleted_help.content,
|
||||||
'created_by_discord_id': deleted_help.created_by_discord_id,
|
"created_by_discord_id": deleted_help.created_by_discord_id,
|
||||||
'created_at': deleted_help.created_at.isoformat(),
|
"created_at": deleted_help.created_at.isoformat(),
|
||||||
'is_active': True,
|
"is_active": True,
|
||||||
'view_count': 0,
|
"view_count": 0,
|
||||||
'display_order': 0
|
"display_order": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
help_commands_service_instance.get_help_by_name = mock_get_help_by_name
|
help_commands_service_instance.get_help_by_name = mock_get_help_by_name
|
||||||
@ -312,33 +318,30 @@ class TestHelpCommandsServiceSearch:
|
|||||||
async def test_search_help_commands(self, help_commands_service_instance):
|
async def test_search_help_commands(self, help_commands_service_instance):
|
||||||
"""Test searching for help commands with filters."""
|
"""Test searching for help commands with filters."""
|
||||||
filters = HelpCommandSearchFilters(
|
filters = HelpCommandSearchFilters(
|
||||||
name_contains='trading',
|
name_contains="trading", category="rules", page=1, page_size=10
|
||||||
category='rules',
|
|
||||||
page=1,
|
|
||||||
page_size=10
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mock API response
|
# Mock API response
|
||||||
api_response = {
|
api_response = {
|
||||||
'help_commands': [
|
"help_commands": [
|
||||||
{
|
{
|
||||||
'id': 1,
|
"id": 1,
|
||||||
'name': 'trading-rules',
|
"name": "trading-rules",
|
||||||
'title': 'Trading Rules',
|
"title": "Trading Rules",
|
||||||
'content': 'Content',
|
"content": "Content",
|
||||||
'category': 'rules',
|
"category": "rules",
|
||||||
'created_by_discord_id': '123',
|
"created_by_discord_id": "123",
|
||||||
'created_at': datetime.now(timezone.utc).isoformat(),
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
'is_active': True,
|
"is_active": True,
|
||||||
'view_count': 100,
|
"view_count": 100,
|
||||||
'display_order': 0
|
"display_order": 0,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
'total_count': 1,
|
"total_count": 1,
|
||||||
'page': 1,
|
"page": 1,
|
||||||
'page_size': 10,
|
"page_size": 10,
|
||||||
'total_pages': 1,
|
"total_pages": 1,
|
||||||
'has_more': False
|
"has_more": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
help_commands_service_instance._client.get.return_value = api_response
|
help_commands_service_instance._client.get.return_value = api_response
|
||||||
@ -348,33 +351,33 @@ class TestHelpCommandsServiceSearch:
|
|||||||
assert isinstance(result, HelpCommandSearchResult)
|
assert isinstance(result, HelpCommandSearchResult)
|
||||||
assert len(result.help_commands) == 1
|
assert len(result.help_commands) == 1
|
||||||
assert result.total_count == 1
|
assert result.total_count == 1
|
||||||
assert result.help_commands[0].name == 'trading-rules'
|
assert result.help_commands[0].name == "trading-rules"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_all_help_topics(self, help_commands_service_instance):
|
async def test_get_all_help_topics(self, help_commands_service_instance):
|
||||||
"""Test getting all help topics."""
|
"""Test getting all help topics."""
|
||||||
# Mock API response
|
# Mock API response
|
||||||
api_response = {
|
api_response = {
|
||||||
'help_commands': [
|
"help_commands": [
|
||||||
{
|
{
|
||||||
'id': i,
|
"id": i,
|
||||||
'name': f'topic-{i}',
|
"name": f"topic-{i}",
|
||||||
'title': f'Topic {i}',
|
"title": f"Topic {i}",
|
||||||
'content': f'Content {i}',
|
"content": f"Content {i}",
|
||||||
'category': 'rules' if i % 2 == 0 else 'guides',
|
"category": "rules" if i % 2 == 0 else "guides",
|
||||||
'created_by_discord_id': '123',
|
"created_by_discord_id": "123",
|
||||||
'created_at': datetime.now(timezone.utc).isoformat(),
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
'is_active': True,
|
"is_active": True,
|
||||||
'view_count': i * 10,
|
"view_count": i * 10,
|
||||||
'display_order': i
|
"display_order": i,
|
||||||
}
|
}
|
||||||
for i in range(1, 6)
|
for i in range(1, 6)
|
||||||
],
|
],
|
||||||
'total_count': 5,
|
"total_count": 5,
|
||||||
'page': 1,
|
"page": 1,
|
||||||
'page_size': 100,
|
"page_size": 100,
|
||||||
'total_pages': 1,
|
"total_pages": 1,
|
||||||
'has_more': False
|
"has_more": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
help_commands_service_instance._client.get.return_value = api_response
|
help_commands_service_instance._client.get.return_value = api_response
|
||||||
@ -386,42 +389,45 @@ class TestHelpCommandsServiceSearch:
|
|||||||
assert all(isinstance(cmd, HelpCommand) for cmd in result)
|
assert all(isinstance(cmd, HelpCommand) for cmd in result)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_help_names_for_autocomplete(self, help_commands_service_instance):
|
async def test_get_help_names_for_autocomplete(
|
||||||
|
self, help_commands_service_instance
|
||||||
|
):
|
||||||
"""Test getting help names for autocomplete."""
|
"""Test getting help names for autocomplete."""
|
||||||
# Mock API response
|
# Mock API response
|
||||||
api_response = {
|
api_response = {
|
||||||
'results': [
|
"results": [
|
||||||
{
|
{
|
||||||
'name': 'trading-rules',
|
"name": "trading-rules",
|
||||||
'title': 'Trading Rules',
|
"title": "Trading Rules",
|
||||||
'category': 'rules'
|
"category": "rules",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'trading-deadline',
|
"name": "trading-deadline",
|
||||||
'title': 'Trading Deadline',
|
"title": "Trading Deadline",
|
||||||
'category': 'info'
|
"category": "info",
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
help_commands_service_instance._client.get.return_value = api_response
|
help_commands_service_instance._client.get.return_value = api_response
|
||||||
|
|
||||||
result = await help_commands_service_instance.get_help_names_for_autocomplete(
|
result = await help_commands_service_instance.get_help_names_for_autocomplete(
|
||||||
partial_name='trading',
|
partial_name="trading", limit=25
|
||||||
limit=25
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert isinstance(result, list)
|
assert isinstance(result, list)
|
||||||
assert len(result) == 2
|
assert len(result) == 2
|
||||||
assert 'trading-rules' in result
|
assert "trading-rules" in result
|
||||||
assert 'trading-deadline' in result
|
assert "trading-deadline" in result
|
||||||
|
|
||||||
|
|
||||||
class TestHelpCommandsServiceViewTracking:
|
class TestHelpCommandsServiceViewTracking:
|
||||||
"""Test view count tracking."""
|
"""Test view count tracking."""
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_increment_view_count(self, help_commands_service_instance, sample_help_command):
|
async def test_increment_view_count(
|
||||||
|
self, help_commands_service_instance, sample_help_command
|
||||||
|
):
|
||||||
"""Test incrementing view count."""
|
"""Test incrementing view count."""
|
||||||
# Mock the API patch call
|
# Mock the API patch call
|
||||||
help_commands_service_instance._client.patch = AsyncMock()
|
help_commands_service_instance._client.patch = AsyncMock()
|
||||||
@ -437,7 +443,7 @@ class TestHelpCommandsServiceViewTracking:
|
|||||||
created_at=sample_help_command.created_at,
|
created_at=sample_help_command.created_at,
|
||||||
is_active=sample_help_command.is_active,
|
is_active=sample_help_command.is_active,
|
||||||
view_count=sample_help_command.view_count + 1,
|
view_count=sample_help_command.view_count + 1,
|
||||||
display_order=sample_help_command.display_order
|
display_order=sample_help_command.display_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def mock_get_help_by_name(name, include_inactive=False):
|
async def mock_get_help_by_name(name, include_inactive=False):
|
||||||
@ -445,7 +451,9 @@ class TestHelpCommandsServiceViewTracking:
|
|||||||
|
|
||||||
help_commands_service_instance.get_help_by_name = mock_get_help_by_name
|
help_commands_service_instance.get_help_by_name = mock_get_help_by_name
|
||||||
|
|
||||||
result = await help_commands_service_instance.increment_view_count("trading-rules")
|
result = await help_commands_service_instance.increment_view_count(
|
||||||
|
"trading-rules"
|
||||||
|
)
|
||||||
|
|
||||||
assert isinstance(result, HelpCommand)
|
assert isinstance(result, HelpCommand)
|
||||||
assert result.view_count == 101
|
assert result.view_count == 101
|
||||||
@ -459,21 +467,21 @@ class TestHelpCommandsServiceStatistics:
|
|||||||
"""Test getting help command statistics."""
|
"""Test getting help command statistics."""
|
||||||
# Mock API response
|
# Mock API response
|
||||||
api_response = {
|
api_response = {
|
||||||
'total_commands': 50,
|
"total_commands": 50,
|
||||||
'active_commands': 45,
|
"active_commands": 45,
|
||||||
'total_views': 5000,
|
"total_views": 5000,
|
||||||
'most_viewed_command': {
|
"most_viewed_command": {
|
||||||
'id': 1,
|
"id": 1,
|
||||||
'name': 'popular-topic',
|
"name": "popular-topic",
|
||||||
'title': 'Popular Topic',
|
"title": "Popular Topic",
|
||||||
'content': 'Content',
|
"content": "Content",
|
||||||
'created_by_discord_id': '123',
|
"created_by_discord_id": "123",
|
||||||
'created_at': datetime.now(timezone.utc).isoformat(),
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
'is_active': True,
|
"is_active": True,
|
||||||
'view_count': 500,
|
"view_count": 500,
|
||||||
'display_order': 0
|
"display_order": 0,
|
||||||
},
|
},
|
||||||
'recent_commands_count': 5
|
"recent_commands_count": 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
help_commands_service_instance._client.get.return_value = api_response
|
help_commands_service_instance._client.get.return_value = api_response
|
||||||
@ -485,7 +493,7 @@ class TestHelpCommandsServiceStatistics:
|
|||||||
assert result.active_commands == 45
|
assert result.active_commands == 45
|
||||||
assert result.total_views == 5000
|
assert result.total_views == 5000
|
||||||
assert result.most_viewed_command is not None
|
assert result.most_viewed_command is not None
|
||||||
assert result.most_viewed_command.name == 'popular-topic'
|
assert result.most_viewed_command.name == "popular-topic"
|
||||||
assert result.recent_commands_count == 5
|
assert result.recent_commands_count == 5
|
||||||
|
|
||||||
|
|
||||||
@ -498,7 +506,9 @@ class TestHelpCommandsServiceErrorHandling:
|
|||||||
from exceptions import APIException, BotException
|
from exceptions import APIException, BotException
|
||||||
|
|
||||||
# Mock the API client to raise an APIException
|
# Mock the API client to raise an APIException
|
||||||
help_commands_service_instance._client.get.side_effect = APIException("Connection error")
|
help_commands_service_instance._client.get.side_effect = APIException(
|
||||||
|
"Connection error"
|
||||||
|
)
|
||||||
|
|
||||||
with pytest.raises(BotException, match="Failed to retrieve help topic 'test'"):
|
with pytest.raises(BotException, match="Failed to retrieve help topic 'test'"):
|
||||||
await help_commands_service_instance.get_help_by_name("test")
|
await help_commands_service_instance.get_help_by_name("test")
|
||||||
|
|||||||
@ -115,6 +115,13 @@ class TestTransactionBuilder:
|
|||||||
svc.get_current_roster.return_value = mock_roster
|
svc.get_current_roster.return_value = mock_roster
|
||||||
return svc
|
return svc
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_league_service(self):
|
||||||
|
"""Patch league_service for all tests so FA lock check uses week 10 (before deadline)."""
|
||||||
|
with patch("services.transaction_builder.league_service") as mock_ls:
|
||||||
|
mock_ls.get_current_state = AsyncMock(return_value=MagicMock(week=10))
|
||||||
|
yield mock_ls
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def builder(self, mock_team, mock_roster_service):
|
def builder(self, mock_team, mock_roster_service):
|
||||||
"""Create a TransactionBuilder for testing with injected roster service."""
|
"""Create a TransactionBuilder for testing with injected roster service."""
|
||||||
@ -152,6 +159,50 @@ class TestTransactionBuilder:
|
|||||||
assert builder.is_empty is False
|
assert builder.is_empty is False
|
||||||
assert move in builder.moves
|
assert move in builder.moves
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_move_from_fa_blocked_after_deadline(self, builder, mock_player):
|
||||||
|
"""Test that adding a player FROM Free Agency is blocked after fa_lock_week."""
|
||||||
|
move = TransactionMove(
|
||||||
|
player=mock_player,
|
||||||
|
from_roster=RosterType.FREE_AGENCY,
|
||||||
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
|
to_team=builder.team,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"services.transaction_builder.league_service"
|
||||||
|
) as mock_league_service:
|
||||||
|
mock_league_service.get_current_state = AsyncMock(
|
||||||
|
return_value=MagicMock(week=15)
|
||||||
|
)
|
||||||
|
|
||||||
|
success, error_message = await builder.add_move(
|
||||||
|
move, check_pending_transactions=False
|
||||||
|
)
|
||||||
|
|
||||||
|
assert success is False
|
||||||
|
assert "Free agency is closed" in error_message
|
||||||
|
assert builder.move_count == 0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_drop_to_fa_allowed_after_deadline(self, builder, mock_player):
|
||||||
|
"""Test that dropping a player TO Free Agency is still allowed after fa_lock_week."""
|
||||||
|
move = TransactionMove(
|
||||||
|
player=mock_player,
|
||||||
|
from_roster=RosterType.MAJOR_LEAGUE,
|
||||||
|
to_roster=RosterType.FREE_AGENCY,
|
||||||
|
from_team=builder.team,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Drop to FA doesn't trigger the FA lock check (autouse fixture provides week 10)
|
||||||
|
success, error_message = await builder.add_move(
|
||||||
|
move, check_pending_transactions=False
|
||||||
|
)
|
||||||
|
|
||||||
|
assert success is True
|
||||||
|
assert error_message == ""
|
||||||
|
assert builder.move_count == 1
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_add_duplicate_move_fails(self, builder, mock_player):
|
async def test_add_duplicate_move_fails(self, builder, mock_player):
|
||||||
"""Test that adding duplicate moves for same player fails."""
|
"""Test that adding duplicate moves for same player fails."""
|
||||||
@ -809,6 +860,13 @@ class TestPendingTransactionValidation:
|
|||||||
"""Create a mock player for testing."""
|
"""Create a mock player for testing."""
|
||||||
return Player(id=12472, name="Test Player", wara=2.5, season=12, pos_1="OF")
|
return Player(id=12472, name="Test Player", wara=2.5, season=12, pos_1="OF")
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_league_service(self):
|
||||||
|
"""Patch league_service so FA lock check and week resolution use week 10."""
|
||||||
|
with patch("services.transaction_builder.league_service") as mock_ls:
|
||||||
|
mock_ls.get_current_state = AsyncMock(return_value=MagicMock(week=10))
|
||||||
|
yield mock_ls
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def builder(self, mock_team):
|
def builder(self, mock_team):
|
||||||
"""Create a TransactionBuilder for testing."""
|
"""Create a TransactionBuilder for testing."""
|
||||||
|
|||||||
@ -124,6 +124,22 @@ class TradeEmbedView(discord.ui.View):
|
|||||||
self, interaction: discord.Interaction, button: discord.ui.Button
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
||||||
):
|
):
|
||||||
"""Handle submit trade button click."""
|
"""Handle submit trade button click."""
|
||||||
|
# Check trade deadline
|
||||||
|
current = await league_service.get_current_state()
|
||||||
|
if not current:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"❌ Could not retrieve league state. Please try again later.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if current.is_past_trade_deadline:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"❌ **The trade deadline has passed** (Week {current.trade_deadline}). "
|
||||||
|
f"This trade can no longer be submitted.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if self.builder.is_empty:
|
if self.builder.is_empty:
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
"Cannot submit empty trade. Add some moves first!", ephemeral=True
|
"Cannot submit empty trade. Add some moves first!", ephemeral=True
|
||||||
@ -328,6 +344,7 @@ class TradeAcceptanceView(discord.ui.View):
|
|||||||
def __init__(self, builder: TradeBuilder):
|
def __init__(self, builder: TradeBuilder):
|
||||||
super().__init__(timeout=3600.0) # 1 hour timeout
|
super().__init__(timeout=3600.0) # 1 hour timeout
|
||||||
self.builder = builder
|
self.builder = builder
|
||||||
|
self._checked_teams: dict[int, Team] = {}
|
||||||
|
|
||||||
async def _get_user_team(self, interaction: discord.Interaction) -> Optional[Team]:
|
async def _get_user_team(self, interaction: discord.Interaction) -> Optional[Team]:
|
||||||
"""Get the team owned by the interacting user."""
|
"""Get the team owned by the interacting user."""
|
||||||
@ -353,6 +370,7 @@ class TradeAcceptanceView(discord.ui.View):
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
self._checked_teams[interaction.user.id] = user_team
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def on_timeout(self) -> None:
|
async def on_timeout(self) -> None:
|
||||||
@ -366,7 +384,7 @@ class TradeAcceptanceView(discord.ui.View):
|
|||||||
self, interaction: discord.Interaction, button: discord.ui.Button
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
||||||
):
|
):
|
||||||
"""Handle accept button click."""
|
"""Handle accept button click."""
|
||||||
user_team = await self._get_user_team(interaction)
|
user_team = self._checked_teams.get(interaction.user.id)
|
||||||
if not user_team:
|
if not user_team:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -401,7 +419,7 @@ class TradeAcceptanceView(discord.ui.View):
|
|||||||
self, interaction: discord.Interaction, button: discord.ui.Button
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
||||||
):
|
):
|
||||||
"""Handle reject button click - moves trade back to DRAFT."""
|
"""Handle reject button click - moves trade back to DRAFT."""
|
||||||
user_team = await self._get_user_team(interaction)
|
user_team = self._checked_teams.get(interaction.user.id)
|
||||||
if not user_team:
|
if not user_team:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -433,7 +451,16 @@ class TradeAcceptanceView(discord.ui.View):
|
|||||||
config = get_config()
|
config = get_config()
|
||||||
|
|
||||||
current = await league_service.get_current_state()
|
current = await league_service.get_current_state()
|
||||||
next_week = current.week + 1 if current else 1
|
if not current or current.is_past_trade_deadline:
|
||||||
|
deadline_msg = (
|
||||||
|
f"❌ **The trade deadline has passed** (Week {current.trade_deadline}). "
|
||||||
|
f"This trade cannot be finalized."
|
||||||
|
if current
|
||||||
|
else "❌ Could not retrieve league state. Please try again later."
|
||||||
|
)
|
||||||
|
await interaction.followup.send(deadline_msg, ephemeral=True)
|
||||||
|
return
|
||||||
|
next_week = current.week + 1
|
||||||
|
|
||||||
fa_team = Team(
|
fa_team = Team(
|
||||||
id=config.free_agent_team_id,
|
id=config.free_agent_team_id,
|
||||||
@ -708,10 +735,10 @@ async def create_trade_embed(builder: TradeBuilder) -> discord.Embed:
|
|||||||
Returns:
|
Returns:
|
||||||
Discord embed with current trade state
|
Discord embed with current trade state
|
||||||
"""
|
"""
|
||||||
|
validation = await builder.validate_trade()
|
||||||
if builder.is_empty:
|
if builder.is_empty:
|
||||||
color = EmbedColors.SECONDARY
|
color = EmbedColors.SECONDARY
|
||||||
else:
|
else:
|
||||||
validation = await builder.validate_trade()
|
|
||||||
color = EmbedColors.SUCCESS if validation.is_legal else EmbedColors.WARNING
|
color = EmbedColors.SUCCESS if validation.is_legal else EmbedColors.WARNING
|
||||||
|
|
||||||
embed = EmbedTemplate.create_base_embed(
|
embed = EmbedTemplate.create_base_embed(
|
||||||
@ -766,7 +793,6 @@ async def create_trade_embed(builder: TradeBuilder) -> discord.Embed:
|
|||||||
inline=False,
|
inline=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
validation = await builder.validate_trade()
|
|
||||||
if validation.is_legal:
|
if validation.is_legal:
|
||||||
status_text = "Trade appears legal"
|
status_text = "Trade appears legal"
|
||||||
else:
|
else:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user