**Problem**: EF Core was not materializing Variants navigation property when using .Select() projection directly in the query. The .Include() was being ignored. **Solution**: Changed approach to: 1. Load entities with .Include() + .ToListAsync() first 2. Then project to DTO with in-memory .Select() This ensures navigation properties are fully loaded before mapping to DTOs. **Impact**: Variants will now properly appear in all product API responses. 🤖 Generated with Claude Code https://claude.com/claude-code Co-Authored-By: Claude <noreply@anthropic.com>
708 lines
25 KiB
C#
708 lines
25 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using LittleShop.Data;
|
|
using LittleShop.Models;
|
|
using LittleShop.DTOs;
|
|
|
|
namespace LittleShop.Services;
|
|
|
|
public class ProductService : IProductService
|
|
{
|
|
private readonly LittleShopContext _context;
|
|
private readonly IWebHostEnvironment _environment;
|
|
|
|
public ProductService(LittleShopContext context, IWebHostEnvironment environment)
|
|
{
|
|
_context = context;
|
|
_environment = environment;
|
|
}
|
|
|
|
public async Task<IEnumerable<ProductDto>> GetAllProductsAsync()
|
|
{
|
|
var products = await _context.Products
|
|
.Include(p => p.Category)
|
|
.Include(p => p.Photos)
|
|
.Include(p => p.MultiBuys)
|
|
.Include(p => p.Variants)
|
|
.Where(p => p.IsActive)
|
|
.AsNoTracking()
|
|
.ToListAsync();
|
|
|
|
return products.Select(p => new ProductDto
|
|
{
|
|
Id = p.Id,
|
|
Name = p.Name,
|
|
Description = p.Description,
|
|
Price = p.Price,
|
|
Weight = p.Weight,
|
|
WeightUnit = p.WeightUnit,
|
|
StockQuantity = p.StockQuantity,
|
|
CategoryId = p.CategoryId,
|
|
CategoryName = p.Category.Name,
|
|
CreatedAt = p.CreatedAt,
|
|
UpdatedAt = p.UpdatedAt,
|
|
IsActive = p.IsActive,
|
|
Photos = p.Photos.OrderBy(ph => ph.SortOrder).Select(ph => new ProductPhotoDto
|
|
{
|
|
Id = ph.Id,
|
|
FileName = ph.FileName,
|
|
FilePath = ph.FilePath,
|
|
AltText = ph.AltText,
|
|
SortOrder = ph.SortOrder
|
|
}).ToList(),
|
|
MultiBuys = p.MultiBuys.Select(mb => new ProductMultiBuyDto
|
|
{
|
|
Id = mb.Id,
|
|
ProductId = mb.ProductId,
|
|
Name = mb.Name,
|
|
Description = mb.Description,
|
|
Quantity = mb.Quantity,
|
|
Price = mb.Price,
|
|
PricePerUnit = mb.PricePerUnit,
|
|
SortOrder = mb.SortOrder,
|
|
IsActive = mb.IsActive,
|
|
CreatedAt = mb.CreatedAt,
|
|
UpdatedAt = mb.UpdatedAt
|
|
}).ToList(),
|
|
Variants = p.Variants.Select(v => new ProductVariantDto
|
|
{
|
|
Id = v.Id,
|
|
ProductId = v.ProductId,
|
|
VariantType = v.VariantType,
|
|
Name = v.Name,
|
|
Price = v.Price,
|
|
StockLevel = v.StockLevel,
|
|
SortOrder = v.SortOrder,
|
|
IsActive = v.IsActive,
|
|
CreatedAt = v.CreatedAt,
|
|
UpdatedAt = v.UpdatedAt
|
|
}).ToList()
|
|
}).ToList();
|
|
}
|
|
|
|
public async Task<IEnumerable<ProductDto>> GetProductsByCategoryAsync(Guid categoryId)
|
|
{
|
|
var products = await _context.Products
|
|
.Include(p => p.Category)
|
|
.Include(p => p.Photos)
|
|
.Include(p => p.MultiBuys)
|
|
.Include(p => p.Variants)
|
|
.Where(p => p.IsActive && p.CategoryId == categoryId)
|
|
.AsNoTracking()
|
|
.ToListAsync();
|
|
|
|
return products.Select(p => new ProductDto
|
|
{
|
|
Id = p.Id,
|
|
Name = p.Name,
|
|
Description = p.Description,
|
|
Price = p.Price,
|
|
Weight = p.Weight,
|
|
WeightUnit = p.WeightUnit,
|
|
StockQuantity = p.StockQuantity,
|
|
CategoryId = p.CategoryId,
|
|
CategoryName = p.Category.Name,
|
|
CreatedAt = p.CreatedAt,
|
|
UpdatedAt = p.UpdatedAt,
|
|
IsActive = p.IsActive,
|
|
Photos = p.Photos.OrderBy(ph => ph.SortOrder).Select(ph => new ProductPhotoDto
|
|
{
|
|
Id = ph.Id,
|
|
FileName = ph.FileName,
|
|
FilePath = ph.FilePath,
|
|
AltText = ph.AltText,
|
|
SortOrder = ph.SortOrder
|
|
}).ToList(),
|
|
MultiBuys = p.MultiBuys.Select(mb => new ProductMultiBuyDto
|
|
{
|
|
Id = mb.Id,
|
|
ProductId = mb.ProductId,
|
|
Name = mb.Name,
|
|
Description = mb.Description,
|
|
Quantity = mb.Quantity,
|
|
Price = mb.Price,
|
|
PricePerUnit = mb.PricePerUnit,
|
|
SortOrder = mb.SortOrder,
|
|
IsActive = mb.IsActive,
|
|
CreatedAt = mb.CreatedAt,
|
|
UpdatedAt = mb.UpdatedAt
|
|
}).ToList(),
|
|
Variants = p.Variants.Select(v => new ProductVariantDto
|
|
{
|
|
Id = v.Id,
|
|
ProductId = v.ProductId,
|
|
VariantType = v.VariantType,
|
|
Name = v.Name,
|
|
Price = v.Price,
|
|
StockLevel = v.StockLevel,
|
|
SortOrder = v.SortOrder,
|
|
IsActive = v.IsActive,
|
|
CreatedAt = v.CreatedAt,
|
|
UpdatedAt = v.UpdatedAt
|
|
}).ToList()
|
|
}).ToList();
|
|
}
|
|
|
|
public async Task<ProductDto?> GetProductByIdAsync(Guid id)
|
|
{
|
|
var product = await _context.Products
|
|
.Include(p => p.Category)
|
|
.Include(p => p.Photos)
|
|
.Include(p => p.MultiBuys)
|
|
.Include(p => p.Variants)
|
|
.FirstOrDefaultAsync(p => p.Id == id);
|
|
|
|
if (product == null) return null;
|
|
|
|
return new ProductDto
|
|
{
|
|
Id = product.Id,
|
|
Name = product.Name,
|
|
Description = product.Description,
|
|
Price = product.Price,
|
|
Weight = product.Weight,
|
|
WeightUnit = product.WeightUnit,
|
|
StockQuantity = product.StockQuantity,
|
|
CategoryId = product.CategoryId,
|
|
CategoryName = product.Category.Name,
|
|
VariantCollectionId = product.VariantCollectionId,
|
|
VariantsJson = product.VariantsJson,
|
|
CreatedAt = product.CreatedAt,
|
|
UpdatedAt = product.UpdatedAt,
|
|
IsActive = product.IsActive,
|
|
Photos = product.Photos.OrderBy(ph => ph.SortOrder).Select(ph => new ProductPhotoDto
|
|
{
|
|
Id = ph.Id,
|
|
FileName = ph.FileName,
|
|
FilePath = ph.FilePath,
|
|
AltText = ph.AltText,
|
|
SortOrder = ph.SortOrder
|
|
}).ToList(),
|
|
MultiBuys = product.MultiBuys.OrderBy(v => v.SortOrder).Select(v => new ProductMultiBuyDto
|
|
{
|
|
Id = v.Id,
|
|
ProductId = v.ProductId,
|
|
Name = v.Name,
|
|
Description = v.Description,
|
|
Quantity = v.Quantity,
|
|
Price = v.Price,
|
|
PricePerUnit = v.PricePerUnit,
|
|
SortOrder = v.SortOrder,
|
|
IsActive = v.IsActive,
|
|
CreatedAt = v.CreatedAt,
|
|
UpdatedAt = v.UpdatedAt
|
|
}).ToList(),
|
|
Variants = product.Variants.OrderBy(v => v.SortOrder).Select(v => new ProductVariantDto
|
|
{
|
|
Id = v.Id,
|
|
ProductId = v.ProductId,
|
|
VariantType = v.VariantType,
|
|
Name = v.Name,
|
|
Price = v.Price,
|
|
StockLevel = v.StockLevel,
|
|
SortOrder = v.SortOrder,
|
|
IsActive = v.IsActive,
|
|
CreatedAt = v.CreatedAt,
|
|
UpdatedAt = v.UpdatedAt
|
|
}).ToList()
|
|
};
|
|
}
|
|
|
|
public async Task<ProductDto> CreateProductAsync(CreateProductDto createProductDto)
|
|
{
|
|
var product = new Product
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = createProductDto.Name,
|
|
Description = string.IsNullOrEmpty(createProductDto.Description) ? " " : createProductDto.Description,
|
|
Price = createProductDto.Price,
|
|
Weight = createProductDto.Weight,
|
|
WeightUnit = createProductDto.WeightUnit,
|
|
StockQuantity = createProductDto.StockQuantity,
|
|
CategoryId = createProductDto.CategoryId,
|
|
VariantCollectionId = createProductDto.VariantCollectionId,
|
|
VariantsJson = createProductDto.VariantsJson,
|
|
CreatedAt = DateTime.UtcNow,
|
|
UpdatedAt = DateTime.UtcNow,
|
|
IsActive = true
|
|
};
|
|
|
|
_context.Products.Add(product);
|
|
await _context.SaveChangesAsync();
|
|
|
|
var category = await _context.Categories.FindAsync(createProductDto.CategoryId);
|
|
|
|
return new ProductDto
|
|
{
|
|
Id = product.Id,
|
|
Name = product.Name,
|
|
Description = product.Description,
|
|
Price = product.Price,
|
|
Weight = product.Weight,
|
|
WeightUnit = product.WeightUnit,
|
|
CategoryId = product.CategoryId,
|
|
CategoryName = category?.Name ?? "",
|
|
CreatedAt = product.CreatedAt,
|
|
UpdatedAt = product.UpdatedAt,
|
|
IsActive = product.IsActive,
|
|
Photos = new List<ProductPhotoDto>(),
|
|
MultiBuys = new List<ProductMultiBuyDto>(),
|
|
Variants = new List<ProductVariantDto>()
|
|
};
|
|
}
|
|
|
|
public async Task<bool> UpdateProductAsync(Guid id, UpdateProductDto updateProductDto)
|
|
{
|
|
var product = await _context.Products.FindAsync(id);
|
|
if (product == null) return false;
|
|
|
|
if (!string.IsNullOrEmpty(updateProductDto.Name))
|
|
product.Name = updateProductDto.Name;
|
|
|
|
if (!string.IsNullOrEmpty(updateProductDto.Description))
|
|
product.Description = updateProductDto.Description;
|
|
|
|
if (updateProductDto.Price.HasValue)
|
|
product.Price = updateProductDto.Price.Value;
|
|
|
|
if (updateProductDto.Weight.HasValue)
|
|
product.Weight = updateProductDto.Weight.Value;
|
|
|
|
if (updateProductDto.WeightUnit.HasValue)
|
|
product.WeightUnit = updateProductDto.WeightUnit.Value;
|
|
|
|
if (updateProductDto.StockQuantity.HasValue)
|
|
product.StockQuantity = updateProductDto.StockQuantity.Value;
|
|
|
|
if (updateProductDto.CategoryId.HasValue)
|
|
product.CategoryId = updateProductDto.CategoryId.Value;
|
|
|
|
if (updateProductDto.VariantCollectionId.HasValue)
|
|
product.VariantCollectionId = updateProductDto.VariantCollectionId.Value;
|
|
|
|
if (updateProductDto.VariantsJson != null)
|
|
product.VariantsJson = updateProductDto.VariantsJson;
|
|
|
|
if (updateProductDto.IsActive.HasValue)
|
|
product.IsActive = updateProductDto.IsActive.Value;
|
|
|
|
product.UpdatedAt = DateTime.UtcNow;
|
|
|
|
await _context.SaveChangesAsync();
|
|
return true;
|
|
}
|
|
|
|
public async Task<bool> DeleteProductAsync(Guid id)
|
|
{
|
|
var product = await _context.Products.FindAsync(id);
|
|
if (product == null) return false;
|
|
|
|
product.IsActive = false;
|
|
await _context.SaveChangesAsync();
|
|
return true;
|
|
}
|
|
|
|
public async Task<bool> AddProductPhotoAsync(Guid productId, IFormFile file, string? altText = null)
|
|
{
|
|
var product = await _context.Products.FindAsync(productId);
|
|
if (product == null) return false;
|
|
|
|
var uploadsPath = Path.Combine(_environment.WebRootPath ?? "wwwroot", "uploads", "products");
|
|
Directory.CreateDirectory(uploadsPath);
|
|
|
|
var fileName = $"{Guid.NewGuid()}_{file.FileName}";
|
|
var filePath = Path.Combine(uploadsPath, fileName);
|
|
|
|
using (var stream = new FileStream(filePath, FileMode.Create))
|
|
{
|
|
await file.CopyToAsync(stream);
|
|
}
|
|
|
|
var maxSortOrder = await _context.ProductPhotos
|
|
.Where(pp => pp.ProductId == productId)
|
|
.Select(pp => (int?)pp.SortOrder)
|
|
.MaxAsync() ?? 0;
|
|
|
|
var productPhoto = new ProductPhoto
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
ProductId = productId,
|
|
FileName = fileName,
|
|
FilePath = $"/uploads/products/{fileName}",
|
|
AltText = altText,
|
|
SortOrder = maxSortOrder + 1,
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
_context.ProductPhotos.Add(productPhoto);
|
|
await _context.SaveChangesAsync();
|
|
return true;
|
|
}
|
|
|
|
public async Task<bool> RemoveProductPhotoAsync(Guid productId, Guid photoId)
|
|
{
|
|
var photo = await _context.ProductPhotos
|
|
.FirstOrDefaultAsync(pp => pp.Id == photoId && pp.ProductId == productId);
|
|
|
|
if (photo == null) return false;
|
|
|
|
var physicalPath = Path.Combine(_environment.WebRootPath, photo.FilePath.TrimStart('/'));
|
|
if (File.Exists(physicalPath))
|
|
{
|
|
File.Delete(physicalPath);
|
|
}
|
|
|
|
_context.ProductPhotos.Remove(photo);
|
|
await _context.SaveChangesAsync();
|
|
return true;
|
|
}
|
|
|
|
public async Task<ProductPhotoDto?> AddProductPhotoAsync(CreateProductPhotoDto photoDto)
|
|
{
|
|
var product = await _context.Products.FindAsync(photoDto.ProductId);
|
|
if (product == null) return null;
|
|
|
|
var existingPhotos = await _context.ProductPhotos
|
|
.Where(pp => pp.ProductId == photoDto.ProductId)
|
|
.ToListAsync();
|
|
|
|
var maxSortOrder = existingPhotos.Any() ? existingPhotos.Max(pp => pp.SortOrder) : 0;
|
|
|
|
var productPhoto = new ProductPhoto
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
ProductId = photoDto.ProductId,
|
|
FileName = Path.GetFileName(photoDto.PhotoUrl),
|
|
FilePath = photoDto.PhotoUrl,
|
|
AltText = photoDto.AltText,
|
|
SortOrder = photoDto.DisplayOrder > 0 ? photoDto.DisplayOrder : maxSortOrder + 1,
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
_context.ProductPhotos.Add(productPhoto);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return new ProductPhotoDto
|
|
{
|
|
Id = productPhoto.Id,
|
|
FileName = productPhoto.FileName,
|
|
FilePath = productPhoto.FilePath,
|
|
AltText = productPhoto.AltText,
|
|
SortOrder = productPhoto.SortOrder
|
|
};
|
|
}
|
|
|
|
public async Task<IEnumerable<ProductDto>> SearchProductsAsync(string searchTerm)
|
|
{
|
|
var query = _context.Products
|
|
.Include(p => p.Category)
|
|
.Include(p => p.Photos)
|
|
.Include(p => p.MultiBuys.Where(v => v.IsActive))
|
|
.Where(p => p.IsActive);
|
|
|
|
if (!string.IsNullOrWhiteSpace(searchTerm))
|
|
{
|
|
searchTerm = searchTerm.ToLower();
|
|
query = query.Where(p =>
|
|
p.Name.ToLower().Contains(searchTerm) ||
|
|
p.Description.ToLower().Contains(searchTerm));
|
|
}
|
|
|
|
return await query.Select(p => new ProductDto
|
|
{
|
|
Id = p.Id,
|
|
Name = p.Name,
|
|
Description = p.Description,
|
|
Price = p.Price,
|
|
Weight = p.Weight,
|
|
WeightUnit = p.WeightUnit,
|
|
StockQuantity = p.StockQuantity,
|
|
CategoryId = p.CategoryId,
|
|
CategoryName = p.Category.Name,
|
|
CreatedAt = p.CreatedAt,
|
|
UpdatedAt = p.UpdatedAt,
|
|
IsActive = p.IsActive,
|
|
Photos = p.Photos.OrderBy(ph => ph.SortOrder).Select(ph => new ProductPhotoDto
|
|
{
|
|
Id = ph.Id,
|
|
FileName = ph.FileName,
|
|
FilePath = ph.FilePath,
|
|
AltText = ph.AltText,
|
|
SortOrder = ph.SortOrder
|
|
}).ToList()
|
|
}).ToListAsync();
|
|
}
|
|
|
|
// Product Multi-Buy Methods
|
|
public async Task<ProductMultiBuyDto> CreateProductMultiBuyAsync(CreateProductMultiBuyDto createMultiBuyDto)
|
|
{
|
|
var product = await _context.Products.FindAsync(createMultiBuyDto.ProductId);
|
|
if (product == null)
|
|
throw new ArgumentException("Product not found");
|
|
|
|
// Check if multi-buy with this quantity already exists
|
|
var existingMultiBuy = await _context.ProductMultiBuys
|
|
.FirstOrDefaultAsync(v => v.ProductId == createMultiBuyDto.ProductId &&
|
|
v.Quantity == createMultiBuyDto.Quantity &&
|
|
v.IsActive);
|
|
|
|
if (existingMultiBuy != null)
|
|
throw new ArgumentException($"A multi-buy with quantity {createMultiBuyDto.Quantity} already exists for this product");
|
|
|
|
var pricePerUnit = createMultiBuyDto.Price / createMultiBuyDto.Quantity;
|
|
|
|
var multiBuy = new ProductMultiBuy
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
ProductId = createMultiBuyDto.ProductId,
|
|
Name = createMultiBuyDto.Name,
|
|
Description = createMultiBuyDto.Description,
|
|
Quantity = createMultiBuyDto.Quantity,
|
|
Price = createMultiBuyDto.Price,
|
|
PricePerUnit = pricePerUnit,
|
|
SortOrder = createMultiBuyDto.SortOrder,
|
|
IsActive = true,
|
|
CreatedAt = DateTime.UtcNow,
|
|
UpdatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
_context.ProductMultiBuys.Add(multiBuy);
|
|
|
|
try
|
|
{
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
catch (Microsoft.EntityFrameworkCore.DbUpdateException ex) when (ex.InnerException?.Message.Contains("UNIQUE constraint failed") == true)
|
|
{
|
|
throw new ArgumentException($"A multi-buy with quantity {createMultiBuyDto.Quantity} already exists for this product");
|
|
}
|
|
|
|
return new ProductMultiBuyDto
|
|
{
|
|
Id = multiBuy.Id,
|
|
ProductId = multiBuy.ProductId,
|
|
Name = multiBuy.Name,
|
|
Description = multiBuy.Description,
|
|
Quantity = multiBuy.Quantity,
|
|
Price = multiBuy.Price,
|
|
PricePerUnit = multiBuy.PricePerUnit,
|
|
SortOrder = multiBuy.SortOrder,
|
|
IsActive = multiBuy.IsActive,
|
|
CreatedAt = multiBuy.CreatedAt,
|
|
UpdatedAt = multiBuy.UpdatedAt
|
|
};
|
|
}
|
|
|
|
public async Task<bool> UpdateProductMultiBuyAsync(Guid id, UpdateProductMultiBuyDto updateMultiBuyDto)
|
|
{
|
|
var multiBuy = await _context.ProductMultiBuys.FindAsync(id);
|
|
if (multiBuy == null) return false;
|
|
|
|
if (!string.IsNullOrEmpty(updateMultiBuyDto.Name))
|
|
multiBuy.Name = updateMultiBuyDto.Name;
|
|
|
|
if (!string.IsNullOrEmpty(updateMultiBuyDto.Description))
|
|
multiBuy.Description = updateMultiBuyDto.Description;
|
|
|
|
if (updateMultiBuyDto.Quantity.HasValue)
|
|
multiBuy.Quantity = updateMultiBuyDto.Quantity.Value;
|
|
|
|
if (updateMultiBuyDto.Price.HasValue)
|
|
multiBuy.Price = updateMultiBuyDto.Price.Value;
|
|
|
|
if (updateMultiBuyDto.Quantity.HasValue || updateMultiBuyDto.Price.HasValue)
|
|
multiBuy.PricePerUnit = multiBuy.Price / multiBuy.Quantity;
|
|
|
|
if (updateMultiBuyDto.SortOrder.HasValue)
|
|
multiBuy.SortOrder = updateMultiBuyDto.SortOrder.Value;
|
|
|
|
if (updateMultiBuyDto.IsActive.HasValue)
|
|
multiBuy.IsActive = updateMultiBuyDto.IsActive.Value;
|
|
|
|
multiBuy.UpdatedAt = DateTime.UtcNow;
|
|
|
|
await _context.SaveChangesAsync();
|
|
return true;
|
|
}
|
|
|
|
public async Task<bool> DeleteProductMultiBuyAsync(Guid id)
|
|
{
|
|
var multiBuy = await _context.ProductMultiBuys.FindAsync(id);
|
|
if (multiBuy == null) return false;
|
|
|
|
multiBuy.IsActive = false;
|
|
multiBuy.UpdatedAt = DateTime.UtcNow;
|
|
await _context.SaveChangesAsync();
|
|
return true;
|
|
}
|
|
|
|
public async Task<IEnumerable<ProductMultiBuyDto>> GetProductMultiBuysAsync(Guid productId)
|
|
{
|
|
return await _context.ProductMultiBuys
|
|
.Where(v => v.ProductId == productId && v.IsActive)
|
|
.OrderBy(v => v.SortOrder)
|
|
.Select(v => new ProductMultiBuyDto
|
|
{
|
|
Id = v.Id,
|
|
ProductId = v.ProductId,
|
|
Name = v.Name,
|
|
Description = v.Description,
|
|
Quantity = v.Quantity,
|
|
Price = v.Price,
|
|
PricePerUnit = v.PricePerUnit,
|
|
SortOrder = v.SortOrder,
|
|
IsActive = v.IsActive,
|
|
CreatedAt = v.CreatedAt,
|
|
UpdatedAt = v.UpdatedAt
|
|
})
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<ProductMultiBuyDto?> GetProductMultiBuyByIdAsync(Guid id)
|
|
{
|
|
var multiBuy = await _context.ProductMultiBuys.FindAsync(id);
|
|
if (multiBuy == null) return null;
|
|
|
|
return new ProductMultiBuyDto
|
|
{
|
|
Id = multiBuy.Id,
|
|
ProductId = multiBuy.ProductId,
|
|
Name = multiBuy.Name,
|
|
Description = multiBuy.Description,
|
|
Quantity = multiBuy.Quantity,
|
|
Price = multiBuy.Price,
|
|
PricePerUnit = multiBuy.PricePerUnit,
|
|
SortOrder = multiBuy.SortOrder,
|
|
IsActive = multiBuy.IsActive,
|
|
CreatedAt = multiBuy.CreatedAt,
|
|
UpdatedAt = multiBuy.UpdatedAt
|
|
};
|
|
}
|
|
|
|
// Product Variant Methods (for color/flavor options)
|
|
public async Task<ProductVariantDto> CreateProductVariantAsync(CreateProductVariantDto createVariantDto)
|
|
{
|
|
var product = await _context.Products.FindAsync(createVariantDto.ProductId);
|
|
if (product == null)
|
|
throw new ArgumentException("Product not found");
|
|
|
|
// Check if variant with this name already exists
|
|
var existingVariant = await _context.ProductVariants
|
|
.FirstOrDefaultAsync(v => v.ProductId == createVariantDto.ProductId &&
|
|
v.Name == createVariantDto.Name &&
|
|
v.IsActive);
|
|
|
|
if (existingVariant != null)
|
|
throw new ArgumentException($"A variant named '{createVariantDto.Name}' already exists for this product");
|
|
|
|
var variant = new ProductVariant
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
ProductId = createVariantDto.ProductId,
|
|
Name = createVariantDto.Name,
|
|
VariantType = createVariantDto.VariantType,
|
|
SortOrder = createVariantDto.SortOrder,
|
|
StockLevel = createVariantDto.StockLevel,
|
|
Price = createVariantDto.Price,
|
|
IsActive = true,
|
|
CreatedAt = DateTime.UtcNow,
|
|
UpdatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
_context.ProductVariants.Add(variant);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return new ProductVariantDto
|
|
{
|
|
Id = variant.Id,
|
|
ProductId = variant.ProductId,
|
|
Name = variant.Name,
|
|
VariantType = variant.VariantType,
|
|
SortOrder = variant.SortOrder,
|
|
StockLevel = variant.StockLevel,
|
|
Price = variant.Price,
|
|
IsActive = variant.IsActive,
|
|
CreatedAt = variant.CreatedAt,
|
|
UpdatedAt = variant.UpdatedAt
|
|
};
|
|
}
|
|
|
|
public async Task<bool> UpdateProductVariantAsync(Guid id, UpdateProductVariantDto updateVariantDto)
|
|
{
|
|
var variant = await _context.ProductVariants.FindAsync(id);
|
|
if (variant == null) return false;
|
|
|
|
if (!string.IsNullOrEmpty(updateVariantDto.Name))
|
|
variant.Name = updateVariantDto.Name;
|
|
|
|
if (!string.IsNullOrEmpty(updateVariantDto.VariantType))
|
|
variant.VariantType = updateVariantDto.VariantType;
|
|
|
|
if (updateVariantDto.SortOrder.HasValue)
|
|
variant.SortOrder = updateVariantDto.SortOrder.Value;
|
|
|
|
if (updateVariantDto.StockLevel.HasValue)
|
|
variant.StockLevel = updateVariantDto.StockLevel.Value;
|
|
|
|
if (updateVariantDto.Price.HasValue)
|
|
variant.Price = updateVariantDto.Price.Value;
|
|
|
|
if (updateVariantDto.IsActive.HasValue)
|
|
variant.IsActive = updateVariantDto.IsActive.Value;
|
|
|
|
variant.UpdatedAt = DateTime.UtcNow;
|
|
await _context.SaveChangesAsync();
|
|
return true;
|
|
}
|
|
|
|
public async Task<bool> DeleteProductVariantAsync(Guid id)
|
|
{
|
|
var variant = await _context.ProductVariants.FindAsync(id);
|
|
if (variant == null) return false;
|
|
|
|
variant.IsActive = false;
|
|
variant.UpdatedAt = DateTime.UtcNow;
|
|
await _context.SaveChangesAsync();
|
|
return true;
|
|
}
|
|
|
|
public async Task<IEnumerable<ProductVariantDto>> GetProductVariantsAsync(Guid productId)
|
|
{
|
|
return await _context.ProductVariants
|
|
.Where(v => v.ProductId == productId && v.IsActive)
|
|
.OrderBy(v => v.SortOrder)
|
|
.Select(v => new ProductVariantDto
|
|
{
|
|
Id = v.Id,
|
|
ProductId = v.ProductId,
|
|
Name = v.Name,
|
|
VariantType = v.VariantType,
|
|
SortOrder = v.SortOrder,
|
|
StockLevel = v.StockLevel,
|
|
Price = v.Price,
|
|
IsActive = v.IsActive,
|
|
CreatedAt = v.CreatedAt,
|
|
UpdatedAt = v.UpdatedAt
|
|
})
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<ProductVariantDto?> GetProductVariantByIdAsync(Guid id)
|
|
{
|
|
var variant = await _context.ProductVariants.FindAsync(id);
|
|
if (variant == null) return null;
|
|
|
|
return new ProductVariantDto
|
|
{
|
|
Id = variant.Id,
|
|
ProductId = variant.ProductId,
|
|
Name = variant.Name,
|
|
VariantType = variant.VariantType,
|
|
SortOrder = variant.SortOrder,
|
|
StockLevel = variant.StockLevel,
|
|
Price = variant.Price,
|
|
IsActive = variant.IsActive,
|
|
CreatedAt = variant.CreatedAt,
|
|
UpdatedAt = variant.UpdatedAt
|
|
};
|
|
}
|
|
} |