using eprintServer.Data; using eprintServer.Entities; using eprintServer.Requests; using eprintServer.Responses; using eprintServer.Enums; using Microsoft.EntityFrameworkCore; namespace eprintServer.Services; public class OrderService { private readonly DataContext _context; public OrderService(DataContext context) { _context = context; } // ============================================================================ // CORRECTED CreateOrderAsync - With Explicit EF Core Tracking // ============================================================================ public async Task CreateOrderAsync(OrderRequest request) { var order = new Order { UserId = request.UserId, Discount = request.Discount, OrderDate = DateTime.UtcNow, Status = OrderStatus.PENDING, Timestamp = DateTime.UtcNow }; decimal subtotal = 0; var orderItems = new List(); // 1. Process Sellable Products if (request.SellableProducts?.Any() == true) { foreach (var item in request.SellableProducts) { var p = await _context.SellableProducts.FindAsync(item.Id); if (p == null) continue; var orderItem = new OrderItem { SellableProductId = item.Id, Quantity = item.Quantity, UnitPrice = (decimal)p.Price }; orderItems.Add(orderItem); subtotal += orderItem.UnitPrice * orderItem.Quantity; } } // 2. Process User Customizables (The fix for your specific payload) if (request.UserCustomizables?.Any() == true) { foreach (var ucReq in request.UserCustomizables) { if (Guid.TryParse(ucReq.Id, out Guid parsedGuid)) { // Fetch the customization AND the linked product to get the price var userCustomization = await _context.UserCustomizations .Include(uc => uc.Product) .FirstOrDefaultAsync(uc => uc.Id == parsedGuid); if (userCustomization != null) { var orderItem = new OrderItem { UserCustomizationId = userCustomization.Id, CustomizableProductId = userCustomization.ProductId, Quantity = ucReq.Quantity, // Use Product BasePrice or a TotalPrice calculated during customization UnitPrice = userCustomization.Product?.BasePrice ?? 0 }; orderItems.Add(orderItem); subtotal += orderItem.UnitPrice * orderItem.Quantity; } } } } // 3. Process Standard Customizable Products (without pre-existing customization) if (request.CustomizableProducts?.Any() == true) { foreach (var cpReq in request.CustomizableProducts) { var cp = await _context.CustomizableProducts.FindAsync(cpReq.Id); if (cp == null) continue; var orderItem = new OrderItem { CustomizableProductId = cp.Id, Quantity = cpReq.Quantity, UnitPrice = cp.BasePrice }; orderItems.Add(orderItem); subtotal += orderItem.UnitPrice * orderItem.Quantity; } } // 4. Process Offers if (request.OfferIds?.Any() == true) { foreach (var id in request.OfferIds) { var offer = await _context.Offers.FindAsync((int)id); if (offer == null) continue; var orderItem = new OrderItem { OfferId = id, Quantity = 1, UnitPrice = offer.OfferPrice.Value }; orderItems.Add(orderItem); subtotal += orderItem.UnitPrice; } } // Finalize Order order.TotalAmount = subtotal - request.Discount; order.OrderItems = orderItems; _context.Orders.Add(order); // This saves both Order and OrderItems because they are linked in the object graph await _context.SaveChangesAsync(); // Reload with all Includes for the response return await GetByIdAsync(order.Id) ?? order; } public async Task UpdateOrderAsync(long id, OrderUpdateRequest request) { var order = await _context.Orders.Include(o => o.OrderItems).FirstOrDefaultAsync(o => o.Id == id); if (order == null) return null; if (request.Discount.HasValue) order.Discount = request.Discount.Value; if (request.Status.HasValue) order.Status = request.Status.Value; // Logic: If any list is provided, clear and refill bool hasSellable = request.SellableProductIds != null && request.SellableProductIds.Any(); bool hasCustom = request.CustomizableProductIds != null && request.CustomizableProductIds.Any(); bool hasOffers = request.OfferIds != null && request.OfferIds.Any(); if (hasSellable || hasCustom || hasOffers) { _context.OrderItems.RemoveRange(order.OrderItems); decimal newSubtotal = 0; if (request.SellableProductIds != null) { foreach (var sid in request.SellableProductIds) { var p = await _context.SellableProducts.FindAsync(sid); if (p != null) { decimal price = (decimal)p.Price; order.OrderItems.Add(new OrderItem { SellableProductId = sid, Quantity = 1, UnitPrice = price }); newSubtotal += price; } } } if (request.CustomizableProductIds != null) { foreach (var cid in request.CustomizableProductIds) { var cp = await _context.CustomizableProducts.FindAsync(cid); if (cp != null) { order.OrderItems.Add(new OrderItem { CustomizableProductId = cid, Quantity = 1, UnitPrice = cp.BasePrice }); newSubtotal += cp.BasePrice; } } } if (request.OfferIds != null) { foreach (var oid in request.OfferIds) { var o = await _context.Offers.FindAsync((int)oid); decimal price = o?.OfferPrice ?? 0; order.OrderItems.Add(new OrderItem { OfferId = oid, Quantity = 1, UnitPrice = price }); newSubtotal += price; } } order.TotalAmount = newSubtotal - order.Discount; } await _context.SaveChangesAsync(); return order; } // New Specific Status Update Method public async Task UpdateStatusAsync(long id, OrderStatus status) { var order = await _context.Orders.FindAsync(id); if (order == null) return false; order.Status = status; await _context.SaveChangesAsync(); return true; } public async Task GetByIdAsync(long id) { return await _context.Orders .AsNoTracking() // Better for read-only scenarios .Where(o => o.Id == id) .Include(o => o.User) .Include(o => o.UserAddress) .Include(o => o.OrderItems) .ThenInclude(oi => oi.SellableProduct) .Include(o => o.OrderItems) .ThenInclude(oi => oi.CustomizableProduct) .Include(o => o.OrderItems) .ThenInclude(oi => oi.UserCustomization) .ThenInclude(uc => uc.Steps) .ThenInclude(s => s.FieldValues) .Include(o => o.OrderItems) .ThenInclude(oi => oi.UserCustomization) .ThenInclude(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .ThenInclude(um => um.File) .Include(o => o.OrderItems) .ThenInclude(oi => oi.UserCustomization) .ThenInclude(uc => uc.Steps) .ThenInclude(s => s.UserMockup) .ThenInclude(um => um.Thumbnail) .FirstOrDefaultAsync(); } public async Task> GetPagedOrdersAsync(int page, int size, long? userId, OrderStatus? status) { var query = _context.Orders .Include(o => o.User) .Include(o => o.UserAddress) .Include(o => o.OrderItems) .ThenInclude(oi => oi.SellableProduct) .Include(o => o.OrderItems) .ThenInclude(oi => oi.CustomizableProduct) .AsQueryable(); if (userId.HasValue) query = query.Where(o => o.UserId == userId.Value); if (status.HasValue) query = query.Where(o => o.Status == status.Value); int totalCount = await query.CountAsync(); var items = await query.OrderByDescending(o => o.OrderDate) .Skip(page * size) .Take(size) .ToListAsync(); return new PagedResult(items, totalCount, page, size); } public async Task DeleteOrderAsync(long id) { var order = await _context.Orders.FindAsync(id); if (order == null) return false; _context.Orders.Remove(order); await _context.SaveChangesAsync(); return true; } public async Task UpdateOrderAddressAsync(long orderId, UpdateOrderAddressRequest request) { // Include UserAddress to modify it if it exists var order = await _context.Orders .Include(o => o.UserAddress) .FirstOrDefaultAsync(o => o.Id == orderId); if (order == null) return null; if (order.UserAddress == null) { order.UserAddress = new UserAddress(); _context.UserAddresses.Add(order.UserAddress); } // Only update fields provided in the request if (!string.IsNullOrEmpty(request.AddressLine1)) order.UserAddress.AddressLine1 = request.AddressLine1; if (!string.IsNullOrEmpty(request.City)) order.UserAddress.City = request.City; if (!string.IsNullOrEmpty(request.Area)) order.UserAddress.Area = request.Area; if (!string.IsNullOrEmpty(request.Region)) order.UserAddress.Region = request.Region; if (!string.IsNullOrEmpty(request.Country)) order.UserAddress.Country = request.Country; if (!string.IsNullOrEmpty(request.ZipCode)) order.UserAddress.ZipCode = request.ZipCode; await _context.SaveChangesAsync(); return order; } }