discounts überarbeitet
This commit is contained in:
@@ -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.")
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,16 +40,33 @@ 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 (customer == null) return (false, null, "Kundenprofil nicht gefunden.");
|
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);
|
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.");
|
||||||
@@ -57,33 +74,27 @@ namespace Webshop.Application.Services.Customers
|
|||||||
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,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
// 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);
|
if (!item.ProductId.HasValue)
|
||||||
// Hier müsste die Logik für FixedAmount-Rabatte hin
|
{
|
||||||
}
|
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)
|
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.DiscountType == DiscountType.FixedAmount.ToString())
|
||||||
{
|
{
|
||||||
if (discount.MinimumOrderAmount.HasValue && cartTotal < discount.MinimumOrderAmount.Value) continue;
|
// Stelle sicher, dass der Rabatt nicht höher als der Betrag ist
|
||||||
|
return Math.Min(baseAmount, discount.DiscountValue);
|
||||||
// Beispiel: Prozentualer Rabatt auf den Warenkorb
|
|
||||||
if (discount.DiscountType == DiscountType.Percentage.ToString())
|
|
||||||
{
|
|
||||||
totalDiscount += cartTotal * (discount.DiscountValue / 100);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (discount.DiscountType == DiscountType.Percentage.ToString())
|
||||||
return totalDiscount;
|
{
|
||||||
|
return baseAmount * (discount.DiscountValue / 100m);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user