Add customer communication system
This commit is contained in:
382
LittleShop/Services/BotMetricsService.cs
Normal file
382
LittleShop/Services/BotMetricsService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
330
LittleShop/Services/BotService.cs
Normal file
330
LittleShop/Services/BotService.cs
Normal 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)];
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
};
|
||||
}
|
||||
}
|
||||
233
LittleShop/Services/CustomerMessageService.cs
Normal file
233
LittleShop/Services/CustomerMessageService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
296
LittleShop/Services/CustomerService.cs
Normal file
296
LittleShop/Services/CustomerService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
24
LittleShop/Services/IBotMetricsService.cs
Normal file
24
LittleShop/Services/IBotMetricsService.cs
Normal 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);
|
||||
}
|
||||
25
LittleShop/Services/IBotService.cs
Normal file
25
LittleShop/Services/IBotService.cs
Normal 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);
|
||||
}
|
||||
20
LittleShop/Services/ICustomerMessageService.cs
Normal file
20
LittleShop/Services/ICustomerMessageService.cs
Normal 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);
|
||||
}
|
||||
19
LittleShop/Services/ICustomerService.cs
Normal file
19
LittleShop/Services/ICustomerService.cs
Normal 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);
|
||||
}
|
||||
15
LittleShop/Services/ITelegramBotManagerService.cs
Normal file
15
LittleShop/Services/ITelegramBotManagerService.cs
Normal 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();
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
207
LittleShop/Services/TelegramBotManagerService.cs
Normal file
207
LittleShop/Services/TelegramBotManagerService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user