Compare commits

..

26 Commits

Author SHA1 Message Date
95c07339ab .gitea/workflows/pipeline.yml aktualisiert
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 26s
2025-11-26 16:02:12 +00:00
2e30755901 Merge pull request 'shipping method weight' (#1) from Feature_Shipping_Method_Add_Weight into develop
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 1m3s
Reviewed-on: #1
2025-11-26 08:55:15 +00:00
Tizian.Breuch
efc70084c3 shipping method weight 2025-11-26 09:56:05 +01:00
Tizian.Breuch
e42c4d6741 bilder main image change
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 26s
2025-11-25 11:58:53 +01:00
Tizian.Breuch
beb971fb2d bilder
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 27s
2025-11-25 11:19:06 +01:00
Tizian.Breuch
6168730cfc bild
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 25s
2025-11-25 11:04:05 +01:00
Tizian.Breuch
d5dad225b5 bilder
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 26s
2025-11-25 10:55:53 +01:00
Tizian.Breuch
6dda8cd106 bilder
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 30s
2025-11-25 10:11:02 +01:00
Tizian.Breuch
b29ecb77df mig 4
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 25s
2025-11-20 15:37:38 +01:00
Tizian.Breuch
063e8418f8 changes 2025-11-20 15:37:13 +01:00
Tizian.Breuch
7a0e56e27a rowversion3
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 26s
2025-11-20 15:19:34 +01:00
Tizian.Breuch
b86c006f25 row version
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 1m10s
2025-11-20 15:05:03 +01:00
Tizian.Breuch
efe186320d test 2025-11-07 15:58:04 +01:00
Tizian.Breuch
dbf46fffac test
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 32s
2025-11-07 15:57:36 +01:00
Tizian.Breuch
f72c1bda2b test
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 27s
2025-11-07 15:51:56 +01:00
Tizian.Breuch
eebfcdf3c7 test
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 27s
2025-11-07 15:48:05 +01:00
Tizian.Breuch
35efac0d6c test
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 27s
2025-11-07 15:39:13 +01:00
Tizian.Breuch
c336b772d4 migration
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 27s
2025-11-07 15:31:14 +01:00
Tizian.Breuch
55c75f84e4 test
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 25s
2025-11-07 15:25:04 +01:00
Tizian.Breuch
6bef982fcc test
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 27s
2025-11-07 15:24:06 +01:00
Tizian.Breuch
627c553e59 test
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 26s
2025-11-07 14:59:32 +01:00
Tizian.Breuch
066a3f4389 rest
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 26s
2025-11-07 14:37:03 +01:00
Tizian.Breuch
6e5d08a8f4 test
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 28s
2025-11-07 14:21:06 +01:00
Tizian.Breuch
dae7d1d979 test
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 26s
2025-11-07 14:15:35 +01:00
Tizian.Breuch
4ef8047460 trst
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 27s
2025-11-07 14:10:00 +01:00
b879095627 .gitea/workflows/pipeline.yml aktualisiert
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 26s
2025-11-07 12:58:42 +00:00
25 changed files with 8654 additions and 70 deletions

View File

@@ -5,7 +5,7 @@ name: Branch - test - Build and Push Backend API Docker Image
on: on:
push: push:
branches: 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 # Definition der Jobs, die in diesem Workflow ausgeführt werden
jobs: jobs:
@@ -39,4 +39,4 @@ jobs:
# Legt fest, dass das Image nach dem Bauen in die Registry gepusht werden soll # Legt fest, dass das Image nach dem Bauen in die Registry gepusht werden soll
push: true push: true
# Definiert die Tags für Ihr Docker-Image in der Registry # Definiert die Tags für Ihr Docker-Image in der Registry
tags: gitea.tzbre.dev/admin/shopsolution-backend:test # WICHTIG: Eindeutiger Name für das Backend-Image tags: gitea.tzbre.dev/admin/shopsolution-backend:dev # WICHTIG: Eindeutiger Name für das Backend-Image

View File

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

View File

@@ -163,6 +163,8 @@ builder.Services.AddControllers()
.AddJsonOptions(options => .AddJsonOptions(options =>
{ {
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
options.JsonSerializerOptions.DefaultIgnoreCondition =
System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;
}); });
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
@@ -209,7 +211,7 @@ builder.Services.AddSwaggerGen(c =>
c.SchemaFilter<AddExampleSchemaFilter>(); c.SchemaFilter<AddExampleSchemaFilter>();
c.OperationFilter<AuthorizeOperationFilter>(); c.OperationFilter<AuthorizeOperationFilter>();
c.OperationFilter<LoginExampleOperationFilter>(); c.OperationFilter<LoginExampleOperationFilter>();
c.OperationFilter<PaymentMethodExampleOperationFilter>(); //c.OperationFilter<PaymentMethodExampleOperationFilter>();
c.OperationFilter<SupplierExampleOperationFilter>(); c.OperationFilter<SupplierExampleOperationFilter>();
c.OperationFilter<ShippingMethodExampleOperationFilter>(); c.OperationFilter<ShippingMethodExampleOperationFilter>();
c.OperationFilter<AdminProductExampleOperationFilter>(); c.OperationFilter<AdminProductExampleOperationFilter>();
@@ -327,6 +329,6 @@ app.Run();
//new git //new image build

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

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

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -613,6 +613,11 @@ namespace Webshop.Infrastructure.Migrations
.HasPrecision(18, 2) .HasPrecision(18, 2)
.HasColumnType("numeric(18,2)"); .HasColumnType("numeric(18,2)");
b.Property<byte[]>("RowVersion")
.IsConcurrencyToken()
.IsRequired()
.HasColumnType("bytea");
b.Property<string>("SKU") b.Property<string>("SKU")
.IsRequired() .IsRequired()
.HasMaxLength(50) .HasMaxLength(50)
@@ -850,9 +855,15 @@ namespace Webshop.Infrastructure.Migrations
b.Property<int>("MaxDeliveryDays") b.Property<int>("MaxDeliveryDays")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<decimal>("MaxWeight")
.HasColumnType("numeric");
b.Property<int>("MinDeliveryDays") b.Property<int>("MinDeliveryDays")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<decimal>("MinWeight")
.HasColumnType("numeric");
b.Property<decimal?>("MinimumOrderAmount") b.Property<decimal?>("MinimumOrderAmount")
.HasPrecision(18, 2) .HasPrecision(18, 2)
.HasColumnType("numeric(18,2)"); .HasColumnType("numeric(18,2)");

View File

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