diff --git a/Webshop.Api/Controllers/Customers/CheckoutController.cs b/Webshop.Api/Controllers/Customers/CheckoutController.cs index 30756e5..7b0f037 100644 --- a/Webshop.Api/Controllers/Customers/CheckoutController.cs +++ b/Webshop.Api/Controllers/Customers/CheckoutController.cs @@ -1,18 +1,58 @@ -// Auto-generiert von CreateWebshopFiles.ps1 +// src/Webshop.Api/Controllers/Customer/CheckoutController.cs using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; - -using System; -using System.Collections.Generic; using System.Threading.Tasks; +using Webshop.Application.Services.Customers.Interfaces; +using Webshop.Application.DTOs.Orders; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Webshop.Application; -namespace Webshop.Api.Controllers.Customers +namespace Webshop.Api.Controllers.Customer { [ApiController] [Route("api/v1/customer/[controller]")] [Authorize(Roles = "Customer")] public class CheckoutController : ControllerBase { + private readonly ICheckoutService _checkoutService; + public CheckoutController(ICheckoutService checkoutService) + { + _checkoutService = checkoutService; + } + + [HttpPost("create-order")] + [ProducesResponseType(typeof(OrderDetailDto), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] + public async Task CreateOrder([FromBody] CreateOrderDto orderDto) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + // UserId aus dem JWT-Token des eingeloggten Kunden extrahieren + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + if (string.IsNullOrEmpty(userId)) + { + return Unauthorized(new { Message = "Benutzer konnte nicht identifiziert werden." }); + } + + var result = await _checkoutService.CreateOrderAsync(orderDto, userId); + + return result.Type switch + { + ServiceResultType.Success => Created($"/api/v1/customer/orders/{result.Value!.Id}", result.Value), + ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }), + ServiceResultType.Conflict => Conflict(new { Message = result.ErrorMessage }), + ServiceResultType.Unauthorized => Unauthorized(new { Message = result.ErrorMessage }), + ServiceResultType.Forbidden => Forbid(), // Forbid returns 403 without a body + _ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = result.ErrorMessage ?? "Ein unerwarteter Fehler ist aufgetreten." }) + }; + } } -} +} \ No newline at end of file diff --git a/Webshop.Application/Services/Customers/CheckoutService.cs b/Webshop.Application/Services/Customers/CheckoutService.cs index 45693ea..d662778 100644 --- a/Webshop.Application/Services/Customers/CheckoutService.cs +++ b/Webshop.Application/Services/Customers/CheckoutService.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Webshop.Application; using Webshop.Application.DTOs.Customers; using Webshop.Application.DTOs.Orders; using Webshop.Application.Services.Customers.Interfaces; @@ -25,12 +26,8 @@ namespace Webshop.Application.Services.Customers private readonly IDiscountService _discountService; public CheckoutService( - ApplicationDbContext context, - IOrderRepository orderRepository, - ICustomerRepository customerRepository, - IShippingMethodRepository shippingMethodRepository, - ISettingService settingService, - IDiscountService discountService) + ApplicationDbContext context, IOrderRepository orderRepository, ICustomerRepository customerRepository, + IShippingMethodRepository shippingMethodRepository, ISettingService settingService, IDiscountService discountService) { _context = context; _orderRepository = orderRepository; @@ -40,41 +37,31 @@ namespace Webshop.Application.Services.Customers _discountService = discountService; } - public async Task<(bool Success, OrderDetailDto? CreatedOrder, string? ErrorMessage)> CreateOrderAsync(CreateOrderDto orderDto, string? userId) + public async Task> CreateOrderAsync(CreateOrderDto orderDto, string? userId) { await using var transaction = await _context.Database.BeginTransactionAsync(); try { - // --- 1. Validierung von Kunde, Adressen und Methoden --- - Customer? customer = null; - if (!string.IsNullOrEmpty(userId)) - { - customer = await _customerRepository.GetByUserIdAsync(userId); - if (customer == null) return (false, null, "Kundenprofil nicht gefunden."); + // 1. Validierung von Kunde, Adressen und Methoden + var customer = await _customerRepository.GetByUserIdAsync(userId!); + if (customer == null) + return ServiceResult.Fail(ServiceResultType.Unauthorized, "Kundenprofil nicht gefunden."); - // Validiere, dass die Adressen zum eingeloggten Kunden gehören - if (!await _context.Addresses.AnyAsync(a => a.Id == orderDto.ShippingAddressId && a.CustomerId == customer.Id) || - !await _context.Addresses.AnyAsync(a => a.Id == orderDto.BillingAddressId && a.CustomerId == customer.Id)) - { - return (false, null, "Ungültige oder nicht zugehörige Liefer- oder Rechnungsadresse."); - } - } - else + if (!await _context.Addresses.AnyAsync(a => a.Id == orderDto.ShippingAddressId && a.CustomerId == customer.Id) || + !await _context.Addresses.AnyAsync(a => a.Id == orderDto.BillingAddressId && a.CustomerId == customer.Id)) { - // Gast-Checkout: Validierung der Gast-Daten - if (string.IsNullOrWhiteSpace(orderDto.GuestEmail)) - return (false, null, "Für Gastbestellungen ist eine E-Mail-Adresse erforderlich."); - // Hinweis: Bei Gastbestellungen müssten die Adress-DTOs mitgesendet und hier neue Adressen erstellt werden. - // Der Einfachheit halber nehmen wir an, die Adress-IDs sind gültig, aber nicht mit einem Kunden verknüpft. + return ServiceResult.Fail(ServiceResultType.Forbidden, "Ungültige oder nicht zugehörige Liefer- oder Rechnungsadresse."); } var shippingMethod = await _shippingMethodRepository.GetByIdAsync(orderDto.ShippingMethodId); - if (shippingMethod == null || !shippingMethod.IsActive) return (false, null, "Ungültige oder inaktive Versandmethode."); + if (shippingMethod == null || !shippingMethod.IsActive) + return ServiceResult.Fail(ServiceResultType.InvalidInput, "Ungültige oder inaktive Versandmethode."); var paymentMethod = await _context.PaymentMethods.FindAsync(orderDto.PaymentMethodId); - if (paymentMethod == null || !paymentMethod.IsActive) return (false, null, "Ungültige oder inaktive Zahlungsmethode."); + if (paymentMethod == null || !paymentMethod.IsActive) + return ServiceResult.Fail(ServiceResultType.InvalidInput, "Ungültige oder inaktive Zahlungsmethode."); - // --- 2. Artikel verarbeiten und Lagerbestand prüfen --- + // 2. Artikel verarbeiten und Lagerbestand prüfen var orderItems = new List(); var productIds = orderDto.Items.Select(i => i.ProductId).ToList(); var products = await _context.Products.Where(p => productIds.Contains(p.Id)).ToListAsync(); @@ -86,15 +73,15 @@ namespace Webshop.Application.Services.Customers if (product == null || !product.IsActive) { await transaction.RollbackAsync(); - return (false, null, $"Ein Produkt im Warenkorb ist nicht mehr verfügbar."); + return ServiceResult.Fail(ServiceResultType.Conflict, "Ein Produkt im Warenkorb ist nicht mehr verfügbar."); } if (product.StockQuantity < itemDto.Quantity) { await transaction.RollbackAsync(); - return (false, null, $"Nicht genügend Lagerbestand für '{product.Name}'. Verfügbar: {product.StockQuantity}, benötigt: {itemDto.Quantity}."); + return ServiceResult.Fail(ServiceResultType.Conflict, $"Nicht genügend Lagerbestand für '{product.Name}'. Verfügbar: {product.StockQuantity}, benötigt: {itemDto.Quantity}."); } - product.StockQuantity -= itemDto.Quantity; // Lagerbestand reduzieren + product.StockQuantity -= itemDto.Quantity; var orderItem = new OrderItem { @@ -110,7 +97,7 @@ namespace Webshop.Application.Services.Customers itemsTotal += orderItem.TotalPrice; } - // --- 3. Preise, Rabatte, Steuern und Gesamtbetrag berechnen --- + // 3. Preise, Rabatte, Steuern und Gesamtbetrag berechnen var discountResult = await _discountService.CalculateDiscountAsync(orderItems, orderDto.CouponCode); decimal discountAmount = discountResult.TotalDiscountAmount; decimal shippingCost = shippingMethod.BaseCost; @@ -121,12 +108,10 @@ namespace Webshop.Application.Services.Customers decimal taxAmount = subTotal * taxRate; decimal orderTotal = subTotal + taxAmount; - // --- 4. Bestellung erstellen --- + // 4. Bestellung erstellen var newOrder = new Order { - CustomerId = customer?.Id, - GuestEmail = customer == null ? orderDto.GuestEmail : null, - GuestPhoneNumber = customer == null ? orderDto.GuestPhoneNumber : null, + CustomerId = customer.Id, OrderNumber = $"WS-{DateTime.UtcNow:yyyyMMdd}-{new Random().Next(1000, 9999)}", OrderDate = DateTimeOffset.UtcNow, OrderStatus = OrderStatus.Pending.ToString(), @@ -144,30 +129,24 @@ namespace Webshop.Application.Services.Customers }; await _orderRepository.AddAsync(newOrder); - // --- 5. Rabattnutzung erhöhen --- + // 5. Rabattnutzung erhöhen if (discountResult.AppliedDiscountIds.Any()) { - var appliedDiscounts = await _context.Discounts - .Where(d => discountResult.AppliedDiscountIds.Contains(d.Id)) - .ToListAsync(); - foreach (var discount in appliedDiscounts) - { - discount.CurrentUsageCount++; - } + var appliedDiscounts = await _context.Discounts.Where(d => discountResult.AppliedDiscountIds.Contains(d.Id)).ToListAsync(); + foreach (var discount in appliedDiscounts) { discount.CurrentUsageCount++; } } - await _context.SaveChangesAsync(); // Speichert Lagerbestandsänderungen & Rabattnutzung + await _context.SaveChangesAsync(); await transaction.CommitAsync(); - // --- 6. Erfolgreiche Antwort erstellen --- + // 6. Erfolgreiche Antwort erstellen var createdOrder = await _orderRepository.GetByIdAsync(newOrder.Id); - var orderDetailDto = MapToOrderDetailDto(createdOrder!); - return (true, orderDetailDto, null); + return ServiceResult.Ok(MapToOrderDetailDto(createdOrder!)); } catch (Exception ex) { await transaction.RollbackAsync(); - return (false, null, $"Ein unerwarteter Fehler ist aufgetreten: {ex.Message}"); + return ServiceResult.Fail(ServiceResultType.Failure, $"Ein unerwarteter Fehler ist aufgetreten: {ex.Message}"); } } diff --git a/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs b/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs index cf1585f..3eab4ec 100644 --- a/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs +++ b/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs @@ -1,12 +1,13 @@ // src/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs using System; using System.Threading.Tasks; +using Webshop.Application; using Webshop.Application.DTOs.Orders; namespace Webshop.Application.Services.Customers.Interfaces { public interface ICheckoutService { - Task<(bool Success, OrderDetailDto? CreatedOrder, string? ErrorMessage)> CreateOrderAsync(CreateOrderDto orderDto, string userId); + Task> CreateOrderAsync(CreateOrderDto orderDto, string? userId); } } \ No newline at end of file