From d076f4e98d67c7afb17ba068c1ea51a47439d116 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Thu, 13 Aug 2026 04:44:08 +0000 Subject: [PATCH] Sla (#11) Co-authored-by: gorbenko.pavel Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/11 --- .gitea/workflows/deploy_backend.yml | 2 +- README.md | 11 +- backend/Directory.Packages.props | 1 + .../Auth/AuthSchemes.cs | 30 + .../Tnsu.ApprovalRoute.Api/Auth/AuthSetup.cs | 150 +++ .../Auth/CurrentUserAccessor.cs | 0 .../Auth/DevAuthHandler.cs | 0 .../Common/ApiResponse.cs | 0 .../Common/ResultExtensions.cs | 0 .../Controllers/RemindersController.cs | 26 + .../Controllers/RequestTypesController.cs | 0 .../TemplateRouteStepsController.cs | 0 .../Controllers/TemplateRoutesController.cs | 0 backend/Tnsu.ApprovalRoute.Api/Dockerfile | 21 + .../Filters/ValidationActionFilter.cs | 0 .../Tnsu.ApprovalRoute.Api/Program.cs | 2 + .../Properties/launchSettings.json | 0 .../Tnsu.ApprovalRoute.Api.csproj | 0 .../appsettings.Development.json | 0 .../Tnsu.ApprovalRoute.Api/appsettings.json | 9 +- .../ApplicationServiceCollectionExtensions.cs | 2 + .../Common/Errors/Errors.cs | 0 .../Common/Interfaces/ICurrentUser.cs | 0 .../Reminders/ApprovalRow.cs | 115 ++ .../Reminders/ReminderDto.cs | 20 + .../Reminders/ReminderService.cs | 261 +++++ .../Reminders/RemindersOptions.cs | 21 + .../RequestTypes/RequestTypeDto.cs | 42 + .../RequestTypes/RequestTypeService.cs | 16 +- .../RequestTypes/RequestTypeValidators.cs | 18 + .../TemplateRouteStepDto.cs | 21 +- .../TemplateRouteStepService.cs | 14 +- .../TemplateRouteStepValidators.cs | 12 + .../TemplateRoutes/ResolveTemplateRouteDto.cs | 15 +- .../TemplateRoutes/TemplateRouteDto.cs | 0 .../TemplateRoutes/TemplateRouteService.cs | 27 +- .../TemplateRoutes/TemplateRouteValidators.cs | 0 .../Tnsu.ApprovalRoute.Application.csproj | 0 .../Entities/RequestType.cs | 29 + .../Entities/TemplateRoute.cs | 0 .../Entities/TemplateRouteStep.cs | 39 + .../Enums/ComparisonOperator.cs | 0 .../Enums/HeadType.cs | 0 .../Enums/ProjectRole.cs | 0 .../Enums/RouteStepStatus.cs | 0 .../Tnsu.ApprovalRoute.Domain.csproj | 0 ...frastructureServiceCollectionExtensions.cs | 7 + .../Persistence/ApprovalRouteDbContext.cs | 12 + .../DatabaseMigrationInitializer.cs | 0 .../Persistence/Entities/AppSetting.cs | 24 + .../20260727103117_InitialCreate.Designer.cs | 0 .../20260727103117_InitialCreate.cs | 0 ...eSectionAndStepConditionFields.Designer.cs | 0 ...peListNameSectionAndStepConditionFields.cs | 0 ...ameRequestTypeListNameToListId.Designer.cs | 0 ...10943_RenameRequestTypeListNameToListId.cs | 0 .../20260812091325_AddSlaSettings.Designer.cs | 254 +++++ .../20260812091325_AddSlaSettings.cs | 74 ++ .../ApprovalRouteDbContextModelSnapshot.cs | 37 + .../SharePoint/CertificateProvider.cs | 70 ++ .../SharePoint/SharePointClient.cs | 145 +++ .../SharePoint/SharePointOptions.cs | 23 + .../Tnsu.ApprovalRoute.Infrastructure.csproj | 2 + .../TnsuPortal/HttpTnsuPortalClient.cs | 0 .../TnsuPortal/ITnsuPortalClient.cs | 0 .../TnsuPortal/TnsuPortalDtos.cs | 0 .../TnsuPortal/TnsuPortalOptions.cs | 0 .../TnsuPortal/TnsuPortalTokenProvider.cs | 0 backend/Tnsu.ApprovalRoute.sln | 14 +- backend/src/.vs/VSWorkspaceState.json | 9 - backend/src/.vs/slnx.sqlite | Bin 557056 -> 0 bytes ...c41e12f7-4a3d-4c40-9653-41a24091003e.vsidx | Bin 123854 -> 0 bytes ...f9a35f81-13ae-4547-9684-90f51c188103.vsidx | Bin 895 -> 0 bytes .../src.slnx/config/applicationhost.config | 1011 ----------------- backend/src/.vs/src.slnx/v18/.wsuo | Bin 43008 -> 0 bytes .../src.slnx/v18/DocumentLayout.backup.json | 37 - .../src/.vs/src.slnx/v18/DocumentLayout.json | 37 - .../Auth/AuthSchemes.cs | 6 - .../Tnsu.ApprovalRoute.Api/Auth/AuthSetup.cs | 41 - backend/src/Tnsu.ApprovalRoute.Api/Dockerfile | 18 - .../RequestTypes/RequestTypeDto.cs | 9 - .../Entities/RequestType.cs | 13 - .../Entities/TemplateRouteStep.cs | 24 - .../ReminderServiceTests.cs | 293 +++++ .../TemplateRouteServiceTests.cs | 74 ++ ...Tnsu.ApprovalRoute.IntegrationTests.csproj | 4 +- docker-compose.yml | 2 +- .../screens/ManageRequestsApproverModal.tsx | 71 +- .../screens/ManageRequestsPhaseTable.tsx | 32 + .../screens/ManageRequestsScreen.module.scss | 8 + .../screens/ManageRequestsScreen.tsx | 40 +- .../components/screens/RequestTypesScreen.tsx | 46 +- .../components/screens/RouteStepFormModal.tsx | 45 +- .../components/ui/AsyncSearchableSelect.tsx | 17 +- .../approvalAdmin/models/requestManage.ts | 18 + .../approvalAdmin/models/requestTypes.ts | 12 + .../models/templateRouteSteps.ts | 11 + .../webparts/approvalAdmin/services/api.ts | 2 +- .../services/requestManageService.ts | 53 +- frontend/src/webparts/approvalAdmin/texts.ts | 31 + .../webparts/approvalAdmin/utils/sla.test.ts | 185 +++ .../src/webparts/approvalAdmin/utils/sla.ts | 154 +++ .../approvalAdmin/utils/stepExecutor.test.ts | 2 + 103 files changed, 2606 insertions(+), 1265 deletions(-) create mode 100644 backend/Tnsu.ApprovalRoute.Api/Auth/AuthSchemes.cs create mode 100644 backend/Tnsu.ApprovalRoute.Api/Auth/AuthSetup.cs rename backend/{src => }/Tnsu.ApprovalRoute.Api/Auth/CurrentUserAccessor.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Api/Auth/DevAuthHandler.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Api/Common/ApiResponse.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Api/Common/ResultExtensions.cs (100%) create mode 100644 backend/Tnsu.ApprovalRoute.Api/Controllers/RemindersController.cs rename backend/{src => }/Tnsu.ApprovalRoute.Api/Controllers/RequestTypesController.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Api/Controllers/TemplateRouteStepsController.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Api/Controllers/TemplateRoutesController.cs (100%) create mode 100644 backend/Tnsu.ApprovalRoute.Api/Dockerfile rename backend/{src => }/Tnsu.ApprovalRoute.Api/Filters/ValidationActionFilter.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Api/Program.cs (95%) rename backend/{src => }/Tnsu.ApprovalRoute.Api/Properties/launchSettings.json (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Api/appsettings.Development.json (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Api/appsettings.json (71%) rename backend/{src => }/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs (88%) rename backend/{src => }/Tnsu.ApprovalRoute.Application/Common/Errors/Errors.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Application/Common/Interfaces/ICurrentUser.cs (100%) create mode 100644 backend/Tnsu.ApprovalRoute.Application/Reminders/ApprovalRow.cs create mode 100644 backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderDto.cs create mode 100644 backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderService.cs create mode 100644 backend/Tnsu.ApprovalRoute.Application/Reminders/RemindersOptions.cs create mode 100644 backend/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs rename backend/{src => }/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs (81%) rename backend/{src => }/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeValidators.cs (64%) rename backend/{src => }/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepDto.cs (62%) rename backend/{src => }/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs (95%) rename backend/{src => }/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepValidators.cs (76%) rename backend/{src => }/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs (59%) rename backend/{src => }/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteDto.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs (91%) rename backend/{src => }/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteValidators.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Application/Tnsu.ApprovalRoute.Application.csproj (100%) create mode 100644 backend/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs rename backend/{src => }/Tnsu.ApprovalRoute.Domain/Entities/TemplateRoute.cs (100%) create mode 100644 backend/Tnsu.ApprovalRoute.Domain/Entities/TemplateRouteStep.cs rename backend/{src => }/Tnsu.ApprovalRoute.Domain/Enums/ComparisonOperator.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Domain/Enums/HeadType.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Domain/Enums/ProjectRole.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Domain/Enums/RouteStepStatus.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Domain/Tnsu.ApprovalRoute.Domain.csproj (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/InfrastructureServiceCollectionExtensions.cs (72%) rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs (77%) rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/Persistence/DatabaseMigrationInitializer.cs (100%) create mode 100644 backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Entities/AppSetting.cs rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260727103117_InitialCreate.Designer.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260727103117_InitialCreate.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731111928_AddRequestTypeListNameSectionAndStepConditionFields.Designer.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731111928_AddRequestTypeListNameSectionAndStepConditionFields.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.Designer.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.cs (100%) create mode 100644 backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260812091325_AddSlaSettings.Designer.cs create mode 100644 backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260812091325_AddSlaSettings.cs rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs (85%) create mode 100644 backend/Tnsu.ApprovalRoute.Infrastructure/SharePoint/CertificateProvider.cs create mode 100644 backend/Tnsu.ApprovalRoute.Infrastructure/SharePoint/SharePointClient.cs create mode 100644 backend/Tnsu.ApprovalRoute.Infrastructure/SharePoint/SharePointOptions.cs rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj (86%) rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/HttpTnsuPortalClient.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/ITnsuPortalClient.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalDtos.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalOptions.cs (100%) rename backend/{src => }/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalTokenProvider.cs (100%) delete mode 100644 backend/src/.vs/VSWorkspaceState.json delete mode 100644 backend/src/.vs/slnx.sqlite delete mode 100644 backend/src/.vs/src.slnx/FileContentIndex/c41e12f7-4a3d-4c40-9653-41a24091003e.vsidx delete mode 100644 backend/src/.vs/src.slnx/FileContentIndex/f9a35f81-13ae-4547-9684-90f51c188103.vsidx delete mode 100644 backend/src/.vs/src.slnx/config/applicationhost.config delete mode 100644 backend/src/.vs/src.slnx/v18/.wsuo delete mode 100644 backend/src/.vs/src.slnx/v18/DocumentLayout.backup.json delete mode 100644 backend/src/.vs/src.slnx/v18/DocumentLayout.json delete mode 100644 backend/src/Tnsu.ApprovalRoute.Api/Auth/AuthSchemes.cs delete mode 100644 backend/src/Tnsu.ApprovalRoute.Api/Auth/AuthSetup.cs delete mode 100644 backend/src/Tnsu.ApprovalRoute.Api/Dockerfile delete mode 100644 backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs delete mode 100644 backend/src/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs delete mode 100644 backend/src/Tnsu.ApprovalRoute.Domain/Entities/TemplateRouteStep.cs create mode 100644 backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/ReminderServiceTests.cs create mode 100644 frontend/src/webparts/approvalAdmin/utils/sla.test.ts create mode 100644 frontend/src/webparts/approvalAdmin/utils/sla.ts diff --git a/.gitea/workflows/deploy_backend.yml b/.gitea/workflows/deploy_backend.yml index 5da8b86..481a034 100644 --- a/.gitea/workflows/deploy_backend.yml +++ b/.gitea/workflows/deploy_backend.yml @@ -25,7 +25,7 @@ jobs: run: dotnet restore backend/Tnsu.ApprovalRoute.sln - name: Publish (local) - run: dotnet publish backend/src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj -c Release -o "$LOCAL_PUBLISH" + run: dotnet publish backend/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj -c Release -o "$LOCAL_PUBLISH" - name: Install cifs-utils run: | diff --git a/README.md b/README.md index 82c2206..05d4a9b 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,10 @@ backend/ Directory.Build.props # общие настройки таргета (net10.0, nullable, etc.) Directory.Packages.props # централизованные версии пакетов Tnsu.ApprovalRoute.sln - src/ - Tnsu.ApprovalRoute.Domain/ # сущности и enum'ы домена - Tnsu.ApprovalRoute.Application/ # сервисы, DTO, валидаторы - Tnsu.ApprovalRoute.Infrastructure/ # EF Core DbContext, миграции, клиент tnsu-portal - Tnsu.ApprovalRoute.Api/ # ASP.NET Core Controllers, Program.cs, авторизация + Tnsu.ApprovalRoute.Domain/ # сущности и enum'ы домена + Tnsu.ApprovalRoute.Application/ # сервисы, DTO, валидаторы + Tnsu.ApprovalRoute.Infrastructure/ # EF Core DbContext, миграции, клиент tnsu-portal + Tnsu.ApprovalRoute.Api/ # ASP.NET Core Controllers, Program.cs, авторизация tests/ Tnsu.ApprovalRoute.IntegrationTests/ frontend/ @@ -61,7 +60,7 @@ npm run build # heft test + package-solution --production ```bash cd backend -dotnet run --project src/Tnsu.ApprovalRoute.Api +dotnet run --project Tnsu.ApprovalRoute.Api ``` Требуется PostgreSQL (строка подключения — `ConnectionStrings:Postgres` в `appsettings.json`). diff --git a/backend/Directory.Packages.props b/backend/Directory.Packages.props index 64c8bfc..0ee4fae 100644 --- a/backend/Directory.Packages.props +++ b/backend/Directory.Packages.props @@ -3,6 +3,7 @@ true + diff --git a/backend/Tnsu.ApprovalRoute.Api/Auth/AuthSchemes.cs b/backend/Tnsu.ApprovalRoute.Api/Auth/AuthSchemes.cs new file mode 100644 index 0000000..27f8132 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Api/Auth/AuthSchemes.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; + +namespace Tnsu.ApprovalRoute.Api.Auth; + +public static class AuthSchemes +{ + /// Локальная разработка и тесты — Entra не настроен (см. AuthSetup). + public const string Dev = "Dev"; + + /// Токен пользователя (делегированный) — обычная работа админки. + public const string Bearer = JwtBearerDefaults.AuthenticationScheme; + + /// + /// Токен приложения (client_credentials) — вызовы без участия пользователя, + /// например Power Automate, который по расписанию забирает напоминания. + /// + public const string AppOnly = "AppOnly"; +} + +public static class AuthPolicies +{ + /// Только пользовательские токены. + public const string Delegated = "Delegated"; + + /// Только токены приложений. + public const string AppOnly = "AppOnlyPolicy"; + + /// Любой из двух — политика по умолчанию для всех контроллеров. + public const string Any = "AnyAuth"; +} diff --git a/backend/Tnsu.ApprovalRoute.Api/Auth/AuthSetup.cs b/backend/Tnsu.ApprovalRoute.Api/Auth/AuthSetup.cs new file mode 100644 index 0000000..5d82424 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Api/Auth/AuthSetup.cs @@ -0,0 +1,150 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; + +namespace Tnsu.ApprovalRoute.Api.Auth; + +public static class AuthSetup +{ + /// + /// Две схемы аутентификации вместо одной: помимо пользовательского токена + /// (админка) API должен принимать токен ПРИЛОЖЕНИЯ — эндпоинт напоминаний + /// вызывает Power Automate по расписанию, там нет ни пользователя, ни + /// интерактивного входа. + /// + /// Сделано по образцу api-tnsu-portal/AuthRegistration.cs, включая настройки + /// Entra — приложение регистрируется то же самое. + /// + public static IServiceCollection AddApprovalRouteAuth(this IServiceCollection services, IConfiguration configuration) + { + var tenantId = configuration["Entra:TenantId"]; + var audience = configuration["Entra:Audience"]; + + if (!string.IsNullOrWhiteSpace(tenantId) && !string.IsNullOrWhiteSpace(audience)) + AddEntraAuth(services, configuration, tenantId, audience); + else + AddDevAuth(services); + + return services; + } + + private static void AddEntraAuth( + IServiceCollection services, IConfiguration configuration, string tenantId, string audience) + { + // Белый список приложений, которым разрешён app-only доступ. Пустой список + // означает «не ограничивать» — иначе на пустой конфигурации сломались бы + // вообще все app-only вызовы, что при выкладке выглядит как необъяснимый 401. + var allowedAppIds = configuration.GetSection("Entra:AllowedAppIds").Get() ?? []; + + var validationParameters = () => new TokenValidationParameters + { + ValidAudiences = [audience, $"api://{audience}"], + ValidIssuers = + [ + $"https://login.microsoftonline.com/{tenantId}/v2.0", + $"https://sts.windows.net/{tenantId}/" + ], + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true + }; + + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = AuthSchemes.Bearer; + options.DefaultChallengeScheme = AuthSchemes.Bearer; + }) + .AddJwtBearer(AuthSchemes.Bearer, options => + { + options.Authority = $"https://login.microsoftonline.com/{tenantId}/v2.0"; + options.TokenValidationParameters = validationParameters(); + // Без этого JwtBearerHandler переименовывает короткие имена claim'ов + // из токена в длинные ClaimTypes-URI (upn → ClaimTypes.Upn, + // unique_name → ClaimTypes.Name — а НЕ ClaimTypes.Email!) ещё до + // того, как код увидит ClaimsPrincipal. CurrentUserAccessor ищет + // ровно сырые имена ("upn", "unique_name") — с ремаппингом они не + // совпадали ни с чем, и email инициатора не находился вообще + // (не только у токенов без claim'а email, как этот — у ЛЮБЫХ). + options.MapInboundClaims = false; + }); + + services.AddAuthentication() + .AddJwtBearer(AuthSchemes.AppOnly, options => + { + options.Authority = $"https://login.microsoftonline.com/{tenantId}/v2.0"; + options.TokenValidationParameters = validationParameters(); + options.MapInboundClaims = false; + options.Events = new JwtBearerEvents + { + // Подпись, издатель и аудитория у app-only токена те же, что у + // пользовательского, поэтому единственное, что реально отличает + // «свой» вызывающий сервис от любого приложения тенанта, — appid. + OnTokenValidated = context => + { + var claims = context.Principal?.Claims; + var appId = claims?.FirstOrDefault(c => c.Type == "appid")?.Value + ?? claims?.FirstOrDefault(c => c.Type == "azp")?.Value; + + if (allowedAppIds.Length > 0 && !allowedAppIds.Contains(appId)) + { + context.HttpContext.RequestServices + .GetRequiredService() + .CreateLogger("Auth.AppOnly") + .LogWarning("Приложение «{AppId}» отклонено — его нет в Entra:AllowedAppIds", appId); + context.Fail($"Приложение «{appId}» не входит в список разрешённых."); + } + + return Task.CompletedTask; + } + }; + }); + + services.AddAuthorization(options => + { + options.AddPolicy(AuthPolicies.Delegated, policy => + { + policy.AuthenticationSchemes.Add(AuthSchemes.Bearer); + policy.RequireAuthenticatedUser(); + }); + + options.AddPolicy(AuthPolicies.AppOnly, policy => + { + policy.AuthenticationSchemes.Add(AuthSchemes.AppOnly); + policy.RequireAuthenticatedUser(); + }); + + // RequireAssertion, а не RequireAuthenticatedUser: политика с двумя + // схемами падает, если ОДНА из них отвергла токен, даже когда вторая + // его приняла. Здесь достаточно, чтобы аутентифицировала любая. + options.AddPolicy(AuthPolicies.Any, policy => + { + policy.AddAuthenticationSchemes(AuthSchemes.Bearer, AuthSchemes.AppOnly); + policy.RequireAssertion(ctx => ctx.User.Identities.Any(i => i.IsAuthenticated)); + }); + + options.DefaultPolicy = options.GetPolicy(AuthPolicies.Any)!; + }); + } + + private static void AddDevAuth(IServiceCollection services) + { + services.AddAuthentication(AuthSchemes.Dev) + .AddScheme(AuthSchemes.Dev, _ => { }); + + // Те же имена политик, но поверх Dev-схемы: иначе контроллер с + // [Authorize(Policy = ...)] падал бы локально с «политика не найдена». + services.AddAuthorization(options => + { + foreach (var name in new[] { AuthPolicies.Delegated, AuthPolicies.AppOnly, AuthPolicies.Any }) + { + options.AddPolicy(name, policy => + { + policy.AuthenticationSchemes.Add(AuthSchemes.Dev); + policy.RequireAuthenticatedUser(); + }); + } + + options.DefaultPolicy = options.GetPolicy(AuthPolicies.Any)!; + }); + } +} diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Auth/CurrentUserAccessor.cs b/backend/Tnsu.ApprovalRoute.Api/Auth/CurrentUserAccessor.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Api/Auth/CurrentUserAccessor.cs rename to backend/Tnsu.ApprovalRoute.Api/Auth/CurrentUserAccessor.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Auth/DevAuthHandler.cs b/backend/Tnsu.ApprovalRoute.Api/Auth/DevAuthHandler.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Api/Auth/DevAuthHandler.cs rename to backend/Tnsu.ApprovalRoute.Api/Auth/DevAuthHandler.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Common/ApiResponse.cs b/backend/Tnsu.ApprovalRoute.Api/Common/ApiResponse.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Api/Common/ApiResponse.cs rename to backend/Tnsu.ApprovalRoute.Api/Common/ApiResponse.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Common/ResultExtensions.cs b/backend/Tnsu.ApprovalRoute.Api/Common/ResultExtensions.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Api/Common/ResultExtensions.cs rename to backend/Tnsu.ApprovalRoute.Api/Common/ResultExtensions.cs diff --git a/backend/Tnsu.ApprovalRoute.Api/Controllers/RemindersController.cs b/backend/Tnsu.ApprovalRoute.Api/Controllers/RemindersController.cs new file mode 100644 index 0000000..5e5d3e4 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Api/Controllers/RemindersController.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Tnsu.ApprovalRoute.Api.Auth; +using Tnsu.ApprovalRoute.Api.Common; +using Tnsu.ApprovalRoute.Application.Reminders; + +namespace Tnsu.ApprovalRoute.Api.Controllers; + +/// +/// Просроченные шаги согласования и исполнения на текущий момент. +/// +/// Политика — та же AnyAuth, что и у остальных контроллеров: эндпоинт вызывает +/// Power Automate по расписанию (токен приложения), но и из админки его удобно +/// открыть под пользователем, чтобы посмотреть, что сервис реально отдаёт. +/// +[ApiController] +[Authorize(Policy = AuthPolicies.Any)] +[Route("api/reminders")] +[Produces("application/json")] +public sealed class RemindersController(ReminderService service) : ControllerBase +{ + [HttpGet] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task List(CancellationToken ct) => + (await service.GetOverdueAsync(DateTimeOffset.UtcNow, ct)).ToActionResult(); +} diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Controllers/RequestTypesController.cs b/backend/Tnsu.ApprovalRoute.Api/Controllers/RequestTypesController.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Api/Controllers/RequestTypesController.cs rename to backend/Tnsu.ApprovalRoute.Api/Controllers/RequestTypesController.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRouteStepsController.cs b/backend/Tnsu.ApprovalRoute.Api/Controllers/TemplateRouteStepsController.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRouteStepsController.cs rename to backend/Tnsu.ApprovalRoute.Api/Controllers/TemplateRouteStepsController.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRoutesController.cs b/backend/Tnsu.ApprovalRoute.Api/Controllers/TemplateRoutesController.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRoutesController.cs rename to backend/Tnsu.ApprovalRoute.Api/Controllers/TemplateRoutesController.cs diff --git a/backend/Tnsu.ApprovalRoute.Api/Dockerfile b/backend/Tnsu.ApprovalRoute.Api/Dockerfile new file mode 100644 index 0000000..d32c8de --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Api/Dockerfile @@ -0,0 +1,21 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY Directory.Build.props Directory.Packages.props ./ +COPY Tnsu.ApprovalRoute.Domain/Tnsu.ApprovalRoute.Domain.csproj Tnsu.ApprovalRoute.Domain/ +COPY Tnsu.ApprovalRoute.Application/Tnsu.ApprovalRoute.Application.csproj Tnsu.ApprovalRoute.Application/ +COPY Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj Tnsu.ApprovalRoute.Infrastructure/ +COPY Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj Tnsu.ApprovalRoute.Api/ +RUN dotnet restore Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj +COPY Tnsu.ApprovalRoute.Domain/ Tnsu.ApprovalRoute.Domain/ +COPY Tnsu.ApprovalRoute.Application/ Tnsu.ApprovalRoute.Application/ +COPY Tnsu.ApprovalRoute.Infrastructure/ Tnsu.ApprovalRoute.Infrastructure/ +COPY Tnsu.ApprovalRoute.Api/ Tnsu.ApprovalRoute.Api/ +RUN dotnet publish Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj -c Release -o /app/publish /p:UseAppHost=false + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final +WORKDIR /app +RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* +COPY --from=build /app/publish . +ENV ASPNETCORE_URLS=http://+:8080 +EXPOSE 8080 +ENTRYPOINT ["dotnet", "Tnsu.ApprovalRoute.Api.dll"] diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Filters/ValidationActionFilter.cs b/backend/Tnsu.ApprovalRoute.Api/Filters/ValidationActionFilter.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Api/Filters/ValidationActionFilter.cs rename to backend/Tnsu.ApprovalRoute.Api/Filters/ValidationActionFilter.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Program.cs b/backend/Tnsu.ApprovalRoute.Api/Program.cs similarity index 95% rename from backend/src/Tnsu.ApprovalRoute.Api/Program.cs rename to backend/Tnsu.ApprovalRoute.Api/Program.cs index 12d7319..178f2f3 100644 --- a/backend/src/Tnsu.ApprovalRoute.Api/Program.cs +++ b/backend/Tnsu.ApprovalRoute.Api/Program.cs @@ -9,6 +9,7 @@ using Scalar.AspNetCore; using Tnsu.ApprovalRoute.Api.Auth; using Tnsu.ApprovalRoute.Api.Filters; using Tnsu.ApprovalRoute.Application; +using Tnsu.ApprovalRoute.Application.Reminders; using Tnsu.ApprovalRoute.Application.Common.Interfaces; using Tnsu.ApprovalRoute.Infrastructure; @@ -35,6 +36,7 @@ builder.Services.AddCors(options => }); builder.Services.AddApprovalRouteInfrastructure(builder.Configuration); builder.Services.AddApprovalRouteApplication(); +builder.Services.Configure(builder.Configuration.GetSection("Reminders")); builder.Services.AddApprovalRouteAuth(builder.Configuration); builder.Services.AddScoped(); builder.Services.AddOpenApi(options => diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Properties/launchSettings.json b/backend/Tnsu.ApprovalRoute.Api/Properties/launchSettings.json similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Api/Properties/launchSettings.json rename to backend/Tnsu.ApprovalRoute.Api/Properties/launchSettings.json diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj b/backend/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj rename to backend/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj diff --git a/backend/src/Tnsu.ApprovalRoute.Api/appsettings.Development.json b/backend/Tnsu.ApprovalRoute.Api/appsettings.Development.json similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Api/appsettings.Development.json rename to backend/Tnsu.ApprovalRoute.Api/appsettings.Development.json diff --git a/backend/src/Tnsu.ApprovalRoute.Api/appsettings.json b/backend/Tnsu.ApprovalRoute.Api/appsettings.json similarity index 71% rename from backend/src/Tnsu.ApprovalRoute.Api/appsettings.json rename to backend/Tnsu.ApprovalRoute.Api/appsettings.json index 12fe82b..8062415 100644 --- a/backend/src/Tnsu.ApprovalRoute.Api/appsettings.json +++ b/backend/Tnsu.ApprovalRoute.Api/appsettings.json @@ -10,7 +10,8 @@ }, "Entra": { "TenantId": "b4c2af35-bfd6-4cff-9af9-556efb53e65a", - "Audience": "9232d9e2-9807-4753-808f-07250e5474b0" + "Audience": "9232d9e2-9807-4753-808f-07250e5474b0", + "AllowedAppIds": [] }, "Dev": { "DefaultUserEmail": "dev@tnsu.kz" @@ -21,6 +22,12 @@ "https://localhost:4321" ] }, + "SharePoint": { + "TenantId": "b4c2af35-bfd6-4cff-9af9-556efb53e65a", + "ClientId": "9232d9e2-9807-4753-808f-07250e5474b0", + "CertificateSettingName": "AzurePfx", + "CertificatePassword": "Rk.xDjccnfyjdktybz1" + }, "TnsuPortal": { "BaseUrl": "https://api.tnsu.kz", "TenantId": "b4c2af35-bfd6-4cff-9af9-556efb53e65a", diff --git a/backend/src/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs b/backend/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs similarity index 88% rename from backend/src/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs rename to backend/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs index e466d09..3e5732c 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs +++ b/backend/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using System.Reflection; using FluentValidation; using Microsoft.Extensions.DependencyInjection; +using Tnsu.ApprovalRoute.Application.Reminders; using Tnsu.ApprovalRoute.Application.RequestTypes; using Tnsu.ApprovalRoute.Application.TemplateRoutes; using Tnsu.ApprovalRoute.Application.TemplateRouteSteps; @@ -11,6 +12,7 @@ public static class ApplicationServiceCollectionExtensions { public static IServiceCollection AddApprovalRouteApplication(this IServiceCollection services) { + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/backend/src/Tnsu.ApprovalRoute.Application/Common/Errors/Errors.cs b/backend/Tnsu.ApprovalRoute.Application/Common/Errors/Errors.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Application/Common/Errors/Errors.cs rename to backend/Tnsu.ApprovalRoute.Application/Common/Errors/Errors.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Application/Common/Interfaces/ICurrentUser.cs b/backend/Tnsu.ApprovalRoute.Application/Common/Interfaces/ICurrentUser.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Application/Common/Interfaces/ICurrentUser.cs rename to backend/Tnsu.ApprovalRoute.Application/Common/Interfaces/ICurrentUser.cs diff --git a/backend/Tnsu.ApprovalRoute.Application/Reminders/ApprovalRow.cs b/backend/Tnsu.ApprovalRoute.Application/Reminders/ApprovalRow.cs new file mode 100644 index 0000000..dacb884 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Application/Reminders/ApprovalRow.cs @@ -0,0 +1,115 @@ +using System.Globalization; +using System.Text.Json; + +namespace Tnsu.ApprovalRoute.Application.Reminders; + +/// +/// Строка списка «… — согласование», разобранная из ответа SharePoint. +/// Имена колонок — как в request-portal (FIELDS в approvalList.service.ts): +/// именно он эти списки и создаёт. +/// +internal sealed record ApprovalRow( + int Id, + int TicketId, + int StepOrder, + string Status, + string Phase, + DateTimeOffset? Date, + string? ApproverEmail, + int? SlaHours, + bool? AskOverdueReason) +{ + public static ApprovalRow? TryParse(JsonElement item) + { + // TicketId — Lookup на заявку; без него строку не с чем связать. + if (!TryGetInt(item, "TicketId", out var ticketId)) return null; + + var approver = item.TryGetProperty("TansuSiteApprover", out var a) && a.ValueKind == JsonValueKind.Object + ? GetString(a, "EMail") + : null; + + return new ApprovalRow( + TryGetInt(item, "Id", out var id) ? id : 0, + ticketId, + TryGetInt(item, "StepOrder", out var order) ? order : 0, + GetString(item, "ApprovalStatus") ?? string.Empty, + // Пустая фаза — старые строки, заведённые до появления колонки; + // трактуем как «Согласование» (так же поступает request-portal). + GetString(item, "Phase") is { Length: > 0 } phase ? phase : ApprovalPhases.Approval, + TryGetDate(item, "Date"), + approver, + TryGetInt(item, "SLAhours", out var sla) ? sla : null, + TryGetBool(item, "AskOverdueReason", out var ask) ? ask : null); + } + + private static string? GetString(JsonElement e, string name) => + e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null; + + /// + /// Целое из JSON — с поправкой на то, КАК SharePoint отдаёт Number-колонки. + /// + /// Одного TryGetInt32 здесь мало: у колонки типа Number значение приходит + /// дробным («1.0», а не «1»), а TryGetInt32 на числе с десятичной точкой + /// возвращает false — и поле молча становилось null. На фронтенде этой + /// проблемы нет вовсе: в JavaScript 1.0 и 1 — одно и то же число, поэтому + /// там те же данные читались верно, а на бэкенде терялись. + /// + /// Строку тоже принимаем: с odata=verbose (и у части полей) SharePoint + /// отдаёт числа строками. + /// + private static bool TryGetInt(JsonElement e, string name, out int value) + { + value = 0; + if (!e.TryGetProperty(name, out var v)) return false; + + if (v.ValueKind == JsonValueKind.Number) + { + if (v.TryGetInt32(out value)) return true; + if (!v.TryGetDouble(out var d) || d is < int.MinValue or > int.MaxValue) return false; + value = (int)Math.Round(d); + return true; + } + + if (v.ValueKind != JsonValueKind.String) return false; + if (!double.TryParse(v.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)) + return false; + if (parsed is < int.MinValue or > int.MaxValue) return false; + + value = (int)Math.Round(parsed); + return true; + } + + private static bool TryGetBool(JsonElement e, string name, out bool value) + { + value = false; + if (!e.TryGetProperty(name, out var v)) return false; + + if (v.ValueKind is JsonValueKind.True or JsonValueKind.False) + { + value = v.GetBoolean(); + return true; + } + + // Да/Нет-колонка тоже может прийти строкой («true»/«false») — см. TryGetInt. + return v.ValueKind == JsonValueKind.String && bool.TryParse(v.GetString(), out value); + } + + private static DateTimeOffset? TryGetDate(JsonElement e, string name) => + e.TryGetProperty(name, out var v) && + v.ValueKind == JsonValueKind.String && + DateTimeOffset.TryParse(v.GetString(), out var parsed) + ? parsed + : null; +} + +internal static class ApprovalPhases +{ + public const string Approval = "Согласование"; + public const string Execution = "Исполнение"; +} + +internal static class ApprovalStatuses +{ + public const string OnApproval = "На согласовании"; + public const string OnExecution = "На исполнении"; +} diff --git a/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderDto.cs b/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderDto.cs new file mode 100644 index 0000000..76a4ce4 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderDto.cs @@ -0,0 +1,20 @@ +namespace Tnsu.ApprovalRoute.Application.Reminders; + +/// Согласующий/исполнитель из строки списка — кому напоминать. +/// +/// На сколько часов просрочен шаг: сейчас минус (активация шага + SLA). +/// Выходные не учитываются — считаются календарные часы. +/// +/// Ссылка на заявку, требующую решения. +/// +/// Спрашивать ли у пользователя причину просрочки. Значение уже с применённым +/// наследованием (шаг перекрывает тип заявки). +/// +public sealed record ReminderDto( + string Email, + int HoursOverdue, + string Url, + bool AskOverdueReason, + int RequestTypeId, + string RequestTypeName, + int RequestId); diff --git a/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderService.cs b/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderService.cs new file mode 100644 index 0000000..9c68e35 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderService.cs @@ -0,0 +1,261 @@ +using System.Text.Json; +using FluentResults; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Tnsu.ApprovalRoute.Domain.Entities; +using Tnsu.ApprovalRoute.Infrastructure.Persistence; +using Tnsu.ApprovalRoute.Infrastructure.SharePoint; + +namespace Tnsu.ApprovalRoute.Application.Reminders; + +/// +/// Кто прямо сейчас просрочил свой шаг согласования или исполнения. +/// +/// Срез «на текущий момент», без памяти о том, что уже отправлено: повторный +/// вызов вернёт ту же заявку, пока по ней не примут решение. Так и задумано — +/// рассылкой и защитой от повторов занимается вызывающая сторона (Power +/// Automate по расписанию). +/// +public sealed class ReminderService( + ApprovalRouteDbContext db, + ISharePointClient sharePoint, + IOptions options, + ILogger logger) +{ + // Ограничение на длину OData-фильтра: Id заявок подставляются пачками, иначе + // на большом списке получится неподъёмной длины URL. + private const int IdChunkSize = 50; + + // $filter по Choice-колонке — сравнение на равенство, а не текстовый поиск, + // поэтому кавычки вокруг значения достаточно (в самих статусах нет апострофов). + private const string ActiveStatusFilter = + $"$filter=ApprovalStatus eq '{ApprovalStatuses.OnApproval}' or ApprovalStatus eq '{ApprovalStatuses.OnExecution}'"; + + public async Task>> GetOverdueAsync( + DateTimeOffset now, CancellationToken cancellationToken) + { + // Точка входа — типы заявок с настроенным SLA. Тип без SLA не участвует + // в напоминаниях вообще, даже если у его шагов SLA задан: включение + // напоминаний — решение уровня типа заявки. + var types = await db.RequestTypes.AsNoTracking() + .Where(x => x.SlaHours != null && x.ListId != null && x.SiteUrl != null) + .ToListAsync(cancellationToken); + + var reminders = new List(); + + foreach (var type in types) + { + try + { + reminders.AddRange(await GetOverdueForTypeAsync(type, now, cancellationToken)); + } + catch (Exception e) + { + // Недоступный список одного типа не должен обнулять напоминания по + // всем остальным: Power Automate получит то, что удалось собрать. + logger.LogError(e, "Напоминания по типу заявки «{Type}» не собраны", type.Name); + } + } + + return Result.Ok>(reminders); + } + + private async Task> GetOverdueForTypeAsync( + RequestType type, DateTimeOffset now, CancellationToken cancellationToken) + { + var siteUrl = type.SiteUrl!; + var dataListTitle = await sharePoint.GetListTitleAsync(siteUrl, type.ListId!, cancellationToken); + if (string.IsNullOrWhiteSpace(dataListTitle)) + { + logger.LogWarning("Список данных типа «{Type}» не найден по url {Url}", type.Name, type.ListId); + return []; + } + + var approvalListTitle = ApprovalListTitleFor(dataListTitle); + if (approvalListTitle is null) + { + logger.LogWarning( + "Список «{List}» назван не по конвенции «… — данные», список согласования для него не найти", + dataListTitle); + return []; + } + + // Шаг 1. Активные строки — прямым $filter по полю ApprovalStatus. + // + // Ранее здесь стоял Search по managed property (ApprovalStatusOWSCHCS) — + // тот же приём, что в request-portal (см. MP_STATUS). На практике он + // строки не нашёл: индекс мог не подхватить именно эту managed property + // для этого списка (она формируется автоматически, и после провижининга + // колонок SLA переиндексация могла ещё не пройти) — сама причина не + // подтверждена, а вот результат — нет. Прямой $filter по полю сработал: + // он не отстаёт от реального состояния списка (не зависит от индексации + // вовсе), и подтверждённой на практике проблемы с ним на этом списке нет. + var active = await ReadRowsAsync(siteUrl, approvalListTitle, ActiveStatusFilter, cancellationToken); + if (active.Count == 0) return []; + + // Шаг 2. По заявкам с активными строками нужны ВСЕ строки маршрута — + // дата активации шага выводится из дат решений предыдущего шага. + var ticketIds = active.Select(r => r.TicketId).Distinct().ToList(); + var allRows = await ReadRowsByTicketsAsync(siteUrl, approvalListTitle, ticketIds, cancellationToken); + var rowsByTicket = allRows.GroupBy(r => r.TicketId).ToDictionary(g => g.Key, g => g.ToList()); + + var createdByTicket = await ReadCreatedAsync(siteUrl, type.ListId!, ticketIds, cancellationToken); + + var reminders = new List(); + foreach (var row in active) + { + if (string.IsNullOrWhiteSpace(row.ApproverEmail)) continue; + if (!rowsByTicket.TryGetValue(row.TicketId, out var ticketRows)) continue; + + createdByTicket.TryGetValue(row.TicketId, out var created); + var activatedAt = ActivatedAt(row, ticketRows, created); + if (activatedAt is null) + { + logger.LogWarning( + "Не удалось определить дату активации строки {Row} заявки {Ticket} — пропущена", + row.Id, row.TicketId); + continue; + } + + // SLA строки перекрывает типовой. У строк, созданных до появления + // колонки, значения нет — тогда работает SLA типа заявки. + var slaHours = row.SlaHours ?? type.SlaHours!.Value; + var deadline = activatedAt.Value.AddHours(slaHours); + if (deadline >= now) continue; + + reminders.Add(new ReminderDto( + row.ApproverEmail!, + (int)Math.Floor((now - deadline).TotalHours), + BuildUrl(type, row), + row.AskOverdueReason ?? type.AskOverdueReason ?? false, + type.Id, + type.Name, + row.TicketId)); + } + + return reminders; + } + + /// + /// Момент, с которого шаг считается активным, — начало отсчёта SLA. + /// + /// Отдельной колонки «когда шаг стал активным» в списке нет (активация — это + /// точечное обновление статуса, дату оно не пишет), поэтому момент выводится + /// из соседних строк: + /// • первый шаг согласования — дата создания заявки; + /// • первый шаг исполнения — последнее решение по фазе согласования + /// (фаза исполнения активируется только когда согласование пройдено целиком); + /// • остальные шаги — последнее решение по предыдущему шагу СВОЕЙ фазы. + /// + /// Везде именно MAX даты, а не «дата предыдущей строки»: шаг могут делить + /// несколько параллельных согласующих с одинаковым StepOrder, и следующий шаг + /// активируется только после решения последнего из них. + /// + /// «Предыдущий шаг» ищется как ближайший меньший существующий StepOrder, а не + /// как order-1: в нумерации бывают пропуски (шаг удалили, шаг не подошёл по + /// условию появления). + /// + private static DateTimeOffset? ActivatedAt( + ApprovalRow row, IReadOnlyList ticketRows, DateTimeOffset? requestCreated) + { + var samePhase = ticketRows.Where(r => r.Phase == row.Phase).ToList(); + var isFirstInPhase = samePhase.All(r => r.StepOrder >= row.StepOrder); + + if (isFirstInPhase) + { + return row.Phase == ApprovalPhases.Execution + ? LastDecisionOf(ticketRows.Where(r => r.Phase == ApprovalPhases.Approval)) + : requestCreated; + } + + var previousOrder = samePhase + .Where(r => r.StepOrder < row.StepOrder) + .Max(r => r.StepOrder); + + return LastDecisionOf(samePhase.Where(r => r.StepOrder == previousOrder)); + } + + private static DateTimeOffset? LastDecisionOf(IEnumerable rows) + { + var dates = rows.Select(r => r.Date).Where(d => d is not null).Select(d => d!.Value).ToList(); + return dates.Count == 0 ? null : dates.Max(); + } + + /// + /// Ссылка ведёт в тот раздел портала, которому принадлежит просроченный шаг: + /// исполнителя незачем высаживать в «На согласовании». Сама заявка всё равно + /// откроется панелью поверх раздела (rpmode=approve). + /// + private string BuildUrl(RequestType type, ApprovalRow row) => + options.Value.RequestUrlTemplate + .Replace("{siteUrl}", type.SiteUrl!.TrimEnd('/')) + .Replace("{pageUrl}", type.PageUrl) + .Replace("{requestId}", row.TicketId.ToString()) + .Replace("{nav}", row.Phase == ApprovalPhases.Execution ? "execution" : "approvals"); + + /// «Заявка на приём — данные» → «Заявка на приём — согласование». + private static string? ApprovalListTitleFor(string dataListTitle) + { + var index = dataListTitle.LastIndexOf('—'); + if (index < 0) return null; + + var suffix = dataListTitle[(index + 1)..].Trim(); + return suffix.Equals("данные", StringComparison.OrdinalIgnoreCase) + ? $"{dataListTitle[..index].TrimEnd()} — согласование" + : null; + } + + private const string RowSelect = + "$select=Id,ApprovalStatus,StepOrder,Phase,Date,SLAhours,AskOverdueReason,TicketId," + + "TansuSiteApprover/EMail&$expand=TansuSiteApprover&$top=5000"; + + private async Task> ReadRowsAsync( + string siteUrl, string listTitle, string filter, CancellationToken cancellationToken) + { + var items = await sharePoint.GetItemsByListTitleAsync( + siteUrl, listTitle, $"{RowSelect}&{filter}", cancellationToken); + + return Parse(items); + } + + private async Task> ReadRowsByTicketsAsync( + string siteUrl, string listTitle, IReadOnlyList ticketIds, CancellationToken cancellationToken) + { + var rows = new List(); + foreach (var chunk in ticketIds.Chunk(IdChunkSize)) + { + var filter = "$filter=" + string.Join(" or ", chunk.Select(id => $"TicketId eq {id}")); + rows.AddRange(await ReadRowsAsync(siteUrl, listTitle, filter, cancellationToken)); + } + + return rows; + } + + /// Даты создания заявок — начало отсчёта для первого шага согласования. + private async Task> ReadCreatedAsync( + string siteUrl, string listUrl, IReadOnlyList ids, CancellationToken cancellationToken) + { + var created = new Dictionary(); + + foreach (var chunk in ids.Chunk(IdChunkSize)) + { + var query = "$select=Id,Created&$top=5000&$filter=" + + string.Join(" or ", chunk.Select(id => $"Id eq {id}")); + + var items = await sharePoint.GetItemsByListUrlAsync(siteUrl, listUrl, query, cancellationToken); + foreach (var item in items) + { + if (!item.TryGetProperty("Id", out var idValue) || !idValue.TryGetInt32(out var id)) continue; + if (!item.TryGetProperty("Created", out var createdValue)) continue; + if (createdValue.ValueKind != JsonValueKind.String) continue; + if (DateTimeOffset.TryParse(createdValue.GetString(), out var parsed)) created[id] = parsed; + } + } + + return created; + } + + private static List Parse(IEnumerable items) => + items.Select(ApprovalRow.TryParse).Where(r => r is not null).Select(r => r!).ToList(); +} diff --git a/backend/Tnsu.ApprovalRoute.Application/Reminders/RemindersOptions.cs b/backend/Tnsu.ApprovalRoute.Application/Reminders/RemindersOptions.cs new file mode 100644 index 0000000..54ff0ac --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Application/Reminders/RemindersOptions.cs @@ -0,0 +1,21 @@ +namespace Tnsu.ApprovalRoute.Application.Reminders; + +public sealed class RemindersOptions +{ + /// + /// Шаблон ссылки на заявку. Подстановки: {siteUrl}, {pageUrl}, {requestId}, {nav}. + /// + /// Портал — одностраничный и живёт в КОРНЕ сайта, отдельного пути к странице + /// нет; всё состояние передаётся параметрами запроса (см. shared/routing.ts + /// в request-portal). Пример живой ссылки: + /// https://tnsukz.sharepoint.com/sites/requests?rpnav=approvals&rpmode=approve&rptype=it-1c-ticket&rpid=11 + /// + /// {pageUrl} — это PageUrl типа заявки, то есть КЛЮЧ ФОРМЫ (it-1c-ticket), а + /// не путь к странице: в портале он и подставляется в rptype. + /// + /// Вынесено в конфигурацию, чтобы смена разметки ссылок в портале правилась + /// настройкой, а не пересборкой сервиса. + /// + public string RequestUrlTemplate { get; set; } = + "{siteUrl}?rpnav={nav}&rpmode=approve&rptype={pageUrl}&rpid={requestId}"; +} diff --git a/backend/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs b/backend/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs new file mode 100644 index 0000000..b8992e4 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs @@ -0,0 +1,42 @@ +namespace Tnsu.ApprovalRoute.Application.RequestTypes; + +// SlaHours/AskOverdueReason добавлены в конец с значением по умолчанию null — +// оба поля необязательны, и старые клиенты, не знающие о них, продолжают +// работать без изменений. null здесь значит «не настроено», а не «ноль»: +// см. RequestType.SlaHours. +public sealed record RequestTypeDto( + int Id, + string Name, + string PageUrl, + string? ListId, + string? SiteUrl, + string Section, + int? SlaHours = null, + bool? AskOverdueReason = null); + +public sealed record CreateRequestTypeRequest( + string Name, + string PageUrl, + string? ListId, + string? SiteUrl, + string Section, + int? SlaHours = null, + bool? AskOverdueReason = null); + +public sealed record UpdateRequestTypeRequest( + string Name, + string PageUrl, + string? ListId, + string? SiteUrl, + string Section, + int? SlaHours = null, + bool? AskOverdueReason = null); + +public sealed record PatchRequestTypeRequest( + string? Name, + string? PageUrl, + string? ListId, + string? SiteUrl, + string? Section, + int? SlaHours = null, + bool? AskOverdueReason = null); diff --git a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs b/backend/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs similarity index 81% rename from backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs rename to backend/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs index 296f600..fa58fa8 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs +++ b/backend/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs @@ -12,7 +12,7 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db) { var items = await db.RequestTypes.AsNoTracking() .OrderBy(x => x.Name) - .Select(x => new RequestTypeDto(x.Id, x.Name, x.PageUrl, x.ListId, x.SiteUrl, x.Section)) + .Select(x => new RequestTypeDto(x.Id, x.Name, x.PageUrl, x.ListId, x.SiteUrl, x.Section, x.SlaHours, x.AskOverdueReason)) .ToListAsync(cancellationToken); return Result.Ok>(items); @@ -39,7 +39,9 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db) PageUrl = request.PageUrl, ListId = request.ListId, SiteUrl = request.SiteUrl, - Section = request.Section + Section = request.Section, + SlaHours = request.SlaHours, + AskOverdueReason = request.AskOverdueReason }; db.RequestTypes.Add(entity); await db.SaveChangesAsync(cancellationToken); @@ -62,6 +64,8 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db) entity.ListId = request.ListId; entity.SiteUrl = request.SiteUrl; entity.Section = request.Section; + entity.SlaHours = request.SlaHours; + entity.AskOverdueReason = request.AskOverdueReason; await db.SaveChangesAsync(cancellationToken); return Result.Ok(ToDto(entity)); @@ -85,6 +89,11 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db) entity.ListId = listId; entity.SiteUrl = siteUrl; entity.Section = request.Section ?? entity.Section; + // PATCH здесь традиционно трактует null как «не менять» (как и остальные + // поля выше), поэтому сбросить SLA обратно в «наследовать» можно только + // через PUT — там значение записывается как есть. + entity.SlaHours = request.SlaHours ?? entity.SlaHours; + entity.AskOverdueReason = request.AskOverdueReason ?? entity.AskOverdueReason; await db.SaveChangesAsync(cancellationToken); return Result.Ok(ToDto(entity)); @@ -102,5 +111,6 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db) return Result.Ok(); } - private static RequestTypeDto ToDto(RequestType e) => new(e.Id, e.Name, e.PageUrl, e.ListId, e.SiteUrl, e.Section); + private static RequestTypeDto ToDto(RequestType e) => + new(e.Id, e.Name, e.PageUrl, e.ListId, e.SiteUrl, e.Section, e.SlaHours, e.AskOverdueReason); } diff --git a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeValidators.cs b/backend/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeValidators.cs similarity index 64% rename from backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeValidators.cs rename to backend/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeValidators.cs index 8a47466..c830a20 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeValidators.cs +++ b/backend/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeValidators.cs @@ -12,6 +12,12 @@ public sealed class CreateRequestTypeRequestValidator : AbstractValidator x) .Must(x => RequestTypeFields.IsListReferenceValid(x.ListId, x.SiteUrl)) .WithMessage("Id списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе."); + // Отрицательный SLA сделал бы шаг просроченным уже в момент активации; + // ноль допустим — это осознанное «напоминать сразу». + RuleFor(x => x.SlaHours) + .GreaterThanOrEqualTo(0) + .When(x => x.SlaHours is not null) + .WithMessage("SLA в часах не может быть отрицательным."); } } @@ -25,6 +31,12 @@ public sealed class UpdateRequestTypeRequestValidator : AbstractValidator x) .Must(x => RequestTypeFields.IsListReferenceValid(x.ListId, x.SiteUrl)) .WithMessage("Id списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе."); + // Отрицательный SLA сделал бы шаг просроченным уже в момент активации; + // ноль допустим — это осознанное «напоминать сразу». + RuleFor(x => x.SlaHours) + .GreaterThanOrEqualTo(0) + .When(x => x.SlaHours is not null) + .WithMessage("SLA в часах не может быть отрицательным."); } } @@ -38,6 +50,12 @@ public sealed class PatchRequestTypeRequestValidator : AbstractValidator x.PageUrl).NotEmpty().WithMessage("Url страницы не может быть пустым.")); When(x => x.Section is not null, () => RuleFor(x => x.Section).NotEmpty().WithMessage("Раздел не может быть пустым.")); + // Отрицательный SLA сделал бы шаг просроченным уже в момент активации; + // ноль допустим — это осознанное «напоминать сразу». + RuleFor(x => x.SlaHours) + .GreaterThanOrEqualTo(0) + .When(x => x.SlaHours is not null) + .WithMessage("SLA в часах не может быть отрицательным."); } } diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepDto.cs b/backend/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepDto.cs similarity index 62% rename from backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepDto.cs rename to backend/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepDto.cs index 200c375..5736e17 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepDto.cs +++ b/backend/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepDto.cs @@ -2,6 +2,11 @@ using Tnsu.ApprovalRoute.Domain.Enums; namespace Tnsu.ApprovalRoute.Application.TemplateRouteSteps; +// SlaHours/AskOverdueReason у шага перекрывают одноимённые поля типа заявки. +// null — «не задано, наследовать», поэтому оба поля nullable и необязательны +// (0 часов и false — это осознанный выбор, а не «пусто»). Добавлены в конец +// записей, чтобы не ломать существующие позиционные вызовы. + public sealed record TemplateRouteStepDto( int Id, int OrderNo, @@ -15,7 +20,9 @@ public sealed record TemplateRouteStepDto( string? FieldName, ComparisonOperator? Operation, string? Value, - string? Warning = null); + string? Warning = null, + int? SlaHours = null, + bool? AskOverdueReason = null); public sealed record CreateTemplateRouteStepRequest( int OrderNo, @@ -27,7 +34,9 @@ public sealed record CreateTemplateRouteStepRequest( bool AllowReassignToOtherApprovers, string? FieldName, ComparisonOperator? Operation, - string? Value); + string? Value, + int? SlaHours = null, + bool? AskOverdueReason = null); public sealed record UpdateTemplateRouteStepRequest( int OrderNo, @@ -39,7 +48,9 @@ public sealed record UpdateTemplateRouteStepRequest( bool AllowReassignToOtherApprovers, string? FieldName, ComparisonOperator? Operation, - string? Value); + string? Value, + int? SlaHours = null, + bool? AskOverdueReason = null); public sealed record PatchTemplateRouteStepRequest( int? OrderNo, @@ -51,4 +62,6 @@ public sealed record PatchTemplateRouteStepRequest( bool? AllowReassignToOtherApprovers, string? FieldName, ComparisonOperator? Operation, - string? Value); + string? Value, + int? SlaHours = null, + bool? AskOverdueReason = null); diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs b/backend/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs similarity index 95% rename from backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs rename to backend/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs index 8d6c11a..516efa9 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs +++ b/backend/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs @@ -16,7 +16,7 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) .OrderBy(x => x.OrderNo) .Select(x => new TemplateRouteStepDto( x.Id, x.OrderNo, x.ExecutorEmail, x.Position, x.ProjectRole, x.HeadType, x.Status, x.RouteId, x.AllowReassignToOtherApprovers, - x.FieldName, x.Operation, x.Value)) + x.FieldName, x.Operation, x.Value, null, x.SlaHours, x.AskOverdueReason)) .ToListAsync(cancellationToken); return Result.Ok>(items); @@ -73,7 +73,9 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) AllowReassignToOtherApprovers = r.AllowReassignToOtherApprovers, FieldName = r.FieldName, Operation = r.Operation, - Value = r.Value + Value = r.Value, + SlaHours = r.SlaHours, + AskOverdueReason = r.AskOverdueReason }; db.TemplateRouteSteps.Add(entity); await db.SaveChangesAsync(cancellationToken); @@ -125,6 +127,8 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) entity.FieldName = r.FieldName; entity.Operation = r.Operation; entity.Value = r.Value; + entity.SlaHours = r.SlaHours; + entity.AskOverdueReason = r.AskOverdueReason; await db.SaveChangesAsync(cancellationToken); return Result.Ok(await ToDtoWithWarningAsync(entity, route, cancellationToken)); @@ -190,6 +194,10 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) entity.FieldName = fieldName; entity.Operation = operation; entity.Value = value; + // Как и остальные поля в PATCH, null значит «не менять». Вернуть шаг к + // наследованию SLA от типа заявки можно через PUT. + entity.SlaHours = r.SlaHours ?? entity.SlaHours; + entity.AskOverdueReason = r.AskOverdueReason ?? entity.AskOverdueReason; await db.SaveChangesAsync(cancellationToken); return Result.Ok(await ToDtoWithWarningAsync(entity, route, cancellationToken)); @@ -281,5 +289,5 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) private static TemplateRouteStepDto ToDto(TemplateRouteStep e) => new( e.Id, e.OrderNo, e.ExecutorEmail, e.Position, e.ProjectRole, e.HeadType, e.Status, e.RouteId, e.AllowReassignToOtherApprovers, - e.FieldName, e.Operation, e.Value); + e.FieldName, e.Operation, e.Value, null, e.SlaHours, e.AskOverdueReason); } diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepValidators.cs b/backend/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepValidators.cs similarity index 76% rename from backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepValidators.cs rename to backend/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepValidators.cs index 1c4ac4b..577edf1 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepValidators.cs +++ b/backend/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepValidators.cs @@ -14,6 +14,12 @@ public sealed class CreateTemplateRouteStepRequestValidator : AbstractValidator< RuleFor(x => x) .Must(x => TemplateRouteStepFields.IsConditionValid(x.FieldName, x.Operation, x.Value)) .WithMessage("Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе."); + // Отрицательный SLA сделал бы шаг просроченным уже в момент активации; + // ноль допустим — это осознанное «напоминать сразу». + RuleFor(x => x.SlaHours) + .GreaterThanOrEqualTo(0) + .When(x => x.SlaHours is not null) + .WithMessage("SLA в часах не может быть отрицательным."); } } @@ -28,6 +34,12 @@ public sealed class UpdateTemplateRouteStepRequestValidator : AbstractValidator< RuleFor(x => x) .Must(x => TemplateRouteStepFields.IsConditionValid(x.FieldName, x.Operation, x.Value)) .WithMessage("Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе."); + // Отрицательный SLA сделал бы шаг просроченным уже в момент активации; + // ноль допустим — это осознанное «напоминать сразу». + RuleFor(x => x.SlaHours) + .GreaterThanOrEqualTo(0) + .When(x => x.SlaHours is not null) + .WithMessage("SLA в часах не может быть отрицательным."); } } diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs b/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs similarity index 59% rename from backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs rename to backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs index ceb8cd6..1aba714 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs +++ b/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs @@ -2,13 +2,26 @@ using Tnsu.ApprovalRoute.Domain.Enums; namespace Tnsu.ApprovalRoute.Application.TemplateRoutes; +/// +/// ДЕЙСТВУЮЩИЙ SLA шага в часах — наследование уже применено: значение шага, а +/// если на шаге не задано, то значение типа заявки. null означает, что SLA не +/// настроен нигде, то есть напоминаний по этому шагу не будет. +/// Резолв отдаёт именно итоговое значение, чтобы вызывающая сторона +/// (request-portal) просто записала его в строку списка согласования и не +/// повторяла у себя правило наследования. +/// +/// +/// Спрашивать ли причину просрочки — так же с уже применённым наследованием. +/// public sealed record ResolvedRouteStepDto( int Id, int OrderNo, string? ExecutorEmail, string? Position, bool AllowReassignToOtherApprovers, - string? Warning = null); + string? Warning = null, + int? SlaHours = null, + bool? AskOverdueReason = null); public sealed record ResolveTemplateRouteResultDto( int RouteId, diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteDto.cs b/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteDto.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteDto.cs rename to backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteDto.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs b/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs similarity index 91% rename from backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs rename to backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs index 8bdbaec..3d210d4 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs +++ b/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs @@ -129,8 +129,11 @@ public sealed class TemplateRouteService( return Result.Fail( new ValidationError("Не удалось определить email инициатора: не передан явно и отсутствует в токене.")); - var requestTypeExists = await db.RequestTypes.AnyAsync(x => x.Id == request.RequestTypeId, cancellationToken); - if (!requestTypeExists) + // Сам тип заявки, а не только факт его существования: из него берутся + // значения SLA по умолчанию для шагов (см. ResolveStepAsync). + var requestType = await db.RequestTypes.AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == request.RequestTypeId, cancellationToken); + if (requestType is null) return Result.Fail(new NotFoundError("Тип заявки", request.RequestTypeId)); var employee = await portalClient.GetEmployeeByEmailAsync(email, cancellationToken); @@ -168,7 +171,8 @@ public sealed class TemplateRouteService( var steps = new List(); foreach (var step in matchingSteps) - steps.Add(await ResolveStepAsync(step, email, employee.Company, projectExecutors, request.DepartmentId, cancellationToken)); + steps.Add(await ResolveStepAsync( + step, email, employee.Company, projectExecutors, request.DepartmentId, requestType, cancellationToken)); var collapsedSteps = CollapseByExecutorEmail(steps); @@ -212,7 +216,8 @@ public sealed class TemplateRouteService( private async Task ResolveStepAsync( TemplateRouteStep step, string initiatorEmail, string initiatorCompany, - IReadOnlyList projectExecutors, Guid? departmentId, CancellationToken cancellationToken) + IReadOnlyList projectExecutors, Guid? departmentId, RequestType requestType, + CancellationToken cancellationToken) { string? resolvedEmail; string? warning = null; @@ -277,7 +282,19 @@ public sealed class TemplateRouteService( resolvedPosition = resolvedEmployee?.Position; } - return new ResolvedRouteStepDto(step.Id, step.OrderNo, resolvedEmail, resolvedPosition, step.AllowReassignToOtherApprovers, warning); + // Наследование SLA применяется ЗДЕСЬ, а не у вызывающей стороны: `??` + // работает именно так, как задумано — значение шага перекрывает типовое, + // и при этом заданный на шаге 0 остаётся нулём, а не подменяется типовым + // (0 — осознанное «напоминать сразу», см. TemplateRouteStep.SlaHours). + return new ResolvedRouteStepDto( + step.Id, + step.OrderNo, + resolvedEmail, + resolvedPosition, + step.AllowReassignToOtherApprovers, + warning, + step.SlaHours ?? requestType.SlaHours, + step.AskOverdueReason ?? requestType.AskOverdueReason); } /// diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteValidators.cs b/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteValidators.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteValidators.cs rename to backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteValidators.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Application/Tnsu.ApprovalRoute.Application.csproj b/backend/Tnsu.ApprovalRoute.Application/Tnsu.ApprovalRoute.Application.csproj similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Application/Tnsu.ApprovalRoute.Application.csproj rename to backend/Tnsu.ApprovalRoute.Application/Tnsu.ApprovalRoute.Application.csproj diff --git a/backend/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs b/backend/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs new file mode 100644 index 0000000..70db213 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs @@ -0,0 +1,29 @@ +namespace Tnsu.ApprovalRoute.Domain.Entities; + +public class RequestType +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string PageUrl { get; set; } = string.Empty; + public string? ListId { get; set; } + public string? SiteUrl { get; set; } + public string Section { get; set; } = string.Empty; + + /// + /// SLA по умолчанию для ВСЕХ шагов заявок этого типа, в часах. Отсчёт идёт от + /// активации шага, а не от создания заявки (см. эндпоинт напоминаний). + /// null — SLA не настроен, напоминания по этому типу не строятся. + /// Значение на шаге () перекрывает это. + /// + public int? SlaHours { get; set; } + + /// + /// Спрашивать ли у согласующего причину просрочки — значение по умолчанию для + /// всех шагов заявок этого типа. null — не задано. + /// Отдаётся в ответе эндпоинта напоминаний, чтобы вызывающая сторона решила, + /// запрашивать причину или просто напомнить. + /// + public bool? AskOverdueReason { get; set; } + + public ICollection Routes { get; set; } = new List(); +} diff --git a/backend/src/Tnsu.ApprovalRoute.Domain/Entities/TemplateRoute.cs b/backend/Tnsu.ApprovalRoute.Domain/Entities/TemplateRoute.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Domain/Entities/TemplateRoute.cs rename to backend/Tnsu.ApprovalRoute.Domain/Entities/TemplateRoute.cs diff --git a/backend/Tnsu.ApprovalRoute.Domain/Entities/TemplateRouteStep.cs b/backend/Tnsu.ApprovalRoute.Domain/Entities/TemplateRouteStep.cs new file mode 100644 index 0000000..665bd01 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Domain/Entities/TemplateRouteStep.cs @@ -0,0 +1,39 @@ +using Tnsu.ApprovalRoute.Domain.Enums; + +namespace Tnsu.ApprovalRoute.Domain.Entities; + +public class TemplateRouteStep +{ + public int Id { get; set; } + public int OrderNo { get; set; } + + public string? ExecutorEmail { get; set; } + public string? Position { get; set; } + public ProjectRole? ProjectRole { get; set; } + public HeadType? HeadType { get; set; } + + public RouteStepStatus Status { get; set; } = RouteStepStatus.Approval; + public int RouteId { get; set; } + public bool AllowReassignToOtherApprovers { get; set; } + + /// + /// SLA этого шага в часах — перекрывает . + /// null означает ИМЕННО «не задано, наследовать от типа заявки», а 0 — + /// осознанное «напоминать сразу», поэтому поле обязано быть nullable и не + /// должно получать значение по умолчанию. + /// + public int? SlaHours { get; set; } + + /// + /// Спрашивать ли причину просрочки на этом шаге — перекрывает + /// . Здесь тоже три состояния: + /// null — наследовать от типа, true/false — явный выбор. + /// + public bool? AskOverdueReason { get; set; } + + public string? FieldName { get; set; } + public ComparisonOperator? Operation { get; set; } + public string? Value { get; set; } + + public TemplateRoute? Route { get; set; } +} diff --git a/backend/src/Tnsu.ApprovalRoute.Domain/Enums/ComparisonOperator.cs b/backend/Tnsu.ApprovalRoute.Domain/Enums/ComparisonOperator.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Domain/Enums/ComparisonOperator.cs rename to backend/Tnsu.ApprovalRoute.Domain/Enums/ComparisonOperator.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Domain/Enums/HeadType.cs b/backend/Tnsu.ApprovalRoute.Domain/Enums/HeadType.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Domain/Enums/HeadType.cs rename to backend/Tnsu.ApprovalRoute.Domain/Enums/HeadType.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Domain/Enums/ProjectRole.cs b/backend/Tnsu.ApprovalRoute.Domain/Enums/ProjectRole.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Domain/Enums/ProjectRole.cs rename to backend/Tnsu.ApprovalRoute.Domain/Enums/ProjectRole.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Domain/Enums/RouteStepStatus.cs b/backend/Tnsu.ApprovalRoute.Domain/Enums/RouteStepStatus.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Domain/Enums/RouteStepStatus.cs rename to backend/Tnsu.ApprovalRoute.Domain/Enums/RouteStepStatus.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Domain/Tnsu.ApprovalRoute.Domain.csproj b/backend/Tnsu.ApprovalRoute.Domain/Tnsu.ApprovalRoute.Domain.csproj similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Domain/Tnsu.ApprovalRoute.Domain.csproj rename to backend/Tnsu.ApprovalRoute.Domain/Tnsu.ApprovalRoute.Domain.csproj diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/InfrastructureServiceCollectionExtensions.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/InfrastructureServiceCollectionExtensions.cs similarity index 72% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/InfrastructureServiceCollectionExtensions.cs rename to backend/Tnsu.ApprovalRoute.Infrastructure/InfrastructureServiceCollectionExtensions.cs index fccf5ce..ca7ef6f 100644 --- a/backend/src/Tnsu.ApprovalRoute.Infrastructure/InfrastructureServiceCollectionExtensions.cs +++ b/backend/Tnsu.ApprovalRoute.Infrastructure/InfrastructureServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Tnsu.ApprovalRoute.Infrastructure.Persistence; +using Tnsu.ApprovalRoute.Infrastructure.SharePoint; using Tnsu.ApprovalRoute.Infrastructure.TnsuPortal; namespace Tnsu.ApprovalRoute.Infrastructure; @@ -20,6 +21,12 @@ public static class InfrastructureServiceCollectionExtensions .UseSnakeCaseNamingConvention()); services.AddAsyncInitializer(); + services.Configure(configuration.GetSection("SharePoint")); + // Провайдер сертификата — синглтон: разобранный .pfx кешируется на всё + // время жизни процесса (см. DbCertificateProvider). + services.AddSingleton(); + services.AddHttpClient(); + services.Configure(configuration.GetSection("TnsuPortal")); services.AddHttpClient(); services.AddHttpClient((sp, client) => diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs similarity index 77% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs rename to backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs index 5aeb2f4..3bf9ee4 100644 --- a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs +++ b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Tnsu.ApprovalRoute.Domain.Entities; +using Tnsu.ApprovalRoute.Infrastructure.Persistence.Entities; namespace Tnsu.ApprovalRoute.Infrastructure.Persistence; @@ -8,9 +9,20 @@ public class ApprovalRouteDbContext(DbContextOptions opt public DbSet RequestTypes => Set(); public DbSet TemplateRoutes => Set(); public DbSet TemplateRouteSteps => Set(); + public DbSet Settings => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity(e => + { + // Имя таблицы задано явно: соглашение по именам сделало бы из + // AppSetting «app_settings», а таблица должна называться так же, как + // в api-tnsu-portal, — settings. + e.ToTable("settings"); + e.HasKey(x => x.OptionName); + e.Property(x => x.OptionName).HasMaxLength(128); + }); + modelBuilder.Entity(e => { e.HasKey(x => x.Id); diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/DatabaseMigrationInitializer.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/DatabaseMigrationInitializer.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/DatabaseMigrationInitializer.cs rename to backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/DatabaseMigrationInitializer.cs diff --git a/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Entities/AppSetting.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Entities/AppSetting.cs new file mode 100644 index 0000000..6b7f8bf --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Entities/AppSetting.cs @@ -0,0 +1,24 @@ +namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Entities; + +/// +/// Настройка, которую нельзя держать в appsettings — сейчас это единственный +/// случай: PFX-сертификат для похода в SharePoint (option_name = 'AzurePfx'). +/// +/// Живёт в Infrastructure, а не в Domain, намеренно: к предметной области +/// маршрутов согласования отношения не имеет, это способ хранения секрета. +/// +/// Форма таблицы повторяет settings в api-tnsu-portal (option_name + file_data), +/// чтобы копию сертификата можно было перенести запросом один в один, без +/// преобразований. +/// +public class AppSetting +{ + /// Ключ настройки. Для сертификата SharePoint — 'AzurePfx'. + public string OptionName { get; set; } = string.Empty; + + /// Двоичное содержимое (для 'AzurePfx' — сам .pfx как есть). + public byte[]? FileData { get; set; } + + /// Текстовое значение — для настроек, которым не нужен файл. + public string? Value { get; set; } +} diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260727103117_InitialCreate.Designer.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260727103117_InitialCreate.Designer.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260727103117_InitialCreate.Designer.cs rename to backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260727103117_InitialCreate.Designer.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260727103117_InitialCreate.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260727103117_InitialCreate.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260727103117_InitialCreate.cs rename to backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260727103117_InitialCreate.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731111928_AddRequestTypeListNameSectionAndStepConditionFields.Designer.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731111928_AddRequestTypeListNameSectionAndStepConditionFields.Designer.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731111928_AddRequestTypeListNameSectionAndStepConditionFields.Designer.cs rename to backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731111928_AddRequestTypeListNameSectionAndStepConditionFields.Designer.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731111928_AddRequestTypeListNameSectionAndStepConditionFields.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731111928_AddRequestTypeListNameSectionAndStepConditionFields.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731111928_AddRequestTypeListNameSectionAndStepConditionFields.cs rename to backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731111928_AddRequestTypeListNameSectionAndStepConditionFields.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.Designer.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.Designer.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.Designer.cs rename to backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.Designer.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.cs rename to backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.cs diff --git a/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260812091325_AddSlaSettings.Designer.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260812091325_AddSlaSettings.Designer.cs new file mode 100644 index 0000000..3630191 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260812091325_AddSlaSettings.Designer.cs @@ -0,0 +1,254 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Tnsu.ApprovalRoute.Infrastructure.Persistence; + +#nullable disable + +namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApprovalRouteDbContext))] + [Migration("20260812091325_AddSlaSettings")] + partial class AddSlaSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.RequestType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AskOverdueReason") + .HasColumnType("boolean") + .HasColumnName("ask_overdue_reason"); + + b.Property("ListId") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("list_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("PageUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("page_url"); + + b.Property("Section") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("section"); + + b.Property("SiteUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("site_url"); + + b.Property("SlaHours") + .HasColumnType("integer") + .HasColumnName("sla_hours"); + + b.HasKey("Id") + .HasName("pk_request_types"); + + b.ToTable("request_types", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ByProject") + .HasColumnType("boolean") + .HasColumnName("by_project"); + + b.Property("CompanyId") + .HasColumnType("uuid") + .HasColumnName("company_id"); + + b.Property("DepartmentId") + .HasColumnType("uuid") + .HasColumnName("department_id"); + + b.Property("RequestTypeId") + .HasColumnType("integer") + .HasColumnName("request_type_id"); + + b.Property("RequiresControl") + .HasColumnType("boolean") + .HasColumnName("requires_control"); + + b.HasKey("Id") + .HasName("pk_template_routes"); + + b.HasIndex("RequestTypeId", "DepartmentId", "CompanyId", "ByProject") + .IsUnique() + .HasDatabaseName("ix_template_routes_request_type_id_department_id_company_id_by"); + + b.ToTable("template_routes", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRouteStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowReassignToOtherApprovers") + .HasColumnType("boolean") + .HasColumnName("allow_reassign_to_other_approvers"); + + b.Property("AskOverdueReason") + .HasColumnType("boolean") + .HasColumnName("ask_overdue_reason"); + + b.Property("ExecutorEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("executor_email"); + + b.Property("FieldName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("field_name"); + + b.Property("HeadType") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("head_type"); + + b.Property("Operation") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("operation"); + + b.Property("OrderNo") + .HasColumnType("integer") + .HasColumnName("order_no"); + + b.Property("Position") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("position"); + + b.Property("ProjectRole") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("project_role"); + + b.Property("RouteId") + .HasColumnType("integer") + .HasColumnName("route_id"); + + b.Property("SlaHours") + .HasColumnType("integer") + .HasColumnName("sla_hours"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("Value") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_template_route_steps"); + + b.HasIndex("RouteId", "OrderNo") + .HasDatabaseName("ix_template_route_steps_route_id_order_no"); + + b.ToTable("template_route_steps", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Infrastructure.Persistence.Entities.AppSetting", b => + { + b.Property("OptionName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("option_name"); + + b.Property("FileData") + .HasColumnType("bytea") + .HasColumnName("file_data"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("OptionName") + .HasName("pk_settings"); + + b.ToTable("settings", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b => + { + b.HasOne("Tnsu.ApprovalRoute.Domain.Entities.RequestType", "RequestType") + .WithMany("Routes") + .HasForeignKey("RequestTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_template_routes_request_types_request_type_id"); + + b.Navigation("RequestType"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRouteStep", b => + { + b.HasOne("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", "Route") + .WithMany("Steps") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_template_route_steps_template_routes_route_id"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.RequestType", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b => + { + b.Navigation("Steps"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260812091325_AddSlaSettings.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260812091325_AddSlaSettings.cs new file mode 100644 index 0000000..90c61dc --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260812091325_AddSlaSettings.cs @@ -0,0 +1,74 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddSlaSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ask_overdue_reason", + table: "template_route_steps", + type: "boolean", + nullable: true); + + migrationBuilder.AddColumn( + name: "sla_hours", + table: "template_route_steps", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "ask_overdue_reason", + table: "request_types", + type: "boolean", + nullable: true); + + migrationBuilder.AddColumn( + name: "sla_hours", + table: "request_types", + type: "integer", + nullable: true); + + migrationBuilder.CreateTable( + name: "settings", + columns: table => new + { + option_name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + file_data = table.Column(type: "bytea", nullable: true), + value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_settings", x => x.option_name); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "settings"); + + migrationBuilder.DropColumn( + name: "ask_overdue_reason", + table: "template_route_steps"); + + migrationBuilder.DropColumn( + name: "sla_hours", + table: "template_route_steps"); + + migrationBuilder.DropColumn( + name: "ask_overdue_reason", + table: "request_types"); + + migrationBuilder.DropColumn( + name: "sla_hours", + table: "request_types"); + } + } +} diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs similarity index 85% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs rename to backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs index 60b1783..a3209ad 100644 --- a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs +++ b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs @@ -31,6 +31,10 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AskOverdueReason") + .HasColumnType("boolean") + .HasColumnName("ask_overdue_reason"); + b.Property("ListId") .HasMaxLength(512) .HasColumnType("character varying(512)") @@ -59,6 +63,10 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations .HasColumnType("character varying(512)") .HasColumnName("site_url"); + b.Property("SlaHours") + .HasColumnType("integer") + .HasColumnName("sla_hours"); + b.HasKey("Id") .HasName("pk_request_types"); @@ -117,6 +125,10 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations .HasColumnType("boolean") .HasColumnName("allow_reassign_to_other_approvers"); + b.Property("AskOverdueReason") + .HasColumnType("boolean") + .HasColumnName("ask_overdue_reason"); + b.Property("ExecutorEmail") .HasMaxLength(256) .HasColumnType("character varying(256)") @@ -155,6 +167,10 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations .HasColumnType("integer") .HasColumnName("route_id"); + b.Property("SlaHours") + .HasColumnType("integer") + .HasColumnName("sla_hours"); + b.Property("Status") .IsRequired() .HasMaxLength(32) @@ -175,6 +191,27 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations b.ToTable("template_route_steps", (string)null); }); + modelBuilder.Entity("Tnsu.ApprovalRoute.Infrastructure.Persistence.Entities.AppSetting", b => + { + b.Property("OptionName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("option_name"); + + b.Property("FileData") + .HasColumnType("bytea") + .HasColumnName("file_data"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("OptionName") + .HasName("pk_settings"); + + b.ToTable("settings", (string)null); + }); + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b => { b.HasOne("Tnsu.ApprovalRoute.Domain.Entities.RequestType", "RequestType") diff --git a/backend/Tnsu.ApprovalRoute.Infrastructure/SharePoint/CertificateProvider.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/SharePoint/CertificateProvider.cs new file mode 100644 index 0000000..806ae4c --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Infrastructure/SharePoint/CertificateProvider.cs @@ -0,0 +1,70 @@ +using System.Security.Cryptography.X509Certificates; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Tnsu.ApprovalRoute.Infrastructure.Persistence; + +namespace Tnsu.ApprovalRoute.Infrastructure.SharePoint; + +public interface ICertificateProvider +{ + Task GetCertificateAsync(CancellationToken cancellationToken); +} + +/// +/// Сертификат для входа в SharePoint — из таблицы settings (option_name задаётся +/// в SharePointOptions.CertificateSettingName, по умолчанию 'AzurePfx'). +/// +/// Синглтон с кешем: разбор .pfx стоит заметно дороже запроса к базе, а сам +/// сертификат меняется раз в год. Блокировка — чтобы параллельные запросы не +/// разбирали его одновременно при холодном старте. +/// +/// Собственная таблица, а не общая с api-tnsu-portal, — осознанный выбор: базы +/// у сервисов разные и связывать их не хотим. Цена — копию сертификата придётся +/// обновлять в двух местах. +/// +public sealed class DbCertificateProvider( + IServiceScopeFactory scopeFactory, + IOptions options) : ICertificateProvider +{ + private readonly SemaphoreSlim _lock = new(1, 1); + private X509Certificate2? _cached; + + public async Task GetCertificateAsync(CancellationToken cancellationToken) + { + if (_cached is not null) return _cached; + + await _lock.WaitAsync(cancellationToken); + try + { + if (_cached is not null) return _cached; + + var settingName = options.Value.CertificateSettingName; + + // Своя область: провайдер — синглтон, а DbContext зарегистрирован + // как scoped, напрямую внедрять его сюда нельзя. + using var scope = scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var pfx = await db.Settings.AsNoTracking() + .Where(x => x.OptionName == settingName) + .Select(x => x.FileData) + .FirstOrDefaultAsync(cancellationToken); + + if (pfx is null || pfx.Length == 0) + throw new InvalidOperationException( + $"Сертификат не найден: в таблице settings нет записи «{settingName}» с непустым file_data."); + + _cached = X509CertificateLoader.LoadPkcs12( + pfx, + options.Value.CertificatePassword, + X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.EphemeralKeySet); + + return _cached; + } + finally + { + _lock.Release(); + } + } +} diff --git a/backend/Tnsu.ApprovalRoute.Infrastructure/SharePoint/SharePointClient.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/SharePoint/SharePointClient.cs new file mode 100644 index 0000000..510a7d0 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Infrastructure/SharePoint/SharePointClient.cs @@ -0,0 +1,145 @@ +using System.Collections.Concurrent; +using System.Net.Http.Headers; +using System.Text.Json; +using Azure.Core; +using Azure.Identity; +using Microsoft.Extensions.Options; + +namespace Tnsu.ApprovalRoute.Infrastructure.SharePoint; + +public interface ISharePointClient +{ + /// Заголовок списка по его серверному относительному URL (см. RequestType.ListId). + Task GetListTitleAsync(string siteUrl, string listServerRelativeUrl, CancellationToken cancellationToken); + + /// Элементы списка данных, найденного по серверному относительному URL. + Task> GetItemsByListUrlAsync( + string siteUrl, string listServerRelativeUrl, string odataQuery, CancellationToken cancellationToken); + + /// Элементы списка, найденного по заголовку (списки согласования ищутся именно так). + Task> GetItemsByListTitleAsync( + string siteUrl, string listTitle, string odataQuery, CancellationToken cancellationToken); +} + +/// +/// Доступ к SharePoint REST от имени приложения (сертификат из таблицы settings). +/// +/// Сырой HttpClient, а не PnP.Core/CSOM — по образцу api-tnsu-portal и потому, +/// что нужно всего одно действие: прочитать элементы двух списков. Тянуть ради +/// этого клиентскую библиотеку со своей объектной моделью незачем. +/// +/// Клиент намеренно «глупый»: отдаёт сырой JSON и ничего не знает ни про SLA, ни +/// про строки маршрута — вся логика напоминаний живёт в Application. +/// +public sealed class SharePointClient( + HttpClient httpClient, + ICertificateProvider certificateProvider, + IOptions options) : ISharePointClient +{ + // Токен запрашивается на КОНКРЕТНЫЙ хост SharePoint, поэтому и кеш по хосту: + // списки разных типов заявок могут жить на разных сайтах. + private readonly ConcurrentDictionary _tokens = new(StringComparer.OrdinalIgnoreCase); + private ClientCertificateCredential? _credential; + private readonly SemaphoreSlim _credentialLock = new(1, 1); + + public async Task GetListTitleAsync( + string siteUrl, string listServerRelativeUrl, CancellationToken cancellationToken) + { + var url = $"{siteUrl.TrimEnd('/')}/_api/web/getList('{Escape(listServerRelativeUrl)}')?$select=Title"; + var json = await GetAsync(siteUrl, url, cancellationToken); + return json.TryGetProperty("Title", out var title) ? title.GetString() : null; + } + + public Task> GetItemsByListUrlAsync( + string siteUrl, string listServerRelativeUrl, string odataQuery, CancellationToken cancellationToken) => + GetItemsAsync( + siteUrl, + $"{siteUrl.TrimEnd('/')}/_api/web/getList('{Escape(listServerRelativeUrl)}')/items?{odataQuery}", + cancellationToken); + + public Task> GetItemsByListTitleAsync( + string siteUrl, string listTitle, string odataQuery, CancellationToken cancellationToken) => + GetItemsAsync( + siteUrl, + $"{siteUrl.TrimEnd('/')}/_api/web/lists/getByTitle('{Escape(listTitle)}')/items?{odataQuery}", + cancellationToken); + + private async Task> GetItemsAsync( + string siteUrl, string url, CancellationToken cancellationToken) + { + var json = await GetAsync(siteUrl, url, cancellationToken); + + // odata=nometadata отдаёт элементы в "value"; на всякий случай терпим и + // «голый» массив, если заголовок Accept вдруг не подействует. + if (json.ValueKind == JsonValueKind.Array) + return json.EnumerateArray().ToList(); + + return json.TryGetProperty("value", out var value) && value.ValueKind == JsonValueKind.Array + ? value.EnumerateArray().ToList() + : []; + } + + private async Task GetAsync(string siteUrl, string url, CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Authorization = new AuthenticationHeaderValue( + "Bearer", await GetTokenAsync(siteUrl, cancellationToken)); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json") + { + Parameters = { new NameValueHeaderValue("odata", "nometadata") } + }); + + using var response = await httpClient.SendAsync(request, cancellationToken); + var body = await response.Content.ReadAsStringAsync(cancellationToken); + + if (!response.IsSuccessStatusCode) + throw new InvalidOperationException( + $"SharePoint ответил {(int)response.StatusCode} на {url}: {body}"); + + using var document = JsonDocument.Parse(body); + return document.RootElement.Clone(); + } + + private async Task GetTokenAsync(string siteUrl, CancellationToken cancellationToken) + { + var host = new Uri(siteUrl).Host; + + // Токен живой ещё пять минут — переиспользуем. Запас нужен, чтобы токен + // не истёк между проверкой и приходом запроса в SharePoint. + if (_tokens.TryGetValue(host, out var cached) && cached.ExpiresOn > DateTimeOffset.UtcNow.AddMinutes(5)) + return cached.Token; + + var credential = await GetCredentialAsync(cancellationToken); + var token = await credential.GetTokenAsync( + new TokenRequestContext([$"https://{host}/.default"]), cancellationToken); + + _tokens[host] = token; + return token.Token; + } + + private async Task GetCredentialAsync(CancellationToken cancellationToken) + { + if (_credential is not null) return _credential; + + await _credentialLock.WaitAsync(cancellationToken); + try + { + if (_credential is not null) return _credential; + + var certificate = await certificateProvider.GetCertificateAsync(cancellationToken); + _credential = new ClientCertificateCredential( + options.Value.TenantId, options.Value.ClientId, certificate); + + return _credential; + } + finally + { + _credentialLock.Release(); + } + } + + // Одинарная кавычка в OData-литерале экранируется её удвоением. Заголовки + // списков содержат тире и кириллицу, но кавычка теоретически возможна — + // без экранирования она сломала бы запрос. + private static string Escape(string value) => value.Replace("'", "''"); +} diff --git a/backend/Tnsu.ApprovalRoute.Infrastructure/SharePoint/SharePointOptions.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/SharePoint/SharePointOptions.cs new file mode 100644 index 0000000..71186da --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Infrastructure/SharePoint/SharePointOptions.cs @@ -0,0 +1,23 @@ +namespace Tnsu.ApprovalRoute.Infrastructure.SharePoint; + +public sealed class SharePointOptions +{ + /// Тенант Entra — тот же, что у остальной авторизации. + public string TenantId { get; set; } = string.Empty; + + /// + /// Приложение, от имени которого идём в SharePoint. Ему нужны разрешения + /// уровня приложения на чтение списков, выданные администратором тенанта. + /// + public string ClientId { get; set; } = string.Empty; + + /// Ключ в таблице settings, под которым лежит .pfx (option_name). + public string CertificateSettingName { get; set; } = "AzurePfx"; + + /// + /// Пароль от .pfx. Намеренно берётся из конфигурации, а не зашивается в код: + /// в api-tnsu-portal он захардкожен в PostgresCertificateProvider, и повторять + /// это здесь не стоит — пароль от сертификата в исходниках не место. + /// + public string CertificatePassword { get; set; } = string.Empty; +} diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj b/backend/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj similarity index 86% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj rename to backend/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj index 33b62bc..7d5e20c 100644 --- a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj +++ b/backend/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj @@ -3,6 +3,8 @@ + + diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/HttpTnsuPortalClient.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/HttpTnsuPortalClient.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/HttpTnsuPortalClient.cs rename to backend/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/HttpTnsuPortalClient.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/ITnsuPortalClient.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/ITnsuPortalClient.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/ITnsuPortalClient.cs rename to backend/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/ITnsuPortalClient.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalDtos.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalDtos.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalDtos.cs rename to backend/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalDtos.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalOptions.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalOptions.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalOptions.cs rename to backend/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalOptions.cs diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalTokenProvider.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalTokenProvider.cs similarity index 100% rename from backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalTokenProvider.cs rename to backend/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalTokenProvider.cs diff --git a/backend/Tnsu.ApprovalRoute.sln b/backend/Tnsu.ApprovalRoute.sln index ee88a58..25d34e5 100644 --- a/backend/Tnsu.ApprovalRoute.sln +++ b/backend/Tnsu.ApprovalRoute.sln @@ -3,15 +3,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tnsu.ApprovalRoute.Domain", "Tnsu.ApprovalRoute.Domain\Tnsu.ApprovalRoute.Domain.csproj", "{67C6FECB-26BF-4A7E-AC40-574F4D88EEAC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tnsu.ApprovalRoute.Domain", "src\Tnsu.ApprovalRoute.Domain\Tnsu.ApprovalRoute.Domain.csproj", "{67C6FECB-26BF-4A7E-AC40-574F4D88EEAC}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tnsu.ApprovalRoute.Application", "Tnsu.ApprovalRoute.Application\Tnsu.ApprovalRoute.Application.csproj", "{2EEA333B-16C7-460E-BD07-AB1F32D6A9A9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tnsu.ApprovalRoute.Application", "src\Tnsu.ApprovalRoute.Application\Tnsu.ApprovalRoute.Application.csproj", "{2EEA333B-16C7-460E-BD07-AB1F32D6A9A9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tnsu.ApprovalRoute.Infrastructure", "Tnsu.ApprovalRoute.Infrastructure\Tnsu.ApprovalRoute.Infrastructure.csproj", "{46EF4247-8B98-4CC9-A981-186C8D24163C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tnsu.ApprovalRoute.Infrastructure", "src\Tnsu.ApprovalRoute.Infrastructure\Tnsu.ApprovalRoute.Infrastructure.csproj", "{46EF4247-8B98-4CC9-A981-186C8D24163C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tnsu.ApprovalRoute.Api", "src\Tnsu.ApprovalRoute.Api\Tnsu.ApprovalRoute.Api.csproj", "{4697B342-BE47-435B-B854-75F9277AD769}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tnsu.ApprovalRoute.Api", "Tnsu.ApprovalRoute.Api\Tnsu.ApprovalRoute.Api.csproj", "{4697B342-BE47-435B-B854-75F9277AD769}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" EndProject @@ -92,10 +90,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {67C6FECB-26BF-4A7E-AC40-574F4D88EEAC} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {2EEA333B-16C7-460E-BD07-AB1F32D6A9A9} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {46EF4247-8B98-4CC9-A981-186C8D24163C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {4697B342-BE47-435B-B854-75F9277AD769} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {F5E2BA0E-7978-414C-98D4-D3C91C162FDE} = {0AB3BF05-4346-4AA6-1389-037BE0695223} EndGlobalSection EndGlobal diff --git a/backend/src/.vs/VSWorkspaceState.json b/backend/src/.vs/VSWorkspaceState.json deleted file mode 100644 index a35ce30..0000000 --- a/backend/src/.vs/VSWorkspaceState.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ExpandedNodes": [ - "", - "\\Tnsu.ApprovalRoute.Api", - "\\Tnsu.ApprovalRoute.Application" - ], - "SelectedNode": "\\Tnsu.ApprovalRoute.Application", - "PreviewInSolutionExplorer": false -} \ No newline at end of file diff --git a/backend/src/.vs/slnx.sqlite b/backend/src/.vs/slnx.sqlite deleted file mode 100644 index 43f81fe3bd3cc78e69ff157e41789af73e3888e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 557056 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

|i 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 Ktf90!lm-Ak10pT} diff --git a/backend/src/.vs/src.slnx/config/applicationhost.config b/backend/src/.vs/src.slnx/config/applicationhost.config deleted file mode 100644 index 223dc4c..0000000 --- a/backend/src/.vs/src.slnx/config/applicationhost.config +++ /dev/null @@ -1,1011 +0,0 @@ - - - - - - - - -

-
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
- -
-
- -
-
- -
-
-
- - -
-
-
-
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/backend/src/.vs/src.slnx/v18/.wsuo b/backend/src/.vs/src.slnx/v18/.wsuo deleted file mode 100644 index 0476f63f3d669a88de2c9342961d33be5a53f96c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43008 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 QKP2|Q@a?tZTeAfIA5pKvw*UYD diff --git a/backend/src/.vs/src.slnx/v18/DocumentLayout.backup.json b/backend/src/.vs/src.slnx/v18/DocumentLayout.backup.json deleted file mode 100644 index 8712180..0000000 --- a/backend/src/.vs/src.slnx/v18/DocumentLayout.backup.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "Version": 1, - "WorkspaceRootPath": "C:\\Users\\trishina.mariya\\Documents\\Projects\\tnsu-approval-route\\backend\\src\\", - "Documents": [ - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\trishina.mariya\\Documents\\Projects\\tnsu-approval-route\\backend\\src\\Tnsu.ApprovalRoute.Api\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:Tnsu.ApprovalRoute.Api\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}" - } - ], - "DocumentGroupContainers": [ - { - "Orientation": 0, - "VerticalTabListWidth": 256, - "DocumentGroups": [ - { - "DockedWidth": 200, - "SelectedChildIndex": 0, - "Children": [ - { - "$type": "Document", - "DocumentIndex": 0, - "Title": "appsettings.json", - "DocumentMoniker": "C:\\Users\\trishina.mariya\\Documents\\Projects\\tnsu-approval-route\\backend\\src\\Tnsu.ApprovalRoute.Api\\appsettings.json", - "RelativeDocumentMoniker": "Tnsu.ApprovalRoute.Api\\appsettings.json", - "ToolTip": "C:\\Users\\trishina.mariya\\Documents\\Projects\\tnsu-approval-route\\backend\\src\\Tnsu.ApprovalRoute.Api\\appsettings.json", - "RelativeToolTip": "Tnsu.ApprovalRoute.Api\\appsettings.json", - "ViewState": "AgIAAAAAAAAAAAAAAAAAABUAAAAXAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|", - "WhenOpened": "2026-07-28T05:52:49.686Z", - "EditorCaption": "" - } - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/backend/src/.vs/src.slnx/v18/DocumentLayout.json b/backend/src/.vs/src.slnx/v18/DocumentLayout.json deleted file mode 100644 index 8712180..0000000 --- a/backend/src/.vs/src.slnx/v18/DocumentLayout.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "Version": 1, - "WorkspaceRootPath": "C:\\Users\\trishina.mariya\\Documents\\Projects\\tnsu-approval-route\\backend\\src\\", - "Documents": [ - { - "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\trishina.mariya\\Documents\\Projects\\tnsu-approval-route\\backend\\src\\Tnsu.ApprovalRoute.Api\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}", - "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:Tnsu.ApprovalRoute.Api\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}" - } - ], - "DocumentGroupContainers": [ - { - "Orientation": 0, - "VerticalTabListWidth": 256, - "DocumentGroups": [ - { - "DockedWidth": 200, - "SelectedChildIndex": 0, - "Children": [ - { - "$type": "Document", - "DocumentIndex": 0, - "Title": "appsettings.json", - "DocumentMoniker": "C:\\Users\\trishina.mariya\\Documents\\Projects\\tnsu-approval-route\\backend\\src\\Tnsu.ApprovalRoute.Api\\appsettings.json", - "RelativeDocumentMoniker": "Tnsu.ApprovalRoute.Api\\appsettings.json", - "ToolTip": "C:\\Users\\trishina.mariya\\Documents\\Projects\\tnsu-approval-route\\backend\\src\\Tnsu.ApprovalRoute.Api\\appsettings.json", - "RelativeToolTip": "Tnsu.ApprovalRoute.Api\\appsettings.json", - "ViewState": "AgIAAAAAAAAAAAAAAAAAABUAAAAXAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|", - "WhenOpened": "2026-07-28T05:52:49.686Z", - "EditorCaption": "" - } - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Auth/AuthSchemes.cs b/backend/src/Tnsu.ApprovalRoute.Api/Auth/AuthSchemes.cs deleted file mode 100644 index 331bf3d..0000000 --- a/backend/src/Tnsu.ApprovalRoute.Api/Auth/AuthSchemes.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Tnsu.ApprovalRoute.Api.Auth; - -public static class AuthSchemes -{ - public const string Dev = "Dev"; -} diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Auth/AuthSetup.cs b/backend/src/Tnsu.ApprovalRoute.Api/Auth/AuthSetup.cs deleted file mode 100644 index e72b17c..0000000 --- a/backend/src/Tnsu.ApprovalRoute.Api/Auth/AuthSetup.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.IdentityModel.Tokens; - -namespace Tnsu.ApprovalRoute.Api.Auth; - -public static class AuthSetup -{ - public static IServiceCollection AddApprovalRouteAuth(this IServiceCollection services, IConfiguration configuration) - { - var tenantId = configuration["Entra:TenantId"]; - var audience = configuration["Entra:Audience"]; - - if (!string.IsNullOrWhiteSpace(tenantId) && !string.IsNullOrWhiteSpace(audience)) - { - services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => - { - options.Authority = $"https://login.microsoftonline.com/{tenantId}/v2.0"; - options.TokenValidationParameters = new TokenValidationParameters - { - ValidIssuers = - [ - $"https://login.microsoftonline.com/{tenantId}/v2.0", - $"https://sts.windows.net/{tenantId}/" - ], - ValidAudiences = [audience, $"api://{audience}"] - }; - }); - } - else - { - services.AddAuthentication(AuthSchemes.Dev) - .AddScheme(AuthSchemes.Dev, _ => { }); - } - - services.AddAuthorization(); - - return services; - } -} diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Dockerfile b/backend/src/Tnsu.ApprovalRoute.Api/Dockerfile deleted file mode 100644 index a9f4110..0000000 --- a/backend/src/Tnsu.ApprovalRoute.Api/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY Directory.Build.props Directory.Packages.props ./ -COPY src/Tnsu.ApprovalRoute.Domain/Tnsu.ApprovalRoute.Domain.csproj src/Tnsu.ApprovalRoute.Domain/ -COPY src/Tnsu.ApprovalRoute.Application/Tnsu.ApprovalRoute.Application.csproj src/Tnsu.ApprovalRoute.Application/ -COPY src/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj src/Tnsu.ApprovalRoute.Infrastructure/ -COPY src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj src/Tnsu.ApprovalRoute.Api/ -RUN dotnet restore src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj -COPY src/ src/ -RUN dotnet publish src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj -c Release -o /app/publish /p:UseAppHost=false - -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final -WORKDIR /app -RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* -COPY --from=build /app/publish . -ENV ASPNETCORE_URLS=http://+:8080 -EXPOSE 8080 -ENTRYPOINT ["dotnet", "Tnsu.ApprovalRoute.Api.dll"] diff --git a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs deleted file mode 100644 index bc44c5a..0000000 --- a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Tnsu.ApprovalRoute.Application.RequestTypes; - -public sealed record RequestTypeDto(int Id, string Name, string PageUrl, string? ListId, string? SiteUrl, string Section); - -public sealed record CreateRequestTypeRequest(string Name, string PageUrl, string? ListId, string? SiteUrl, string Section); - -public sealed record UpdateRequestTypeRequest(string Name, string PageUrl, string? ListId, string? SiteUrl, string Section); - -public sealed record PatchRequestTypeRequest(string? Name, string? PageUrl, string? ListId, string? SiteUrl, string? Section); diff --git a/backend/src/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs b/backend/src/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs deleted file mode 100644 index b47fa57..0000000 --- a/backend/src/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Tnsu.ApprovalRoute.Domain.Entities; - -public class RequestType -{ - public int Id { get; set; } - public string Name { get; set; } = string.Empty; - public string PageUrl { get; set; } = string.Empty; - public string? ListId { get; set; } - public string? SiteUrl { get; set; } - public string Section { get; set; } = string.Empty; - - public ICollection Routes { get; set; } = new List(); -} diff --git a/backend/src/Tnsu.ApprovalRoute.Domain/Entities/TemplateRouteStep.cs b/backend/src/Tnsu.ApprovalRoute.Domain/Entities/TemplateRouteStep.cs deleted file mode 100644 index ce5a6be..0000000 --- a/backend/src/Tnsu.ApprovalRoute.Domain/Entities/TemplateRouteStep.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Tnsu.ApprovalRoute.Domain.Enums; - -namespace Tnsu.ApprovalRoute.Domain.Entities; - -public class TemplateRouteStep -{ - public int Id { get; set; } - public int OrderNo { get; set; } - - public string? ExecutorEmail { get; set; } - public string? Position { get; set; } - public ProjectRole? ProjectRole { get; set; } - public HeadType? HeadType { get; set; } - - public RouteStepStatus Status { get; set; } = RouteStepStatus.Approval; - public int RouteId { get; set; } - public bool AllowReassignToOtherApprovers { get; set; } - - public string? FieldName { get; set; } - public ComparisonOperator? Operation { get; set; } - public string? Value { get; set; } - - public TemplateRoute? Route { get; set; } -} diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/ReminderServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/ReminderServiceTests.cs new file mode 100644 index 0000000..3467c06 --- /dev/null +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/ReminderServiceTests.cs @@ -0,0 +1,293 @@ +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Shouldly; +using Tnsu.ApprovalRoute.Application.Reminders; +using Tnsu.ApprovalRoute.Application.RequestTypes; +using Tnsu.ApprovalRoute.Infrastructure.Persistence; +using Tnsu.ApprovalRoute.Infrastructure.SharePoint; +using Tnsu.ApprovalRoute.IntegrationTests.Infrastructure; + +namespace Tnsu.ApprovalRoute.IntegrationTests.ApplicationServices; + +/// +/// Проверяется прежде всего вычисление даты, с которой отсчитывается SLA: +/// отдельной колонки «шаг стал активным» в списке согласования нет, и момент +/// выводится из соседних строк — это самая хрупкая часть эндпоинта. +/// +/// SharePoint подменён моком, отдающим сырой JSON ровно в том виде, в каком его +/// возвращает REST, — так тесты заодно покрывают и разбор ответа. +/// +public sealed class ReminderServiceTests : IClassFixture +{ + private const string SiteUrl = "https://tnsukz.sharepoint.com/sites/requests"; + private const string DataListTitle = "Заявка тест — данные"; + private const string ApprovalListTitle = "Заявка тест — согласование"; + + private static readonly DateTimeOffset Now = new(2026, 8, 12, 12, 0, 0, TimeSpan.Zero); + + private readonly ServiceProvider _services; + private readonly RequestTypeService _requestTypeService; + + public ReminderServiceTests(PostgresDatabaseFixture fixture) + { + _services = TestServiceProviderFactory.Build(fixture.ConnectionString); + _requestTypeService = _services.GetRequiredService(); + } + + // База у тестов класса общая, а сервис обходит ВСЕ типы с настроенным SLA — + // поэтому у каждого теста свой список: типы соседних тестов на мок не + // настроены, заголовок списка для них не найдётся, и в выборку они не попадут. + private async Task CreateTypeWithSlaAsync(string name, int slaHours, bool? askOverdueReason = null) + { + var listUrl = $"/sites/requests/Lists/{name.Replace(" ", string.Empty)}"; + await _requestTypeService.CreateAsync( + new CreateRequestTypeRequest(name, $"/{name}", listUrl, SiteUrl, "General", slaHours, askOverdueReason), + CancellationToken.None); + + return listUrl; + } + + private static JsonElement Json(object value) => JsonSerializer.SerializeToElement(value); + + private static object Row( + int id, int ticketId, int stepOrder, string status, string phase, + string? date = null, string email = "approver@tnsu.kz", object? slaHours = null, bool? ask = null) => + new + { + Id = id, + TicketId = ticketId, + StepOrder = stepOrder, + ApprovalStatus = status, + Phase = phase, + Date = date, + SLAhours = slaHours, + AskOverdueReason = ask, + TansuSiteApprover = new { EMail = email } + }; + + private ReminderService BuildService( + string listUrl, IReadOnlyList allRows, DateTimeOffset requestCreated) + { + var sharePoint = new Mock(); + + sharePoint + .Setup(x => x.GetListTitleAsync(SiteUrl, listUrl, It.IsAny())) + .ReturnsAsync(DataListTitle); + + // Активные строки сервис отбирает прямым $filter по ApprovalStatus — мок + // отвечает так же: по значению статуса, а не по признаку решённости. + var activeRows = allRows + .Where(r => (string)r.GetType().GetProperty("ApprovalStatus")!.GetValue(r)! is + "На согласовании" or "На исполнении") + .ToList(); + sharePoint + .Setup(x => x.GetItemsByListTitleAsync( + SiteUrl, ApprovalListTitle, It.Is(q => q.Contains("ApprovalStatus eq")), It.IsAny())) + .ReturnsAsync(activeRows.Select(Json).ToList()); + + sharePoint + .Setup(x => x.GetItemsByListTitleAsync( + SiteUrl, ApprovalListTitle, It.Is(q => q.Contains("TicketId eq")), It.IsAny())) + .ReturnsAsync(allRows.Select(Json).ToList()); + + sharePoint + .Setup(x => x.GetItemsByListUrlAsync(SiteUrl, listUrl, It.IsAny(), It.IsAny())) + .ReturnsAsync([Json(new { Id = 1, Created = requestCreated.ToString("O") })]); + + return new ReminderService( + _services.GetRequiredService(), + sharePoint.Object, + Options.Create(new RemindersOptions()), + NullLogger.Instance); + } + + [Fact(DisplayName = "Первый шаг согласования: SLA отсчитывается от создания заявки")] + public async Task FirstApprovalStep_CountsFromRequestCreated() + { + var listUrl = await CreateTypeWithSlaAsync("Sla first step", slaHours: 24); + // Создана 48 часов назад, SLA 24 часа → просрочка ровно 24 часа. + var service = BuildService(listUrl, + [Row(10, 1, 1, "На согласовании", "Согласование")], + requestCreated: Now.AddHours(-48)); + + var result = await service.GetOverdueAsync(Now, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + var reminder = result.Value.ShouldHaveSingleItem(); + reminder.Email.ShouldBe("approver@tnsu.kz"); + reminder.HoursOverdue.ShouldBe(24); + reminder.RequestId.ShouldBe(1); + } + + [Fact(DisplayName = "Срок ещё не вышел — напоминания нет")] + public async Task WithinSla_ReturnsNothing() + { + var listUrl = await CreateTypeWithSlaAsync("Sla not due", slaHours: 24); + var service = BuildService(listUrl, + [Row(10, 1, 1, "На согласовании", "Согласование")], + requestCreated: Now.AddHours(-2)); + + var result = await service.GetOverdueAsync(Now, CancellationToken.None); + + result.Value.ShouldBeEmpty(); + } + + [Fact(DisplayName = "Второй шаг: SLA отсчитывается от решения предыдущего шага, а не от создания заявки")] + public async Task SecondStep_CountsFromPreviousStepDecision() + { + var listUrl = await CreateTypeWithSlaAsync("Sla second step", slaHours: 10); + // Заявка создана давно, но предыдущий шаг решён 12 часов назад: + // при отсчёте от создания просрочка была бы огромной, верная — 2 часа. + var service = BuildService(listUrl, + [ + Row(10, 1, 1, "Согласовано", "Согласование", date: Now.AddHours(-12).ToString("O")), + Row(11, 1, 2, "На согласовании", "Согласование", email: "second@tnsu.kz") + ], + requestCreated: Now.AddDays(-30)); + + var result = await service.GetOverdueAsync(Now, CancellationToken.None); + + var reminder = result.Value.ShouldHaveSingleItem(); + reminder.Email.ShouldBe("second@tnsu.kz"); + reminder.HoursOverdue.ShouldBe(2); + } + + [Fact(DisplayName = "Параллельный предыдущий шаг: отсчёт от ПОСЛЕДНЕГО решения, а не от первого")] + public async Task ParallelPreviousStep_CountsFromLastDecision() + { + var listUrl = await CreateTypeWithSlaAsync("Sla parallel", slaHours: 10); + // Двое делят StepOrder = 1. Следующий шаг активируется только после + // решения второго (-12 ч), а не первого (-40 ч). + var service = BuildService(listUrl, + [ + Row(10, 1, 1, "Согласовано", "Согласование", date: Now.AddHours(-40).ToString("O")), + Row(11, 1, 1, "Согласовано", "Согласование", date: Now.AddHours(-12).ToString("O")), + Row(12, 1, 2, "На согласовании", "Согласование", email: "next@tnsu.kz") + ], + requestCreated: Now.AddDays(-30)); + + var result = await service.GetOverdueAsync(Now, CancellationToken.None); + + result.Value.ShouldHaveSingleItem().HoursOverdue.ShouldBe(2); + } + + [Fact(DisplayName = "Первый шаг исполнения: отсчёт от последнего решения фазы согласования")] + public async Task FirstExecutionStep_CountsFromLastApprovalDecision() + { + var listUrl = await CreateTypeWithSlaAsync("Sla execution", slaHours: 5); + var service = BuildService(listUrl, + [ + Row(10, 1, 1, "Согласовано", "Согласование", date: Now.AddHours(-30).ToString("O")), + Row(11, 1, 2, "Согласовано", "Согласование", date: Now.AddHours(-8).ToString("O")), + Row(12, 1, 1, "На исполнении", "Исполнение", email: "executor@tnsu.kz") + ], + requestCreated: Now.AddDays(-30)); + + var result = await service.GetOverdueAsync(Now, CancellationToken.None); + + var reminder = result.Value.ShouldHaveSingleItem(); + reminder.Email.ShouldBe("executor@tnsu.kz"); + reminder.HoursOverdue.ShouldBe(3); + } + + [Fact(DisplayName = "SLA строки списка перекрывает SLA типа заявки")] + public async Task RowSla_OverridesRequestTypeSla() + { + var listUrl = await CreateTypeWithSlaAsync("Sla row override", slaHours: 100); + // По типу 100 часов — просрочки не было бы; в строке 1 час — просрочка 5 часов. + var service = BuildService(listUrl, + [Row(10, 1, 1, "На согласовании", "Согласование", slaHours: 1)], + requestCreated: Now.AddHours(-6)); + + var result = await service.GetOverdueAsync(Now, CancellationToken.None); + + result.Value.ShouldHaveSingleItem().HoursOverdue.ShouldBe(5); + } + + // SharePoint отдаёт Number-колонки ДРОБНЫМИ («1.0», а не «1»), и на этом + // разбор спотыкался: JsonElement.TryGetInt32 на числе с десятичной точкой + // возвращает false, поле молча становилось null и SLA строки терялся. + // На фронтенде проблемы не было — в JavaScript 1.0 и 1 неразличимы. + [Fact(DisplayName = "SLA строки читается, даже когда SharePoint отдаёт его дробным числом")] + public async Task RowSla_WrittenAsDecimalNumber_IsStillRead() + { + var listUrl = await CreateTypeWithSlaAsync("Sla decimal", slaHours: 100); + // По типу 100 часов — просрочки не было бы; в строке 1 час — просрочка 5 часов. + var service = BuildService(listUrl, + // Именно decimal: System.Text.Json сохраняет у него хвостовой ноль и + // пишет «1.0», тогда как double 1.0 сериализуется в «1» и баг не + // воспроизводит. + [Row(10, 1, 1, "На согласовании", "Согласование", slaHours: 1.0m)], + requestCreated: Now.AddHours(-6)); + + var result = await service.GetOverdueAsync(Now, CancellationToken.None); + + result.Value.ShouldHaveSingleItem().HoursOverdue.ShouldBe(5); + } + + [Fact(DisplayName = "Флаг опроса причины наследуется от типа заявки, если в строке не задан")] + public async Task AskOverdueReason_InheritsFromRequestType() + { + var listUrl = await CreateTypeWithSlaAsync("Sla ask reason", slaHours: 1, askOverdueReason: true); + var service = BuildService(listUrl, + [Row(10, 1, 1, "На согласовании", "Согласование")], + requestCreated: Now.AddHours(-6)); + + var result = await service.GetOverdueAsync(Now, CancellationToken.None); + + result.Value.ShouldHaveSingleItem().AskOverdueReason.ShouldBeTrue(); + } + + // Портал — одностраничный и живёт в корне сайта; вид ссылки подтверждён + // живым примером: + // https://tnsukz.sharepoint.com/sites/requests?rpnav=approvals&rpmode=approve&rptype=it-1c-ticket&rpid=11 + [Fact(DisplayName = "Ссылка на заявку собирается в том же виде, что и в портале")] + public async Task Url_MatchesPortalFormat() + { + var listUrl = await CreateTypeWithSlaAsync("it-1c-ticket", slaHours: 1); + var service = BuildService(listUrl, + [Row(10, 11, 1, "На согласовании", "Согласование")], + requestCreated: Now.AddHours(-6)); + + var result = await service.GetOverdueAsync(Now, CancellationToken.None); + + result.Value.ShouldHaveSingleItem().Url.ShouldBe( + "https://tnsukz.sharepoint.com/sites/requests" + + "?rpnav=approvals&rpmode=approve&rptype=/it-1c-ticket&rpid=11"); + } + + [Fact(DisplayName = "Просроченный шаг исполнения ведёт в раздел «На исполнении», а не в согласование")] + public async Task Url_ForExecutionPhase_PointsToExecutionSection() + { + var listUrl = await CreateTypeWithSlaAsync("Sla exec nav", slaHours: 1); + var service = BuildService(listUrl, + [ + Row(10, 1, 1, "Согласовано", "Согласование", date: Now.AddHours(-6).ToString("O")), + Row(11, 1, 1, "На исполнении", "Исполнение", email: "executor@tnsu.kz") + ], + requestCreated: Now.AddDays(-30)); + + var result = await service.GetOverdueAsync(Now, CancellationToken.None); + + result.Value.ShouldHaveSingleItem().Url.ShouldContain("rpnav=execution"); + } + + [Fact(DisplayName = "Ждущие очереди строки в напоминания не попадают")] + public async Task QueuedRows_AreIgnored() + { + var listUrl = await CreateTypeWithSlaAsync("Sla queued", slaHours: 1); + var service = BuildService(listUrl, + [ + Row(10, 1, 1, "На согласовании", "Согласование"), + Row(11, 1, 2, "Ждёт очереди", "Согласование", email: "waiting@tnsu.kz") + ], + requestCreated: Now.AddHours(-6)); + + var result = await service.GetOverdueAsync(Now, CancellationToken.None); + + result.Value.ShouldHaveSingleItem().Email.ShouldBe("approver@tnsu.kz"); + } +} diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs index 8bbd080..cc77636 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs @@ -181,6 +181,80 @@ public sealed class TemplateRouteServiceTests : IClassFixture 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 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(); + 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() { diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Tnsu.ApprovalRoute.IntegrationTests.csproj b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Tnsu.ApprovalRoute.IntegrationTests.csproj index 4b9680b..74da7e1 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Tnsu.ApprovalRoute.IntegrationTests.csproj +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Tnsu.ApprovalRoute.IntegrationTests.csproj @@ -5,8 +5,8 @@ - - + + diff --git a/docker-compose.yml b/docker-compose.yml index a664dbb..8112d3c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: api: build: context: ./backend - dockerfile: src/Tnsu.ApprovalRoute.Api/Dockerfile + dockerfile: Tnsu.ApprovalRoute.Api/Dockerfile container_name: tnsu-approval-route-api restart: unless-stopped depends_on: diff --git a/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsApproverModal.tsx b/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsApproverModal.tsx index 36947ce..6a6c119 100644 --- a/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsApproverModal.tsx +++ b/frontend/src/webparts/approvalAdmin/components/screens/ManageRequestsApproverModal.tsx @@ -3,30 +3,59 @@ import { Employee } from '../../models'; import { texts } from '../../texts'; import { findEmployeeByEmail, toEmployeeOption } from '../../utils/employee'; import { useAsyncSubmit } from '../../hooks/useAsyncSubmit'; -import { Field, FormGrid, FormModal, SearchableSelect } from '../ui'; +import { + boolToTriState, + isValidSlaHours, + parseSlaHours, + slaHoursToInput, + triStateToBool, + TriState, + TRI_STATE_OPTIONS +} from '../../utils/sla'; +import { Field, FormGrid, FormModal, SearchableSelect, Select, TextInput } from '../ui'; export interface IManageRequestsApproverModalProps { title: string; initialEmail?: string; + /** SLA строки в часах на момент открытия. null — в строке не задано, работает SLA типа заявки. */ + // eslint-disable-next-line @rushstack/no-new-null -- null = «не задано», это не 0 + initialSlaHours: number | null; + /** Опрос причины просрочки по строке. null — берётся от типа заявки. */ + // eslint-disable-next-line @rushstack/no-new-null -- см. initialSlaHours + initialAskOverdueReason: boolean | null; employees: Employee[]; employeesLoading: boolean; /** Email'ы (в любом регистре), уже занятые другими шагами той же фазы — этот человек не может встать в фазу второй раз. */ excludedEmails: string[]; - onSave: (email: string, position: string) => Promise; + onSave: ( + email: string, + position: string, + // eslint-disable-next-line @rushstack/no-new-null -- null = «не задано» + slaHours: number | null, + // eslint-disable-next-line @rushstack/no-new-null -- null = «не задано» + askOverdueReason: boolean | null + ) => Promise; onClose: () => void; } /** - * Переназначение согласующего/исполнителя существующего шага (см. - * ManageRequestsScreen). Должность НЕ вводится — берётся из справочника - * сотрудников (api.tnsu.kz/Employee/All, см. Employee.position в - * models/dictionaries.ts) на момент сохранения, отдельного поля для неё - * в этой форме нет. Добавление НОВОГО шага через модалку не идёт — см. + * Правка существующего шага: кто согласует и какой у шага SLA. + * + * Одна форма на оба вида правки намеренно — раньше SLA жил в отдельной модалке + * со своей кнопкой-флажком в строке, и получалось два похожих диалога и лишняя + * иконка в и без того плотном ряду действий. Правки применяются одним + * сохранением (см. saveModal в ManageRequestsScreen). + * + * Должность НЕ вводится — берётся из справочника сотрудников + * (api.tnsu.kz/Employee/All, см. Employee.position в models/dictionaries.ts) на + * момент сохранения. Добавление НОВОГО шага через модалку не идёт — см. * плашку-пикер под таблицей в ManageRequestsPhaseTable. */ export const ManageRequestsApproverModal: React.FC = ({ title, initialEmail = '', + initialSlaHours, + initialAskOverdueReason, employees, employeesLoading, excludedEmails, @@ -34,6 +63,10 @@ export const ManageRequestsApproverModal: React.FC { const [email, setEmail] = React.useState(initialEmail); + const [slaHours, setSlaHours] = React.useState(() => slaHoursToInput(initialSlaHours)); + const [askOverdueReason, setAskOverdueReason] = React.useState( + () => boolToTriState(initialAskOverdueReason) + ); const submit = useAsyncSubmit(); // Тот, кто уже занят в этой фазе, исключён из списка выбора — дубликат @@ -51,8 +84,14 @@ export const ManageRequestsApproverModal: React.FC onSave(trimmedEmail, position)); + submit.run(() => + onSave(trimmedEmail, position, parseSlaHours(slaHours), triStateToBool(askOverdueReason)) + ); }; return ( @@ -69,6 +108,22 @@ export const ManageRequestsApproverModal: React.FC + + setSlaHours(e.target.value)} + placeholder={texts.manageRequests.slaHoursPlaceholder} + /> + + + setForm({ ...form, askOverdueReason: e.target.value as TriState })} + /> + )} diff --git a/frontend/src/webparts/approvalAdmin/components/screens/RouteStepFormModal.tsx b/frontend/src/webparts/approvalAdmin/components/screens/RouteStepFormModal.tsx index e20aec4..43b1643 100644 --- a/frontend/src/webparts/approvalAdmin/components/screens/RouteStepFormModal.tsx +++ b/frontend/src/webparts/approvalAdmin/components/screens/RouteStepFormModal.tsx @@ -18,6 +18,15 @@ import { useSharePointFields } from '../../hooks/useSharePointFields'; import { ApprovalRouteApi } from '../../services/api'; import { isNumericFieldType, SharePointListsClient } from '../../services/sharePointLists'; import { Checkbox, Field, FormGrid, FormModal, RadioGroup, SearchableSelect, Select, TextInput } from '../ui'; +import { + boolToTriState, + isValidSlaHours, + parseSlaHours, + slaHoursToInput, + triStateToBool, + TriState, + TRI_STATE_OPTIONS +} from '../../utils/sla'; export interface IRouteStepFormModalProps { api: ApprovalRouteApi; @@ -47,6 +56,9 @@ interface IFormState { fieldName: string; operation: ComparisonOperator; value: string; + /** Пусто — наследовать SLA от типа заявки; «0» — напоминать сразу. Это разные вещи. */ + slaHours: string; + askOverdueReason: TriState; } const STATUS_OPTIONS = [RouteStepStatus.Approval, RouteStepStatus.Execution] @@ -78,7 +90,9 @@ function toFormState(editing: TemplateRouteStep | undefined, nextOrderNo: number hasCondition: false, fieldName: '', operation: ComparisonOperator.GreaterThan, - value: '' + value: '', + slaHours: '', + askOverdueReason: '' }; } return { @@ -93,7 +107,9 @@ function toFormState(editing: TemplateRouteStep | undefined, nextOrderNo: number hasCondition: Boolean(editing.fieldName), fieldName: editing.fieldName ?? '', operation: editing.operation && editing.operation !== ComparisonOperator.None ? editing.operation : ComparisonOperator.GreaterThan, - value: editing.value ?? '' + value: editing.value ?? '', + slaHours: slaHoursToInput(editing.slaHours), + askOverdueReason: boolToTriState(editing.askOverdueReason) }; } @@ -164,6 +180,10 @@ export const RouteStepFormModal: React.FC = ({ submit.fail(texts.routeSteps.validationConditionNumber); return; } + if (!isValidSlaHours(form.slaHours)) { + submit.fail(texts.routeSteps.validationSlaHours); + return; + } const payload = { orderNo, @@ -175,7 +195,9 @@ export const RouteStepFormModal: React.FC = ({ headType: form.kind === 'headType' ? form.headType : null, fieldName: form.hasCondition ? form.fieldName.trim() : null, operation: form.hasCondition ? form.operation : null, - value: form.hasCondition ? form.value.trim() : null + value: form.hasCondition ? form.value.trim() : null, + slaHours: parseSlaHours(form.slaHours), + askOverdueReason: triStateToBool(form.askOverdueReason) }; const request = editing @@ -283,6 +305,23 @@ export const RouteStepFormModal: React.FC = ({ /> + + setForm({ ...form, slaHours: e.target.value })} + placeholder={texts.routeSteps.slaHoursPlaceholder} + /> + + +