// src/Webshop.Application/Services/Customers/CheckoutService.cs using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Webshop.Application; using Webshop.Application.DTOs.Customers; using Webshop.Application.DTOs.Orders; using Webshop.Application.DTOs.Shipping; using Webshop.Application.Services.Customers.Interfaces; using Webshop.Application.Services.Public.Interfaces; using Webshop.Application.Services.Public; // Für DiscountCalculationResult using Webshop.Domain.Entities; using Webshop.Domain.Enums; using Webshop.Domain.Interfaces; using Webshop.Infrastructure.Data; namespace Webshop.Application.Services.Customers { public class CheckoutService : ICheckoutService { private readonly ApplicationDbContext _context; private readonly IOrderRepository _orderRepository; private readonly ICustomerRepository _customerRepository; private readonly IShippingMethodRepository _shippingMethodRepository; private readonly ISettingService _settingService; private readonly IDiscountService _discountService; private readonly ICartService _cartService; private readonly IEmailService _emailService; private readonly ILogger _logger; public CheckoutService( ApplicationDbContext context, IOrderRepository orderRepository, ICustomerRepository customerRepository, IShippingMethodRepository shippingMethodRepository, ISettingService settingService, IDiscountService discountService, ICartService cartService, IEmailService emailService, ILogger logger) { _context = context; _orderRepository = orderRepository; _customerRepository = customerRepository; _shippingMethodRepository = shippingMethodRepository; _settingService = settingService; _discountService = discountService; _cartService = cartService; _emailService = emailService; _logger = logger; } // ================================================================================= // FIX: Überladung für CartItemDto (Löst den Konvertierungsfehler im Controller) // ================================================================================= public async Task>> GetCompatibleShippingMethodsAsync(List items) { // Mappe CartItemDto zu CreateOrderItemDto, damit wir die Logik wiederverwenden können var mappedItems = items.Select(x => new CreateOrderItemDto { ProductId = x.ProductId, Quantity = x.Quantity, ProductVariantId = x.ProductVariantId }); return await GetCompatibleShippingMethodsAsync(mappedItems); } // Die Hauptlogik basierend auf CreateOrderItemDto (für den Checkout) public async Task>> GetCompatibleShippingMethodsAsync(IEnumerable items) { if (items == null || !items.Any()) return ServiceResult.Ok>(new List()); var productIds = items.Select(i => i.ProductId).ToList(); var products = await _context.Products.Where(p => productIds.Contains(p.Id)).ToListAsync(); decimal totalWeight = 0; foreach (var item in items) { var product = products.FirstOrDefault(p => p.Id == item.ProductId); if (product != null) { totalWeight += (decimal)(product.Weight ?? 0) * item.Quantity; } } var allMethods = await _shippingMethodRepository.GetAllAsync(); var compatibleMethods = allMethods .Where(sm => sm.IsActive && totalWeight >= sm.MinWeight && totalWeight <= sm.MaxWeight) .Select(sm => new ShippingMethodDto { Id = sm.Id, Name = sm.Name, Description = sm.Description, Cost = sm.BaseCost, IsActive = sm.IsActive, MinDeliveryDays = sm.MinDeliveryDays, MaxDeliveryDays = sm.MaxDeliveryDays, MinWeight = sm.MinWeight, MaxWeight = sm.MaxWeight }) .ToList(); return ServiceResult.Ok>(compatibleMethods); } public async Task> CreateOrderAsync(CreateOrderDto orderDto, string? userId) { await using var transaction = await _context.Database.BeginTransactionAsync(); try { // 1.1 Kunde Validieren var customer = await _customerRepository.GetByUserIdAsync(userId!); if (customer == null) return ServiceResult.Fail(ServiceResultType.Unauthorized, "Kundenprofil fehlt."); // E-Mail Logik var userEmail = await _context.Users .Where(u => u.Id == userId) .Select(u => u.Email) .FirstOrDefaultAsync(); if (string.IsNullOrEmpty(userEmail)) return ServiceResult.Fail(ServiceResultType.Failure, "Keine E-Mail gefunden."); // ================================================================================= // 1.2 Warenkorb aus der Datenbank laden (Single Source of Truth) // ================================================================================= var cart = await _context.Carts .Include(c => c.Items) .FirstOrDefaultAsync(c => c.UserId == userId); if (cart == null || !cart.Items.Any()) { return ServiceResult.Fail(ServiceResultType.InvalidInput, "Ihr Warenkorb ist leer. Bitte fügen Sie Produkte hinzu."); } // 1.3 Adressen Validieren // Wir laden beide Adressen und stellen sicher, dass sie dem Kunden gehören var addresses = await _context.Addresses .Where(a => (a.Id == orderDto.ShippingAddressId || a.Id == orderDto.BillingAddressId) && a.CustomerId == customer.Id) .ToListAsync(); if (!addresses.Any(a => a.Id == orderDto.ShippingAddressId) || !addresses.Any(a => a.Id == orderDto.BillingAddressId)) { return ServiceResult.Fail(ServiceResultType.Forbidden, "Die angegebenen Adressen sind ungültig oder gehören nicht zu diesem Konto."); } // 1.4 Methoden Validieren var shippingMethod = await _shippingMethodRepository.GetByIdAsync(orderDto.ShippingMethodId); if (shippingMethod == null || !shippingMethod.IsActive) return ServiceResult.Fail(ServiceResultType.InvalidInput, "Versandart ungültig."); var paymentMethod = await _context.PaymentMethods.FindAsync(orderDto.PaymentMethodId); if (paymentMethod == null || !paymentMethod.IsActive) return ServiceResult.Fail(ServiceResultType.InvalidInput, "Zahlungsart ungültig."); // ================================================================================= // 1.5 Produkte Validieren & Vorbereiten (BASIEREND AUF CART ITEMS) // ================================================================================= var productIds = cart.Items.Select(i => i.ProductId).Distinct().ToList(); var products = await _context.Products.Where(p => productIds.Contains(p.Id)).ToListAsync(); var validationErrors = new StringBuilder(); decimal totalOrderWeight = 0; decimal itemsTotal = 0; var preparedOrderItems = new List(); foreach (var cartItem in cart.Items) { var product = products.FirstOrDefault(p => p.Id == cartItem.ProductId); if (product == null) { validationErrors.AppendLine($"- Ein Produkt im Warenkorb (ID {cartItem.ProductId}) existiert nicht mehr."); continue; } if (!product.IsActive) { validationErrors.AppendLine($"- '{product.Name}' ist derzeit nicht verfügbar."); } // Prüfung Menge if (product.StockQuantity < cartItem.Quantity) { validationErrors.AppendLine($"- '{product.Name}': Nicht genügend Bestand (Verfügbar: {product.StockQuantity}, im Korb: {cartItem.Quantity})."); } if (validationErrors.Length == 0) { // Gewicht berechnen (Fallback auf 0 falls null) totalOrderWeight += (decimal)(product.Weight ?? 0) * cartItem.Quantity; var orderItem = new OrderItem { ProductId = product.Id, ProductVariantId = cartItem.ProductVariantId, ProductName = product.Name, ProductSKU = product.SKU, Quantity = cartItem.Quantity, UnitPrice = product.Price, // Preis IMMER frisch aus der DB nehmen! TotalPrice = product.Price * cartItem.Quantity }; preparedOrderItems.Add(orderItem); itemsTotal += orderItem.TotalPrice; } } // Abbruch bei Produktfehlern if (validationErrors.Length > 0) { await transaction.RollbackAsync(); return ServiceResult.Fail(ServiceResultType.Conflict, $"Bestellung nicht möglich:\n{validationErrors}"); } // 1.6 Gewichtsprüfung if (totalOrderWeight < shippingMethod.MinWeight || totalOrderWeight > shippingMethod.MaxWeight) { await transaction.RollbackAsync(); return ServiceResult.Fail(ServiceResultType.InvalidInput, $"Gesamtgewicht ({totalOrderWeight} kg) passt nicht zur Versandart '{shippingMethod.Name}'."); } // ================================================================================= // PHASE 2: BERECHNUNG (Finanzen) // ================================================================================= decimal discountAmount = 0; var discountResult = new DiscountCalculationResult(); // Rabatt berechnen if (!string.IsNullOrWhiteSpace(orderDto.CouponCode)) { discountResult = await _discountService.CalculateDiscountAsync(preparedOrderItems, orderDto.CouponCode); if (!discountResult.IsValid) { await transaction.RollbackAsync(); return ServiceResult.Fail(ServiceResultType.InvalidInput, $"Gutscheinfehler: {discountResult.ErrorMessage ?? "Code ungültig."}"); } discountAmount = discountResult.TotalDiscountAmount; } // Gesamtsummen berechnen decimal shippingCost = shippingMethod.BaseCost; decimal subTotalBeforeDiscount = itemsTotal + shippingCost; // Rabatt darf nicht höher als die Summe sein if (discountAmount > subTotalBeforeDiscount) { discountAmount = subTotalBeforeDiscount; } decimal subTotalAfterDiscount = subTotalBeforeDiscount - discountAmount; // Steuer berechnen (Settings abrufen) decimal taxRate = await _settingService.GetSettingValueAsync("GlobalTaxRate", 0.19m); decimal taxAmount = subTotalAfterDiscount * taxRate; decimal orderTotal = subTotalAfterDiscount + taxAmount; // ================================================================================= // PHASE 3: EXECUTION (Schreiben in DB) // ================================================================================= // 3.1 Lagerbestand reduzieren foreach (var oi in preparedOrderItems) { var product = products.First(p => p.Id == oi.ProductId); // Letzte Sicherheitsprüfung (Concurrency) if (product.StockQuantity < oi.Quantity) { await transaction.RollbackAsync(); return ServiceResult.Fail(ServiceResultType.Conflict, $"Lagerbestand für '{product.Name}' hat sich während des Vorgangs geändert."); } product.StockQuantity -= oi.Quantity; } // 3.2 Order erstellen var newOrder = new Order { CustomerId = customer.Id, OrderNumber = GenerateOrderNumber(), OrderDate = DateTimeOffset.UtcNow, OrderStatus = OrderStatus.Pending.ToString(), PaymentStatus = PaymentStatus.Pending.ToString(), OrderTotal = orderTotal, ShippingCost = shippingCost, TaxAmount = taxAmount, DiscountAmount = discountAmount, PaymentMethod = paymentMethod.Name, PaymentMethodId = paymentMethod.Id, ShippingMethodId = shippingMethod.Id, BillingAddressId = orderDto.BillingAddressId, ShippingAddressId = orderDto.ShippingAddressId, OrderItems = preparedOrderItems }; await _orderRepository.AddAsync(newOrder); // 3.3 Discount Nutzung erhöhen if (discountResult.AppliedDiscountIds.Any()) { var appliedDiscounts = await _context.Discounts .Where(d => discountResult.AppliedDiscountIds.Contains(d.Id)) .ToListAsync(); foreach (var d in appliedDiscounts) d.CurrentUsageCount++; } // 3.4 Speichern & Commit try { await _context.SaveChangesAsync(); await transaction.CommitAsync(); } catch (DbUpdateConcurrencyException ex) { await transaction.RollbackAsync(); _logger.LogWarning(ex, "Concurrency Fehler beim Checkout (User {UserId}).", userId); return ServiceResult.Fail(ServiceResultType.Conflict, "Ein oder mehrere Artikel wurden soeben von einem anderen Kunden gekauft."); } // ================================================================================= // PHASE 4: CLEANUP & POST-PROCESSING // ================================================================================= // 4.1 Warenkorb leeren try { await _cartService.ClearCartAsync(userId!); } catch (Exception ex) { _logger.LogError(ex, "Cart Clear Failed for Order {OrderNumber}", newOrder.OrderNumber); } // 4.2 Bestätigungs-Email senden try { await _emailService.SendOrderConfirmationAsync(newOrder.Id, userEmail); } catch (Exception ex) { _logger.LogError(ex, "Email Failed for Order {OrderNumber}", newOrder.OrderNumber); } // 4.3 Mapping für Rückgabe // Wir setzen die Navigation Properties manuell, um DB-Reload zu sparen newOrder.PaymentMethodInfo = paymentMethod; newOrder.ShippingAddress = addresses.First(a => a.Id == orderDto.ShippingAddressId); newOrder.BillingAddress = addresses.First(a => a.Id == orderDto.BillingAddressId); return ServiceResult.Ok(MapToOrderDetailDto(newOrder)); } catch (Exception ex) { await transaction.RollbackAsync(); _logger.LogError(ex, "Unerwarteter Fehler im Checkout für User {UserId}", userId); return ServiceResult.Fail(ServiceResultType.Failure, "Ein unerwarteter Fehler ist aufgetreten."); } } private string GenerateOrderNumber() { return $"WS-{DateTime.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString().Substring(0, 6).ToUpper()}"; } private OrderDetailDto MapToOrderDetailDto(Order order) { return new OrderDetailDto { Id = order.Id, OrderNumber = order.OrderNumber, OrderDate = order.OrderDate, CustomerId = order.CustomerId, Status = Enum.TryParse(order.OrderStatus, true, out var os) ? os : OrderStatus.Pending, TotalAmount = order.OrderTotal, ShippingAddress = new AddressDto { Id = order.ShippingAddress.Id, FirstName = order.ShippingAddress.FirstName, LastName = order.ShippingAddress.LastName, Street = order.ShippingAddress.Street, HouseNumber = order.ShippingAddress.HouseNumber, City = order.ShippingAddress.City, PostalCode = order.ShippingAddress.PostalCode, Country = order.ShippingAddress.Country, Type = order.ShippingAddress.Type }, BillingAddress = new AddressDto { Id = order.BillingAddress.Id, FirstName = order.BillingAddress.FirstName, LastName = order.BillingAddress.LastName, Street = order.BillingAddress.Street, HouseNumber = order.BillingAddress.HouseNumber, City = order.BillingAddress.City, PostalCode = order.BillingAddress.PostalCode, Country = order.BillingAddress.Country, Type = order.BillingAddress.Type }, PaymentMethod = order.PaymentMethodInfo?.Name ?? order.PaymentMethod, PaymentStatus = Enum.TryParse(order.PaymentStatus, true, out var ps) ? ps : PaymentStatus.Pending, OrderItems = order.OrderItems.Select(oi => new OrderItemDto { Id = oi.Id, ProductId = oi.ProductId, ProductVariantId = oi.ProductVariantId, ProductName = oi.ProductName, ProductSKU = oi.ProductSKU, Quantity = oi.Quantity, UnitPrice = oi.UnitPrice, TotalPrice = oi.TotalPrice }).ToList() }; } } }