Compare commits

..

4 Commits

Author SHA1 Message Date
Tizian.Breuch
9ab23ef29a Merge branch 'develop' of https://gitea.tzbre.dev/admin/ShopSolution-backend into develop
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 26s
2025-11-07 11:03:15 +01:00
Tizian.Breuch
6353591638 test bilder 2025-11-07 11:03:05 +01:00
c2e8761690 .gitea/workflows/pipeline.yml aktualisiert
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 25s
2025-11-07 09:54:31 +00:00
Tizian.Breuch
03993211f6 new git
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 1m8s
2025-10-23 14:45:24 +02:00
53 changed files with 344 additions and 12526 deletions

View File

@@ -5,7 +5,7 @@ name: Branch - test - Build and Push Backend API Docker Image
on:
push:
branches:
- test # Wird ausgelöst bei jedem Push auf den Master-Branch
- develop # Wird ausgelöst bei jedem Push auf den Master-Branch
# Definition der Jobs, die in diesem Workflow ausgeführt werden
jobs:
@@ -39,4 +39,4 @@ jobs:
# Legt fest, dass das Image nach dem Bauen in die Registry gepusht werden soll
push: true
# Definiert die Tags für Ihr Docker-Image in der Registry
tags: gitea.tzbre.dev/admin/shopsolution-backend:dev # WICHTIG: Eindeutiger Name für das Backend-Image
tags: gitea.tzbre.dev/admin/shopsolution-backend:test # WICHTIG: Eindeutiger Name für das Backend-Image

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,37 +2,39 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using Resend;
using System;
using System.Collections.Generic;
using System.IO;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Webshop.Application;
using System.Web;
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<ApplicationUser> _userManager;
private readonly IEmailService _emailService;
private readonly IConfiguration _configuration;
private readonly IResend _resend;
private readonly ApplicationDbContext _context;
public AuthService(
UserManager<ApplicationUser> userManager,
IEmailService emailService,
IConfiguration configuration,
IResend resend,
ApplicationDbContext context)
{
_userManager = userManager;
_emailService = emailService;
_configuration = configuration;
_resend = resend;
_context = context;
}
@@ -64,9 +66,7 @@ namespace Webshop.Application.Services.Auth
_context.Customers.Add(customerProfile);
await _context.SaveChangesAsync();
// Link generieren und Service aufrufen
await SendConfirmationLinkAsync(user);
await SendEmailConfirmationEmail(user);
return ServiceResult.Ok();
}
@@ -102,16 +102,18 @@ 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<AuthResponseDto>(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<AuthResponseDto>(ServiceResultType.Forbidden, "Keine Berechtigung für den Admin-Zugang.");
}
return loginResult;
return loginResult; // Return the successful login result
}
public async Task<ServiceResult> ConfirmEmailAsync(string userId, string token)
@@ -124,10 +126,11 @@ 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);
var result = await _userManager.ConfirmEmailAsync(user, token); // The token from the URL is already decoded by ASP.NET Core
return result.Succeeded
? ServiceResult.Ok()
: ServiceResult.Fail(ServiceResultType.Failure, "E-Mail-Bestätigung fehlgeschlagen.");
@@ -136,33 +139,48 @@ namespace Webshop.Application.Services.Auth
public async Task<ServiceResult> ResendEmailConfirmationAsync(string email)
{
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)
{
return ServiceResult.Fail(ServiceResultType.InvalidInput, "Diese E-Mail-Adresse ist bereits bestätigt.");
}
await SendConfirmationLinkAsync(user);
await SendEmailConfirmationEmail(user);
return ServiceResult.Ok();
}
public async Task<ServiceResult> ForgotPasswordAsync(ForgotPasswordRequestDto request)
{
var user = await _userManager.FindByEmailAsync(request.Email);
if (user != null)
if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
{
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);
return ServiceResult.Ok(); // Do not reveal user existence or status
}
// 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).
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);
return ServiceResult.Ok();
}
@@ -171,6 +189,7 @@ 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.");
}
@@ -181,17 +200,26 @@ namespace Webshop.Application.Services.Auth
: ServiceResult.Fail(ServiceResultType.InvalidInput, string.Join(" ", result.Errors.Select(e => e.Description)));
}
// --- Helper Methods ---
private async Task SendConfirmationLinkAsync(ApplicationUser user)
private async Task SendEmailConfirmationEmail(ApplicationUser user)
{
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var clientUrl = _configuration["App:ClientUrl"]; // z.B. https://localhost:5001/api/v1/Auth
var encodedToken = HttpUtility.UrlEncode(token);
var clientUrl = _configuration["App:ClientUrl"]!;
var confirmationLink = $"{clientUrl}/confirm-email?userId={user.Id}&token={encodedToken}";
// WICHTIG: Uri.EscapeDataString ist sicherer für Token in URLs als HttpUtility
var confirmationLink = $"{clientUrl}/confirm-email?userId={user.Id}&token={Uri.EscapeDataString(token)}";
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
);
await _emailService.SendEmailConfirmationAsync(user.Email!, confirmationLink);
var message = new EmailMessage();
message.To.Add(user.Email!);
message.From = _configuration["Resend:FromEmail"]!;
message.Subject = "Willkommen! Bitte bestätigen Sie Ihre E-Mail-Adresse";
message.HtmlBody = emailHtmlBody;
await _resend.EmailSendAsync(message);
}
private string GenerateJwtToken(ApplicationUser user, IList<string> roles)
@@ -223,5 +251,22 @@ namespace Webshop.Application.Services.Auth
return new JwtSecurityTokenHandler().WriteToken(token);
}
private async Task<string> LoadAndFormatEmailTemplate(string titel, string haupttext, string callToActionText, string callToActionLink)
{
var templatePath = Path.Combine(AppContext.BaseDirectory, "Templates", "_EmailTemplate.html");
if (!File.Exists(templatePath))
{
return $"<h1>{titel}</h1><p>{haupttext}</p><a href='{callToActionLink}'>{callToActionText}</a>";
}
var template = await File.ReadAllTextAsync(templatePath);
template = template.Replace("{{ShopName}}", _configuration["ShopInfo:Name"] ?? "Ihr Webshop");
template = template.Replace("{{Titel}}", titel);
template = template.Replace("{{Haupttext}}", haupttext);
template = template.Replace("{{CallToActionText}}", callToActionText);
template = template.Replace("{{CallToActionLink}}", callToActionLink);
template = template.Replace("{{Jahr}}", DateTime.UtcNow.Year.ToString());
return template;
}
}
}

View File

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

View File

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

View File

@@ -1,14 +1,13 @@
// src/Webshop.Application/Services/Customers/CustomerService.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using System; // Für Uri
using Resend;
using System.Linq;
using System.Threading.Tasks;
using Webshop.Application; // Für ServiceResult
using System.Web;
using Webshop.Application.DTOs;
using Webshop.Application.DTOs.Auth;
using Webshop.Application.DTOs.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;
@@ -19,18 +18,14 @@ namespace Webshop.Application.Services.Customers
private readonly ICustomerRepository _customerRepository;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IConfiguration _configuration;
private readonly IEmailService _emailService; // NEU: Statt IResend
private readonly IResend _resend;
public CustomerService(
ICustomerRepository customerRepository,
UserManager<ApplicationUser> userManager,
IConfiguration configuration,
IEmailService emailService) // NEU: Injected
public CustomerService(ICustomerRepository customerRepository, UserManager<ApplicationUser> userManager, IConfiguration configuration, IResend resend)
{
_customerRepository = customerRepository;
_userManager = userManager;
_configuration = configuration;
_emailService = emailService;
_resend = resend;
}
public async Task<ServiceResult<CustomerDto>> GetMyProfileAsync(string userId)
@@ -65,20 +60,17 @@ 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;
@@ -111,29 +103,28 @@ 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"]; // Hier stand vorher ! , besser prüfen oder Null-Check
var clientUrl = _configuration["App:ClientUrl"]!;
var confirmationLink = $"{clientUrl}/confirm-email-change?userId={user.Id}&newEmail={HttpUtility.UrlEncode(newEmail)}&token={HttpUtility.UrlEncode(token)}";
// Link bauen (Uri.EscapeDataString ist sicherer in URLs als HttpUtility)
var confirmationLink = $"{clientUrl}/confirm-email-change?userId={user.Id}&newEmail={Uri.EscapeDataString(newEmail)}&token={Uri.EscapeDataString(token)}";
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 = $"<h1>E-Mail-Änderung Bestätigung</h1><p>Bitte klicken Sie auf den folgenden Link, um Ihre neue E-Mail-Adresse zu bestätigen:</p><p><a href='{confirmationLink}'>Neue E-Mail-Adresse bestätigen</a></p>";
await _resend.EmailSendAsync(message);
// --- REFACTORED: Nutzung des EmailService ---
await _emailService.SendEmailChangeConfirmationAsync(newEmail, confirmationLink);
// Wir geben die Erfolgsmeldung im Result zurück (passend zu deinem Controller)
// Die Erfolgsmeldung wird hier als Teil des "Ok"-Results übermittelt
return ServiceResult.Ok("Bestätigungs-E-Mail wurde an die neue Adresse gesendet. Bitte prüfen Sie Ihr Postfach.");
}
}

View File

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

View File

@@ -1,20 +1,13 @@
using System.Collections.Generic;
// src/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs
using System;
using System.Threading.Tasks;
using 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<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);
}
}

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,21 @@ using Webshop.Domain.Entities;
namespace Webshop.Application.Services.Public.Interfaces
{
/// <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
{

View File

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

View File

@@ -1,4 +1,5 @@
// src/Webshop.Application/Services/Public/ProductService.cs
// /src/Webshop.Application/Services/Public/ProductService.cs
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
@@ -12,6 +13,10 @@ using Webshop.Infrastructure.Data;
namespace Webshop.Application.Services.Public
{
/// <summary>
/// Service, der f<>r die Abfrage von Produktinformationen f<>r den <20>ffentlichen Shop-Bereich zust<73>ndig ist.
/// Alle hier zur<75>ckgegebenen Daten sind f<>r jeden Besucher sichtbar.
/// </summary>
public class ProductService : IProductService
{
private readonly ApplicationDbContext _context;
@@ -21,53 +26,96 @@ namespace Webshop.Application.Services.Public
_context = context;
}
/// <summary>
/// Ruft eine Liste aller aktiven Produkte f<>r die allgemeine Produkt<6B>bersicht ab.
/// </summary>
/// <returns>Eine Liste von ProductDto-Objekten.</returns>
public async Task<ServiceResult<IEnumerable<ProductDto>>> GetAllProductsAsync()
{
// Startet eine Abfrage auf die 'Products'-Tabelle in der Datenbank.
var products = await _context.Products
.Include(p => p.Productcategories).ThenInclude(pc => pc.categorie)
// L<>dt die zugeh<65>rigen 'Productcategories'-Verkn<6B>pfungen mit.
.Include(p => p.Productcategories)
// L<>dt von dort aus die eigentliche 'categorie'-Entit<69>t.
.ThenInclude(pc => pc.categorie)
// L<>dt auch die zugeh<65>rigen Produktbilder mit.
.Include(p => p.Images)
// Filtert die Ergebnisse: Nur Produkte, die als 'IsActive' markiert sind.
.Where(p => p.IsActive)
// F<>hrt die Abfrage auf der Datenbank aus und l<>dt die Ergebnisse in eine Liste.
.ToListAsync();
// Wandelt jede Product-Entit<69>t in ein ProductDto um.
// 'useShortDescription: true' sorgt daf<61>r, dass die k<>rzere Beschreibung f<>r die Listenansicht verwendet wird.
var dtos = products.Select(p => MapToDto(p, useShortDescription: true)).ToList();
// Gibt das erfolgreiche Ergebnis mit der DTO-Liste zur<75>ck.
return ServiceResult.Ok<IEnumerable<ProductDto>>(dtos);
}
/// <summary>
/// Ruft ein einzelnes, aktives Produkt anhand seines eindeutigen Slugs ab (f<>r die Produktdetailseite).
/// </summary>
/// <param name="slug">Der URL-freundliche Slug des Produkts.</param>
/// <returns>Ein einzelnes ProductDto oder ein NotFound-Ergebnis.</returns>
public async Task<ServiceResult<ProductDto>> GetProductBySlugAsync(string slug)
{
// Startet eine Abfrage auf die 'Products'-Tabelle.
var product = await _context.Products
// L<>dt alle notwendigen verkn<6B>pften Daten f<>r die Detailansicht.
.Include(p => p.Productcategories).ThenInclude(pc => pc.categorie)
.Include(p => p.Images)
// Findet das erste Produkt, das dem Slug entspricht UND aktiv ist.
.FirstOrDefaultAsync(p => p.Slug == slug && p.IsActive);
// Wenn kein Produkt gefunden wurde (oder es inaktiv ist), wird ein Fehler zur<75>ckgegeben.
if (product == null)
{
return ServiceResult.Fail<ProductDto>(ServiceResultType.NotFound, $"Produkt mit dem Slug '{slug}' wurde nicht gefunden.");
}
// Wandelt die gefundene Entit<69>t in ein DTO um.
// 'useShortDescription: false' sorgt daf<61>r, dass die vollst<73>ndige, lange Beschreibung verwendet wird.
return ServiceResult.Ok(MapToDto(product, useShortDescription: false));
}
/// <summary>
/// Ruft eine Liste der als "hervorgehoben" markierten Produkte ab (z.B. f<>r die Startseite).
/// </summary>
/// <returns>Eine Liste von ProductDto-Objekten, sortiert nach ihrer Anzeigereihenfolge.</returns>
public async Task<ServiceResult<IEnumerable<ProductDto>>> GetFeaturedProductsAsync()
{
// Startet eine Abfrage auf die 'Products'-Tabelle.
var products = await _context.Products
// Filtert nur Produkte, die sowohl aktiv als auch als "featured" markiert sind.
.Where(p => p.IsActive && p.IsFeatured)
// Sortiert die Ergebnisse nach der vom Admin festgelegten Reihenfolge.
.OrderBy(p => p.FeaturedDisplayOrder)
// L<>dt die zugeh<65>rigen Bilder und Kategorien.
.Include(p => p.Images)
.Include(p => p.Productcategories).ThenInclude(pc => pc.categorie)
.ToListAsync();
// Wandelt die Entit<69>ten in DTOs um (mit kurzer Beschreibung).
var dtos = products.Select(p => MapToDto(p, useShortDescription: true)).ToList();
return ServiceResult.Ok<IEnumerable<ProductDto>>(dtos);
}
// Private Hilfsmethode, um Codeduplizierung zu vermeiden
/// <summary>
/// Private Hilfsmethode, um eine Product-Entit<69>t in ein ProductDto zu konvertieren.
/// Dies vermeidet Codeduplizierung und stellt eine konsistente Datenstruktur sicher.
/// </summary>
/// <param name="product">Die Product-Entit<69>t aus der Datenbank.</param>
/// <param name="useShortDescription">Gibt an, ob die Kurz- oder Langbeschreibung verwendet werden soll.</param>
/// <returns>Ein bef<65>lltes ProductDto-Objekt.</returns>
private ProductDto MapToDto(Product product, bool useShortDescription)
{
return new ProductDto
{
Id = product.Id,
Name = product.Name,
// W<>hlt die passende Beschreibung basierend auf dem 'useShortDescription'-Flag.
Description = useShortDescription ? product.ShortDescription : product.Description,
Price = product.Price,
OldPrice = product.OldPrice,
@@ -75,12 +123,14 @@ namespace Webshop.Application.Services.Public
IsInStock = product.IsInStock,
StockQuantity = product.StockQuantity,
Slug = product.Slug,
// Mappt die verkn<6B>pften Kategorien auf eine Liste von CategorieDto.
categories = product.Productcategories.Select(pc => new CategorieDto
{
Id = pc.categorie.Id,
Name = pc.categorie.Name,
Slug = pc.categorie.Slug
}).ToList(),
// Mappt die verkn<6B>pften Bilder, sortiert nach DisplayOrder, auf eine Liste von ProductImageDto.
Images = product.Images.OrderBy(i => i.DisplayOrder).Select(img => new ProductImageDto
{
Id = img.Id,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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