Add customer communication system

This commit is contained in:
sysadmin
2025-08-27 18:02:39 +01:00
parent 1f7c0af497
commit eae5be3e7c
136 changed files with 14552 additions and 97 deletions

View File

@@ -0,0 +1,382 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using LittleShop.Data;
using LittleShop.DTOs;
using LittleShop.Models;
namespace LittleShop.Services;
public class BotMetricsService : IBotMetricsService
{
private readonly LittleShopContext _context;
private readonly ILogger<BotMetricsService> _logger;
public BotMetricsService(LittleShopContext context, ILogger<BotMetricsService> logger)
{
_context = context;
_logger = logger;
}
// Metrics Methods
public async Task<BotMetricDto> RecordMetricAsync(Guid botId, CreateBotMetricDto dto)
{
var metric = new BotMetric
{
Id = Guid.NewGuid(),
BotId = botId,
MetricType = dto.MetricType,
Value = dto.Value,
Category = dto.Category,
Description = dto.Description,
Metadata = JsonSerializer.Serialize(dto.Metadata),
RecordedAt = DateTime.UtcNow
};
_context.BotMetrics.Add(metric);
await _context.SaveChangesAsync();
return MapMetricToDto(metric);
}
public async Task<bool> RecordMetricsBatchAsync(Guid botId, BotMetricsBatchDto dto)
{
try
{
var metrics = dto.Metrics.Select(m => new BotMetric
{
Id = Guid.NewGuid(),
BotId = botId,
MetricType = m.MetricType,
Value = m.Value,
Category = m.Category,
Description = m.Description,
Metadata = JsonSerializer.Serialize(m.Metadata),
RecordedAt = DateTime.UtcNow
}).ToList();
_context.BotMetrics.AddRange(metrics);
await _context.SaveChangesAsync();
_logger.LogInformation("Recorded {Count} metrics for bot {BotId}", metrics.Count, botId);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to record metrics batch for bot {BotId}", botId);
return false;
}
}
public async Task<IEnumerable<BotMetricDto>> GetBotMetricsAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null)
{
var query = _context.BotMetrics.Where(m => m.BotId == botId);
if (startDate.HasValue)
query = query.Where(m => m.RecordedAt >= startDate.Value);
if (endDate.HasValue)
query = query.Where(m => m.RecordedAt <= endDate.Value);
var metrics = await query
.OrderByDescending(m => m.RecordedAt)
.Take(1000) // Limit to prevent large results
.ToListAsync();
return metrics.Select(MapMetricToDto);
}
public async Task<BotMetricsSummaryDto> GetMetricsSummaryAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null)
{
var bot = await _context.Bots.FindAsync(botId);
if (bot == null)
return new BotMetricsSummaryDto { BotId = botId };
var start = startDate ?? DateTime.UtcNow.AddDays(-30);
var end = endDate ?? DateTime.UtcNow;
var metrics = await _context.BotMetrics
.Where(m => m.BotId == botId && m.RecordedAt >= start && m.RecordedAt <= end)
.ToListAsync();
var sessions = await _context.BotSessions
.Where(s => s.BotId == botId && s.StartedAt >= start && s.StartedAt <= end)
.ToListAsync();
var summary = new BotMetricsSummaryDto
{
BotId = botId,
BotName = bot.Name,
PeriodStart = start,
PeriodEnd = end,
TotalSessions = sessions.Count,
UniqueSessions = sessions.Select(s => s.SessionIdentifier).Distinct().Count(),
TotalOrders = sessions.Sum(s => s.OrderCount),
TotalRevenue = sessions.Sum(s => s.TotalSpent),
TotalMessages = sessions.Sum(s => s.MessageCount),
TotalErrors = (int)metrics.Where(m => m.MetricType == Enums.MetricType.Error).Sum(m => m.Value)
};
// Calculate average response time
var responseTimeMetrics = metrics.Where(m => m.MetricType == Enums.MetricType.ResponseTime).ToList();
if (responseTimeMetrics.Any())
summary.AverageResponseTime = responseTimeMetrics.Average(m => m.Value);
// Calculate uptime percentage
var uptimeMetrics = metrics.Where(m => m.MetricType == Enums.MetricType.Uptime).ToList();
if (uptimeMetrics.Any())
{
var totalPossibleUptime = (end - start).TotalMinutes;
var actualUptime = uptimeMetrics.Sum(m => m.Value);
summary.UptimePercentage = (actualUptime / (decimal)totalPossibleUptime) * 100;
}
// Group metrics by type
summary.MetricsByType = metrics
.GroupBy(m => m.MetricType.ToString())
.ToDictionary(g => g.Key, g => g.Sum(m => m.Value));
// Generate time series data (daily aggregation)
summary.TimeSeries = GenerateTimeSeries(metrics, start, end);
return summary;
}
// Session Methods
public async Task<BotSessionDto> StartSessionAsync(Guid botId, CreateBotSessionDto dto)
{
var session = new BotSession
{
Id = Guid.NewGuid(),
BotId = botId,
SessionIdentifier = dto.SessionIdentifier,
Platform = dto.Platform,
StartedAt = DateTime.UtcNow,
LastActivityAt = DateTime.UtcNow,
Language = dto.Language,
Country = dto.Country,
IsAnonymous = dto.IsAnonymous,
Metadata = JsonSerializer.Serialize(dto.Metadata),
OrderCount = 0,
MessageCount = 0,
TotalSpent = 0
};
_context.BotSessions.Add(session);
await _context.SaveChangesAsync();
_logger.LogInformation("Started session {SessionId} for bot {BotId}", session.Id, botId);
return MapSessionToDto(session);
}
public async Task<bool> UpdateSessionAsync(Guid sessionId, UpdateBotSessionDto dto)
{
var session = await _context.BotSessions.FindAsync(sessionId);
if (session == null)
return false;
session.LastActivityAt = DateTime.UtcNow;
if (dto.OrderCount.HasValue)
session.OrderCount = dto.OrderCount.Value;
if (dto.MessageCount.HasValue)
session.MessageCount = dto.MessageCount.Value;
if (dto.TotalSpent.HasValue)
session.TotalSpent = dto.TotalSpent.Value;
if (dto.EndSession.HasValue && dto.EndSession.Value)
session.EndedAt = DateTime.UtcNow;
if (dto.Metadata != null)
session.Metadata = JsonSerializer.Serialize(dto.Metadata);
await _context.SaveChangesAsync();
return true;
}
public async Task<BotSessionDto?> GetSessionAsync(Guid sessionId)
{
var session = await _context.BotSessions.FindAsync(sessionId);
return session != null ? MapSessionToDto(session) : null;
}
public async Task<IEnumerable<BotSessionDto>> GetBotSessionsAsync(Guid botId, bool activeOnly = false)
{
var query = _context.BotSessions.Where(s => s.BotId == botId);
if (activeOnly)
query = query.Where(s => !s.EndedAt.HasValue);
var sessions = await query
.OrderByDescending(s => s.StartedAt)
.Take(100)
.ToListAsync();
return sessions.Select(MapSessionToDto);
}
public async Task<BotSessionSummaryDto> GetSessionSummaryAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null)
{
var query = _context.BotSessions.Where(s => s.BotId == botId);
if (startDate.HasValue)
query = query.Where(s => s.StartedAt >= startDate.Value);
if (endDate.HasValue)
query = query.Where(s => s.StartedAt <= endDate.Value);
var sessions = await query.ToListAsync();
var summary = new BotSessionSummaryDto
{
TotalSessions = sessions.Count,
ActiveSessions = sessions.Count(s => !s.EndedAt.HasValue),
CompletedSessions = sessions.Count(s => s.EndedAt.HasValue)
};
if (sessions.Any())
{
var completedSessions = sessions.Where(s => s.EndedAt.HasValue).ToList();
if (completedSessions.Any())
{
summary.AverageSessionDuration = (decimal)completedSessions
.Average(s => (s.EndedAt!.Value - s.StartedAt).TotalMinutes);
}
summary.AverageOrdersPerSession = (decimal)sessions.Average(s => s.OrderCount);
summary.AverageSpendPerSession = sessions.Average(s => s.TotalSpent);
summary.SessionsByPlatform = sessions
.GroupBy(s => s.Platform)
.ToDictionary(g => g.Key, g => g.Count());
summary.SessionsByCountry = sessions
.Where(s => !string.IsNullOrEmpty(s.Country))
.GroupBy(s => s.Country)
.ToDictionary(g => g.Key, g => g.Count());
summary.SessionsByLanguage = sessions
.GroupBy(s => s.Language)
.ToDictionary(g => g.Key, g => g.Count());
}
return summary;
}
public async Task<bool> EndSessionAsync(Guid sessionId)
{
var session = await _context.BotSessions.FindAsync(sessionId);
if (session == null || session.EndedAt.HasValue)
return false;
session.EndedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogInformation("Ended session {SessionId}", sessionId);
return true;
}
public async Task<int> CleanupInactiveSessionsAsync(int inactiveMinutes = 30)
{
var cutoffTime = DateTime.UtcNow.AddMinutes(-inactiveMinutes);
var inactiveSessions = await _context.BotSessions
.Where(s => !s.EndedAt.HasValue && s.LastActivityAt < cutoffTime)
.ToListAsync();
foreach (var session in inactiveSessions)
{
session.EndedAt = DateTime.UtcNow;
}
await _context.SaveChangesAsync();
_logger.LogInformation("Cleaned up {Count} inactive sessions", inactiveSessions.Count);
return inactiveSessions.Count;
}
// Helper Methods
private BotMetricDto MapMetricToDto(BotMetric metric)
{
var metadata = new Dictionary<string, object>();
try
{
metadata = JsonSerializer.Deserialize<Dictionary<string, object>>(metric.Metadata)
?? new Dictionary<string, object>();
}
catch { }
return new BotMetricDto
{
Id = metric.Id,
BotId = metric.BotId,
MetricType = metric.MetricType,
Value = metric.Value,
Category = metric.Category,
Description = metric.Description,
RecordedAt = metric.RecordedAt,
Metadata = metadata
};
}
private BotSessionDto MapSessionToDto(BotSession session)
{
var metadata = new Dictionary<string, object>();
try
{
metadata = JsonSerializer.Deserialize<Dictionary<string, object>>(session.Metadata)
?? new Dictionary<string, object>();
}
catch { }
return new BotSessionDto
{
Id = session.Id,
BotId = session.BotId,
SessionIdentifier = session.SessionIdentifier,
Platform = session.Platform,
StartedAt = session.StartedAt,
LastActivityAt = session.LastActivityAt,
EndedAt = session.EndedAt,
OrderCount = session.OrderCount,
MessageCount = session.MessageCount,
TotalSpent = session.TotalSpent,
Language = session.Language,
Country = session.Country,
IsAnonymous = session.IsAnonymous,
Metadata = metadata
};
}
private List<TimeSeriesDataPoint> GenerateTimeSeries(List<BotMetric> metrics, DateTime start, DateTime end)
{
var dataPoints = new List<TimeSeriesDataPoint>();
var currentDate = start.Date;
while (currentDate <= end.Date)
{
var dayMetrics = metrics.Where(m => m.RecordedAt.Date == currentDate).ToList();
if (dayMetrics.Any())
{
dataPoints.Add(new TimeSeriesDataPoint
{
Timestamp = currentDate,
Label = currentDate.ToString("yyyy-MM-dd"),
Value = dayMetrics.Sum(m => m.Value)
});
}
currentDate = currentDate.AddDays(1);
}
return dataPoints;
}
}

View File

@@ -0,0 +1,330 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using LittleShop.Data;
using LittleShop.DTOs;
using LittleShop.Enums;
using LittleShop.Models;
namespace LittleShop.Services;
public class BotService : IBotService
{
private readonly LittleShopContext _context;
private readonly ILogger<BotService> _logger;
public BotService(LittleShopContext context, ILogger<BotService> logger)
{
_context = context;
_logger = logger;
}
public async Task<BotRegistrationResponseDto> RegisterBotAsync(BotRegistrationDto dto)
{
_logger.LogInformation("Registering new bot: {BotName}", dto.Name);
var botKey = await GenerateBotKeyAsync();
var bot = new Bot
{
Id = Guid.NewGuid(),
Name = dto.Name,
Description = dto.Description,
Type = dto.Type,
BotKey = botKey,
Status = BotStatus.Active,
Settings = JsonSerializer.Serialize(dto.InitialSettings),
Version = dto.Version,
PersonalityName = string.IsNullOrEmpty(dto.PersonalityName) ? AssignDefaultPersonality(dto.Name) : dto.PersonalityName,
CreatedAt = DateTime.UtcNow,
IsActive = true
};
_context.Bots.Add(bot);
await _context.SaveChangesAsync();
_logger.LogInformation("Bot registered successfully: {BotId}", bot.Id);
return new BotRegistrationResponseDto
{
BotId = bot.Id,
BotKey = botKey,
Name = bot.Name,
Settings = dto.InitialSettings
};
}
public async Task<BotDto?> AuthenticateBotAsync(string botKey)
{
var bot = await _context.Bots
.Include(b => b.Sessions)
.Include(b => b.Metrics)
.FirstOrDefaultAsync(b => b.BotKey == botKey && b.Status == BotStatus.Active);
if (bot == null)
{
_logger.LogWarning("Authentication failed for bot key: {BotKey}", botKey.Substring(0, 8) + "...");
return null;
}
// Update last seen
bot.LastSeenAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
return MapToDto(bot);
}
public async Task<BotDto?> GetBotByIdAsync(Guid id)
{
var bot = await _context.Bots
.Include(b => b.Sessions)
.Include(b => b.Metrics)
.FirstOrDefaultAsync(b => b.Id == id);
return bot != null ? MapToDto(bot) : null;
}
public async Task<BotDto?> GetBotByKeyAsync(string botKey)
{
var bot = await _context.Bots
.Include(b => b.Sessions)
.Include(b => b.Metrics)
.FirstOrDefaultAsync(b => b.BotKey == botKey);
return bot != null ? MapToDto(bot) : null;
}
public async Task<IEnumerable<BotDto>> GetAllBotsAsync()
{
try
{
var bots = await _context.Bots
.Include(b => b.Sessions)
.Include(b => b.Metrics)
.OrderByDescending(b => b.CreatedAt)
.ToListAsync();
return bots.Select(MapToDto);
}
catch (Microsoft.Data.Sqlite.SqliteException ex) when (ex.Message.Contains("no such table"))
{
// Tables don't exist yet - return empty list
return new List<BotDto>();
}
}
public async Task<IEnumerable<BotDto>> GetActiveBots()
{
try
{
var bots = await _context.Bots
.Include(b => b.Sessions)
.Include(b => b.Metrics)
.Where(b => b.Status == BotStatus.Active && b.IsActive)
.OrderByDescending(b => b.LastSeenAt)
.ToListAsync();
return bots.Select(MapToDto);
}
catch (Microsoft.Data.Sqlite.SqliteException ex) when (ex.Message.Contains("no such table"))
{
// Tables don't exist yet - return empty list
return new List<BotDto>();
}
}
public async Task<bool> UpdateBotSettingsAsync(Guid botId, UpdateBotSettingsDto dto)
{
var bot = await _context.Bots.FindAsync(botId);
if (bot == null)
return false;
bot.Settings = JsonSerializer.Serialize(dto.Settings);
bot.LastConfigSyncAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogInformation("Updated settings for bot {BotId}", botId);
return true;
}
public async Task<bool> UpdateBotStatusAsync(Guid botId, BotStatus status)
{
var bot = await _context.Bots.FindAsync(botId);
if (bot == null)
return false;
var oldStatus = bot.Status;
bot.Status = status;
bot.IsActive = status == BotStatus.Active;
await _context.SaveChangesAsync();
_logger.LogInformation("Bot {BotId} status changed from {OldStatus} to {NewStatus}",
botId, oldStatus, status);
return true;
}
public async Task<bool> RecordHeartbeatAsync(Guid botId, BotHeartbeatDto dto)
{
var bot = await _context.Bots.FindAsync(botId);
if (bot == null)
return false;
bot.LastSeenAt = DateTime.UtcNow;
bot.Version = dto.Version;
bot.IpAddress = dto.IpAddress;
// Record uptime metric
var uptimeMetric = new BotMetric
{
Id = Guid.NewGuid(),
BotId = botId,
MetricType = MetricType.Uptime,
Value = 1,
Metadata = JsonSerializer.Serialize(dto.Status),
RecordedAt = DateTime.UtcNow,
Category = "System",
Description = "Heartbeat"
};
_context.BotMetrics.Add(uptimeMetric);
await _context.SaveChangesAsync();
return true;
}
public async Task<bool> DeleteBotAsync(Guid botId)
{
var bot = await _context.Bots.FindAsync(botId);
if (bot == null)
return false;
bot.Status = BotStatus.Deleted;
bot.IsActive = false;
await _context.SaveChangesAsync();
_logger.LogInformation("Bot {BotId} marked as deleted", botId);
return true;
}
public async Task<Dictionary<string, object>> GetBotSettingsAsync(Guid botId)
{
var bot = await _context.Bots.FindAsync(botId);
if (bot == null)
return new Dictionary<string, object>();
try
{
return JsonSerializer.Deserialize<Dictionary<string, object>>(bot.Settings)
?? new Dictionary<string, object>();
}
catch
{
return new Dictionary<string, object>();
}
}
public async Task<bool> ValidateBotKeyAsync(string botKey)
{
return await _context.Bots.AnyAsync(b => b.BotKey == botKey && b.Status == BotStatus.Active);
}
public Task<string> GenerateBotKeyAsync()
{
const string prefix = "bot_";
const int keyLength = 32;
using var rng = RandomNumberGenerator.Create();
var bytes = new byte[keyLength];
rng.GetBytes(bytes);
var key = prefix + Convert.ToBase64String(bytes)
.Replace("+", "")
.Replace("/", "")
.Replace("=", "")
.Substring(0, keyLength);
return Task.FromResult(key);
}
public async Task<bool> UpdatePlatformInfoAsync(Guid botId, UpdatePlatformInfoDto dto)
{
var bot = await _context.Bots.FindAsync(botId);
if (bot == null)
return false;
bot.PlatformUsername = dto.PlatformUsername;
bot.PlatformDisplayName = dto.PlatformDisplayName;
bot.PlatformId = dto.PlatformId;
await _context.SaveChangesAsync();
_logger.LogInformation("Updated platform info for bot {BotId}: @{Username} ({DisplayName})",
botId, dto.PlatformUsername, dto.PlatformDisplayName);
return true;
}
private BotDto MapToDto(Bot bot)
{
var settings = new Dictionary<string, object>();
try
{
settings = JsonSerializer.Deserialize<Dictionary<string, object>>(bot.Settings)
?? new Dictionary<string, object>();
}
catch { }
var activeSessions = bot.Sessions.Count(s => !s.EndedAt.HasValue);
var totalRevenue = bot.Sessions.Sum(s => s.TotalSpent);
var totalOrders = bot.Sessions.Sum(s => s.OrderCount);
return new BotDto
{
Id = bot.Id,
Name = bot.Name,
Description = bot.Description,
Type = bot.Type,
Status = bot.Status,
CreatedAt = bot.CreatedAt,
LastSeenAt = bot.LastSeenAt,
LastConfigSyncAt = bot.LastConfigSyncAt,
IsActive = bot.IsActive,
Version = bot.Version,
IpAddress = bot.IpAddress,
PlatformUsername = bot.PlatformUsername,
PlatformDisplayName = bot.PlatformDisplayName,
PlatformId = bot.PlatformId,
PersonalityName = bot.PersonalityName,
Settings = settings,
TotalSessions = bot.Sessions.Count,
ActiveSessions = activeSessions,
TotalRevenue = totalRevenue,
TotalOrders = totalOrders
};
}
private string AssignDefaultPersonality(string botName)
{
var personalities = new[] { "Alan", "Dave", "Sarah", "Mike", "Emma", "Tom" };
// Smart assignment based on name
var lowerName = botName.ToLower();
if (lowerName.Contains("alan")) return "Alan";
if (lowerName.Contains("dave")) return "Dave";
if (lowerName.Contains("sarah")) return "Sarah";
if (lowerName.Contains("mike")) return "Mike";
if (lowerName.Contains("emma")) return "Emma";
if (lowerName.Contains("tom")) return "Tom";
// Random assignment if no match
var random = new Random();
return personalities[random.Next(personalities.Length)];
}
}

View File

@@ -163,14 +163,18 @@ public class CryptoPaymentService : ICryptoPaymentService
private static string GenerateWalletAddress(CryptoCurrency currency)
{
// Placeholder wallet addresses - in production these would come from BTCPay Server
var guid = Guid.NewGuid().ToString("N"); // 32 characters
return currency switch
{
CryptoCurrency.BTC => "bc1q" + Guid.NewGuid().ToString("N")[..26],
CryptoCurrency.XMR => "4" + Guid.NewGuid().ToString("N")[..94],
CryptoCurrency.USDT => "0x" + Guid.NewGuid().ToString("N")[..38],
CryptoCurrency.LTC => "ltc1q" + Guid.NewGuid().ToString("N")[..26],
CryptoCurrency.ETH => "0x" + Guid.NewGuid().ToString("N")[..38],
_ => "placeholder_" + Guid.NewGuid().ToString("N")[..20]
CryptoCurrency.BTC => "bc1q" + guid[..26],
CryptoCurrency.XMR => "4" + guid + guid[..32], // XMR needs ~95 chars, use double GUID
CryptoCurrency.USDT => "0x" + guid[..32], // ERC-20 address
CryptoCurrency.LTC => "ltc1q" + guid[..26],
CryptoCurrency.ETH => "0x" + guid[..32],
CryptoCurrency.ZEC => "zs1" + guid + guid[..29], // Shielded address
CryptoCurrency.DASH => "X" + guid[..30],
CryptoCurrency.DOGE => "D" + guid[..30],
_ => "placeholder_" + guid[..20]
};
}
}

View File

@@ -0,0 +1,233 @@
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using LittleShop.Data;
using LittleShop.DTOs;
using LittleShop.Models;
namespace LittleShop.Services;
public class CustomerMessageService : ICustomerMessageService
{
private readonly LittleShopContext _context;
private readonly IMapper _mapper;
private readonly ILogger<CustomerMessageService> _logger;
public CustomerMessageService(LittleShopContext context, IMapper mapper, ILogger<CustomerMessageService> logger)
{
_context = context;
_mapper = mapper;
_logger = logger;
}
public async Task<CustomerMessageDto?> CreateMessageAsync(CreateCustomerMessageDto createMessageDto)
{
try
{
var message = _mapper.Map<CustomerMessage>(createMessageDto);
message.Id = Guid.NewGuid();
message.Direction = MessageDirection.AdminToCustomer;
message.CreatedAt = DateTime.UtcNow;
message.Status = MessageStatus.Pending;
message.Platform = "Telegram";
// Generate thread ID if this is a new conversation
if (message.ParentMessageId == null)
{
message.ThreadId = Guid.NewGuid();
}
else
{
// Get parent message's thread ID
var parentMessage = await _context.CustomerMessages
.FirstOrDefaultAsync(m => m.Id == message.ParentMessageId);
message.ThreadId = parentMessage?.ThreadId ?? Guid.NewGuid();
}
_context.CustomerMessages.Add(message);
await _context.SaveChangesAsync();
_logger.LogInformation("Created message {MessageId} for customer {CustomerId}",
message.Id, message.CustomerId);
// Return the created message with includes
var createdMessage = await GetMessageByIdAsync(message.Id);
return createdMessage;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating message for customer {CustomerId}", createMessageDto.CustomerId);
return null;
}
}
public async Task<CustomerMessageDto?> GetMessageByIdAsync(Guid id)
{
var message = await _context.CustomerMessages
.Include(m => m.Customer)
.Include(m => m.Order)
.Include(m => m.AdminUser)
.Include(m => m.ParentMessage)
.Include(m => m.Replies)
.FirstOrDefaultAsync(m => m.Id == id);
if (message == null) return null;
return _mapper.Map<CustomerMessageDto>(message);
}
public async Task<IEnumerable<CustomerMessageDto>> GetCustomerMessagesAsync(Guid customerId)
{
var messages = await _context.CustomerMessages
.Include(m => m.Customer)
.Include(m => m.Order)
.Include(m => m.AdminUser)
.Where(m => m.CustomerId == customerId && !m.IsArchived)
.OrderByDescending(m => m.CreatedAt)
.ToListAsync();
return messages.Select(m => _mapper.Map<CustomerMessageDto>(m));
}
public async Task<IEnumerable<CustomerMessageDto>> GetOrderMessagesAsync(Guid orderId)
{
var messages = await _context.CustomerMessages
.Include(m => m.Customer)
.Include(m => m.Order)
.Include(m => m.AdminUser)
.Where(m => m.OrderId == orderId && !m.IsArchived)
.OrderByDescending(m => m.CreatedAt)
.ToListAsync();
return messages.Select(m => _mapper.Map<CustomerMessageDto>(m));
}
public async Task<IEnumerable<CustomerMessageDto>> GetPendingMessagesAsync(string platform = "Telegram")
{
var messages = await _context.CustomerMessages
.Include(m => m.Customer)
.Include(m => m.Order)
.Include(m => m.AdminUser)
.Where(m => m.Status == MessageStatus.Pending &&
m.Platform == platform &&
m.Direction == MessageDirection.AdminToCustomer &&
(m.ScheduledFor == null || m.ScheduledFor <= DateTime.UtcNow) &&
(m.ExpiresAt == null || m.ExpiresAt > DateTime.UtcNow))
.OrderBy(m => m.Priority)
.ThenBy(m => m.CreatedAt)
.Take(50) // Limit for performance
.ToListAsync();
return messages.Select(m => _mapper.Map<CustomerMessageDto>(m));
}
public async Task<bool> MarkMessageAsSentAsync(Guid messageId, string? platformMessageId = null)
{
var message = await _context.CustomerMessages.FindAsync(messageId);
if (message == null) return false;
message.MarkAsSent();
if (!string.IsNullOrEmpty(platformMessageId))
{
message.PlatformMessageId = platformMessageId;
}
await _context.SaveChangesAsync();
_logger.LogInformation("Marked message {MessageId} as sent", messageId);
return true;
}
public async Task<bool> MarkMessageAsDeliveredAsync(Guid messageId)
{
var message = await _context.CustomerMessages.FindAsync(messageId);
if (message == null) return false;
message.MarkAsDelivered();
await _context.SaveChangesAsync();
_logger.LogInformation("Marked message {MessageId} as delivered", messageId);
return true;
}
public async Task<bool> MarkMessageAsFailedAsync(Guid messageId, string reason)
{
var message = await _context.CustomerMessages.FindAsync(messageId);
if (message == null) return false;
message.MarkAsFailed(reason);
await _context.SaveChangesAsync();
_logger.LogWarning("Marked message {MessageId} as failed: {Reason}", messageId, reason);
return true;
}
public async Task<MessageThreadDto?> GetMessageThreadAsync(Guid threadId)
{
var messages = await _context.CustomerMessages
.Include(m => m.Customer)
.Include(m => m.Order)
.Include(m => m.AdminUser)
.Where(m => m.ThreadId == threadId)
.OrderBy(m => m.CreatedAt)
.ToListAsync();
if (!messages.Any()) return null;
var firstMessage = messages.First();
var thread = new MessageThreadDto
{
ThreadId = threadId,
Subject = firstMessage.Subject,
CustomerId = firstMessage.CustomerId,
CustomerName = firstMessage.Customer?.DisplayName ?? "Unknown",
OrderId = firstMessage.OrderId,
OrderReference = firstMessage.Order?.Id.ToString().Substring(0, 8),
StartedAt = firstMessage.CreatedAt,
LastMessageAt = messages.Max(m => m.CreatedAt),
MessageCount = messages.Count,
HasUnreadMessages = messages.Any(m => m.Direction == MessageDirection.CustomerToAdmin && m.Status != MessageStatus.Read),
RequiresResponse = messages.Any(m => m.RequiresResponse && m.Status != MessageStatus.Read),
Messages = messages.Select(m => _mapper.Map<CustomerMessageDto>(m)).ToList()
};
return thread;
}
public async Task<IEnumerable<MessageThreadDto>> GetActiveThreadsAsync()
{
var threads = await _context.CustomerMessages
.Include(m => m.Customer)
.Include(m => m.Order)
.Where(m => !m.IsArchived)
.GroupBy(m => m.ThreadId)
.Select(g => new MessageThreadDto
{
ThreadId = g.Key ?? Guid.Empty,
Subject = g.First().Subject,
CustomerId = g.First().CustomerId,
CustomerName = g.First().Customer != null ? g.First().Customer.DisplayName : "Unknown",
OrderId = g.First().OrderId,
OrderReference = g.First().Order != null ? g.First().Order.Id.ToString().Substring(0, 8) : null,
StartedAt = g.Min(m => m.CreatedAt),
LastMessageAt = g.Max(m => m.CreatedAt),
MessageCount = g.Count(),
HasUnreadMessages = g.Any(m => m.Direction == MessageDirection.CustomerToAdmin && m.Status != MessageStatus.Read),
RequiresResponse = g.Any(m => m.RequiresResponse && m.Status != MessageStatus.Read)
})
.OrderByDescending(t => t.LastMessageAt)
.Take(100) // Limit for performance
.ToListAsync();
return threads;
}
public async Task<bool> ValidateCustomerExistsAsync(Guid customerId)
{
return await _context.Customers.AnyAsync(c => c.Id == customerId);
}
public async Task<bool> ValidateOrderBelongsToCustomerAsync(Guid orderId, Guid customerId)
{
return await _context.Orders.AnyAsync(o => o.Id == orderId && o.CustomerId == customerId);
}
}

View File

@@ -0,0 +1,296 @@
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using LittleShop.Data;
using LittleShop.DTOs;
using LittleShop.Models;
namespace LittleShop.Services;
public class CustomerService : ICustomerService
{
private readonly LittleShopContext _context;
private readonly IMapper _mapper;
private readonly ILogger<CustomerService> _logger;
public CustomerService(LittleShopContext context, IMapper mapper, ILogger<CustomerService> logger)
{
_context = context;
_mapper = mapper;
_logger = logger;
}
public async Task<CustomerDto?> GetCustomerByIdAsync(Guid id)
{
var customer = await _context.Customers
.Include(c => c.Orders)
.FirstOrDefaultAsync(c => c.Id == id);
if (customer == null) return null;
var dto = _mapper.Map<CustomerDto>(customer);
dto.DisplayName = customer.DisplayName;
dto.CustomerType = customer.CustomerType;
return dto;
}
public async Task<CustomerDto?> GetCustomerByTelegramUserIdAsync(long telegramUserId)
{
var customer = await _context.Customers
.Include(c => c.Orders)
.FirstOrDefaultAsync(c => c.TelegramUserId == telegramUserId);
if (customer == null) return null;
var dto = _mapper.Map<CustomerDto>(customer);
dto.DisplayName = customer.DisplayName;
dto.CustomerType = customer.CustomerType;
return dto;
}
public async Task<CustomerDto> CreateCustomerAsync(CreateCustomerDto createCustomerDto)
{
// Check if customer already exists
var existingCustomer = await _context.Customers
.FirstOrDefaultAsync(c => c.TelegramUserId == createCustomerDto.TelegramUserId);
if (existingCustomer != null)
{
throw new InvalidOperationException($"Customer with Telegram ID {createCustomerDto.TelegramUserId} already exists");
}
var customer = _mapper.Map<Customer>(createCustomerDto);
customer.Id = Guid.NewGuid();
customer.CreatedAt = DateTime.UtcNow;
customer.UpdatedAt = DateTime.UtcNow;
customer.LastActiveAt = DateTime.UtcNow;
customer.IsActive = true;
// Set data retention date (default: 2 years after creation)
customer.DataRetentionDate = DateTime.UtcNow.AddYears(2);
_context.Customers.Add(customer);
await _context.SaveChangesAsync();
_logger.LogInformation("Created new customer {CustomerId} for Telegram user {TelegramUserId}",
customer.Id, customer.TelegramUserId);
var dto = _mapper.Map<CustomerDto>(customer);
dto.DisplayName = customer.DisplayName;
dto.CustomerType = customer.CustomerType;
return dto;
}
public async Task<CustomerDto?> UpdateCustomerAsync(Guid id, UpdateCustomerDto updateCustomerDto)
{
var customer = await _context.Customers.FindAsync(id);
if (customer == null) return null;
_mapper.Map(updateCustomerDto, customer);
customer.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogInformation("Updated customer {CustomerId}", id);
var dto = _mapper.Map<CustomerDto>(customer);
dto.DisplayName = customer.DisplayName;
dto.CustomerType = customer.CustomerType;
return dto;
}
public async Task<bool> DeleteCustomerAsync(Guid id)
{
var customer = await _context.Customers.FindAsync(id);
if (customer == null) return false;
// Instead of hard delete, mark as inactive for data retention compliance
customer.IsActive = false;
customer.DataRetentionDate = DateTime.UtcNow.AddDays(30); // Delete in 30 days
customer.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogInformation("Marked customer {CustomerId} for deletion", id);
return true;
}
public async Task<IEnumerable<CustomerDto>> GetAllCustomersAsync()
{
var customers = await _context.Customers
.Where(c => c.IsActive)
.Include(c => c.Orders)
.OrderByDescending(c => c.LastActiveAt)
.ToListAsync();
return customers.Select(c =>
{
var dto = _mapper.Map<CustomerDto>(c);
dto.DisplayName = c.DisplayName;
dto.CustomerType = c.CustomerType;
return dto;
});
}
public async Task<IEnumerable<CustomerDto>> SearchCustomersAsync(string searchTerm)
{
var query = _context.Customers
.Where(c => c.IsActive)
.Include(c => c.Orders)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(searchTerm))
{
searchTerm = searchTerm.ToLower();
query = query.Where(c =>
c.TelegramUsername.ToLower().Contains(searchTerm) ||
c.TelegramDisplayName.ToLower().Contains(searchTerm) ||
c.TelegramFirstName.ToLower().Contains(searchTerm) ||
c.TelegramLastName.ToLower().Contains(searchTerm) ||
(c.Email != null && c.Email.ToLower().Contains(searchTerm)));
}
var customers = await query
.OrderByDescending(c => c.LastActiveAt)
.Take(50) // Limit search results
.ToListAsync();
return customers.Select(c =>
{
var dto = _mapper.Map<CustomerDto>(c);
dto.DisplayName = c.DisplayName;
dto.CustomerType = c.CustomerType;
return dto;
});
}
public async Task<CustomerDto?> GetOrCreateCustomerAsync(long telegramUserId, string displayName, string username = "", string firstName = "", string lastName = "")
{
// Try to find existing customer
var customer = await _context.Customers
.Include(c => c.Orders)
.FirstOrDefaultAsync(c => c.TelegramUserId == telegramUserId);
if (customer != null)
{
// Update customer information if provided
bool updated = false;
if (!string.IsNullOrEmpty(displayName) && customer.TelegramDisplayName != displayName)
{
customer.TelegramDisplayName = displayName;
updated = true;
}
if (!string.IsNullOrEmpty(username) && customer.TelegramUsername != username)
{
customer.TelegramUsername = username;
updated = true;
}
if (!string.IsNullOrEmpty(firstName) && customer.TelegramFirstName != firstName)
{
customer.TelegramFirstName = firstName;
updated = true;
}
if (!string.IsNullOrEmpty(lastName) && customer.TelegramLastName != lastName)
{
customer.TelegramLastName = lastName;
updated = true;
}
customer.LastActiveAt = DateTime.UtcNow;
if (updated)
{
customer.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogInformation("Updated existing customer {CustomerId} information", customer.Id);
}
else
{
await _context.SaveChangesAsync(); // Just update LastActiveAt
}
var existingDto = _mapper.Map<CustomerDto>(customer);
existingDto.DisplayName = customer.DisplayName;
existingDto.CustomerType = customer.CustomerType;
return existingDto;
}
// Create new customer
customer = new Customer
{
Id = Guid.NewGuid(),
TelegramUserId = telegramUserId,
TelegramUsername = username,
TelegramDisplayName = displayName,
TelegramFirstName = firstName,
TelegramLastName = lastName,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
LastActiveAt = DateTime.UtcNow,
DataRetentionDate = DateTime.UtcNow.AddYears(2),
IsActive = true,
AllowOrderUpdates = true,
AllowMarketing = false,
Language = "en",
Timezone = "UTC"
};
_context.Customers.Add(customer);
await _context.SaveChangesAsync();
_logger.LogInformation("Created new customer {CustomerId} for Telegram user {TelegramUserId} ({DisplayName})",
customer.Id, telegramUserId, displayName);
var dto = _mapper.Map<CustomerDto>(customer);
dto.DisplayName = customer.DisplayName;
dto.CustomerType = customer.CustomerType;
return dto;
}
public async Task UpdateCustomerMetricsAsync(Guid customerId)
{
var customer = await _context.Customers
.Include(c => c.Orders)
.FirstOrDefaultAsync(c => c.Id == customerId);
if (customer == null) return;
customer.UpdateMetrics();
await _context.SaveChangesAsync();
_logger.LogInformation("Updated metrics for customer {CustomerId}", customerId);
}
public async Task<bool> BlockCustomerAsync(Guid customerId, string reason)
{
var customer = await _context.Customers.FindAsync(customerId);
if (customer == null) return false;
customer.IsBlocked = true;
customer.BlockReason = reason;
customer.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogWarning("Blocked customer {CustomerId} - Reason: {Reason}", customerId, reason);
return true;
}
public async Task<bool> UnblockCustomerAsync(Guid customerId)
{
var customer = await _context.Customers.FindAsync(customerId);
if (customer == null) return false;
customer.IsBlocked = false;
customer.BlockReason = null;
customer.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogInformation("Unblocked customer {CustomerId}", customerId);
return true;
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LittleShop.DTOs;
namespace LittleShop.Services;
public interface IBotMetricsService
{
// Metrics
Task<BotMetricDto> RecordMetricAsync(Guid botId, CreateBotMetricDto dto);
Task<bool> RecordMetricsBatchAsync(Guid botId, BotMetricsBatchDto dto);
Task<IEnumerable<BotMetricDto>> GetBotMetricsAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null);
Task<BotMetricsSummaryDto> GetMetricsSummaryAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null);
// Sessions
Task<BotSessionDto> StartSessionAsync(Guid botId, CreateBotSessionDto dto);
Task<bool> UpdateSessionAsync(Guid sessionId, UpdateBotSessionDto dto);
Task<BotSessionDto?> GetSessionAsync(Guid sessionId);
Task<IEnumerable<BotSessionDto>> GetBotSessionsAsync(Guid botId, bool activeOnly = false);
Task<BotSessionSummaryDto> GetSessionSummaryAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null);
Task<bool> EndSessionAsync(Guid sessionId);
Task<int> CleanupInactiveSessionsAsync(int inactiveMinutes = 30);
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LittleShop.DTOs;
using LittleShop.Enums;
namespace LittleShop.Services;
public interface IBotService
{
Task<BotRegistrationResponseDto> RegisterBotAsync(BotRegistrationDto dto);
Task<BotDto?> AuthenticateBotAsync(string botKey);
Task<BotDto?> GetBotByIdAsync(Guid id);
Task<BotDto?> GetBotByKeyAsync(string botKey);
Task<IEnumerable<BotDto>> GetAllBotsAsync();
Task<IEnumerable<BotDto>> GetActiveBots();
Task<bool> UpdateBotSettingsAsync(Guid botId, UpdateBotSettingsDto dto);
Task<bool> UpdateBotStatusAsync(Guid botId, BotStatus status);
Task<bool> RecordHeartbeatAsync(Guid botId, BotHeartbeatDto dto);
Task<bool> DeleteBotAsync(Guid botId);
Task<Dictionary<string, object>> GetBotSettingsAsync(Guid botId);
Task<bool> ValidateBotKeyAsync(string botKey);
Task<string> GenerateBotKeyAsync();
Task<bool> UpdatePlatformInfoAsync(Guid botId, UpdatePlatformInfoDto dto);
}

View File

@@ -0,0 +1,20 @@
using LittleShop.DTOs;
using LittleShop.Models;
namespace LittleShop.Services;
public interface ICustomerMessageService
{
Task<CustomerMessageDto?> CreateMessageAsync(CreateCustomerMessageDto createMessageDto);
Task<CustomerMessageDto?> GetMessageByIdAsync(Guid id);
Task<IEnumerable<CustomerMessageDto>> GetCustomerMessagesAsync(Guid customerId);
Task<IEnumerable<CustomerMessageDto>> GetOrderMessagesAsync(Guid orderId);
Task<IEnumerable<CustomerMessageDto>> GetPendingMessagesAsync(string platform = "Telegram");
Task<bool> MarkMessageAsSentAsync(Guid messageId, string? platformMessageId = null);
Task<bool> MarkMessageAsDeliveredAsync(Guid messageId);
Task<bool> MarkMessageAsFailedAsync(Guid messageId, string reason);
Task<MessageThreadDto?> GetMessageThreadAsync(Guid threadId);
Task<IEnumerable<MessageThreadDto>> GetActiveThreadsAsync();
Task<bool> ValidateCustomerExistsAsync(Guid customerId);
Task<bool> ValidateOrderBelongsToCustomerAsync(Guid orderId, Guid customerId);
}

View File

@@ -0,0 +1,19 @@
using LittleShop.DTOs;
using LittleShop.Models;
namespace LittleShop.Services;
public interface ICustomerService
{
Task<CustomerDto?> GetCustomerByIdAsync(Guid id);
Task<CustomerDto?> GetCustomerByTelegramUserIdAsync(long telegramUserId);
Task<CustomerDto> CreateCustomerAsync(CreateCustomerDto createCustomerDto);
Task<CustomerDto?> UpdateCustomerAsync(Guid id, UpdateCustomerDto updateCustomerDto);
Task<bool> DeleteCustomerAsync(Guid id);
Task<IEnumerable<CustomerDto>> GetAllCustomersAsync();
Task<IEnumerable<CustomerDto>> SearchCustomersAsync(string searchTerm);
Task<CustomerDto?> GetOrCreateCustomerAsync(long telegramUserId, string displayName, string username = "", string firstName = "", string lastName = "");
Task UpdateCustomerMetricsAsync(Guid customerId);
Task<bool> BlockCustomerAsync(Guid customerId, string reason);
Task<bool> UnblockCustomerAsync(Guid customerId);
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace LittleShop.Services;
public interface ITelegramBotManagerService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
Task<bool> AddBotAsync(Guid botId, string botToken);
Task<bool> RemoveBotAsync(Guid botId);
Task<bool> UpdateBotSettingsAsync(Guid botId);
Task<int> GetActiveBotCount();
}

View File

@@ -10,16 +10,19 @@ public class OrderService : IOrderService
{
private readonly LittleShopContext _context;
private readonly ILogger<OrderService> _logger;
private readonly ICustomerService _customerService;
public OrderService(LittleShopContext context, ILogger<OrderService> logger)
public OrderService(LittleShopContext context, ILogger<OrderService> logger, ICustomerService customerService)
{
_context = context;
_logger = logger;
_customerService = customerService;
}
public async Task<IEnumerable<OrderDto>> GetAllOrdersAsync()
{
var orders = await _context.Orders
.Include(o => o.Customer)
.Include(o => o.Items)
.ThenInclude(oi => oi.Product)
.Include(o => o.Payments)
@@ -32,6 +35,7 @@ public class OrderService : IOrderService
public async Task<IEnumerable<OrderDto>> GetOrdersByIdentityAsync(string identityReference)
{
var orders = await _context.Orders
.Include(o => o.Customer)
.Include(o => o.Items)
.ThenInclude(oi => oi.Product)
.Include(o => o.Payments)
@@ -45,6 +49,7 @@ public class OrderService : IOrderService
public async Task<OrderDto?> GetOrderByIdAsync(Guid id)
{
var order = await _context.Orders
.Include(o => o.Customer)
.Include(o => o.Items)
.ThenInclude(oi => oi.Product)
.Include(o => o.Payments)
@@ -59,10 +64,38 @@ public class OrderService : IOrderService
try
{
// Handle customer creation/linking during checkout
Guid? customerId = null;
string? identityReference = null;
if (createOrderDto.CustomerInfo != null)
{
// Create customer during checkout process
var customer = await _customerService.GetOrCreateCustomerAsync(
createOrderDto.CustomerInfo.TelegramUserId,
createOrderDto.CustomerInfo.TelegramDisplayName,
createOrderDto.CustomerInfo.TelegramUsername,
createOrderDto.CustomerInfo.TelegramFirstName,
createOrderDto.CustomerInfo.TelegramLastName);
customerId = customer?.Id;
}
else if (createOrderDto.CustomerId.HasValue)
{
// Order for existing customer
customerId = createOrderDto.CustomerId;
}
else
{
// Anonymous order (legacy support)
identityReference = createOrderDto.IdentityReference;
}
var order = new Order
{
Id = Guid.NewGuid(),
IdentityReference = createOrderDto.IdentityReference,
CustomerId = customerId,
IdentityReference = identityReference,
Status = OrderStatus.PendingPayment,
TotalAmount = 0,
Currency = "GBP",
@@ -105,8 +138,16 @@ public class OrderService : IOrderService
await _context.SaveChangesAsync();
await transaction.CommitAsync();
_logger.LogInformation("Created order {OrderId} for identity {Identity} with total {Total}",
order.Id, createOrderDto.IdentityReference, totalAmount);
if (customerId.HasValue)
{
_logger.LogInformation("Created order {OrderId} for customer {CustomerId} with total {Total}",
order.Id, customerId.Value, totalAmount);
}
else
{
_logger.LogInformation("Created order {OrderId} for identity {Identity} with total {Total}",
order.Id, identityReference, totalAmount);
}
// Reload order with includes
var createdOrder = await GetOrderByIdAsync(order.Id);
@@ -175,8 +216,26 @@ public class OrderService : IOrderService
return new OrderDto
{
Id = order.Id,
CustomerId = order.CustomerId,
IdentityReference = order.IdentityReference,
Status = order.Status,
Customer = order.Customer != null ? new CustomerSummaryDto
{
Id = order.Customer.Id,
DisplayName = !string.IsNullOrEmpty(order.Customer.TelegramDisplayName) ? order.Customer.TelegramDisplayName :
!string.IsNullOrEmpty(order.Customer.TelegramUsername) ? $"@{order.Customer.TelegramUsername}" :
$"{order.Customer.TelegramFirstName} {order.Customer.TelegramLastName}".Trim(),
TelegramUsername = order.Customer.TelegramUsername,
TotalOrders = order.Customer.TotalOrders,
TotalSpent = order.Customer.TotalSpent,
CustomerType = order.Customer.TotalOrders == 0 ? "New" :
order.Customer.TotalOrders == 1 ? "First-time" :
order.Customer.TotalOrders < 5 ? "Regular" :
order.Customer.TotalOrders < 20 ? "Loyal" : "VIP",
RiskScore = order.Customer.RiskScore,
LastActiveAt = order.Customer.LastActiveAt,
IsBlocked = order.Customer.IsBlocked
} : null,
TotalAmount = order.TotalAmount,
Currency = order.Currency,
ShippingName = order.ShippingName,

View File

@@ -0,0 +1,207 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using LittleShop.Data;
namespace LittleShop.Services;
public class TelegramBotManagerService : BackgroundService, ITelegramBotManagerService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<TelegramBotManagerService> _logger;
private readonly ConcurrentDictionary<Guid, BotInstance> _activeBots = new();
public TelegramBotManagerService(IServiceProvider serviceProvider, ILogger<TelegramBotManagerService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("🤖 Telegram Bot Manager Service starting...");
try
{
// Load all active bots from database
await LoadActiveBotsAsync();
// Keep service running
while (!stoppingToken.IsCancellationRequested)
{
// Periodic health checks and cleanup
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
await PerformHealthChecksAsync();
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("Telegram Bot Manager Service is stopping.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in Telegram Bot Manager Service");
}
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Telegram Bot Manager Service started");
await base.StartAsync(cancellationToken);
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stopping all Telegram bots...");
foreach (var bot in _activeBots.Values)
{
try
{
await bot.StopAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error stopping bot {BotId}", bot.BotId);
}
}
_activeBots.Clear();
await base.StopAsync(cancellationToken);
}
public async Task<bool> AddBotAsync(Guid botId, string botToken)
{
try
{
_logger.LogInformation("Adding bot {BotId} to Telegram manager", botId);
// Validate token first
using var httpClient = new HttpClient();
var response = await httpClient.GetAsync($"https://api.telegram.org/bot{botToken}/getMe");
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("Invalid bot token for bot {BotId}", botId);
return false;
}
// Create bot instance (placeholder for now - will implement Telegram.Bot later)
var botInstance = new BotInstance
{
BotId = botId,
BotToken = botToken,
IsRunning = false
};
_activeBots.TryAdd(botId, botInstance);
_logger.LogInformation("✅ Bot {BotId} added successfully", botId);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to add bot {BotId}", botId);
return false;
}
}
public async Task<bool> RemoveBotAsync(Guid botId)
{
if (_activeBots.TryRemove(botId, out var botInstance))
{
await botInstance.StopAsync();
_logger.LogInformation("Bot {BotId} removed from Telegram manager", botId);
return true;
}
return false;
}
public async Task<bool> UpdateBotSettingsAsync(Guid botId)
{
if (_activeBots.TryGetValue(botId, out var botInstance))
{
// Reload settings from database
_logger.LogInformation("Updating settings for bot {BotId}", botId);
return true;
}
return false;
}
public Task<int> GetActiveBotCount()
{
return Task.FromResult(_activeBots.Count(x => x.Value.IsRunning));
}
private async Task LoadActiveBotsAsync()
{
try
{
using var scope = _serviceProvider.CreateScope();
var botService = scope.ServiceProvider.GetRequiredService<IBotService>();
var activeBots = await botService.GetActiveBots();
_logger.LogInformation("Loading {Count} active bots", activeBots.Count());
foreach (var bot in activeBots)
{
// Look for telegram token in settings
if (bot.Settings.TryGetValue("telegram", out var telegramSettings) &&
telegramSettings is System.Text.Json.JsonElement telegramElement &&
telegramElement.TryGetProperty("botToken", out var tokenElement))
{
var token = tokenElement.GetString();
if (!string.IsNullOrEmpty(token) && token != "YOUR_BOT_TOKEN_HERE")
{
await AddBotAsync(bot.Id, token);
}
}
}
}
catch (Microsoft.Data.Sqlite.SqliteException ex) when (ex.Message.Contains("no such table"))
{
_logger.LogWarning("Bot tables don't exist yet. Skipping bot loading until database is fully initialized.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading active bots");
}
}
private async Task PerformHealthChecksAsync()
{
foreach (var kvp in _activeBots)
{
try
{
// Placeholder for health check logic
// In real implementation, would ping Telegram API
_logger.LogDebug("Health check for bot {BotId}", kvp.Key);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Health check failed for bot {BotId}", kvp.Key);
}
}
}
}
public class BotInstance
{
public Guid BotId { get; set; }
public string BotToken { get; set; } = string.Empty;
public bool IsRunning { get; set; }
public DateTime StartedAt { get; set; }
public Task StopAsync()
{
IsRunning = false;
return Task.CompletedTask;
}
}