brackt/app/services/qualifying-points-discord.server.ts
Claude df15386d2d
Add Discord notifications for qualifying points updates
Fire a gold-colored Discord embed whenever QP is awarded during a season
(tennis/golf majors via processQualifyingEvent, CS2 Swiss stages via
assignCs2EliminationQP) so league members see progress throughout the
season rather than only when QP finalize into fantasy placements.

- discord.ts: QPEventEntry interface + sendQualifyingPointsUpdateNotification
  (gold embed with Points Awarded, No Points, and QP Standings sections)
- qualifying-points-discord.server.ts: notifyQualifyingPointsUpdate — looks
  up affected leagues, maps drafted participants to teams/owners, assembles
  entries, and fires the notification per league webhook
- scoring-calculator.ts: call notifyQualifyingPointsUpdate at end of
  processQualifyingEvent (covers tennis, golf, CS2 Champions bracket)
- cs2-major-stage.ts: call notifyQualifyingPointsUpdate at end of
  assignCs2EliminationQP (covers all CS2 Swiss stages incl. 0-QP exits)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnJP1NTYXHxL2kbE4fZaD6
2026-07-01 16:05:44 +00:00

138 lines
4.5 KiB
TypeScript

import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq, inArray } from "drizzle-orm";
import { findDiscordIdsByUserIds } from "~/models/account";
import { getUserDisplayName } from "~/models/user";
import {
sendQualifyingPointsUpdateNotification,
type QPEventEntry,
} from "~/services/discord";
export async function notifyQualifyingPointsUpdate(
sportsSeasonId: string,
scoringEventId: string,
db: ReturnType<typeof database>
): Promise<void> {
const event = await db.query.scoringEvents.findFirst({
where: eq(schema.scoringEvents.id, scoringEventId),
with: {
sportsSeason: {
with: { sport: true },
},
},
});
if (!event) return;
const eventName = event.name;
const sportName = event.sportsSeason?.sport?.name;
const seasonSports = await db.query.seasonSports.findMany({
where: eq(schema.seasonSports.sportsSeasonId, sportsSeasonId),
});
if (seasonSports.length === 0) return;
// QP earned in this specific event
const eventResultRows = await db.query.eventResults.findMany({
where: eq(schema.eventResults.scoringEventId, scoringEventId),
});
const qpEarnedById = new Map<string, number>(
eventResultRows
.filter((r) => r.qualifyingPointsAwarded !== null)
.map((r) => [r.seasonParticipantId, parseFloat(r.qualifyingPointsAwarded!)])
);
if (qpEarnedById.size === 0) return;
// Current running QP totals for the season
const qpTotals = await db.query.seasonParticipantQualifyingTotals.findMany({
where: eq(schema.seasonParticipantQualifyingTotals.sportsSeasonId, sportsSeasonId),
});
const qpTotalById = new Map<string, number>(
qpTotals.map((t) => [t.participantId, parseFloat(t.totalQualifyingPoints)])
);
for (const { seasonId } of seasonSports) {
const season = await db.query.seasons.findFirst({
where: eq(schema.seasons.id, seasonId),
with: { league: true },
});
const webhookUrl = season?.league?.discordWebhookUrl;
if (!webhookUrl) continue;
const seasonName = `${season!.league!.name} ${season!.year}`;
// Map drafted participants → teams in this season
const picks = await db.query.draftPicks.findMany({
where: eq(schema.draftPicks.seasonId, seasonId),
with: { team: true },
});
const teamByParticipantId = new Map<
string,
{ teamName: string; ownerId: string | null }
>();
for (const pick of picks) {
teamByParticipantId.set(pick.participantId, {
teamName: pick.team.name,
ownerId: pick.team.ownerId,
});
}
// Only include participants that appear in both the event and this league's draft
const relevantParticipantIds = [...qpEarnedById.keys()].filter((id) =>
teamByParticipantId.has(id)
);
if (relevantParticipantIds.length === 0) continue;
// Participant display names
const participants = await db.query.seasonParticipants.findMany({
where: inArray(schema.seasonParticipants.id, relevantParticipantIds),
});
const participantNameById = new Map(participants.map((p) => [p.id, p.name]));
// Owner usernames + Discord IDs
const ownerIds = [
...new Set(
relevantParticipantIds
.map((id) => teamByParticipantId.get(id)?.ownerId)
.filter((id): id is string => id !== null && id !== undefined)
),
];
const users = ownerIds.length
? await db.query.users.findMany({
where: inArray(schema.users.id, ownerIds),
})
: [];
const usernameByUserId = new Map(
users
.map((u) => [u.id, getUserDisplayName(u)] as [string, string | null])
.filter((entry): entry is [string, string] => entry[1] !== null)
);
const optedInUserIds = users.filter((u) => u.discordPingEnabled).map((u) => u.id);
const discordIdByUserId = await findDiscordIdsByUserIds(optedInUserIds);
const entries: QPEventEntry[] = relevantParticipantIds.map((participantId) => {
const teamInfo = teamByParticipantId.get(participantId)!;
const ownerId = teamInfo.ownerId;
return {
participantName: participantNameById.get(participantId) ?? participantId,
qpEarned: qpEarnedById.get(participantId) ?? 0,
qpTotal: qpTotalById.get(participantId) ?? 0,
ownerUsername: ownerId ? (usernameByUserId.get(ownerId) ?? undefined) : undefined,
ownerDiscordUserId: ownerId ? discordIdByUserId.get(ownerId) : undefined,
};
});
await sendQualifyingPointsUpdateNotification({
webhookUrl,
seasonName,
sportName,
eventName,
entries,
});
}
}