discounts überarbeitet

This commit is contained in:
Tizian.Breuch
2025-08-29 13:57:39 +02:00
parent 6e6f38397b
commit a2e3a44e94
7 changed files with 257 additions and 112 deletions

View File

@@ -1,9 +1,10 @@
// src/Webshop.Api/Controllers/Admin/AdminDiscountsController.cs // src/Webshop.Api/Controllers/Admin/AdminDiscountsController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Webshop.Application;
using Webshop.Application.DTOs.Discounts; using Webshop.Application.DTOs.Discounts;
using Webshop.Application.Services.Admin.Interfaces; using Webshop.Application.Services.Admin.Interfaces;
@@ -58,11 +59,20 @@ namespace Webshop.Api.Controllers.Admin
/// </remarks> /// </remarks>
/// <param name="discountDto">Das Datenobjekt des zu erstellenden Rabatts.</param> /// <param name="discountDto">Das Datenobjekt des zu erstellenden Rabatts.</param>
[HttpPost] [HttpPost]
[ProducesResponseType(typeof(DiscountDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<DiscountDto>> CreateDiscount([FromBody] DiscountDto discountDto) public async Task<ActionResult<DiscountDto>> CreateDiscount([FromBody] DiscountDto discountDto)
{ {
if (!ModelState.IsValid) return BadRequest(ModelState); 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 });
} }
/// <summary> /// <summary>
@@ -71,15 +81,23 @@ namespace Webshop.Api.Controllers.Admin
/// <param name="id">Die ID des zu aktualisierenden Rabatts.</param> /// <param name="id">Die ID des zu aktualisierenden Rabatts.</param>
/// <param name="discountDto">Die neuen Daten f<>r den Rabatt.</param> /// <param name="discountDto">Die neuen Daten f<>r den Rabatt.</param>
[HttpPut("{id}")] [HttpPut("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateDiscount(Guid id, [FromBody] DiscountDto discountDto) public async Task<IActionResult> 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 <20>berein.");
if (!ModelState.IsValid) return BadRequest(ModelState); if (!ModelState.IsValid) return BadRequest(ModelState);
var success = await _adminDiscountService.UpdateDiscountAsync(discountDto); var result = await _adminDiscountService.UpdateDiscountAsync(discountDto);
if (!success) return NotFound();
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.")
};
} }
/// <summary> /// <summary>
@@ -87,12 +105,18 @@ namespace Webshop.Api.Controllers.Admin
/// </summary> /// </summary>
/// <param name="id">Die ID des zu l<>schenden Rabatts.</param> /// <param name="id">Die ID des zu l<>schenden Rabatts.</param>
[HttpDelete("{id}")] [HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> DeleteDiscount(Guid id) public async Task<IActionResult> DeleteDiscount(Guid id)
{ {
var success = await _adminDiscountService.DeleteDiscountAsync(id); var result = await _adminDiscountService.DeleteDiscountAsync(id);
if (!success) return NotFound();
return NoContent(); return result.Type switch
{
ServiceResultType.Success => NoContent(),
ServiceResultType.NotFound => NotFound(new { Message = result.ErrorMessage }),
_ => StatusCode(StatusCodes.Status500InternalServerError, "Ein unerwarteter Fehler ist aufgetreten.")
};
} }
} }
} }

View File

@@ -1,6 +1,7 @@
// Auto-generiert von CreateWebshopFiles.ps1 // Auto-generiert von CreateWebshopFiles.ps1
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -8,6 +9,7 @@ namespace Webshop.Application.DTOs.Orders
{ {
public class CreateOrderItemDto public class CreateOrderItemDto
{ {
[Required]
public Guid ProductId { get; set; } public Guid ProductId { get; set; }
public Guid? ProductVariantId { get; set; } public Guid? ProductVariantId { get; set; }
public int Quantity { get; set; } public int Quantity { get; set; }

View File

@@ -1,4 +1,5 @@
// src/Webshop.Application/Services/Admin/AdminDiscountService.cs // src/Webshop.Application/Services/Admin/AdminDiscountService.cs
using Microsoft.EntityFrameworkCore;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -8,16 +9,19 @@ using Webshop.Application.Services.Admin.Interfaces;
using Webshop.Domain.Entities; using Webshop.Domain.Entities;
using Webshop.Domain.Enums; using Webshop.Domain.Enums;
using Webshop.Domain.Interfaces; using Webshop.Domain.Interfaces;
using Webshop.Infrastructure.Data;
namespace Webshop.Application.Services.Admin namespace Webshop.Application.Services.Admin
{ {
public class AdminDiscountService : IAdminDiscountService public class AdminDiscountService : IAdminDiscountService
{ {
private readonly IDiscountRepository _discountRepository; private readonly IDiscountRepository _discountRepository;
private readonly ApplicationDbContext _context; // F<>r Unique-Pr<50>fungen
public AdminDiscountService(IDiscountRepository discountRepository) public AdminDiscountService(IDiscountRepository discountRepository, ApplicationDbContext context)
{ {
_discountRepository = discountRepository; _discountRepository = discountRepository;
_context = context;
} }
public async Task<IEnumerable<DiscountDto>> GetAllDiscountsAsync() public async Task<IEnumerable<DiscountDto>> GetAllDiscountsAsync()
@@ -32,19 +36,41 @@ namespace Webshop.Application.Services.Admin
return discount != null ? MapToDto(discount) : null; return discount != null ? MapToDto(discount) : null;
} }
public async Task<DiscountDto?> CreateDiscountAsync(DiscountDto discountDto) public async Task<ServiceResult<DiscountDto>> CreateDiscountAsync(DiscountDto discountDto)
{ {
// Validierung: Gutscheincode muss eindeutig sein, wenn er ben<65>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<DiscountDto>(ServiceResultType.InvalidInput, $"Der Gutscheincode '{discountDto.CouponCode}' existiert bereits.");
}
}
var discount = MapToEntity(discountDto); var discount = MapToEntity(discountDto);
discount.Id = Guid.NewGuid(); discount.Id = Guid.NewGuid();
await _discountRepository.AddAsync(discount); await _discountRepository.AddAsync(discount);
return MapToDto(discount); return ServiceResult.Ok(MapToDto(discount));
} }
public async Task<bool> UpdateDiscountAsync(DiscountDto discountDto) public async Task<ServiceResult> UpdateDiscountAsync(DiscountDto discountDto)
{ {
var existingDiscount = await _discountRepository.GetByIdAsync(discountDto.Id); 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 // Update simple properties
existingDiscount.Name = discountDto.Name; existingDiscount.Name = discountDto.Name;
@@ -59,33 +85,39 @@ namespace Webshop.Application.Services.Admin
existingDiscount.MinimumOrderAmount = discountDto.MinimumOrderAmount; existingDiscount.MinimumOrderAmount = discountDto.MinimumOrderAmount;
existingDiscount.MaximumUsageCount = discountDto.MaximumUsageCount; existingDiscount.MaximumUsageCount = discountDto.MaximumUsageCount;
// << HIER IST DER WICHTIGE, WIEDERHERGESTELLTE TEIL >>
// Sync assigned products // Sync assigned products
existingDiscount.ProductDiscounts.Clear(); existingDiscount.ProductDiscounts.Clear();
foreach (var productId in discountDto.AssignedProductIds) 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 // Sync assigned categories
existingDiscount.categorieDiscounts.Clear(); existingDiscount.categorieDiscounts.Clear();
foreach (var categoryId in discountDto.AssignedCategoryIds) 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); await _discountRepository.UpdateAsync(existingDiscount);
return true; return ServiceResult.Ok();
} }
public async Task<bool> DeleteDiscountAsync(Guid id) public async Task<ServiceResult> DeleteDiscountAsync(Guid id)
{ {
var discount = await _discountRepository.GetByIdAsync(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); await _discountRepository.DeleteAsync(id);
return true; return ServiceResult.Ok();
} }
// Helper methods for mapping // Helper methods for mapping
private DiscountDto MapToDto(Discount discount) private DiscountDto MapToDto(Discount discount)
{ {

View File

@@ -2,6 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Webshop.Application; // << NEU: F<>r ServiceResult >>
using Webshop.Application.DTOs.Discounts; using Webshop.Application.DTOs.Discounts;
namespace Webshop.Application.Services.Admin.Interfaces namespace Webshop.Application.Services.Admin.Interfaces
@@ -10,8 +11,8 @@ namespace Webshop.Application.Services.Admin.Interfaces
{ {
Task<IEnumerable<DiscountDto>> GetAllDiscountsAsync(); Task<IEnumerable<DiscountDto>> GetAllDiscountsAsync();
Task<DiscountDto?> GetDiscountByIdAsync(Guid id); Task<DiscountDto?> GetDiscountByIdAsync(Guid id);
Task<DiscountDto?> CreateDiscountAsync(DiscountDto discountDto); Task<ServiceResult<DiscountDto>> CreateDiscountAsync(DiscountDto discountDto);
Task<bool> UpdateDiscountAsync(DiscountDto discountDto); Task<ServiceResult> UpdateDiscountAsync(DiscountDto discountDto);
Task<bool> DeleteDiscountAsync(Guid id); Task<ServiceResult> DeleteDiscountAsync(Guid id);
} }
} }

View File

@@ -40,50 +40,61 @@ namespace Webshop.Application.Services.Customers
_discountService = discountService; _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(); await using var transaction = await _context.Database.BeginTransactionAsync();
try try
{ {
// --- 1. Validierung der Eingabedaten --- // --- 1. Validierung von Kunde, Adressen und Methoden ---
var customer = await _customerRepository.GetByUserIdAsync(userId); Customer? customer = null;
if (!string.IsNullOrEmpty(userId))
{
customer = await _customerRepository.GetByUserIdAsync(userId);
if (customer == null) return (false, null, "Kundenprofil nicht gefunden."); 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); 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 (false, null, "Ungültige oder inaktive Versandmethode.");
var paymentMethod = await _context.PaymentMethods.FindAsync(orderDto.PaymentMethodId); 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 (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 --- // --- 2. Artikel verarbeiten und Lagerbestand prüfen ---
var orderItems = new List<OrderItem>(); var orderItems = new List<OrderItem>();
var productIds = orderDto.Items.Select(i => i.ProductId).ToList();
var products = await _context.Products.Where(p => productIds.Contains(p.Id)).ToListAsync();
decimal itemsTotal = 0; decimal itemsTotal = 0;
foreach (var itemDto in orderDto.Items) 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) if (product == null || !product.IsActive)
{ {
await transaction.RollbackAsync(); 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) if (product.StockQuantity < itemDto.Quantity)
{ {
await transaction.RollbackAsync(); 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; // Lagerbestand reduzieren
product.StockQuantity -= itemDto.Quantity;
var orderItem = new OrderItem var orderItem = new OrderItem
{ {
@@ -95,13 +106,13 @@ namespace Webshop.Application.Services.Customers
UnitPrice = product.Price, UnitPrice = product.Price,
TotalPrice = product.Price * itemDto.Quantity TotalPrice = product.Price * itemDto.Quantity
}; };
orderItems.Add(orderItem); orderItems.Add(orderItem);
itemsTotal += orderItem.TotalPrice; itemsTotal += orderItem.TotalPrice;
} }
// --- 3. Preise, Rabatte, Steuern und Gesamtbetrag berechnen --- // --- 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 shippingCost = shippingMethod.BaseCost;
decimal subTotal = itemsTotal + shippingCost - discountAmount; decimal subTotal = itemsTotal + shippingCost - discountAmount;
if (subTotal < 0) subTotal = 0; if (subTotal < 0) subTotal = 0;
@@ -113,47 +124,53 @@ namespace Webshop.Application.Services.Customers
// --- 4. Bestellung erstellen --- // --- 4. Bestellung erstellen ---
var newOrder = new Order 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)}", OrderNumber = $"WS-{DateTime.UtcNow:yyyyMMdd}-{new Random().Next(1000, 9999)}",
OrderDate = DateTimeOffset.UtcNow, OrderDate = DateTimeOffset.UtcNow,
OrderStatus = OrderStatus.Pending.ToString(), OrderStatus = OrderStatus.Pending.ToString(),
PaymentStatus = PaymentStatus.Pending.ToString(), // Wird später vom Zahlungsanbieter aktualisiert PaymentStatus = PaymentStatus.Pending.ToString(),
OrderTotal = orderTotal, OrderTotal = orderTotal,
ShippingCost = shippingCost, ShippingCost = shippingCost,
TaxAmount = taxAmount, TaxAmount = taxAmount,
DiscountAmount = discountAmount, DiscountAmount = discountAmount,
PaymentMethod = paymentMethod.Name, PaymentMethod = paymentMethod.Name,
PaymentMethodId = paymentMethod.Id, PaymentMethodId = paymentMethod.Id,
ShippingMethodId = shippingMethod.Id, ShippingMethodId = shippingMethod.Id,
BillingAddressId = orderDto.BillingAddressId, BillingAddressId = orderDto.BillingAddressId,
ShippingAddressId = orderDto.ShippingAddressId, ShippingAddressId = orderDto.ShippingAddressId,
OrderItems = orderItems OrderItems = orderItems
}; };
await _orderRepository.AddAsync(newOrder);
await _orderRepository.AddAsync(newOrder); // Speichert die Bestellung und die Artikel // --- 5. Rabattnutzung erhöhen ---
await _context.SaveChangesAsync(); // Speichert die Lagerbestandsänderungen 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 await _context.SaveChangesAsync(); // Speichert Lagerbestandsänderungen & Rabattnutzung
await transaction.CommitAsync();
// --- 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
// --- 6. Erfolgreiche Antwort erstellen ---
var createdOrder = await _orderRepository.GetByIdAsync(newOrder.Id);
var orderDetailDto = MapToOrderDetailDto(createdOrder!);
return (true, orderDetailDto, null); return (true, orderDetailDto, null);
} }
catch (Exception ex) catch (Exception ex)
{ {
await transaction.RollbackAsync(); await transaction.RollbackAsync();
// Hier Fehler loggen
return (false, null, $"Ein unerwarteter Fehler ist aufgetreten: {ex.Message}"); 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) private OrderDetailDto MapToOrderDetailDto(Order order)
{ {
return new OrderDetailDto return new OrderDetailDto
@@ -164,10 +181,35 @@ namespace Webshop.Application.Services.Customers
CustomerId = order.CustomerId, CustomerId = order.CustomerId,
Status = Enum.Parse<OrderStatus>(order.OrderStatus), Status = Enum.Parse<OrderStatus>(order.OrderStatus),
TotalAmount = order.OrderTotal, TotalAmount = order.OrderTotal,
ShippingAddress = new AddressDto { /* Mapping */ }, ShippingAddress = new AddressDto
BillingAddress = new AddressDto { /* Mapping */ }, {
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, PaymentMethod = order.PaymentMethod,
PaymentStatus = Enum.Parse<PaymentStatus>(order.PaymentStatus), PaymentStatus = Enum.Parse<PaymentStatus>(order.PaymentStatus),
ShippingTrackingNumber = order.ShippingTrackingNumber,
ShippedDate = order.ShippedDate,
DeliveredDate = order.DeliveredDate,
OrderItems = order.OrderItems.Select(oi => new OrderItemDto OrderItems = order.OrderItems.Select(oi => new OrderItemDto
{ {
Id = oi.Id, Id = oi.Id,

View File

@@ -6,85 +6,113 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Webshop.Application.Services.Public.Interfaces; using Webshop.Application.Services.Public.Interfaces;
using Webshop.Domain.Entities; using Webshop.Domain.Entities;
using Webshop.Domain.Interfaces; using Webshop.Domain.Enums;
using Webshop.Infrastructure.Data; using Webshop.Infrastructure.Data;
namespace Webshop.Application.Services.Public namespace Webshop.Application.Services.Public
{ {
public class DiscountService : IDiscountService public class DiscountService : IDiscountService
{ {
private readonly ApplicationDbContext _context; // Direkter Zugriff auf DbSets private readonly ApplicationDbContext _context;
private readonly IDiscountRepository _discountRepository;
private readonly IProductRepository _productRepository;
public DiscountService( public DiscountService(ApplicationDbContext context)
ApplicationDbContext context,
IDiscountRepository discountRepository,
IProductRepository productRepository)
{ {
_context = context; _context = context;
_discountRepository = discountRepository;
_productRepository = productRepository;
} }
public async Task<decimal> CalculateDiscountAsync(List<OrderItem> orderItems, string? couponCode) public async Task<DiscountCalculationResult> CalculateDiscountAsync(List<OrderItem> 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 // 1. Lade alle potenziell anwendbaren Rabatte
var discounts = await _context.Discounts var query = _context.Discounts
.Include(d => d.ProductDiscounts) .Include(d => d.ProductDiscounts)
.Include(d => d.categorieDiscounts) .Include(d => d.categorieDiscounts)
.Where(d => d.IsActive && d.StartDate <= DateTimeOffset.UtcNow && (!d.EndDate.HasValue || d.EndDate >= DateTimeOffset.UtcNow)) .Where(d => d.IsActive && d.StartDate <= now &&
.ToListAsync(); (!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)) 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 else
{ {
// Rabatte ohne Gutscheincode, falls zutreffend // Lade die Kategorie-Zuweisungen aller Produkte im Warenkorb
discounts = discounts.Where(d => !d.RequiresCouponCode).ToList(); 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 decimal applicableItemsTotal = 0;
foreach (var item in orderItems) foreach (var item in orderItems)
{ {
decimal itemDiscount = 0; if (!item.ProductId.HasValue)
// 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); continue;
// Hier müsste die Logik für FixedAmount-Rabatte hin
} }
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) result.TotalDiscountAmount = discountAmount;
var cartDiscounts = discounts.Where(d => !d.ProductDiscounts.Any() && !d.categorieDiscounts.Any()).ToList(); result.AppliedDiscountIds.Add(bestDiscount.Id);
decimal cartTotal = orderItems.Sum(i => i.TotalPrice); return result;
foreach (var discount in cartDiscounts) }
// Private Helper-Methode zur Berechnung des Rabattwertes
private decimal CalculateAmount(Discount discount, decimal baseAmount)
{ {
if (discount.MinimumOrderAmount.HasValue && cartTotal < discount.MinimumOrderAmount.Value) continue; if (discount.DiscountType == DiscountType.FixedAmount.ToString())
{
// Beispiel: Prozentualer Rabatt auf den Warenkorb // Stelle sicher, dass der Rabatt nicht höher als der Betrag ist
return Math.Min(baseAmount, discount.DiscountValue);
}
if (discount.DiscountType == DiscountType.Percentage.ToString()) if (discount.DiscountType == DiscountType.Percentage.ToString())
{ {
totalDiscount += cartTotal * (discount.DiscountValue / 100); return baseAmount * (discount.DiscountValue / 100m);
} }
} return 0;
return totalDiscount;
} }
} }
} }

View File

@@ -6,8 +6,24 @@ using Webshop.Domain.Entities;
namespace Webshop.Application.Services.Public.Interfaces namespace Webshop.Application.Services.Public.Interfaces
{ {
/// <summary>
/// Stellt das Ergebnis der Rabattberechnung dar.
/// </summary>
public class DiscountCalculationResult
{
/// <summary>
/// Der gesamte berechnete Rabattbetrag.
/// </summary>
public decimal TotalDiscountAmount { get; set; } = 0;
/// <summary>
/// Die IDs der Rabatte, die erfolgreich angewendet wurden.
/// </summary>
public List<Guid> AppliedDiscountIds { get; set; } = new List<Guid>();
}
public interface IDiscountService public interface IDiscountService
{ {
Task<decimal> CalculateDiscountAsync(List<OrderItem> orderItems, string? couponCode); Task<DiscountCalculationResult> CalculateDiscountAsync(List<OrderItem> orderItems, string? couponCode);
} }
} }