diff --git a/Webshop.Api/Controllers/Customers/CheckoutController.cs b/Webshop.Api/Controllers/Customers/CheckoutController.cs index 7b0f037..be65ca2 100644 --- a/Webshop.Api/Controllers/Customers/CheckoutController.cs +++ b/Webshop.Api/Controllers/Customers/CheckoutController.cs @@ -4,9 +4,11 @@ using Microsoft.AspNetCore.Authorization; using System.Threading.Tasks; using Webshop.Application.Services.Customers.Interfaces; using Webshop.Application.DTOs.Orders; +using Webshop.Application.DTOs.Shipping; // Neu using System.Security.Claims; using Microsoft.AspNetCore.Http; using Webshop.Application; +using System.Collections.Generic; namespace Webshop.Api.Controllers.Customer { @@ -22,11 +24,26 @@ namespace Webshop.Api.Controllers.Customer _checkoutService = checkoutService; } + // --- NEU: Endpoint um verfügbare Versandmethoden basierend auf dem Warenkorb zu holen --- + [HttpPost("available-shipping-methods")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + public async Task 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")] [ProducesResponseType(typeof(OrderDetailDto), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] public async Task CreateOrder([FromBody] CreateOrderDto orderDto) { @@ -35,7 +52,6 @@ namespace Webshop.Api.Controllers.Customer return BadRequest(ModelState); } - // UserId aus dem JWT-Token des eingeloggten Kunden extrahieren var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); if (string.IsNullOrEmpty(userId)) { @@ -50,7 +66,7 @@ namespace Webshop.Api.Controllers.Customer ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }), ServiceResultType.Conflict => Conflict(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." }) }; } diff --git a/Webshop.Api/Program.cs b/Webshop.Api/Program.cs index eaa60d7..43415bd 100644 --- a/Webshop.Api/Program.cs +++ b/Webshop.Api/Program.cs @@ -12,6 +12,7 @@ using System.Text.Json.Serialization; // --- Eigene Namespaces --- using Webshop.Api.SwaggerFilters; +using Webshop.Application.DTOs.Email; using Webshop.Application.Services.Admin; using Webshop.Application.Services.Admin.Interfaces; using Webshop.Application.Services.Auth; @@ -111,8 +112,9 @@ builder.Services.Configure(options => options.ApiToken = builder.Configuration["Resend:ApiToken"]!; }); builder.Services.AddTransient(); +builder.Services.Configure(builder.Configuration.GetSection("EmailSettings")); -// Repositories +// --- Repositories --- builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -121,17 +123,32 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); // War doppelt builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddScoped(); -// Services +// --- Services --- +// Public & Core builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // WICHTIG: Für Auth & Checkout + +// Customer Scope +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // War doppelt +builder.Services.AddScoped(); // WICHTIG: Für Checkout Cleanup +builder.Services.AddScoped(); + +// Admin Scope builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -139,21 +156,9 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); // War doppelt +builder.Services.AddScoped(); // War doppelt builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Webshop.Application/DTOs/Email/EmailSettings.cs b/Webshop.Application/DTOs/Email/EmailSettings.cs new file mode 100644 index 0000000..2f4b4bc --- /dev/null +++ b/Webshop.Application/DTOs/Email/EmailSettings.cs @@ -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"; + } +} \ No newline at end of file diff --git a/Webshop.Application/DTOs/Shipping/CartItemDto.cs b/Webshop.Application/DTOs/Shipping/CartItemDto.cs new file mode 100644 index 0000000..5da20f8 --- /dev/null +++ b/Webshop.Application/DTOs/Shipping/CartItemDto.cs @@ -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; } + } +} \ No newline at end of file diff --git a/Webshop.Application/DTOs/Shipping/ShippingCalculationRequestDto.cs b/Webshop.Application/DTOs/Shipping/ShippingCalculationRequestDto.cs new file mode 100644 index 0000000..0f415e2 --- /dev/null +++ b/Webshop.Application/DTOs/Shipping/ShippingCalculationRequestDto.cs @@ -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 Items { get; set; } = new(); + } + + +} \ No newline at end of file diff --git a/Webshop.Application/Services/Auth/AuthService.cs b/Webshop.Application/Services/Auth/AuthService.cs index a38f7c7..9796880 100644 --- a/Webshop.Application/Services/Auth/AuthService.cs +++ b/Webshop.Application/Services/Auth/AuthService.cs @@ -2,39 +2,37 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; -using Resend; using System; -using System.IO; +using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Text; using System.Threading.Tasks; -using System.Web; +using Webshop.Application; using Webshop.Application.DTOs.Auth; +using Webshop.Application.Services.Public.Interfaces; using Webshop.Domain.Identity; using Webshop.Infrastructure.Data; -using Webshop.Application; -using System.Collections.Generic; namespace Webshop.Application.Services.Auth { public class AuthService : IAuthService { private readonly UserManager _userManager; + private readonly IEmailService _emailService; private readonly IConfiguration _configuration; - private readonly IResend _resend; private readonly ApplicationDbContext _context; public AuthService( UserManager userManager, + IEmailService emailService, IConfiguration configuration, - IResend resend, ApplicationDbContext context) { _userManager = userManager; + _emailService = emailService; _configuration = configuration; - _resend = resend; _context = context; } @@ -66,7 +64,9 @@ namespace Webshop.Application.Services.Auth _context.Customers.Add(customerProfile); await _context.SaveChangesAsync(); - await SendEmailConfirmationEmail(user); + // Link generieren und Service aufrufen + await SendConfirmationLinkAsync(user); + return ServiceResult.Ok(); } @@ -102,18 +102,16 @@ namespace Webshop.Application.Services.Auth var loginResult = await LoginUserAsync(request); if (loginResult.Type != ServiceResultType.Success) { - // Propagate the specific login failure (e.g., Unauthorized) return ServiceResult.Fail(loginResult.Type, loginResult.ErrorMessage!); } var user = await _userManager.FindByEmailAsync(request.Email); - // This check is belt-and-suspenders, but good practice if (user == null || !await _userManager.IsInRoleAsync(user, "Admin")) { return ServiceResult.Fail(ServiceResultType.Forbidden, "Keine Berechtigung für den Admin-Zugang."); } - return loginResult; // Return the successful login result + return loginResult; } public async Task ConfirmEmailAsync(string userId, string token) @@ -126,11 +124,10 @@ namespace Webshop.Application.Services.Auth var user = await _userManager.FindByIdAsync(userId); if (user == null) { - // Do not reveal that the user does not exist 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 ? ServiceResult.Ok() : ServiceResult.Fail(ServiceResultType.Failure, "E-Mail-Bestätigung fehlgeschlagen."); @@ -139,48 +136,33 @@ namespace Webshop.Application.Services.Auth public async Task ResendEmailConfirmationAsync(string email) { var user = await _userManager.FindByEmailAsync(email); - - if (user == null) - { - return ServiceResult.Ok(); // Do not reveal user existence - } + if (user == null) return ServiceResult.Ok(); if (user.EmailConfirmed) { return ServiceResult.Fail(ServiceResultType.InvalidInput, "Diese E-Mail-Adresse ist bereits bestätigt."); } - await SendEmailConfirmationEmail(user); + await SendConfirmationLinkAsync(user); return ServiceResult.Ok(); } public async Task ForgotPasswordAsync(ForgotPasswordRequestDto request) { 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); - 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); - + // WICHTIG: Wir geben IMMER Ok zurück, auch wenn der User nicht existiert. + // Das verhindert "User Enumeration" (Hacker können nicht prüfen, welche E-Mails existieren). return ServiceResult.Ok(); } @@ -189,7 +171,6 @@ namespace Webshop.Application.Services.Auth var user = await _userManager.FindByEmailAsync(request.Email); 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."); } @@ -200,26 +181,17 @@ namespace Webshop.Application.Services.Auth : 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 encodedToken = HttpUtility.UrlEncode(token); - var clientUrl = _configuration["App:ClientUrl"]!; - var confirmationLink = $"{clientUrl}/confirm-email?userId={user.Id}&token={encodedToken}"; + var clientUrl = _configuration["App:ClientUrl"]; // z.B. https://localhost:5001/api/v1/Auth - var emailHtmlBody = await LoadAndFormatEmailTemplate( - "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 - ); + // WICHTIG: Uri.EscapeDataString ist sicherer für Token in URLs als HttpUtility + var confirmationLink = $"{clientUrl}/confirm-email?userId={user.Id}&token={Uri.EscapeDataString(token)}"; - 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); + await _emailService.SendEmailConfirmationAsync(user.Email!, confirmationLink); } private string GenerateJwtToken(ApplicationUser user, IList roles) @@ -251,22 +223,5 @@ namespace Webshop.Application.Services.Auth return new JwtSecurityTokenHandler().WriteToken(token); } - - private async Task LoadAndFormatEmailTemplate(string titel, string haupttext, string callToActionText, string callToActionLink) - { - var templatePath = Path.Combine(AppContext.BaseDirectory, "Templates", "_EmailTemplate.html"); - if (!File.Exists(templatePath)) - { - return $"

{titel}

{haupttext}

{callToActionText}"; - } - 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; - } } } \ No newline at end of file diff --git a/Webshop.Application/Services/Customers/CartService.cs b/Webshop.Application/Services/Customers/CartService.cs new file mode 100644 index 0000000..60538d9 --- /dev/null +++ b/Webshop.Application/Services/Customers/CartService.cs @@ -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(); + } + } + } +} \ No newline at end of file diff --git a/Webshop.Application/Services/Customers/CheckoutService.cs b/Webshop.Application/Services/Customers/CheckoutService.cs index 760d496..53ec71e 100644 --- a/Webshop.Application/Services/Customers/CheckoutService.cs +++ b/Webshop.Application/Services/Customers/CheckoutService.cs @@ -1,15 +1,19 @@ -// /src/Webshop.Application/Services/Customers/CheckoutService.cs +// src/Webshop.Application/Services/Customers/CheckoutService.cs using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading.Tasks; using Webshop.Application; using Webshop.Application.DTOs.Customers; using Webshop.Application.DTOs.Orders; +using Webshop.Application.DTOs.Shipping; using Webshop.Application.Services.Customers.Interfaces; using Webshop.Application.Services.Public.Interfaces; +using Webshop.Application.Services.Public; // Für DiscountCalculationResult using Webshop.Domain.Entities; using Webshop.Domain.Enums; using Webshop.Domain.Interfaces; @@ -25,10 +29,20 @@ namespace Webshop.Application.Services.Customers private readonly IShippingMethodRepository _shippingMethodRepository; private readonly ISettingService _settingService; private readonly IDiscountService _discountService; + private readonly ICartService _cartService; + private readonly IEmailService _emailService; + private readonly ILogger _logger; public CheckoutService( - ApplicationDbContext context, IOrderRepository orderRepository, ICustomerRepository customerRepository, - IShippingMethodRepository shippingMethodRepository, ISettingService settingService, IDiscountService discountService) + ApplicationDbContext context, + IOrderRepository orderRepository, + ICustomerRepository customerRepository, + IShippingMethodRepository shippingMethodRepository, + ISettingService settingService, + IDiscountService discountService, + ICartService cartService, + IEmailService emailService, + ILogger logger) { _context = context; _orderRepository = orderRepository; @@ -36,6 +50,63 @@ namespace Webshop.Application.Services.Customers _shippingMethodRepository = shippingMethodRepository; _settingService = settingService; _discountService = discountService; + _cartService = cartService; + _emailService = emailService; + _logger = logger; + } + + // ================================================================================= + // FIX: Überladung für CartItemDto (Löst den Konvertierungsfehler im Controller) + // ================================================================================= + public async Task>> GetCompatibleShippingMethodsAsync(List 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>> GetCompatibleShippingMethodsAsync(IEnumerable items) + { + if (items == null || !items.Any()) + return ServiceResult.Ok>(new List()); + + 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>(compatibleMethods); } public async Task> CreateOrderAsync(CreateOrderDto orderDto, string? userId) @@ -43,102 +114,135 @@ namespace Webshop.Application.Services.Customers await using var transaction = await _context.Database.BeginTransactionAsync(); try { - // --- 1. Validierung von Kunde, Adressen und Methoden --- + // 1.1 Kunde Validieren var customer = await _customerRepository.GetByUserIdAsync(userId!); if (customer == null) return ServiceResult.Fail(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(ServiceResultType.Forbidden, "Ungültige oder nicht zugehörige Liefer- oder Rechnungsadresse."); + return ServiceResult.Fail(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(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); if (shippingMethod == null || !shippingMethod.IsActive) - return ServiceResult.Fail(ServiceResultType.InvalidInput, "Ungültige oder inaktive Versandmethode."); + return ServiceResult.Fail(ServiceResultType.InvalidInput, "Versandmethode ungültig."); var paymentMethod = await _context.PaymentMethods.FindAsync(orderDto.PaymentMethodId); if (paymentMethod == null || !paymentMethod.IsActive) - return ServiceResult.Fail(ServiceResultType.InvalidInput, "Ungültige oder inaktive Zahlungsmethode."); + return ServiceResult.Fail(ServiceResultType.InvalidInput, "Zahlungsmethode ungültig."); - // --- 2. Artikel verarbeiten und Lagerbestand prüfen --- - var orderItems = new List(); - var productIds = orderDto.Items.Select(i => i.ProductId).ToList(); + // 1.4 Produkte Validieren + if (orderDto.Items == null || !orderDto.Items.Any()) + return ServiceResult.Fail(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 validationErrors = new StringBuilder(); + decimal totalOrderWeight = 0; decimal itemsTotal = 0; + var preparedOrderItems = new List(); foreach (var itemDto in orderDto.Items) { var product = products.FirstOrDefault(p => p.Id == itemDto.ProductId); - if (product == null || !product.IsActive) - { - await transaction.RollbackAsync(); - return ServiceResult.Fail(ServiceResultType.Conflict, "Ein Produkt im Warenkorb ist nicht mehr verfügbar."); - } - if (product.StockQuantity < itemDto.Quantity) - { - await transaction.RollbackAsync(); - return ServiceResult.Fail(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."); - var orderItem = new OrderItem + if (validationErrors.Length == 0) { - ProductId = product.Id, - ProductVariantId = itemDto.ProductVariantId, - ProductName = product.Name, - ProductSKU = product.SKU, - Quantity = itemDto.Quantity, - UnitPrice = product.Price, - TotalPrice = product.Price * itemDto.Quantity - }; - orderItems.Add(orderItem); - itemsTotal += orderItem.TotalPrice; + totalOrderWeight += (decimal)(product.Weight ?? 0) * itemDto.Quantity; + var orderItem = new OrderItem + { + ProductId = product.Id, + ProductVariantId = itemDto.ProductVariantId, + ProductName = product.Name, + ProductSKU = product.SKU, + Quantity = itemDto.Quantity, + UnitPrice = product.Price, + TotalPrice = product.Price * itemDto.Quantity + }; + preparedOrderItems.Add(orderItem); + itemsTotal += orderItem.TotalPrice; + } } - // --- 3. Preise, Rabatte, Steuern und Gesamtbetrag berechnen --- + if (validationErrors.Length > 0) + { + await transaction.RollbackAsync(); + return ServiceResult.Fail(ServiceResultType.Conflict, $"Fehler:\n{validationErrors}"); + } + + if (totalOrderWeight < shippingMethod.MinWeight || totalOrderWeight > shippingMethod.MaxWeight) + { + await transaction.RollbackAsync(); + return ServiceResult.Fail(ServiceResultType.InvalidInput, $"Gesamtgewicht ({totalOrderWeight} kg) passt nicht zur Versandart."); + } + + // 2. Preise & Rabatte decimal discountAmount = 0; var discountResult = new DiscountCalculationResult(); 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(ServiceResultType.InvalidInput, $"Gutscheinfehler: {discountResult.ErrorMessage ?? "Code ungültig."}"); + } discountAmount = discountResult.TotalDiscountAmount; - - if (discountAmount <= 0) - { - await transaction.RollbackAsync(); - return ServiceResult.Fail(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(ServiceResultType.Failure, "Es können nicht mehrere Rabatte gleichzeitig angewendet werden."); - } } decimal shippingCost = shippingMethod.BaseCost; decimal subTotalBeforeDiscount = itemsTotal + shippingCost; - - if (discountAmount > subTotalBeforeDiscount) - { - discountAmount = subTotalBeforeDiscount; - } + if (discountAmount > subTotalBeforeDiscount) discountAmount = subTotalBeforeDiscount; decimal subTotalAfterDiscount = subTotalBeforeDiscount - discountAmount; - decimal taxRate = await _settingService.GetSettingValueAsync("GlobalTaxRate", 0.19m); - decimal taxAmount = subTotalAfterDiscount * taxRate; + decimal taxAmount = subTotalAfterDiscount * taxRate; // Steuer auf reduzierten Betrag 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(ServiceResultType.Conflict, "Lagerbestand hat sich während des Vorgangs geändert."); + } + product.StockQuantity -= itemDto.Quantity; + } + var newOrder = new Order { CustomerId = customer.Id, - OrderNumber = $"WS-{DateTime.UtcNow:yyyyMMdd}-{new Random().Next(1000, 9999)}", + OrderNumber = GenerateOrderNumber(), OrderDate = DateTimeOffset.UtcNow, OrderStatus = OrderStatus.Pending.ToString(), PaymentStatus = PaymentStatus.Pending.ToString(), @@ -151,33 +255,58 @@ namespace Webshop.Application.Services.Customers ShippingMethodId = shippingMethod.Id, BillingAddressId = orderDto.BillingAddressId, ShippingAddressId = orderDto.ShippingAddressId, - OrderItems = orderItems + OrderItems = preparedOrderItems }; + await _orderRepository.AddAsync(newOrder); - // --- 5. Rabattnutzung erhöhen --- if (discountResult.AppliedDiscountIds.Any()) { - var appliedDiscounts = await _context.Discounts.Where(d => discountResult.AppliedDiscountIds.Contains(d.Id)).ToListAsync(); - foreach (var discount in appliedDiscounts) { discount.CurrentUsageCount++; } + var appliedDiscounts = await _context.Discounts + .Where(d => discountResult.AppliedDiscountIds.Contains(d.Id)) + .ToListAsync(); + foreach (var d in appliedDiscounts) d.CurrentUsageCount++; } - await _context.SaveChangesAsync(); - await transaction.CommitAsync(); + try + { + await _context.SaveChangesAsync(); + await transaction.CommitAsync(); + } + catch (DbUpdateConcurrencyException ex) + { + await transaction.RollbackAsync(); + _logger.LogWarning(ex, "Concurrency Konflikt bei Bestellung."); + return ServiceResult.Fail(ServiceResultType.Conflict, "Artikel wurden soeben von einem anderen Kunden gekauft."); + } - // --- 6. Erfolgreiche Antwort erstellen --- - var createdOrder = await _orderRepository.GetByIdAsync(newOrder.Id); - return ServiceResult.Ok(MapToOrderDetailDto(createdOrder!)); + // 4. Post-Processing (Warenkorb & Email) + try { await _cartService.ClearCartAsync(userId!); } + 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) { await transaction.RollbackAsync(); - // Log the full exception for debugging purposes - // _logger.LogError(ex, "An unexpected error occurred in CreateOrderAsync."); - return ServiceResult.Fail(ServiceResultType.Failure, "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut."); + _logger.LogError(ex, "Checkout Exception"); + return ServiceResult.Fail(ServiceResultType.Failure, "Ein unerwarteter Fehler ist aufgetreten."); } } + private string GenerateOrderNumber() + { + return $"WS-{DateTime.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString().Substring(0, 6).ToUpper()}"; + } + private OrderDetailDto MapToOrderDetailDto(Order order) { return new OrderDetailDto @@ -212,11 +341,8 @@ namespace Webshop.Application.Services.Customers Country = order.BillingAddress.Country, Type = order.BillingAddress.Type }, - PaymentMethod = order.PaymentMethodInfo?.Name, + PaymentMethod = order.PaymentMethodInfo?.Name ?? order.PaymentMethod, PaymentStatus = Enum.TryParse(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 { Id = oi.Id, diff --git a/Webshop.Application/Services/Customers/CustomerService.cs b/Webshop.Application/Services/Customers/CustomerService.cs index 3574738..2f2a5cf 100644 --- a/Webshop.Application/Services/Customers/CustomerService.cs +++ b/Webshop.Application/Services/Customers/CustomerService.cs @@ -1,13 +1,14 @@ -// src/Webshop.Application/Services/Customers/CustomerService.cs using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; -using Resend; +using System; // Für Uri using System.Linq; using System.Threading.Tasks; -using System.Web; -using Webshop.Application.DTOs; +using Webshop.Application; // Für ServiceResult using Webshop.Application.DTOs.Auth; 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.Interfaces; @@ -18,14 +19,18 @@ namespace Webshop.Application.Services.Customers private readonly ICustomerRepository _customerRepository; private readonly UserManager _userManager; private readonly IConfiguration _configuration; - private readonly IResend _resend; + private readonly IEmailService _emailService; // NEU: Statt IResend - public CustomerService(ICustomerRepository customerRepository, UserManager userManager, IConfiguration configuration, IResend resend) + public CustomerService( + ICustomerRepository customerRepository, + UserManager userManager, + IConfiguration configuration, + IEmailService emailService) // NEU: Injected { _customerRepository = customerRepository; _userManager = userManager; _configuration = configuration; - _resend = resend; + _emailService = emailService; } public async Task> GetMyProfileAsync(string userId) @@ -60,17 +65,20 @@ namespace Webshop.Application.Services.Customers var identityUser = await _userManager.FindByIdAsync(userId); if (identityUser == null) return ServiceResult.Fail(ServiceResultType.NotFound, "Benutzerkonto nicht gefunden."); + // Sicherheitscheck: Passwort prüfen if (!await _userManager.CheckPasswordAsync(identityUser, profileDto.CurrentPassword)) { return ServiceResult.Fail(ServiceResultType.InvalidInput, "Das zur Bestätigung eingegebene Passwort ist falsch."); } + // Customer Daten aktualisieren customer.FirstName = profileDto.FirstName; customer.LastName = profileDto.LastName; customer.DefaultShippingAddressId = profileDto.DefaultShippingAddressId; customer.DefaultBillingAddressId = profileDto.DefaultBillingAddressId; await _customerRepository.UpdateAsync(customer); + // User Daten (Telefon) aktualisieren if (!string.IsNullOrEmpty(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); if (user == null) return ServiceResult.Fail(ServiceResultType.NotFound, "Benutzer nicht gefunden."); + // Passwort prüfen if (!await _userManager.CheckPasswordAsync(user, currentPassword)) { 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) { return ServiceResult.Fail(ServiceResultType.Conflict, "Die neue E-Mail-Adresse ist bereits registriert."); } + // Token generieren var token = await _userManager.GenerateChangeEmailTokenAsync(user, newEmail); - var clientUrl = _configuration["App:ClientUrl"]!; - var confirmationLink = $"{clientUrl}/confirm-email-change?userId={user.Id}&newEmail={HttpUtility.UrlEncode(newEmail)}&token={HttpUtility.UrlEncode(token)}"; + var clientUrl = _configuration["App:ClientUrl"]; // Hier stand vorher ! , besser prüfen oder Null-Check - var message = new EmailMessage(); - message.From = _configuration["Resend:FromEmail"]!; - message.To.Add(newEmail); - message.Subject = "Bestätigen Sie Ihre neue E-Mail-Adresse"; - message.HtmlBody = $"

E-Mail-Änderung Bestätigung

Bitte klicken Sie auf den folgenden Link, um Ihre neue E-Mail-Adresse zu bestätigen:

Neue E-Mail-Adresse bestätigen

"; - await _resend.EmailSendAsync(message); + // Link bauen (Uri.EscapeDataString ist sicherer in URLs als HttpUtility) + var confirmationLink = $"{clientUrl}/confirm-email-change?userId={user.Id}&newEmail={Uri.EscapeDataString(newEmail)}&token={Uri.EscapeDataString(token)}"; - // 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."); } } diff --git a/Webshop.Application/Services/Customers/Interfaces/ICartService.cs b/Webshop.Application/Services/Customers/Interfaces/ICartService.cs new file mode 100644 index 0000000..51cc2c7 --- /dev/null +++ b/Webshop.Application/Services/Customers/Interfaces/ICartService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Webshop.Application.Services.Customers.Interfaces +{ + public interface ICartService + { + Task ClearCartAsync(string userId); + } +} \ No newline at end of file diff --git a/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs b/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs index 3eab4ec..723d859 100644 --- a/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs +++ b/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs @@ -1,13 +1,20 @@ -// src/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs -using System; +using System.Collections.Generic; using System.Threading.Tasks; -using Webshop.Application; using Webshop.Application.DTOs.Orders; +using Webshop.Application.DTOs.Shipping; +using Webshop.Application; // Für ServiceResult namespace Webshop.Application.Services.Customers.Interfaces { public interface ICheckoutService { + // Methode 1: Für den Checkout (interne Verarbeitung) + Task>> GetCompatibleShippingMethodsAsync(IEnumerable items); + + // +++ METHODE 2: DIESE FEHLTE +++ + // Für den Warenkorb-Check (kommt vom Controller als List) + Task>> GetCompatibleShippingMethodsAsync(List items); + Task> CreateOrderAsync(CreateOrderDto orderDto, string? userId); } } \ No newline at end of file diff --git a/Webshop.Application/Services/Customers/Interfaces/ICustomerService.cs b/Webshop.Application/Services/Customers/Interfaces/ICustomerService.cs index 306adef..0b43ea2 100644 --- a/Webshop.Application/Services/Customers/Interfaces/ICustomerService.cs +++ b/Webshop.Application/Services/Customers/Interfaces/ICustomerService.cs @@ -1,11 +1,9 @@ -// src/Webshop.Application/Services/Customers/ICustomerService.cs -using System.Threading.Tasks; -using Webshop.Application; -using Webshop.Application.DTOs; +using System.Threading.Tasks; +using Webshop.Application; // Für ServiceResult using Webshop.Application.DTOs.Auth; using Webshop.Application.DTOs.Customers; -namespace Webshop.Application.Services.Customers +namespace Webshop.Application.Services.Customers.Interfaces // Namespace angepasst auf .Interfaces { public interface ICustomerService { diff --git a/Webshop.Application/Services/Public/DiscountCalculationResult.cs b/Webshop.Application/Services/Public/DiscountCalculationResult.cs new file mode 100644 index 0000000..12cf2a5 --- /dev/null +++ b/Webshop.Application/Services/Public/DiscountCalculationResult.cs @@ -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 AppliedDiscountIds { get; set; } = new List(); + 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); + } +} \ No newline at end of file diff --git a/Webshop.Application/Services/Public/EmailService.cs b/Webshop.Application/Services/Public/EmailService.cs new file mode 100644 index 0000000..82c3e92 --- /dev/null +++ b/Webshop.Application/Services/Public/EmailService.cs @@ -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 _logger; + private readonly IConfiguration _configuration; + + public EmailService( + IResend resend, + IWebHostEnvironment env, + ILogger 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 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 = "

{{ShopName}}

{{Titel}}

{{Haupttext}}

{{CallToActionText}}

"; + } + + // 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()); + } + } +} \ No newline at end of file diff --git a/Webshop.Application/Services/Public/Interfaces/IDiscountService.cs b/Webshop.Application/Services/Public/Interfaces/IDiscountService.cs index c45e150..ea91311 100644 --- a/Webshop.Application/Services/Public/Interfaces/IDiscountService.cs +++ b/Webshop.Application/Services/Public/Interfaces/IDiscountService.cs @@ -6,21 +6,7 @@ using Webshop.Domain.Entities; namespace Webshop.Application.Services.Public.Interfaces { - /// - /// Stellt das Ergebnis der Rabattberechnung dar. - /// - public class DiscountCalculationResult - { - /// - /// Der gesamte berechnete Rabattbetrag. - /// - public decimal TotalDiscountAmount { get; set; } = 0; - /// - /// Die IDs der Rabatte, die erfolgreich angewendet wurden. - /// - public List AppliedDiscountIds { get; set; } = new List(); - } public interface IDiscountService { diff --git a/Webshop.Application/Services/Public/Interfaces/IEmailService.cs b/Webshop.Application/Services/Public/Interfaces/IEmailService.cs new file mode 100644 index 0000000..0fa6bd3 --- /dev/null +++ b/Webshop.Application/Services/Public/Interfaces/IEmailService.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; + +namespace Webshop.Application.Services.Public.Interfaces +{ + public interface IEmailService + { + /// + /// Sendet den Link zur Bestätigung der Registrierung. + /// + Task SendEmailConfirmationAsync(string toEmail, string confirmationLink); + + /// + /// Sendet den Link zum Zurücksetzen des Passworts. + /// + Task SendPasswordResetAsync(string toEmail, string resetLink); + + /// + /// Sendet eine Bestellbestätigung. + /// + Task SendOrderConfirmationAsync(Guid orderId, string toEmail); + + /// + /// Sendet einen Bestätigungslink bei E-Mail-Änderung. + /// + Task SendEmailChangeConfirmationAsync(string toEmail, string confirmationLink); + } +} \ No newline at end of file diff --git a/Webshop.Infrastructure/Migrations/20251126100815_checkout.Designer.cs b/Webshop.Infrastructure/Migrations/20251126100815_checkout.Designer.cs new file mode 100644 index 0000000..86d72e1 --- /dev/null +++ b/Webshop.Infrastructure/Migrations/20251126100815_checkout.Designer.cs @@ -0,0 +1,1372 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Webshop.Infrastructure.Data; + +#nullable disable + +namespace Webshop.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251126100815_checkout")] + partial class checkout + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.18") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("Roles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", (string)null); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CompanyName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("HouseNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("State") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Street") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Categorie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DisplayOrder") + .HasColumnType("integer"); + + b.Property("ImageUrl") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModifiedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ParentcategorieId") + .HasColumnType("uuid"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("ParentcategorieId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("categories"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.CategorieDiscount", b => + { + b.Property("categorieId") + .HasColumnType("uuid"); + + b.Property("DiscountId") + .HasColumnType("uuid"); + + b.HasKey("categorieId", "DiscountId"); + + b.HasIndex("DiscountId"); + + b.ToTable("categorieDiscounts"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AspNetUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DefaultBillingAddressId") + .HasColumnType("uuid"); + + b.Property("DefaultShippingAddressId") + .HasColumnType("uuid"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("AspNetUserId") + .IsUnique(); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Discount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CouponCode") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CurrentUsageCount") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DiscountType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DiscountValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaximumUsageCount") + .HasColumnType("integer"); + + b.Property("MinimumOrderAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RequiresCouponCode") + .HasColumnType("boolean"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CouponCode") + .IsUnique() + .HasFilter("\"CouponCode\" IS NOT NULL"); + + b.ToTable("Discounts"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdminNotes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("BillingAddressId") + .HasColumnType("uuid"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("CustomerNotes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DeliveredDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("GuestEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GuestPhoneNumber") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("OrderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrderNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrderStatus") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrderTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PaymentMethodId") + .HasColumnType("uuid"); + + b.Property("PaymentStatus") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ShippedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ShippingAddressId") + .HasColumnType("uuid"); + + b.Property("ShippingCost") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ShippingMethodId") + .HasColumnType("uuid"); + + b.Property("ShippingTrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TaxAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TransactionId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("BillingAddressId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.HasIndex("PaymentMethodId"); + + b.HasIndex("ShippingAddressId"); + + b.HasIndex("ShippingMethodId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProductSKU") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("TotalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductVariantId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.PaymentMethod", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("jsonb"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PaymentGatewayType") + .HasColumnType("integer"); + + b.Property("ProcessingFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.ToTable("PaymentMethods"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("FeaturedDisplayOrder") + .HasColumnType("integer"); + + b.Property("Height") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsFeatured") + .HasColumnType("boolean"); + + b.Property("IsInStock") + .HasColumnType("boolean"); + + b.Property("LastModifiedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Length") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("OldPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("PurchasePrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .HasColumnType("bytea"); + + b.Property("SKU") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ShortDescription") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StockQuantity") + .HasColumnType("integer"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("Weight") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Width") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("SKU") + .IsUnique(); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("SupplierId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.ProductDiscount", b => + { + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("DiscountId") + .HasColumnType("uuid"); + + b.HasKey("ProductId", "DiscountId"); + + b.HasIndex("DiscountId"); + + b.ToTable("ProductDiscounts"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.ProductImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DisplayOrder") + .HasColumnType("integer"); + + b.Property("IsMainImage") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductImages", (string)null); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.ProductVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ImageUrl") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PriceAdjustment") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SKU") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StockQuantity") + .HasColumnType("integer"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductVariants"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Productcategorie", b => + { + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("categorieId") + .HasColumnType("uuid"); + + b.HasKey("ProductId", "categorieId"); + + b.HasIndex("categorieId"); + + b.ToTable("Productcategories"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("ReviewDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ProductId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Group") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Value") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.ShippingMethod", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BaseCost") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("EstimatedDeliveryTime") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDeliveryDays") + .HasColumnType("integer"); + + b.Property("MaxWeight") + .HasColumnType("numeric"); + + b.Property("MinDeliveryDays") + .HasColumnType("integer"); + + b.Property("MinWeight") + .HasColumnType("numeric"); + + b.Property("MinimumOrderAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RequiresTracking") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("ShippingMethods"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.ShopInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CompanyRegistrationNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Country") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("FacebookUrl") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("InstagramUrl") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ShopName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Slogan") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Street") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TwitterUrl") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("VatNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("ShopInfos"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressId") + .HasColumnType("uuid"); + + b.Property("ContactPerson") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.ToTable("Suppliers"); + }); + + modelBuilder.Entity("Webshop.Domain.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastActive") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Webshop.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Webshop.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Webshop.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Webshop.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Address", b => + { + b.HasOne("Webshop.Domain.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Categorie", b => + { + b.HasOne("Webshop.Domain.Entities.Categorie", "Parentcategorie") + .WithMany("Subcategories") + .HasForeignKey("ParentcategorieId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Parentcategorie"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.CategorieDiscount", b => + { + b.HasOne("Webshop.Domain.Entities.Discount", "Discount") + .WithMany("categorieDiscounts") + .HasForeignKey("DiscountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Webshop.Domain.Entities.Categorie", "categorie") + .WithMany("categorieDiscounts") + .HasForeignKey("categorieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Discount"); + + b.Navigation("categorie"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Customer", b => + { + b.HasOne("Webshop.Domain.Identity.ApplicationUser", "User") + .WithOne("Customer") + .HasForeignKey("Webshop.Domain.Entities.Customer", "AspNetUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Order", b => + { + b.HasOne("Webshop.Domain.Entities.Address", "BillingAddress") + .WithMany() + .HasForeignKey("BillingAddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Webshop.Domain.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId"); + + b.HasOne("Webshop.Domain.Entities.PaymentMethod", "PaymentMethodInfo") + .WithMany() + .HasForeignKey("PaymentMethodId"); + + b.HasOne("Webshop.Domain.Entities.Address", "ShippingAddress") + .WithMany() + .HasForeignKey("ShippingAddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Webshop.Domain.Entities.ShippingMethod", "ShippingMethodInfo") + .WithMany() + .HasForeignKey("ShippingMethodId"); + + b.Navigation("BillingAddress"); + + b.Navigation("Customer"); + + b.Navigation("PaymentMethodInfo"); + + b.Navigation("ShippingAddress"); + + b.Navigation("ShippingMethodInfo"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.OrderItem", b => + { + b.HasOne("Webshop.Domain.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Webshop.Domain.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Webshop.Domain.Entities.ProductVariant", "ProductVariant") + .WithMany() + .HasForeignKey("ProductVariantId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Order"); + + b.Navigation("Product"); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Product", b => + { + b.HasOne("Webshop.Domain.Entities.Supplier", "Supplier") + .WithMany("Products") + .HasForeignKey("SupplierId"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.ProductDiscount", b => + { + b.HasOne("Webshop.Domain.Entities.Discount", "Discount") + .WithMany("ProductDiscounts") + .HasForeignKey("DiscountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Webshop.Domain.Entities.Product", "Product") + .WithMany("ProductDiscounts") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Discount"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.ProductImage", b => + { + b.HasOne("Webshop.Domain.Entities.Product", "Product") + .WithMany("Images") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.ProductVariant", b => + { + b.HasOne("Webshop.Domain.Entities.Product", "Product") + .WithMany("Variants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Productcategorie", b => + { + b.HasOne("Webshop.Domain.Entities.Product", "Product") + .WithMany("Productcategories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Webshop.Domain.Entities.Categorie", "categorie") + .WithMany("Productcategories") + .HasForeignKey("categorieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("categorie"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Review", b => + { + b.HasOne("Webshop.Domain.Entities.Customer", "Customer") + .WithMany("Reviews") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Webshop.Domain.Entities.Product", "Product") + .WithMany("Reviews") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Supplier", b => + { + b.HasOne("Webshop.Domain.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId"); + + b.Navigation("Address"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Categorie", b => + { + b.Navigation("Productcategories"); + + b.Navigation("Subcategories"); + + b.Navigation("categorieDiscounts"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Discount", b => + { + b.Navigation("ProductDiscounts"); + + b.Navigation("categorieDiscounts"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Order", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Product", b => + { + b.Navigation("Images"); + + b.Navigation("ProductDiscounts"); + + b.Navigation("Productcategories"); + + b.Navigation("Reviews"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("Webshop.Domain.Entities.Supplier", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("Webshop.Domain.Identity.ApplicationUser", b => + { + b.Navigation("Customer"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Webshop.Infrastructure/Migrations/20251126100815_checkout.cs b/Webshop.Infrastructure/Migrations/20251126100815_checkout.cs new file mode 100644 index 0000000..ee025c4 --- /dev/null +++ b/Webshop.Infrastructure/Migrations/20251126100815_checkout.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Webshop.Infrastructure.Migrations +{ + /// + public partial class checkout : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +}