checkout
This commit is contained in:
@@ -134,6 +134,8 @@ builder.Services.AddScoped<IShopInfoService, ShopInfoService>();
|
|||||||
builder.Services.AddScoped<ISettingService, SettingService>();
|
builder.Services.AddScoped<ISettingService, SettingService>();
|
||||||
builder.Services.AddScoped<IAdminSettingService, AdminSettingService>();
|
builder.Services.AddScoped<IAdminSettingService, AdminSettingService>();
|
||||||
builder.Services.AddScoped<IAdminDiscountService, AdminDiscountService>();
|
builder.Services.AddScoped<IAdminDiscountService, AdminDiscountService>();
|
||||||
|
builder.Services.AddScoped<ICheckoutService, CheckoutService>();
|
||||||
|
builder.Services.AddScoped<IDiscountService, DiscountService>();
|
||||||
|
|
||||||
//fehlt noch
|
//fehlt noch
|
||||||
builder.Services.AddScoped<IReviewService, ReviewService>();
|
builder.Services.AddScoped<IReviewService, ReviewService>();
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// Auto-generiert von CreateWebshopFiles.ps1
|
// src/Webshop.Application/DTOs/Orders/CreateOrderDto.cs
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
|
|
||||||
namespace Webshop.Application.DTOs.Orders
|
namespace Webshop.Application.DTOs.Orders
|
||||||
{
|
{
|
||||||
@@ -15,6 +13,9 @@ namespace Webshop.Application.DTOs.Orders
|
|||||||
public Guid BillingAddressId { get; set; }
|
public Guid BillingAddressId { get; set; }
|
||||||
public Guid PaymentMethodId { get; set; }
|
public Guid PaymentMethodId { get; set; }
|
||||||
public Guid ShippingMethodId { get; set; }
|
public Guid ShippingMethodId { get; set; }
|
||||||
|
|
||||||
|
public string? CouponCode { get; set; } // << NEU >>
|
||||||
|
|
||||||
public List<CreateOrderItemDto> Items { get; set; } = new List<CreateOrderItemDto>();
|
public List<CreateOrderItemDto> Items { get; set; } = new List<CreateOrderItemDto>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,185 @@
|
|||||||
// Auto-generiert von CreateWebshopFiles.ps1
|
// src/Webshop.Application/Services/Customers/CheckoutService.cs
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Webshop.Application.DTOs.Customers;
|
||||||
|
using Webshop.Application.DTOs.Orders;
|
||||||
using Webshop.Application.Services.Customers.Interfaces;
|
using Webshop.Application.Services.Customers.Interfaces;
|
||||||
|
using Webshop.Application.Services.Public.Interfaces;
|
||||||
|
using Webshop.Domain.Entities;
|
||||||
|
using Webshop.Domain.Enums;
|
||||||
|
using Webshop.Domain.Interfaces;
|
||||||
|
using Webshop.Infrastructure.Data;
|
||||||
|
|
||||||
namespace Webshop.Application.Services.Customers
|
namespace Webshop.Application.Services.Customers
|
||||||
{
|
{
|
||||||
public class CheckoutService : ICheckoutService
|
public class CheckoutService : ICheckoutService
|
||||||
{
|
{
|
||||||
// Fügen Sie hier Abhängigkeiten per Dependency Injection hinzu (z.B. Repositories)
|
private readonly ApplicationDbContext _context;
|
||||||
|
private readonly IOrderRepository _orderRepository;
|
||||||
|
private readonly ICustomerRepository _customerRepository;
|
||||||
|
private readonly IShippingMethodRepository _shippingMethodRepository;
|
||||||
|
private readonly ISettingService _settingService;
|
||||||
|
private readonly IDiscountService _discountService;
|
||||||
|
|
||||||
// public CheckoutService(IYourRepository repository) { }
|
public CheckoutService(
|
||||||
|
ApplicationDbContext context,
|
||||||
|
IOrderRepository orderRepository,
|
||||||
|
ICustomerRepository customerRepository,
|
||||||
|
IShippingMethodRepository shippingMethodRepository,
|
||||||
|
ISettingService settingService,
|
||||||
|
IDiscountService discountService)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_orderRepository = orderRepository;
|
||||||
|
_customerRepository = customerRepository;
|
||||||
|
_shippingMethodRepository = shippingMethodRepository;
|
||||||
|
_settingService = settingService;
|
||||||
|
_discountService = discountService;
|
||||||
|
}
|
||||||
|
|
||||||
// Fügen Sie hier Service-Methoden hinzu
|
public async Task<(bool Success, OrderDetailDto? CreatedOrder, string? ErrorMessage)> CreateOrderAsync(CreateOrderDto orderDto, string userId)
|
||||||
|
{
|
||||||
|
// Startet eine Datenbank-Transaktion. Entweder alles klappt, oder alles wird zurückgerollt.
|
||||||
|
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// --- 1. Validierung der Eingabedaten ---
|
||||||
|
var customer = await _customerRepository.GetByUserIdAsync(userId);
|
||||||
|
if (customer == null) return (false, null, "Kundenprofil nicht gefunden.");
|
||||||
|
|
||||||
|
var shippingMethod = await _shippingMethodRepository.GetByIdAsync(orderDto.ShippingMethodId);
|
||||||
|
if (shippingMethod == null || !shippingMethod.IsActive) return (false, null, "Ungültige oder inaktive Versandmethode.");
|
||||||
|
|
||||||
|
var paymentMethod = await _context.PaymentMethods.FindAsync(orderDto.PaymentMethodId);
|
||||||
|
if (paymentMethod == null || !paymentMethod.IsActive) return (false, null, "Ungültige oder inaktive Zahlungsmethode.");
|
||||||
|
|
||||||
|
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 (false, null, "Ungültige oder nicht zugehörige Liefer- oder Rechnungsadresse.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. Artikel verarbeiten und Lagerbestand prüfen ---
|
||||||
|
var orderItems = new List<OrderItem>();
|
||||||
|
decimal itemsTotal = 0;
|
||||||
|
|
||||||
|
foreach (var itemDto in orderDto.Items)
|
||||||
|
{
|
||||||
|
var product = await _context.Products.FindAsync(itemDto.ProductId);
|
||||||
|
if (product == null || !product.IsActive)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
return (false, null, $"Produkt mit ID {itemDto.ProductId} ist nicht verfügbar.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (product.StockQuantity < itemDto.Quantity)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
return (false, null, $"Nicht genügend Lagerbestand für '{product.Name}'. Verfügbar: {product.StockQuantity}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lagerbestand reduzieren
|
||||||
|
product.StockQuantity -= itemDto.Quantity;
|
||||||
|
|
||||||
|
var orderItem = new OrderItem
|
||||||
|
{
|
||||||
|
ProductId = product.Id,
|
||||||
|
ProductVariantId = itemDto.ProductVariantId,
|
||||||
|
ProductName = product.Name,
|
||||||
|
ProductSKU = product.SKU,
|
||||||
|
Quantity = itemDto.Quantity,
|
||||||
|
UnitPrice = product.Price,
|
||||||
|
TotalPrice = product.Price * itemDto.Quantity
|
||||||
|
};
|
||||||
|
|
||||||
|
orderItems.Add(orderItem);
|
||||||
|
itemsTotal += orderItem.TotalPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3. Preise, Rabatte, Steuern und Gesamtbetrag berechnen ---
|
||||||
|
decimal discountAmount = await _discountService.CalculateDiscountAsync(orderItems, orderDto.CouponCode);
|
||||||
|
decimal shippingCost = shippingMethod.BaseCost;
|
||||||
|
decimal subTotal = itemsTotal + shippingCost - discountAmount;
|
||||||
|
if (subTotal < 0) subTotal = 0;
|
||||||
|
|
||||||
|
decimal taxRate = await _settingService.GetSettingValueAsync<decimal>("GlobalTaxRate", 0.19m);
|
||||||
|
decimal taxAmount = subTotal * taxRate;
|
||||||
|
decimal orderTotal = subTotal + taxAmount;
|
||||||
|
|
||||||
|
// --- 4. Bestellung erstellen ---
|
||||||
|
var newOrder = new Order
|
||||||
|
{
|
||||||
|
CustomerId = customer.Id,
|
||||||
|
OrderNumber = $"WS-{DateTime.UtcNow:yyyyMMdd}-{new Random().Next(1000, 9999)}",
|
||||||
|
OrderDate = DateTimeOffset.UtcNow,
|
||||||
|
OrderStatus = OrderStatus.Pending.ToString(),
|
||||||
|
PaymentStatus = PaymentStatus.Pending.ToString(), // Wird später vom Zahlungsanbieter aktualisiert
|
||||||
|
|
||||||
|
OrderTotal = orderTotal,
|
||||||
|
ShippingCost = shippingCost,
|
||||||
|
TaxAmount = taxAmount,
|
||||||
|
DiscountAmount = discountAmount,
|
||||||
|
|
||||||
|
PaymentMethod = paymentMethod.Name,
|
||||||
|
PaymentMethodId = paymentMethod.Id,
|
||||||
|
ShippingMethodId = shippingMethod.Id,
|
||||||
|
|
||||||
|
BillingAddressId = orderDto.BillingAddressId,
|
||||||
|
ShippingAddressId = orderDto.ShippingAddressId,
|
||||||
|
|
||||||
|
OrderItems = orderItems
|
||||||
|
};
|
||||||
|
|
||||||
|
await _orderRepository.AddAsync(newOrder); // Speichert die Bestellung und die Artikel
|
||||||
|
await _context.SaveChangesAsync(); // Speichert die Lagerbestandsänderungen
|
||||||
|
|
||||||
|
await transaction.CommitAsync(); // Transaktion erfolgreich abschließen
|
||||||
|
|
||||||
|
// --- 5. Erfolgreiche Antwort erstellen ---
|
||||||
|
var createdOrder = await _orderRepository.GetByIdAsync(newOrder.Id); // Lade die erstellte Bestellung mit allen Details
|
||||||
|
var orderDetailDto = MapToOrderDetailDto(createdOrder!); // Mapping zum DTO
|
||||||
|
|
||||||
|
return (true, orderDetailDto, null);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
// Hier Fehler loggen
|
||||||
|
return (false, null, $"Ein unerwarteter Fehler ist aufgetreten: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper-Methode für das Mapping, um Code-Duplizierung zu vermeiden
|
||||||
|
private OrderDetailDto MapToOrderDetailDto(Order order)
|
||||||
|
{
|
||||||
|
return new OrderDetailDto
|
||||||
|
{
|
||||||
|
Id = order.Id,
|
||||||
|
OrderNumber = order.OrderNumber,
|
||||||
|
OrderDate = order.OrderDate,
|
||||||
|
CustomerId = order.CustomerId,
|
||||||
|
Status = Enum.Parse<OrderStatus>(order.OrderStatus),
|
||||||
|
TotalAmount = order.OrderTotal,
|
||||||
|
ShippingAddress = new AddressDto { /* Mapping */ },
|
||||||
|
BillingAddress = new AddressDto { /* Mapping */ },
|
||||||
|
PaymentMethod = order.PaymentMethod,
|
||||||
|
PaymentStatus = Enum.Parse<PaymentStatus>(order.PaymentStatus),
|
||||||
|
OrderItems = order.OrderItems.Select(oi => new OrderItemDto
|
||||||
|
{
|
||||||
|
Id = oi.Id,
|
||||||
|
ProductId = oi.ProductId,
|
||||||
|
ProductVariantId = oi.ProductVariantId,
|
||||||
|
ProductName = oi.ProductName,
|
||||||
|
ProductSKU = oi.ProductSKU,
|
||||||
|
Quantity = oi.Quantity,
|
||||||
|
UnitPrice = oi.UnitPrice,
|
||||||
|
TotalPrice = oi.TotalPrice
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
// Auto-generiert von CreateWebshopFiles.ps1
|
// src/Webshop.Application/Services/Customers/Interfaces/ICheckoutService.cs
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Webshop.Application.DTOs;
|
using Webshop.Application.DTOs.Orders;
|
||||||
using Webshop.Application.DTOs.Auth;
|
|
||||||
using Webshop.Application.DTOs.Users;
|
|
||||||
|
|
||||||
|
|
||||||
namespace Webshop.Application.Services.Customers.Interfaces
|
namespace Webshop.Application.Services.Customers.Interfaces
|
||||||
{
|
{
|
||||||
public interface ICheckoutService
|
public interface ICheckoutService
|
||||||
{
|
{
|
||||||
// Fügen Sie hier Methodensignaturen hinzu
|
Task<(bool Success, OrderDetailDto? CreatedOrder, string? ErrorMessage)> CreateOrderAsync(CreateOrderDto orderDto, string userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
90
Webshop.Application/Services/Public/DiscountService.cs
Normal file
90
Webshop.Application/Services/Public/DiscountService.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// src/Webshop.Application/Services/Public/DiscountService.cs
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Webshop.Application.Services.Public.Interfaces;
|
||||||
|
using Webshop.Domain.Entities;
|
||||||
|
using Webshop.Domain.Interfaces;
|
||||||
|
using Webshop.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace Webshop.Application.Services.Public
|
||||||
|
{
|
||||||
|
public class DiscountService : IDiscountService
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _context; // Direkter Zugriff auf DbSets
|
||||||
|
private readonly IDiscountRepository _discountRepository;
|
||||||
|
private readonly IProductRepository _productRepository;
|
||||||
|
|
||||||
|
public DiscountService(
|
||||||
|
ApplicationDbContext context,
|
||||||
|
IDiscountRepository discountRepository,
|
||||||
|
IProductRepository productRepository)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_discountRepository = discountRepository;
|
||||||
|
_productRepository = productRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<decimal> CalculateDiscountAsync(List<OrderItem> orderItems, string? couponCode)
|
||||||
|
{
|
||||||
|
decimal totalDiscount = 0;
|
||||||
|
|
||||||
|
// 1. Hole alle relevanten Rabatte
|
||||||
|
var discounts = await _context.Discounts
|
||||||
|
.Include(d => d.ProductDiscounts)
|
||||||
|
.Include(d => d.categorieDiscounts)
|
||||||
|
.Where(d => d.IsActive && d.StartDate <= DateTimeOffset.UtcNow && (!d.EndDate.HasValue || d.EndDate >= DateTimeOffset.UtcNow))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Filtern nach Gutscheincode
|
||||||
|
if (!string.IsNullOrEmpty(couponCode))
|
||||||
|
{
|
||||||
|
discounts = discounts.Where(d => d.CouponCode == couponCode && d.RequiresCouponCode).ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Rabatte ohne Gutscheincode, falls zutreffend
|
||||||
|
discounts = discounts.Where(d => !d.RequiresCouponCode).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Wende Rabatte auf Artikel an
|
||||||
|
foreach (var item in orderItems)
|
||||||
|
{
|
||||||
|
decimal itemDiscount = 0;
|
||||||
|
|
||||||
|
// Wir nehmen an, dass ein Artikel nur den besten Rabatt erhalten kann
|
||||||
|
var applicableDiscount = discounts.FirstOrDefault(d =>
|
||||||
|
d.ProductDiscounts.Any(pd => pd.ProductId == item.ProductId) || // Rabatt auf Produkt
|
||||||
|
d.categorieDiscounts.Any(cd => _context.Productcategories.Any(pc => pc.ProductId == item.ProductId && pc.categorieId == cd.categorieId)) // Rabatt auf Kategorie
|
||||||
|
);
|
||||||
|
|
||||||
|
if (applicableDiscount != null)
|
||||||
|
{
|
||||||
|
itemDiscount = item.UnitPrice * item.Quantity * (applicableDiscount.DiscountValue / 100);
|
||||||
|
// Hier müsste die Logik für FixedAmount-Rabatte hin
|
||||||
|
}
|
||||||
|
|
||||||
|
totalDiscount += itemDiscount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Wende Warenkorb-Rabatte an (falls keine spezifischen Produkte/Kategorien zugewiesen sind)
|
||||||
|
var cartDiscounts = discounts.Where(d => !d.ProductDiscounts.Any() && !d.categorieDiscounts.Any()).ToList();
|
||||||
|
|
||||||
|
decimal cartTotal = orderItems.Sum(i => i.TotalPrice);
|
||||||
|
foreach (var discount in cartDiscounts)
|
||||||
|
{
|
||||||
|
if (discount.MinimumOrderAmount.HasValue && cartTotal < discount.MinimumOrderAmount.Value) continue;
|
||||||
|
|
||||||
|
// Beispiel: Prozentualer Rabatt auf den Warenkorb
|
||||||
|
if (discount.DiscountType == DiscountType.Percentage.ToString())
|
||||||
|
{
|
||||||
|
totalDiscount += cartTotal * (discount.DiscountValue / 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalDiscount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// src/Webshop.Application/Services/Public/Interfaces/IDiscountService.cs
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Webshop.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Webshop.Application.Services.Public.Interfaces
|
||||||
|
{
|
||||||
|
public interface IDiscountService
|
||||||
|
{
|
||||||
|
Task<decimal> CalculateDiscountAsync(List<OrderItem> orderItems, string? couponCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,8 @@ using Webshop.Infrastructure.Data;
|
|||||||
namespace Webshop.Infrastructure.Migrations
|
namespace Webshop.Infrastructure.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
[Migration("20250812095336_discountAndInfo")]
|
[Migration("20250812113218_checkout")]
|
||||||
partial class discountAndInfo
|
partial class checkout
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
@@ -7,7 +7,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||||||
namespace Webshop.Infrastructure.Migrations
|
namespace Webshop.Infrastructure.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class discountAndInfo : Migration
|
public partial class checkout : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
Reference in New Issue
Block a user