using CartWise.Application.Interfaces; using CartWise.Application.Results; using CartWise.Domain.Entities; using CartWise.Domain.Enums; using Microsoft.EntityFrameworkCore; namespace CartWise.Application.Services; public class ShoppingListService : IShoppingListService { private readonly IApplicationDbContext _db; private readonly TimeProvider _timeProvider; public ShoppingListService(IApplicationDbContext db, TimeProvider timeProvider) { _db = db; _timeProvider = timeProvider; } public async Task GetActiveListAsync(Guid householdId, CancellationToken cancellationToken = default) { return await _db.ShoppingLists .Include(l => l.Items) .AsNoTracking() .Where(l => l.HouseholdId == householdId && l.Status == ShoppingListStatus.Active) .FirstOrDefaultAsync(cancellationToken); } public async Task> CreateListAsync(Guid householdId, string name, string createdByUserId, CancellationToken cancellationToken = default) { var hasActiveList = await _db.ShoppingLists .AsNoTracking() .AnyAsync(l => l.HouseholdId == householdId && l.Status == ShoppingListStatus.Active, cancellationToken); if (hasActiveList) { return Result.Failure("This household already has an active shopping list."); } ShoppingList list; try { list = new ShoppingList(householdId, name, createdByUserId, _timeProvider.GetUtcNow().UtcDateTime); } catch (ArgumentException ex) { return Result.Failure(ex.Message); } _db.ShoppingLists.Add(list); await _db.SaveChangesAsync(cancellationToken); return Result.Success(list); } public async Task> AddItemAsync( Guid shoppingListId, string displayName, decimal? quantity, string? unit, string addedByUserId, CancellationToken cancellationToken = default) { var list = await _db.ShoppingLists .Include(l => l.Items) .FirstOrDefaultAsync(l => l.ShoppingListId == shoppingListId, cancellationToken); if (list is null) { return Result.Failure("Shopping list not found."); } ShoppingListItem item; try { item = list.AddItem(displayName, quantity, unit, addedByUserId, _timeProvider.GetUtcNow().UtcDateTime); } catch (Exception ex) when (ex is ArgumentException or InvalidOperationException) { return Result.Failure(ex.Message); } // EF Core can't reliably infer Added state for a new child discovered only via a mutated // navigation collection on an already-tracked parent, since ShoppingListItemId is a // client-generated Guid rather than a store-generated key. Mark it explicitly. _db.ShoppingListItems.Add(item); await _db.SaveChangesAsync(cancellationToken); return Result.Success(item); } public async Task> TogglePurchasedAsync(Guid householdId, Guid itemId, Guid expectedRowVersion, CancellationToken cancellationToken = default) { var item = await FindItemForHouseholdAsync(householdId, itemId, cancellationToken); if (item is null) { return Result.Failure("Item not found."); } _db.Entry(item).Property(i => i.RowVersion).OriginalValue = expectedRowVersion; item.TogglePurchased(_timeProvider.GetUtcNow().UtcDateTime); try { await _db.SaveChangesAsync(cancellationToken); } catch (DbUpdateConcurrencyException) { return Result.Failure("This item was changed by someone else. Please refresh and try again."); } return Result.Success(item); } public async Task> ToggleSkippedAsync(Guid householdId, Guid itemId, Guid expectedRowVersion, CancellationToken cancellationToken = default) { var item = await FindItemForHouseholdAsync(householdId, itemId, cancellationToken); if (item is null) { return Result.Failure("Item not found."); } _db.Entry(item).Property(i => i.RowVersion).OriginalValue = expectedRowVersion; item.ToggleSkipped(); try { await _db.SaveChangesAsync(cancellationToken); } catch (DbUpdateConcurrencyException) { return Result.Failure("This item was changed by someone else. Please refresh and try again."); } return Result.Success(item); } public async Task DeleteItemAsync(Guid householdId, Guid itemId, Guid expectedRowVersion, CancellationToken cancellationToken = default) { var item = await FindItemForHouseholdAsync(householdId, itemId, cancellationToken); if (item is null) { return Result.Failure("Item not found."); } _db.Entry(item).Property(i => i.RowVersion).OriginalValue = expectedRowVersion; _db.ShoppingListItems.Remove(item); try { await _db.SaveChangesAsync(cancellationToken); } catch (DbUpdateConcurrencyException) { return Result.Failure("This item was changed by someone else. Please refresh and try again."); } return Result.Success(); } private Task FindItemForHouseholdAsync(Guid householdId, Guid itemId, CancellationToken cancellationToken) { return ( from item in _db.ShoppingListItems join list in _db.ShoppingLists on item.ShoppingListId equals list.ShoppingListId where item.ShoppingListItemId == itemId && list.HouseholdId == householdId select item ).FirstOrDefaultAsync(cancellationToken); } }