littleshop/LittleShop/Services/ProductService.cs
sysadmin a281bb2896 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>
2025-08-20 17:37:24 +01:00

322 lines
11 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()
{
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();
}
}