Sla (#11)
Tests / backend-test (push) Successful in 1m50s
Tests / frontend-test (push) Successful in 49s

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/11
This commit was merged in pull request #11.
This commit is contained in:
2026-08-13 04:44:08 +00:00
parent 7f2aabbf29
commit d076f4e98d
103 changed files with 2606 additions and 1265 deletions
@@ -181,6 +181,80 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
resolved.Value.Steps[0].ExecutorEmail.ShouldBe("approver@tnsu.kz");
}
// SLA приезжает в резолв уже с применённым наследованием — это осознанное
// решение: строку списка согласования пишет request-portal, и он не должен
// повторять у себя правило «шаг перекрывает тип».
private async Task<int> CreateRequestTypeWithSlaAsync(string name, int? slaHours, bool? askOverdueReason) =>
(await _requestTypeService.CreateAsync(
new CreateRequestTypeRequest(name, $"/{name}", null, null, "General", slaHours, askOverdueReason),
CancellationToken.None)).Value.Id;
private async Task<ResolvedRouteStepDto> ResolveSingleStepWithSlaAsync(
string requestTypeName, int? typeSla, bool? typeAsk, int? stepSla, bool? stepAsk)
{
var requestTypeId = await CreateRequestTypeWithSlaAsync(requestTypeName, typeSla, typeAsk);
var route = await _service.CreateAsync(
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None);
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(
1, "approver@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null,
stepSla, stepAsk),
CancellationToken.None);
var resolved = await _service.ResolveAsync(
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval),
CancellationToken.None);
resolved.IsSuccess.ShouldBeTrue();
return resolved.Value.Steps.ShouldHaveSingleItem();
}
[Fact(DisplayName = "Резолв: шаг без своего SLA наследует часы и флаг от типа заявки")]
public async Task ResolveAsync_StepWithoutSla_InheritsFromRequestType()
{
var step = await ResolveSingleStepWithSlaAsync(
"Sla inherit", typeSla: 48, typeAsk: true, stepSla: null, stepAsk: null);
step.SlaHours.ShouldBe(48);
step.AskOverdueReason.ShouldBe(true);
}
[Fact(DisplayName = "Резолв: SLA шага перекрывает SLA типа заявки")]
public async Task ResolveAsync_StepSla_OverridesRequestTypeSla()
{
var step = await ResolveSingleStepWithSlaAsync(
"Sla override", typeSla: 48, typeAsk: true, stepSla: 8, stepAsk: false);
step.SlaHours.ShouldBe(8);
step.AskOverdueReason.ShouldBe(false);
}
// Ключевое отличие «не задано» от «ноль»: ноль на шаге — это осознанное
// «напоминать сразу», и подменять его типовым значением нельзя.
[Fact(DisplayName = "Резолв: заданный на шаге SLA = 0 не подменяется значением типа заявки")]
public async Task ResolveAsync_StepSlaZero_IsNotReplacedByRequestTypeSla()
{
var step = await ResolveSingleStepWithSlaAsync(
"Sla zero", typeSla: 48, typeAsk: true, stepSla: 0, stepAsk: false);
step.SlaHours.ShouldBe(0);
step.AskOverdueReason.ShouldBe(false);
}
[Fact(DisplayName = "Резолв: SLA не настроен ни на шаге, ни на типе — в ответе null")]
public async Task ResolveAsync_NoSlaAnywhere_ReturnsNull()
{
var step = await ResolveSingleStepWithSlaAsync(
"Sla absent", typeSla: null, typeAsk: null, stepSla: null, stepAsk: null);
step.SlaHours.ShouldBeNull();
step.AskOverdueReason.ShouldBeNull();
}
[Fact(DisplayName = "Резолв маршрута схлопывает шаги, резолвящиеся в одинаковый email, оставляя шаг с меньшим OrderNo")]
public async Task ResolveAsync_StepsResolveToSameEmail_CollapsesToLowestOrderNo()
{