feat: program.getEditionSettings + updateEditionSettings
Backs the new consolidated Edition tab on /admin/settings. getEditionSettings returns a merged view of Program-level fields (defaultAttendeeCap, visaStatusVisibleToMembers) plus LIVE_FINAL round config (attendeeEditCutoffHours, confirmationWindowHours, with sensible defaults). Round-derived values are null when the round doesn't exist yet. updateEditionSettings is partial — only supplied fields are written. Round config writes merge into the existing configJson so other keys are preserved. Audit-logged as PROGRAM_EDITION_SETTINGS_UPDATE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
177
tests/unit/program-edition-settings.test.ts
Normal file
177
tests/unit/program-edition-settings.test.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
import { prisma, createCaller } from '../setup'
|
||||
import {
|
||||
createTestUser,
|
||||
createTestProgram,
|
||||
createTestCompetition,
|
||||
createTestRound,
|
||||
cleanupTestData,
|
||||
uid,
|
||||
} from '../helpers'
|
||||
import { programRouter } from '../../src/server/routers/program'
|
||||
|
||||
describe('program.getEditionSettings', () => {
|
||||
const programIds: string[] = []
|
||||
const userIds: string[] = []
|
||||
|
||||
afterAll(async () => {
|
||||
for (const programId of programIds) {
|
||||
await cleanupTestData(programId, [])
|
||||
}
|
||||
if (userIds.length > 0) {
|
||||
await prisma.user.deleteMany({ where: { id: { in: userIds } } })
|
||||
}
|
||||
})
|
||||
|
||||
it('returns the merged view (program fields + LIVE_FINAL round config)', async () => {
|
||||
const admin = await createTestUser('SUPER_ADMIN')
|
||||
userIds.push(admin.id)
|
||||
const program = await createTestProgram({
|
||||
name: `edition-merged-${uid()}`,
|
||||
defaultAttendeeCap: 5,
|
||||
})
|
||||
programIds.push(program.id)
|
||||
await prisma.program.update({
|
||||
where: { id: program.id },
|
||||
data: { visaStatusVisibleToMembers: false },
|
||||
})
|
||||
const competition = await createTestCompetition(program.id)
|
||||
await createTestRound(competition.id, {
|
||||
roundType: 'LIVE_FINAL',
|
||||
sortOrder: 99,
|
||||
configJson: { attendeeEditCutoffHours: 24, confirmationWindowHours: 12 },
|
||||
})
|
||||
|
||||
const caller = createCaller(programRouter, {
|
||||
id: admin.id,
|
||||
email: admin.email,
|
||||
role: 'SUPER_ADMIN',
|
||||
})
|
||||
const result = await caller.getEditionSettings({ programId: program.id })
|
||||
expect(result.defaultAttendeeCap).toBe(5)
|
||||
expect(result.visaStatusVisibleToMembers).toBe(false)
|
||||
expect(result.attendeeEditCutoffHours).toBe(24)
|
||||
expect(result.confirmationWindowHours).toBe(12)
|
||||
expect(result.liveFinalRoundId).not.toBeNull()
|
||||
})
|
||||
|
||||
it('falls back to defaults when LIVE_FINAL round has no relevant config', async () => {
|
||||
const admin = await createTestUser('SUPER_ADMIN')
|
||||
userIds.push(admin.id)
|
||||
const program = await createTestProgram({ name: `edition-defaults-${uid()}` })
|
||||
programIds.push(program.id)
|
||||
const competition = await createTestCompetition(program.id)
|
||||
await createTestRound(competition.id, {
|
||||
roundType: 'LIVE_FINAL',
|
||||
sortOrder: 99,
|
||||
configJson: {},
|
||||
})
|
||||
|
||||
const caller = createCaller(programRouter, {
|
||||
id: admin.id,
|
||||
email: admin.email,
|
||||
role: 'SUPER_ADMIN',
|
||||
})
|
||||
const result = await caller.getEditionSettings({ programId: program.id })
|
||||
expect(result.attendeeEditCutoffHours).toBe(48)
|
||||
expect(result.confirmationWindowHours).toBe(24)
|
||||
})
|
||||
|
||||
it('returns nulls for round-derived fields when no LIVE_FINAL round exists', async () => {
|
||||
const admin = await createTestUser('SUPER_ADMIN')
|
||||
userIds.push(admin.id)
|
||||
const program = await createTestProgram({ name: `edition-noround-${uid()}` })
|
||||
programIds.push(program.id)
|
||||
|
||||
const caller = createCaller(programRouter, {
|
||||
id: admin.id,
|
||||
email: admin.email,
|
||||
role: 'SUPER_ADMIN',
|
||||
})
|
||||
const result = await caller.getEditionSettings({ programId: program.id })
|
||||
expect(result.attendeeEditCutoffHours).toBeNull()
|
||||
expect(result.confirmationWindowHours).toBeNull()
|
||||
expect(result.liveFinalRoundId).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('program.updateEditionSettings', () => {
|
||||
const programIds: string[] = []
|
||||
const userIds: string[] = []
|
||||
|
||||
afterAll(async () => {
|
||||
for (const programId of programIds) {
|
||||
await cleanupTestData(programId, [])
|
||||
}
|
||||
if (userIds.length > 0) {
|
||||
await prisma.user.deleteMany({ where: { id: { in: userIds } } })
|
||||
}
|
||||
})
|
||||
|
||||
it('writes program fields + round configJson + audit-logs', async () => {
|
||||
const admin = await createTestUser('SUPER_ADMIN')
|
||||
userIds.push(admin.id)
|
||||
const program = await createTestProgram({ name: `edition-update-${uid()}` })
|
||||
programIds.push(program.id)
|
||||
const competition = await createTestCompetition(program.id)
|
||||
const round = await createTestRound(competition.id, {
|
||||
roundType: 'LIVE_FINAL',
|
||||
sortOrder: 99,
|
||||
})
|
||||
|
||||
const caller = createCaller(programRouter, {
|
||||
id: admin.id,
|
||||
email: admin.email,
|
||||
role: 'SUPER_ADMIN',
|
||||
})
|
||||
await caller.updateEditionSettings({
|
||||
programId: program.id,
|
||||
defaultAttendeeCap: 4,
|
||||
visaStatusVisibleToMembers: false,
|
||||
attendeeEditCutoffHours: 36,
|
||||
confirmationWindowHours: 18,
|
||||
})
|
||||
|
||||
const refreshed = await prisma.program.findUniqueOrThrow({ where: { id: program.id } })
|
||||
expect(refreshed.defaultAttendeeCap).toBe(4)
|
||||
expect(refreshed.visaStatusVisibleToMembers).toBe(false)
|
||||
|
||||
const updatedRound = await prisma.round.findUniqueOrThrow({ where: { id: round.id } })
|
||||
const cfg = updatedRound.configJson as Record<string, unknown>
|
||||
expect(cfg.attendeeEditCutoffHours).toBe(36)
|
||||
expect(cfg.confirmationWindowHours).toBe(18)
|
||||
|
||||
const audit = await prisma.auditLog.findFirst({
|
||||
where: { action: 'PROGRAM_EDITION_SETTINGS_UPDATE', entityId: program.id },
|
||||
})
|
||||
expect(audit).not.toBeNull()
|
||||
})
|
||||
|
||||
it('preserves untouched configJson keys', async () => {
|
||||
const admin = await createTestUser('SUPER_ADMIN')
|
||||
userIds.push(admin.id)
|
||||
const program = await createTestProgram({ name: `edition-preserve-${uid()}` })
|
||||
programIds.push(program.id)
|
||||
const competition = await createTestCompetition(program.id)
|
||||
const round = await createTestRound(competition.id, {
|
||||
roundType: 'LIVE_FINAL',
|
||||
sortOrder: 99,
|
||||
configJson: { foo: 'bar', attendeeEditCutoffHours: 48 },
|
||||
})
|
||||
|
||||
const caller = createCaller(programRouter, {
|
||||
id: admin.id,
|
||||
email: admin.email,
|
||||
role: 'SUPER_ADMIN',
|
||||
})
|
||||
await caller.updateEditionSettings({
|
||||
programId: program.id,
|
||||
attendeeEditCutoffHours: 24,
|
||||
})
|
||||
|
||||
const updated = await prisma.round.findUniqueOrThrow({ where: { id: round.id } })
|
||||
const cfg = updated.configJson as Record<string, unknown>
|
||||
expect(cfg.foo).toBe('bar')
|
||||
expect(cfg.attendeeEditCutoffHours).toBe(24)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user