From 2eb63b507746765210097ed493c0f8496895b2ad Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Thu, 13 Aug 2026 17:06:38 +0500 Subject: [PATCH] =?UTF-8?q?warning=20=D0=BE=D0=B1=D1=80=D0=B0=D1=82=D0=BD?= =?UTF-8?q?=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TemplateRoutes/ResolveTemplateRouteDto.cs | 10 +-- .../TemplateRoutes/TemplateRouteService.cs | 74 +++++++++++++------ 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs b/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs index 2029871..1aba714 100644 --- a/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs +++ b/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs @@ -23,18 +23,10 @@ public sealed record ResolvedRouteStepDto( int? SlaHours = null, bool? AskOverdueReason = null); -/// -/// Предупреждение уровня всего резолва (не привязано к конкретному шагу) — например, -/// в маршруте есть шаг с условием по полю IsManager, а в заявке не передано значение -/// поля Position, из которого IsManager вычисляется. Это не ошибка (резолв всё равно -/// отработал, IsManager просто посчитался как «false»), но может означать, что -/// вызывающая сторона забыла передать поле, и шаг из-за этого не появился, хотя должен был. -/// public sealed record ResolveTemplateRouteResultDto( int RouteId, bool RequiresControl, - IReadOnlyList Steps, - string? Warning = null); + IReadOnlyList Steps); /// Значение поля списка SharePoint (field_name шага) для проверки условий шагов при resolve. public sealed record ResolveFieldValueDto(string FieldName, string Value); diff --git a/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs b/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs index dd5dd0d..7456376 100644 --- a/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs +++ b/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs @@ -171,36 +171,32 @@ public sealed class TemplateRouteService( // того, кто отправил заявку). Это свободный текст, поэтому определяем // «руководитель или нет» по ключевым словам в нём. Нет такого поля в заявке — // считаем «нет» (а не подставляем должность инициатора как фолбэк). - fieldValues.TryGetValue("Position", out var formPosition); - fieldValues["IsManager"] = IsManagerPosition(formPosition) ? "true" : "false"; + fieldValues.TryGetValue(PositionField, out var formPosition); + fieldValues[ManagerConditionField] = IsManagerPosition(formPosition) ? "true" : "false"; - // Предупреждение уровня резолва, а не шага: условие по IsManager есть в маршруте - // (хотя бы на одном шаге, независимо от статуса — фильтр по s.Status ниже сузил бы - // проверку до конкретной фазы, а «забыли передать Position» это не про фазу), но - // Position в заявке не пришёл вовсе. Если поле пришло, но пустой строкой или не - // похоже на руководящую должность — это осознанный «false», предупреждать не о чем. - string? generalWarning = null; - if (string.IsNullOrWhiteSpace(formPosition) && - route.Steps.Any(s => string.Equals(s.FieldName, "IsManager", StringComparison.OrdinalIgnoreCase))) - { - generalWarning = "В маршруте есть шаг с условием по полю «IsManager», но в заявке не передано значение " + - "поля «Position» (должность) — условие считается невыполненным."; - } + // Должность в заявке не передана вовсе — условие по IsManager вычислить не из + // чего. Такой шаг НЕ выбрасываем: молча потерять согласующего хуже, чем показать + // лишнего, поэтому шаг попадает в результат и несёт предупреждение (см. ниже + // ResolveStepAsync/conditionWarning). Пустая строка или должность, не похожая на + // руководящую, — это осознанный «нет», предупреждать не о чем. + var positionMissing = string.IsNullOrWhiteSpace(formPosition); var matchingSteps = route.Steps .Where(s => s.Status == request.Status) - .Where(s => MatchesCondition(s, fieldValues)) + .Where(s => MatchesCondition(s, fieldValues) || HasUnevaluatableManagerCondition(s, positionMissing)) .OrderBy(s => s.OrderNo) .ToList(); var steps = new List(); foreach (var step in matchingSteps) steps.Add(await ResolveStepAsync( - step, email, employee.Company, projectExecutors, request.DepartmentId, requestType, cancellationToken)); + step, email, employee.Company, projectExecutors, request.DepartmentId, requestType, + HasUnevaluatableManagerCondition(step, positionMissing) ? PositionMissingWarning : null, + cancellationToken)); var collapsedSteps = CollapseByExecutorEmail(steps); - return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RequiresControl, collapsedSteps, generalWarning)); + return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RequiresControl, collapsedSteps)); } /// @@ -238,9 +234,16 @@ public sealed class TemplateRouteService( x.ByProject == byProject, cancellationToken); + /// + /// Предупреждение про УСЛОВИЕ шага (в отличие от предупреждений ниже, которые все про + /// неопределившегося исполнителя). Приходит снаружи, потому что условия проверяются + /// в ResolveAsync, а не здесь. Если исполнитель к тому же не резолвится — тексты + /// склеиваются, а не затирают друг друга: это независимые проблемы одного шага. + /// private async Task ResolveStepAsync( TemplateRouteStep step, string initiatorEmail, string initiatorCompany, IReadOnlyList projectExecutors, Guid? departmentId, RequestType requestType, + string? conditionWarning, CancellationToken cancellationToken) { string? resolvedEmail; @@ -311,13 +314,16 @@ public sealed class TemplateRouteService( // работает именно так, как задумано — значение шага перекрывает типовое, // и при этом заданный на шаге 0 остаётся нулём, а не подменяется типовым // (0 — осознанное «напоминать сразу», см. TemplateRouteStep.SlaHours). + var combinedWarning = string.Join(" ", new[] { conditionWarning, warning } + .Where(w => !string.IsNullOrWhiteSpace(w))); + return new ResolvedRouteStepDto( step.Id, step.OrderNo, resolvedEmail, resolvedPosition, step.AllowReassignToOtherApprovers, - warning, + combinedWarning.Length == 0 ? null : combinedWarning, step.SlaHours ?? requestType.SlaHours, step.AskOverdueReason ?? requestType.AskOverdueReason); } @@ -353,13 +359,33 @@ public sealed class TemplateRouteService( private static bool TryParseNumber(string? value, out double number) => double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out number); + /// Поле заявки, из которого вычисляется . + private const string PositionField = "Position"; + + /// Синтетическое поле условия шага — вычисляется, а не приходит из заявки. + private const string ManagerConditionField = "IsManager"; + + private const string PositionMissingWarning = + "В заявке не передана должность (поле «Position») — условие шага по «IsManager» вычислить не из чего, " + + "поэтому шаг включён в маршрут, хотя условие могло и не выполниться."; + /// - /// Должность в справочнике api.tnsu.kz — свободный текст без структуры (нет - /// отдельного флага «это руководитель»), поэтому определяем это по ключевым - /// словам в названии должности. Список подобран по фактическим должностям - /// в справочнике — Начальник/Руководитель/Директор/Заместитель/Главный/ - /// Заведующий складом. «Ведущий», «Старший» и «Менеджер» намеренно не входят: - /// это старший специалист, а не должность с подчинёнными. + /// Условие шага завязано на IsManager, но вычислить его не из чего: должность + /// в заявке не передана. Такой шаг попадает в результат вопреки условию и с + /// предупреждением — см. ResolveAsync. + /// + private static bool HasUnevaluatableManagerCondition(TemplateRouteStep step, bool positionMissing) => + positionMissing && + string.Equals(step.FieldName, ManagerConditionField, StringComparison.OrdinalIgnoreCase) && + step.Operation is not (null or ComparisonOperator.None); + + /// + /// Должность — свободный текст без структуры (нет отдельного флага «это + /// руководитель»), поэтому определяем это по ключевым словам в её названии. + /// Список подобран по фактическим должностям в справочнике api.tnsu.kz — + /// Начальник/Руководитель/Директор/Заместитель/Главный/Заведующий складом. + /// «Ведущий», «Старший» и «Менеджер» намеренно не входят: это старший + /// специалист, а не должность с подчинёнными. /// private static readonly string[] ManagerPositionKeywords = ["начальник", "руководитель", "директор", "заместитель", "главный", "заведующий склад"];