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
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:
@@ -9,6 +9,8 @@ using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Webshop.Application;
|
||||
using System.Collections.Generic;
|
||||
using Webshop.Application.Services.Customers;
|
||||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||
|
||||
namespace Webshop.Api.Controllers.Customer
|
||||
{
|
||||
@@ -18,25 +20,34 @@ namespace Webshop.Api.Controllers.Customer
|
||||
public class CheckoutController : ControllerBase
|
||||
{
|
||||
private readonly ICheckoutService _checkoutService;
|
||||
private readonly ICartService _cartService;
|
||||
|
||||
public CheckoutController(ICheckoutService checkoutService)
|
||||
public CheckoutController(ICheckoutService checkoutService , ICartService cartService)
|
||||
{
|
||||
_checkoutService = checkoutService;
|
||||
_cartService = cartService;
|
||||
}
|
||||
|
||||
// --- NEU: Endpoint um verf<72>gbare Versandmethoden basierend auf dem Warenkorb zu holen ---
|
||||
[HttpPost("available-shipping-methods")]
|
||||
[HttpGet("available-shipping-methods")] // War vorher POST
|
||||
[ProducesResponseType(typeof(IEnumerable<ShippingMethodDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> GetAvailableShippingMethods([FromBody] ShippingCalculationRequestDto request)
|
||||
public async Task<IActionResult> GetAvailableShippingMethods()
|
||||
{
|
||||
var result = await _checkoutService.GetCompatibleShippingMethodsAsync(request.Items);
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
|
||||
// 1. Warenkorb laden
|
||||
var cartResult = await _cartService.GetCartAsync(userId!);
|
||||
if (cartResult.Value == null || !cartResult.Value.Items.Any())
|
||||
{
|
||||
return Ok(new List<ShippingMethodDto>()); // Leerer Korb -> keine Methoden
|
||||
}
|
||||
|
||||
// 2. Berechnung aufrufen (nutzt die Overload Methode mit List<CartItemDto>)
|
||||
var result = await _checkoutService.GetCompatibleShippingMethodsAsync(cartResult.Value.Items);
|
||||
|
||||
return result.Type switch
|
||||
{
|
||||
ServiceResultType.Success => Ok(result.Value),
|
||||
ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = "Fehler beim Laden der Versandmethoden." })
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = "Fehler." })
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -117,23 +117,31 @@ 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();
|
||||
@@ -143,20 +151,20 @@ namespace Webshop.Application.Services.Customers
|
||||
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;
|
||||
|
||||
// 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<decimal>("GlobalTaxRate", 0.19m);
|
||||
decimal taxAmount = subTotalAfterDiscount * taxRate; // Steuer auf reduzierten Betrag
|
||||
decimal taxAmount = subTotalAfterDiscount * taxRate;
|
||||
|
||||
decimal orderTotal = subTotalAfterDiscount + taxAmount;
|
||||
|
||||
// 3. Ausführung (Execution)
|
||||
foreach (var itemDto in orderDto.Items)
|
||||
// =================================================================================
|
||||
// PHASE 3: EXECUTION (Schreiben in DB)
|
||||
// =================================================================================
|
||||
|
||||
// 3.1 Lagerbestand reduzieren
|
||||
foreach (var oi in preparedOrderItems)
|
||||
{
|
||||
var product = products.First(p => p.Id == itemDto.ProductId);
|
||||
if (product.StockQuantity < itemDto.Quantity)
|
||||
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 hat sich während des Vorgangs geändert.");
|
||||
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Conflict, $"Lagerbestand für '{product.Name}' hat sich während des Vorgangs geändert.");
|
||||
}
|
||||
product.StockQuantity -= itemDto.Quantity;
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user