import type { CapMode } from '@prisma/client' import type { PolicyResolution } from '@/types/competition' import type { MemberContext } from './competition-context' // ============================================================================ // System Defaults (Layer 1) // ============================================================================ export const SYSTEM_DEFAULT_CAP = 15 export const SYSTEM_DEFAULT_CAP_MODE: CapMode = 'SOFT' export const SYSTEM_DEFAULT_SOFT_BUFFER = 2 // ============================================================================ // Effective Cap Resolution (5-layer precedence) // ============================================================================ /** * Resolves the effective assignment cap for a jury member. * * Precedence (first non-null wins): * Layer 4b: selfServiceCap (bounded by admin max, only if allowJurorCapAdjustment) * Layer 4a: maxAssignmentsOverride (admin per-member override) * Layer 3: juryGroup.defaultMaxAssignments * Layer 1: SYSTEM_DEFAULT_CAP (15) */ export function resolveEffectiveCap(ctx: MemberContext): PolicyResolution { const group = ctx.juryGroup // Layer 4b: Self-service cap (juror-set during onboarding) if (group?.allowJurorCapAdjustment && ctx.member.selfServiceCap != null) { const adminMax = ctx.member.maxAssignmentsOverride ?? group.defaultMaxAssignments ?? SYSTEM_DEFAULT_CAP const bounded = Math.min(ctx.member.selfServiceCap, adminMax) return { value: bounded, source: 'member', explanation: `Self-service cap ${ctx.member.selfServiceCap} (bounded to ${adminMax})`, } } // Layer 4a: Admin per-member override if (ctx.member.maxAssignmentsOverride != null) { return { value: ctx.member.maxAssignmentsOverride, source: 'member', explanation: `Admin per-member override: ${ctx.member.maxAssignmentsOverride}`, } } // Layer 3: Jury group default if (group) { return { value: group.defaultMaxAssignments, source: 'jury_group', explanation: `Jury group "${group.name}" default: ${group.defaultMaxAssignments}`, } } // Layer 1: System default return { value: SYSTEM_DEFAULT_CAP, source: 'system', explanation: `System default: ${SYSTEM_DEFAULT_CAP}`, } } // ============================================================================ // Effective Cap Mode Resolution // ============================================================================ /** * Resolves the effective cap mode for a jury member. * * Precedence: * Layer 4a: capModeOverride (admin per-member) * Layer 3: juryGroup.defaultCapMode * Layer 1: SYSTEM_DEFAULT_CAP_MODE (SOFT) */ export function resolveEffectiveCapMode(ctx: MemberContext): PolicyResolution { // Layer 4a: Admin per-member override if (ctx.member.capModeOverride != null) { return { value: ctx.member.capModeOverride, source: 'member', explanation: `Admin per-member cap mode override: ${ctx.member.capModeOverride}`, } } // Layer 3: Jury group default if (ctx.juryGroup) { return { value: ctx.juryGroup.defaultCapMode, source: 'jury_group', explanation: `Jury group "${ctx.juryGroup.name}" default: ${ctx.juryGroup.defaultCapMode}`, } } // Layer 1: System default return { value: SYSTEM_DEFAULT_CAP_MODE, source: 'system', explanation: `System default: ${SYSTEM_DEFAULT_CAP_MODE}`, } } // ============================================================================ // Effective Soft Cap Buffer Resolution // ============================================================================ /** * Resolves the effective soft cap buffer. * Only meaningful when capMode is SOFT. * * Precedence: * Layer 3: juryGroup.softCapBuffer * Layer 1: SYSTEM_DEFAULT_SOFT_BUFFER (2) */ export function resolveEffectiveSoftCapBuffer( ctx: MemberContext, ): PolicyResolution { if (ctx.juryGroup) { return { value: ctx.juryGroup.softCapBuffer, source: 'jury_group', explanation: `Jury group "${ctx.juryGroup.name}" buffer: ${ctx.juryGroup.softCapBuffer}`, } } return { value: SYSTEM_DEFAULT_SOFT_BUFFER, source: 'system', explanation: `System default buffer: ${SYSTEM_DEFAULT_SOFT_BUFFER}`, } } // ============================================================================ // Effective Category Bias Resolution // ============================================================================ /** * Resolves the effective category bias (startup ratio) for a jury member. * * Precedence: * Layer 4b: selfServiceRatio (if allowJurorRatioAdjustment) * Layer 4a: preferredStartupRatio (admin per-member) * Layer 3: juryGroup.defaultCategoryQuotas (derived ratio) * Default: null (no preference) */ export function resolveEffectiveCategoryBias( ctx: MemberContext, ): PolicyResolution | null> { const group = ctx.juryGroup // Layer 4b: Self-service ratio if (group?.allowJurorRatioAdjustment && ctx.member.selfServiceRatio != null) { const ratio = ctx.member.selfServiceRatio return { value: { STARTUP: ratio, BUSINESS_CONCEPT: 1 - ratio }, source: 'member', explanation: `Self-service ratio: ${Math.round(ratio * 100)}% startup`, } } // Layer 4a: Admin per-member preferred ratio if (ctx.member.preferredStartupRatio != null) { const ratio = ctx.member.preferredStartupRatio return { value: { STARTUP: ratio, BUSINESS_CONCEPT: 1 - ratio }, source: 'member', explanation: `Admin-set ratio: ${Math.round(ratio * 100)}% startup`, } } // Layer 3: Jury group default category quotas (derive ratio from quotas) if (group?.categoryQuotasEnabled && group.defaultCategoryQuotas) { const quotas = group.defaultCategoryQuotas as Record< string, { min: number; max: number } > const totalMax = Object.values(quotas).reduce((sum, q) => sum + q.max, 0) if (totalMax > 0) { const bias: Record = {} for (const [cat, q] of Object.entries(quotas)) { bias[cat] = q.max / totalMax } return { value: bias, source: 'jury_group', explanation: `Derived from group category quotas`, } } } // No preference return { value: null, source: 'system', explanation: 'No category bias configured', } } // ============================================================================ // Aggregate Policy Evaluation // ============================================================================ export type AssignmentPolicyResult = { effectiveCap: PolicyResolution effectiveCapMode: PolicyResolution softCapBuffer: PolicyResolution categoryBias: PolicyResolution | null> canAssignMore: boolean remainingCapacity: number isOverCap: boolean overCapBy: number } /** * Evaluates all assignment policies for a member in one call. * Returns resolved values with provenance plus computed flags. */ export function evaluateAssignmentPolicy( ctx: MemberContext, ): AssignmentPolicyResult { const effectiveCap = resolveEffectiveCap(ctx) const effectiveCapMode = resolveEffectiveCapMode(ctx) const softCapBuffer = resolveEffectiveSoftCapBuffer(ctx) const categoryBias = resolveEffectiveCategoryBias(ctx) const cap = effectiveCap.value const mode = effectiveCapMode.value const buffer = softCapBuffer.value const count = ctx.currentAssignmentCount const isOverCap = count > cap const overCapBy = Math.max(0, count - cap) const remainingCapacity = mode === 'NONE' ? Infinity : mode === 'SOFT' ? Math.max(0, cap + buffer - count) : Math.max(0, cap - count) const canAssignMore = mode === 'NONE' ? true : mode === 'SOFT' ? count < cap + buffer : count < cap return { effectiveCap, effectiveCapMode, softCapBuffer, categoryBias, canAssignMore, remainingCapacity, isOverCap, overCapBy, } }