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>
This commit is contained in:
55
LittleShop/Areas/Admin/Controllers/ActivityController.cs
Normal file
55
LittleShop/Areas/Admin/Controllers/ActivityController.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using LittleShop.Services;
|
||||
|
||||
namespace LittleShop.Areas.Admin.Controllers;
|
||||
|
||||
[Area("Admin")]
|
||||
[Authorize(Policy = "AdminOnly")]
|
||||
public class ActivityController : Controller
|
||||
{
|
||||
private readonly IBotActivityService _activityService;
|
||||
private readonly ILogger<ActivityController> _logger;
|
||||
|
||||
public ActivityController(IBotActivityService activityService, ILogger<ActivityController> logger)
|
||||
{
|
||||
_activityService = activityService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// GET: /Admin/Activity
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
// GET: /Admin/Activity/Live
|
||||
public IActionResult Live()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
// API endpoint for initial data load
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetSummary()
|
||||
{
|
||||
var summary = await _activityService.GetLiveActivitySummaryAsync();
|
||||
return Json(summary);
|
||||
}
|
||||
|
||||
// API endpoint for activity stats
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetStats(int hoursBack = 24)
|
||||
{
|
||||
var stats = await _activityService.GetActivityTypeStatsAsync(hoursBack);
|
||||
return Json(stats);
|
||||
}
|
||||
|
||||
// API endpoint for recent activities
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRecent(int minutesBack = 5)
|
||||
{
|
||||
var activities = await _activityService.GetRecentActivitiesAsync(minutesBack);
|
||||
return Json(activities);
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,8 @@ public class DashboardController : Controller
|
||||
ViewData["TotalRevenue"] = orders.Where(o => o.PaidAt.HasValue).Sum(o => o.TotalAmount).ToString("F2");
|
||||
|
||||
// Enhanced metrics
|
||||
ViewData["TotalVariations"] = products.Sum(p => p.Variations.Count);
|
||||
ViewData["TotalMultiBuys"] = products.Sum(p => p.MultiBuys.Count);
|
||||
ViewData["TotalVariants"] = products.Sum(p => p.Variants.Count);
|
||||
ViewData["PendingOrders"] = orders.Count(o => o.Status == LittleShop.Enums.OrderStatus.PendingPayment);
|
||||
ViewData["ShippedOrders"] = orders.Count(o => o.Status == LittleShop.Enums.OrderStatus.Shipped);
|
||||
ViewData["TotalStock"] = products.Sum(p => p.StockQuantity);
|
||||
|
||||
@@ -156,7 +156,7 @@ public class ProductsController : Controller
|
||||
return NotFound();
|
||||
|
||||
ViewData["Product"] = product;
|
||||
var variations = await _productService.GetProductVariationsAsync(id);
|
||||
var variations = await _productService.GetProductMultiBuysAsync(id);
|
||||
return View(variations);
|
||||
}
|
||||
|
||||
@@ -174,15 +174,15 @@ public class ProductsController : Controller
|
||||
ViewData["Product"] = product;
|
||||
|
||||
// Get existing quantities to help user avoid duplicates
|
||||
var existingQuantities = await _productService.GetProductVariationsAsync(productId);
|
||||
var existingQuantities = await _productService.GetProductMultiBuysAsync(productId);
|
||||
ViewData["ExistingQuantities"] = existingQuantities.Select(v => v.Quantity).ToList();
|
||||
|
||||
return View(new CreateProductVariationDto { ProductId = productId });
|
||||
return View(new CreateProductMultiBuyDto { ProductId = productId });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> CreateVariation(CreateProductVariationDto model)
|
||||
public async Task<IActionResult> CreateVariation(CreateProductMultiBuyDto model)
|
||||
{
|
||||
// Debug form data
|
||||
Console.WriteLine("=== FORM DEBUG ===");
|
||||
@@ -210,7 +210,7 @@ public class ProductsController : Controller
|
||||
ViewData["Product"] = product;
|
||||
|
||||
// Re-populate existing quantities for error display
|
||||
var existingQuantities = await _productService.GetProductVariationsAsync(model.ProductId);
|
||||
var existingQuantities = await _productService.GetProductMultiBuysAsync(model.ProductId);
|
||||
ViewData["ExistingQuantities"] = existingQuantities.Select(v => v.Quantity).ToList();
|
||||
|
||||
return View(model);
|
||||
@@ -218,7 +218,7 @@ public class ProductsController : Controller
|
||||
|
||||
try
|
||||
{
|
||||
await _productService.CreateProductVariationAsync(model);
|
||||
await _productService.CreateProductMultiBuyAsync(model);
|
||||
return RedirectToAction(nameof(Variations), new { id = model.ProductId });
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
@@ -237,7 +237,7 @@ public class ProductsController : Controller
|
||||
ViewData["Product"] = product;
|
||||
|
||||
// Re-populate existing quantities for error display
|
||||
var existingQuantities = await _productService.GetProductVariationsAsync(model.ProductId);
|
||||
var existingQuantities = await _productService.GetProductMultiBuysAsync(model.ProductId);
|
||||
ViewData["ExistingQuantities"] = existingQuantities.Select(v => v.Quantity).ToList();
|
||||
|
||||
return View(model);
|
||||
@@ -246,14 +246,14 @@ public class ProductsController : Controller
|
||||
|
||||
public async Task<IActionResult> EditVariation(Guid id)
|
||||
{
|
||||
var variation = await _productService.GetProductVariationByIdAsync(id);
|
||||
var variation = await _productService.GetProductMultiBuyByIdAsync(id);
|
||||
if (variation == null)
|
||||
return NotFound();
|
||||
|
||||
var product = await _productService.GetProductByIdAsync(variation.ProductId);
|
||||
ViewData["Product"] = product;
|
||||
|
||||
var model = new UpdateProductVariationDto
|
||||
var model = new UpdateProductMultiBuyDto
|
||||
{
|
||||
Name = variation.Name,
|
||||
Description = variation.Description,
|
||||
@@ -268,21 +268,21 @@ public class ProductsController : Controller
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EditVariation(Guid id, UpdateProductVariationDto model)
|
||||
public async Task<IActionResult> EditVariation(Guid id, UpdateProductMultiBuyDto model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
var variation = await _productService.GetProductVariationByIdAsync(id);
|
||||
var variation = await _productService.GetProductMultiBuyByIdAsync(id);
|
||||
var product = await _productService.GetProductByIdAsync(variation!.ProductId);
|
||||
ViewData["Product"] = product;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var success = await _productService.UpdateProductVariationAsync(id, model);
|
||||
var success = await _productService.UpdateProductMultiBuyAsync(id, model);
|
||||
if (!success)
|
||||
return NotFound();
|
||||
|
||||
var variationToRedirect = await _productService.GetProductVariationByIdAsync(id);
|
||||
var variationToRedirect = await _productService.GetProductMultiBuyByIdAsync(id);
|
||||
return RedirectToAction(nameof(Variations), new { id = variationToRedirect!.ProductId });
|
||||
}
|
||||
|
||||
@@ -290,11 +290,11 @@ public class ProductsController : Controller
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> DeleteVariation(Guid id)
|
||||
{
|
||||
var variation = await _productService.GetProductVariationByIdAsync(id);
|
||||
var variation = await _productService.GetProductMultiBuyByIdAsync(id);
|
||||
if (variation == null)
|
||||
return NotFound();
|
||||
|
||||
await _productService.DeleteProductVariationAsync(id);
|
||||
await _productService.DeleteProductMultiBuyAsync(id);
|
||||
return RedirectToAction(nameof(Variations), new { id = variation.ProductId });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user