using Distribution.Infra.UoW; namespace Distribution.Features.Item; public class ItemService(IItemRepository repo, IUnitOfWork uow, SysConfig.SysConfigService sysConfig) { private static UomConversionResponse MapConversion(Models.UomConversion c) => new(c.Id, c.FK_FromUomId, c.FromUom.Name, c.FK_ToUomId, c.ToUom.Name, c.Factor); private static ItemSummaryResponse MapSummary(Models.Item i) => new(i.Id, i.Sku, i.Name, i.FK_CategoryId, i.Category.Name, i.FK_SubCategoryId, i.SubCategory?.Name, i.FK_BrandId, i.Brand?.Name, i.FK_BaseUomId, i.BaseUom.Name, i.SalePrice, i.ReorderLevel, i.TracksSerialNumbers, i.TracksBatches, i.TracksExpiryDate, i.HasWarranty, i.WarrantyPeriodDays, i.AllowCustomWarrantyPeriod, i.SyncSource, i.IsActive, i.CreatedAt); private static ItemDetailResponse MapDetail(Models.Item i) => new(i.Id, i.Sku, i.Name, i.Description, i.FK_CategoryId, i.Category.Name, i.FK_SubCategoryId, i.SubCategory?.Name, i.FK_BrandId, i.Brand?.Name, i.FK_BaseUomId, i.BaseUom.Name, i.SalePrice, i.ReorderLevel, i.TracksSerialNumbers, i.TracksBatches, i.TracksExpiryDate, i.HasWarranty, i.WarrantyPeriodDays, i.AllowCustomWarrantyPeriod, i.SyncSource, i.IsActive, i.CreatedAt, i.Conversions.Select(MapConversion).ToList()); /// /// Rejects per-item tracking flags that are off system-wide, and warranty-period values that /// don't make sense given HasWarranty/AllowCustomWarrantyPeriod. /// private async Task ValidateTrackingFlagsAsync(bool tracksSerialNumbers, bool tracksBatches, bool tracksExpiryDate, bool hasWarranty, int? warrantyPeriodDays, bool allowCustomWarrantyPeriod) { if (tracksSerialNumbers && !await sysConfig.GetSerialNumberTrackingEnabledAsync()) throw new InvalidOperationException("Serial number tracking is disabled system-wide."); if (tracksBatches && !await sysConfig.GetBatchTrackingEnabledAsync()) throw new InvalidOperationException("Batch tracking is disabled system-wide."); if (tracksExpiryDate && !await sysConfig.GetExpiryDateTrackingEnabledAsync()) throw new InvalidOperationException("Expiry date tracking is disabled system-wide."); if (hasWarranty && !await sysConfig.GetWarrantyTrackingEnabledAsync()) throw new InvalidOperationException("Warranty tracking is disabled system-wide."); if (allowCustomWarrantyPeriod && !await sysConfig.GetCustomWarrantyOverrideEnabledAsync()) throw new InvalidOperationException("Custom warranty period override is disabled system-wide."); if (!hasWarranty && (warrantyPeriodDays is not null || allowCustomWarrantyPeriod)) throw new InvalidOperationException("Warranty period/override can only be set when HasWarranty is true."); if (warrantyPeriodDays is not null && !allowCustomWarrantyPeriod) throw new InvalidOperationException("WarrantyPeriodDays can only be set when AllowCustomWarrantyPeriod is true."); if (warrantyPeriodDays is < 0) throw new InvalidOperationException("WarrantyPeriodDays cannot be negative."); } public async Task> GetAllAsync(Guid? categoryId = null, Guid? subCategoryId = null, Guid? brandId = null) => (await repo.GetAllAsync(categoryId, subCategoryId, brandId)) .Select(MapSummary) .ToList(); public async Task GetByIdAsync(Guid id) { var item = await repo.GetByIdAsync(id); if (item is null) return null; return MapDetail(item); } public async Task CreateAsync(CreateItemRequest request, CancellationToken ct = default) { if (await repo.SkuExistsAsync(request.Sku)) throw new InvalidOperationException($"SKU '{request.Sku}' already exists."); await ValidateTrackingFlagsAsync(request.TracksSerialNumbers, request.TracksBatches, request.TracksExpiryDate, request.HasWarranty, request.WarrantyPeriodDays, request.AllowCustomWarrantyPeriod); var item = new Models.Item { Sku = request.Sku, Name = request.Name, Description = request.Description, FK_CategoryId = request.CategoryId, FK_SubCategoryId = request.SubCategoryId, FK_BrandId = request.BrandId, FK_BaseUomId = request.BaseUomId, SalePrice = request.SalePrice, ReorderLevel = request.ReorderLevel, TracksSerialNumbers = request.TracksSerialNumbers, TracksBatches = request.TracksBatches, TracksExpiryDate = request.TracksExpiryDate, HasWarranty = request.HasWarranty, WarrantyPeriodDays = request.WarrantyPeriodDays, AllowCustomWarrantyPeriod = request.AllowCustomWarrantyPeriod, SyncSource = Models.SyncSource.Manual }; var conversions = (request.Conversions ?? []).Select(c => new Models.UomConversion { FK_ItemId = item.Id, FK_FromUomId = c.FromUomId, FK_ToUomId = c.ToUomId, Factor = c.Factor }).ToList(); item.Conversions = conversions; await uow.BeginAsync(ct); try { await repo.AddAsync(item); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return (await GetByIdAsync(item.Id))!; } public async Task UpdateAsync(Guid id, UpdateItemRequest request, CancellationToken ct = default) { var item = await repo.GetByIdAsync(id); if (item is null) return null; if (item.SyncSource == Models.SyncSource.Erp) return null; await ValidateTrackingFlagsAsync(request.TracksSerialNumbers, request.TracksBatches, request.TracksExpiryDate, request.HasWarranty, request.WarrantyPeriodDays, request.AllowCustomWarrantyPeriod); item.Name = request.Name; item.Description = request.Description; item.FK_CategoryId = request.CategoryId; item.FK_SubCategoryId = request.SubCategoryId; item.FK_BrandId = request.BrandId; item.FK_BaseUomId = request.BaseUomId; item.SalePrice = request.SalePrice; item.ReorderLevel = request.ReorderLevel; item.TracksSerialNumbers = request.TracksSerialNumbers; item.TracksBatches = request.TracksBatches; item.TracksExpiryDate = request.TracksExpiryDate; item.HasWarranty = request.HasWarranty; item.WarrantyPeriodDays = request.WarrantyPeriodDays; item.AllowCustomWarrantyPeriod = request.AllowCustomWarrantyPeriod; item.IsActive = request.IsActive; await uow.BeginAsync(ct); try { repo.Update(item); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return (await GetByIdAsync(id))!; } public async Task DeleteAsync(Guid id, CancellationToken ct = default) { var item = await repo.GetByIdAsync(id); if (item is null || !item.IsActive) return false; if (item.SyncSource == Models.SyncSource.Erp) return false; await uow.BeginAsync(ct); try { repo.Remove(item); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return true; } public async Task?> ReplaceConversionsAsync(Guid id, ReplaceConversionsRequest request, CancellationToken ct = default) { var item = await repo.GetByIdAsync(id); if (item is null || !item.IsActive) return null; if (item.SyncSource == Models.SyncSource.Erp) return null; var conversions = request.Conversions.Select(c => new Models.UomConversion { FK_ItemId = id, FK_FromUomId = c.FromUomId, FK_ToUomId = c.ToUomId, Factor = c.Factor }).ToList(); await uow.BeginAsync(ct); try { await repo.ReplaceConversionsAsync(id, conversions); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return (await GetByIdAsync(id))!.Conversions; } }