littleshop/LittleShop/Services/BotActivityService.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

225 lines
7.3 KiB
C#

using System.Text.Json;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using LittleShop.Data;
using LittleShop.DTOs;
using LittleShop.Hubs;
using LittleShop.Models;
namespace LittleShop.Services;
public class BotActivityService : IBotActivityService
{
private readonly LittleShopContext _context;
private readonly IHubContext<ActivityHub> _hubContext;
private readonly ILogger<BotActivityService> _logger;
public BotActivityService(
LittleShopContext context,
IHubContext<ActivityHub> hubContext,
ILogger<BotActivityService> logger)
{
_context = context;
_hubContext = hubContext;
_logger = logger;
}
public async Task<BotActivity> LogActivityAsync(CreateBotActivityDto dto)
{
var activity = new BotActivity
{
Id = Guid.NewGuid(),
BotId = dto.BotId,
SessionIdentifier = dto.SessionIdentifier,
UserDisplayName = dto.UserDisplayName,
ActivityType = dto.ActivityType,
ActivityDescription = dto.ActivityDescription,
ProductId = dto.ProductId,
ProductName = dto.ProductName,
OrderId = dto.OrderId,
CategoryName = dto.CategoryName,
Value = dto.Value,
Quantity = dto.Quantity,
Platform = dto.Platform,
DeviceType = dto.DeviceType,
Location = dto.Location,
Timestamp = DateTime.UtcNow,
Metadata = dto.Metadata
};
_context.BotActivities.Add(activity);
await _context.SaveChangesAsync();
// Broadcast the activity to connected clients
await BroadcastActivityAsync(activity);
_logger.LogInformation("Activity logged: {User} - {Type} - {Description}",
activity.UserDisplayName, activity.ActivityType, activity.ActivityDescription);
return activity;
}
public async Task<List<BotActivityDto>> GetRecentActivitiesAsync(int minutesBack = 5)
{
var cutoffTime = DateTime.UtcNow.AddMinutes(-minutesBack);
var activities = await _context.BotActivities
.Include(a => a.Bot)
.Where(a => a.Timestamp >= cutoffTime)
.OrderByDescending(a => a.Timestamp)
.Take(100)
.Select(a => MapToDto(a))
.ToListAsync();
return activities;
}
public async Task<List<BotActivityDto>> GetActivitiesBySessionAsync(string sessionIdentifier)
{
var activities = await _context.BotActivities
.Include(a => a.Bot)
.Where(a => a.SessionIdentifier == sessionIdentifier)
.OrderByDescending(a => a.Timestamp)
.Take(200)
.Select(a => MapToDto(a))
.ToListAsync();
return activities;
}
public async Task<List<BotActivityDto>> GetActivitiesByBotAsync(Guid botId, int limit = 100)
{
var activities = await _context.BotActivities
.Include(a => a.Bot)
.Where(a => a.BotId == botId)
.OrderByDescending(a => a.Timestamp)
.Take(limit)
.Select(a => MapToDto(a))
.ToListAsync();
return activities;
}
public async Task<LiveActivitySummaryDto> GetLiveActivitySummaryAsync()
{
var fiveMinutesAgo = DateTime.UtcNow.AddMinutes(-5);
var oneMinuteAgo = DateTime.UtcNow.AddMinutes(-1);
var recentActivities = await _context.BotActivities
.Include(a => a.Bot)
.Where(a => a.Timestamp >= fiveMinutesAgo)
.ToListAsync();
var activeUsers = recentActivities
.Where(a => a.Timestamp >= oneMinuteAgo)
.Select(a => a.SessionIdentifier)
.Distinct()
.Count();
var activeUserNames = recentActivities
.Where(a => a.Timestamp >= oneMinuteAgo)
.Select(a => a.UserDisplayName)
.Distinct()
.Take(10)
.ToList();
var productViews = recentActivities
.Where(a => a.ActivityType == "ViewProduct")
.Count();
var cartsActive = recentActivities
.Where(a => a.ActivityType == "AddToCart" || a.ActivityType == "UpdateCart")
.Select(a => a.SessionIdentifier)
.Distinct()
.Count();
var totalCartValue = recentActivities
.Where(a => a.ActivityType == "AddToCart" && a.Value.HasValue)
.Sum(a => a.Value ?? 0);
var summary = new LiveActivitySummaryDto
{
ActiveUsers = activeUsers,
TotalActivitiesLast5Min = recentActivities.Count,
ProductViewsLast5Min = productViews,
CartsActiveNow = cartsActive,
TotalValueInCartsNow = totalCartValue,
ActiveUserNames = activeUserNames,
RecentActivities = recentActivities
.OrderByDescending(a => a.Timestamp)
.Take(20)
.Select(a => MapToDto(a))
.ToList()
};
return summary;
}
public async Task<List<BotActivityDto>> GetProductActivitiesAsync(Guid productId, int limit = 50)
{
var activities = await _context.BotActivities
.Include(a => a.Bot)
.Where(a => a.ProductId == productId)
.OrderByDescending(a => a.Timestamp)
.Take(limit)
.Select(a => MapToDto(a))
.ToListAsync();
return activities;
}
public async Task<Dictionary<string, int>> GetActivityTypeStatsAsync(int hoursBack = 24)
{
var cutoffTime = DateTime.UtcNow.AddHours(-hoursBack);
var stats = await _context.BotActivities
.Where(a => a.Timestamp >= cutoffTime)
.GroupBy(a => a.ActivityType)
.Select(g => new { Type = g.Key, Count = g.Count() })
.ToDictionaryAsync(x => x.Type, x => x.Count);
return stats;
}
public async Task BroadcastActivityAsync(BotActivity activity)
{
try
{
var dto = MapToDto(activity);
await _hubContext.Clients.All.SendAsync("NewActivity", dto);
// Also send summary update
var summary = await GetLiveActivitySummaryAsync();
await _hubContext.Clients.All.SendAsync("SummaryUpdate", summary);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error broadcasting activity");
}
}
private static BotActivityDto MapToDto(BotActivity activity)
{
return new BotActivityDto
{
Id = activity.Id,
BotId = activity.BotId,
BotName = activity.Bot?.Name ?? "Unknown Bot",
SessionIdentifier = activity.SessionIdentifier,
UserDisplayName = activity.UserDisplayName,
ActivityType = activity.ActivityType,
ActivityDescription = activity.ActivityDescription,
ProductId = activity.ProductId,
ProductName = activity.ProductName,
OrderId = activity.OrderId,
CategoryName = activity.CategoryName,
Value = activity.Value,
Quantity = activity.Quantity,
Platform = activity.Platform,
DeviceType = activity.DeviceType,
Location = activity.Location,
Timestamp = activity.Timestamp,
Metadata = activity.Metadata
};
}
}