littleshop/LittleShop.Tests/Integration/OrdersWithVariantsTests.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

373 lines
12 KiB
C#

using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using FluentAssertions;
using LittleShop.DTOs;
using LittleShop.Models;
using LittleShop.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace LittleShop.Tests.Integration;
public class OrdersWithVariantsTests : IClassFixture<TestWebApplicationFactory>
{
private readonly HttpClient _client;
private readonly TestWebApplicationFactory _factory;
public OrdersWithVariantsTests(TestWebApplicationFactory factory)
{
_factory = factory;
_client = _factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
[Fact]
public async Task CreateOrder_WithVariants_ShouldStoreVariantInfo()
{
// Arrange
var product = await CreateProductWithVariants();
var orderRequest = new CreateOrderDto
{
IdentityReference = "test_user_" + Guid.NewGuid(),
ShippingName = "John Doe",
ShippingAddress = "123 Test St",
ShippingCity = "Test City",
ShippingPostCode = "TE1 1ST",
ShippingCountry = "United Kingdom",
Items = new List<CreateOrderItemDto>
{
new()
{
ProductId = product.Id,
Quantity = 1,
SelectedVariant = "Red"
},
new()
{
ProductId = product.Id,
Quantity = 2,
SelectedVariant = "Blue"
}
}
};
// Act
var response = await _client.PostAsJsonAsync("/api/orders", orderRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var order = await response.Content.ReadFromJsonAsync<OrderDto>();
order.Should().NotBeNull();
order!.Items.Should().HaveCount(2);
order.Items.Should().Contain(i => i.SelectedVariant == "Red" && i.Quantity == 1);
order.Items.Should().Contain(i => i.SelectedVariant == "Blue" && i.Quantity == 2);
}
[Fact]
public async Task CreateOrder_WithMultiBuy_ShouldApplyMultiBuyPricing()
{
// Arrange
var product = await CreateProductWithMultiBuys();
var multiBuy = product.MultiBuys.First(mb => mb.Quantity == 3);
var orderRequest = new CreateOrderDto
{
IdentityReference = "test_user_" + Guid.NewGuid(),
ShippingName = "Jane Doe",
ShippingAddress = "456 Test Ave",
ShippingCity = "Test Town",
ShippingPostCode = "TT2 2ST",
ShippingCountry = "United Kingdom",
Items = new List<CreateOrderItemDto>
{
new()
{
ProductId = product.Id,
ProductMultiBuyId = multiBuy.Id,
Quantity = 3
}
}
};
// Act
var response = await _client.PostAsJsonAsync("/api/orders", orderRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var order = await response.Content.ReadFromJsonAsync<OrderDto>();
order.Should().NotBeNull();
order!.Items.Should().HaveCount(1);
var item = order.Items.First();
item.Quantity.Should().Be(3);
item.ProductMultiBuyId.Should().Be(multiBuy.Id);
item.UnitPrice.Should().Be(multiBuy.PricePerUnit);
item.TotalPrice.Should().Be(multiBuy.Price);
}
[Fact]
public async Task CreateOrder_WithMultiBuyAndVariant_ShouldStoreBoth()
{
// Arrange
var product = await CreateProductWithMultiBuysAndVariants();
var multiBuy = product.MultiBuys.First(mb => mb.Quantity == 2);
var orderRequest = new CreateOrderDto
{
IdentityReference = "test_user_" + Guid.NewGuid(),
ShippingName = "Bob Smith",
ShippingAddress = "789 Test Rd",
ShippingCity = "Testville",
ShippingPostCode = "TV3 3ST",
ShippingCountry = "United Kingdom",
Items = new List<CreateOrderItemDto>
{
new()
{
ProductId = product.Id,
ProductMultiBuyId = multiBuy.Id,
Quantity = 2,
SelectedVariant = "Vanilla"
}
}
};
// Act
var response = await _client.PostAsJsonAsync("/api/orders", orderRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var order = await response.Content.ReadFromJsonAsync<OrderDto>();
order.Should().NotBeNull();
var item = order.Items.First();
item.ProductMultiBuyId.Should().Be(multiBuy.Id);
item.SelectedVariant.Should().Be("Vanilla");
item.Quantity.Should().Be(2);
}
[Fact]
public async Task CreateOrder_MixedItems_ShouldCalculateTotalCorrectly()
{
// Arrange
var regularProduct = await CreateRegularProduct();
var variantProduct = await CreateProductWithVariants();
var multiBuyProduct = await CreateProductWithMultiBuys();
var multiBuy = multiBuyProduct.MultiBuys.First(mb => mb.Quantity == 3);
var orderRequest = new CreateOrderDto
{
IdentityReference = "test_user_" + Guid.NewGuid(),
ShippingName = "Alice Johnson",
ShippingAddress = "321 Mixed St",
ShippingCity = "Complex City",
ShippingPostCode = "CC4 4ST",
ShippingCountry = "United Kingdom",
Items = new List<CreateOrderItemDto>
{
// Regular product
new()
{
ProductId = regularProduct.Id,
Quantity = 2
},
// Product with variant
new()
{
ProductId = variantProduct.Id,
Quantity = 1,
SelectedVariant = "Blue"
},
// Product with multi-buy
new()
{
ProductId = multiBuyProduct.Id,
ProductMultiBuyId = multiBuy.Id,
Quantity = 3
}
}
};
// Act
var response = await _client.PostAsJsonAsync("/api/orders", orderRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var order = await response.Content.ReadFromJsonAsync<OrderDto>();
order.Should().NotBeNull();
order!.Items.Should().HaveCount(3);
// Verify total calculation
var expectedTotal = (regularProduct.Price * 2) +
variantProduct.Price +
multiBuy.Price;
order.TotalAmount.Should().Be(expectedTotal);
}
[Fact]
public async Task GetOrder_ShouldIncludeVariantAndMultiBuyInfo()
{
// Arrange
var product = await CreateProductWithMultiBuysAndVariants();
var multiBuy = product.MultiBuys.First();
var orderRequest = new CreateOrderDto
{
IdentityReference = "test_user_" + Guid.NewGuid(),
ShippingName = "Test User",
ShippingAddress = "Test Address",
ShippingCity = "Test City",
ShippingPostCode = "TE1 1ST",
ShippingCountry = "United Kingdom",
Items = new List<CreateOrderItemDto>
{
new()
{
ProductId = product.Id,
ProductMultiBuyId = multiBuy.Id,
Quantity = multiBuy.Quantity,
SelectedVariant = "Chocolate"
}
}
};
var createResponse = await _client.PostAsJsonAsync("/api/orders", orderRequest);
var createdOrder = await createResponse.Content.ReadFromJsonAsync<OrderDto>();
// Act
var getResponse = await _client.GetAsync($"/api/orders/by-identity/{orderRequest.IdentityReference}/{createdOrder!.Id}");
// Assert
getResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var retrievedOrder = await getResponse.Content.ReadFromJsonAsync<OrderDto>();
retrievedOrder.Should().NotBeNull();
var item = retrievedOrder!.Items.First();
item.ProductMultiBuyId.Should().Be(multiBuy.Id);
item.SelectedVariant.Should().Be("Chocolate");
}
private async Task<ProductDto> CreateRegularProduct()
{
var category = await CreateTestCategory();
var productDto = new CreateProductDto
{
Name = "Regular Product",
Description = "A regular product without variants",
Price = 15.00m,
CategoryId = category.Id,
Weight = 1.0m,
WeightUnit = (int)LittleShop.Enums.ProductWeightUnit.Kilograms
};
var response = await _client.PostAsJsonAsync("/api/admin/products", productDto);
response.EnsureSuccessStatusCode();
return (await response.Content.ReadFromJsonAsync<ProductDto>())!;
}
private async Task<ProductDto> CreateProductWithVariants()
{
var product = await CreateRegularProduct();
// Add variants
var variants = new[] { "Red", "Blue", "Green" };
foreach (var variantName in variants)
{
var variantDto = new CreateProductVariantDto
{
ProductId = product.Id,
Name = variantName,
VariantType = "Color",
StockLevel = 100
};
await _client.PostAsJsonAsync($"/api/admin/products/{product.Id}/variants", variantDto);
}
// Refresh product to get variants
var response = await _client.GetAsync($"/api/catalog/products/{product.Id}");
return (await response.Content.ReadFromJsonAsync<ProductDto>())!;
}
private async Task<ProductDto> CreateProductWithMultiBuys()
{
var product = await CreateRegularProduct();
// Add multi-buys
var multiBuys = new[]
{
new CreateProductMultiBuyDto
{
ProductId = product.Id,
Name = "Twin Pack",
Quantity = 2,
Price = 25.00m
},
new CreateProductMultiBuyDto
{
ProductId = product.Id,
Name = "Triple Pack",
Quantity = 3,
Price = 35.00m
}
};
foreach (var multiBuyDto in multiBuys)
{
await _client.PostAsJsonAsync($"/api/admin/products/{product.Id}/multibuys", multiBuyDto);
}
// Refresh product to get multi-buys
var response = await _client.GetAsync($"/api/catalog/products/{product.Id}");
return (await response.Content.ReadFromJsonAsync<ProductDto>())!;
}
private async Task<ProductDto> CreateProductWithMultiBuysAndVariants()
{
var product = await CreateProductWithMultiBuys();
// Add variants
var variants = new[] { "Vanilla", "Chocolate", "Strawberry" };
foreach (var variantName in variants)
{
var variantDto = new CreateProductVariantDto
{
ProductId = product.Id,
Name = variantName,
VariantType = "Flavor",
StockLevel = 50
};
await _client.PostAsJsonAsync($"/api/admin/products/{product.Id}/variants", variantDto);
}
// Refresh product
var response = await _client.GetAsync($"/api/catalog/products/{product.Id}");
return (await response.Content.ReadFromJsonAsync<ProductDto>())!;
}
private async Task<CategoryDto> CreateTestCategory()
{
var categoryDto = new CreateCategoryDto
{
Name = "Test Category " + Guid.NewGuid(),
Description = "Test category for integration tests"
};
var response = await _client.PostAsJsonAsync("/api/admin/categories", categoryDto);
response.EnsureSuccessStatusCode();
return (await response.Content.ReadFromJsonAsync<CategoryDto>())!;
}
}