using Distribution.Features.Item; using Distribution.Features.PriceList; using Distribution.Features.Store; using Distribution.Features.Stock; using Distribution.Infra.Authorization; using Distribution.Infra.UoW; using Distribution.Models; using Microsoft.Extensions.Configuration; namespace Distribution.Features.Order; public class OrderService( IOrderRepository repo, IStoreRepository storeRepo, IItemRepository itemRepo, IStockRepository stockRepo, PriceListService priceListService, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess, IConfiguration configuration) { private const int GlobalScopeRoleLevel = 50; private static OrderResponse ToResponse(Models.Order o, List items) => new(o.Id, o.OrderDate, o.FK_StoreId, o.TotalAmount, items.Select(i => new OrderItemResponse(i.FK_ItemId, i.Quantity, i.UnitPrice)).ToList(), o.IsActive, o.CreatedAt); private bool HasGlobalScope => currentUser.RoleLevel >= GlobalScopeRoleLevel; private async Task HasStoreScopeAsync(Guid storeId) { if (HasGlobalScope || currentUser.UserId is null) return HasGlobalScope; return await scopeAccess.HasStoreScopeAsync(currentUser.UserId.Value, storeId); } /// /// applyPricingAndDiscounts (price-list half, 14_pricing_and_suppliers.md): resolves the /// region-scoped active price list, then global, then Item.SalePrice, and validates any /// caller-supplied override falls within [MinPrice ?? Price, Price]. Returns null if no price /// can be resolved, or if the requested price is outside the allowed band. /// private async Task ResolveUnitPriceAsync(Guid itemId, decimal? requestedUnitPrice, Models.Store store, DateTime orderDate) { var item = await itemRepo.GetByIdAsync(itemId); if (item is null) return null; var regionId = store.Route?.FK_RegionId; var resolution = await priceListService.ResolvePriceAsync(itemId, regionId, item.SalePrice, orderDate); if (resolution is null) return null; if (!requestedUnitPrice.HasValue) return resolution.Price; var min = resolution.MinPrice ?? resolution.Price; var max = resolution.Price; if (requestedUnitPrice.Value < min || requestedUnitPrice.Value > max) return null; return requestedUnitPrice.Value; } /// /// checkStockAvailability (mainPlan.md §1): confirms the requested quantity does not exceed /// total on-hand stock for the item, summed across every warehouse/vehicle location. Does not /// reserve or decrement stock — actual decrement happens later at loading/delivery time /// (04_loading_event.md / future 05-06 ride execution), not at order placement. /// private async Task HasSufficientStockAsync(Guid itemId, decimal quantity) { var balances = await stockRepo.GetBalancesByItemAsync(itemId); var totalAvailable = balances.Sum(b => b.Quantity); return quantity <= totalAvailable; } public async Task> GetAllAsync() { var orders = (await repo.GetAllAsync()).Where(o => o.IsActive).ToList(); if (!HasGlobalScope && currentUser.UserId is not null) { var accessibleStoreIds = (await scopeAccess.GetAccessibleStoreIdsAsync(currentUser.UserId.Value)).ToHashSet(); orders = orders.Where(o => accessibleStoreIds.Contains(o.FK_StoreId)).ToList(); } var result = new List(); foreach (var order in orders) { var items = await repo.GetItemsByOrderIdAsync(order.Id); result.Add(ToResponse(order, items)); } return result; } public async Task GetByIdAsync(Guid id) { var order = await repo.GetByIdAsync(id); if (order is null || !order.IsActive) return null; if (!await HasStoreScopeAsync(order.FK_StoreId)) return null; var items = await repo.GetItemsByOrderIdAsync(id); return ToResponse(order, items); } public async Task?> GetByStoreIdAsync(Guid storeId) { if (!await HasStoreScopeAsync(storeId)) return null; var orders = await repo.GetByStoreIdAsync(storeId); var result = new List(); foreach (var order in orders) { var items = await repo.GetItemsByOrderIdAsync(order.Id); result.Add(ToResponse(order, items)); } return result; } public async Task CreateAsync(CreateOrderRequest request, CancellationToken ct = default) { if (!await HasStoreScopeAsync(request.StoreId)) return null; var store = await storeRepo.GetByIdWithRegionAsync(request.StoreId); if (store is null || !store.IsActive) return null; if (store.KycStatus != KycStatus.Verified) { var threshold = configuration.GetValue("StoreManagement:UnverifiedKycOrderThreshold"); if (request.TotalAmount > threshold) return null; } var order = new Models.Order { FK_StoreId = request.StoreId, OrderDate = request.OrderDate, TotalAmount = request.TotalAmount }; var orderItems = new List(); foreach (var i in request.Items) { if (!await HasSufficientStockAsync(i.ItemId, i.Quantity)) return null; var unitPrice = await ResolveUnitPriceAsync(i.ItemId, i.UnitPrice, store, request.OrderDate); if (unitPrice is null) return null; orderItems.Add(new Models.OrderItem { FK_OrderId = order.Id, FK_ItemId = i.ItemId, Quantity = i.Quantity, UnitPrice = unitPrice.Value }); } await uow.BeginAsync(ct); try { await repo.AddAsync(order); await repo.AddItemsAsync(orderItems); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return ToResponse(order, orderItems); } public async Task UpdateAsync(Guid id, UpdateOrderRequest request, CancellationToken ct = default) { var order = await repo.GetByIdAsync(id); if (order is null || !order.IsActive) return null; if (!await HasStoreScopeAsync(order.FK_StoreId)) return null; order.OrderDate = request.OrderDate; order.TotalAmount = request.TotalAmount; await uow.BeginAsync(ct); try { repo.Update(order); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } var items = await repo.GetItemsByOrderIdAsync(id); return ToResponse(order, items); } public async Task DeleteAsync(Guid id, CancellationToken ct = default) { var order = await repo.GetByIdAsync(id); if (order is null || !order.IsActive) return false; if (!await HasStoreScopeAsync(order.FK_StoreId)) return false; await uow.BeginAsync(ct); try { repo.Remove(order); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return true; } }