using Distribution.Features.Item; using Distribution.Infra.UoW; namespace Distribution.Features.PriceList; public class PriceListService(IPriceListRepository repo, IItemRepository itemRepo, IUnitOfWork uow) { private static PriceListItemResponse MapItem(Models.PriceListItem i) => new(i.Id, i.FK_ItemId, i.Item.Sku, i.Item.Name, i.Price, i.MinPrice); private static PriceListResponse MapDetail(Models.PriceList pl) => new(pl.Id, pl.Name, pl.FK_RegionId, pl.Region?.Name, pl.EffectiveFrom, pl.EffectiveTo, pl.IsActive, pl.CreatedAt, pl.Items.Where(i => i.IsActive).Select(MapItem).ToList()); public async Task> GetAllAsync(Guid? regionId = null) { var lists = await repo.GetAllAsync(regionId); return lists.Select(pl => new PriceListResponse( pl.Id, pl.Name, pl.FK_RegionId, pl.Region?.Name, pl.EffectiveFrom, pl.EffectiveTo, pl.IsActive, pl.CreatedAt, [])).ToList(); } public async Task GetByIdAsync(Guid id) { var pl = await repo.GetByIdAsync(id); if (pl is null || !pl.IsActive) return null; return MapDetail(pl); } public async Task CreateAsync(CreatePriceListRequest request, CancellationToken ct = default) { var priceList = new Models.PriceList { Name = request.Name, FK_RegionId = request.RegionId, EffectiveFrom = request.EffectiveFrom, EffectiveTo = request.EffectiveTo }; await uow.BeginAsync(ct); try { await repo.AddAsync(priceList); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return (await GetByIdAsync(priceList.Id))!; } public async Task UpdateAsync(Guid id, UpdatePriceListRequest request, CancellationToken ct = default) { var priceList = await repo.GetByIdAsync(id); if (priceList is null || !priceList.IsActive) return null; foreach (var item in priceList.Items.Where(i => i.IsActive)) { if (await repo.HasOverlapAsync(item.FK_ItemId, request.RegionId, request.EffectiveFrom, request.EffectiveTo, id)) throw new InvalidOperationException( $"Overlapping price list already exists for item '{item.Item.Sku}' in the same region for the requested date range."); } priceList.Name = request.Name; priceList.FK_RegionId = request.RegionId; priceList.EffectiveFrom = request.EffectiveFrom; priceList.EffectiveTo = request.EffectiveTo; await uow.BeginAsync(ct); try { repo.Update(priceList); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return (await GetByIdAsync(id))!; } public async Task DeleteAsync(Guid id, CancellationToken ct = default) { var priceList = await repo.GetByIdAsync(id); if (priceList is null || !priceList.IsActive) return false; await uow.BeginAsync(ct); try { foreach (var item in priceList.Items.Where(i => i.IsActive)) repo.RemoveItem(item); repo.Remove(priceList); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return true; } public async Task UpsertItemAsync(Guid priceListId, string sku, UpsertPriceListItemRequest request, CancellationToken ct = default) { var priceList = await repo.GetByIdAsync(priceListId); if (priceList is null || !priceList.IsActive) return null; var item = await itemRepo.GetBySkuAsync(sku); if (item is null) return null; if (request.MinPrice.HasValue && request.MinPrice.Value > request.Price) throw new InvalidOperationException( $"MinPrice ({request.MinPrice}) cannot exceed Price ({request.Price}) for item '{sku}'."); if (await repo.HasOverlapAsync(item.Id, priceList.FK_RegionId, priceList.EffectiveFrom, priceList.EffectiveTo, priceListId)) throw new InvalidOperationException( $"Overlapping price list already exists for item '{sku}' in the same region for this date range."); var existing = await repo.GetItemAsync(priceListId, item.Id); await uow.BeginAsync(ct); try { if (existing is null) { await repo.AddItemAsync(new Models.PriceListItem { FK_PriceListId = priceListId, FK_ItemId = item.Id, Price = request.Price, MinPrice = request.MinPrice }); } else { existing.Price = request.Price; existing.MinPrice = request.MinPrice; repo.UpdateItem(existing); } await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return (await GetByIdAsync(priceListId))!; } public async Task RemoveItemAsync(Guid priceListId, string sku, CancellationToken ct = default) { var priceList = await repo.GetByIdAsync(priceListId); if (priceList is null || !priceList.IsActive) return null; var item = await itemRepo.GetBySkuAsync(sku); if (item is null) return null; var existing = await repo.GetItemAsync(priceListId, item.Id); if (existing is null) return null; await uow.BeginAsync(ct); try { repo.RemoveItem(existing); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return (await GetByIdAsync(priceListId))!; } /// /// applyPricingAndDiscounts (price-list half): region-scoped active price list -> global /// active price list -> Item.SalePrice -> null if none apply. The returned PriceResolution /// carries MinPrice so the caller can validate a negotiated UnitPrice against the band; /// the Item.SalePrice fallback has no MinPrice (fixed, non-negotiable). /// public async Task ResolvePriceAsync(Guid itemId, Guid? regionId, decimal? itemSalePrice, DateTime orderDate) { if (regionId.HasValue) { var regionMatches = await repo.GetActiveItemsAsync(itemId, regionId, orderDate); if (regionMatches.Count > 0) return new PriceResolution(regionMatches[0].Price, regionMatches[0].MinPrice); } var globalMatches = await repo.GetActiveItemsAsync(itemId, null, orderDate); if (globalMatches.Count > 0) return new PriceResolution(globalMatches[0].Price, globalMatches[0].MinPrice); return itemSalePrice.HasValue ? new PriceResolution(itemSalePrice.Value, null) : null; } }