package postgres import ( "context" "encoding/json" "time" "mixmaker/backend/internal/application" "mixmaker/backend/internal/domain" ) func (s *Store) EnqueueDiscordRoleSync(ctx context.Context, eventID, action string, generation int64) error { _, err := s.pool.Exec(ctx, `INSERT INTO discord_role_sync_jobs(event_id,action,generation,role_snapshot) SELECT $1,$2,$3,COALESCE(jsonb_agg(discord_role_id) FILTER (WHERE discord_role_id IS NOT NULL),'[]'::jsonb) FROM discord_managed_roles WHERE scope='event' AND event_id=$1 ON CONFLICT(event_id,action,generation) DO UPDATE SET status='pending',available_at=now(),updated_at=now(),completed_at=NULL`, eventID, action, generation) return err } func (s *Store) SeedDiscordRoleSyncJobs(ctx context.Context) error { if _, err := s.pool.Exec(ctx, `INSERT INTO discord_role_sync_jobs(event_id,action,generation) SELECT e.id,'reconcile',e.version FROM events e JOIN event_rosters er ON er.event_id=e.id WHERE e.state IN ('RostersConfirmed','BracketDraft','Live') AND COALESCE((er.body->>'confirmed')::boolean,false)=true ON CONFLICT(event_id,action,generation) DO NOTHING`); err != nil { return err } _, err := s.pool.Exec(ctx, `INSERT INTO discord_role_sync_jobs(event_id,action,generation) SELECT DISTINCT e.id,'rsvp',e.version FROM events e JOIN rsvps r ON r.event_id=e.id WHERE e.state NOT IN ('Completed','Cancelled') ON CONFLICT(event_id,action,generation) DO NOTHING`) return err } func (s *Store) ScheduleGlobalDiscordRoleSync(ctx context.Context, bucket int64) error { _, err := s.pool.Exec(ctx, `INSERT INTO discord_role_sync_jobs(event_id,action,generation) VALUES('__global__','full_reconcile',$1) ON CONFLICT(event_id,action,generation) DO NOTHING`, bucket) return err } func (s *Store) ClaimDiscordRoleSyncJob(ctx context.Context) (application.DiscordRoleSyncJob, error) { var job application.DiscordRoleSyncJob var snapshot []byte err := s.pool.QueryRow(ctx, `WITH candidate AS ( SELECT id FROM discord_role_sync_jobs WHERE (status='pending' AND available_at<=now()) OR (status='processing' AND updated_at>'confirmed')::boolean,false)=true ORDER BY er.event_id`) if err != nil { return nil, err } defer rows.Close() rosters := make([]domain.RosterDraft, 0) for rows.Next() { var body []byte var roster domain.RosterDraft if err = rows.Scan(&body); err != nil { return nil, err } if err = json.Unmarshal(body, &roster); err != nil { return nil, err } rosters = append(rosters, roster) } if err = rows.Err(); err != nil { return nil, err } out := make([]application.DiscordRoleRoster, 0, len(rosters)) for _, roster := range rosters { item, rosterErr := s.discordRoleRoster(ctx, roster) if rosterErr != nil { return nil, rosterErr } out = append(out, item) } return out, nil } func (s *Store) GetDiscordRoleRegistration(ctx context.Context, eventID string) (application.DiscordRoleRegistration, error) { event, err := s.GetEvent(ctx, eventID) if err != nil { return application.DiscordRoleRegistration{}, err } registrations, err := s.ListRSVPs(ctx, eventID) if err != nil { return application.DiscordRoleRegistration{}, err } playerIDs := make([]string, 0, len(registrations)) for _, registration := range registrations { playerIDs = append(playerIDs, registration.PlayerID) } discordIDs, err := s.playerDiscordIDs(ctx, playerIDs) if err != nil { return application.DiscordRoleRegistration{}, err } return application.DiscordRoleRegistration{Event: event, Registrations: registrations, PlayerDiscordIDs: discordIDs}, nil } func (s *Store) ListActiveDiscordRoleRegistrations(ctx context.Context) ([]application.DiscordRoleRegistration, error) { rows, err := s.pool.Query(ctx, `SELECT id FROM events WHERE state NOT IN ('Completed','Cancelled') ORDER BY id`) if err != nil { return nil, err } eventIDs := make([]string, 0) for rows.Next() { var eventID string if err = rows.Scan(&eventID); err != nil { rows.Close() return nil, err } eventIDs = append(eventIDs, eventID) } rows.Close() if err = rows.Err(); err != nil { return nil, err } out := make([]application.DiscordRoleRegistration, 0, len(eventIDs)) for _, eventID := range eventIDs { registration, registrationErr := s.GetDiscordRoleRegistration(ctx, eventID) if registrationErr != nil { return nil, registrationErr } out = append(out, registration) } return out, nil } func (s *Store) discordRoleRoster(ctx context.Context, roster domain.RosterDraft) (application.DiscordRoleRoster, error) { playerIDs := make([]string, 0) for _, team := range roster.Teams { for _, slot := range team.Slots { if slot.PlayerID != "" { playerIDs = append(playerIDs, slot.PlayerID) } } } discordIDs, err := s.playerDiscordIDs(ctx, playerIDs) if err != nil { return application.DiscordRoleRoster{}, err } return application.DiscordRoleRoster{Roster: roster, PlayerDiscordIDs: discordIDs}, nil } func (s *Store) playerDiscordIDs(ctx context.Context, playerIDs []string) (map[string]string, error) { discordIDs := make(map[string]string) if len(playerIDs) == 0 { return discordIDs, nil } rows, err := s.pool.Query(ctx, `SELECT p.id,a.discord_id FROM players p JOIN accounts a ON a.id=p.account_id WHERE p.id=ANY($1)`, playerIDs) if err != nil { return nil, err } defer rows.Close() for rows.Next() { var playerID, discordID string if err = rows.Scan(&playerID, &discordID); err != nil { return nil, err } discordIDs[playerID] = discordID } return discordIDs, rows.Err() }