littleshop/LittleShop.Tests/Unit/ProductMultiBuyServiceTests.cs
SysAdmin 034b8facee Implement product multi-buys and variants system
Major restructuring of product variations:
- Renamed ProductVariation to ProductMultiBuy for quantity-based pricing (e.g., "3 for £25")
- Added new ProductVariant model for string-based options (colors, flavors)
- Complete separation of multi-buy pricing from variant selection

Features implemented:
- Multi-buy deals with automatic price-per-unit calculation
- Product variants for colors/flavors/sizes with stock tracking
- TeleBot checkout supports both multi-buys and variant selection
- Shopping cart correctly calculates multi-buy bundle prices
- Order system tracks selected variants and multi-buy choices
- Real-time bot activity monitoring with SignalR
- Public bot directory page with QR codes for Telegram launch
- Admin dashboard shows multi-buy and variant metrics

Technical changes:
- Updated all DTOs, services, and controllers
- Fixed cart total calculation for multi-buy bundles
- Comprehensive test coverage for new functionality
- All existing tests passing with new features

Database changes:
- Migrated ProductVariations to ProductMultiBuys
- Added ProductVariants table
- Updated OrderItems to track variants

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-21 00:30:12 +01:00

252 lines
7.5 KiB
C#

using FluentAssertions;
using LittleShop.Data;
using LittleShop.DTOs;
using LittleShop.Models;
using LittleShop.Services;
using LittleShop.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Hosting;
using Moq;
using Xunit;
namespace LittleShop.Tests.Unit;
public class ProductMultiBuyServiceTests : IDisposable
{
private readonly LittleShopContext _context;
private readonly IProductService _productService;
private readonly Mock<IWebHostEnvironment> _mockEnvironment;
public ProductMultiBuyServiceTests()
{
var options = new DbContextOptionsBuilder<LittleShopContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_context = new LittleShopContext(options);
_mockEnvironment = new Mock<IWebHostEnvironment>();
_mockEnvironment.Setup(e => e.WebRootPath).Returns("/test/wwwroot");
_productService = new ProductService(_context, _mockEnvironment.Object);
}
[Fact]
public async Task CreateProductMultiBuy_ShouldCreateSuccessfully()
{
// Arrange
var product = await CreateTestProduct();
var createDto = new CreateProductMultiBuyDto
{
ProductId = product.Id,
Name = "3 for £25",
Description = "Save £5 when you buy 3",
Quantity = 3,
Price = 25.00m
};
// Act
var result = await _productService.CreateProductMultiBuyAsync(createDto);
// Assert
result.Should().NotBeNull();
result.Name.Should().Be("3 for £25");
result.Quantity.Should().Be(3);
result.Price.Should().Be(25.00m);
result.PricePerUnit.Should().BeApproximately(8.33m, 0.01m);
}
[Fact]
public async Task CreateProductMultiBuy_DuplicateQuantity_ShouldThrow()
{
// Arrange
var product = await CreateTestProduct();
// Create first multi-buy
await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto
{
ProductId = product.Id,
Name = "Twin Pack",
Quantity = 2,
Price = 19.00m
});
// Try to create duplicate quantity
var duplicateDto = new CreateProductMultiBuyDto
{
ProductId = product.Id,
Name = "Double Deal",
Quantity = 2, // Same quantity
Price = 18.00m
};
// Act & Assert
var act = async () => await _productService.CreateProductMultiBuyAsync(duplicateDto);
await act.Should().ThrowAsync<ArgumentException>()
.WithMessage("*already exists*");
}
[Fact]
public async Task GetProductWithMultiBuys_ShouldReturnAllMultiBuys()
{
// Arrange
var product = await CreateTestProduct();
// Create multiple multi-buys
await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto
{
ProductId = product.Id,
Name = "Twin Pack",
Quantity = 2,
Price = 19.00m
});
await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto
{
ProductId = product.Id,
Name = "Triple Pack",
Quantity = 3,
Price = 25.00m
});
// Act
var result = await _productService.GetProductByIdAsync(product.Id);
// Assert
result.Should().NotBeNull();
result!.MultiBuys.Should().HaveCount(2);
result.MultiBuys.Should().Contain(mb => mb.Name == "Twin Pack");
result.MultiBuys.Should().Contain(mb => mb.Name == "Triple Pack");
result.MultiBuys.Should().BeInAscendingOrder(mb => mb.Quantity);
}
[Fact]
public async Task UpdateProductMultiBuy_ShouldUpdateSuccessfully()
{
// Arrange
var product = await CreateTestProduct();
var multiBuy = await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto
{
ProductId = product.Id,
Name = "Original Name",
Quantity = 2,
Price = 20.00m
});
var updateDto = new UpdateProductMultiBuyDto
{
Name = "Updated Name",
Description = "New description",
Price = 18.00m,
SortOrder = 10,
IsActive = false
};
// Act
var success = await _productService.UpdateProductMultiBuyAsync(multiBuy.Id, updateDto);
// Assert
success.Should().BeTrue();
// Get the updated multi-buy to verify changes
var updated = await _productService.GetProductMultiBuyByIdAsync(multiBuy.Id);
updated.Should().NotBeNull();
updated!.Name.Should().Be("Updated Name");
updated.Description.Should().Be("New description");
updated.Price.Should().Be(18.00m);
updated.PricePerUnit.Should().Be(9.00m);
updated.SortOrder.Should().Be(10);
updated.IsActive.Should().BeFalse();
}
[Fact]
public async Task DeleteProductMultiBuy_ShouldSoftDelete()
{
// Arrange
var product = await CreateTestProduct();
var multiBuy = await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto
{
ProductId = product.Id,
Name = "To Delete",
Quantity = 2,
Price = 20.00m
});
// Act
await _productService.DeleteProductMultiBuyAsync(multiBuy.Id);
// Assert
var deletedMultiBuy = await _context.ProductMultiBuys
.IgnoreQueryFilters()
.FirstOrDefaultAsync(mb => mb.Id == multiBuy.Id);
deletedMultiBuy.Should().NotBeNull();
deletedMultiBuy!.IsActive.Should().BeFalse();
}
[Fact]
public async Task PricePerUnit_ShouldCalculateCorrectly()
{
// Arrange & Act
var product = await CreateTestProduct();
var singleItem = await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto
{
ProductId = product.Id,
Name = "Single",
Quantity = 1,
Price = 10.00m
});
var twinPack = await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto
{
ProductId = product.Id,
Name = "Twin Pack",
Quantity = 2,
Price = 19.00m
});
var triplePack = await _productService.CreateProductMultiBuyAsync(new CreateProductMultiBuyDto
{
ProductId = product.Id,
Name = "Triple Pack",
Quantity = 3,
Price = 25.00m
});
// Assert
singleItem.PricePerUnit.Should().Be(10.00m);
twinPack.PricePerUnit.Should().Be(9.50m);
triplePack.PricePerUnit.Should().BeApproximately(8.33m, 0.01m);
}
private async Task<Product> CreateTestProduct()
{
var category = new Category
{
Id = Guid.NewGuid(),
Name = "Test Category",
IsActive = true
};
_context.Categories.Add(category);
var product = new Product
{
Id = Guid.NewGuid(),
Name = "Test Product",
Price = 10.00m,
CategoryId = category.Id,
IsActive = true,
Weight = 1.0m,
WeightUnit = ProductWeightUnit.Kilograms
};
_context.Products.Add(product);
await _context.SaveChangesAsync();
return product;
}
public void Dispose()
{
_context.Dispose();
}
}