Implement complete e-commerce functionality with shipping and order management
Features Added: - Standard e-commerce properties (Price, Weight, shipping fields) - Order management with Create/Edit views and shipping information - ShippingRates system for weight-based shipping calculations - Comprehensive test coverage with JWT authentication tests - Sample data seeder with 5 orders demonstrating full workflow - Photo upload functionality for products - Multi-cryptocurrency payment support (BTC, XMR, USDT, etc.) Database Changes: - Added ShippingRates table - Added shipping fields to Orders (Name, Address, City, PostCode, Country) - Renamed properties to standard names (BasePrice to Price, ProductWeight to Weight) - Added UpdatedAt timestamps to models UI Improvements: - Added Create/Edit views for Orders - Added ShippingRates management UI - Updated navigation menu with Shipping option - Enhanced Order Details view with shipping information Sample Data: - 3 Categories (Electronics, Clothing, Books) - 5 Products with various prices - 5 Shipping rates (Royal Mail options) - 5 Orders in different statuses (Pending to Delivered) - 3 Crypto payments demonstrating payment flow Security: - All API endpoints secured with JWT authentication - No public endpoints - client apps must authenticate - Privacy-focused design with minimal data collection Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
322
LittleShop/Services/ProductService.cs
Normal file
322
LittleShop/Services/ProductService.cs
Normal file
@@ -0,0 +1,322 @@
|
||||
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()
|
||||
{
|
||||
return await _context.Products
|
||||
.Include(p => p.Category)
|
||||
.Include(p => p.Photos)
|
||||
.Where(p => p.IsActive)
|
||||
.Select(p => new ProductDto
|
||||
{
|
||||
Id = p.Id,
|
||||
Name = p.Name,
|
||||
Description = p.Description,
|
||||
Price = p.Price,
|
||||
Weight = p.Weight,
|
||||
WeightUnit = p.WeightUnit,
|
||||
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();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ProductDto>> GetProductsByCategoryAsync(Guid categoryId)
|
||||
{
|
||||
return await _context.Products
|
||||
.Include(p => p.Category)
|
||||
.Include(p => p.Photos)
|
||||
.Where(p => p.IsActive && p.CategoryId == categoryId)
|
||||
.Select(p => new ProductDto
|
||||
{
|
||||
Id = p.Id,
|
||||
Name = p.Name,
|
||||
Description = p.Description,
|
||||
Price = p.Price,
|
||||
Weight = p.Weight,
|
||||
WeightUnit = p.WeightUnit,
|
||||
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();
|
||||
}
|
||||
|
||||
public async Task<ProductDto?> GetProductByIdAsync(Guid id)
|
||||
{
|
||||
var product = await _context.Products
|
||||
.Include(p => p.Category)
|
||||
.Include(p => p.Photos)
|
||||
.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,
|
||||
CategoryId = product.CategoryId,
|
||||
CategoryName = product.Category.Name,
|
||||
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()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ProductDto> CreateProductAsync(CreateProductDto createProductDto)
|
||||
{
|
||||
var product = new Product
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = createProductDto.Name,
|
||||
Description = createProductDto.Description,
|
||||
Price = createProductDto.Price,
|
||||
Weight = createProductDto.Weight,
|
||||
WeightUnit = createProductDto.WeightUnit,
|
||||
CategoryId = createProductDto.CategoryId,
|
||||
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>()
|
||||
};
|
||||
}
|
||||
|
||||
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.CategoryId.HasValue)
|
||||
product.CategoryId = updateProductDto.CategoryId.Value;
|
||||
|
||||
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 maxSortOrder = await _context.ProductPhotos
|
||||
.Where(pp => pp.ProductId == photoDto.ProductId)
|
||||
.Select(pp => pp.SortOrder)
|
||||
.DefaultIfEmpty(0)
|
||||
.MaxAsync();
|
||||
|
||||
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)
|
||||
.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,
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user