From f91fc13a87869a6b1b1b971eb7ffbdf946425bba Mon Sep 17 00:00:00 2001 From: "Tizian.Breuch" Date: Tue, 12 Aug 2025 13:32:25 +0200 Subject: [PATCH] checkout --- Webshop.Api/Program.cs | 2 + .../DTOs/Orders/CreateOrderDto.cs | 9 +- .../Services/Customers/CheckoutService.cs | 179 +++++++++++++++++- .../Customers/Interfaces/ICheckoutService.cs | 12 +- .../Services/Public/DiscountService.cs | 90 +++++++++ .../Public/Interfaces/IDiscountService.cs | 13 ++ ...cs => 20250812113218_checkout.Designer.cs} | 4 +- ...tAndInfo.cs => 20250812113218_checkout.cs} | 2 +- 8 files changed, 290 insertions(+), 21 deletions(-) create mode 100644 Webshop.Application/Services/Public/DiscountService.cs create mode 100644 Webshop.Application/Services/Public/Interfaces/IDiscountService.cs rename Webshop.Infrastructure/Migrations/{20250812095336_discountAndInfo.Designer.cs => 20250812113218_checkout.Designer.cs} (99%) rename Webshop.Infrastructure/Migrations/{20250812095336_discountAndInfo.cs => 20250812113218_checkout.cs} (99%) diff --git a/Webshop.Api/Program.cs b/Webshop.Api/Program.cs index 9604424..ade98bb 100644 --- a/Webshop.Api/Program.cs +++ b/Webshop.Api/Program.cs @@ -134,6 +134,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); //fehlt noch builder.Services.AddScoped(); diff --git a/Webshop.Application/DTOs/Orders/CreateOrderDto.cs b/Webshop.Application/DTOs/Orders/CreateOrderDto.cs index f1e75b6..2359168 100644 --- a/Webshop.Application/DTOs/Orders/CreateOrderDto.cs +++ b/Webshop.Application/DTOs/Orders/CreateOrderDto.cs @@ -1,8 +1,6 @@ -// Auto-generiert von CreateWebshopFiles.ps1 +// src/Webshop.Application/DTOs/Orders/CreateOrderDto.cs using System; using System.Collections.Generic; -using System.Threading.Tasks; - namespace Webshop.Application.DTOs.Orders { @@ -15,6 +13,9 @@ namespace Webshop.Application.DTOs.Orders public Guid BillingAddressId { get; set; } public Guid PaymentMethodId { get; set; } public Guid ShippingMethodId { get; set; } + + public string? CouponCode { get; set; } // << NEU >> + public List Items { get; set; } = new List(); } -} +} \ No newline at end of file diff --git a/Webshop.Application/Services/Customers/CheckoutService.cs b/Webshop.Application/Services/Customers/CheckoutService.cs index e61f9e7..ecfe356 100644 --- a/Webshop.Application/Services/Customers/CheckoutService.cs +++ b/Webshop.Application/Services/Customers/CheckoutService.cs @@ -1,18 +1,185 @@ -// Auto-generiert von CreateWebshopFiles.ps1 +// src/Webshop.Application/Services/Customers/CheckoutService.cs +using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Webshop.Application.DTOs.Customers; +using Webshop.Application.DTOs.Orders; using Webshop.Application.Services.Customers.Interfaces; - +using Webshop.Application.Services.Public.Interfaces; +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 { - // Fügen Sie hier Abhängigkeiten per Dependency Injection hinzu (z.B. Repositories) + private readonly ApplicationDbContext _context; + private readonly IOrderRepository _orderRepository; + private readonly ICustomerRepository _customerRepository; + private readonly IShippingMethodRepository _shippingMethodRepository; + private readonly ISettingService _settingService; + private readonly IDiscountService _discountService; - // public CheckoutService(IYourRepository repository) { } + public CheckoutService( + ApplicationDbContext context, + IOrderRepository orderRepository, + ICustomerRepository customerRepository, + IShippingMethodRepository shippingMethodRepository, + ISettingService settingService, + IDiscountService discountService) + { + _context = context; + _orderRepository = orderRepository; + _customerRepository = customerRepository; + _shippingMethodRepository = shippingMethodRepository; + _settingService = settingService; + _discountService = discountService; + } - // Fügen Sie hier Service-Methoden hinzu + public async Task<(bool Success, OrderDetailDto? CreatedOrder, string? ErrorMessage)> CreateOrderAsync(CreateOrderDto orderDto, string userId) + { + // Startet eine Datenbank-Transaktion. Entweder alles klappt, oder alles wird zurückgerollt. + await using var transaction = await _context.Database.BeginTransactionAsync(); + + try + { + // --- 1. Validierung der Eingabedaten --- + var customer = await _customerRepository.GetByUserIdAsync(userId); + if (customer == null) return (false, null, "Kundenprofil nicht gefunden."); + + var shippingMethod = await _shippingMethodRepository.GetByIdAsync(orderDto.ShippingMethodId); + if (shippingMethod == null || !shippingMethod.IsActive) return (false, null, "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 (!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."); + } + + // --- 2. Artikel verarbeiten und Lagerbestand prüfen --- + var orderItems = new List(); + decimal itemsTotal = 0; + + foreach (var itemDto in orderDto.Items) + { + var product = await _context.Products.FindAsync(itemDto.ProductId); + if (product == null || !product.IsActive) + { + await transaction.RollbackAsync(); + return (false, null, $"Produkt mit ID {itemDto.ProductId} ist nicht 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}."); + } + + // Lagerbestand reduzieren + product.StockQuantity -= 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 + }; + + orderItems.Add(orderItem); + itemsTotal += orderItem.TotalPrice; + } + + // --- 3. Preise, Rabatte, Steuern und Gesamtbetrag berechnen --- + decimal discountAmount = await _discountService.CalculateDiscountAsync(orderItems, orderDto.CouponCode); + decimal shippingCost = shippingMethod.BaseCost; + decimal subTotal = itemsTotal + shippingCost - discountAmount; + if (subTotal < 0) subTotal = 0; + + decimal taxRate = await _settingService.GetSettingValueAsync("GlobalTaxRate", 0.19m); + decimal taxAmount = subTotal * taxRate; + decimal orderTotal = subTotal + taxAmount; + + // --- 4. Bestellung erstellen --- + var newOrder = new Order + { + CustomerId = customer.Id, + OrderNumber = $"WS-{DateTime.UtcNow:yyyyMMdd}-{new Random().Next(1000, 9999)}", + OrderDate = DateTimeOffset.UtcNow, + OrderStatus = OrderStatus.Pending.ToString(), + PaymentStatus = PaymentStatus.Pending.ToString(), // Wird später vom Zahlungsanbieter aktualisiert + + OrderTotal = orderTotal, + ShippingCost = shippingCost, + TaxAmount = taxAmount, + DiscountAmount = discountAmount, + + PaymentMethod = paymentMethod.Name, + PaymentMethodId = paymentMethod.Id, + ShippingMethodId = shippingMethod.Id, + + BillingAddressId = orderDto.BillingAddressId, + ShippingAddressId = orderDto.ShippingAddressId, + + OrderItems = orderItems + }; + + await _orderRepository.AddAsync(newOrder); // Speichert die Bestellung und die Artikel + await _context.SaveChangesAsync(); // Speichert die Lagerbestandsänderungen + + await transaction.CommitAsync(); // Transaktion erfolgreich abschließen + + // --- 5. Erfolgreiche Antwort erstellen --- + var createdOrder = await _orderRepository.GetByIdAsync(newOrder.Id); // Lade die erstellte Bestellung mit allen Details + var orderDetailDto = MapToOrderDetailDto(createdOrder!); // Mapping zum DTO + + return (true, orderDetailDto, null); + } + catch (Exception ex) + { + await transaction.RollbackAsync(); + // Hier Fehler loggen + return (false, null, $"Ein unerwarteter Fehler ist aufgetreten: {ex.Message}"); + } + } + + // Helper-Methode für das Mapping, um Code-Duplizierung zu vermeiden + private OrderDetailDto MapToOrderDetailDto(Order order) + { + return new OrderDetailDto + { + Id = order.Id, + OrderNumber = order.OrderNumber, + OrderDate = order.OrderDate, + CustomerId = order.CustomerId, + Status = Enum.Parse(order.OrderStatus), + TotalAmount = order.OrderTotal, + ShippingAddress = new AddressDto { /* Mapping */ }, + BillingAddress = new AddressDto { /* Mapping */ }, + PaymentMethod = order.PaymentMethod, + PaymentStatus = Enum.Parse(order.PaymentStatus), + 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() + }; + } } -} +} \ No newline at end of file diff --git a/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs b/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs index 32ce9ba..cf1585f 100644 --- a/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs +++ b/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs @@ -1,16 +1,12 @@ -// Auto-generiert von CreateWebshopFiles.ps1 +// src/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs using System; -using System.Collections.Generic; using System.Threading.Tasks; -using Webshop.Application.DTOs; -using Webshop.Application.DTOs.Auth; -using Webshop.Application.DTOs.Users; - +using Webshop.Application.DTOs.Orders; namespace Webshop.Application.Services.Customers.Interfaces { public interface ICheckoutService { - // Fügen Sie hier Methodensignaturen hinzu + Task<(bool Success, OrderDetailDto? CreatedOrder, string? ErrorMessage)> CreateOrderAsync(CreateOrderDto orderDto, string userId); } -} +} \ No newline at end of file diff --git a/Webshop.Application/Services/Public/DiscountService.cs b/Webshop.Application/Services/Public/DiscountService.cs new file mode 100644 index 0000000..d16d303 --- /dev/null +++ b/Webshop.Application/Services/Public/DiscountService.cs @@ -0,0 +1,90 @@ +// src/Webshop.Application/Services/Public/DiscountService.cs +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Webshop.Application.Services.Public.Interfaces; +using Webshop.Domain.Entities; +using Webshop.Domain.Interfaces; +using Webshop.Infrastructure.Data; + +namespace Webshop.Application.Services.Public +{ + public class DiscountService : IDiscountService + { + private readonly ApplicationDbContext _context; // Direkter Zugriff auf DbSets + private readonly IDiscountRepository _discountRepository; + private readonly IProductRepository _productRepository; + + public DiscountService( + ApplicationDbContext context, + IDiscountRepository discountRepository, + IProductRepository productRepository) + { + _context = context; + _discountRepository = discountRepository; + _productRepository = productRepository; + } + + public async Task CalculateDiscountAsync(List orderItems, string? couponCode) + { + decimal totalDiscount = 0; + + // 1. Hole alle relevanten Rabatte + var discounts = await _context.Discounts + .Include(d => d.ProductDiscounts) + .Include(d => d.categorieDiscounts) + .Where(d => d.IsActive && d.StartDate <= DateTimeOffset.UtcNow && (!d.EndDate.HasValue || d.EndDate >= DateTimeOffset.UtcNow)) + .ToListAsync(); + + // Filtern nach Gutscheincode + if (!string.IsNullOrEmpty(couponCode)) + { + discounts = discounts.Where(d => d.CouponCode == couponCode && d.RequiresCouponCode).ToList(); + } + else + { + // Rabatte ohne Gutscheincode, falls zutreffend + discounts = discounts.Where(d => !d.RequiresCouponCode).ToList(); + } + + // 2. Wende Rabatte auf Artikel an + foreach (var item in orderItems) + { + decimal itemDiscount = 0; + + // Wir nehmen an, dass ein Artikel nur den besten Rabatt erhalten kann + var applicableDiscount = discounts.FirstOrDefault(d => + d.ProductDiscounts.Any(pd => pd.ProductId == item.ProductId) || // Rabatt auf Produkt + d.categorieDiscounts.Any(cd => _context.Productcategories.Any(pc => pc.ProductId == item.ProductId && pc.categorieId == cd.categorieId)) // Rabatt auf Kategorie + ); + + if (applicableDiscount != null) + { + itemDiscount = item.UnitPrice * item.Quantity * (applicableDiscount.DiscountValue / 100); + // Hier müsste die Logik für FixedAmount-Rabatte hin + } + + totalDiscount += itemDiscount; + } + + // 3. Wende Warenkorb-Rabatte an (falls keine spezifischen Produkte/Kategorien zugewiesen sind) + var cartDiscounts = discounts.Where(d => !d.ProductDiscounts.Any() && !d.categorieDiscounts.Any()).ToList(); + + decimal cartTotal = orderItems.Sum(i => i.TotalPrice); + foreach (var discount in cartDiscounts) + { + if (discount.MinimumOrderAmount.HasValue && cartTotal < discount.MinimumOrderAmount.Value) continue; + + // Beispiel: Prozentualer Rabatt auf den Warenkorb + if (discount.DiscountType == DiscountType.Percentage.ToString()) + { + totalDiscount += cartTotal * (discount.DiscountValue / 100); + } + } + + return totalDiscount; + } + } +} \ No newline at end of file diff --git a/Webshop.Application/Services/Public/Interfaces/IDiscountService.cs b/Webshop.Application/Services/Public/Interfaces/IDiscountService.cs new file mode 100644 index 0000000..9ba3087 --- /dev/null +++ b/Webshop.Application/Services/Public/Interfaces/IDiscountService.cs @@ -0,0 +1,13 @@ +// src/Webshop.Application/Services/Public/Interfaces/IDiscountService.cs +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Webshop.Domain.Entities; + +namespace Webshop.Application.Services.Public.Interfaces +{ + public interface IDiscountService + { + Task CalculateDiscountAsync(List orderItems, string? couponCode); + } +} \ No newline at end of file diff --git a/Webshop.Infrastructure/Migrations/20250812095336_discountAndInfo.Designer.cs b/Webshop.Infrastructure/Migrations/20250812113218_checkout.Designer.cs similarity index 99% rename from Webshop.Infrastructure/Migrations/20250812095336_discountAndInfo.Designer.cs rename to Webshop.Infrastructure/Migrations/20250812113218_checkout.Designer.cs index 80a8890..139313f 100644 --- a/Webshop.Infrastructure/Migrations/20250812095336_discountAndInfo.Designer.cs +++ b/Webshop.Infrastructure/Migrations/20250812113218_checkout.Designer.cs @@ -12,8 +12,8 @@ using Webshop.Infrastructure.Data; namespace Webshop.Infrastructure.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20250812095336_discountAndInfo")] - partial class discountAndInfo + [Migration("20250812113218_checkout")] + partial class checkout { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) diff --git a/Webshop.Infrastructure/Migrations/20250812095336_discountAndInfo.cs b/Webshop.Infrastructure/Migrations/20250812113218_checkout.cs similarity index 99% rename from Webshop.Infrastructure/Migrations/20250812095336_discountAndInfo.cs rename to Webshop.Infrastructure/Migrations/20250812113218_checkout.cs index 1daeb28..95a30b0 100644 --- a/Webshop.Infrastructure/Migrations/20250812095336_discountAndInfo.cs +++ b/Webshop.Infrastructure/Migrations/20250812113218_checkout.cs @@ -7,7 +7,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Webshop.Infrastructure.Migrations { /// - public partial class discountAndInfo : Migration + public partial class checkout : Migration { /// protected override void Up(MigrationBuilder migrationBuilder)