diff --git a/Webshop.Api/Controllers/Admin/AdminDiscountsController.cs b/Webshop.Api/Controllers/Admin/AdminDiscountsController.cs index ceea512..13051aa 100644 --- a/Webshop.Api/Controllers/Admin/AdminDiscountsController.cs +++ b/Webshop.Api/Controllers/Admin/AdminDiscountsController.cs @@ -1,9 +1,10 @@ // src/Webshop.Api/Controllers/Admin/AdminDiscountsController.cs -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Webshop.Application; using Webshop.Application.DTOs.Discounts; using Webshop.Application.Services.Admin.Interfaces; @@ -58,11 +59,20 @@ namespace Webshop.Api.Controllers.Admin /// /// Das Datenobjekt des zu erstellenden Rabatts. [HttpPost] + [ProducesResponseType(typeof(DiscountDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> CreateDiscount([FromBody] DiscountDto discountDto) { if (!ModelState.IsValid) return BadRequest(ModelState); - var createdDiscount = await _adminDiscountService.CreateDiscountAsync(discountDto); - return CreatedAtAction(nameof(GetDiscountById), new { id = createdDiscount.Id }, createdDiscount); + + var result = await _adminDiscountService.CreateDiscountAsync(discountDto); + + if (result.Type == ServiceResultType.Success) + { + return CreatedAtAction(nameof(GetDiscountById), new { id = result.Value!.Id }, result.Value); + } + + return BadRequest(new { Message = result.ErrorMessage }); } /// @@ -71,15 +81,23 @@ namespace Webshop.Api.Controllers.Admin /// Die ID des zu aktualisierenden Rabatts. /// Die neuen Daten für den Rabatt. [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdateDiscount(Guid id, [FromBody] DiscountDto discountDto) { - if (id != discountDto.Id) return BadRequest(); + if (id != discountDto.Id) return BadRequest("ID in URL und Body stimmen nicht überein."); if (!ModelState.IsValid) return BadRequest(ModelState); - var success = await _adminDiscountService.UpdateDiscountAsync(discountDto); - if (!success) return NotFound(); + var result = await _adminDiscountService.UpdateDiscountAsync(discountDto); - return NoContent(); + return result.Type switch + { + ServiceResultType.Success => NoContent(), + ServiceResultType.NotFound => NotFound(new { Message = result.ErrorMessage }), + ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Ein unerwarteter Fehler ist aufgetreten.") + }; } /// @@ -87,12 +105,18 @@ namespace Webshop.Api.Controllers.Admin /// /// Die ID des zu löschenden Rabatts. [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteDiscount(Guid id) { - var success = await _adminDiscountService.DeleteDiscountAsync(id); - if (!success) return NotFound(); + var result = await _adminDiscountService.DeleteDiscountAsync(id); - return NoContent(); + return result.Type switch + { + ServiceResultType.Success => NoContent(), + ServiceResultType.NotFound => NotFound(new { Message = result.ErrorMessage }), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Ein unerwarteter Fehler ist aufgetreten.") + }; } } } \ No newline at end of file diff --git a/Webshop.Application/DTOs/Orders/CreateOrderItemDto.cs b/Webshop.Application/DTOs/Orders/CreateOrderItemDto.cs index d49e99a..bb93c4c 100644 --- a/Webshop.Application/DTOs/Orders/CreateOrderItemDto.cs +++ b/Webshop.Application/DTOs/Orders/CreateOrderItemDto.cs @@ -1,6 +1,7 @@ // Auto-generiert von CreateWebshopFiles.ps1 using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; @@ -8,6 +9,7 @@ namespace Webshop.Application.DTOs.Orders { public class CreateOrderItemDto { + [Required] public Guid ProductId { get; set; } public Guid? ProductVariantId { get; set; } public int Quantity { get; set; } diff --git a/Webshop.Application/Services/Admin/AdminDiscountService.cs b/Webshop.Application/Services/Admin/AdminDiscountService.cs index 2e14c84..2c34b84 100644 --- a/Webshop.Application/Services/Admin/AdminDiscountService.cs +++ b/Webshop.Application/Services/Admin/AdminDiscountService.cs @@ -1,4 +1,5 @@ // src/Webshop.Application/Services/Admin/AdminDiscountService.cs +using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; @@ -8,16 +9,19 @@ using Webshop.Application.Services.Admin.Interfaces; using Webshop.Domain.Entities; using Webshop.Domain.Enums; using Webshop.Domain.Interfaces; +using Webshop.Infrastructure.Data; namespace Webshop.Application.Services.Admin { public class AdminDiscountService : IAdminDiscountService { private readonly IDiscountRepository _discountRepository; + private readonly ApplicationDbContext _context; // Für Unique-Prüfungen - public AdminDiscountService(IDiscountRepository discountRepository) + public AdminDiscountService(IDiscountRepository discountRepository, ApplicationDbContext context) { _discountRepository = discountRepository; + _context = context; } public async Task> GetAllDiscountsAsync() @@ -32,19 +36,41 @@ namespace Webshop.Application.Services.Admin return discount != null ? MapToDto(discount) : null; } - public async Task CreateDiscountAsync(DiscountDto discountDto) + public async Task> CreateDiscountAsync(DiscountDto discountDto) { + // Validierung: Gutscheincode muss eindeutig sein, wenn er benötigt wird + if (discountDto.RequiresCouponCode && !string.IsNullOrEmpty(discountDto.CouponCode)) + { + var existing = await _context.Discounts.FirstOrDefaultAsync(d => d.CouponCode == discountDto.CouponCode); + if (existing != null) + { + return ServiceResult.Fail(ServiceResultType.InvalidInput, $"Der Gutscheincode '{discountDto.CouponCode}' existiert bereits."); + } + } + var discount = MapToEntity(discountDto); discount.Id = Guid.NewGuid(); await _discountRepository.AddAsync(discount); - return MapToDto(discount); + return ServiceResult.Ok(MapToDto(discount)); } - public async Task UpdateDiscountAsync(DiscountDto discountDto) + public async Task UpdateDiscountAsync(DiscountDto discountDto) { var existingDiscount = await _discountRepository.GetByIdAsync(discountDto.Id); - if (existingDiscount == null) return false; + if (existingDiscount == null) + { + return ServiceResult.Fail(ServiceResultType.NotFound, "Rabatt nicht gefunden."); + } + + if (discountDto.RequiresCouponCode && !string.IsNullOrEmpty(discountDto.CouponCode)) + { + var existing = await _context.Discounts.FirstOrDefaultAsync(d => d.CouponCode == discountDto.CouponCode && d.Id != discountDto.Id); + if (existing != null) + { + return ServiceResult.Fail(ServiceResultType.InvalidInput, $"Der Gutscheincode '{discountDto.CouponCode}' wird bereits von einem anderen Rabatt verwendet."); + } + } // Update simple properties existingDiscount.Name = discountDto.Name; @@ -59,33 +85,39 @@ namespace Webshop.Application.Services.Admin existingDiscount.MinimumOrderAmount = discountDto.MinimumOrderAmount; existingDiscount.MaximumUsageCount = discountDto.MaximumUsageCount; + // << HIER IST DER WICHTIGE, WIEDERHERGESTELLTE TEIL >> // Sync assigned products existingDiscount.ProductDiscounts.Clear(); foreach (var productId in discountDto.AssignedProductIds) { - existingDiscount.ProductDiscounts.Add(new ProductDiscount { ProductId = productId }); + existingDiscount.ProductDiscounts.Add(new ProductDiscount { DiscountId = existingDiscount.Id, ProductId = productId }); } // Sync assigned categories existingDiscount.categorieDiscounts.Clear(); foreach (var categoryId in discountDto.AssignedCategoryIds) { - existingDiscount.categorieDiscounts.Add(new CategorieDiscount { categorieId = categoryId }); + existingDiscount.categorieDiscounts.Add(new CategorieDiscount { DiscountId = existingDiscount.Id, categorieId = categoryId }); } + // << ENDE DES WICHTIGEN TEILS >> await _discountRepository.UpdateAsync(existingDiscount); - return true; + return ServiceResult.Ok(); } - public async Task DeleteDiscountAsync(Guid id) + public async Task DeleteDiscountAsync(Guid id) { var discount = await _discountRepository.GetByIdAsync(id); - if (discount == null) return false; + if (discount == null) + { + return ServiceResult.Fail(ServiceResultType.NotFound, "Rabatt nicht gefunden."); + } await _discountRepository.DeleteAsync(id); - return true; + return ServiceResult.Ok(); } + // Helper methods for mapping private DiscountDto MapToDto(Discount discount) { diff --git a/Webshop.Application/Services/Admin/Interfaces/IAdminDiscountService.cs b/Webshop.Application/Services/Admin/Interfaces/IAdminDiscountService.cs index 3d41737..18cb6cd 100644 --- a/Webshop.Application/Services/Admin/Interfaces/IAdminDiscountService.cs +++ b/Webshop.Application/Services/Admin/Interfaces/IAdminDiscountService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Webshop.Application; // << NEU: Für ServiceResult >> using Webshop.Application.DTOs.Discounts; namespace Webshop.Application.Services.Admin.Interfaces @@ -10,8 +11,8 @@ namespace Webshop.Application.Services.Admin.Interfaces { Task> GetAllDiscountsAsync(); Task GetDiscountByIdAsync(Guid id); - Task CreateDiscountAsync(DiscountDto discountDto); - Task UpdateDiscountAsync(DiscountDto discountDto); - Task DeleteDiscountAsync(Guid id); + Task> CreateDiscountAsync(DiscountDto discountDto); + Task UpdateDiscountAsync(DiscountDto discountDto); + Task DeleteDiscountAsync(Guid id); } } \ No newline at end of file diff --git a/Webshop.Application/Services/Customers/CheckoutService.cs b/Webshop.Application/Services/Customers/CheckoutService.cs index ecfe356..45693ea 100644 --- a/Webshop.Application/Services/Customers/CheckoutService.cs +++ b/Webshop.Application/Services/Customers/CheckoutService.cs @@ -40,16 +40,33 @@ namespace Webshop.Application.Services.Customers _discountService = discountService; } - public async Task<(bool Success, OrderDetailDto? CreatedOrder, string? ErrorMessage)> CreateOrderAsync(CreateOrderDto orderDto, string userId) + 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."); + // --- 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."); + + // 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 + { + // 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. + } var shippingMethod = await _shippingMethodRepository.GetByIdAsync(orderDto.ShippingMethodId); if (shippingMethod == null || !shippingMethod.IsActive) return (false, null, "Ungültige oder inaktive Versandmethode."); @@ -57,33 +74,27 @@ namespace Webshop.Application.Services.Customers 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(); + var productIds = orderDto.Items.Select(i => i.ProductId).ToList(); + var products = await _context.Products.Where(p => productIds.Contains(p.Id)).ToListAsync(); decimal itemsTotal = 0; foreach (var itemDto in orderDto.Items) { - var product = await _context.Products.FindAsync(itemDto.ProductId); + var product = products.FirstOrDefault(p => p.Id == itemDto.ProductId); if (product == null || !product.IsActive) { await transaction.RollbackAsync(); - return (false, null, $"Produkt mit ID {itemDto.ProductId} ist nicht verfügbar."); + return (false, null, $"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}."); + return (false, null, $"Nicht genügend Lagerbestand für '{product.Name}'. Verfügbar: {product.StockQuantity}, benötigt: {itemDto.Quantity}."); } - // Lagerbestand reduzieren - product.StockQuantity -= itemDto.Quantity; + product.StockQuantity -= itemDto.Quantity; // Lagerbestand reduzieren var orderItem = new OrderItem { @@ -95,13 +106,13 @@ namespace Webshop.Application.Services.Customers 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); + var discountResult = await _discountService.CalculateDiscountAsync(orderItems, orderDto.CouponCode); + decimal discountAmount = discountResult.TotalDiscountAmount; decimal shippingCost = shippingMethod.BaseCost; decimal subTotal = itemsTotal + shippingCost - discountAmount; if (subTotal < 0) subTotal = 0; @@ -113,47 +124,53 @@ namespace Webshop.Application.Services.Customers // --- 4. Bestellung erstellen --- var newOrder = new Order { - CustomerId = customer.Id, + CustomerId = customer?.Id, + GuestEmail = customer == null ? orderDto.GuestEmail : null, + GuestPhoneNumber = customer == null ? orderDto.GuestPhoneNumber : null, 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 - + 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 = orderItems }; + await _orderRepository.AddAsync(newOrder); - await _orderRepository.AddAsync(newOrder); // Speichert die Bestellung und die Artikel - await _context.SaveChangesAsync(); // Speichert die Lagerbestandsänderungen + // --- 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++; + } + } - 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 + await _context.SaveChangesAsync(); // Speichert Lagerbestandsänderungen & Rabattnutzung + await transaction.CommitAsync(); + // --- 6. Erfolgreiche Antwort erstellen --- + var createdOrder = await _orderRepository.GetByIdAsync(newOrder.Id); + var orderDetailDto = MapToOrderDetailDto(createdOrder!); 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 @@ -164,10 +181,35 @@ namespace Webshop.Application.Services.Customers CustomerId = order.CustomerId, Status = Enum.Parse(order.OrderStatus), TotalAmount = order.OrderTotal, - ShippingAddress = new AddressDto { /* Mapping */ }, - BillingAddress = new AddressDto { /* Mapping */ }, + 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.PaymentMethod, PaymentStatus = Enum.Parse(order.PaymentStatus), + ShippingTrackingNumber = order.ShippingTrackingNumber, + ShippedDate = order.ShippedDate, + DeliveredDate = order.DeliveredDate, OrderItems = order.OrderItems.Select(oi => new OrderItemDto { Id = oi.Id, diff --git a/Webshop.Application/Services/Public/DiscountService.cs b/Webshop.Application/Services/Public/DiscountService.cs index d16d303..e4e45d7 100644 --- a/Webshop.Application/Services/Public/DiscountService.cs +++ b/Webshop.Application/Services/Public/DiscountService.cs @@ -6,85 +6,113 @@ using System.Linq; using System.Threading.Tasks; using Webshop.Application.Services.Public.Interfaces; using Webshop.Domain.Entities; -using Webshop.Domain.Interfaces; +using Webshop.Domain.Enums; 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; + private readonly ApplicationDbContext _context; - public DiscountService( - ApplicationDbContext context, - IDiscountRepository discountRepository, - IProductRepository productRepository) + public DiscountService(ApplicationDbContext context) { _context = context; - _discountRepository = discountRepository; - _productRepository = productRepository; } - public async Task CalculateDiscountAsync(List orderItems, string? couponCode) + public async Task CalculateDiscountAsync(List orderItems, string? couponCode) { - decimal totalDiscount = 0; + var result = new DiscountCalculationResult(); + var now = DateTimeOffset.UtcNow; + decimal itemsTotal = orderItems.Sum(i => i.TotalPrice); - // 1. Hole alle relevanten Rabatte - var discounts = await _context.Discounts + // 1. Lade alle potenziell anwendbaren Rabatte + var query = _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(); + .Where(d => d.IsActive && d.StartDate <= now && + (!d.EndDate.HasValue || d.EndDate >= now) && + (!d.MaximumUsageCount.HasValue || d.CurrentUsageCount < d.MaximumUsageCount.Value)); - // Filtern nach Gutscheincode + var potentialDiscounts = await query.ToListAsync(); + + Discount? bestDiscount = null; + + // 2. Finde den besten anwendbaren Rabatt if (!string.IsNullOrEmpty(couponCode)) { - discounts = discounts.Where(d => d.CouponCode == couponCode && d.RequiresCouponCode).ToList(); + // Wenn ein Code eingegeben wurde, suchen wir nur nach diesem einen Rabatt + bestDiscount = potentialDiscounts.FirstOrDefault(d => d.RequiresCouponCode && d.CouponCode == couponCode); } + + if (bestDiscount == null) + { + return result; // Kein passender Rabatt gefunden + } + + // 3. Prüfe die Bedingungen des besten Rabatts + if (bestDiscount.MinimumOrderAmount.HasValue && itemsTotal < bestDiscount.MinimumOrderAmount.Value) + { + return result; // Mindestbestellwert nicht erreicht + } + + decimal discountAmount = 0; + + // 4. Berechne den Rabattbetrag + // Fall A: Rabatt gilt für den gesamten Warenkorb + if (!bestDiscount.ProductDiscounts.Any() && !bestDiscount.categorieDiscounts.Any()) + { + discountAmount = CalculateAmount(bestDiscount, itemsTotal); + } + // Fall B: Rabatt gilt für spezifische Produkte/Kategorien else { - // Rabatte ohne Gutscheincode, falls zutreffend - discounts = discounts.Where(d => !d.RequiresCouponCode).ToList(); - } + // Lade die Kategorie-Zuweisungen aller Produkte im Warenkorb + var productCategoryIds = await _context.Productcategories + .Where(pc => orderItems.Select(oi => oi.ProductId).Contains(pc.ProductId)) + .ToDictionaryAsync(pc => pc.ProductId, pc => pc.categorieId); - // 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) + decimal applicableItemsTotal = 0; + foreach (var item in orderItems) { - itemDiscount = item.UnitPrice * item.Quantity * (applicableDiscount.DiscountValue / 100); - // Hier müsste die Logik für FixedAmount-Rabatte hin - } + if (!item.ProductId.HasValue) + { + continue; + } - totalDiscount += itemDiscount; + Guid productId = item.ProductId.Value; + + bool isApplicable = bestDiscount.ProductDiscounts.Any(pd => pd.ProductId == productId) || + (productCategoryIds.ContainsKey(productId) && + bestDiscount.categorieDiscounts.Any(cd => cd.categorieId == productCategoryIds[productId])); + + if (isApplicable) + { + applicableItemsTotal += item.TotalPrice; + } + } + discountAmount = CalculateAmount(bestDiscount, applicableItemsTotal); } - // 3. Wende Warenkorb-Rabatte an (falls keine spezifischen Produkte/Kategorien zugewiesen sind) - var cartDiscounts = discounts.Where(d => !d.ProductDiscounts.Any() && !d.categorieDiscounts.Any()).ToList(); + result.TotalDiscountAmount = discountAmount; + result.AppliedDiscountIds.Add(bestDiscount.Id); - decimal cartTotal = orderItems.Sum(i => i.TotalPrice); - foreach (var discount in cartDiscounts) + return result; + } + + // Private Helper-Methode zur Berechnung des Rabattwertes + private decimal CalculateAmount(Discount discount, decimal baseAmount) + { + if (discount.DiscountType == DiscountType.FixedAmount.ToString()) { - 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); - } + // Stelle sicher, dass der Rabatt nicht höher als der Betrag ist + return Math.Min(baseAmount, discount.DiscountValue); } - - return totalDiscount; + if (discount.DiscountType == DiscountType.Percentage.ToString()) + { + return baseAmount * (discount.DiscountValue / 100m); + } + return 0; } } } \ No newline at end of file diff --git a/Webshop.Application/Services/Public/Interfaces/IDiscountService.cs b/Webshop.Application/Services/Public/Interfaces/IDiscountService.cs index 9ba3087..c45e150 100644 --- a/Webshop.Application/Services/Public/Interfaces/IDiscountService.cs +++ b/Webshop.Application/Services/Public/Interfaces/IDiscountService.cs @@ -6,8 +6,24 @@ using Webshop.Domain.Entities; namespace Webshop.Application.Services.Public.Interfaces { + /// + /// Stellt das Ergebnis der Rabattberechnung dar. + /// + public class DiscountCalculationResult + { + /// + /// Der gesamte berechnete Rabattbetrag. + /// + public decimal TotalDiscountAmount { get; set; } = 0; + + /// + /// Die IDs der Rabatte, die erfolgreich angewendet wurden. + /// + public List AppliedDiscountIds { get; set; } = new List(); + } + public interface IDiscountService { - Task CalculateDiscountAsync(List orderItems, string? couponCode); + Task CalculateDiscountAsync(List orderItems, string? couponCode); } } \ No newline at end of file