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:
152
LittleShop/Controllers/BotDirectoryController.cs
Normal file
152
LittleShop/Controllers/BotDirectoryController.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using QRCoder;
|
||||
using LittleShop.Data;
|
||||
using LittleShop.DTOs;
|
||||
using LittleShop.Enums;
|
||||
|
||||
namespace LittleShop.Controllers;
|
||||
|
||||
public class BotDirectoryController : Controller
|
||||
{
|
||||
private readonly LittleShopContext _context;
|
||||
private readonly ILogger<BotDirectoryController> _logger;
|
||||
|
||||
public BotDirectoryController(LittleShopContext context, ILogger<BotDirectoryController> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// GET: /bots
|
||||
[HttpGet("/bots")]
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var bots = await _context.Bots
|
||||
.Where(b => b.Status == BotStatus.Active && b.IsActive)
|
||||
.OrderBy(b => b.Name)
|
||||
.Select(b => new BotDirectoryDto
|
||||
{
|
||||
Id = b.Id,
|
||||
Name = b.Name,
|
||||
Description = b.Description,
|
||||
Type = b.Type.ToString(),
|
||||
PersonalityName = b.PersonalityName,
|
||||
TelegramUsername = b.Settings.Contains("\"telegram_username\":")
|
||||
? ExtractTelegramUsername(b.Settings)
|
||||
: b.PlatformUsername,
|
||||
LastSeenAt = b.LastSeenAt,
|
||||
CreatedAt = b.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return View(bots);
|
||||
}
|
||||
|
||||
// GET: /bots/qr/{id}
|
||||
[HttpGet("/bots/qr/{id}")]
|
||||
public async Task<IActionResult> GetQRCode(Guid id)
|
||||
{
|
||||
var bot = await _context.Bots
|
||||
.Where(b => b.Id == id && b.Status == BotStatus.Active)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (bot == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
// Extract Telegram username from settings JSON or use platform username
|
||||
string? telegramUsername = ExtractTelegramUsername(bot.Settings) ?? bot.PlatformUsername;
|
||||
|
||||
if (string.IsNullOrEmpty(telegramUsername))
|
||||
{
|
||||
return NotFound("Bot does not have a Telegram username configured");
|
||||
}
|
||||
|
||||
// Generate Telegram deep link
|
||||
string telegramUrl = $"https://t.me/{telegramUsername}";
|
||||
|
||||
// Generate QR code
|
||||
using (QRCodeGenerator qrGenerator = new QRCodeGenerator())
|
||||
{
|
||||
QRCodeData qrCodeData = qrGenerator.CreateQrCode(telegramUrl, QRCodeGenerator.ECCLevel.Q);
|
||||
using (PngByteQRCode qrCode = new PngByteQRCode(qrCodeData))
|
||||
{
|
||||
byte[] qrCodeImage = qrCode.GetGraphic(20);
|
||||
return File(qrCodeImage, "image/png");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ExtractTelegramUsername(string settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = System.Text.Json.JsonDocument.Parse(settings);
|
||||
if (json.RootElement.TryGetProperty("telegram_username", out var username))
|
||||
{
|
||||
return username.GetString();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// JSON parsing failed
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class BotDirectoryDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string PersonalityName { get; set; } = string.Empty;
|
||||
public string? TelegramUsername { get; set; }
|
||||
public DateTime? LastSeenAt { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public string GetBadgeColor()
|
||||
{
|
||||
return Type.ToLower() switch
|
||||
{
|
||||
"sales" => "primary",
|
||||
"support" => "success",
|
||||
"marketing" => "warning",
|
||||
"technical" => "info",
|
||||
_ => "secondary"
|
||||
};
|
||||
}
|
||||
|
||||
public string GetStatusBadge()
|
||||
{
|
||||
if (!LastSeenAt.HasValue)
|
||||
return "Never Connected";
|
||||
|
||||
var timeSinceLastSeen = DateTime.UtcNow - LastSeenAt.Value;
|
||||
if (timeSinceLastSeen.TotalMinutes < 5)
|
||||
return "Online";
|
||||
else if (timeSinceLastSeen.TotalHours < 1)
|
||||
return "Recently Active";
|
||||
else if (timeSinceLastSeen.TotalDays < 1)
|
||||
return "Active Today";
|
||||
else
|
||||
return $"Last seen {timeSinceLastSeen.Days} days ago";
|
||||
}
|
||||
|
||||
public string GetStatusColor()
|
||||
{
|
||||
if (!LastSeenAt.HasValue)
|
||||
return "secondary";
|
||||
|
||||
var timeSinceLastSeen = DateTime.UtcNow - LastSeenAt.Value;
|
||||
if (timeSinceLastSeen.TotalMinutes < 5)
|
||||
return "success";
|
||||
else if (timeSinceLastSeen.TotalHours < 1)
|
||||
return "info";
|
||||
else
|
||||
return "secondary";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user