переход на реальное api
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
namespace Tnsu.ApprovalRoute.Api.Common;
|
||||
|
||||
public sealed record ApiResponse<T>(T Data, IReadOnlyList<string> Warnings);
|
||||
|
||||
public sealed record ApiError(string Message);
|
||||
@@ -12,7 +12,7 @@ public static class ResultExtensions
|
||||
return result.Errors[0].ToActionResult();
|
||||
|
||||
var warnings = result.Reasons.OfType<Warning>().Select(w => w.Message).ToList();
|
||||
return new OkObjectResult(new { data = result.Value, warnings });
|
||||
return new OkObjectResult(new ApiResponse<T>(result.Value, warnings));
|
||||
}
|
||||
|
||||
public static IActionResult ToActionResult(this Result result)
|
||||
@@ -25,9 +25,9 @@ public static class ResultExtensions
|
||||
|
||||
private static IActionResult ToActionResult(this IError error) => error switch
|
||||
{
|
||||
NotFoundError => new NotFoundObjectResult(new { message = error.Message }),
|
||||
ValidationError => new BadRequestObjectResult(new { message = error.Message }),
|
||||
ConflictError => new ConflictObjectResult(new { message = error.Message }),
|
||||
_ => new ObjectResult(new { message = error.Message }) { StatusCode = StatusCodes.Status500InternalServerError }
|
||||
NotFoundError => new NotFoundObjectResult(new ApiError(error.Message)),
|
||||
ValidationError => new BadRequestObjectResult(new ApiError(error.Message)),
|
||||
ConflictError => new ConflictObjectResult(new ApiError(error.Message)),
|
||||
_ => new ObjectResult(new ApiError(error.Message)) { StatusCode = StatusCodes.Status500InternalServerError }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,25 +8,36 @@ namespace Tnsu.ApprovalRoute.Api.Controllers;
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/request-types")]
|
||||
[Produces("application/json")]
|
||||
public sealed class RequestTypesController(RequestTypeService service) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<RequestTypeDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> List(CancellationToken ct) =>
|
||||
(await service.ListAsync(ct)).ToActionResult();
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
[ProducesResponseType(typeof(ApiResponse<RequestTypeDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Get(Guid id, CancellationToken ct) =>
|
||||
(await service.GetAsync(id, ct)).ToActionResult();
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(ApiResponse<RequestTypeDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> Create(CreateRequestTypeRequest body, CancellationToken ct) =>
|
||||
(await service.CreateAsync(body, ct)).ToActionResult();
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[ProducesResponseType(typeof(ApiResponse<RequestTypeDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Update(Guid id, UpdateRequestTypeRequest body, CancellationToken ct) =>
|
||||
(await service.UpdateAsync(id, body, ct)).ToActionResult();
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct) =>
|
||||
(await service.DeleteAsync(id, ct)).ToActionResult();
|
||||
}
|
||||
|
||||
@@ -8,25 +8,37 @@ namespace Tnsu.ApprovalRoute.Api.Controllers;
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/template-routes/{routeId:guid}/steps")]
|
||||
[Produces("application/json")]
|
||||
public sealed class TemplateRouteStepsController(TemplateRouteStepService service) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<TemplateRouteStepDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> List(Guid routeId, CancellationToken ct) =>
|
||||
(await service.ListAsync(routeId, ct)).ToActionResult();
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TemplateRouteStepDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Get(Guid routeId, Guid id, CancellationToken ct) =>
|
||||
(await service.GetAsync(routeId, id, ct)).ToActionResult();
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(ApiResponse<TemplateRouteStepDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Create(Guid routeId, CreateTemplateRouteStepRequest body, CancellationToken ct) =>
|
||||
(await service.CreateAsync(routeId, body, ct)).ToActionResult();
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TemplateRouteStepDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Update(Guid routeId, Guid id, UpdateTemplateRouteStepRequest body, CancellationToken ct) =>
|
||||
(await service.UpdateAsync(routeId, id, body, ct)).ToActionResult();
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(Guid routeId, Guid id, CancellationToken ct) =>
|
||||
(await service.DeleteAsync(routeId, id, ct)).ToActionResult();
|
||||
}
|
||||
|
||||
@@ -8,29 +8,46 @@ namespace Tnsu.ApprovalRoute.Api.Controllers;
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/template-routes")]
|
||||
[Produces("application/json")]
|
||||
public sealed class TemplateRoutesController(TemplateRouteService service) : ControllerBase
|
||||
{
|
||||
[HttpGet("resolve")]
|
||||
[ProducesResponseType(typeof(ApiResponse<ResolveTemplateRouteResultDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Resolve([FromQuery] ResolveTemplateRouteRequest request, CancellationToken ct) =>
|
||||
(await service.ResolveAsync(request, ct)).ToActionResult();
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<TemplateRouteDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> List([FromQuery] Guid? requestTypeId, CancellationToken ct) =>
|
||||
(await service.ListAsync(requestTypeId, ct)).ToActionResult();
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TemplateRouteDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Get(Guid id, CancellationToken ct) =>
|
||||
(await service.GetAsync(id, ct)).ToActionResult();
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(ApiResponse<TemplateRouteDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Create(CreateTemplateRouteRequest body, CancellationToken ct) =>
|
||||
(await service.CreateAsync(body, ct)).ToActionResult();
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TemplateRouteDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Update(Guid id, UpdateTemplateRouteRequest body, CancellationToken ct) =>
|
||||
(await service.UpdateAsync(id, body, ct)).ToActionResult();
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct) =>
|
||||
(await service.DeleteAsync(id, ct)).ToActionResult();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Tnsu.ApprovalRoute.Api.Common;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Api.Filters;
|
||||
|
||||
@@ -23,10 +24,8 @@ public sealed class ValidationActionFilter(IServiceProvider serviceProvider) : I
|
||||
if (result.IsValid)
|
||||
continue;
|
||||
|
||||
context.Result = new BadRequestObjectResult(new
|
||||
{
|
||||
message = string.Join(" ", result.Errors.Select(e => e.ErrorMessage))
|
||||
});
|
||||
context.Result = new BadRequestObjectResult(
|
||||
new ApiError(string.Join(" ", result.Errors.Select(e => e.ErrorMessage))));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,22 +6,22 @@ public sealed record TemplateRouteDto(
|
||||
Guid Id,
|
||||
RouteType RouteType,
|
||||
Guid RequestTypeId,
|
||||
Guid? DepartmentId,
|
||||
Guid CompanyId,
|
||||
string? Department,
|
||||
string Company,
|
||||
bool ByProject,
|
||||
bool RequiresControl);
|
||||
|
||||
public sealed record CreateTemplateRouteRequest(
|
||||
RouteType RouteType,
|
||||
Guid RequestTypeId,
|
||||
Guid? DepartmentId,
|
||||
Guid CompanyId,
|
||||
string? Department,
|
||||
string Company,
|
||||
bool ByProject,
|
||||
bool RequiresControl);
|
||||
|
||||
public sealed record UpdateTemplateRouteRequest(
|
||||
RouteType RouteType,
|
||||
Guid? DepartmentId,
|
||||
Guid CompanyId,
|
||||
string? Department,
|
||||
string Company,
|
||||
bool ByProject,
|
||||
bool RequiresControl);
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Tnsu.ApprovalRoute.Application.TemplateRoutes;
|
||||
|
||||
public sealed class TemplateRouteService(
|
||||
ApprovalRouteDbContext db,
|
||||
HttpTnsuPortalClient portalClient,
|
||||
ITnsuPortalClient portalClient,
|
||||
ICurrentUser currentUser)
|
||||
{
|
||||
public async Task<Result<IReadOnlyList<TemplateRouteDto>>> ListAsync(Guid? requestTypeId, CancellationToken cancellationToken)
|
||||
@@ -22,7 +22,7 @@ public sealed class TemplateRouteService(
|
||||
|
||||
var items = await query
|
||||
.Select(x => new TemplateRouteDto(
|
||||
x.Id, x.RouteType, x.RequestTypeId, x.DepartmentId, x.CompanyId, x.ByProject, x.RequiresControl))
|
||||
x.Id, x.RouteType, x.RequestTypeId, x.Department, x.Company, x.ByProject, x.RequiresControl))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Result.Ok<IReadOnlyList<TemplateRouteDto>>(items);
|
||||
@@ -43,7 +43,8 @@ public sealed class TemplateRouteService(
|
||||
if (!requestTypeExists)
|
||||
return Result.Fail<TemplateRouteDto>(new NotFoundError("Тип заявки", r.RequestTypeId));
|
||||
|
||||
var duplicate = await HasDuplicateAsync(r.RequestTypeId, r.DepartmentId, r.CompanyId, r.ByProject, excludeId: null, cancellationToken);
|
||||
var department = Normalize(r.Department);
|
||||
var duplicate = await HasDuplicateAsync(r.RequestTypeId, department, r.Company, r.ByProject, excludeId: null, cancellationToken);
|
||||
if (duplicate)
|
||||
return Result.Fail<TemplateRouteDto>(new ConflictError(
|
||||
"Маршрут с таким типом заявки, отделом, компанией и признаком «по проекту» уже существует."));
|
||||
@@ -52,8 +53,8 @@ public sealed class TemplateRouteService(
|
||||
{
|
||||
RouteType = r.RouteType,
|
||||
RequestTypeId = r.RequestTypeId,
|
||||
DepartmentId = r.DepartmentId,
|
||||
CompanyId = r.CompanyId,
|
||||
Department = department,
|
||||
Company = r.Company,
|
||||
ByProject = r.ByProject,
|
||||
RequiresControl = r.RequiresControl
|
||||
};
|
||||
@@ -69,14 +70,15 @@ public sealed class TemplateRouteService(
|
||||
if (entity is null)
|
||||
return Result.Fail<TemplateRouteDto>(new NotFoundError("Маршрут", id));
|
||||
|
||||
var duplicate = await HasDuplicateAsync(entity.RequestTypeId, r.DepartmentId, r.CompanyId, r.ByProject, excludeId: entity.Id, cancellationToken);
|
||||
var department = Normalize(r.Department);
|
||||
var duplicate = await HasDuplicateAsync(entity.RequestTypeId, department, r.Company, r.ByProject, excludeId: entity.Id, cancellationToken);
|
||||
if (duplicate)
|
||||
return Result.Fail<TemplateRouteDto>(new ConflictError(
|
||||
"Маршрут с таким типом заявки, отделом, компанией и признаком «по проекту» уже существует."));
|
||||
|
||||
entity.RouteType = r.RouteType;
|
||||
entity.DepartmentId = r.DepartmentId;
|
||||
entity.CompanyId = r.CompanyId;
|
||||
entity.Department = department;
|
||||
entity.Company = r.Company;
|
||||
entity.ByProject = r.ByProject;
|
||||
entity.RequiresControl = r.RequiresControl;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
@@ -109,23 +111,28 @@ public sealed class TemplateRouteService(
|
||||
return Result.Fail<ResolveTemplateRouteResultDto>(new NotFoundError("Тип заявки", request.RequestTypeId));
|
||||
|
||||
var employee = await portalClient.GetEmployeeByEmailAsync(email, cancellationToken);
|
||||
if (employee is null)
|
||||
return Result.Fail<ResolveTemplateRouteResultDto>(
|
||||
new NotFoundError("Сотрудник по email в справочнике api.tnsu.kz не найден", Guid.Empty));
|
||||
|
||||
var byProject = request.ProjectId is not null;
|
||||
var department = Normalize(employee.Department);
|
||||
|
||||
var route = await db.TemplateRoutes.AsNoTracking()
|
||||
.Include(x => x.Steps)
|
||||
.Where(x =>
|
||||
x.RequestTypeId == request.RequestTypeId &&
|
||||
x.CompanyId == employee.CompanyId &&
|
||||
x.Company == employee.Company &&
|
||||
x.ByProject == byProject &&
|
||||
x.DepartmentId == employee.DepartmentId)
|
||||
x.Department == department)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
?? await db.TemplateRoutes.AsNoTracking()
|
||||
.Include(x => x.Steps)
|
||||
.Where(x =>
|
||||
x.RequestTypeId == request.RequestTypeId &&
|
||||
x.CompanyId == employee.CompanyId &&
|
||||
x.Company == employee.Company &&
|
||||
x.ByProject == byProject &&
|
||||
x.DepartmentId == null)
|
||||
x.Department == null)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (route is null)
|
||||
@@ -147,25 +154,25 @@ public sealed class TemplateRouteService(
|
||||
}
|
||||
|
||||
private Task<bool> HasDuplicateAsync(
|
||||
Guid requestTypeId, Guid? departmentId, Guid companyId, bool byProject, Guid? excludeId, CancellationToken cancellationToken) =>
|
||||
Guid requestTypeId, string? department, string company, bool byProject, Guid? excludeId, CancellationToken cancellationToken) =>
|
||||
db.TemplateRoutes.AnyAsync(x =>
|
||||
x.Id != excludeId &&
|
||||
x.RequestTypeId == requestTypeId &&
|
||||
x.DepartmentId == departmentId &&
|
||||
x.CompanyId == companyId &&
|
||||
x.Department == department &&
|
||||
x.Company == company &&
|
||||
x.ByProject == byProject,
|
||||
cancellationToken);
|
||||
|
||||
private static ResolvedRouteStepDto ResolveStep(TemplateRouteStep step, IReadOnlyList<ProjectExecutorInfo> projectExecutors)
|
||||
{
|
||||
// Для шагов с ролью в проекте фактический исполнитель приходит из данных проекта (api.tnsu.kz),
|
||||
// а не из статичного ExecutorEmail шаблона — тот используется только для шагов без привязки к проекту.
|
||||
// Для шагов с ролью в проекте фактический исполнитель (email) приходит из данных проекта (api.tnsu.kz);
|
||||
// должность берётся из шаблона, т.к. проект её не отдаёт. Для шагов без роли — статичный ExecutorEmail шаблона.
|
||||
if (step.ProjectRole != ProjectRole.None)
|
||||
{
|
||||
var projectExecutor = projectExecutors.FirstOrDefault(e => e.ProjectRole == step.ProjectRole);
|
||||
if (projectExecutor is not null)
|
||||
return new ResolvedRouteStepDto(
|
||||
step.Id, step.OrderNo, projectExecutor.Email, projectExecutor.Position,
|
||||
step.Id, step.OrderNo, projectExecutor.Email, step.Position,
|
||||
step.ProjectRole, step.HeadType, step.Status, step.AllowReassignToOtherApprovers);
|
||||
}
|
||||
|
||||
@@ -174,6 +181,8 @@ public sealed class TemplateRouteService(
|
||||
step.ProjectRole, step.HeadType, step.Status, step.AllowReassignToOtherApprovers);
|
||||
}
|
||||
|
||||
private static string? Normalize(string? value) => string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
|
||||
private static TemplateRouteDto ToDto(TemplateRoute e) => new(
|
||||
e.Id, e.RouteType, e.RequestTypeId, e.DepartmentId, e.CompanyId, e.ByProject, e.RequiresControl);
|
||||
e.Id, e.RouteType, e.RequestTypeId, e.Department, e.Company, e.ByProject, e.RequiresControl);
|
||||
}
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ public sealed class CreateTemplateRouteRequestValidator : AbstractValidator<Crea
|
||||
{
|
||||
public CreateTemplateRouteRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.CompanyId).NotEqual(Guid.Empty).WithMessage("Компания обязательна.");
|
||||
RuleFor(x => x.Company).NotEmpty().WithMessage("Компания обязательна.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public sealed class UpdateTemplateRouteRequestValidator : AbstractValidator<Upda
|
||||
{
|
||||
public UpdateTemplateRouteRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.CompanyId).NotEqual(Guid.Empty).WithMessage("Компания обязательна.");
|
||||
RuleFor(x => x.Company).NotEmpty().WithMessage("Компания обязательна.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@ public class TemplateRoute
|
||||
public RouteType RouteType { get; set; } = RouteType.Sequential;
|
||||
public Guid RequestTypeId { get; set; }
|
||||
|
||||
// Внешние справочники (1С/ZUP) — хранятся как ссылки без FK в этой БД.
|
||||
public Guid? DepartmentId { get; set; }
|
||||
public Guid CompanyId { get; set; }
|
||||
// Компания и отдел инициатора — названия из справочника AD (api-tnsu-portal /Employee/ByEmail),
|
||||
// хранятся строками без FK в этой БД. Отдел опционален, компания обязательна.
|
||||
public string? Department { get; set; }
|
||||
public string Company { get; set; } = string.Empty;
|
||||
public bool ByProject { get; set; }
|
||||
|
||||
public bool RequiresControl { get; set; }
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
namespace Tnsu.ApprovalRoute.Domain.Enums;
|
||||
|
||||
// Роли в проекте — соответствуют именованным полям проекта в api-tnsu-portal
|
||||
// (project_manager / planner / manager_hse / security).
|
||||
public enum ProjectRole
|
||||
{
|
||||
None,
|
||||
Manager,
|
||||
Engineer,
|
||||
Observer
|
||||
ProjectManager,
|
||||
Planner,
|
||||
ManagerHSE,
|
||||
Security
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ public static class InfrastructureServiceCollectionExtensions
|
||||
|
||||
services.Configure<TnsuPortalOptions>(configuration.GetSection("TnsuPortal"));
|
||||
services.AddHttpClient<TnsuPortalTokenProvider>();
|
||||
services.AddHttpClient<HttpTnsuPortalClient>((sp, client) =>
|
||||
services.AddHttpClient<ITnsuPortalClient, HttpTnsuPortalClient>((sp, client) =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<TnsuPortalOptions>>().Value;
|
||||
client.BaseAddress = new Uri(options.BaseUrl);
|
||||
|
||||
@@ -22,8 +22,10 @@ public class ApprovalRouteDbContext(DbContextOptions<ApprovalRouteDbContext> opt
|
||||
{
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.RouteType).HasConversion<string>().HasMaxLength(32).IsRequired();
|
||||
e.Property(x => x.Company).HasMaxLength(256).IsRequired();
|
||||
e.Property(x => x.Department).HasMaxLength(256);
|
||||
e.HasOne(x => x.RequestType).WithMany(x => x.Routes).HasForeignKey(x => x.RequestTypeId);
|
||||
e.HasIndex(x => new { x.RequestTypeId, x.DepartmentId, x.CompanyId, x.ByProject }).IsUnique();
|
||||
e.HasIndex(x => new { x.RequestTypeId, x.Department, x.Company, x.ByProject }).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<TemplateRouteStep>(e =>
|
||||
|
||||
+12
-9
@@ -12,7 +12,7 @@ using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
||||
namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApprovalRouteDbContext))]
|
||||
[Migration("20260723074943_InitialCreate")]
|
||||
[Migration("20260724071302_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@@ -61,13 +61,16 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("by_project");
|
||||
|
||||
b.Property<Guid>("CompanyId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("company_id");
|
||||
b.Property<string>("Company")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("company");
|
||||
|
||||
b.Property<Guid?>("DepartmentId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("department_id");
|
||||
b.Property<string>("Department")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("department");
|
||||
|
||||
b.Property<Guid>("RequestTypeId")
|
||||
.HasColumnType("uuid")
|
||||
@@ -86,9 +89,9 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_template_routes");
|
||||
|
||||
b.HasIndex("RequestTypeId", "DepartmentId", "CompanyId", "ByProject")
|
||||
b.HasIndex("RequestTypeId", "Department", "Company", "ByProject")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_template_routes_request_type_id_department_id_company_id_by");
|
||||
.HasDatabaseName("ix_template_routes_request_type_id_department_company_by_proje");
|
||||
|
||||
b.ToTable("template_routes", (string)null);
|
||||
});
|
||||
+4
-4
@@ -31,8 +31,8 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
route_type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
request_type_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
department_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
company_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
department = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
company = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
by_project = table.Column<bool>(type: "boolean", nullable: false),
|
||||
requires_control = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
@@ -78,9 +78,9 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
columns: new[] { "route_id", "order_no" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_template_routes_request_type_id_department_id_company_id_by",
|
||||
name: "ix_template_routes_request_type_id_department_company_by_proje",
|
||||
table: "template_routes",
|
||||
columns: new[] { "request_type_id", "department_id", "company_id", "by_project" },
|
||||
columns: new[] { "request_type_id", "department", "company", "by_project" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
+11
-8
@@ -58,13 +58,16 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("by_project");
|
||||
|
||||
b.Property<Guid>("CompanyId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("company_id");
|
||||
b.Property<string>("Company")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("company");
|
||||
|
||||
b.Property<Guid?>("DepartmentId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("department_id");
|
||||
b.Property<string>("Department")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("department");
|
||||
|
||||
b.Property<Guid>("RequestTypeId")
|
||||
.HasColumnType("uuid")
|
||||
@@ -83,9 +86,9 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_template_routes");
|
||||
|
||||
b.HasIndex("RequestTypeId", "DepartmentId", "CompanyId", "ByProject")
|
||||
b.HasIndex("RequestTypeId", "Department", "Company", "ByProject")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_template_routes_request_type_id_department_id_company_id_by");
|
||||
.HasDatabaseName("ix_template_routes_request_type_id_department_company_by_proje");
|
||||
|
||||
b.ToTable("template_routes", (string)null);
|
||||
});
|
||||
|
||||
@@ -1,20 +1,74 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Tnsu.ApprovalRoute.Domain.Enums;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
|
||||
|
||||
// Клиент api.tnsu.kz (api-tnsu-portal). GetEmployeeByEmailAsync и GetProjectExecutorsAsync — заглушки:
|
||||
// на момент написания у api-tnsu-portal нет эндпоинта GET /Employee/ByEmail (есть только ByIIN/Manager),
|
||||
// а форма ответа GET /Project/{oid} в его openapi-спеке не типизирована. Заменить на реальные вызовы,
|
||||
// когда эндпоинты будут готовы — токен уже прокидывается через TnsuPortalTokenProvider.
|
||||
public sealed class HttpTnsuPortalClient(HttpClient httpClient, TnsuPortalTokenProvider tokenProvider)
|
||||
// Клиент api.tnsu.kz (api-tnsu-portal). Сервисный токен прокидывается через TnsuPortalTokenProvider.
|
||||
public sealed class HttpTnsuPortalClient(HttpClient httpClient, TnsuPortalTokenProvider tokenProvider) : ITnsuPortalClient
|
||||
{
|
||||
public Task<EmployeeDirectoryInfo> GetEmployeeByEmailAsync(string email, CancellationToken cancellationToken)
|
||||
public async Task<EmployeeDirectoryInfo?> GetEmployeeByEmailAsync(string email, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new EmployeeDirectoryInfo(
|
||||
DepartmentId: Guid.Parse("00000000-0000-0000-0000-000000000001"),
|
||||
CompanyId: Guid.Parse("00000000-0000-0000-0000-000000000002")));
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"/Employee/ByEmail?email={Uri.EscapeDataString(email)}");
|
||||
await AuthorizeAsync(request, cancellationToken);
|
||||
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
return null;
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var employee = await response.Content.ReadFromJsonAsync<EmployeeResponse>(cancellationToken);
|
||||
if (employee is null)
|
||||
return null;
|
||||
|
||||
return new EmployeeDirectoryInfo(employee.Company ?? string.Empty, employee.Section ?? string.Empty);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<ProjectExecutorInfo>> GetProjectExecutorsAsync(Guid projectId, CancellationToken cancellationToken)
|
||||
public async Task<IReadOnlyList<ProjectExecutorInfo>> GetProjectExecutorsAsync(Guid projectId, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<ProjectExecutorInfo>>([]);
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"/Project/{projectId}");
|
||||
await AuthorizeAsync(request, cancellationToken);
|
||||
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
return [];
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var project = await response.Content.ReadFromJsonAsync<ProjectResponse>(cancellationToken);
|
||||
if (project is null)
|
||||
return [];
|
||||
|
||||
// Каждая роль проекта — email сотрудника в соответствующем поле (project_manager / planner / manager_hse / security).
|
||||
var executors = new List<ProjectExecutorInfo>();
|
||||
AddIfPresent(executors, ProjectRole.ProjectManager, project.ProjectManager);
|
||||
AddIfPresent(executors, ProjectRole.Planner, project.Planner);
|
||||
AddIfPresent(executors, ProjectRole.ManagerHSE, project.ManagerHSE);
|
||||
AddIfPresent(executors, ProjectRole.Security, project.Security);
|
||||
|
||||
return executors;
|
||||
}
|
||||
|
||||
private static void AddIfPresent(List<ProjectExecutorInfo> executors, ProjectRole role, string? email)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(email))
|
||||
executors.Add(new ProjectExecutorInfo(role, email));
|
||||
}
|
||||
|
||||
private async Task AuthorizeAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var token = await tokenProvider.GetAccessTokenAsync(cancellationToken);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
|
||||
private sealed record EmployeeResponse(
|
||||
[property: JsonPropertyName("company")] string? Company,
|
||||
[property: JsonPropertyName("section")] string? Section);
|
||||
|
||||
private sealed record ProjectResponse(
|
||||
[property: JsonPropertyName("projectManager")] string? ProjectManager,
|
||||
[property: JsonPropertyName("planner")] string? Planner,
|
||||
[property: JsonPropertyName("managerHSE")] string? ManagerHSE,
|
||||
[property: JsonPropertyName("security")] string? Security);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
|
||||
|
||||
public interface ITnsuPortalClient
|
||||
{
|
||||
Task<EmployeeDirectoryInfo?> GetEmployeeByEmailAsync(string email, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<ProjectExecutorInfo>> GetProjectExecutorsAsync(Guid projectId, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -2,6 +2,6 @@ using Tnsu.ApprovalRoute.Domain.Enums;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
|
||||
|
||||
public sealed record EmployeeDirectoryInfo(Guid DepartmentId, Guid CompanyId);
|
||||
public sealed record EmployeeDirectoryInfo(string Company, string Department);
|
||||
|
||||
public sealed record ProjectExecutorInfo(string Email, string Position, ProjectRole ProjectRole);
|
||||
public sealed record ProjectExecutorInfo(ProjectRole ProjectRole, string Email);
|
||||
|
||||
Reference in New Issue
Block a user