More robust is sub resolution by persisting it
Some checks are pending
E2E Tests / e2e (push) Waiting to run
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run

This commit is contained in:
Kalle
2026-07-11 15:41:14 +03:00
parent 3956b1d5e3
commit 9174ad8484
24 changed files with 1264 additions and 5 deletions

View File

@@ -881,6 +881,8 @@ export interface TournamentTeamMember {
userId: number;
role: Generated<"OWNER" | "MANAGER" | "REGULAR">;
isStayAsSub: Generated<DBBoolean>;
/** Set when the member was added to the roster after registration closed. */
isSub: Generated<DBBoolean>;
// denormalized from TournamentTeam.isLooking
isLooking: Generated<DBBoolean>;
}

View File

@@ -18,6 +18,7 @@ const createMember = (userId: number) =>
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
}) as const;

View File

@@ -51,6 +51,7 @@ describe("tournamentSummary()", () => {
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
})),
name: `Team ${teamId}`,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -411,6 +411,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "OWNER",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -428,6 +429,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -445,6 +447,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -462,6 +465,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -479,6 +483,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
],
@@ -527,6 +532,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "OWNER",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -544,6 +550,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -561,6 +568,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -578,6 +586,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
],
@@ -621,6 +630,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "OWNER",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -638,6 +648,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -655,6 +666,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -672,6 +684,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
],
@@ -715,6 +728,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "OWNER",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -732,6 +746,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -749,6 +764,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -766,6 +782,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -783,6 +800,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
],
@@ -831,6 +849,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "OWNER",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -848,6 +867,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -865,6 +885,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -882,6 +903,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -899,6 +921,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -916,6 +939,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
],
@@ -959,6 +983,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "OWNER",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -976,6 +1001,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -993,6 +1019,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -1010,6 +1037,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -1027,6 +1055,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
],
@@ -1075,6 +1104,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "OWNER",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -1092,6 +1122,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -1109,6 +1140,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
{
@@ -1126,6 +1158,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
isSub: 0,
customAvatarUrl: null,
},
],

File diff suppressed because it is too large Load Diff

View File

@@ -208,6 +208,7 @@ export async function findById(id: number) {
"PlusTier.tier as plusTier",
"TournamentTeamMember.role",
"TournamentTeamMember.createdAt",
"TournamentTeamMember.isSub",
sql<string | null> /*sql*/`coalesce(
"TournamentTeamMember"."inGameName",
"User"."inGameName"

View File

@@ -143,6 +143,8 @@ export function create({
.returning("id")
.executeTakeFirstOrThrow();
const isSub = (await registrationClosedNow(trx, tournamentId)) ? 1 : 0;
const inGameName = await resolveInGameName(trx, tournamentId, userId);
await trx
@@ -152,6 +154,7 @@ export function create({
userId,
role: "OWNER",
inGameName,
isSub,
})
.execute();
@@ -174,6 +177,7 @@ export function create({
tournamentTeamId: tournamentTeam.id,
userId: memberUserId,
inGameName: memberInGameName,
isSub,
})
.execute();
@@ -286,6 +290,12 @@ export function upsertRegistration({
.execute();
}
const isSub =
membersToAdd.length > 0 &&
(await registrationClosedNow(trx, tournamentId))
? 1
: 0;
for (const userId of membersToAdd) {
const isOwner = isNew && userId === ownerUserId;
const inGameName =
@@ -298,6 +308,7 @@ export function upsertRegistration({
tournamentTeamId: id,
userId,
inGameName,
isSub,
...(isOwner ? { role: "OWNER" as const } : {}),
})
.execute();
@@ -344,6 +355,35 @@ export function upsertRegistration({
});
}
/**
* Whether the tournament's registration is closed at the current moment, based on
* the organizer-set `regClosesAt` if present, otherwise the tournament start time.
* Members added while registration is closed are persisted as subs.
*/
async function registrationClosedNow(
trx: Transaction<DB>,
tournamentId: number,
) {
const { regClosesAt } = await trx
.selectFrom("Tournament")
.innerJoin("CalendarEvent", "CalendarEvent.tournamentId", "Tournament.id")
.innerJoin(
"CalendarEventDate",
"CalendarEventDate.eventId",
"CalendarEvent.id",
)
.select(
sql<number>`coalesce(
"Tournament"."settings" ->> 'regClosesAt',
min("CalendarEventDate"."startTime")
)`.as("regClosesAt"),
)
.where("Tournament.id", "=", tournamentId)
.executeTakeFirstOrThrow();
return regClosesAt <= databaseTimestampNow();
}
async function resolveInGameName(
trx: Transaction<DB>,
tournamentId: number,
@@ -409,6 +449,7 @@ export function copyFromAnotherTournament({
"TournamentTeamMember.inGameName",
"TournamentTeamMember.role",
"TournamentTeamMember.userId",
"TournamentTeamMember.isSub",
// -- exclude these
// "TournamentTeamMember.tournamentTeamId"
@@ -718,6 +759,7 @@ export function join({
).tournamentId;
const inGameName = await resolveInGameName(trx, tournamentId, userId);
const isSub = (await registrationClosedNow(trx, tournamentId)) ? 1 : 0;
await trx
.insertInto("TournamentTeamMember")
@@ -725,6 +767,7 @@ export function join({
tournamentTeamId: newTeamId,
userId,
inGameName,
isSub,
})
.execute();

View File

@@ -5,7 +5,6 @@ import { ModeImage, StageImage } from "~/components/Image";
import type { Tables } from "~/db/tables";
import { useUser } from "~/features/auth/core/user";
import type { TournamentDataTeam } from "~/features/tournament-bracket/core/Tournament.server";
import { databaseTimestampToDate } from "~/utils/dates";
import { userPage } from "~/utils/urls";
import { accountCreatedInTheLastSixMonths } from "~/utils/users";
import { useTournament, useTournamentFriendCodes } from "../routes/to.$id";
@@ -62,9 +61,6 @@ export function TeamWithRoster({
<ul className={styles.teamWithRosterMembers}>
{team.members.map((member) => {
const friendCode = friendCodes?.[member.userId];
const isSub =
databaseTimestampToDate(member.createdAt) >
tournament.registrationClosesAt;
const name = () => {
if (!tournament.ctx.settings.requireInGameNames) {
@@ -79,7 +75,7 @@ export function TeamWithRoster({
{member.role === "OWNER" ? (
<span className={`${styles.teamMemberNameRole}`}>C</span>
) : null}
{isSub && member.role !== "OWNER" ? (
{member.isSub && member.role !== "OWNER" ? (
<span
className={`${styles.teamMemberNameRole} ${styles.teamMemberNameRoleSub}`}
>

View File

@@ -104,6 +104,7 @@ describe("syncLiveStreams tournament streamers", () => {
twitch: "player_one",
plusTier: null,
role: "OWNER",
isSub: 0,
createdAt: 0,
streamTwitch: null,
streamViewerCount: null,
@@ -145,6 +146,7 @@ describe("syncLiveStreams tournament streamers", () => {
twitch: "dropped_player",
plusTier: null,
role: "OWNER",
isSub: 0,
createdAt: 0,
streamTwitch: null,
streamViewerCount: null,
@@ -199,6 +201,7 @@ describe("syncLiveStreams tournament streamers", () => {
twitch: "different_account",
plusTier: null,
role: "OWNER",
isSub: 0,
createdAt: 0,
streamTwitch: null,
streamViewerCount: null,
@@ -236,6 +239,7 @@ describe("syncLiveStreams tournament streamers", () => {
twitch: "streamer_a",
plusTier: null,
role: "OWNER",
isSub: 0,
createdAt: 0,
streamTwitch: null,
streamViewerCount: null,
@@ -276,6 +280,7 @@ describe("syncLiveStreams tournament streamers", () => {
twitch: "streamer_b",
plusTier: null,
role: "OWNER",
isSub: 0,
createdAt: 0,
streamTwitch: null,
streamViewerCount: null,

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,31 @@
export function up(db) {
db.transaction(() => {
db.prepare(
/*sql*/ `alter table "TournamentTeamMember" add column "isSub" integer not null default 0`,
).run();
// backfill from the previous time-based detection: a member registered after
// the tournament's registration closed (organizer-set regClosesAt, otherwise
// the tournament start time) is considered a sub
db.prepare(
/*sql*/ `
update "TournamentTeamMember"
set "isSub" = 1
where "createdAt" > (
select coalesce(
"Tournament"."settings" ->> 'regClosesAt',
min("CalendarEventDate"."startTime")
)
from "TournamentTeam"
inner join "Tournament"
on "Tournament"."id" = "TournamentTeam"."tournamentId"
inner join "CalendarEvent"
on "CalendarEvent"."tournamentId" = "Tournament"."id"
inner join "CalendarEventDate"
on "CalendarEventDate"."eventId" = "CalendarEvent"."id"
where "TournamentTeam"."id" = "TournamentTeamMember"."tournamentTeamId"
)
`,
).run();
})();
}