diff --git a/Webshop.Api/Controllers/Admin/AdminDiscountsController.cs b/Webshop.Api/Controllers/Admin/AdminDiscountsController.cs index 13051aa..e9d9009 100644 --- a/Webshop.Api/Controllers/Admin/AdminDiscountsController.cs +++ b/Webshop.Api/Controllers/Admin/AdminDiscountsController.cs @@ -1,5 +1,6 @@ // src/Webshop.Api/Controllers/Admin/AdminDiscountsController.cs using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; @@ -29,10 +30,11 @@ namespace Webshop.Api.Controllers.Admin /// Ruft eine Liste aller konfigurierten Rabatte ab. /// [HttpGet] - public async Task>> GetAllDiscounts() + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task GetAllDiscounts() { - var discounts = await _adminDiscountService.GetAllDiscountsAsync(); - return Ok(discounts); + var result = await _adminDiscountService.GetAllDiscountsAsync(); + return Ok(result.Value); } /// @@ -40,11 +42,18 @@ namespace Webshop.Api.Controllers.Admin /// /// Die ID des Rabatts. [HttpGet("{id}")] - public async Task> GetDiscountById(Guid id) + [ProducesResponseType(typeof(DiscountDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task GetDiscountById(Guid id) { - var discount = await _adminDiscountService.GetDiscountByIdAsync(id); - if (discount == null) return NotFound(); - return Ok(discount); + var result = await _adminDiscountService.GetDiscountByIdAsync(id); + + return result.Type switch + { + ServiceResultType.Success => Ok(result.Value), + ServiceResultType.NotFound => NotFound(new { Message = result.ErrorMessage }), + _ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = result.ErrorMessage ?? "Ein unerwarteter Fehler ist aufgetreten." }) + }; } /// @@ -60,19 +69,24 @@ 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) + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] + public async Task CreateDiscount([FromBody] DiscountDto discountDto) { - if (!ModelState.IsValid) return BadRequest(ModelState); + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } var result = await _adminDiscountService.CreateDiscountAsync(discountDto); - if (result.Type == ServiceResultType.Success) + return result.Type switch { - return CreatedAtAction(nameof(GetDiscountById), new { id = result.Value!.Id }, result.Value); - } - - return BadRequest(new { Message = result.ErrorMessage }); + ServiceResultType.Success => CreatedAtAction(nameof(GetDiscountById), new { id = result.Value!.Id }, result.Value), + ServiceResultType.Conflict => Conflict(new { Message = result.ErrorMessage }), + ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }), + _ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = result.ErrorMessage ?? "Ein unerwarteter Fehler ist aufgetreten." }) + }; } /// @@ -82,12 +96,19 @@ namespace Webshop.Api.Controllers.Admin /// Die neuen Daten für den Rabatt. [HttpPut("{id}")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] public async Task UpdateDiscount(Guid id, [FromBody] DiscountDto discountDto) { - if (id != discountDto.Id) return BadRequest("ID in URL und Body stimmen nicht überein."); - if (!ModelState.IsValid) return BadRequest(ModelState); + if (id != discountDto.Id) + { + return BadRequest(new { Message = "ID in der URL und im Body stimmen nicht überein." }); + } + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } var result = await _adminDiscountService.UpdateDiscountAsync(discountDto); @@ -95,8 +116,9 @@ namespace Webshop.Api.Controllers.Admin { ServiceResultType.Success => NoContent(), ServiceResultType.NotFound => NotFound(new { Message = result.ErrorMessage }), + ServiceResultType.Conflict => Conflict(new { Message = result.ErrorMessage }), ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }), - _ => StatusCode(StatusCodes.Status500InternalServerError, "Ein unerwarteter Fehler ist aufgetreten.") + _ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = result.ErrorMessage ?? "Ein unerwarteter Fehler ist aufgetreten." }) }; } @@ -106,7 +128,7 @@ namespace Webshop.Api.Controllers.Admin /// Die ID des zu löschenden Rabatts. [HttpDelete("{id}")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task DeleteDiscount(Guid id) { var result = await _adminDiscountService.DeleteDiscountAsync(id); @@ -115,7 +137,7 @@ namespace Webshop.Api.Controllers.Admin { ServiceResultType.Success => NoContent(), ServiceResultType.NotFound => NotFound(new { Message = result.ErrorMessage }), - _ => StatusCode(StatusCodes.Status500InternalServerError, "Ein unerwarteter Fehler ist aufgetreten.") + _ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = result.ErrorMessage ?? "Ein unerwarteter Fehler ist aufgetreten." }) }; } } diff --git a/Webshop.Application/Services/Admin/AdminDiscountService.cs b/Webshop.Application/Services/Admin/AdminDiscountService.cs index 2c34b84..3854cf6 100644 --- a/Webshop.Application/Services/Admin/AdminDiscountService.cs +++ b/Webshop.Application/Services/Admin/AdminDiscountService.cs @@ -4,47 +4,49 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Webshop.Application; using Webshop.Application.DTOs.Discounts; 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, ApplicationDbContext context) + public AdminDiscountService(IDiscountRepository discountRepository) { _discountRepository = discountRepository; - _context = context; } - public async Task> GetAllDiscountsAsync() + public async Task>> GetAllDiscountsAsync() { var discounts = await _discountRepository.GetAllAsync(); - return discounts.Select(MapToDto).ToList(); + var dtos = discounts.Select(MapToDto).ToList(); + return ServiceResult.Ok>(dtos); } - public async Task GetDiscountByIdAsync(Guid id) + public async Task> GetDiscountByIdAsync(Guid id) { var discount = await _discountRepository.GetByIdAsync(id); - return discount != null ? MapToDto(discount) : null; + if (discount == null) + { + return ServiceResult.Fail(ServiceResultType.NotFound, $"Rabatt mit ID '{id}' nicht gefunden."); + } + return ServiceResult.Ok(MapToDto(discount)); } 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); + var existing = await _discountRepository.GetByCouponCodeAsync(discountDto.CouponCode); if (existing != null) { - return ServiceResult.Fail(ServiceResultType.InvalidInput, $"Der Gutscheincode '{discountDto.CouponCode}' existiert bereits."); + return ServiceResult.Fail(ServiceResultType.Conflict, $"Der Gutscheincode '{discountDto.CouponCode}' existiert bereits."); } } @@ -60,15 +62,15 @@ namespace Webshop.Application.Services.Admin var existingDiscount = await _discountRepository.GetByIdAsync(discountDto.Id); if (existingDiscount == null) { - return ServiceResult.Fail(ServiceResultType.NotFound, "Rabatt nicht gefunden."); + return ServiceResult.Fail(ServiceResultType.NotFound, $"Rabatt mit ID '{discountDto.Id}' 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) + var existing = await _discountRepository.GetByCouponCodeAsync(discountDto.CouponCode); + if (existing != null && existing.Id != discountDto.Id) { - return ServiceResult.Fail(ServiceResultType.InvalidInput, $"Der Gutscheincode '{discountDto.CouponCode}' wird bereits von einem anderen Rabatt verwendet."); + return ServiceResult.Fail(ServiceResultType.Conflict, $"Der Gutscheincode '{discountDto.CouponCode}' wird bereits von einem anderen Rabatt verwendet."); } } @@ -81,25 +83,29 @@ namespace Webshop.Application.Services.Admin existingDiscount.EndDate = discountDto.EndDate; existingDiscount.IsActive = discountDto.IsActive; existingDiscount.RequiresCouponCode = discountDto.RequiresCouponCode; - existingDiscount.CouponCode = discountDto.CouponCode; + existingDiscount.CouponCode = discountDto.RequiresCouponCode ? discountDto.CouponCode : null; 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) + if (discountDto.AssignedProductIds != null) { - existingDiscount.ProductDiscounts.Add(new ProductDiscount { DiscountId = existingDiscount.Id, ProductId = productId }); + foreach (var productId in discountDto.AssignedProductIds) + { + existingDiscount.ProductDiscounts.Add(new ProductDiscount { DiscountId = existingDiscount.Id, ProductId = productId }); + } } // Sync assigned categories existingDiscount.categorieDiscounts.Clear(); - foreach (var categoryId in discountDto.AssignedCategoryIds) + if (discountDto.AssignedCategoryIds != null) { - existingDiscount.categorieDiscounts.Add(new CategorieDiscount { DiscountId = existingDiscount.Id, categorieId = categoryId }); + foreach (var categoryId in discountDto.AssignedCategoryIds) + { + existingDiscount.categorieDiscounts.Add(new CategorieDiscount { DiscountId = existingDiscount.Id, categorieId = categoryId }); + } } - // << ENDE DES WICHTIGEN TEILS >> await _discountRepository.UpdateAsync(existingDiscount); return ServiceResult.Ok(); @@ -110,7 +116,7 @@ namespace Webshop.Application.Services.Admin var discount = await _discountRepository.GetByIdAsync(id); if (discount == null) { - return ServiceResult.Fail(ServiceResultType.NotFound, "Rabatt nicht gefunden."); + return ServiceResult.Fail(ServiceResultType.NotFound, $"Rabatt mit ID '{id}' nicht gefunden."); } await _discountRepository.DeleteAsync(id); @@ -154,7 +160,7 @@ namespace Webshop.Application.Services.Admin EndDate = dto.EndDate, IsActive = dto.IsActive, RequiresCouponCode = dto.RequiresCouponCode, - CouponCode = dto.CouponCode, + CouponCode = dto.RequiresCouponCode ? dto.CouponCode : null, MinimumOrderAmount = dto.MinimumOrderAmount, MaximumUsageCount = dto.MaximumUsageCount, CurrentUsageCount = dto.CurrentUsageCount, diff --git a/Webshop.Application/Services/Admin/Interfaces/IAdminDiscountService.cs b/Webshop.Application/Services/Admin/Interfaces/IAdminDiscountService.cs index 18cb6cd..fd4cd8b 100644 --- a/Webshop.Application/Services/Admin/Interfaces/IAdminDiscountService.cs +++ b/Webshop.Application/Services/Admin/Interfaces/IAdminDiscountService.cs @@ -2,15 +2,15 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Webshop.Application; // << NEU: Für ServiceResult >> +using Webshop.Application; using Webshop.Application.DTOs.Discounts; namespace Webshop.Application.Services.Admin.Interfaces { public interface IAdminDiscountService { - Task> GetAllDiscountsAsync(); - Task GetDiscountByIdAsync(Guid id); + Task>> GetAllDiscountsAsync(); + Task> GetDiscountByIdAsync(Guid id); Task> CreateDiscountAsync(DiscountDto discountDto); Task UpdateDiscountAsync(DiscountDto discountDto); Task DeleteDiscountAsync(Guid id); diff --git a/Webshop.Domain/Interfaces/IDiscountRepository.cs b/Webshop.Domain/Interfaces/IDiscountRepository.cs index f3f3dd0..9e83615 100644 --- a/Webshop.Domain/Interfaces/IDiscountRepository.cs +++ b/Webshop.Domain/Interfaces/IDiscountRepository.cs @@ -10,6 +10,7 @@ namespace Webshop.Domain.Interfaces { Task> GetAllAsync(); Task GetByIdAsync(Guid id); + Task GetByCouponCodeAsync(string couponCode); // << NEUE METHODE HINZUFÜGEN Task AddAsync(Discount discount); Task UpdateAsync(Discount discount); Task DeleteAsync(Guid id); diff --git a/Webshop.Infrastructure/Repositories/DiscountRepository.cs b/Webshop.Infrastructure/Repositories/DiscountRepository.cs index bae5945..04f0ac9 100644 --- a/Webshop.Infrastructure/Repositories/DiscountRepository.cs +++ b/Webshop.Infrastructure/Repositories/DiscountRepository.cs @@ -35,6 +35,17 @@ namespace Webshop.Infrastructure.Repositories .FirstOrDefaultAsync(d => d.Id == id); } + // << NEUE METHODE IMPLEMENTIEREN >> + public async Task GetByCouponCodeAsync(string couponCode) + { + // Case-insensitive Suche für den Gutscheincode + return await _context.Discounts + .Include(d => d.ProductDiscounts) + .Include(d => d.categorieDiscounts) + .FirstOrDefaultAsync(d => d.CouponCode != null && d.CouponCode.ToUpper() == couponCode.ToUpper()); + } + // << ENDE DER NEUEN METHODE >> + public async Task AddAsync(Discount discount) { await _context.Discounts.AddAsync(discount); @@ -43,8 +54,31 @@ namespace Webshop.Infrastructure.Repositories public async Task UpdateAsync(Discount discount) { - _context.Discounts.Update(discount); - await _context.SaveChangesAsync(); + // Sicherstellen, dass die verknüpften Entitäten nicht neu hinzugefügt werden + var existingDiscount = await _context.Discounts + .Include(d => d.ProductDiscounts) + .Include(d => d.categorieDiscounts) + .FirstOrDefaultAsync(d => d.Id == discount.Id); + + if (existingDiscount != null) + { + _context.Entry(existingDiscount).CurrentValues.SetValues(discount); + + // Manuelles Synchronisieren der Many-to-Many-Beziehungen + existingDiscount.ProductDiscounts.Clear(); + foreach (var pd in discount.ProductDiscounts) + { + existingDiscount.ProductDiscounts.Add(pd); + } + + existingDiscount.categorieDiscounts.Clear(); + foreach (var cd in discount.categorieDiscounts) + { + existingDiscount.categorieDiscounts.Add(cd); + } + + await _context.SaveChangesAsync(); + } } public async Task DeleteAsync(Guid id)