using Microsoft.EntityFrameworkCore; using LittleShop.Data; using LittleShop.Models; using LittleShop.DTOs; using LittleShop.Enums; namespace LittleShop.Services; public class OrderService : IOrderService { private readonly LittleShopContext _context; private readonly ILogger _logger; private readonly ICustomerService _customerService; private readonly IPushNotificationService _pushNotificationService; private readonly ITeleBotMessagingService _teleBotMessagingService; public OrderService(LittleShopContext context, ILogger logger, ICustomerService customerService, IPushNotificationService pushNotificationService, ITeleBotMessagingService teleBotMessagingService) { _context = context; _logger = logger; _customerService = customerService; _pushNotificationService = pushNotificationService; _teleBotMessagingService = teleBotMessagingService; } public async Task> GetAllOrdersAsync() { var orders = await _context.Orders .Include(o => o.Customer) .Include(o => o.Items) .ThenInclude(oi => oi.Product) .Include(o => o.Items) .ThenInclude(oi => oi.ProductMultiBuy) .Include(o => o.Payments) .OrderByDescending(o => o.CreatedAt) .ToListAsync(); return orders.Select(MapToDto); } public async Task> GetOrdersByIdentityAsync(string identityReference) { var orders = await _context.Orders .Include(o => o.Customer) .Include(o => o.Items) .ThenInclude(oi => oi.Product) .Include(o => o.Items) .ThenInclude(oi => oi.ProductMultiBuy) .Include(o => o.Payments) .Where(o => o.IdentityReference == identityReference) .OrderByDescending(o => o.CreatedAt) .ToListAsync(); return orders.Select(MapToDto); } public async Task> GetOrdersByCustomerIdAsync(Guid customerId) { var orders = await _context.Orders .Include(o => o.Customer) .Include(o => o.Items) .ThenInclude(oi => oi.Product) .Include(o => o.Items) .ThenInclude(oi => oi.ProductMultiBuy) .Include(o => o.Payments) .Where(o => o.CustomerId == customerId) .OrderByDescending(o => o.CreatedAt) .ToListAsync(); return orders.Select(MapToDto); } public async Task GetOrderByIdAsync(Guid id) { var order = await _context.Orders .Include(o => o.Customer) .Include(o => o.Items) .ThenInclude(oi => oi.Product) .Include(o => o.Items) .ThenInclude(oi => oi.ProductMultiBuy) .Include(o => o.Payments) .FirstOrDefaultAsync(o => o.Id == id); return order == null ? null : MapToDto(order); } public async Task CreateOrderAsync(CreateOrderDto createOrderDto) { using var transaction = await _context.Database.BeginTransactionAsync(); 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(), CustomerId = customerId, IdentityReference = identityReference, Status = OrderStatus.PendingPayment, TotalAmount = 0, Currency = "GBP", ShippingName = createOrderDto.ShippingName, ShippingAddress = createOrderDto.ShippingAddress, ShippingCity = createOrderDto.ShippingCity, ShippingPostCode = createOrderDto.ShippingPostCode, ShippingCountry = createOrderDto.ShippingCountry, Notes = createOrderDto.Notes, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; _context.Orders.Add(order); decimal totalAmount = 0; foreach (var itemDto in createOrderDto.Items) { var product = await _context.Products.FindAsync(itemDto.ProductId); if (product == null || !product.IsActive) { throw new ArgumentException($"Product {itemDto.ProductId} not found or inactive"); } ProductMultiBuy? multiBuy = null; decimal unitPrice = product.Price; if (itemDto.ProductMultiBuyId.HasValue) { multiBuy = await _context.ProductMultiBuys.FindAsync(itemDto.ProductMultiBuyId.Value); if (multiBuy == null || !multiBuy.IsActive || multiBuy.ProductId != itemDto.ProductId) { throw new ArgumentException($"Product multi-buy {itemDto.ProductMultiBuyId} not found, inactive, or doesn't belong to product {itemDto.ProductId}"); } // When using a multi-buy, the quantity represents how many of that multi-buy bundle // For example: buying 2 of the "3 for Β£25" multi-buy means 6 total items for Β£50 unitPrice = multiBuy.Price; } var orderItem = new OrderItem { Id = Guid.NewGuid(), OrderId = order.Id, ProductId = itemDto.ProductId, ProductMultiBuyId = itemDto.ProductMultiBuyId, Quantity = itemDto.Quantity, UnitPrice = unitPrice, TotalPrice = unitPrice * itemDto.Quantity }; _context.OrderItems.Add(orderItem); totalAmount += orderItem.TotalPrice; } order.TotalAmount = totalAmount; await _context.SaveChangesAsync(); await transaction.CommitAsync(); 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); } // Send notification about new order to admin users await SendNewOrderNotification(order); // Reload order with includes var createdOrder = await GetOrderByIdAsync(order.Id); return createdOrder!; } catch { await transaction.RollbackAsync(); throw; } } public async Task UpdateOrderStatusAsync(Guid id, UpdateOrderStatusDto updateOrderStatusDto) { var order = await _context.Orders .Include(o => o.Customer) .FirstOrDefaultAsync(o => o.Id == id); if (order == null) return false; var previousStatus = order.Status; order.Status = updateOrderStatusDto.Status; if (!string.IsNullOrEmpty(updateOrderStatusDto.TrackingNumber)) { order.TrackingNumber = updateOrderStatusDto.TrackingNumber; } if (!string.IsNullOrEmpty(updateOrderStatusDto.Notes)) { order.Notes = updateOrderStatusDto.Notes; } if (updateOrderStatusDto.Status == OrderStatus.Shipped && order.ShippedAt == null) { order.ShippedAt = DateTime.UtcNow; } order.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Updated order {OrderId} status from {PreviousStatus} to {NewStatus}", id, previousStatus, updateOrderStatusDto.Status); // Send push notifications for status changes await SendOrderStatusNotification(order, previousStatus, updateOrderStatusDto.Status); return true; } public async Task CancelOrderAsync(Guid id, string identityReference) { var order = await _context.Orders.FindAsync(id); if (order == null || order.IdentityReference != identityReference) return false; if (order.Status != OrderStatus.PendingPayment) { return false; // Can only cancel pending orders } order.Status = OrderStatus.Cancelled; order.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Cancelled order {OrderId} by identity {Identity}", id, identityReference); return true; } private static OrderDto MapToDto(Order order) { 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, ShippingAddress = order.ShippingAddress, ShippingCity = order.ShippingCity, ShippingPostCode = order.ShippingPostCode, ShippingCountry = order.ShippingCountry, Notes = order.Notes, TrackingNumber = order.TrackingNumber, CreatedAt = order.CreatedAt, UpdatedAt = order.UpdatedAt, PaidAt = order.PaidAt, // Workflow timestamps AcceptedAt = order.AcceptedAt, PackingStartedAt = order.PackingStartedAt, DispatchedAt = order.DispatchedAt, ExpectedDeliveryDate = order.ExpectedDeliveryDate, ActualDeliveryDate = order.ActualDeliveryDate, OnHoldAt = order.OnHoldAt, // Workflow details AcceptedByUser = order.AcceptedByUser, PackedByUser = order.PackedByUser, DispatchedByUser = order.DispatchedByUser, OnHoldReason = order.OnHoldReason, // Legacy field (for backward compatibility) ShippedAt = order.ShippedAt, Items = order.Items.Select(oi => new OrderItemDto { Id = oi.Id, ProductId = oi.ProductId, ProductMultiBuyId = oi.ProductMultiBuyId, ProductName = oi.Product.Name, ProductMultiBuyName = oi.ProductMultiBuy?.Name, Quantity = oi.Quantity, UnitPrice = oi.UnitPrice, TotalPrice = oi.TotalPrice }).ToList(), Payments = order.Payments.Select(cp => new CryptoPaymentDto { Id = cp.Id, OrderId = cp.OrderId, Currency = cp.Currency, WalletAddress = cp.WalletAddress, RequiredAmount = cp.RequiredAmount, PaidAmount = cp.PaidAmount, Status = cp.Status, BTCPayInvoiceId = cp.BTCPayInvoiceId, TransactionHash = cp.TransactionHash, CreatedAt = cp.CreatedAt, PaidAt = cp.PaidAt, ExpiresAt = cp.ExpiresAt }).ToList() }; } // Enhanced workflow methods public async Task AcceptOrderAsync(Guid id, string userName, AcceptOrderDto acceptDto) { var order = await _context.Orders .Include(o => o.Customer) .FirstOrDefaultAsync(o => o.Id == id); if (order == null || order.Status != OrderStatus.PaymentReceived) return false; var previousStatus = order.Status; order.Status = OrderStatus.Accepted; order.AcceptedAt = DateTime.UtcNow; order.AcceptedByUser = userName; if (!string.IsNullOrEmpty(acceptDto.Notes)) order.Notes = acceptDto.Notes; order.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Order {OrderId} accepted by {User}", id, userName); // Send push notifications await SendOrderStatusNotification(order, previousStatus, OrderStatus.Accepted); return true; } public async Task StartPackingAsync(Guid id, string userName, StartPackingDto packingDto) { var order = await _context.Orders .Include(o => o.Customer) .FirstOrDefaultAsync(o => o.Id == id); if (order == null || order.Status != OrderStatus.Accepted) return false; var previousStatus = order.Status; order.Status = OrderStatus.Packing; order.PackingStartedAt = DateTime.UtcNow; order.PackedByUser = userName; if (!string.IsNullOrEmpty(packingDto.Notes)) order.Notes = packingDto.Notes; order.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Order {OrderId} packing started by {User}", id, userName); // Send push notifications await SendOrderStatusNotification(order, previousStatus, OrderStatus.Packing); return true; } public async Task DispatchOrderAsync(Guid id, string userName, DispatchOrderDto dispatchDto) { var order = await _context.Orders .Include(o => o.Customer) .FirstOrDefaultAsync(o => o.Id == id); if (order == null || order.Status != OrderStatus.Packing) return false; order.Status = OrderStatus.Dispatched; order.DispatchedAt = DateTime.UtcNow; order.DispatchedByUser = userName; order.TrackingNumber = dispatchDto.TrackingNumber; // Calculate expected delivery date (working days only) var expectedDate = DateTime.UtcNow.AddDays(dispatchDto.EstimatedDeliveryDays); while (expectedDate.DayOfWeek == DayOfWeek.Saturday || expectedDate.DayOfWeek == DayOfWeek.Sunday) { expectedDate = expectedDate.AddDays(1); } order.ExpectedDeliveryDate = expectedDate; if (!string.IsNullOrEmpty(dispatchDto.Notes)) order.Notes = dispatchDto.Notes; order.UpdatedAt = DateTime.UtcNow; // Set legacy field for backward compatibility order.ShippedAt = order.DispatchedAt; await _context.SaveChangesAsync(); _logger.LogInformation("Order {OrderId} dispatched by {User} with tracking {TrackingNumber}", id, userName, dispatchDto.TrackingNumber); // Send push notifications await SendOrderStatusNotification(order, OrderStatus.Packing, OrderStatus.Dispatched); return true; } public async Task PutOnHoldAsync(Guid id, string userName, PutOnHoldDto holdDto) { var order = await _context.Orders.FindAsync(id); if (order == null || order.Status == OrderStatus.OnHold) return false; order.Status = OrderStatus.OnHold; order.OnHoldAt = DateTime.UtcNow; order.OnHoldReason = holdDto.Reason; if (!string.IsNullOrEmpty(holdDto.Notes)) order.Notes = holdDto.Notes; order.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Order {OrderId} put on hold by {User}: {Reason}", id, userName, holdDto.Reason); return true; } public async Task RemoveFromHoldAsync(Guid id, string userName) { var order = await _context.Orders.FindAsync(id); if (order == null || order.Status != OrderStatus.OnHold) return false; // Return to appropriate status based on workflow progress if (order.AcceptedAt.HasValue && order.PackingStartedAt.HasValue) order.Status = OrderStatus.Packing; else if (order.AcceptedAt.HasValue) order.Status = OrderStatus.Accepted; else order.Status = OrderStatus.PaymentReceived; order.OnHoldAt = null; order.OnHoldReason = null; order.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Order {OrderId} removed from hold by {User}, returned to {Status}", id, userName, order.Status); return true; } public async Task MarkDeliveredAsync(Guid id, MarkDeliveredDto deliveredDto) { var order = await _context.Orders.FindAsync(id); if (order == null || order.Status != OrderStatus.Dispatched) return false; order.Status = OrderStatus.Delivered; order.ActualDeliveryDate = deliveredDto.ActualDeliveryDate ?? DateTime.UtcNow; if (!string.IsNullOrEmpty(deliveredDto.Notes)) order.Notes = deliveredDto.Notes; order.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Order {OrderId} marked as delivered", id); return true; } // Workflow queries public async Task> GetOrdersByStatusAsync(OrderStatus status) { var orders = await _context.Orders .Include(o => o.Customer) .Include(o => o.Items) .ThenInclude(oi => oi.Product) .Include(o => o.Items) .ThenInclude(oi => oi.ProductMultiBuy) .Include(o => o.Payments) .Where(o => o.Status == status) .OrderByDescending(o => o.CreatedAt) .ToListAsync(); return orders.Select(MapToDto); } public async Task> GetOrdersRequiringActionAsync() { var orders = await _context.Orders .Include(o => o.Customer) .Include(o => o.Items) .ThenInclude(oi => oi.Product) .Include(o => o.Items) .ThenInclude(oi => oi.ProductMultiBuy) .Include(o => o.Payments) .Where(o => o.Status == OrderStatus.PendingPayment || o.Status == OrderStatus.PaymentReceived) .OrderByDescending(o => o.CreatedAt) .ToListAsync(); return orders.Select(MapToDto); } public async Task> GetOrdersForPackingAsync() { return await GetOrdersByStatusAsync(OrderStatus.Accepted); } public async Task> GetOrdersOnHoldAsync() { return await GetOrdersByStatusAsync(OrderStatus.OnHold); } private async Task SendNewOrderNotification(Order order) { try { var title = "πŸ›’ New Order Received"; var body = $"Order #{order.Id.ToString()[..8]} created for Β£{order.TotalAmount:F2}. Awaiting payment."; // Send notification to all admin users about new order await _pushNotificationService.SendOrderNotificationAsync(order.Id, title, body); _logger.LogInformation("Sent new order notification for order {OrderId}", order.Id); } catch (Exception ex) { _logger.LogError(ex, "Failed to send new order notification for order {OrderId}", order.Id); } } private async Task SendOrderStatusNotification(Order order, OrderStatus previousStatus, OrderStatus newStatus) { try { var title = GetOrderStatusNotificationTitle(newStatus); var body = GetOrderStatusNotificationBody(order, previousStatus, newStatus); // Send notification to admin users about order status change await _pushNotificationService.SendOrderNotificationAsync(order.Id, title, body); // Send TeleBot message to customer (if customer exists) if (order.Customer != null) { await _teleBotMessagingService.SendOrderStatusUpdateAsync(order.Id, newStatus); } _logger.LogInformation("Sent order status notifications for order {OrderId}: {Status} (Admin + Customer)", order.Id, newStatus); } catch (Exception ex) { _logger.LogError(ex, "Failed to send order status notification for order {OrderId}", order.Id); } } private static string GetOrderStatusNotificationTitle(OrderStatus status) { return status switch { OrderStatus.PaymentReceived => "πŸ’° Payment Confirmed", OrderStatus.Accepted => "βœ… Order Accepted", OrderStatus.Packing => "πŸ“¦ Being Packed", OrderStatus.Dispatched => "🚚 Order Dispatched", OrderStatus.Delivered => "πŸŽ‰ Order Delivered", OrderStatus.OnHold => "⏸️ Order On Hold", OrderStatus.Cancelled => "❌ Order Cancelled", OrderStatus.Refunded => "πŸ’Έ Order Refunded", _ => "πŸ“‹ Order Updated" }; } private static string GetOrderStatusNotificationBody(Order order, OrderStatus previousStatus, OrderStatus newStatus) { var orderId = order.Id.ToString()[..8]; var amount = order.TotalAmount.ToString("F2"); return newStatus switch { OrderStatus.PaymentReceived => $"Order #{orderId} payment confirmed (Β£{amount}). Ready for acceptance.", OrderStatus.Accepted => $"Order #{orderId} has been accepted and is ready for packing.", OrderStatus.Packing => $"Order #{orderId} is being packed. Will be dispatched soon.", OrderStatus.Dispatched => $"Order #{orderId} dispatched with tracking: {order.TrackingNumber ?? "TBA"}", OrderStatus.Delivered => $"Order #{orderId} has been delivered successfully.", OrderStatus.OnHold => $"Order #{orderId} has been put on hold: {order.OnHoldReason}", OrderStatus.Cancelled => $"Order #{orderId} has been cancelled.", OrderStatus.Refunded => $"Order #{orderId} has been refunded (Β£{amount}).", _ => $"Order #{orderId} status updated from {previousStatus} to {newStatus}." }; } }