littleshop/LittleShop/Services/CustomerMessageService.cs

247 lines
9.4 KiB
C#

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";
// Use customer-based threading - all messages for a customer are in one conversation
message.ThreadId = message.CustomerId;
_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 customerId)
{
// Get all messages for this customer (customer-based conversation)
var messages = await _context.CustomerMessages
.Include(m => m.Customer)
.Include(m => m.Order)
.Include(m => m.AdminUser)
.Where(m => m.CustomerId == customerId && !m.IsArchived)
.OrderBy(m => m.CreatedAt)
.ToListAsync();
if (!messages.Any()) return null;
var customer = messages.First().Customer;
var thread = new MessageThreadDto
{
ThreadId = customerId, // Use CustomerId as thread identifier
Subject = customer?.DisplayName ?? "Unknown Customer", // Customer name as subject
CustomerId = customerId,
CustomerName = customer?.DisplayName ?? "Unknown",
OrderId = null, // No single order - conversation spans multiple orders
OrderReference = null, // No single order reference
StartedAt = messages.Min(m => m.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()
{
// Group by Customer instead of ThreadId for customer-based conversations
var threads = await _context.CustomerMessages
.Include(m => m.Customer)
.Include(m => m.Order)
.Where(m => !m.IsArchived)
.GroupBy(m => m.CustomerId)
.Select(g => new MessageThreadDto
{
ThreadId = g.First().CustomerId, // Use CustomerId as conversation identifier
Subject = g.First().Customer != null ? g.First().Customer.DisplayName : "Unknown Customer",
CustomerId = g.First().CustomerId,
CustomerName = g.First().Customer != null ? g.First().Customer.DisplayName : "Unknown",
OrderId = null, // No single order - will show multiple orders in thread
OrderReference = null, // No single order reference
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);
}
public async Task<bool> CreateCustomerToAdminMessageAsync(CustomerMessage message)
{
try
{
// Use customer-based threading for all messages
message.ThreadId = message.CustomerId;
_context.CustomerMessages.Add(message);
await _context.SaveChangesAsync();
_logger.LogInformation("Created customer message {MessageId} from customer {CustomerId}",
message.Id, message.CustomerId);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating customer message from customer {CustomerId}", message.CustomerId);
return false;
}
}
}