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; public OrderService(LittleShopContext context, ILogger logger, ICustomerService customerService) { _context = context; _logger = logger; _customerService = customerService; } 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.ProductVariation) .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.ProductVariation) .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.ProductVariation) .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.ProductVariation) .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"); } ProductVariation? variation = null; decimal unitPrice = product.Price; if (itemDto.ProductVariationId.HasValue) { variation = await _context.ProductVariations.FindAsync(itemDto.ProductVariationId.Value); if (variation == null || !variation.IsActive || variation.ProductId != itemDto.ProductId) { throw new ArgumentException($"Product variation {itemDto.ProductVariationId} not found, inactive, or doesn't belong to product {itemDto.ProductId}"); } // When using a variation, the quantity represents how many of that variation bundle // For example: buying 2 of the "3 for £25" variation means 6 total items for £50 unitPrice = variation.Price; } var orderItem = new OrderItem { Id = Guid.NewGuid(), OrderId = order.Id, ProductId = itemDto.ProductId, ProductVariationId = itemDto.ProductVariationId, 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); } // 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.FindAsync(id); if (order == null) return false; 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 to {Status}", id, 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, ProductVariationId = oi.ProductVariationId, ProductName = oi.Product.Name, ProductVariationName = oi.ProductVariation?.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.FindAsync(id); if (order == null || order.Status != OrderStatus.PaymentReceived) return false; 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); return true; } public async Task StartPackingAsync(Guid id, string userName, StartPackingDto packingDto) { var order = await _context.Orders.FindAsync(id); if (order == null || order.Status != OrderStatus.Accepted) return false; 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); return true; } public async Task DispatchOrderAsync(Guid id, string userName, DispatchOrderDto dispatchDto) { var order = await _context.Orders.FindAsync(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); 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.ProductVariation) .Include(o => o.Payments) .Where(o => o.Status == status) .OrderByDescending(o => o.CreatedAt) .ToListAsync(); return orders.Select(MapToDto); } public async Task> GetOrdersRequiringActionAsync() { return await GetOrdersByStatusAsync(OrderStatus.PaymentReceived); } public async Task> GetOrdersForPackingAsync() { return await GetOrdersByStatusAsync(OrderStatus.Accepted); } public async Task> GetOrdersOnHoldAsync() { return await GetOrdersByStatusAsync(OrderStatus.OnHold); } }