This commit is contained in:
Tizian.Breuch
2025-11-26 17:02:18 +01:00
parent 2e30755901
commit ee5abfd829
18 changed files with 1968 additions and 208 deletions

View File

@@ -4,9 +4,11 @@ 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;
namespace Webshop.Api.Controllers.Customer namespace Webshop.Api.Controllers.Customer
{ {
@@ -22,11 +24,26 @@ namespace Webshop.Api.Controllers.Customer
_checkoutService = checkoutService; _checkoutService = checkoutService;
} }
// --- NEU: Endpoint um verf<72>gbare Versandmethoden basierend auf dem Warenkorb zu holen ---
[HttpPost("available-shipping-methods")]
[ProducesResponseType(typeof(IEnumerable<ShippingMethodDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> GetAvailableShippingMethods([FromBody] ShippingCalculationRequestDto request)
{
var result = await _checkoutService.GetCompatibleShippingMethodsAsync(request.Items);
return result.Type switch
{
ServiceResultType.Success => Ok(result.Value),
ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }),
_ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = "Fehler beim Laden der Versandmethoden." })
};
}
[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)
{ {
@@ -35,7 +52,6 @@ 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))
{ {
@@ -50,7 +66,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(), // Forbid returns 403 without a body ServiceResultType.Forbidden => Forbid(),
_ => 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

@@ -12,6 +12,7 @@ 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;
@@ -111,8 +112,9 @@ 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>();
@@ -121,17 +123,32 @@ 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>(); builder.Services.AddScoped<IDiscountRepository, DiscountRepository>(); // War doppelt
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<IPaymentMethodService, PaymentMethodService>();
builder.Services.AddScoped<ICategorieService, CategorieService>(); builder.Services.AddScoped<ICategorieService, CategorieService>();
builder.Services.AddScoped<IPaymentMethodService, PaymentMethodService>();
builder.Services.AddScoped<IShopInfoService, ShopInfoService>();
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>();
@@ -139,21 +156,9 @@ 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>(); builder.Services.AddScoped<IAdminDiscountService, AdminDiscountService>(); // War doppelt
builder.Services.AddScoped<IAdminSettingService, AdminSettingService>(); builder.Services.AddScoped<IAdminSettingService, AdminSettingService>(); // War doppelt
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>();

View File

@@ -0,0 +1,12 @@
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

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,13 @@
// 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

@@ -2,39 +2,37 @@
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.IO; using System.Collections.Generic;
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 System.Web; using Webshop.Application;
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;
} }
@@ -66,7 +64,9 @@ namespace Webshop.Application.Services.Auth
_context.Customers.Add(customerProfile); _context.Customers.Add(customerProfile);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
await SendEmailConfirmationEmail(user); // Link generieren und Service aufrufen
await SendConfirmationLinkAsync(user);
return ServiceResult.Ok(); return ServiceResult.Ok();
} }
@@ -102,18 +102,16 @@ 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 the successful login result return loginResult;
} }
public async Task<ServiceResult> ConfirmEmailAsync(string userId, string token) public async Task<ServiceResult> ConfirmEmailAsync(string userId, string token)
@@ -126,11 +124,10 @@ 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); // The token from the URL is already decoded by ASP.NET Core var result = await _userManager.ConfirmEmailAsync(user, token);
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.");
@@ -139,48 +136,33 @@ 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 SendEmailConfirmationEmail(user); await SendConfirmationLinkAsync(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 || !(await _userManager.IsEmailConfirmedAsync(user))) if (user != null)
{ {
return ServiceResult.Ok(); // Do not reveal user existence or status var token = await _userManager.GeneratePasswordResetTokenAsync(user);
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);
} }
var token = await _userManager.GeneratePasswordResetTokenAsync(user); // WICHTIG: Wir geben IMMER Ok zurück, auch wenn der User nicht existiert.
var clientUrl = _configuration["App:ClientUrl"] ?? "http://localhost:3000"; // Das verhindert "User Enumeration" (Hacker können nicht prüfen, welche E-Mails existieren).
// 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();
} }
@@ -189,7 +171,6 @@ 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.");
} }
@@ -200,26 +181,17 @@ 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)));
} }
private async Task SendEmailConfirmationEmail(ApplicationUser user) // --- Helper Methods ---
private async Task SendConfirmationLinkAsync(ApplicationUser user)
{ {
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var encodedToken = HttpUtility.UrlEncode(token); var clientUrl = _configuration["App:ClientUrl"]; // z.B. https://localhost:5001/api/v1/Auth
var clientUrl = _configuration["App:ClientUrl"]!;
var confirmationLink = $"{clientUrl}/confirm-email?userId={user.Id}&token={encodedToken}";
var emailHtmlBody = await LoadAndFormatEmailTemplate( // WICHTIG: Uri.EscapeDataString ist sicherer für Token in URLs als HttpUtility
"Bestätigen Sie Ihre E-Mail-Adresse", var confirmationLink = $"{clientUrl}/confirm-email?userId={user.Id}&token={Uri.EscapeDataString(token)}";
"Vielen Dank für Ihre Registrierung! Bitte klicken Sie auf den Button unten, um Ihr Konto zu aktivieren.",
"Konto aktivieren",
confirmationLink
);
var message = new EmailMessage(); await _emailService.SendEmailConfirmationAsync(user.Email!, confirmationLink);
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)
@@ -251,22 +223,5 @@ 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

@@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
using Webshop.Application.Services.Customers.Interfaces;
using Webshop.Infrastructure.Data;
namespace Webshop.Application.Services.Customers
{
public class CartService : ICartService
{
private readonly ApplicationDbContext _context;
public CartService(ApplicationDbContext context)
{
_context = context;
}
public async Task ClearCartAsync(string userId)
{
// Wir suchen den Warenkorb des Users
// Annahme: Es gibt eine 'Carts' Tabelle und 'CartItems'
var cart = await _context.Carts
.Include(c => c.Items)
.FirstOrDefaultAsync(c => c.UserId == userId);
if (cart != null && cart.Items.Any())
{
// Option A: Nur Items löschen, Warenkorb behalten
_context.CartItems.RemoveRange(cart.Items);
// Option B: Ganzen Warenkorb löschen (dann wird er beim nächsten AddToCart neu erstellt)
// _context.Carts.Remove(cart);
await _context.SaveChangesAsync();
}
}
}
}

View File

@@ -1,15 +1,19 @@
// /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;
@@ -25,10 +29,20 @@ 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, IOrderRepository orderRepository, ICustomerRepository customerRepository, ApplicationDbContext context,
IShippingMethodRepository shippingMethodRepository, ISettingService settingService, IDiscountService discountService) IOrderRepository orderRepository,
ICustomerRepository customerRepository,
IShippingMethodRepository shippingMethodRepository,
ISettingService settingService,
IDiscountService discountService,
ICartService cartService,
IEmailService emailService,
ILogger<CheckoutService> logger)
{ {
_context = context; _context = context;
_orderRepository = orderRepository; _orderRepository = orderRepository;
@@ -36,6 +50,63 @@ 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)
@@ -43,47 +114,67 @@ namespace Webshop.Application.Services.Customers
await using var transaction = await _context.Database.BeginTransactionAsync(); await using var transaction = await _context.Database.BeginTransactionAsync();
try try
{ {
// --- 1. Validierung von Kunde, Adressen und Methoden --- // 1.1 Kunde Validieren
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 nicht gefunden."); return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Unauthorized, "Kundenprofil nicht gefunden.");
if (!await _context.Addresses.AnyAsync(a => a.Id == orderDto.ShippingAddressId && a.CustomerId == customer.Id) || // =================================================================================
!await _context.Addresses.AnyAsync(a => a.Id == orderDto.BillingAddressId && a.CustomerId == customer.Id)) // FIX: E-Mail holen (Passend zu deiner Entity Customer.cs)
// Customer hat keine Email, aber eine AspNetUserId. Wir holen die Mail aus der Users Tabelle.
// =================================================================================
var userEmail = await _context.Users
.Where(u => u.Id == userId) // userId entspricht customer.AspNetUserId
.Select(u => u.Email)
.FirstOrDefaultAsync();
if (string.IsNullOrEmpty(userEmail))
{ {
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Forbidden, "Ungültige oder nicht zugehörige Liefer- oder Rechnungsadresse."); return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Failure, "Keine E-Mail-Adresse für den Benutzer gefunden.");
} }
// 1.2 Adressen Validieren
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.3 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, "Ungültige oder inaktive Versandmethode."); return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "Versandmethode ungültig.");
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, "Ungültige oder inaktive Zahlungsmethode."); return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "Zahlungsmethode ungültig.");
// --- 2. Artikel verarbeiten und Lagerbestand prüfen --- // 1.4 Produkte Validieren
var orderItems = new List<OrderItem>(); if (orderDto.Items == null || !orderDto.Items.Any())
var productIds = orderDto.Items.Select(i => i.ProductId).ToList(); return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "Warenkorb ist leer.");
var productIds = orderDto.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 itemDto in orderDto.Items) foreach (var itemDto in orderDto.Items)
{ {
var product = products.FirstOrDefault(p => p.Id == itemDto.ProductId); var product = products.FirstOrDefault(p => p.Id == itemDto.ProductId);
if (product == null || !product.IsActive)
{
await transaction.RollbackAsync();
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}.");
}
product.StockQuantity -= itemDto.Quantity; if (product == null) { validationErrors.AppendLine($"- Produkt {itemDto.ProductId} fehlt."); continue; }
if (!product.IsActive) validationErrors.AppendLine($"- '{product.Name}' ist nicht verfügbar.");
if (product.StockQuantity < itemDto.Quantity) validationErrors.AppendLine($"- '{product.Name}': Zu wenig Bestand.");
if (validationErrors.Length == 0)
{
totalOrderWeight += (decimal)(product.Weight ?? 0) * itemDto.Quantity;
var orderItem = new OrderItem var orderItem = new OrderItem
{ {
ProductId = product.Id, ProductId = product.Id,
@@ -94,51 +185,64 @@ namespace Webshop.Application.Services.Customers
UnitPrice = product.Price, UnitPrice = product.Price,
TotalPrice = product.Price * itemDto.Quantity TotalPrice = product.Price * itemDto.Quantity
}; };
orderItems.Add(orderItem); preparedOrderItems.Add(orderItem);
itemsTotal += orderItem.TotalPrice; itemsTotal += orderItem.TotalPrice;
} }
}
// --- 3. Preise, Rabatte, Steuern und Gesamtbetrag berechnen --- if (validationErrors.Length > 0)
{
await transaction.RollbackAsync();
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Conflict, $"Fehler:\n{validationErrors}");
}
if (totalOrderWeight < shippingMethod.MinWeight || totalOrderWeight > shippingMethod.MaxWeight)
{
await transaction.RollbackAsync();
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, $"Gesamtgewicht ({totalOrderWeight} kg) passt nicht zur Versandart.");
}
// 2. Preise & Rabatte
decimal discountAmount = 0; decimal discountAmount = 0;
var discountResult = new DiscountCalculationResult(); var discountResult = new DiscountCalculationResult();
if (!string.IsNullOrWhiteSpace(orderDto.CouponCode)) if (!string.IsNullOrWhiteSpace(orderDto.CouponCode))
{ {
discountResult = await _discountService.CalculateDiscountAsync(orderItems, orderDto.CouponCode); discountResult = await _discountService.CalculateDiscountAsync(preparedOrderItems, orderDto.CouponCode);
if (!discountResult.IsValid)
{
await transaction.RollbackAsync();
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, $"Gutscheinfehler: {discountResult.ErrorMessage ?? "Code ungültig."}");
}
discountAmount = discountResult.TotalDiscountAmount; discountAmount = discountResult.TotalDiscountAmount;
if (discountAmount <= 0)
{
await transaction.RollbackAsync();
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.InvalidInput, "Der angegebene Gutscheincode ist ungültig, abgelaufen oder nicht auf die Produkte im Warenkorb anwendbar.");
}
if (discountResult.AppliedDiscountIds.Count > 1)
{
await transaction.RollbackAsync();
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Failure, "Es können nicht mehrere Rabatte gleichzeitig angewendet werden.");
}
} }
decimal shippingCost = shippingMethod.BaseCost; decimal shippingCost = shippingMethod.BaseCost;
decimal subTotalBeforeDiscount = itemsTotal + shippingCost; decimal subTotalBeforeDiscount = itemsTotal + shippingCost;
if (discountAmount > subTotalBeforeDiscount) discountAmount = subTotalBeforeDiscount;
if (discountAmount > subTotalBeforeDiscount)
{
discountAmount = subTotalBeforeDiscount;
}
decimal subTotalAfterDiscount = subTotalBeforeDiscount - discountAmount; decimal subTotalAfterDiscount = subTotalBeforeDiscount - discountAmount;
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; // Steuer auf reduzierten Betrag
decimal orderTotal = subTotalAfterDiscount + taxAmount; decimal orderTotal = subTotalAfterDiscount + taxAmount;
// --- 4. Bestellung erstellen --- // 3. Ausführung (Execution)
foreach (var itemDto in orderDto.Items)
{
var product = products.First(p => p.Id == itemDto.ProductId);
if (product.StockQuantity < itemDto.Quantity)
{
await transaction.RollbackAsync();
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Conflict, "Lagerbestand hat sich während des Vorgangs geändert.");
}
product.StockQuantity -= itemDto.Quantity;
}
var newOrder = new Order var newOrder = new Order
{ {
CustomerId = customer.Id, CustomerId = customer.Id,
OrderNumber = $"WS-{DateTime.UtcNow:yyyyMMdd}-{new Random().Next(1000, 9999)}", OrderNumber = GenerateOrderNumber(),
OrderDate = DateTimeOffset.UtcNow, OrderDate = DateTimeOffset.UtcNow,
OrderStatus = OrderStatus.Pending.ToString(), OrderStatus = OrderStatus.Pending.ToString(),
PaymentStatus = PaymentStatus.Pending.ToString(), PaymentStatus = PaymentStatus.Pending.ToString(),
@@ -151,33 +255,58 @@ namespace Webshop.Application.Services.Customers
ShippingMethodId = shippingMethod.Id, ShippingMethodId = shippingMethod.Id,
BillingAddressId = orderDto.BillingAddressId, BillingAddressId = orderDto.BillingAddressId,
ShippingAddressId = orderDto.ShippingAddressId, ShippingAddressId = orderDto.ShippingAddressId,
OrderItems = orderItems OrderItems = preparedOrderItems
}; };
await _orderRepository.AddAsync(newOrder); await _orderRepository.AddAsync(newOrder);
// --- 5. Rabattnutzung erhöhen ---
if (discountResult.AppliedDiscountIds.Any()) if (discountResult.AppliedDiscountIds.Any())
{ {
var appliedDiscounts = await _context.Discounts.Where(d => discountResult.AppliedDiscountIds.Contains(d.Id)).ToListAsync(); var appliedDiscounts = await _context.Discounts
foreach (var discount in appliedDiscounts) { discount.CurrentUsageCount++; } .Where(d => discountResult.AppliedDiscountIds.Contains(d.Id))
.ToListAsync();
foreach (var d in appliedDiscounts) d.CurrentUsageCount++;
} }
try
{
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
await transaction.CommitAsync(); await transaction.CommitAsync();
}
catch (DbUpdateConcurrencyException ex)
{
await transaction.RollbackAsync();
_logger.LogWarning(ex, "Concurrency Konflikt bei Bestellung.");
return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Conflict, "Artikel wurden soeben von einem anderen Kunden gekauft.");
}
// --- 6. Erfolgreiche Antwort erstellen --- // 4. Post-Processing (Warenkorb & Email)
var createdOrder = await _orderRepository.GetByIdAsync(newOrder.Id); try { await _cartService.ClearCartAsync(userId!); }
return ServiceResult.Ok(MapToOrderDetailDto(createdOrder!)); catch (Exception ex) { _logger.LogError(ex, "Warenkorb konnte nicht geleert werden."); }
try { await _emailService.SendOrderConfirmationAsync(newOrder.Id, userEmail); }
catch (Exception ex) { _logger.LogError(ex, "Bestätigungs-Email fehlgeschlagen."); }
// Manuelles Setzen der Navigation Properties für das Mapping
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();
// Log the full exception for debugging purposes _logger.LogError(ex, "Checkout Exception");
// _logger.LogError(ex, "An unexpected error occurred in CreateOrderAsync."); return ServiceResult.Fail<OrderDetailDto>(ServiceResultType.Failure, "Ein unerwarteter Fehler ist aufgetreten.");
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
@@ -212,11 +341,8 @@ 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, PaymentMethod = order.PaymentMethodInfo?.Name ?? order.PaymentMethod,
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,13 +1,14 @@
// src/Webshop.Application/Services/Customers/CustomerService.cs
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Resend; using System; // Für Uri
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web; using Webshop.Application; // Für ServiceResult
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;
@@ -18,14 +19,18 @@ 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 IResend _resend; private readonly IEmailService _emailService; // NEU: Statt IResend
public CustomerService(ICustomerRepository customerRepository, UserManager<ApplicationUser> userManager, IConfiguration configuration, IResend resend) public CustomerService(
ICustomerRepository customerRepository,
UserManager<ApplicationUser> userManager,
IConfiguration configuration,
IEmailService emailService) // NEU: Injected
{ {
_customerRepository = customerRepository; _customerRepository = customerRepository;
_userManager = userManager; _userManager = userManager;
_configuration = configuration; _configuration = configuration;
_resend = resend; _emailService = emailService;
} }
public async Task<ServiceResult<CustomerDto>> GetMyProfileAsync(string userId) public async Task<ServiceResult<CustomerDto>> GetMyProfileAsync(string userId)
@@ -60,17 +65,20 @@ 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;
@@ -103,28 +111,29 @@ 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"]!; var clientUrl = _configuration["App:ClientUrl"]; // Hier stand vorher ! , besser prüfen oder Null-Check
var confirmationLink = $"{clientUrl}/confirm-email-change?userId={user.Id}&newEmail={HttpUtility.UrlEncode(newEmail)}&token={HttpUtility.UrlEncode(token)}";
var message = new EmailMessage(); // Link bauen (Uri.EscapeDataString ist sicherer in URLs als HttpUtility)
message.From = _configuration["Resend:FromEmail"]!; var confirmationLink = $"{clientUrl}/confirm-email-change?userId={user.Id}&newEmail={Uri.EscapeDataString(newEmail)}&token={Uri.EscapeDataString(token)}";
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);
// Die Erfolgsmeldung wird hier als Teil des "Ok"-Results übermittelt // --- REFACTORED: Nutzung des EmailService ---
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

@@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Webshop.Application.Services.Customers.Interfaces
{
public interface ICartService
{
Task ClearCartAsync(string userId);
}
}

View File

@@ -1,13 +1,20 @@
// src/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs using System.Collections.Generic;
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,11 +1,9 @@
// src/Webshop.Application/Services/Customers/ICustomerService.cs using System.Threading.Tasks;
using System.Threading.Tasks; using Webshop.Application; // Für ServiceResult
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 namespace Webshop.Application.Services.Customers.Interfaces // Namespace angepasst auf .Interfaces
{ {
public interface ICustomerService public interface ICustomerService
{ {

View File

@@ -0,0 +1,18 @@
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

@@ -0,0 +1,133 @@
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(_env.WebRootPath ?? _env.ContentRootPath, "templates", "email-template.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,21 +6,7 @@ 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

@@ -0,0 +1,28 @@
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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
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)
{
}
}
}