littleshop/LittleShop/Controllers/BotDirectoryController.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

152 lines
4.5 KiB
C#

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";
}
}