Схлопывание шагов резолва: PositionCode не теряется, конфликт — с предупреждением
CollapseByExecutorEmail просто выкидывал дубликаты по email, оставляя первую строку как есть — если у неё PositionCode был пуст (исполнитель задан email/должностью), а у отброшенного дубликата задан (пришёл через HeadType), тот терялся молча. Теперь: PositionCode пустой у оставленной строки — переносится с дубликата. Задан у обеих и они РАЗНЫЕ (один и тот же человек получен через два несовместимых HeadType, например Chief и DirectionHead одновременно) — оставляем первый попавшийся и добавляем предупреждение с обоими значениями, а не выбираем молча. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -516,17 +516,50 @@ public sealed class TemplateRouteService(
|
||||
!string.IsNullOrWhiteSpace(position) &&
|
||||
ManagerPositionKeywords.Any(keyword => position.Contains(keyword, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>
|
||||
/// Один и тот же исполнитель, полученный из разных шагов, в результате должен
|
||||
/// остаться один раз (иначе он одобрял бы одну заявку дважды) — но PositionCode
|
||||
/// схлопнутой строки не должен молча потеряться. Если у уже сохранённой строки
|
||||
/// он пуст, а у дубликата задан — переносим его на сохранённую строку. Если он
|
||||
/// задан у обеих строк и они РАЗНЫЕ (один и тот же email получен через две
|
||||
/// несовместимые роли шага, например Chief и CompanyDirectors одновременно) —
|
||||
/// это не должно молча выбрать одну из них без следа: добавляем предупреждение
|
||||
/// и оставляем любую (первую) — потерять исполнителя из-за такой неоднозначности
|
||||
/// хуже, чем показать чуть неточный PositionCode с пояснением.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<ResolvedRouteStepDto> CollapseByExecutorEmail(IReadOnlyList<ResolvedRouteStepDto> steps)
|
||||
{
|
||||
var seenEmails = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var indexByEmail = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
var result = new List<ResolvedRouteStepDto>();
|
||||
|
||||
foreach (var step in steps)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(step.ExecutorEmail) && !seenEmails.Add(step.ExecutorEmail))
|
||||
if (string.IsNullOrWhiteSpace(step.ExecutorEmail))
|
||||
{
|
||||
result.Add(step);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(step);
|
||||
if (!indexByEmail.TryGetValue(step.ExecutorEmail, out var index))
|
||||
{
|
||||
indexByEmail[step.ExecutorEmail] = result.Count;
|
||||
result.Add(step);
|
||||
continue;
|
||||
}
|
||||
|
||||
var existing = result[index];
|
||||
if (existing.PositionCode is null && step.PositionCode is not null)
|
||||
{
|
||||
result[index] = existing with { PositionCode = step.PositionCode };
|
||||
}
|
||||
else if (existing.PositionCode is not null && step.PositionCode is not null && existing.PositionCode != step.PositionCode)
|
||||
{
|
||||
var conflictWarning =
|
||||
$"Исполнитель «{step.ExecutorEmail}» получен через разные роли шага ({existing.PositionCode} и {step.PositionCode}) — оставлен {existing.PositionCode}.";
|
||||
var combinedWarning = string.Join(" ", new[] { existing.Warning, conflictWarning }
|
||||
.Where(w => !string.IsNullOrWhiteSpace(w)));
|
||||
result[index] = existing with { Warning = combinedWarning };
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user