1 Commits
test ... main

Author SHA1 Message Date
Tizian.Breuch
03993211f6 new git
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 1m8s
2025-10-23 14:45:24 +02:00
52 changed files with 299 additions and 12549 deletions

View File

@@ -77,12 +77,15 @@ namespace Webshop.Api.Controllers.Admin
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
public async Task<IActionResult> UpdateAdminProduct(Guid id, [FromForm] UpdateAdminProductDto productDto) public async Task<IActionResult> UpdateAdminProduct(Guid id, [FromForm] UpdateAdminProductDto productDto)
{ {
// ============================================================================== if (id != productDto.Id)
// DEINE PERFEKTE L<>SUNG: URL-ID erzwingen {
// ============================================================================== return BadRequest(new { Message = "ID in der URL und im Body stimmen nicht <20>berein." });
productDto.Id = id; }
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Jetzt den Service mit dem garantiert korrekten DTO aufrufen.
var result = await _adminProductService.UpdateAdminProductAsync(productDto); var result = await _adminProductService.UpdateAdminProductAsync(productDto);
return result.Type switch return result.Type switch
@@ -90,7 +93,8 @@ namespace Webshop.Api.Controllers.Admin
ServiceResultType.Success => NoContent(), ServiceResultType.Success => NoContent(),
ServiceResultType.NotFound => NotFound(new { Message = result.ErrorMessage }), ServiceResultType.NotFound => NotFound(new { Message = result.ErrorMessage }),
ServiceResultType.Conflict => Conflict(new { Message = result.ErrorMessage }), ServiceResultType.Conflict => Conflict(new { Message = result.ErrorMessage }),
_ => BadRequest(new { Message = result.ErrorMessage ?? "Ein Fehler ist aufgetreten." }) ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }),
_ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = result.ErrorMessage ?? "Ein unerwarteter Fehler ist aufgetreten." })
}; };
} }

View File

@@ -1,54 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Webshop.Application;
using Webshop.Application.DTOs.Customers; // Für CartDto
using Webshop.Application.DTOs.Shipping; // Für CartItemDto
using Webshop.Application.Services.Customers.Interfaces;
namespace Webshop.Api.Controllers.Customer
{
[ApiController]
[Route("api/v1/customer/[controller]")]
[Authorize(Roles = "Customer")]
public class CartController : ControllerBase // <--- WICHTIG: Muss public sein und erben
{
private readonly ICartService _cartService;
public CartController(ICartService cartService)
{
_cartService = cartService;
}
[HttpGet]
[ProducesResponseType(typeof(CartDto), StatusCodes.Status200OK)]
public async Task<IActionResult> GetCart()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var result = await _cartService.GetCartAsync(userId!);
return Ok(result.Value);
}
[HttpPost("items")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> AddToCart([FromBody] CartItemDto item)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var result = await _cartService.AddToCartAsync(userId!, item);
return result.Type == ServiceResultType.Success ? Ok() : BadRequest(new { Message = result.ErrorMessage });
}
[HttpDelete("items/{productId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> RemoveItem(Guid productId)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
await _cartService.RemoveFromCartAsync(userId!, productId);
return NoContent();
}
}
}

View File

@@ -4,13 +4,9 @@ using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Webshop.Application.Services.Customers.Interfaces; using Webshop.Application.Services.Customers.Interfaces;
using Webshop.Application.DTOs.Orders; using Webshop.Application.DTOs.Orders;
using Webshop.Application.DTOs.Shipping; // Neu
using System.Security.Claims; using System.Security.Claims;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Webshop.Application; using Webshop.Application;
using System.Collections.Generic;
using Webshop.Application.Services.Customers;
using Microsoft.AspNetCore.Cors.Infrastructure;
namespace Webshop.Api.Controllers.Customer namespace Webshop.Api.Controllers.Customer
{ {
@@ -20,41 +16,17 @@ namespace Webshop.Api.Controllers.Customer
public class CheckoutController : ControllerBase public class CheckoutController : ControllerBase
{ {
private readonly ICheckoutService _checkoutService; private readonly ICheckoutService _checkoutService;
private readonly ICartService _cartService;
public CheckoutController(ICheckoutService checkoutService , ICartService cartService) public CheckoutController(ICheckoutService checkoutService)
{ {
_checkoutService = checkoutService; _checkoutService = checkoutService;
_cartService = cartService;
}
[HttpGet("available-shipping-methods")] // War vorher POST
[ProducesResponseType(typeof(IEnumerable<ShippingMethodDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetAvailableShippingMethods()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
// 1. Warenkorb laden
var cartResult = await _cartService.GetCartAsync(userId!);
if (cartResult.Value == null || !cartResult.Value.Items.Any())
{
return Ok(new List<ShippingMethodDto>()); // Leerer Korb -> keine Methoden
}
// 2. Berechnung aufrufen (nutzt die Overload Methode mit List<CartItemDto>)
var result = await _checkoutService.GetCompatibleShippingMethodsAsync(cartResult.Value.Items);
return result.Type switch
{
ServiceResultType.Success => Ok(result.Value),
_ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = "Fehler." })
};
} }
[HttpPost("create-order")] [HttpPost("create-order")]
[ProducesResponseType(typeof(OrderDetailDto), StatusCodes.Status201Created)] [ProducesResponseType(typeof(OrderDetailDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderDto orderDto) public async Task<IActionResult> CreateOrder([FromBody] CreateOrderDto orderDto)
{ {
@@ -63,6 +35,7 @@ namespace Webshop.Api.Controllers.Customer
return BadRequest(ModelState); return BadRequest(ModelState);
} }
// UserId aus dem JWT-Token des eingeloggten Kunden extrahieren
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrEmpty(userId)) if (string.IsNullOrEmpty(userId))
{ {
@@ -77,7 +50,7 @@ namespace Webshop.Api.Controllers.Customer
ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }), ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }),
ServiceResultType.Conflict => Conflict(new { Message = result.ErrorMessage }), ServiceResultType.Conflict => Conflict(new { Message = result.ErrorMessage }),
ServiceResultType.Unauthorized => Unauthorized(new { Message = result.ErrorMessage }), ServiceResultType.Unauthorized => Unauthorized(new { Message = result.ErrorMessage }),
ServiceResultType.Forbidden => Forbid(), ServiceResultType.Forbidden => Forbid(), // Forbid returns 403 without a body
_ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = result.ErrorMessage ?? "Ein unerwarteter Fehler ist aufgetreten." }) _ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = result.ErrorMessage ?? "Ein unerwarteter Fehler ist aufgetreten." })
}; };
} }

View File

@@ -10,7 +10,6 @@ using Webshop.Application.DTOs.Auth;
using Webshop.Application.DTOs.Customers; using Webshop.Application.DTOs.Customers;
using Webshop.Application.DTOs.Email; using Webshop.Application.DTOs.Email;
using Webshop.Application.Services.Customers; using Webshop.Application.Services.Customers;
using Webshop.Application.Services.Customers.Interfaces;
namespace Webshop.Api.Controllers.Customer namespace Webshop.Api.Controllers.Customer
{ {

View File

@@ -12,7 +12,6 @@ using System.Text.Json.Serialization;
// --- Eigene Namespaces --- // --- Eigene Namespaces ---
using Webshop.Api.SwaggerFilters; using Webshop.Api.SwaggerFilters;
using Webshop.Application.DTOs.Email;
using Webshop.Application.Services.Admin; using Webshop.Application.Services.Admin;
using Webshop.Application.Services.Admin.Interfaces; using Webshop.Application.Services.Admin.Interfaces;
using Webshop.Application.Services.Auth; using Webshop.Application.Services.Auth;
@@ -27,7 +26,6 @@ using Webshop.Infrastructure.Data;
using Webshop.Infrastructure.Repositories; using Webshop.Infrastructure.Repositories;
using Webshop.Infrastructure.Services; using Webshop.Infrastructure.Services;
var options = new WebApplicationOptions var options = new WebApplicationOptions
{ {
Args = args, Args = args,
@@ -113,9 +111,8 @@ builder.Services.Configure<ResendClientOptions>(options =>
options.ApiToken = builder.Configuration["Resend:ApiToken"]!; options.ApiToken = builder.Configuration["Resend:ApiToken"]!;
}); });
builder.Services.AddTransient<IResend, ResendClient>(); builder.Services.AddTransient<IResend, ResendClient>();
builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));
// --- Repositories --- // Repositories
builder.Services.AddScoped<IProductRepository, ProductRepository>(); builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<ISupplierRepository, SupplierRepository>(); builder.Services.AddScoped<ISupplierRepository, SupplierRepository>();
builder.Services.AddScoped<ICustomerRepository, CustomerRepository>(); builder.Services.AddScoped<ICustomerRepository, CustomerRepository>();
@@ -124,32 +121,17 @@ builder.Services.AddScoped<ICategorieRepository, CategorieRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>(); builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IShippingMethodRepository, ShippingMethodRepository>(); builder.Services.AddScoped<IShippingMethodRepository, ShippingMethodRepository>();
builder.Services.AddScoped<IAddressRepository, AddressRepository>(); builder.Services.AddScoped<IAddressRepository, AddressRepository>();
builder.Services.AddScoped<IDiscountRepository, DiscountRepository>(); // War doppelt builder.Services.AddScoped<IDiscountRepository, DiscountRepository>();
builder.Services.AddScoped<ISettingRepository, SettingRepository>(); builder.Services.AddScoped<ISettingRepository, SettingRepository>();
builder.Services.AddScoped<IShopInfoRepository, ShopInfoRepository>(); builder.Services.AddScoped<IShopInfoRepository, ShopInfoRepository>();
builder.Services.AddScoped<IDiscountRepository, DiscountRepository>();
builder.Services.AddScoped<IReviewRepository, ReviewRepository>(); builder.Services.AddScoped<IReviewRepository, ReviewRepository>();
// --- Services --- // Services
// Public & Core
builder.Services.AddScoped<IAuthService, AuthService>(); builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<IProductService, ProductService>(); builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<ICategorieService, CategorieService>();
builder.Services.AddScoped<IPaymentMethodService, PaymentMethodService>(); builder.Services.AddScoped<IPaymentMethodService, PaymentMethodService>();
builder.Services.AddScoped<IShopInfoService, ShopInfoService>(); builder.Services.AddScoped<ICategorieService, CategorieService>();
builder.Services.AddScoped<ISettingService, SettingService>();
builder.Services.AddScoped<IDiscountService, DiscountService>();
builder.Services.AddScoped<IReviewService, ReviewService>();
builder.Services.AddScoped<IEmailService, EmailService>(); // WICHTIG: F<>r Auth & Checkout
// Customer Scope
builder.Services.AddScoped<ICustomerService, CustomerService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IAddressService, AddressService>();
builder.Services.AddScoped<ICheckoutService, CheckoutService>(); // War doppelt
builder.Services.AddScoped<ICartService, CartService>(); // WICHTIG: F<>r Checkout Cleanup
builder.Services.AddScoped<ICustomerReviewService, CustomerReviewService>();
// Admin Scope
builder.Services.AddScoped<IAdminUserService, AdminUserService>(); builder.Services.AddScoped<IAdminUserService, AdminUserService>();
builder.Services.AddScoped<IAdminProductService, AdminProductService>(); builder.Services.AddScoped<IAdminProductService, AdminProductService>();
builder.Services.AddScoped<IAdminSupplierService, AdminSupplierService>(); builder.Services.AddScoped<IAdminSupplierService, AdminSupplierService>();
@@ -157,9 +139,21 @@ builder.Services.AddScoped<IAdminPaymentMethodService, AdminPaymentMethodService
builder.Services.AddScoped<IAdminCategorieService, AdminCategorieService>(); builder.Services.AddScoped<IAdminCategorieService, AdminCategorieService>();
builder.Services.AddScoped<IAdminOrderService, AdminOrderService>(); builder.Services.AddScoped<IAdminOrderService, AdminOrderService>();
builder.Services.AddScoped<IAdminShippingMethodService, AdminShippingMethodService>(); builder.Services.AddScoped<IAdminShippingMethodService, AdminShippingMethodService>();
builder.Services.AddScoped<IAdminDiscountService, AdminDiscountService>(); // War doppelt builder.Services.AddScoped<IAdminDiscountService, AdminDiscountService>();
builder.Services.AddScoped<IAdminSettingService, AdminSettingService>(); // War doppelt builder.Services.AddScoped<IAdminSettingService, AdminSettingService>();
builder.Services.AddScoped<ICustomerService, CustomerService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IAddressService, AddressService>();
builder.Services.AddScoped<ICheckoutService, CheckoutService>();
builder.Services.AddScoped<IAdminShopInfoService, AdminShopInfoService>(); builder.Services.AddScoped<IAdminShopInfoService, AdminShopInfoService>();
builder.Services.AddScoped<IShopInfoService, ShopInfoService>();
builder.Services.AddScoped<ISettingService, SettingService>();
builder.Services.AddScoped<IAdminSettingService, AdminSettingService>();
builder.Services.AddScoped<IAdminDiscountService, AdminDiscountService>();
builder.Services.AddScoped<ICheckoutService, CheckoutService>();
builder.Services.AddScoped<IDiscountService, DiscountService>();
builder.Services.AddScoped<IReviewService, ReviewService>();
builder.Services.AddScoped<ICustomerReviewService, CustomerReviewService>();
builder.Services.AddScoped<IAdminReviewService, AdminReviewService>(); builder.Services.AddScoped<IAdminReviewService, AdminReviewService>();
builder.Services.AddScoped<IAdminAnalyticsService, AdminAnalyticsService>(); builder.Services.AddScoped<IAdminAnalyticsService, AdminAnalyticsService>();
builder.Services.AddScoped<IAdminAddressService, AdminAddressService>(); builder.Services.AddScoped<IAdminAddressService, AdminAddressService>();
@@ -169,8 +163,6 @@ builder.Services.AddControllers()
.AddJsonOptions(options => .AddJsonOptions(options =>
{ {
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
options.JsonSerializerOptions.DefaultIgnoreCondition =
System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;
}); });
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
@@ -217,7 +209,7 @@ builder.Services.AddSwaggerGen(c =>
c.SchemaFilter<AddExampleSchemaFilter>(); c.SchemaFilter<AddExampleSchemaFilter>();
c.OperationFilter<AuthorizeOperationFilter>(); c.OperationFilter<AuthorizeOperationFilter>();
c.OperationFilter<LoginExampleOperationFilter>(); c.OperationFilter<LoginExampleOperationFilter>();
//c.OperationFilter<PaymentMethodExampleOperationFilter>(); c.OperationFilter<PaymentMethodExampleOperationFilter>();
c.OperationFilter<SupplierExampleOperationFilter>(); c.OperationFilter<SupplierExampleOperationFilter>();
c.OperationFilter<ShippingMethodExampleOperationFilter>(); c.OperationFilter<ShippingMethodExampleOperationFilter>();
c.OperationFilter<AdminProductExampleOperationFilter>(); c.OperationFilter<AdminProductExampleOperationFilter>();
@@ -335,6 +327,6 @@ app.Run();
//new image build //new git

View File

@@ -16,19 +16,7 @@
"ExpirationMinutes": 120 "ExpirationMinutes": 120
}, },
"Resend": { "Resend": {
"ApiToken": "re_Zd6aFcvz_CqSa9Krs7WCHXjngDVLTYhGv", "ApiToken": "re_Zd6aFcvz_CqSa9Krs7WCHXjngDVLTYhGv"
"FromEmail": "onboarding@resend.dev"
}, },
"App": { "App:BaseUrl": "https://shopsolution-backend.tzbre.dev"
"BaseUrl": "https://shopsolution-backend.tzbre.dev",
"ClientUrl": "https://shopsolution-frontend.tzbre.dev",
"FrontendUrl": "https://shopsolution-frontend.tzbre.dev"
},
"ShopInfo": {
"Name": "Mein Webshop"
},
"EmailSettings": {
"SenderName": "Mein Webshop Support",
"SenderEmail": "noreply@shopsolution-frontend.tzbre.dev"
}
} }

View File

@@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using Webshop.Application.DTOs.Shipping; // Wichtig für CartItemDto
namespace Webshop.Application.DTOs.Customers
{
public class CartDto
{
public Guid Id { get; set; }
public string UserId { get; set; } = string.Empty;
public List<CartItemDto> Items { get; set; } = new List<CartItemDto>();
// Optional: Gesamtsumme zur Anzeige im Frontend
public decimal TotalPrice { get; set; }
}
}

View File

@@ -1,12 +0,0 @@
namespace Webshop.Application.DTOs.Email
{
public class EmailSettings
{
public string SmtpServer { get; set; } = "smtp.example.com";
public int Port { get; set; } = 587;
public string SenderName { get; set; } = "Mein Webshop";
public string SenderEmail { get; set; } = "noreply@meinwebshop.de";
public string Username { get; set; } = "user";
public string Password { get; set; } = "pass";
}
}

View File

@@ -1,27 +1,21 @@
// src/Webshop.Application/DTOs/Orders/CreateOrderDto.cs
using System; using System;
using System.ComponentModel.DataAnnotations; using System.Collections.Generic;
namespace Webshop.Application.DTOs.Orders namespace Webshop.Application.DTOs.Orders
{ {
public class CreateOrderDto public class CreateOrderDto
{ {
// Items Liste wurde ENTFERNT! public Guid? CustomerId { get; set; } // Nullable für Gastbestellung
public string? GuestEmail { get; set; }
[Required] public string? GuestPhoneNumber { get; set; }
public Guid ShippingAddressId { get; set; } public Guid ShippingAddressId { get; set; }
[Required]
public Guid BillingAddressId { get; set; } public Guid BillingAddressId { get; set; }
[Required]
public Guid PaymentMethodId { get; set; } public Guid PaymentMethodId { get; set; }
[Required]
public Guid ShippingMethodId { get; set; } public Guid ShippingMethodId { get; set; }
public string? CouponCode { get; set; } public string? CouponCode { get; set; } // << NEU >>
// Optional: Falls du Gastbestellungen ohne vorherigen DB-Cart erlauben willst, public List<CreateOrderItemDto> Items { get; set; } = new List<CreateOrderItemDto>();
// bräuchtest du eine separate Logik. Wir gehen hier vom Standard User-Flow aus.
} }
} }

View File

@@ -27,6 +27,5 @@ namespace Webshop.Application.DTOs.Products
// << NEU >> // << NEU >>
public bool IsFeatured { get; set; } public bool IsFeatured { get; set; }
public int FeaturedDisplayOrder { get; set; } public int FeaturedDisplayOrder { get; set; }
public byte[] RowVersion { get; set; }
} }
} }

View File

@@ -22,13 +22,12 @@ namespace Webshop.Application.DTOs.Products
[Required] [Required]
public string Slug { get; set; } public string Slug { get; set; }
// << ZURÜCK ZU IFormFile >>
public IFormFile? MainImageFile { get; set; } public IFormFile? MainImageFile { get; set; }
public List<IFormFile>? AdditionalImageFiles { get; set; } public List<IFormFile>? AdditionalImageFiles { get; set; }
public List<Guid>? ImagesToDelete { get; set; } public List<Guid>? ImagesToDelete { get; set; }
public Guid? SetMainImageId { get; set; } public List<Guid> CategorieIds { get; set; } = new List<Guid>();
public List<Guid>? CategorieIds { get; set; } = new List<Guid>();
public decimal? Weight { get; set; } public decimal? Weight { get; set; }
public decimal? OldPrice { get; set; } public decimal? OldPrice { get; set; }
@@ -36,7 +35,5 @@ namespace Webshop.Application.DTOs.Products
public decimal? PurchasePrice { get; set; } public decimal? PurchasePrice { get; set; }
public bool IsFeatured { get; set; } public bool IsFeatured { get; set; }
public int FeaturedDisplayOrder { get; set; } public int FeaturedDisplayOrder { get; set; }
public string? RowVersion { get; set; }
} }
} }

View File

@@ -1,13 +0,0 @@
using System;
namespace Webshop.Application.DTOs.Shipping
{
public class CartItemDto
{
public Guid ProductId { get; set; }
public int Quantity { get; set; }
// +++ DIESE ZEILE HAT GEFEHLT +++
public Guid? ProductVariantId { get; set; }
}
}

View File

@@ -1,13 +0,0 @@
// src/Webshop.Application/DTOs/Shipping/ShippingCalculationRequestDto.cs
using System;
using System.Collections.Generic;
namespace Webshop.Application.DTOs.Shipping
{
public class ShippingCalculationRequestDto
{
public List<CartItemDto> Items { get; set; } = new();
}
}

View File

@@ -1,4 +1,8 @@
// Auto-generiert von CreateWebshopFiles.ps1
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Webshop.Application.DTOs.Shipping namespace Webshop.Application.DTOs.Shipping
{ {
@@ -11,9 +15,5 @@ namespace Webshop.Application.DTOs.Shipping
public bool IsActive { get; set; } public bool IsActive { get; set; }
public int MinDeliveryDays { get; set; } public int MinDeliveryDays { get; set; }
public int MaxDeliveryDays { get; set; } public int MaxDeliveryDays { get; set; }
// NEU: Gewichtsgrenzen f<>r diese Methode
public decimal MinWeight { get; set; }
public decimal MaxWeight { get; set; }
} }
} }

View File

@@ -47,9 +47,7 @@ namespace Webshop.Application.Services.Admin
} }
string? imageUrl = null; string? imageUrl = null;
if (categorieDto.ImageFile != null)
// FIX: Pr<50>fung auf Length > 0 hinzugef<65>gt, wie im ProductService
if (categorieDto.ImageFile != null && categorieDto.ImageFile.Length > 0)
{ {
await using var stream = categorieDto.ImageFile.OpenReadStream(); await using var stream = categorieDto.ImageFile.OpenReadStream();
imageUrl = await _fileStorageService.SaveFileAsync(stream, categorieDto.ImageFile.FileName, categorieDto.ImageFile.ContentType); imageUrl = await _fileStorageService.SaveFileAsync(stream, categorieDto.ImageFile.FileName, categorieDto.ImageFile.ContentType);
@@ -86,41 +84,23 @@ namespace Webshop.Application.Services.Admin
return ServiceResult.Fail(ServiceResultType.Conflict, "Eine andere Kategorie mit diesem Slug existiert bereits."); return ServiceResult.Fail(ServiceResultType.Conflict, "Eine andere Kategorie mit diesem Slug existiert bereits.");
} }
// ----------------------------------------------------------------------- string? imageUrl = existing.ImageUrl;
// BILD UPDATE LOGIK (Verbessert) if (categorieDto.ImageFile != null)
// -----------------------------------------------------------------------
// 1. Neues Bild hochladen, falls vorhanden
if (categorieDto.ImageFile != null && categorieDto.ImageFile.Length > 0)
{ {
await using var stream = categorieDto.ImageFile.OpenReadStream(); await using var stream = categorieDto.ImageFile.OpenReadStream();
var newUrl = await _fileStorageService.SaveFileAsync(stream, categorieDto.ImageFile.FileName, categorieDto.ImageFile.ContentType); imageUrl = await _fileStorageService.SaveFileAsync(stream, categorieDto.ImageFile.FileName, categorieDto.ImageFile.ContentType);
// Bestehendes Bild <20>berschreiben
existing.ImageUrl = newUrl;
} }
// 2. Optional: Logik zum L<>schen des Bildes else if (string.IsNullOrEmpty(categorieDto.ImageUrl) && !string.IsNullOrEmpty(existing.ImageUrl))
// ACHTUNG: Die vorherige Logik hat das Bild oft versehentlich gel<65>scht, wenn das DTO
// das Feld 'ImageUrl' leer hatte (was bei Datei-Uploads oft passiert).
// Wir l<>schen nur, wenn KEIN neues File da ist UND explizit ein leerer String <20>bergeben wurde,
// aber wir m<>ssen vorsichtig sein, dass null (nicht gesendet) das Bild nicht l<>scht.
// Im Zweifel: Wenn du das Bild l<>schen willst, br<62>uchtest du ein explizites Flag (z.B. bool DeleteImage).
// Hier belassen wir es dabei: Wenn kein neues Bild kommt, bleibt das alte erhalten.
// Falls du explizit L<>schen unterst<73>tzen willst, wenn ImageUrl leer ist, nutze dies mit Vorsicht:
/*
else if (categorieDto.ImageUrl != null && string.IsNullOrEmpty(categorieDto.ImageUrl))
{ {
// Nur l<>schen, wenn ImageUrl explizit als leerer String (nicht null) gesendet wurde // Hier k<>nnte Logik zum L<>schen der alten Datei stehen, falls gew<EFBFBD>nscht
existing.ImageUrl = null; imageUrl = null;
} }
*/
// Mapping der restlichen Felder
existing.Name = categorieDto.Name; existing.Name = categorieDto.Name;
existing.Slug = categorieDto.Slug; existing.Slug = categorieDto.Slug;
existing.Description = categorieDto.Description; existing.Description = categorieDto.Description;
existing.ParentcategorieId = categorieDto.ParentcategorieId; existing.ParentcategorieId = categorieDto.ParentcategorieId;
existing.ImageUrl = imageUrl;
existing.IsActive = categorieDto.IsActive; existing.IsActive = categorieDto.IsActive;
existing.DisplayOrder = categorieDto.DisplayOrder; existing.DisplayOrder = categorieDto.DisplayOrder;
existing.LastModifiedDate = DateTimeOffset.UtcNow; existing.LastModifiedDate = DateTimeOffset.UtcNow;
@@ -144,6 +124,8 @@ namespace Webshop.Application.Services.Admin
return ServiceResult.Fail(ServiceResultType.Conflict, "Kategorie kann nicht gel<65>scht werden, da sie als <20>bergeordnete Kategorie f<>r andere Kategorien dient."); return ServiceResult.Fail(ServiceResultType.Conflict, "Kategorie kann nicht gel<65>scht werden, da sie als <20>bergeordnete Kategorie f<>r andere Kategorien dient.");
} }
// Hier k<>nnte man auch pr<70>fen, ob Produkte dieser Kategorie zugeordnet sind.
await _categorieRepository.DeleteAsync(id); await _categorieRepository.DeleteAsync(id);
return ServiceResult.Ok(); return ServiceResult.Ok();
} }

View File

@@ -100,8 +100,7 @@ namespace Webshop.Application.Services.Admin
IsFeatured = productDto.IsFeatured, IsFeatured = productDto.IsFeatured,
FeaturedDisplayOrder = productDto.FeaturedDisplayOrder, FeaturedDisplayOrder = productDto.FeaturedDisplayOrder,
Images = images, Images = images,
Productcategories = productDto.CategorieIds.Select(cId => new Productcategorie { categorieId = cId }).ToList(), Productcategories = productDto.CategorieIds.Select(cId => new Productcategorie { categorieId = cId }).ToList()
RowVersion = Guid.NewGuid().ToByteArray()
}; };
await _productRepository.AddProductAsync(newProduct); await _productRepository.AddProductAsync(newProduct);
@@ -110,208 +109,20 @@ namespace Webshop.Application.Services.Admin
// ... (UpdateAdminProductAsync und DeleteAdminProductAsync und MapToAdminDto bleiben unver<65>ndert) ... // ... (UpdateAdminProductAsync und DeleteAdminProductAsync und MapToAdminDto bleiben unver<65>ndert) ...
#region Unchanged Methods #region Unchanged Methods
// src/Webshop.Application/Services/Admin/AdminProductService.cs
public async Task<ServiceResult> UpdateAdminProductAsync(UpdateAdminProductDto productDto) public async Task<ServiceResult> UpdateAdminProductAsync(UpdateAdminProductDto productDto)
{ {
Console.WriteLine($"---- UPDATE START: Produkt-ID {productDto.Id} ----"); var existingProduct = await _context.Products.Include(p => p.Images).Include(p => p.Productcategories).FirstOrDefaultAsync(p => p.Id == productDto.Id);
if (existingProduct == null) { return ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt mit ID '{productDto.Id}' nicht gefunden."); }
// 1. Produkt laden (nur um sicherzugehen, dass es existiert und f<>r alte Bilder) var skuExists = await _context.Products.AnyAsync(p => p.SKU == productDto.SKU && p.Id != productDto.Id);
var existingProduct = await _context.Products if (skuExists) { return ServiceResult.Fail(ServiceResultType.Conflict, $"Ein anderes Produkt mit der SKU '{productDto.SKU}' existiert bereits."); }
.Include(p => p.Images) var slugExists = await _context.Products.AnyAsync(p => p.Slug == productDto.Slug && p.Id != productDto.Id);
.Include(p => p.Productcategories) if (slugExists) { return ServiceResult.Fail(ServiceResultType.Conflict, $"Ein anderes Produkt mit dem Slug '{productDto.Slug}' existiert bereits."); }
.FirstOrDefaultAsync(p => p.Id == productDto.Id); if (productDto.ImagesToDelete != null && productDto.ImagesToDelete.Any()) { var imagesToRemove = existingProduct.Images.Where(img => productDto.ImagesToDelete.Contains(img.Id)).ToList(); _context.ProductImages.RemoveRange(imagesToRemove); }
if (productDto.MainImageFile != null) { var existingMainImage = existingProduct.Images.FirstOrDefault(img => img.IsMainImage); if (existingMainImage != null) _context.ProductImages.Remove(existingMainImage); await using var stream = productDto.MainImageFile.OpenReadStream(); var url = await _fileStorageService.SaveFileAsync(stream, productDto.MainImageFile.FileName, productDto.MainImageFile.ContentType); existingProduct.Images.Add(new ProductImage { Url = url, IsMainImage = true, DisplayOrder = 1 }); }
if (existingProduct == null) if (productDto.AdditionalImageFiles != null && productDto.AdditionalImageFiles.Any()) { int displayOrder = (existingProduct.Images.Any() ? existingProduct.Images.Max(i => i.DisplayOrder) : 0) + 1; foreach (var file in productDto.AdditionalImageFiles) { await using var stream = file.OpenReadStream(); var url = await _fileStorageService.SaveFileAsync(stream, file.FileName, file.ContentType); existingProduct.Images.Add(new ProductImage { Url = url, IsMainImage = false, DisplayOrder = displayOrder++ }); } }
{ existingProduct.Name = productDto.Name; existingProduct.Description = productDto.Description; existingProduct.SKU = productDto.SKU; existingProduct.Price = productDto.Price; existingProduct.IsActive = productDto.IsActive; existingProduct.StockQuantity = productDto.StockQuantity; existingProduct.Slug = productDto.Slug; existingProduct.Weight = productDto.Weight; existingProduct.OldPrice = productDto.OldPrice; existingProduct.SupplierId = productDto.SupplierId; existingProduct.PurchasePrice = productDto.PurchasePrice; existingProduct.LastModifiedDate = DateTimeOffset.UtcNow; existingProduct.IsFeatured = productDto.IsFeatured; existingProduct.FeaturedDisplayOrder = productDto.FeaturedDisplayOrder;
return ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt nicht gefunden."); existingProduct.Productcategories.Clear(); if (productDto.CategorieIds != null) { foreach (var categorieId in productDto.CategorieIds) { existingProduct.Productcategories.Add(new Productcategorie { categorieId = categorieId }); } }
} await _productRepository.UpdateProductAsync(existingProduct); return ServiceResult.Ok();
// 2. Optionaler Concurrency Check
if (!string.IsNullOrEmpty(productDto.RowVersion))
{
string dbRowVersion = Convert.ToBase64String(existingProduct.RowVersion ?? new byte[0]);
string incomingRowVersion = productDto.RowVersion.Trim().Replace(" ", "+");
if (dbRowVersion != incomingRowVersion)
{
Console.WriteLine("!!! KONFLIKT: Versionen unterschiedlich !!!");
return ServiceResult.Fail(ServiceResultType.Conflict, "Das Produkt wurde zwischenzeitlich bearbeitet.");
}
}
// -----------------------------------------------------------------------
// SCHRITT A: BILDER UPDATE (DIREKT AM CONTEXT)
// -----------------------------------------------------------------------
bool imagesChanged = false;
// A1. Bilder l<>schen
if (productDto.ImagesToDelete != null && productDto.ImagesToDelete.Any())
{
// Wir suchen die zu l<>schenden Bilder direkt im Context
var imagesToRemove = existingProduct.Images.Where(img => productDto.ImagesToDelete.Contains(img.Id)).ToList();
if (imagesToRemove.Any())
{
_context.ProductImages.RemoveRange(imagesToRemove);
imagesChanged = true;
}
}
// Hilfsfunktion um saubere neue Bilder zu erstellen
ProductImage CreateNewImage(string url, bool isMain, int order)
{
return new ProductImage
{
Id = Guid.NewGuid(), // WICHTIG: Neue ID client-seitig generieren
ProductId = existingProduct.Id, // WICHTIG: Explizite Zuordnung zum Produkt
Url = url,
IsMainImage = isMain,
DisplayOrder = order
};
}
// A2. Hauptbild
if (productDto.MainImageFile != null)
{
// Altes Hauptbild finden und l<>schen
var existingMainImage = existingProduct.Images.FirstOrDefault(img => img.IsMainImage);
if (existingMainImage != null)
{
_context.ProductImages.Remove(existingMainImage);
}
await using var stream = productDto.MainImageFile.OpenReadStream();
var url = await _fileStorageService.SaveFileAsync(stream, productDto.MainImageFile.FileName, productDto.MainImageFile.ContentType);
// NEU: Direktes Hinzuf<75>gen zum Context (Umgeht Collection-Tracking Probleme)
var newMainImage = CreateNewImage(url, true, 1);
_context.ProductImages.Add(newMainImage);
imagesChanged = true;
}
// >>> NEU: A2.5 Szenario: BESTEHENDES Bild als Hauptbild <<<
else if (productDto.SetMainImageId.HasValue)
{
// Wir suchen das Bild in der Liste, die wir schon aus der DB geladen haben
var targetImage = existingProduct.Images.FirstOrDefault(img => img.Id == productDto.SetMainImageId.Value);
// Nur ausf<73>hren, wenn Bild existiert und noch nicht Main ist
if (targetImage != null && !targetImage.IsMainImage)
{
Console.WriteLine($"---- Setze existierendes Bild {targetImage.Id} als MAIN ----");
// 1. Alle Bilder auf "Nicht-Main" setzen
foreach (var img in existingProduct.Images)
{
img.IsMainImage = false;
// Optional: Ordnung korrigieren, damit Main immer vorne ist
if (img.DisplayOrder == 1) img.DisplayOrder = 2;
}
// 2. Das gew<65>hlte Bild auf Main setzen
targetImage.IsMainImage = true;
targetImage.DisplayOrder = 1;
// Da wir hier getrackte Entities <20>ndern, generiert EF Core automatisch UPDATEs
imagesChanged = true;
}
}
// A3. Zusatzbilder
if (productDto.AdditionalImageFiles != null && productDto.AdditionalImageFiles.Any())
{
// H<>chste DisplayOrder ermitteln (sicher gegen NullReference)
int currentMaxOrder = existingProduct.Images.Any() ? existingProduct.Images.Max(i => i.DisplayOrder) : 0;
int displayOrder = currentMaxOrder + 1;
foreach (var file in productDto.AdditionalImageFiles)
{
await using var stream = file.OpenReadStream();
var url = await _fileStorageService.SaveFileAsync(stream, file.FileName, file.ContentType);
// NEU: Direktes Hinzuf<75>gen zum Context
var newAdditionalImage = CreateNewImage(url, false, displayOrder++);
_context.ProductImages.Add(newAdditionalImage);
}
imagesChanged = true;
}
// -----------------------------------------------------------------------
// SCHRITT A SPEICHERN
// -----------------------------------------------------------------------
if (imagesChanged)
{
try
{
Console.WriteLine("---- SPEICHERE BILDER (Context Add) ----");
await _context.SaveChangesAsync();
// WICHTIG: State neu laden f<>r Schritt B
// Wir m<>ssen dem Context sagen, dass existingProduct neu geladen werden soll,
// damit die Navigations-Properties (Images) wieder aktuell sind.
await _context.Entry(existingProduct).ReloadAsync();
// Da wir die Collection umgangen haben, m<>ssen wir sie explizit neu laden
await _context.Entry(existingProduct).Collection(p => p.Images).LoadAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Fehler Bild-Save: {ex.Message}");
// Inner Exception loggen, falls vorhanden
if (ex.InnerException != null) Console.WriteLine($"Inner: {ex.InnerException.Message}");
return ServiceResult.Fail(ServiceResultType.Failure, "Fehler beim Speichern der Bilder.");
}
}
// -----------------------------------------------------------------------
// SCHRITT B: PRODUKT EIGENSCHAFTEN
// -----------------------------------------------------------------------
// Kategorien update
if (productDto.CategorieIds != null)
{
var currentIds = existingProduct.Productcategories.Select(pc => pc.categorieId).ToList();
if (!new HashSet<Guid>(currentIds).SetEquals(productDto.CategorieIds))
{
existingProduct.Productcategories.Clear();
foreach (var cId in productDto.CategorieIds)
{
existingProduct.Productcategories.Add(new Productcategorie { categorieId = cId });
}
}
}
// Mapping
existingProduct.Name = productDto.Name;
existingProduct.Description = productDto.Description;
existingProduct.SKU = productDto.SKU;
existingProduct.Price = productDto.Price;
existingProduct.IsActive = productDto.IsActive;
existingProduct.StockQuantity = productDto.StockQuantity;
existingProduct.Slug = productDto.Slug;
existingProduct.Weight = productDto.Weight;
existingProduct.OldPrice = productDto.OldPrice;
existingProduct.SupplierId = productDto.SupplierId;
existingProduct.PurchasePrice = productDto.PurchasePrice;
existingProduct.IsFeatured = productDto.IsFeatured;
existingProduct.FeaturedDisplayOrder = productDto.FeaturedDisplayOrder;
// Metadaten
existingProduct.LastModifiedDate = DateTimeOffset.UtcNow;
existingProduct.RowVersion = Guid.NewGuid().ToByteArray();
try
{
Console.WriteLine("---- SPEICHERE PRODUKT-DATEN ----");
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
return ServiceResult.Fail(ServiceResultType.Conflict, "Konflikt beim Speichern der Produktdaten.");
}
return ServiceResult.Ok();
} }
public async Task<ServiceResult> DeleteAdminProductAsync(Guid id) public async Task<ServiceResult> DeleteAdminProductAsync(Guid id)
@@ -323,7 +134,7 @@ namespace Webshop.Application.Services.Admin
private AdminProductDto MapToAdminDto(Product product) private AdminProductDto MapToAdminDto(Product product)
{ {
return new AdminProductDto { Id = product.Id, Name = product.Name, Description = product.Description, SKU = product.SKU, Price = product.Price, OldPrice = product.OldPrice, IsActive = product.IsActive, IsInStock = product.IsInStock, StockQuantity = product.StockQuantity, Weight = product.Weight, Slug = product.Slug, CreatedDate = product.CreatedDate, LastModifiedDate = product.LastModifiedDate, SupplierId = product.SupplierId, PurchasePrice = product.PurchasePrice, IsFeatured = product.IsFeatured, FeaturedDisplayOrder = product.FeaturedDisplayOrder, categorieIds = product.Productcategories.Select(pc => pc.categorieId).ToList(), Images = product.Images.OrderBy(i => i.DisplayOrder).Select(img => new ProductImageDto { Id = img.Id, Url = img.Url, IsMainImage = img.IsMainImage, DisplayOrder = img.DisplayOrder }).ToList(), RowVersion = product.RowVersion }; return new AdminProductDto { Id = product.Id, Name = product.Name, Description = product.Description, SKU = product.SKU, Price = product.Price, OldPrice = product.OldPrice, IsActive = product.IsActive, IsInStock = product.IsInStock, StockQuantity = product.StockQuantity, Weight = product.Weight, Slug = product.Slug, CreatedDate = product.CreatedDate, LastModifiedDate = product.LastModifiedDate, SupplierId = product.SupplierId, PurchasePrice = product.PurchasePrice, IsFeatured = product.IsFeatured, FeaturedDisplayOrder = product.FeaturedDisplayOrder, categorieIds = product.Productcategories.Select(pc => pc.categorieId).ToList(), Images = product.Images.OrderBy(i => i.DisplayOrder).Select(img => new ProductImageDto { Id = img.Id, Url = img.Url, IsMainImage = img.IsMainImage, DisplayOrder = img.DisplayOrder }).ToList() };
} }
#endregion #endregion
} }

View File

@@ -1,13 +1,14 @@
using System; // src/Webshop.Application/Services/Admin/AdminShippingMethodService.cs
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; // Hinzufügen für .AnyAsync
using Webshop.Application; using Webshop.Application;
using Webshop.Application.DTOs.Shipping; using Webshop.Application.DTOs.Shipping;
using Webshop.Domain.Entities; using Webshop.Domain.Entities;
using Webshop.Domain.Interfaces; using Webshop.Domain.Interfaces;
using Webshop.Infrastructure.Data; using Webshop.Infrastructure.Data; // Hinzufügen für DbContext
namespace Webshop.Application.Services.Admin namespace Webshop.Application.Services.Admin
{ {
@@ -54,15 +55,14 @@ namespace Webshop.Application.Services.Admin
Description = shippingMethodDto.Description, Description = shippingMethodDto.Description,
BaseCost = shippingMethodDto.Cost, BaseCost = shippingMethodDto.Cost,
IsActive = shippingMethodDto.IsActive, IsActive = shippingMethodDto.IsActive,
// +++ KORREKTUR +++
MinDeliveryDays = shippingMethodDto.MinDeliveryDays, MinDeliveryDays = shippingMethodDto.MinDeliveryDays,
MaxDeliveryDays = shippingMethodDto.MaxDeliveryDays, MaxDeliveryDays = shippingMethodDto.MaxDeliveryDays
// NEU: Gewichte mappen
MinWeight = shippingMethodDto.MinWeight,
MaxWeight = shippingMethodDto.MaxWeight
}; };
await _shippingMethodRepository.AddAsync(newMethod); await _shippingMethodRepository.AddAsync(newMethod);
// Verwende MapToDto, um eine konsistente Antwort zu gewährleisten
return ServiceResult.Ok(MapToDto(newMethod)); return ServiceResult.Ok(MapToDto(newMethod));
} }
@@ -75,7 +75,6 @@ namespace Webshop.Application.Services.Admin
} }
var allMethods = await _shippingMethodRepository.GetAllAsync(); var allMethods = await _shippingMethodRepository.GetAllAsync();
// Prüfen, ob der Name von einer ANDEREN Methode bereits verwendet wird
if (allMethods.Any(sm => sm.Name.Equals(shippingMethodDto.Name, StringComparison.OrdinalIgnoreCase) && sm.Id != shippingMethodDto.Id)) if (allMethods.Any(sm => sm.Name.Equals(shippingMethodDto.Name, StringComparison.OrdinalIgnoreCase) && sm.Id != shippingMethodDto.Id))
{ {
return ServiceResult.Fail(ServiceResultType.Conflict, $"Eine andere Versandmethode mit dem Namen '{shippingMethodDto.Name}' existiert bereits."); return ServiceResult.Fail(ServiceResultType.Conflict, $"Eine andere Versandmethode mit dem Namen '{shippingMethodDto.Name}' existiert bereits.");
@@ -85,11 +84,9 @@ namespace Webshop.Application.Services.Admin
existingMethod.Description = shippingMethodDto.Description; existingMethod.Description = shippingMethodDto.Description;
existingMethod.BaseCost = shippingMethodDto.Cost; existingMethod.BaseCost = shippingMethodDto.Cost;
existingMethod.IsActive = shippingMethodDto.IsActive; existingMethod.IsActive = shippingMethodDto.IsActive;
// +++ KORREKTUR +++
existingMethod.MinDeliveryDays = shippingMethodDto.MinDeliveryDays; existingMethod.MinDeliveryDays = shippingMethodDto.MinDeliveryDays;
existingMethod.MaxDeliveryDays = shippingMethodDto.MaxDeliveryDays; existingMethod.MaxDeliveryDays = shippingMethodDto.MaxDeliveryDays;
// NEU: Gewichte aktualisieren
existingMethod.MinWeight = shippingMethodDto.MinWeight;
existingMethod.MaxWeight = shippingMethodDto.MaxWeight;
await _shippingMethodRepository.UpdateAsync(existingMethod); await _shippingMethodRepository.UpdateAsync(existingMethod);
return ServiceResult.Ok(); return ServiceResult.Ok();
@@ -122,11 +119,9 @@ namespace Webshop.Application.Services.Admin
Description = sm.Description, Description = sm.Description,
Cost = sm.BaseCost, Cost = sm.BaseCost,
IsActive = sm.IsActive, IsActive = sm.IsActive,
// +++ KORREKTUR +++
MinDeliveryDays = sm.MinDeliveryDays, MinDeliveryDays = sm.MinDeliveryDays,
MaxDeliveryDays = sm.MaxDeliveryDays, MaxDeliveryDays = sm.MaxDeliveryDays
// NEU: Gewichte ins DTO übertragen
MinWeight = sm.MinWeight,
MaxWeight = sm.MaxWeight
}; };
} }
} }

View File

@@ -2,37 +2,39 @@
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Resend;
using System; using System;
using System.Collections.Generic; using System.IO;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Webshop.Application; using System.Web;
using Webshop.Application.DTOs.Auth; using Webshop.Application.DTOs.Auth;
using Webshop.Application.Services.Public.Interfaces;
using Webshop.Domain.Identity; using Webshop.Domain.Identity;
using Webshop.Infrastructure.Data; using Webshop.Infrastructure.Data;
using Webshop.Application;
using System.Collections.Generic;
namespace Webshop.Application.Services.Auth namespace Webshop.Application.Services.Auth
{ {
public class AuthService : IAuthService public class AuthService : IAuthService
{ {
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly IEmailService _emailService;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly IResend _resend;
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
public AuthService( public AuthService(
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
IEmailService emailService,
IConfiguration configuration, IConfiguration configuration,
IResend resend,
ApplicationDbContext context) ApplicationDbContext context)
{ {
_userManager = userManager; _userManager = userManager;
_emailService = emailService;
_configuration = configuration; _configuration = configuration;
_resend = resend;
_context = context; _context = context;
} }
@@ -64,9 +66,7 @@ namespace Webshop.Application.Services.Auth
_context.Customers.Add(customerProfile); _context.Customers.Add(customerProfile);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
// Link generieren und Service aufrufen await SendEmailConfirmationEmail(user);
await SendConfirmationLinkAsync(user);
return ServiceResult.Ok(); return ServiceResult.Ok();
} }
@@ -102,16 +102,18 @@ namespace Webshop.Application.Services.Auth
var loginResult = await LoginUserAsync(request); var loginResult = await LoginUserAsync(request);
if (loginResult.Type != ServiceResultType.Success) if (loginResult.Type != ServiceResultType.Success)
{ {
// Propagate the specific login failure (e.g., Unauthorized)
return ServiceResult.Fail<AuthResponseDto>(loginResult.Type, loginResult.ErrorMessage!); return ServiceResult.Fail<AuthResponseDto>(loginResult.Type, loginResult.ErrorMessage!);
} }
var user = await _userManager.FindByEmailAsync(request.Email); var user = await _userManager.FindByEmailAsync(request.Email);
// This check is belt-and-suspenders, but good practice
if (user == null || !await _userManager.IsInRoleAsync(user, "Admin")) if (user == null || !await _userManager.IsInRoleAsync(user, "Admin"))
{ {
return ServiceResult.Fail<AuthResponseDto>(ServiceResultType.Forbidden, "Keine Berechtigung für den Admin-Zugang."); return ServiceResult.Fail<AuthResponseDto>(ServiceResultType.Forbidden, "Keine Berechtigung für den Admin-Zugang.");
} }
return loginResult; return loginResult; // Return the successful login result
} }
public async Task<ServiceResult> ConfirmEmailAsync(string userId, string token) public async Task<ServiceResult> ConfirmEmailAsync(string userId, string token)
@@ -124,10 +126,11 @@ namespace Webshop.Application.Services.Auth
var user = await _userManager.FindByIdAsync(userId); var user = await _userManager.FindByIdAsync(userId);
if (user == null) if (user == null)
{ {
// Do not reveal that the user does not exist
return ServiceResult.Fail(ServiceResultType.NotFound, "Ungültiger Bestätigungsversuch."); return ServiceResult.Fail(ServiceResultType.NotFound, "Ungültiger Bestätigungsversuch.");
} }
var result = await _userManager.ConfirmEmailAsync(user, token); var result = await _userManager.ConfirmEmailAsync(user, token); // The token from the URL is already decoded by ASP.NET Core
return result.Succeeded return result.Succeeded
? ServiceResult.Ok() ? ServiceResult.Ok()
: ServiceResult.Fail(ServiceResultType.Failure, "E-Mail-Bestätigung fehlgeschlagen."); : ServiceResult.Fail(ServiceResultType.Failure, "E-Mail-Bestätigung fehlgeschlagen.");
@@ -136,33 +139,48 @@ namespace Webshop.Application.Services.Auth
public async Task<ServiceResult> ResendEmailConfirmationAsync(string email) public async Task<ServiceResult> ResendEmailConfirmationAsync(string email)
{ {
var user = await _userManager.FindByEmailAsync(email); var user = await _userManager.FindByEmailAsync(email);
if (user == null) return ServiceResult.Ok();
if (user == null)
{
return ServiceResult.Ok(); // Do not reveal user existence
}
if (user.EmailConfirmed) if (user.EmailConfirmed)
{ {
return ServiceResult.Fail(ServiceResultType.InvalidInput, "Diese E-Mail-Adresse ist bereits bestätigt."); return ServiceResult.Fail(ServiceResultType.InvalidInput, "Diese E-Mail-Adresse ist bereits bestätigt.");
} }
await SendConfirmationLinkAsync(user); await SendEmailConfirmationEmail(user);
return ServiceResult.Ok(); return ServiceResult.Ok();
} }
public async Task<ServiceResult> ForgotPasswordAsync(ForgotPasswordRequestDto request) public async Task<ServiceResult> ForgotPasswordAsync(ForgotPasswordRequestDto request)
{ {
var user = await _userManager.FindByEmailAsync(request.Email); var user = await _userManager.FindByEmailAsync(request.Email);
if (user != null) if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
{ {
var token = await _userManager.GeneratePasswordResetTokenAsync(user); return ServiceResult.Ok(); // Do not reveal user existence or status
var clientUrl = _configuration["App:ClientUrl"];
// Sicherstellen, dass URL-Parameter escaped sind
var resetLink = $"{clientUrl}/reset-password?token={Uri.EscapeDataString(token)}&email={Uri.EscapeDataString(request.Email)}";
await _emailService.SendPasswordResetAsync(user.Email!, resetLink);
} }
// WICHTIG: Wir geben IMMER Ok zurück, auch wenn der User nicht existiert. var token = await _userManager.GeneratePasswordResetTokenAsync(user);
// Das verhindert "User Enumeration" (Hacker können nicht prüfen, welche E-Mails existieren). var clientUrl = _configuration["App:ClientUrl"] ?? "http://localhost:3000";
// Important: URL-encode components separately to avoid encoding the whole URL structure
var resetLink = $"{clientUrl}/reset-password?email={HttpUtility.UrlEncode(request.Email)}&token={HttpUtility.UrlEncode(token)}";
var emailHtmlBody = await LoadAndFormatEmailTemplate(
"Setzen Sie Ihr Passwort zurück",
"Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gesendet. Klicken Sie auf den Button unten, um ein neues Passwort festzulegen.",
"Passwort zurücksetzen",
resetLink
);
var message = new EmailMessage();
message.To.Add(request.Email);
message.From = _configuration["Resend:FromEmail"]!;
message.Subject = "Anleitung zum Zurücksetzen Ihres Passworts";
message.HtmlBody = emailHtmlBody;
await _resend.EmailSendAsync(message);
return ServiceResult.Ok(); return ServiceResult.Ok();
} }
@@ -171,6 +189,7 @@ namespace Webshop.Application.Services.Auth
var user = await _userManager.FindByEmailAsync(request.Email); var user = await _userManager.FindByEmailAsync(request.Email);
if (user == null) if (user == null)
{ {
// Don't reveal user non-existence, but the error message will be generic
return ServiceResult.Fail(ServiceResultType.InvalidInput, "Fehler beim Zurücksetzen des Passworts."); return ServiceResult.Fail(ServiceResultType.InvalidInput, "Fehler beim Zurücksetzen des Passworts.");
} }
@@ -181,17 +200,26 @@ namespace Webshop.Application.Services.Auth
: ServiceResult.Fail(ServiceResultType.InvalidInput, string.Join(" ", result.Errors.Select(e => e.Description))); : ServiceResult.Fail(ServiceResultType.InvalidInput, string.Join(" ", result.Errors.Select(e => e.Description)));
} }
// --- Helper Methods --- private async Task SendEmailConfirmationEmail(ApplicationUser user)
private async Task SendConfirmationLinkAsync(ApplicationUser user)
{ {
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var clientUrl = _configuration["App:ClientUrl"]; // z.B. https://localhost:5001/api/v1/Auth var encodedToken = HttpUtility.UrlEncode(token);
var clientUrl = _configuration["App:ClientUrl"]!;
var confirmationLink = $"{clientUrl}/confirm-email?userId={user.Id}&token={encodedToken}";
// WICHTIG: Uri.EscapeDataString ist sicherer für Token in URLs als HttpUtility var emailHtmlBody = await LoadAndFormatEmailTemplate(
var confirmationLink = $"{clientUrl}/confirm-email?userId={user.Id}&token={Uri.EscapeDataString(token)}"; "Bestätigen Sie Ihre E-Mail-Adresse",
"Vielen Dank für Ihre Registrierung! Bitte klicken Sie auf den Button unten, um Ihr Konto zu aktivieren.",
"Konto aktivieren",
confirmationLink
);
await _emailService.SendEmailConfirmationAsync(user.Email!, confirmationLink); var message = new EmailMessage();
message.To.Add(user.Email!);
message.From = _configuration["Resend:FromEmail"]!;
message.Subject = "Willkommen! Bitte bestätigen Sie Ihre E-Mail-Adresse";
message.HtmlBody = emailHtmlBody;
await _resend.EmailSendAsync(message);
} }
private string GenerateJwtToken(ApplicationUser user, IList<string> roles) private string GenerateJwtToken(ApplicationUser user, IList<string> roles)
@@ -223,5 +251,22 @@ namespace Webshop.Application.Services.Auth
return new JwtSecurityTokenHandler().WriteToken(token); return new JwtSecurityTokenHandler().WriteToken(token);
} }
private async Task<string> LoadAndFormatEmailTemplate(string titel, string haupttext, string callToActionText, string callToActionLink)
{
var templatePath = Path.Combine(AppContext.BaseDirectory, "Templates", "_EmailTemplate.html");
if (!File.Exists(templatePath))
{
return $"<h1>{titel}</h1><p>{haupttext}</p><a href='{callToActionLink}'>{callToActionText}</a>";
}
var template = await File.ReadAllTextAsync(templatePath);
template = template.Replace("{{ShopName}}", _configuration["ShopInfo:Name"] ?? "Ihr Webshop");
template = template.Replace("{{Titel}}", titel);
template = template.Replace("{{Haupttext}}", haupttext);
template = template.Replace("{{CallToActionText}}", callToActionText);
template = template.Replace("{{CallToActionLink}}", callToActionLink);
template = template.Replace("{{Jahr}}", DateTime.UtcNow.Year.ToString());
return template;
}
} }
} }

View File

@@ -1,114 +0,0 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;
using Webshop.Application;
using Webshop.Application.DTOs.Customers; // <--- DIESE ZEILE HAT GEFEHLT (für CartDto)
using Webshop.Application.DTOs.Shipping; // (für CartItemDto)
using Webshop.Application.Services.Customers.Interfaces;
using Webshop.Domain.Entities;
using Webshop.Infrastructure.Data;
namespace Webshop.Application.Services.Customers
{
public class CartService : ICartService
{
private readonly ApplicationDbContext _context;
public CartService(ApplicationDbContext context)
{
_context = context;
}
// Die Implementierung muss exakt so aussehen:
public async Task<ServiceResult<CartDto>> GetCartAsync(string userId)
{
var cart = await GetCartEntity(userId);
// Wenn kein Warenkorb da ist, geben wir einen leeren zurück (Success)
if (cart == null)
{
return ServiceResult.Ok(new CartDto { UserId = userId });
}
var dto = new CartDto
{
Id = cart.Id,
UserId = userId,
Items = cart.Items.Select(i => new CartItemDto
{
ProductId = i.ProductId,
ProductVariantId = i.ProductVariantId,
Quantity = i.Quantity
}).ToList()
};
return ServiceResult.Ok(dto);
}
public async Task<ServiceResult> AddToCartAsync(string userId, CartItemDto itemDto)
{
var cart = await GetCartEntity(userId);
if (cart == null)
{
cart = new Cart { UserId = userId, Id = Guid.NewGuid() };
_context.Carts.Add(cart);
}
var existingItem = cart.Items.FirstOrDefault(i => i.ProductId == itemDto.ProductId && i.ProductVariantId == itemDto.ProductVariantId);
if (existingItem != null)
{
existingItem.Quantity += itemDto.Quantity;
}
else
{
var newItem = new CartItem
{
CartId = cart.Id,
ProductId = itemDto.ProductId,
ProductVariantId = itemDto.ProductVariantId,
Quantity = itemDto.Quantity
};
// WICHTIG: Zur Collection hinzufügen UND dem Context Bescheid geben
cart.Items.Add(newItem);
_context.CartItems.Add(newItem);
}
cart.LastModified = DateTime.UtcNow;
await _context.SaveChangesAsync();
return ServiceResult.Ok();
}
public async Task<ServiceResult> RemoveFromCartAsync(string userId, Guid productId)
{
var cart = await GetCartEntity(userId);
if (cart == null) return ServiceResult.Fail(ServiceResultType.NotFound, "Warenkorb leer.");
var item = cart.Items.FirstOrDefault(i => i.ProductId == productId);
if (item != null)
{
_context.CartItems.Remove(item);
await _context.SaveChangesAsync();
}
return ServiceResult.Ok();
}
public async Task ClearCartAsync(string userId)
{
var cart = await GetCartEntity(userId);
if (cart != null && cart.Items.Any())
{
_context.CartItems.RemoveRange(cart.Items);
await _context.SaveChangesAsync();
}
}
private async Task<Cart?> GetCartEntity(string userId)
{
return await _context.Carts
.Include(c => c.Items)
.FirstOrDefaultAsync(c => c.UserId == userId);
}
}
}

View File

@@ -1,19 +1,15 @@
// src/Webshop.Application/Services/Customers/CheckoutService.cs // /src/Webshop.Application/Services/Customers/CheckoutService.cs
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Webshop.Application; using Webshop.Application;
using Webshop.Application.DTOs.Customers; using Webshop.Application.DTOs.Customers;
using Webshop.Application.DTOs.Orders; using Webshop.Application.DTOs.Orders;
using Webshop.Application.DTOs.Shipping;
using Webshop.Application.Services.Customers.Interfaces; using Webshop.Application.Services.Customers.Interfaces;
using Webshop.Application.Services.Public.Interfaces; using Webshop.Application.Services.Public.Interfaces;
using Webshop.Application.Services.Public; // Für DiscountCalculationResult
using Webshop.Domain.Entities; using Webshop.Domain.Entities;
using Webshop.Domain.Enums; using Webshop.Domain.Enums;
using Webshop.Domain.Interfaces; using Webshop.Domain.Interfaces;
@@ -29,20 +25,10 @@ namespace Webshop.Application.Services.Customers
private readonly IShippingMethodRepository _shippingMethodRepository; private readonly IShippingMethodRepository _shippingMethodRepository;
private readonly ISettingService _settingService; private readonly ISettingService _settingService;
private readonly IDiscountService _discountService; private readonly IDiscountService _discountService;
private readonly ICartService _cartService;
private readonly IEmailService _emailService;
private readonly ILogger<CheckoutService> _logger;
public CheckoutService( public CheckoutService(
ApplicationDbContext context, ApplicationDbContext context, IOrderRepository orderRepository, ICustomerRepository customerRepository,
IOrderRepository orderRepository, IShippingMethodRepository shippingMethodRepository, ISettingService settingService, IDiscountService discountService)
ICustomerRepository customerRepository,
IShippingMethodRepository shippingMethodRepository,
ISettingService settingService,
IDiscountService discountService,
ICartService cartService,
IEmailService emailService,
ILogger<CheckoutService> logger)
{ {
_context = context; _context = context;
_orderRepository = orderRepository; _orderRepository = orderRepository;
@@ -50,63 +36,6 @@ namespace Webshop.Application.Services.Customers
_shippingMethodRepository = shippingMethodRepository; _shippingMethodRepository = shippingMethodRepository;
_settingService = settingService; _settingService = settingService;
_discountService = discountService; _discountService = discountService;
_cartService = cartService;
_emailService = emailService;
_logger = logger;
}
// =================================================================================
// FIX: Überladung für CartItemDto (Löst den Konvertierungsfehler im Controller)
// =================================================================================
public async Task<ServiceResult<IEnumerable<ShippingMethodDto>>> GetCompatibleShippingMethodsAsync(List<CartItemDto> items)
{
// Mappe CartItemDto zu CreateOrderItemDto, damit wir die Logik wiederverwenden können
var mappedItems = items.Select(x => new CreateOrderItemDto
{
ProductId = x.ProductId,
Quantity = x.Quantity,
ProductVariantId = x.ProductVariantId
});
return await GetCompatibleShippingMethodsAsync(mappedItems);
}
// Die Hauptlogik basierend auf CreateOrderItemDto (für den Checkout)
public async Task<ServiceResult<IEnumerable<ShippingMethodDto>>> GetCompatibleShippingMethodsAsync(IEnumerable<CreateOrderItemDto> items)
{
if (items == null || !items.Any())
return ServiceResult.Ok<IEnumerable<ShippingMethodDto>>(new List<ShippingMethodDto>());
var productIds = items.Select(i => i.ProductId).ToList();
var products = await _context.Products.Where(p => productIds.Contains(p.Id)).ToListAsync();
decimal totalWeight = 0;
foreach (var item in items)
{
var product = products.FirstOrDefault(p => p.Id == item.ProductId);
if (product != null)
{
totalWeight += (decimal)(product.Weight ?? 0) * item.Quantity;
}
}
var allMethods = await _shippingMethodRepository.GetAllAsync();
var compatibleMethods = allMethods
.Where(sm => sm.IsActive && totalWeight >= sm.MinWeight && totalWeight <= sm.MaxWeight)
.Select(sm => new ShippingMethodDto
{
Id = sm.Id,
Name = sm.Name,
Description = sm.Description,
Cost = sm.BaseCost,
IsActive = sm.IsActive,
MinDeliveryDays = sm.MinDeliveryDays,
MaxDeliveryDays = sm.MaxDeliveryDays,
MinWeight = sm.MinWeight,
MaxWeight = sm.MaxWeight
})
.ToList();
return ServiceResult.Ok<IEnumerable<ShippingMethodDto>>(compatibleMethods);
} }
public async Task<ServiceResult<OrderDetailDto>> CreateOrderAsync(CreateOrderDto orderDto, string? userId) public async Task<ServiceResult<OrderDetailDto>> CreateOrderAsync(CreateOrderDto orderDto, string? userId)
@@ -114,144 +43,86 @@ namespace Webshop.Application.Services.Customers
await using var transaction = await _context.Database.BeginTransactionAsync(); await using var transaction = await _context.Database.BeginTransactionAsync();
try try
{ {
// 1.1 Kunde Validieren // --- 1. Validierung von Kunde, Adressen und Methoden ---
var customer = await _customerRepository.GetByUserIdAsync(userId!); var customer = await _customerRepository.GetByUserIdAsync(userId!);
if (customer == null) if (customer == null)
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Unauthorized, "Kundenprofil fehlt."); return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Unauthorized, "Kundenprofil nicht gefunden.");
// E-Mail Logik if (!await _context.Addresses.AnyAsync(a => a.Id == orderDto.ShippingAddressId && a.CustomerId == customer.Id) ||
var userEmail = await _context.Users !await _context.Addresses.AnyAsync(a => a.Id == orderDto.BillingAddressId && a.CustomerId == customer.Id))
.Where(u => u.Id == userId)
.Select(u => u.Email)
.FirstOrDefaultAsync();
if (string.IsNullOrEmpty(userEmail))
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Failure, "Keine E-Mail gefunden.");
// =================================================================================
// 1.2 Warenkorb aus der Datenbank laden (Single Source of Truth)
// =================================================================================
var cart = await _context.Carts
.Include(c => c.Items)
.FirstOrDefaultAsync(c => c.UserId == userId);
if (cart == null || !cart.Items.Any())
{ {
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "Ihr Warenkorb ist leer. Bitte fügen Sie Produkte hinzu."); return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Forbidden, "Ungültige oder nicht zugehörige Liefer- oder Rechnungsadresse.");
} }
// 1.3 Adressen Validieren
// Wir laden beide Adressen und stellen sicher, dass sie dem Kunden gehören
var addresses = await _context.Addresses
.Where(a => (a.Id == orderDto.ShippingAddressId || a.Id == orderDto.BillingAddressId) && a.CustomerId == customer.Id)
.ToListAsync();
if (!addresses.Any(a => a.Id == orderDto.ShippingAddressId) || !addresses.Any(a => a.Id == orderDto.BillingAddressId))
{
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Forbidden, "Die angegebenen Adressen sind ungültig oder gehören nicht zu diesem Konto.");
}
// 1.4 Methoden Validieren
var shippingMethod = await _shippingMethodRepository.GetByIdAsync(orderDto.ShippingMethodId); var shippingMethod = await _shippingMethodRepository.GetByIdAsync(orderDto.ShippingMethodId);
if (shippingMethod == null || !shippingMethod.IsActive) if (shippingMethod == null || !shippingMethod.IsActive)
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "Versandart ungültig."); return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "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) if (paymentMethod == null || !paymentMethod.IsActive)
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "Zahlungsart ungültig."); return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "Ungültige oder inaktive Zahlungsmethode.");
// ================================================================================= // --- 2. Artikel verarbeiten und Lagerbestand prüfen ---
// 1.5 Produkte Validieren & Vorbereiten (BASIEREND AUF CART ITEMS) var orderItems = new List<OrderItem>();
// ================================================================================= var productIds = orderDto.Items.Select(i => i.ProductId).ToList();
var productIds = cart.Items.Select(i => i.ProductId).Distinct().ToList();
var products = await _context.Products.Where(p => productIds.Contains(p.Id)).ToListAsync(); var products = await _context.Products.Where(p => productIds.Contains(p.Id)).ToListAsync();
var validationErrors = new StringBuilder();
decimal totalOrderWeight = 0;
decimal itemsTotal = 0; decimal itemsTotal = 0;
var preparedOrderItems = new List<OrderItem>();
foreach (var cartItem in cart.Items) foreach (var itemDto in orderDto.Items)
{ {
var product = products.FirstOrDefault(p => p.Id == cartItem.ProductId); var product = products.FirstOrDefault(p => p.Id == itemDto.ProductId);
if (product == null || !product.IsActive)
if (product == null)
{ {
validationErrors.AppendLine($"- Ein Produkt im Warenkorb (ID {cartItem.ProductId}) existiert nicht mehr."); await transaction.RollbackAsync();
continue; return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Conflict, "Ein Produkt im Warenkorb ist nicht mehr verfügbar.");
}
if (product.StockQuantity < itemDto.Quantity)
{
await transaction.RollbackAsync();
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Conflict, $"Nicht genügend Lagerbestand für '{product.Name}'. Verfügbar: {product.StockQuantity}, benötigt: {itemDto.Quantity}.");
} }
if (!product.IsActive) product.StockQuantity -= itemDto.Quantity;
{
validationErrors.AppendLine($"- '{product.Name}' ist derzeit nicht verfügbar.");
}
// Prüfung Menge
if (product.StockQuantity < cartItem.Quantity)
{
validationErrors.AppendLine($"- '{product.Name}': Nicht genügend Bestand (Verfügbar: {product.StockQuantity}, im Korb: {cartItem.Quantity}).");
}
if (validationErrors.Length == 0)
{
// Gewicht berechnen (Fallback auf 0 falls null)
totalOrderWeight += (decimal)(product.Weight ?? 0) * cartItem.Quantity;
var orderItem = new OrderItem var orderItem = new OrderItem
{ {
ProductId = product.Id, ProductId = product.Id,
ProductVariantId = cartItem.ProductVariantId, ProductVariantId = itemDto.ProductVariantId,
ProductName = product.Name, ProductName = product.Name,
ProductSKU = product.SKU, ProductSKU = product.SKU,
Quantity = cartItem.Quantity, Quantity = itemDto.Quantity,
UnitPrice = product.Price, // Preis IMMER frisch aus der DB nehmen! UnitPrice = product.Price,
TotalPrice = product.Price * cartItem.Quantity TotalPrice = product.Price * itemDto.Quantity
}; };
preparedOrderItems.Add(orderItem); orderItems.Add(orderItem);
itemsTotal += orderItem.TotalPrice; itemsTotal += orderItem.TotalPrice;
} }
}
// Abbruch bei Produktfehlern
if (validationErrors.Length > 0)
{
await transaction.RollbackAsync();
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Conflict, $"Bestellung nicht möglich:\n{validationErrors}");
}
// 1.6 Gewichtsprüfung
if (totalOrderWeight < shippingMethod.MinWeight || totalOrderWeight > shippingMethod.MaxWeight)
{
await transaction.RollbackAsync();
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, $"Gesamtgewicht ({totalOrderWeight} kg) passt nicht zur Versandart '{shippingMethod.Name}'.");
}
// =================================================================================
// PHASE 2: BERECHNUNG (Finanzen)
// =================================================================================
// --- 3. Preise, Rabatte, Steuern und Gesamtbetrag berechnen ---
decimal discountAmount = 0; decimal discountAmount = 0;
var discountResult = new DiscountCalculationResult(); var discountResult = new DiscountCalculationResult();
// Rabatt berechnen
if (!string.IsNullOrWhiteSpace(orderDto.CouponCode)) if (!string.IsNullOrWhiteSpace(orderDto.CouponCode))
{ {
discountResult = await _discountService.CalculateDiscountAsync(preparedOrderItems, orderDto.CouponCode); discountResult = await _discountService.CalculateDiscountAsync(orderItems, orderDto.CouponCode);
discountAmount = discountResult.TotalDiscountAmount;
if (!discountResult.IsValid) if (discountAmount <= 0)
{ {
await transaction.RollbackAsync(); await transaction.RollbackAsync();
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, $"Gutscheinfehler: {discountResult.ErrorMessage ?? "Code ungültig."}"); return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "Der angegebene Gutscheincode ist ungültig, abgelaufen oder nicht auf die Produkte im Warenkorb anwendbar.");
} }
discountAmount = discountResult.TotalDiscountAmount;
if (discountResult.AppliedDiscountIds.Count > 1)
{
await transaction.RollbackAsync();
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Failure, "Es können nicht mehrere Rabatte gleichzeitig angewendet werden.");
}
} }
// Gesamtsummen berechnen
decimal shippingCost = shippingMethod.BaseCost; decimal shippingCost = shippingMethod.BaseCost;
decimal subTotalBeforeDiscount = itemsTotal + shippingCost; decimal subTotalBeforeDiscount = itemsTotal + shippingCost;
// Rabatt darf nicht höher als die Summe sein
if (discountAmount > subTotalBeforeDiscount) if (discountAmount > subTotalBeforeDiscount)
{ {
discountAmount = subTotalBeforeDiscount; discountAmount = subTotalBeforeDiscount;
@@ -259,34 +130,15 @@ namespace Webshop.Application.Services.Customers
decimal subTotalAfterDiscount = subTotalBeforeDiscount - discountAmount; decimal subTotalAfterDiscount = subTotalBeforeDiscount - discountAmount;
// Steuer berechnen (Settings abrufen)
decimal taxRate = await _settingService.GetSettingValueAsync<decimal>("GlobalTaxRate", 0.19m); decimal taxRate = await _settingService.GetSettingValueAsync<decimal>("GlobalTaxRate", 0.19m);
decimal taxAmount = subTotalAfterDiscount * taxRate; decimal taxAmount = subTotalAfterDiscount * taxRate;
decimal orderTotal = subTotalAfterDiscount + taxAmount; decimal orderTotal = subTotalAfterDiscount + taxAmount;
// ================================================================================= // --- 4. Bestellung erstellen ---
// PHASE 3: EXECUTION (Schreiben in DB)
// =================================================================================
// 3.1 Lagerbestand reduzieren
foreach (var oi in preparedOrderItems)
{
var product = products.First(p => p.Id == oi.ProductId);
// Letzte Sicherheitsprüfung (Concurrency)
if (product.StockQuantity < oi.Quantity)
{
await transaction.RollbackAsync();
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Conflict, $"Lagerbestand für '{product.Name}' hat sich während des Vorgangs geändert.");
}
product.StockQuantity -= oi.Quantity;
}
// 3.2 Order erstellen
var newOrder = new Order var newOrder = new Order
{ {
CustomerId = customer.Id, CustomerId = customer.Id,
OrderNumber = GenerateOrderNumber(), 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(), PaymentStatus = PaymentStatus.Pending.ToString(),
@@ -299,72 +151,33 @@ namespace Webshop.Application.Services.Customers
ShippingMethodId = shippingMethod.Id, ShippingMethodId = shippingMethod.Id,
BillingAddressId = orderDto.BillingAddressId, BillingAddressId = orderDto.BillingAddressId,
ShippingAddressId = orderDto.ShippingAddressId, ShippingAddressId = orderDto.ShippingAddressId,
OrderItems = preparedOrderItems OrderItems = orderItems
}; };
await _orderRepository.AddAsync(newOrder); await _orderRepository.AddAsync(newOrder);
// 3.3 Discount Nutzung erhöhen // --- 5. Rabattnutzung erhöhen ---
if (discountResult.AppliedDiscountIds.Any()) if (discountResult.AppliedDiscountIds.Any())
{ {
var appliedDiscounts = await _context.Discounts var appliedDiscounts = await _context.Discounts.Where(d => discountResult.AppliedDiscountIds.Contains(d.Id)).ToListAsync();
.Where(d => discountResult.AppliedDiscountIds.Contains(d.Id)) foreach (var discount in appliedDiscounts) { discount.CurrentUsageCount++; }
.ToListAsync();
foreach (var d in appliedDiscounts) d.CurrentUsageCount++;
} }
// 3.4 Speichern & Commit
try
{
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
await transaction.CommitAsync(); await transaction.CommitAsync();
}
catch (DbUpdateConcurrencyException ex)
{
await transaction.RollbackAsync();
_logger.LogWarning(ex, "Concurrency Fehler beim Checkout (User {UserId}).", userId);
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Conflict, "Ein oder mehrere Artikel wurden soeben von einem anderen Kunden gekauft.");
}
// ================================================================================= // --- 6. Erfolgreiche Antwort erstellen ---
// PHASE 4: CLEANUP & POST-PROCESSING var createdOrder = await _orderRepository.GetByIdAsync(newOrder.Id);
// ================================================================================= return ServiceResult.Ok(MapToOrderDetailDto(createdOrder!));
// 4.1 Warenkorb leeren
try
{
await _cartService.ClearCartAsync(userId!);
}
catch (Exception ex) { _logger.LogError(ex, "Cart Clear Failed for Order {OrderNumber}", newOrder.OrderNumber); }
// 4.2 Bestätigungs-Email senden
try
{
await _emailService.SendOrderConfirmationAsync(newOrder.Id, userEmail);
}
catch (Exception ex) { _logger.LogError(ex, "Email Failed for Order {OrderNumber}", newOrder.OrderNumber); }
// 4.3 Mapping für Rückgabe
// Wir setzen die Navigation Properties manuell, um DB-Reload zu sparen
newOrder.PaymentMethodInfo = paymentMethod;
newOrder.ShippingAddress = addresses.First(a => a.Id == orderDto.ShippingAddressId);
newOrder.BillingAddress = addresses.First(a => a.Id == orderDto.BillingAddressId);
return ServiceResult.Ok(MapToOrderDetailDto(newOrder));
} }
catch (Exception ex) catch (Exception ex)
{ {
await transaction.RollbackAsync(); await transaction.RollbackAsync();
_logger.LogError(ex, "Unerwarteter Fehler im Checkout für User {UserId}", userId); // Log the full exception for debugging purposes
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Failure, "Ein unerwarteter Fehler ist aufgetreten."); // _logger.LogError(ex, "An unexpected error occurred in CreateOrderAsync.");
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Failure, "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.");
} }
} }
private string GenerateOrderNumber()
{
return $"WS-{DateTime.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString().Substring(0, 6).ToUpper()}";
}
private OrderDetailDto MapToOrderDetailDto(Order order) private OrderDetailDto MapToOrderDetailDto(Order order)
{ {
return new OrderDetailDto return new OrderDetailDto
@@ -399,8 +212,11 @@ namespace Webshop.Application.Services.Customers
Country = order.BillingAddress.Country, Country = order.BillingAddress.Country,
Type = order.BillingAddress.Type Type = order.BillingAddress.Type
}, },
PaymentMethod = order.PaymentMethodInfo?.Name ?? order.PaymentMethod, PaymentMethod = order.PaymentMethodInfo?.Name,
PaymentStatus = Enum.TryParse<PaymentStatus>(order.PaymentStatus, true, out var ps) ? ps : PaymentStatus.Pending, PaymentStatus = Enum.TryParse<PaymentStatus>(order.PaymentStatus, true, out var ps) ? ps : PaymentStatus.Pending,
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

@@ -1,14 +1,13 @@
// src/Webshop.Application/Services/Customers/CustomerService.cs
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using System; // Für Uri using Resend;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Webshop.Application; // Für ServiceResult using System.Web;
using Webshop.Application.DTOs;
using Webshop.Application.DTOs.Auth; using Webshop.Application.DTOs.Auth;
using Webshop.Application.DTOs.Customers; using Webshop.Application.DTOs.Customers;
using Webshop.Application.Services.Customers.Interfaces; // Namespace korrigiert
using Webshop.Application.Services.Public.Interfaces; // WICHTIG: Für IEmailService
using Webshop.Domain.Entities;
using Webshop.Domain.Identity; using Webshop.Domain.Identity;
using Webshop.Domain.Interfaces; using Webshop.Domain.Interfaces;
@@ -19,18 +18,14 @@ namespace Webshop.Application.Services.Customers
private readonly ICustomerRepository _customerRepository; private readonly ICustomerRepository _customerRepository;
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly IEmailService _emailService; // NEU: Statt IResend private readonly IResend _resend;
public CustomerService( public CustomerService(ICustomerRepository customerRepository, UserManager<ApplicationUser> userManager, IConfiguration configuration, IResend resend)
ICustomerRepository customerRepository,
UserManager<ApplicationUser> userManager,
IConfiguration configuration,
IEmailService emailService) // NEU: Injected
{ {
_customerRepository = customerRepository; _customerRepository = customerRepository;
_userManager = userManager; _userManager = userManager;
_configuration = configuration; _configuration = configuration;
_emailService = emailService; _resend = resend;
} }
public async Task<ServiceResult<CustomerDto>> GetMyProfileAsync(string userId) public async Task<ServiceResult<CustomerDto>> GetMyProfileAsync(string userId)
@@ -65,20 +60,17 @@ namespace Webshop.Application.Services.Customers
var identityUser = await _userManager.FindByIdAsync(userId); var identityUser = await _userManager.FindByIdAsync(userId);
if (identityUser == null) return ServiceResult.Fail(ServiceResultType.NotFound, "Benutzerkonto nicht gefunden."); if (identityUser == null) return ServiceResult.Fail(ServiceResultType.NotFound, "Benutzerkonto nicht gefunden.");
// Sicherheitscheck: Passwort prüfen
if (!await _userManager.CheckPasswordAsync(identityUser, profileDto.CurrentPassword)) if (!await _userManager.CheckPasswordAsync(identityUser, profileDto.CurrentPassword))
{ {
return ServiceResult.Fail(ServiceResultType.InvalidInput, "Das zur Bestätigung eingegebene Passwort ist falsch."); return ServiceResult.Fail(ServiceResultType.InvalidInput, "Das zur Bestätigung eingegebene Passwort ist falsch.");
} }
// Customer Daten aktualisieren
customer.FirstName = profileDto.FirstName; customer.FirstName = profileDto.FirstName;
customer.LastName = profileDto.LastName; customer.LastName = profileDto.LastName;
customer.DefaultShippingAddressId = profileDto.DefaultShippingAddressId; customer.DefaultShippingAddressId = profileDto.DefaultShippingAddressId;
customer.DefaultBillingAddressId = profileDto.DefaultBillingAddressId; customer.DefaultBillingAddressId = profileDto.DefaultBillingAddressId;
await _customerRepository.UpdateAsync(customer); await _customerRepository.UpdateAsync(customer);
// User Daten (Telefon) aktualisieren
if (!string.IsNullOrEmpty(profileDto.PhoneNumber) && identityUser.PhoneNumber != profileDto.PhoneNumber) if (!string.IsNullOrEmpty(profileDto.PhoneNumber) && identityUser.PhoneNumber != profileDto.PhoneNumber)
{ {
identityUser.PhoneNumber = profileDto.PhoneNumber; identityUser.PhoneNumber = profileDto.PhoneNumber;
@@ -111,29 +103,28 @@ namespace Webshop.Application.Services.Customers
var user = await _userManager.FindByIdAsync(userId); var user = await _userManager.FindByIdAsync(userId);
if (user == null) return ServiceResult.Fail(ServiceResultType.NotFound, "Benutzer nicht gefunden."); if (user == null) return ServiceResult.Fail(ServiceResultType.NotFound, "Benutzer nicht gefunden.");
// Passwort prüfen
if (!await _userManager.CheckPasswordAsync(user, currentPassword)) if (!await _userManager.CheckPasswordAsync(user, currentPassword))
{ {
return ServiceResult.Fail(ServiceResultType.InvalidInput, "Das zur Bestätigung eingegebene Passwort ist falsch."); return ServiceResult.Fail(ServiceResultType.InvalidInput, "Das zur Bestätigung eingegebene Passwort ist falsch.");
} }
// Prüfen, ob neue Email schon existiert (außer es ist die eigene)
if (user.Email != newEmail && await _userManager.FindByEmailAsync(newEmail) != null) if (user.Email != newEmail && await _userManager.FindByEmailAsync(newEmail) != null)
{ {
return ServiceResult.Fail(ServiceResultType.Conflict, "Die neue E-Mail-Adresse ist bereits registriert."); return ServiceResult.Fail(ServiceResultType.Conflict, "Die neue E-Mail-Adresse ist bereits registriert.");
} }
// Token generieren
var token = await _userManager.GenerateChangeEmailTokenAsync(user, newEmail); var token = await _userManager.GenerateChangeEmailTokenAsync(user, newEmail);
var clientUrl = _configuration["App:ClientUrl"]; // Hier stand vorher ! , besser prüfen oder Null-Check var clientUrl = _configuration["App:ClientUrl"]!;
var confirmationLink = $"{clientUrl}/confirm-email-change?userId={user.Id}&newEmail={HttpUtility.UrlEncode(newEmail)}&token={HttpUtility.UrlEncode(token)}";
// Link bauen (Uri.EscapeDataString ist sicherer in URLs als HttpUtility) var message = new EmailMessage();
var confirmationLink = $"{clientUrl}/confirm-email-change?userId={user.Id}&newEmail={Uri.EscapeDataString(newEmail)}&token={Uri.EscapeDataString(token)}"; message.From = _configuration["Resend:FromEmail"]!;
message.To.Add(newEmail);
message.Subject = "Bestätigen Sie Ihre neue E-Mail-Adresse";
message.HtmlBody = $"<h1>E-Mail-Änderung Bestätigung</h1><p>Bitte klicken Sie auf den folgenden Link, um Ihre neue E-Mail-Adresse zu bestätigen:</p><p><a href='{confirmationLink}'>Neue E-Mail-Adresse bestätigen</a></p>";
await _resend.EmailSendAsync(message);
// --- REFACTORED: Nutzung des EmailService --- // Die Erfolgsmeldung wird hier als Teil des "Ok"-Results übermittelt
await _emailService.SendEmailChangeConfirmationAsync(newEmail, confirmationLink);
// Wir geben die Erfolgsmeldung im Result zurück (passend zu deinem Controller)
return ServiceResult.Ok("Bestätigungs-E-Mail wurde an die neue Adresse gesendet. Bitte prüfen Sie Ihr Postfach."); return ServiceResult.Ok("Bestätigungs-E-Mail wurde an die neue Adresse gesendet. Bitte prüfen Sie Ihr Postfach.");
} }
} }

View File

@@ -1,16 +0,0 @@
using System.Threading.Tasks;
using System; // Für Guid
using Webshop.Application.DTOs.Customers; // <--- WICHTIG für CartDto
using Webshop.Application.DTOs.Shipping;
using Webshop.Application;
namespace Webshop.Application.Services.Customers.Interfaces
{
public interface ICartService
{
Task<ServiceResult<CartDto>> GetCartAsync(string userId);
Task<ServiceResult> AddToCartAsync(string userId, CartItemDto item);
Task<ServiceResult> RemoveFromCartAsync(string userId, Guid productId);
Task ClearCartAsync(string userId);
}
}

View File

@@ -1,20 +1,13 @@
using System.Collections.Generic; // src/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Webshop.Application;
using Webshop.Application.DTOs.Orders; using Webshop.Application.DTOs.Orders;
using Webshop.Application.DTOs.Shipping;
using Webshop.Application; // F<>r ServiceResult
namespace Webshop.Application.Services.Customers.Interfaces namespace Webshop.Application.Services.Customers.Interfaces
{ {
public interface ICheckoutService public interface ICheckoutService
{ {
// Methode 1: F<>r den Checkout (interne Verarbeitung)
Task<ServiceResult<IEnumerable<ShippingMethodDto>>> GetCompatibleShippingMethodsAsync(IEnumerable<CreateOrderItemDto> items);
// +++ METHODE 2: DIESE FEHLTE +++
// F<>r den Warenkorb-Check (kommt vom Controller als List<CartItemDto>)
Task<ServiceResult<IEnumerable<ShippingMethodDto>>> GetCompatibleShippingMethodsAsync(List<CartItemDto> items);
Task<ServiceResult<OrderDetailDto>> CreateOrderAsync(CreateOrderDto orderDto, string? userId); Task<ServiceResult<OrderDetailDto>> CreateOrderAsync(CreateOrderDto orderDto, string? userId);
} }
} }

View File

@@ -1,9 +1,11 @@
using System.Threading.Tasks; // src/Webshop.Application/Services/Customers/ICustomerService.cs
using Webshop.Application; // Für ServiceResult using System.Threading.Tasks;
using Webshop.Application;
using Webshop.Application.DTOs;
using Webshop.Application.DTOs.Auth; using Webshop.Application.DTOs.Auth;
using Webshop.Application.DTOs.Customers; using Webshop.Application.DTOs.Customers;
namespace Webshop.Application.Services.Customers.Interfaces // Namespace angepasst auf .Interfaces namespace Webshop.Application.Services.Customers
{ {
public interface ICustomerService public interface ICustomerService
{ {

View File

@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Webshop.Application.Services.Public
{
public class DiscountCalculationResult
{
public decimal TotalDiscountAmount { get; set; } = 0;
public List<Guid> AppliedDiscountIds { get; set; } = new List<Guid>();
public string? ErrorMessage { get; set; }
// FIX: IsValid existierte nicht. Wir definieren es hier:
// Ein Resultat ist gültig, wenn es keine Fehlermeldung gibt.
// (Oder alternativ: wenn der Betrag > 0 ist, je nach Logik)
public bool IsValid => string.IsNullOrEmpty(ErrorMessage);
}
}

View File

@@ -1,133 +0,0 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration; // Für Absender-Config
using Resend;
using System;
using System.IO;
using System.Threading.Tasks;
using Webshop.Application.Services.Public.Interfaces;
namespace Webshop.Application.Services.Public
{
public class EmailService : IEmailService
{
private readonly IResend _resend;
private readonly IWebHostEnvironment _env;
private readonly ILogger<EmailService> _logger;
private readonly IConfiguration _configuration;
public EmailService(
IResend resend,
IWebHostEnvironment env,
ILogger<EmailService> logger,
IConfiguration configuration)
{
_resend = resend;
_env = env;
_logger = logger;
_configuration = configuration;
}
public async Task SendEmailConfirmationAsync(string toEmail, string confirmationLink)
{
var subject = "Willkommen! Bitte bestätigen Sie Ihre E-Mail-Adresse";
var title = "E-Mail bestätigen";
var body = "Vielen Dank für Ihre Registrierung! Bitte klicken Sie auf den Button unten, um Ihr Konto zu aktivieren.";
var btnText = "Konto aktivieren";
await SendEmailInternalAsync(toEmail, subject, title, body, btnText, confirmationLink);
}
public async Task SendPasswordResetAsync(string toEmail, string resetLink)
{
var subject = "Passwort zurücksetzen";
var title = "Passwort vergessen?";
var body = "Wir haben eine Anfrage zum Zurücksetzen Ihres Passworts erhalten. Klicken Sie hier:";
var btnText = "Passwort zurücksetzen";
await SendEmailInternalAsync(toEmail, subject, title, body, btnText, resetLink);
}
public async Task SendOrderConfirmationAsync(Guid orderId, string toEmail)
{
// Link zur Bestellübersicht (anpassen an dein Frontend Routing)
var orderLink = $"{_configuration["App:FrontendUrl"]}/orders/{orderId}";
var subject = $"Bestellbestätigung #{orderId.ToString().Substring(0, 8).ToUpper()}";
var title = "Vielen Dank für Ihre Bestellung!";
var body = $"Wir haben Ihre Bestellung #{orderId} erhalten und bearbeiten sie umgehend.";
var btnText = "Bestellung ansehen";
await SendEmailInternalAsync(toEmail, subject, title, body, btnText, orderLink);
}
public async Task SendEmailChangeConfirmationAsync(string toEmail, string confirmationLink)
{
var subject = "Änderung Ihrer E-Mail-Adresse bestätigen";
var title = "Neue E-Mail bestätigen";
var body = "Sie haben eine Änderung Ihrer E-Mail-Adresse angefordert. Bitte bestätigen Sie dies:";
var btnText = "E-Mail bestätigen";
await SendEmailInternalAsync(toEmail, subject, title, body, btnText, confirmationLink);
}
// --- Interne Methode für Resend ---
private async Task SendEmailInternalAsync(string toEmail, string subject, string title, string body, string btnText, string btnLink)
{
try
{
// 1. Template laden
string htmlContent = await LoadAndFormatTemplate(title, body, btnText, btnLink);
// 2. Resend Message bauen
var message = new EmailMessage();
message.To.Add(toEmail);
// Hole Absender aus Config oder Fallback
message.From = _configuration["Resend:FromEmail"] ?? "noreply@deinwebshop.com";
message.Subject = subject;
message.HtmlBody = htmlContent;
// 3. Senden
await _resend.EmailSendAsync(message);
_logger.LogInformation($"E-Mail '{subject}' erfolgreich an {toEmail} gesendet via Resend.");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Fehler beim Senden der E-Mail an {toEmail} via Resend.");
// Optional: Exception werfen, wenn der Prozess abbrechen soll.
// Meistens besser: Loggen und "Silent Fail", damit z.B. der Checkout nicht abbricht.
}
}
private async Task<string> LoadAndFormatTemplate(string title, string body, string btnText, string btnLink)
{
// Pfad: wwwroot/templates/email-template.html
var templatePath = Path.Combine(AppContext.BaseDirectory, "Templates", "_EmailTemplate.html");
string template;
if (File.Exists(templatePath))
{
template = await File.ReadAllTextAsync(templatePath);
}
else
{
// Fallback HTML, falls Datei fehlt
_logger.LogWarning($"Template nicht gefunden unter {templatePath}");
template = "<html><body><h1>{{ShopName}}</h1><h2>{{Titel}}</h2><p>{{Haupttext}}</p><p><a href='{{CallToActionLink}}'>{{CallToActionText}}</a></p></body></html>";
}
// Platzhalter ersetzen
var shopName = _configuration["ShopInfo:Name"] ?? "Webshop";
return template
.Replace("{{ShopName}}", shopName)
.Replace("{{Titel}}", title)
.Replace("{{Haupttext}}", body)
.Replace("{{CallToActionText}}", btnText)
.Replace("{{CallToActionLink}}", btnLink)
.Replace("{{Jahr}}", DateTime.UtcNow.Year.ToString());
}
}
}

View File

@@ -6,7 +6,21 @@ 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
{ {

View File

@@ -1,28 +0,0 @@
using System;
using System.Threading.Tasks;
namespace Webshop.Application.Services.Public.Interfaces
{
public interface IEmailService
{
/// <summary>
/// Sendet den Link zur Bestätigung der Registrierung.
/// </summary>
Task SendEmailConfirmationAsync(string toEmail, string confirmationLink);
/// <summary>
/// Sendet den Link zum Zurücksetzen des Passworts.
/// </summary>
Task SendPasswordResetAsync(string toEmail, string resetLink);
/// <summary>
/// Sendet eine Bestellbestätigung.
/// </summary>
Task SendOrderConfirmationAsync(Guid orderId, string toEmail);
/// <summary>
/// Sendet einen Bestätigungslink bei E-Mail-Änderung.
/// </summary>
Task SendEmailChangeConfirmationAsync(string toEmail, string confirmationLink);
}
}

View File

@@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Webshop.Domain.Entities
{
public class Cart
{
[Key]
public Guid Id { get; set; }
// Verknüpfung zum User (String, da Identity User Id ein String ist)
public string? UserId { get; set; }
// Falls du auch Gast-Warenkörbe ohne Login unterstützt:
public string? SessionId { get; set; }
public DateTime LastModified { get; set; } = DateTime.UtcNow;
public virtual ICollection<CartItem> Items { get; set; } = new List<CartItem>();
}
}

View File

@@ -1,26 +0,0 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Webshop.Domain.Entities
{
public class CartItem
{
[Key]
public Guid Id { get; set; }
public Guid CartId { get; set; }
[ForeignKey("CartId")]
public virtual Cart Cart { get; set; } = default!;
public Guid ProductId { get; set; }
[ForeignKey("ProductId")]
public virtual Product Product { get; set; } = default!;
public Guid? ProductVariantId { get; set; } // Optional, falls du Varianten nutzt
public int Quantity { get; set; }
}
}

View File

@@ -53,7 +53,5 @@ namespace Webshop.Domain.Entities
public virtual ICollection<ProductDiscount> ProductDiscounts { get; set; } = new List<ProductDiscount>(); public virtual ICollection<ProductDiscount> ProductDiscounts { get; set; } = new List<ProductDiscount>();
public virtual ICollection<Productcategorie> Productcategories { get; set; } = new List<Productcategorie>(); public virtual ICollection<Productcategorie> Productcategories { get; set; } = new List<Productcategorie>();
public virtual ICollection<ProductImage> Images { get; set; } = new List<ProductImage>(); public virtual ICollection<ProductImage> Images { get; set; } = new List<ProductImage>();
[ConcurrencyCheck]
public byte[] RowVersion { get; set; }
} }
} }

View File

@@ -1,28 +1,27 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Webshop.Domain.Entities namespace Webshop.Domain.Entities;
/// <summary>
/// Konfigurierbare Versandoptionen für den Checkout.
/// </summary>
public class ShippingMethod
{ {
/// <summary>
/// Konfigurierbare Versandoptionen für den Checkout.
/// </summary>
public class ShippingMethod
{
[Key] [Key]
public Guid Id { get; set; } public Guid Id { get; set; }
[Required] [Required]
[MaxLength(100)] [MaxLength(100)]
public string Name { get; set; } = string.Empty; public string Name { get; set; }
[MaxLength(500)] [MaxLength(500)]
public string? Description { get; set; } public string? Description { get; set; }
// Grundkosten der Versandmethode // Precision wird via Fluent API konfiguriert
[Required] [Required]
public decimal BaseCost { get; set; } public decimal BaseCost { get; set; }
// Optional: Mindestbestellwert (für kostenlosen Versand etc.)
public decimal? MinimumOrderAmount { get; set; } public decimal? MinimumOrderAmount { get; set; }
[Required] [Required]
@@ -36,11 +35,4 @@ namespace Webshop.Domain.Entities
public int MinDeliveryDays { get; set; } public int MinDeliveryDays { get; set; }
public int MaxDeliveryDays { get; set; } public int MaxDeliveryDays { get; set; }
// NEU: Gewichtsbasierte Berechnung
// Einheit: kg (oder die Standardeinheit deines Shops)
public decimal MinWeight { get; set; } = 0;
public decimal MaxWeight { get; set; } = 0;
}
} }

View File

@@ -13,7 +13,6 @@ namespace Webshop.Domain.Interfaces
Task<Product?> GetBySlugAsync(string slug); Task<Product?> GetBySlugAsync(string slug);
Task AddProductAsync(Product product); Task AddProductAsync(Product product);
Task UpdateProductAsync(Product product); Task UpdateProductAsync(Product product);
Task SaveChangesAsync();
Task DeleteProductAsync(Guid id); Task DeleteProductAsync(Guid id);
} }
} }

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Webshop.Domain.Entities; using Webshop.Domain.Entities;
using Webshop.Domain.Identity; using Webshop.Domain.Identity;
@@ -25,16 +26,12 @@ namespace Webshop.Infrastructure.Data
public DbSet<PaymentMethod> PaymentMethods { get; set; } = default!; public DbSet<PaymentMethod> PaymentMethods { get; set; } = default!;
public DbSet<Setting> Settings { get; set; } = default!; public DbSet<Setting> Settings { get; set; } = default!;
// Verknüpfungstabellen und Details
public DbSet<Productcategorie> Productcategories { get; set; } = default!; public DbSet<Productcategorie> Productcategories { get; set; } = default!;
public DbSet<ProductDiscount> ProductDiscounts { get; set; } = default!; public DbSet<ProductDiscount> ProductDiscounts { get; set; } = default!;
public DbSet<CategorieDiscount> categorieDiscounts { get; set; } = default!; public DbSet<CategorieDiscount> categorieDiscounts { get; set; } = default!;
public DbSet<ProductImage> ProductImages { get; set; } = default!; public DbSet<ProductImage> ProductImages { get; set; } = default!;
public DbSet<ShopInfo> ShopInfos { get; set; } = default!; public DbSet<ShopInfo> ShopInfos { get; set; } = default!;
// +++ NEU: Warenkorb Tabellen +++
public DbSet<Cart> Carts { get; set; } = default!;
public DbSet<CartItem> CartItems { get; set; } = default!;
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
@@ -51,17 +48,6 @@ namespace Webshop.Infrastructure.Data
} }
} }
// +++ NEU: Warenkorb Konfiguration +++
modelBuilder.Entity<Cart>(entity =>
{
// Wenn ein Warenkorb gelöscht wird, lösche alle Items darin automatisch
entity.HasMany(c => c.Items)
.WithOne(i => i.Cart)
.HasForeignKey(i => i.CartId)
.OnDelete(DeleteBehavior.Cascade);
});
// Bestehende Konfigurationen (bleiben unverändert)
modelBuilder.Entity<Productcategorie>().HasKey(pc => new { pc.ProductId, pc.categorieId }); modelBuilder.Entity<Productcategorie>().HasKey(pc => new { pc.ProductId, pc.categorieId });
modelBuilder.Entity<ProductDiscount>().HasKey(pd => new { pd.ProductId, pd.DiscountId }); modelBuilder.Entity<ProductDiscount>().HasKey(pd => new { pd.ProductId, pd.DiscountId });
modelBuilder.Entity<CategorieDiscount>().HasKey(cd => new { cd.categorieId, cd.DiscountId }); modelBuilder.Entity<CategorieDiscount>().HasKey(cd => new { cd.categorieId, cd.DiscountId });
@@ -157,8 +143,8 @@ namespace Webshop.Infrastructure.Data
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<ApplicationUser>() modelBuilder.Entity<ApplicationUser>()
.HasOne(a => a.Customer) .HasOne(a => a.Customer) // Ein ApplicationUser hat ein optionales Customer-Profil
.WithOne(c => c.User) .WithOne(c => c.User) // Ein Customer-Profil ist mit genau einem User verknüpft
.HasForeignKey<Customer>(c => c.AspNetUserId); .HasForeignKey<Customer>(c => c.AspNetUserId);
modelBuilder.Entity<ProductDiscount>(entity => modelBuilder.Entity<ProductDiscount>(entity =>
@@ -172,6 +158,8 @@ namespace Webshop.Infrastructure.Data
.HasForeignKey(pd => pd.DiscountId); .HasForeignKey(pd => pd.DiscountId);
}); });
// << NEU: Beziehungskonfiguration für CategorieDiscount >>
modelBuilder.Entity<CategorieDiscount>(entity => modelBuilder.Entity<CategorieDiscount>(entity =>
{ {
entity.HasOne(cd => cd.categorie) entity.HasOne(cd => cd.categorie)
@@ -185,16 +173,20 @@ namespace Webshop.Infrastructure.Data
modelBuilder.Entity<Review>(entity => modelBuilder.Entity<Review>(entity =>
{ {
// Beziehung zu Product
entity.HasOne(r => r.Product) entity.HasOne(r => r.Product)
.WithMany(p => p.Reviews) .WithMany(p => p.Reviews)
.HasForeignKey(r => r.ProductId) .HasForeignKey(r => r.ProductId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade); // Lösche Bewertungen, wenn das Produkt gelöscht wird
// Beziehung zu Customer
entity.HasOne(r => r.Customer) entity.HasOne(r => r.Customer)
.WithMany(c => c.Reviews) .WithMany(c => c.Reviews)
.HasForeignKey(r => r.CustomerId) .HasForeignKey(r => r.CustomerId)
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull); // Setze CustomerId auf NULL, wenn der Kunde gelöscht wird
}); });
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Webshop.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class row : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<byte[]>(
name: "RowVersion",
table: "Products",
type: "bytea",
rowVersion: true,
nullable: false,
defaultValue: new byte[0]);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "RowVersion",
table: "Products");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Webshop.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class rowversion : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Webshop.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class rowversion2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Webshop.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class rowversion3 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Webshop.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class rowversion4 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@@ -1,40 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Webshop.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class shippingmethodWeight : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "MaxWeight",
table: "ShippingMethods",
type: "numeric",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "MinWeight",
table: "ShippingMethods",
type: "numeric",
nullable: false,
defaultValue: 0m);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "MaxWeight",
table: "ShippingMethods");
migrationBuilder.DropColumn(
name: "MinWeight",
table: "ShippingMethods");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Webshop.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class checkout : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,76 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Webshop.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class checkoutcart : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Carts",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<string>(type: "text", nullable: true),
SessionId = table.Column<string>(type: "text", nullable: true),
LastModified = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Carts", x => x.Id);
});
migrationBuilder.CreateTable(
name: "CartItems",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
CartId = table.Column<Guid>(type: "uuid", nullable: false),
ProductId = table.Column<Guid>(type: "uuid", nullable: false),
ProductVariantId = table.Column<Guid>(type: "uuid", nullable: true),
Quantity = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CartItems", x => x.Id);
table.ForeignKey(
name: "FK_CartItems_Carts_CartId",
column: x => x.CartId,
principalTable: "Carts",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_CartItems_Products_ProductId",
column: x => x.ProductId,
principalTable: "Products",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_CartItems_CartId",
table: "CartItems",
column: "CartId");
migrationBuilder.CreateIndex(
name: "IX_CartItems_ProductId",
table: "CartItems",
column: "ProductId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CartItems");
migrationBuilder.DropTable(
name: "Carts");
}
}
}

View File

@@ -215,53 +215,6 @@ namespace Webshop.Infrastructure.Migrations
b.ToTable("Addresses"); b.ToTable("Addresses");
}); });
modelBuilder.Entity("Webshop.Domain.Entities.Cart", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("LastModified")
.HasColumnType("timestamp with time zone");
b.Property<string>("SessionId")
.HasColumnType("text");
b.Property<string>("UserId")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Carts");
});
modelBuilder.Entity("Webshop.Domain.Entities.CartItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("CartId")
.HasColumnType("uuid");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.Property<Guid?>("ProductVariantId")
.HasColumnType("uuid");
b.Property<int>("Quantity")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("CartId");
b.HasIndex("ProductId");
b.ToTable("CartItems");
});
modelBuilder.Entity("Webshop.Domain.Entities.Categorie", b => modelBuilder.Entity("Webshop.Domain.Entities.Categorie", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -660,11 +613,6 @@ namespace Webshop.Infrastructure.Migrations
.HasPrecision(18, 2) .HasPrecision(18, 2)
.HasColumnType("numeric(18,2)"); .HasColumnType("numeric(18,2)");
b.Property<byte[]>("RowVersion")
.IsConcurrencyToken()
.IsRequired()
.HasColumnType("bytea");
b.Property<string>("SKU") b.Property<string>("SKU")
.IsRequired() .IsRequired()
.HasMaxLength(50) .HasMaxLength(50)
@@ -902,15 +850,9 @@ namespace Webshop.Infrastructure.Migrations
b.Property<int>("MaxDeliveryDays") b.Property<int>("MaxDeliveryDays")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<decimal>("MaxWeight")
.HasColumnType("numeric");
b.Property<int>("MinDeliveryDays") b.Property<int>("MinDeliveryDays")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<decimal>("MinWeight")
.HasColumnType("numeric");
b.Property<decimal?>("MinimumOrderAmount") b.Property<decimal?>("MinimumOrderAmount")
.HasPrecision(18, 2) .HasPrecision(18, 2)
.HasColumnType("numeric(18,2)"); .HasColumnType("numeric(18,2)");
@@ -1160,25 +1102,6 @@ namespace Webshop.Infrastructure.Migrations
b.Navigation("Customer"); b.Navigation("Customer");
}); });
modelBuilder.Entity("Webshop.Domain.Entities.CartItem", b =>
{
b.HasOne("Webshop.Domain.Entities.Cart", "Cart")
.WithMany("Items")
.HasForeignKey("CartId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Webshop.Domain.Entities.Product", "Product")
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Cart");
b.Navigation("Product");
});
modelBuilder.Entity("Webshop.Domain.Entities.Categorie", b => modelBuilder.Entity("Webshop.Domain.Entities.Categorie", b =>
{ {
b.HasOne("Webshop.Domain.Entities.Categorie", "Parentcategorie") b.HasOne("Webshop.Domain.Entities.Categorie", "Parentcategorie")
@@ -1377,11 +1300,6 @@ namespace Webshop.Infrastructure.Migrations
b.Navigation("Address"); b.Navigation("Address");
}); });
modelBuilder.Entity("Webshop.Domain.Entities.Cart", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("Webshop.Domain.Entities.Categorie", b => modelBuilder.Entity("Webshop.Domain.Entities.Categorie", b =>
{ {
b.Navigation("Productcategories"); b.Navigation("Productcategories");

View File

@@ -55,9 +55,5 @@ namespace Webshop.Infrastructure.Repositories
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
} }
} }
public async Task SaveChangesAsync()
{
await _context.SaveChangesAsync();
}
} }
} }