package db import ( "context" "fmt" "time" "github.com/pkg/errors" "github.com/uptrace/bun" ) // SeasonLeagueFreeAgent tracks players registered as free agents in a season_league. type SeasonLeagueFreeAgent struct { bun.BaseModel `bun:"table:season_league_free_agents,alias:slfa"` SeasonID int `bun:",pk,notnull"` LeagueID int `bun:",pk,notnull"` PlayerID int `bun:",pk,notnull"` RegisteredAt int64 `bun:",notnull"` RegisteredByUserID int `bun:",notnull"` Season *Season `bun:"rel:belongs-to,join:season_id=id"` League *League `bun:"rel:belongs-to,join:league_id=id"` Player *Player `bun:"rel:belongs-to,join:player_id=id"` RegisteredBy *User `bun:"rel:belongs-to,join:registered_by_user_id=id"` } // FixtureFreeAgent tracks which free agents are nominated for specific fixtures. type FixtureFreeAgent struct { bun.BaseModel `bun:"table:fixture_free_agents,alias:ffa"` FixtureID int `bun:",pk,notnull"` PlayerID int `bun:",pk,notnull"` TeamID int `bun:",notnull"` NominatedByUserID int `bun:",notnull"` NominatedAt int64 `bun:",notnull"` Fixture *Fixture `bun:"rel:belongs-to,join:fixture_id=id"` Player *Player `bun:"rel:belongs-to,join:player_id=id"` Team *Team `bun:"rel:belongs-to,join:team_id=id"` NominatedBy *User `bun:"rel:belongs-to,join:nominated_by_user_id=id"` } // RegisterFreeAgent registers a player as a free agent in a season_league. func RegisterFreeAgent( ctx context.Context, tx bun.Tx, seasonID, leagueID, playerID int, audit *AuditMeta, ) error { user := CurrentUser(ctx) if user == nil { return errors.New("user cannot be nil") } entry := &SeasonLeagueFreeAgent{ SeasonID: seasonID, LeagueID: leagueID, PlayerID: playerID, RegisteredAt: time.Now().Unix(), RegisteredByUserID: user.ID, } info := &AuditInfo{ Action: "free_agents.add", ResourceType: "season_league_free_agent", ResourceID: fmt.Sprintf("%d-%d-%d", seasonID, leagueID, playerID), Details: map[string]any{ "season_id": seasonID, "league_id": leagueID, "player_id": playerID, }, } err := Insert(tx, entry).WithAudit(audit, info).Exec(ctx) if err != nil { return errors.Wrap(err, "Insert") } return nil } // UnregisterFreeAgent removes a player's free agent registration and all their nominations. func UnregisterFreeAgent( ctx context.Context, tx bun.Tx, seasonID, leagueID, playerID int, audit *AuditMeta, ) error { // First remove all nominations for this player err := RemoveAllFreeAgentNominationsForPlayer(ctx, tx, playerID) if err != nil { return errors.Wrap(err, "RemoveAllFreeAgentNominationsForPlayer") } // Then remove the registration _, err = tx.NewDelete(). Model((*SeasonLeagueFreeAgent)(nil)). Where("season_id = ?", seasonID). Where("league_id = ?", leagueID). Where("player_id = ?", playerID). Exec(ctx) if err != nil { return errors.Wrap(err, "tx.NewDelete") } info := &AuditInfo{ Action: "free_agents.remove", ResourceType: "season_league_free_agent", ResourceID: fmt.Sprintf("%d-%d-%d", seasonID, leagueID, playerID), Details: map[string]any{ "season_id": seasonID, "league_id": leagueID, "player_id": playerID, }, } err = LogSuccess(ctx, tx, audit, info) if err != nil { return errors.Wrap(err, "LogSuccess") } return nil } // GetFreeAgentsForSeasonLeague returns all players registered as free agents in a season_league. func GetFreeAgentsForSeasonLeague( ctx context.Context, tx bun.Tx, seasonID, leagueID int, ) ([]*SeasonLeagueFreeAgent, error) { entries := []*SeasonLeagueFreeAgent{} err := tx.NewSelect(). Model(&entries). Where("slfa.season_id = ?", seasonID). Where("slfa.league_id = ?", leagueID). Relation("Player", func(q *bun.SelectQuery) *bun.SelectQuery { return q.Relation("User") }). Relation("RegisteredBy"). Order("slfa.registered_at ASC"). Scan(ctx) if err != nil { return nil, errors.Wrap(err, "tx.NewSelect") } return entries, nil } // IsFreeAgentRegistered checks if a player is registered as a free agent in a season_league. func IsFreeAgentRegistered( ctx context.Context, tx bun.Tx, seasonID, leagueID, playerID int, ) (bool, error) { count, err := tx.NewSelect(). Model((*SeasonLeagueFreeAgent)(nil)). Where("season_id = ?", seasonID). Where("league_id = ?", leagueID). Where("player_id = ?", playerID). Count(ctx) if err != nil { return false, errors.Wrap(err, "tx.NewSelect") } return count > 0, nil } // NominateFreeAgent nominates a free agent for a specific fixture on behalf of a team. func NominateFreeAgent( ctx context.Context, tx bun.Tx, fixtureID, playerID, teamID int, audit *AuditMeta, ) error { user := CurrentUser(ctx) if user == nil { return errors.New("user cannot be nil") } // Check if already nominated by another team existing := new(FixtureFreeAgent) err := tx.NewSelect(). Model(existing). Where("ffa.fixture_id = ?", fixtureID). Where("ffa.player_id = ?", playerID). Scan(ctx) if err == nil { // Found existing nomination if existing.TeamID != teamID { return BadRequest("Player already nominated for this fixture by another team") } return BadRequest("Player already nominated for this fixture") } if err.Error() != "sql: no rows in result set" { return errors.Wrap(err, "tx.NewSelect") } // Check max 2 free agents per team per fixture count, err := tx.NewSelect(). Model((*FixtureFreeAgent)(nil)). Where("fixture_id = ?", fixtureID). Where("team_id = ?", teamID). Count(ctx) if err != nil { return errors.Wrap(err, "tx.NewSelect count") } if count >= 2 { return BadRequest("Maximum of 2 free agents per team per fixture") } entry := &FixtureFreeAgent{ FixtureID: fixtureID, PlayerID: playerID, TeamID: teamID, NominatedByUserID: user.ID, NominatedAt: time.Now().Unix(), } info := &AuditInfo{ Action: "free_agents.nominate", ResourceType: "fixture_free_agent", ResourceID: fmt.Sprintf("%d-%d", fixtureID, playerID), Details: map[string]any{ "fixture_id": fixtureID, "player_id": playerID, "team_id": teamID, }, } err = Insert(tx, entry).WithAudit(audit, info).Exec(ctx) if err != nil { return errors.Wrap(err, "Insert") } return nil } // GetNominatedFreeAgents returns all free agents nominated for a fixture. func GetNominatedFreeAgents( ctx context.Context, tx bun.Tx, fixtureID int, ) ([]*FixtureFreeAgent, error) { entries := []*FixtureFreeAgent{} err := tx.NewSelect(). Model(&entries). Where("ffa.fixture_id = ?", fixtureID). Relation("Player", func(q *bun.SelectQuery) *bun.SelectQuery { return q.Relation("User") }). Relation("Team"). Relation("NominatedBy"). Order("ffa.nominated_at ASC"). Scan(ctx) if err != nil { return nil, errors.Wrap(err, "tx.NewSelect") } return entries, nil } // GetNominatedFreeAgentsByTeam returns free agents nominated by a specific team for a fixture. func GetNominatedFreeAgentsByTeam( ctx context.Context, tx bun.Tx, fixtureID, teamID int, ) ([]*FixtureFreeAgent, error) { entries := []*FixtureFreeAgent{} err := tx.NewSelect(). Model(&entries). Where("ffa.fixture_id = ?", fixtureID). Where("ffa.team_id = ?", teamID). Relation("Player", func(q *bun.SelectQuery) *bun.SelectQuery { return q.Relation("User") }). Order("ffa.nominated_at ASC"). Scan(ctx) if err != nil { return nil, errors.Wrap(err, "tx.NewSelect") } return entries, nil } // RemoveAllFreeAgentNominationsForPlayer deletes all nominations for a player. // Used for cascade deletion on team join and unregister. func RemoveAllFreeAgentNominationsForPlayer( ctx context.Context, tx bun.Tx, playerID int, ) error { _, err := tx.NewDelete(). Model((*FixtureFreeAgent)(nil)). Where("player_id = ?", playerID). Exec(ctx) if err != nil { return errors.Wrap(err, "tx.NewDelete") } return nil } // RemoveFreeAgentNomination removes a specific nomination. func RemoveFreeAgentNomination( ctx context.Context, tx bun.Tx, fixtureID, playerID int, audit *AuditMeta, ) error { _, err := tx.NewDelete(). Model((*FixtureFreeAgent)(nil)). Where("fixture_id = ?", fixtureID). Where("player_id = ?", playerID). Exec(ctx) if err != nil { return errors.Wrap(err, "tx.NewDelete") } info := &AuditInfo{ Action: "free_agents.remove_nomination", ResourceType: "fixture_free_agent", ResourceID: fmt.Sprintf("%d-%d", fixtureID, playerID), Details: map[string]any{ "fixture_id": fixtureID, "player_id": playerID, }, } err = LogSuccess(ctx, tx, audit, info) if err != nil { return errors.Wrap(err, "LogSuccess") } return nil } // RemoveFreeAgentRegistrationForPlayer removes all free agent registrations for a player. // Used on team join. func RemoveFreeAgentRegistrationForPlayer( ctx context.Context, tx bun.Tx, playerID int, ) error { _, err := tx.NewDelete(). Model((*SeasonLeagueFreeAgent)(nil)). Where("player_id = ?", playerID). Exec(ctx) if err != nil { return errors.Wrap(err, "tx.NewDelete") } return nil }