direkt aus dem warenkorb holen
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 29s

This commit is contained in:
Webtree-design
2025-11-28 13:12:24 +01:00
parent 19366febb8
commit d48d83c87d
3 changed files with 144 additions and 69 deletions

View File

@@ -1,21 +1,27 @@
// src/Webshop.Application/DTOs/Orders/CreateOrderDto.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Webshop.Application.DTOs.Orders
{
public class CreateOrderDto
{
public Guid? CustomerId { get; set; } // Nullable für Gastbestellung
public string? GuestEmail { get; set; }
public string? GuestPhoneNumber { get; set; }
// Items Liste wurde ENTFERNT!
[Required]
public Guid ShippingAddressId { get; set; }
[Required]
public Guid BillingAddressId { get; set; }
[Required]
public Guid PaymentMethodId { get; set; }
[Required]
public Guid ShippingMethodId { get; set; }
public string? CouponCode { get; set; } // << NEU >>
public string? CouponCode { get; set; }
public List<CreateOrderItemDto> Items { get; set; } = new List<CreateOrderItemDto>();
// Optional: Falls du Gastbestellungen ohne vorherigen DB-Cart erlauben willst,
// bräuchtest du eine separate Logik. Wir gehen hier vom Standard User-Flow aus.
}
}

View File

@@ -117,46 +117,54 @@ namespace Webshop.Application.Services.Customers
// 1.1 Kunde Validieren
var customer = await _customerRepository.GetByUserIdAsync(userId!);
if (customer == null)
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Unauthorized, "Kundenprofil nicht gefunden.");
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Unauthorized, "Kundenprofil fehlt.");
// =================================================================================
// 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.
// =================================================================================
// E-Mail Logik
var userEmail = await _context.Users
.Where(u => u.Id == userId) // userId entspricht customer.AspNetUserId
.Where(u => u.Id == userId)
.Select(u => u.Email)
.FirstOrDefaultAsync();
if (string.IsNullOrEmpty(userEmail))
return ServiceResult.Fail<OrderDetailDto>(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<OrderDetailDto>(ServiceResultType.Failure, "Keine E-Mail-Adresse für den Benutzer gefunden.");
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "Ihr Warenkorb ist leer. Bitte fügen Sie Produkte hinzu.");
}
// 1.2 Adressen Validieren
// 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();
.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<OrderDetailDto>(ServiceResultType.Forbidden, "Die angegebenen Adressen sind ungültig oder gehören nicht zu diesem Konto.");
}
// 1.3 Methoden Validieren
// 1.4 Methoden Validieren
var shippingMethod = await _shippingMethodRepository.GetByIdAsync(orderDto.ShippingMethodId);
if (shippingMethod == null || !shippingMethod.IsActive)
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "Versandmethode ungültig.");
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "Versandart ungültig.");
var paymentMethod = await _context.PaymentMethods.FindAsync(orderDto.PaymentMethodId);
if (paymentMethod == null || !paymentMethod.IsActive)
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "Zahlungsmethode ungültig.");
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "Zahlungsart ungültig.");
// 1.4 Produkte Validieren
if (orderDto.Items == null || !orderDto.Items.Any())
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "Warenkorb ist leer.");
// =================================================================================
// 1.5 Produkte Validieren & Vorbereiten (BASIEREND AUF CART ITEMS)
// =================================================================================
var productIds = orderDto.Items.Select(i => i.ProductId).Distinct().ToList();
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();
@@ -164,48 +172,69 @@ namespace Webshop.Application.Services.Customers
decimal itemsTotal = 0;
var preparedOrderItems = new List<OrderItem>();
foreach (var itemDto in orderDto.Items)
foreach (var cartItem in cart.Items)
{
var product = products.FirstOrDefault(p => p.Id == itemDto.ProductId);
var product = products.FirstOrDefault(p => p.Id == cartItem.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 (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)
{
totalOrderWeight += (decimal)(product.Weight ?? 0) * itemDto.Quantity;
// Gewicht berechnen (Fallback auf 0 falls null)
totalOrderWeight += (decimal)(product.Weight ?? 0) * cartItem.Quantity;
var orderItem = new OrderItem
{
ProductId = product.Id,
ProductVariantId = itemDto.ProductVariantId,
ProductVariantId = cartItem.ProductVariantId,
ProductName = product.Name,
ProductSKU = product.SKU,
Quantity = itemDto.Quantity,
UnitPrice = product.Price,
TotalPrice = product.Price * itemDto.Quantity
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<OrderDetailDto>(ServiceResultType.Conflict, $"Fehler:\n{validationErrors}");
return ServiceResult.Fail<OrderDetailDto>(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<OrderDetailDto>(ServiceResultType.InvalidInput, $"Gesamtgewicht ({totalOrderWeight} kg) passt nicht zur Versandart.");
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, $"Gesamtgewicht ({totalOrderWeight} kg) passt nicht zur Versandart '{shippingMethod.Name}'.");
}
// 2. Preise & Rabatte
// =================================================================================
// 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);
@@ -218,27 +247,42 @@ namespace Webshop.Application.Services.Customers
discountAmount = discountResult.TotalDiscountAmount;
}
// Gesamtsummen berechnen
decimal shippingCost = shippingMethod.BaseCost;
decimal subTotalBeforeDiscount = itemsTotal + shippingCost;
if (discountAmount > subTotalBeforeDiscount) discountAmount = subTotalBeforeDiscount;
decimal subTotalAfterDiscount = subTotalBeforeDiscount - discountAmount;
decimal taxRate = await _settingService.GetSettingValueAsync<decimal>("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)
// Rabatt darf nicht höher als die Summe sein
if (discountAmount > subTotalBeforeDiscount)
{
var product = products.First(p => p.Id == itemDto.ProductId);
if (product.StockQuantity < itemDto.Quantity)
{
await transaction.RollbackAsync();
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Conflict, "Lagerbestand hat sich während des Vorgangs geändert.");
}
product.StockQuantity -= itemDto.Quantity;
discountAmount = subTotalBeforeDiscount;
}
decimal subTotalAfterDiscount = subTotalBeforeDiscount - discountAmount;
// Steuer berechnen (Settings abrufen)
decimal taxRate = await _settingService.GetSettingValueAsync<decimal>("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<OrderDetailDto>(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,
@@ -260,6 +304,7 @@ namespace Webshop.Application.Services.Customers
await _orderRepository.AddAsync(newOrder);
// 3.3 Discount Nutzung erhöhen
if (discountResult.AppliedDiscountIds.Any())
{
var appliedDiscounts = await _context.Discounts
@@ -268,6 +313,7 @@ namespace Webshop.Application.Services.Customers
foreach (var d in appliedDiscounts) d.CurrentUsageCount++;
}
// 3.4 Speichern & Commit
try
{
await _context.SaveChangesAsync();
@@ -276,18 +322,30 @@ namespace Webshop.Application.Services.Customers
catch (DbUpdateConcurrencyException ex)
{
await transaction.RollbackAsync();
_logger.LogWarning(ex, "Concurrency Konflikt bei Bestellung.");
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Conflict, "Artikel wurden soeben von einem anderen Kunden gekauft.");
_logger.LogWarning(ex, "Concurrency Fehler beim Checkout (User {UserId}).", userId);
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Conflict, "Ein oder mehrere 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."); }
// =================================================================================
// PHASE 4: CLEANUP & POST-PROCESSING
// =================================================================================
try { await _emailService.SendOrderConfirmationAsync(newOrder.Id, userEmail); }
catch (Exception ex) { _logger.LogError(ex, "Bestätigungs-Email fehlgeschlagen."); }
// 4.1 Warenkorb leeren
try
{
await _cartService.ClearCartAsync(userId!);
}
catch (Exception ex) { _logger.LogError(ex, "Cart Clear Failed for Order {OrderNumber}", newOrder.OrderNumber); }
// Manuelles Setzen der Navigation Properties für das Mapping
// 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);
@@ -297,7 +355,7 @@ namespace Webshop.Application.Services.Customers
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Checkout Exception");
_logger.LogError(ex, "Unerwarteter Fehler im Checkout für User {UserId}", userId);
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Failure, "Ein unerwarteter Fehler ist aufgetreten.");
}
}