using Distribution.Features.Item; using Distribution.Features.Order; using Distribution.Features.PriceList; using Distribution.Features.Ride; using Distribution.Features.Stock; using Distribution.Features.Store; using Distribution.Infra.Authorization; using Distribution.Infra.UoW; using Distribution.Models; using Microsoft.Extensions.Configuration; namespace Distribution.Features.Sale; public class SaleService( ISaleRepository repo, IOrderRepository orderRepo, IStoreRepository storeRepo, IItemRepository itemRepo, PriceListService priceListService, IStockRepository stockRepo, IRideRepository rideRepo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess, IConfiguration configuration) { private const int GlobalScopeRoleLevel = 50; 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); } private async Task ResolveUnitPriceAsync(Guid itemId, decimal? requestedUnitPrice, Models.Store store, DateTime saleDate) { 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, saleDate); 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; } private static SaleResponse ToResponse(Models.Sale sale, Models.Order order, List items) => new( sale.Id, sale.SaleDate, sale.FK_StoreId, sale.CreatedByUserId, sale.FK_RideId, sale.TotalAmount, sale.Status, sale.PaymentStatus, sale.FulfillImmediately, sale.CompletedAt, order.Id, order.Status, order.FulfilledAt, items.Select(i => new SaleItemResponse(i.FK_ItemId, i.Quantity, i.QuantityFulfilled, i.UnitPrice)).ToList(), sale.IsActive, sale.CreatedAt); public async Task> GetAllAsync(Guid? rideId, int page, int pageSize) { IEnumerable? storeIds = null; if (!HasGlobalScope && currentUser.UserId is not null) storeIds = await scopeAccess.GetAccessibleStoreIdsAsync(currentUser.UserId.Value); var total = await repo.GetCountAsync(rideId, storeIds); var sales = await repo.GetAllAsync(rideId, storeIds, page, pageSize); var result = new List(); foreach (var sale in sales) { var order = await repo.GetOrderBySaleIdAsync(sale.Id); if (order is null) continue; var items = await orderRepo.GetItemsByOrderIdAsync(order.Id); result.Add(ToResponse(sale, order, items)); } return new PagedResponse(page, pageSize, total, result); } public async Task GetByIdAsync(Guid id) { var sale = await repo.GetByIdAsync(id); if (sale is null || !sale.IsActive) return null; if (!await HasStoreScopeAsync(sale.FK_StoreId)) return null; var order = await repo.GetOrderBySaleIdAsync(id); if (order is null) return null; var items = await orderRepo.GetItemsByOrderIdAsync(order.Id); return ToResponse(sale, order, items); } public async Task?> GetByStoreIdAsync(Guid storeId) { if (!await HasStoreScopeAsync(storeId)) return null; var sales = await repo.GetByStoreIdAsync(storeId); var result = new List(); foreach (var sale in sales) { var order = await repo.GetOrderBySaleIdAsync(sale.Id); if (order is null) continue; var items = await orderRepo.GetItemsByOrderIdAsync(order.Id); result.Add(ToResponse(sale, order, items)); } return result; } public async Task CreateAsync(CreateSaleRequest request, CancellationToken ct = default) { if (!await HasStoreScopeAsync(request.StoreId)) return null; var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); 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 saleStatus = request.FulfillImmediately ? SaleStatus.Completed : SaleStatus.Pending; var orderStatus = request.FulfillImmediately ? OrderStatus.Fulfilled : OrderStatus.Pending; var fulfilledAt = request.FulfillImmediately ? DateTime.Now : (DateTime?)null; var sale = new Models.Sale { FK_StoreId = request.StoreId, SaleDate = request.SaleDate, CreatedByUserId = userId, FK_RideId = request.RideId, TotalAmount = request.TotalAmount, Status = saleStatus, FulfillImmediately = request.FulfillImmediately, CompletedAt = request.FulfillImmediately ? DateTime.Now : null }; var orderItems = new List(); foreach (var item in request.Items) { var unitPrice = await ResolveUnitPriceAsync(item.ItemId, item.UnitPrice, store, request.SaleDate); if (unitPrice is null) return null; orderItems.Add(new Models.OrderItem { FK_ItemId = item.ItemId, Quantity = item.Quantity, QuantityFulfilled = request.FulfillImmediately ? item.Quantity : 0, UnitPrice = unitPrice.Value }); } var order = new Models.Order { FK_SaleId = sale.Id, FK_StoreId = request.StoreId, OrderDate = request.SaleDate, TotalAmount = request.TotalAmount, Status = orderStatus, FulfilledAt = fulfilledAt }; foreach (var orderItem in orderItems) orderItem.FK_OrderId = order.Id; Guid? vehicleId = null; if (request.FulfillImmediately && request.RideId.HasValue) { var ride = await rideRepo.GetByIdAsync(request.RideId.Value); if (ride is null) return null; vehicleId = ride.FK_VehicleId; foreach (var item in orderItems) { var balance = await stockRepo.GetBalanceAsync(item.FK_ItemId, vehicleId.Value); if (balance is null || balance.Quantity < item.Quantity) return null; } } await uow.BeginAsync(ct); try { await repo.AddAsync(sale); await orderRepo.AddAsync(order); await orderRepo.AddItemsAsync(orderItems); if (vehicleId.HasValue) { foreach (var item in orderItems) { var balance = await stockRepo.GetBalanceAsync(item.FK_ItemId, vehicleId.Value); balance!.Quantity -= item.Quantity; balance.UpdatedAt = DateTime.Now; stockRepo.UpdateBalance(balance); await stockRepo.AddMovementAsync(new StockMovement { FK_ItemId = item.FK_ItemId, FromLocationId = vehicleId.Value, FromLocationType = LocationType.Vehicle, Quantity = item.Quantity, Type = MovementType.Sale, ReferenceId = order.Id }); } } await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return ToResponse(sale, order, orderItems); } }