major-domo-v2/utils/transaction_logging.py
Cal Corum b8b55e90a3 Add trade acceptance workflow with transaction logging (v2.22.0)
- Add multi-team trade acceptance system requiring all GMs to approve
- TradeBuilder tracks accepted_teams with accept_trade/reject_trade methods
- TradeAcceptanceView with Accept/Reject buttons validates GM permissions
- Create transactions when all teams accept (frozen=false for immediate effect)
- Add post_trade_to_log() for rich trade embeds in #transaction-log
- Trade embeds show grouped player moves by receiving team with sWAR
- Add 10 comprehensive tests for acceptance tracking methods
- All 36 trade builder tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 13:46:10 -06:00

261 lines
9.0 KiB
Python

"""
Transaction Logging Utility
Provides centralized function for posting transaction notifications
to the #transaction-log channel.
"""
from typing import List, Optional, Dict
import discord
from config import get_config
from models.transaction import Transaction
from models.team import Team
from models.trade import Trade
from services.trade_builder import TradeBuilder
from views.embeds import EmbedTemplate, EmbedColors
from utils.logging import get_contextual_logger
logger = get_contextual_logger(f'{__name__}')
async def post_transaction_to_log(
bot: discord.Client,
transactions: List[Transaction],
team: Optional[Team] = None
) -> bool:
"""
Post a transaction to the #transaction-log channel.
Args:
bot: Discord bot instance
transactions: List of Transaction objects to post
team: Optional team override (if None, determined from transactions)
Returns:
True if posted successfully, False otherwise
"""
try:
if not transactions:
logger.warning("No transactions provided to post_transaction_to_log")
return False
# Get guild and channel
config = get_config()
guild = bot.get_guild(config.guild_id)
if not guild:
logger.warning(f"Could not find guild {config.guild_id}")
return False
channel = discord.utils.get(guild.text_channels, name='transaction-log')
if not channel:
logger.warning("Could not find #transaction-log channel")
return False
# Determine the team for the embed (team making the moves)
if team is None:
team = await _determine_team_from_transactions(transactions)
# Build move string
move_string = ""
week_num = transactions[0].week
season = transactions[0].season
for txn in transactions:
# Format: PlayerName (sWAR) from OLDTEAM to NEWTEAM
move_string += (
f'**{txn.player.name}** ({txn.player.wara:.2f}) '
f'from {txn.oldteam.abbrev} to {txn.newteam.abbrev}\n'
)
# Create embed matching legacy format
embed = EmbedTemplate.create_base_embed(
title=f'Week {week_num} Transaction',
description=team.sname if hasattr(team, 'sname') else team.lname,
color=EmbedColors.INFO
)
# Set team color if available
if hasattr(team, 'color') and team.color:
try:
# Remove # if present and convert to int
color_hex = team.color.replace('#', '')
embed.color = discord.Color(int(color_hex, 16))
except (ValueError, AttributeError):
pass # Use default color on error
# Set team thumbnail if available
if hasattr(team, 'thumbnail') and team.thumbnail:
embed.set_thumbnail(url=team.thumbnail)
# Add player moves field
embed.add_field(name='Player Moves', value=move_string, inline=False)
# Add footer with SBA branding using current season from transaction
embed.set_footer(
text=f"SBa Season {season}",
icon_url="https://sombaseball.ddns.net/static/images/sba-logo.png"
)
# Post to channel
await channel.send(embed=embed)
logger.info(f"Transaction posted to log: {transactions[0].moveid}, {len(transactions)} moves")
return True
except Exception as e:
logger.error(f"Error posting transaction to log: {e}")
return False
async def _determine_team_from_transactions(transactions: List[Transaction]) -> Team:
"""
Determine which team to display for the transaction embed.
Uses the major league affiliate of the team involved in the transaction
to ensure consistent branding (no MiL or IL team logos).
Logic:
- Use newteam's ML affiliate if it's not FA
- Otherwise use oldteam's ML affiliate if it's not FA
- Otherwise default to newteam
Args:
transactions: List of transactions
Returns:
Team to display in the embed (always Major League team)
"""
first_move = transactions[0]
# Check newteam first
if first_move.newteam.abbrev.upper() != 'FA':
try:
return await first_move.newteam.major_league_affiliate()
except Exception as e:
logger.warning(f"Could not get ML affiliate for {first_move.newteam.abbrev}: {e}")
return first_move.newteam
# Check oldteam
if first_move.oldteam.abbrev.upper() != 'FA':
try:
return await first_move.oldteam.major_league_affiliate()
except Exception as e:
logger.warning(f"Could not get ML affiliate for {first_move.oldteam.abbrev}: {e}")
return first_move.oldteam
# Default to newteam (both are FA)
return first_move.newteam
async def post_trade_to_log(
bot: discord.Client,
builder: TradeBuilder,
transactions: List[Transaction],
effective_week: int
) -> bool:
"""
Post a completed trade to the #transaction-log channel.
Creates a rich embed showing all teams involved and player movements
in a clear, organized format.
Args:
bot: Discord bot instance
builder: TradeBuilder with trade details
transactions: List of created Transaction objects
effective_week: Week the trade takes effect
Returns:
True if posted successfully, False otherwise
"""
try:
if not transactions:
logger.warning("No transactions provided to post_trade_to_log")
return False
# Get guild and channel
config = get_config()
guild = bot.get_guild(config.guild_id)
if not guild:
logger.warning(f"Could not find guild {config.guild_id}")
return False
channel = discord.utils.get(guild.text_channels, name='transaction-log')
if not channel:
logger.warning("Could not find #transaction-log channel")
return False
# Get participating teams
teams = builder.participating_teams
team_abbrevs = "".join(t.abbrev for t in teams)
# Create the trade embed
embed = EmbedTemplate.create_base_embed(
title=f"🤝 Trade Complete: {team_abbrevs}",
description=f"**Week {effective_week}** • Season {builder.trade.season}",
color=EmbedColors.SUCCESS
)
# Group transactions by receiving team (who's getting who)
moves_by_receiver: Dict[str, List[str]] = {}
for txn in transactions:
# Get the ML affiliate for proper team naming
try:
receiving_team = await txn.newteam.major_league_affiliate()
receiving_abbrev = receiving_team.abbrev
except Exception:
receiving_abbrev = txn.newteam.abbrev
if receiving_abbrev not in moves_by_receiver:
moves_by_receiver[receiving_abbrev] = []
# Format: PlayerName (sWAR) from OLDTEAM
try:
sending_team = await txn.oldteam.major_league_affiliate()
sending_abbrev = sending_team.abbrev
except Exception:
sending_abbrev = txn.oldteam.abbrev
moves_by_receiver[receiving_abbrev].append(
f"**{txn.player.name}** ({txn.player.wara:.1f}) from {sending_abbrev}"
)
# Add a field for each team receiving players
for team_abbrev, moves in moves_by_receiver.items():
# Find the team object for potential thumbnail
team_obj = next((t for t in teams if t.abbrev == team_abbrev), None)
team_name = team_obj.sname if team_obj else team_abbrev
embed.add_field(
name=f"📥 {team_name} receives:",
value="\n".join(moves),
inline=False
)
# Set thumbnail to first team's logo (or could alternate)
primary_team = teams[0] if teams else None
if primary_team and hasattr(primary_team, 'thumbnail') and primary_team.thumbnail:
embed.set_thumbnail(url=primary_team.thumbnail)
# Set team color from first team
if primary_team and hasattr(primary_team, 'color') and primary_team.color:
try:
color_hex = primary_team.color.replace('#', '')
embed.color = discord.Color(int(color_hex, 16))
except (ValueError, AttributeError):
pass
# Add footer with trade ID and SBA branding
embed.set_footer(
text=f"Trade ID: {builder.trade_id} • SBA Season {builder.trade.season}",
icon_url="https://sombaseball.ddns.net/static/images/sba-logo.png"
)
# Post to channel
await channel.send(embed=embed)
logger.info(f"Trade posted to log: {builder.trade_id}, {len(transactions)} moves, {len(teams)} teams")
return True
except Exception as e:
logger.error(f"Error posting trade to log: {e}")
return False