Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { GraphQLFieldResolver } from 'graphql';
import db from '../../../../database';

interface JudgingWithDivisionId {
divisionId: string;
}

/**
* Resolver for Judging.advancementPercentage field.
* Returns the advancement percentage for a division from event settings.
* Returns null if advancement is disabled (0%).
*/
export const judgingAdvancementPercentageResolver: GraphQLFieldResolver<
JudgingWithDivisionId,
unknown,
unknown,
Promise<number | null>
> = async (judging: JudgingWithDivisionId) => {
try {
const division = await db.divisions.byId(judging.divisionId).get();

if (!division) {
throw new Error(`Division not found for division ID: ${judging.divisionId}`);
}

// Get event settings to check advancement percentage
const eventSettings = await db.events.byId(division.event_id).getSettings();

if (!eventSettings) {
throw new Error(`Event settings not found for division ID: ${judging.divisionId}`);
}

// Return null if advancement is disabled (0%), otherwise return the percentage
return eventSettings.advancement_percent === 0 ? null : eventSettings.advancement_percent;
} catch (error) {
console.error('Error fetching advancement percentage for division:', judging.divisionId, error);
throw error;
}
};
2 changes: 2 additions & 0 deletions apps/backend/src/lib/graphql/resolvers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { divisionJudgingResolver } from './divisions/judging/judging';
import { judgingSessionsResolver } from './divisions/judging/judging-sessions';
import { judgingRoomsResolver } from './divisions/judging/judging-rooms';
import { judgingSessionLengthResolver } from './divisions/judging/judging-session-length';
import { judgingAdvancementPercentageResolver } from './divisions/judging/judging-advancement-percentage';
import { judgingRubricsResolver } from './divisions/judging/judging-rubrics';
import { judgingDeliberationResolver } from './divisions/judging/judging-deliberation';
import { judgingFinalDeliberationResolver } from './divisions/judging/judging-final-deliberation';
Expand Down Expand Up @@ -108,6 +109,7 @@ export const resolvers = {
sessions: judgingSessionsResolver,
rooms: judgingRoomsResolver,
sessionLength: judgingSessionLengthResolver,
advancementPercentage: judgingAdvancementPercentageResolver,
rubrics: judgingRubricsResolver,
deliberation: judgingDeliberationResolver,
finalDeliberation: judgingFinalDeliberationResolver,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { GraphQLFieldResolver } from 'graphql';
import { FinalDeliberationStage, FinalDeliberationAwards } from '@lems/database';
import { FinalDeliberationStage } from '@lems/database';
import { RedisEventTypes } from '@lems/types/api/lems/redis';
import { MutationError, MutationErrorCode } from '@lems/types/api/lems';
import type { GraphQLContext } from '../../../apollo-server';
import db from '../../../../database';
import { getRedisPubSub } from '../../../../redis/redis-pubsub';
import { validateStageCompletion } from './handlers/validators';
import { handleChampionsStageCompletion } from './handlers/champions';
import { handleCoreAwardsStageCompletion } from './handlers/core-awards';
import { handleOptionalAwardsStageCompletion } from './handlers/optional-awards';
import { handleReviewStageCompletion } from './handlers/review';

interface AdvanceFinalDeliberationStageArgs {
divisionId: string;
Expand Down Expand Up @@ -83,11 +88,17 @@ export const advanceFinalDeliberationStageResolver: GraphQLFieldResolver<
}

// Validate current stage before advancing
await validateStageCompletion(divisionId, deliberation);
await validateStageCompletion(deliberation);

// If leaving champions stage, handle advancement
// Handle stage-specific completion logic
if (deliberation.stage === 'champions') {
await handleChampionsStageCompletion(divisionId);
await handleChampionsStageCompletion(divisionId, deliberation.awards.champions);
} else if (deliberation.stage === 'core-awards') {
await handleCoreAwardsStageCompletion(divisionId);
} else if (deliberation.stage === 'optional-awards') {
await handleOptionalAwardsStageCompletion(divisionId);
} else if (deliberation.stage === 'review') {
await handleReviewStageCompletion(divisionId);
}

// Update to next stage and clear stage-specific data
Expand Down Expand Up @@ -126,82 +137,3 @@ export const advanceFinalDeliberationStageResolver: GraphQLFieldResolver<
status: updated.status
};
};

/**
* Validates that the current stage has all required data before advancing
*/
async function validateStageCompletion(
divisionId: string,
deliberation: { stage: FinalDeliberationStage; awards: FinalDeliberationAwards }
): Promise<void> {
switch (deliberation.stage) {
case 'champions': {
// Validate champions placement (at least 1st place must be assigned)
const champions = deliberation.awards.champions || {};
if (!champions['1']) {
throw new MutationError(
MutationErrorCode.FORBIDDEN,
'Cannot advance from champions stage without assigning 1st place'
);
}
break;
}
case 'core-awards': {
// Validate that required core awards are assigned
const awards = deliberation.awards;
if (!awards['innovation-project'] || awards['innovation-project'].length === 0) {
throw new MutationError(
MutationErrorCode.FORBIDDEN,
'Innovation Project award must be assigned before advancing'
);
}
if (!awards['robot-design'] || awards['robot-design'].length === 0) {
throw new MutationError(
MutationErrorCode.FORBIDDEN,
'Robot Design award must be assigned before advancing'
);
}
if (!awards['core-values'] || awards['core-values'].length === 0) {
throw new MutationError(
MutationErrorCode.FORBIDDEN,
'Core Values award must be assigned before advancing'
);
}
break;
}
// optional-awards and review don't require validation
}
}

/**
* Handles advancement award creation when leaving champions stage
*/
async function handleChampionsStageCompletion(divisionId: string): Promise<void> {
// Get division to check if advancement is enabled
const division = await db.divisions.byId(divisionId).get();
if (!division) return;

// Check if advancement is enabled
const event = await db.raw.sql
.selectFrom('events')
.innerJoin('event_settings', 'event_settings.event_id', 'events.id')
.select('event_settings.advancement_percent')
.where('events.id', '=', division.event_id)
.executeTakeFirst();

if (!event || event.advancement_percent === 0) {
// Advancement not enabled
return;
}

// TODO: Calculate advancing teams based on advancement percentage
// This will require:
// 1. Get all teams in division
// 2. Calculate total ranks for each team
// 3. Sort by total rank with tiebreakers (CV rank, then team number)
// 4. Take top N% based on advancement_percent
// 5. Store in deliberation.awards or separate advancement tracking

// For now, this is a placeholder for the advancement logic
// Implementation will be added in a follow-up
}
Loading