// 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 nicht gefunden."); // ================================================================================= // FIX: E-Mail holen (Passend zu deiner Entity Customer.cs) // Customer hat keine Email, aber eine AspNetUserId. Wir holen die Mail aus der Users Tabelle. // ================================================================================= var userEmail = await _context.Users .Where(u => u.Id == userId) // userId entspricht customer.AspNetUserId .Select(u => u.Email) .FirstOrDefaultAsync(); if (string.IsNullOrEmpty(userEmail)) { return ServiceResult.Fail(ServiceResultType.Failure, "Keine E-Mail-Adresse für den Benutzer gefunden."); } // 1.2 Adressen Validieren 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.3 Methoden Validieren var shippingMethod = await _shippingMethodRepository.GetByIdAsync(orderDto.ShippingMethodId); if (shippingMethod == null || !shippingMethod.IsActive) return ServiceResult.Fail(ServiceResultType.InvalidInput, "Versandmethode ungültig."); var paymentMethod = await _context.PaymentMethods.FindAsync(orderDto.PaymentMethodId); if (paymentMethod == null || !paymentMethod.IsActive) return ServiceResult.Fail(ServiceResultType.InvalidInput, "Zahlungsmethode ungültig."); // 1.4 Produkte Validieren if (orderDto.Items == null || !orderDto.Items.Any()) return ServiceResult.Fail(ServiceResultType.InvalidInput, "Warenkorb ist leer."); var productIds = orderDto.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 itemDto in orderDto.Items) { var product = products.FirstOrDefault(p => p.Id == itemDto.ProductId); if (product == null) { validationErrors.AppendLine($"- Produkt {itemDto.ProductId} fehlt."); continue; } if (!product.IsActive) validationErrors.AppendLine($"- '{product.Name}' ist nicht verfügbar."); if (product.StockQuantity < itemDto.Quantity) validationErrors.AppendLine($"- '{product.Name}': Zu wenig Bestand."); if (validationErrors.Length == 0) { totalOrderWeight += (decimal)(product.Weight ?? 0) * itemDto.Quantity; var orderItem = new OrderItem { ProductId = product.Id, ProductVariantId = itemDto.ProductVariantId, ProductName = product.Name, ProductSKU = product.SKU, Quantity = itemDto.Quantity, UnitPrice = product.Price, TotalPrice = product.Price * itemDto.Quantity }; preparedOrderItems.Add(orderItem); itemsTotal += orderItem.TotalPrice; } } if (validationErrors.Length > 0) { await transaction.RollbackAsync(); return ServiceResult.Fail(ServiceResultType.Conflict, $"Fehler:\n{validationErrors}"); } if (totalOrderWeight < shippingMethod.MinWeight || totalOrderWeight > shippingMethod.MaxWeight) { await transaction.RollbackAsync(); return ServiceResult.Fail(ServiceResultType.InvalidInput, $"Gesamtgewicht ({totalOrderWeight} kg) passt nicht zur Versandart."); } // 2. Preise & Rabatte decimal discountAmount = 0; var discountResult = new DiscountCalculationResult(); 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; } decimal shippingCost = shippingMethod.BaseCost; decimal subTotalBeforeDiscount = itemsTotal + shippingCost; if (discountAmount > subTotalBeforeDiscount) discountAmount = subTotalBeforeDiscount; decimal subTotalAfterDiscount = subTotalBeforeDiscount - discountAmount; decimal taxRate = await _settingService.GetSettingValueAsync("GlobalTaxRate", 0.19m); decimal taxAmount = subTotalAfterDiscount * taxRate; // Steuer auf reduzierten Betrag decimal orderTotal = subTotalAfterDiscount + taxAmount; // 3. Ausführung (Execution) foreach (var itemDto in orderDto.Items) { var product = products.First(p => p.Id == itemDto.ProductId); if (product.StockQuantity < itemDto.Quantity) { await transaction.RollbackAsync(); return ServiceResult.Fail(ServiceResultType.Conflict, "Lagerbestand hat sich während des Vorgangs geändert."); } product.StockQuantity -= itemDto.Quantity; } 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); if (discountResult.AppliedDiscountIds.Any()) { var appliedDiscounts = await _context.Discounts .Where(d => discountResult.AppliedDiscountIds.Contains(d.Id)) .ToListAsync(); foreach (var d in appliedDiscounts) d.CurrentUsageCount++; } try { await _context.SaveChangesAsync(); await transaction.CommitAsync(); } catch (DbUpdateConcurrencyException ex) { await transaction.RollbackAsync(); _logger.LogWarning(ex, "Concurrency Konflikt bei Bestellung."); return ServiceResult.Fail(ServiceResultType.Conflict, "Artikel wurden soeben von einem anderen Kunden gekauft."); } // 4. Post-Processing (Warenkorb & Email) try { await _cartService.ClearCartAsync(userId!); } catch (Exception ex) { _logger.LogError(ex, "Warenkorb konnte nicht geleert werden."); } try { await _emailService.SendOrderConfirmationAsync(newOrder.Id, userEmail); } catch (Exception ex) { _logger.LogError(ex, "Bestätigungs-Email fehlgeschlagen."); } // Manuelles Setzen der Navigation Properties für das Mapping 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, "Checkout Exception"); 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() }; } } }