using System.Globalization; using System.Text.Json; using FluentResults; using Microsoft.EntityFrameworkCore; using Tnsu.ApprovalRoute.Application.Common.Errors; using Tnsu.ApprovalRoute.Application.Common.Interfaces; using Tnsu.ApprovalRoute.Application.ListPartitioning; using Tnsu.ApprovalRoute.Infrastructure.Persistence; using Tnsu.ApprovalRoute.Infrastructure.SharePoint; namespace Tnsu.ApprovalRoute.Application.BasisRequests; /// /// Заявки, поданные по одному основанию (колонка BasisKey: «ct:<Id>», /// «need:<oid>») — для таблицы прежних оплат в «Заявке на оплату». /// /// Читаем от имени ПРИЛОЖЕНИЯ: права на элементы заявок разорваны поштучно, и /// под своей учёткой заявитель видел бы только собственные заявки — сумма /// прежних оплат по СТ занижалась бы, а проверка превышения пропускала бы /// переплату. Открыть чужую заявку при этом может только тот, у кого на неё /// есть права: CanOpen проверяется по каждому элементу для вызывающего. /// public sealed class BasisRequestsService( ApprovalRouteDbContext db, ISharePointClient sharePoint, ListPartitionRegistry partitionRegistry, ICurrentUser currentUser) { private const string BasisKeyField = "BasisKey"; private const string AmountField = "PayAmount"; // Больше по одному основанию не бывает: СТ оплачивают частями, но не сотнями. private const int MaxItemsPerPartition = 500; // Проверка прав — отдельный запрос на элемент; ограничиваем параллельность, // чтобы не упереться в троттлинг SharePoint (429). private const int MaxParallelPermissionChecks = 4; public async Task>> GetByBasisAsync( int requestTypeId, string basisKey, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(basisKey)) return Result.Fail>(new ValidationError("Не указано основание (basisKey).")); var requestType = await db.RequestTypes.AsNoTracking() .FirstOrDefaultAsync(x => x.Id == requestTypeId, cancellationToken); if (requestType is null) return Result.Fail>(new NotFoundError("Тип заявки", requestTypeId)); if (string.IsNullOrWhiteSpace(requestType.ListId) || string.IsNullOrWhiteSpace(requestType.SiteUrl)) return Result.Ok>([]); var siteUrl = requestType.SiteUrl; var archived = await partitionRegistry.GetArchivedAsync(requestType, cancellationToken); var current = await partitionRegistry.BuildCurrentAsync(requestType, archived, cancellationToken); var listIds = archived.Select(x => x.ListId).Append(current.ListId).ToList(); // Кавычка в OData-литерале удваивается; сам литерал кодируем — в ключе бывают «:» и прочее. var literal = Uri.EscapeDataString(basisKey.Trim().Replace("'", "''")); var query = $"$select=Id,RequestUniqueCode,RequestStatus,{AmountField}" + $"&$filter={BasisKeyField} eq '{literal}'&$orderby=Id&$top={MaxItemsPerPartition}"; var found = new List<(JsonElement Item, string ListId)>(); foreach (var listId in listIds) { var items = await sharePoint.GetItemsByListUrlAsync(siteUrl, listId, query, cancellationToken); found.AddRange(items.Select(i => (i, listId))); } var email = currentUser.Email; using var gate = new SemaphoreSlim(MaxParallelPermissionChecks); var result = await Task.WhenAll(found.Select(async x => { var id = GetInt(x.Item, "Id") ?? 0; var canOpen = false; if (!string.IsNullOrWhiteSpace(email) && id > 0) { await gate.WaitAsync(cancellationToken); try { canOpen = await sharePoint.CanUserViewItemAsync(siteUrl, x.ListId, id, email, cancellationToken); } finally { gate.Release(); } } return new BasisRequestDto( id, x.ListId, GetString(x.Item, "RequestUniqueCode"), GetString(x.Item, "RequestStatus") ?? string.Empty, GetDouble(x.Item, AmountField) ?? 0, canOpen); })); return Result.Ok>(result); } private static string? GetString(JsonElement e, string name) => e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null; private static int? GetInt(JsonElement e, string name) => e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number && v.TryGetInt32(out var i) ? i : null; private static double? GetDouble(JsonElement e, string name) { if (!e.TryGetProperty(name, out var v)) return null; if (v.ValueKind == JsonValueKind.Number && v.TryGetDouble(out var d)) return d; if (v.ValueKind == JsonValueKind.String && double.TryParse(v.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)) return parsed; return null; } }