Skip to content
Open
Changes from all 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
18 changes: 12 additions & 6 deletions web/Areas/ClinicalScheduler/Services/ScheduleEditService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,9 @@ await _auditService.LogPrimaryEvaluatorSetAsync(
_logger.LogError(saveEx, "Database save failed for MothraId='{MothraId}', RotationId={RotationId}, WeekIds=[{WeekIds}]",
LogSanitizer.SanitizeId(mothraId), rotationId, string.Join(",", weekIds));

// Check if this is a database constraint violation (typically a duplicate key error)
var errorMessage = saveEx.Message?.ToLower();
var innerMessage = saveEx.InnerException?.Message?.ToLower();

if ((errorMessage != null && (errorMessage.Contains("duplicate") || errorMessage.Contains("unique") || errorMessage.Contains("constraint") || errorMessage.Contains("violation of primary key"))) ||
(innerMessage != null && (innerMessage.Contains("duplicate") || innerMessage.Contains("unique") || innerMessage.Contains("constraint") || innerMessage.Contains("violation of primary key"))))
// Only a unique/duplicate-key violation means the instructor is already scheduled.
// Other DB errors (foreign-key, check constraints, etc.) fall through to the generic message.
if (IsDuplicateKeyViolation(saveEx))
{
throw new InvalidOperationException($"Instructor {mothraId} appears to already be scheduled for one or more of the specified weeks. Please refresh the page and try again.", saveEx);
}
Expand All @@ -262,6 +259,15 @@ await _auditService.LogPrimaryEvaluatorSetAsync(
}
}

// SQL Server: 2627 = unique constraint violation, 2601 = duplicate key in a unique index.
// EF wraps the provider error in DbUpdateException, so unwrap to the underlying SqlException.
private static bool IsDuplicateKeyViolation(Exception ex)
{
var sqlEx = ex as SqlException ?? ex.InnerException as SqlException;
return sqlEx is not null
&& sqlEx.Errors.Cast<SqlError>().Any(e => e.Number is 2627 or 2601);
}

public async Task<(bool success, bool wasPrimaryEvaluator, string? instructorName)> RemoveInstructorScheduleAsync(
int instructorScheduleId,
CancellationToken cancellationToken = default)
Expand Down
Loading