Warenkorb & Bestellungen API

This commit is contained in:
Aminekan
2026-03-19 12:30:15 +01:00
parent d297055cc4
commit 7434fc2e38
22 changed files with 345 additions and 14 deletions

View File

@@ -1,6 +1,88 @@
namespace ShopAPI.Controllers using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ShopAPI.DTOs;
using ShopAPI.Models;
using ShopAPI.Services;
using System.Security.Claims;
namespace ShopAPI.Controllers
{ {
public class OrderController [Authorize]
[ApiController]
[Route("api/[controller]")]
public class OrderController : ControllerBase
{ {
private readonly OrderService _orderService;
public OrderController(OrderService orderService)
{
_orderService = orderService;
}
// hilfsmethode - eingeloggten User ID bekommen
private int GetUserId() => int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
// Get api/order (Admin -> alle Bestellungen)
[HttpGet]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> GetAll()
{
var orders = await _orderService.GetAllAsync();
return Ok(orders);
}
// Get api/order/my (Customer -> meine Bestellungen)
[HttpGet("my")]
public async Task<IActionResult> GetMyOrders()
{
var orders = await _orderService.GetMyOrderAsync(GetUserId());
return Ok(orders);
}
// Get api/order/{id}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var order = await _orderService.GetByIdAsync(id);
if(order == null) return NotFound(new {message = "Bestellung nicht gefunden"});
return Ok(order);
}
[HttpPost]
public async Task<IActionResult> Create(OrderDto dto)
{
try
{
var order = await _orderService.CreateAsync(GetUserId(), dto);
return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
}
catch (Exception ex)
{
return BadRequest(new { message = ex.Message });
}
}
[HttpPut("{id}/cancel")]
public async Task<IActionResult> Cancel(int id)
{
try
{
var order = await _orderService.CancelAsync(id, GetUserId());
if (order == null) return NotFound(new { message = "Bestellung nicht gefunden" });
return Ok(order);
}
catch (Exception ex)
{
return BadRequest(new { message = ex.Message });
}
}
[HttpPut("{id}/status")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> UpdateStatus(int id, UpdateStatusDto dto)
{
var order = await _orderService.UpdatesStatusAsync(id, dto.Status);
if (order == null) return NotFound(new { message = "Bestellung nicht gefunden" });
return Ok(order);
}
}
} }
}

View File

@@ -1,7 +1,37 @@
namespace ShopAPI.Controllers using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ShopAPI.DTOs;
using ShopAPI.Services;
using System.Security.Claims;
namespace ShopAPI.Controllers
{ {
public class PaymentController [Authorize]
[ApiController]
[Route("api/[controller]")]
public class PaymentController : ControllerBase
{ {
private readonly PaymentService _paymentService;
public PaymentController(PaymentService paymentService)
{
_paymentService = paymentService;
}
private int GetUserId() => int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
// Post api/payment/pay/{orderId} (Bestellung bezahlen)
[HttpPost("pay/{orderId}")]
public async Task<IActionResult> Pay(int orderId, PaymentDto dto)
{
try
{
var payment = await _paymentService.PayAsync(orderId, GetUserId(), dto);
return Ok(payment);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
} }
} }

View File

@@ -1,6 +1,9 @@
namespace ShopAPI.DTOs using ShopAPI.Models;
namespace ShopAPI.DTOs
{ {
public class OrderDto public class OrderDto
{ {
public List<OrderItemDto> Items { get; set; } = new();
} }
} }

View File

@@ -0,0 +1,8 @@
namespace ShopAPI.DTOs
{
public class OrderItemDto
{
public int ProductId { get; set; }
public int Quantity { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace ShopAPI.DTOs
{
public class PaymentDto
{
public string Method { get; set; } = string.Empty; // "CreditCard", "PayPal", "Cach".
}
}

View File

@@ -0,0 +1,7 @@
namespace ShopAPI.DTOs
{
public class UpdateStatusDto
{
public string Status { get; set; } = string.Empty;
}
}

View File

@@ -40,13 +40,19 @@ namespace ShopAPI
}); });
builder.Services.AddAuthorization(); builder.Services.AddAuthorization();
builder.Services.AddControllers(); builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.ReferenceHandler =
System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
});
// Hier: jeder benutzer bekommt nur seine Daten , und keine vermischung zwischen Requests // Hier: jeder benutzer bekommt nur seine Daten , und keine vermischung zwischen Requests
builder.Services.AddScoped<AuthService>(); builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<ProductService>(); builder.Services.AddScoped<ProductService>();
builder.Services.AddScoped<CategoryService>(); builder.Services.AddScoped<CategoryService>();
builder.Services.AddScoped<OrderService>();
builder.Services.AddScoped<PaymentService>();
var app = builder.Build(); var app = builder.Build();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();

View File

@@ -1,6 +1,141 @@
namespace ShopAPI.Services
using Microsoft.EntityFrameworkCore;
using ShopAPI.Data;
using ShopAPI.DTOs;
using ShopAPI.Models;
namespace ShopAPI.Services
{ {
public class OrderService public class OrderService
{ {
public readonly AppDbContext _db;
public OrderService(AppDbContext db)
{
_db = db;
}
// Get ALL Orders for a User
public async Task<List<Order>> GetAllAsync()
{
return await _db.Orders
.Include(o => o.User)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.Include(o => o.Payment)
.ToListAsync();
}
// Get My Orders (Customer) : Gib mir alle Bestellungen von diesem User
public async Task<List<Order>> GetMyOrderAsync(int userId)
{
return await _db.Orders
.Where(o => o.UserId == userId)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.Include(o => o.Payment)
.ToListAsync();
}
// Get By ID : Gib mir Die eine Bestellung mit dieser ID
public async Task<Order?> GetByIdAsync(int id)
{
return await _db.Orders
.Include(o => o.User)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.Include(o => o.Payment)
.FirstOrDefaultAsync(o => o.Id == id);
}
// Create Order
public async Task<Order> CreateAsync(int userId, OrderDto dto)
{
foreach (var item in dto.Items)
{
var product = await _db.Products.FindAsync(item.ProductId);
if (product == null)
{
throw new Exception($"Produkt {item.ProductId} nciht gefunden");
}
if (product.Stock < item.Quantity)
{
throw new Exception($"Nicht genung lager für {product.Name}");
}
}
// Bestellung erstellen
var order = new Order
{
UserId = userId,
Createdat = DateTime.UtcNow,
Status = "Pending"
};
_db.Orders.Add(order);
await _db.SaveChangesAsync();
// OrderItems
decimal total = 0;
foreach (var item in dto.Items)
{
var product = await _db.Products.FindAsync(item.ProductId);
var orderItem = new OrderItem
{
OrderId = order.Id,
ProductId = item.ProductId,
Quantity = item.Quantity,
UnitPrice = product!.Price
};
product.Stock -= item.Quantity; // Lager aktualisieren
_db.OrderItems.Add(orderItem);
}
await _db.SaveChangesAsync();
return await GetByIdAsync(order.Id) ?? order;
}
// Cancel order
public async Task<Order?> CancelAsync(int id, int userId)
{
var order = await _db.Orders
.Include(o => o.OrderItems)
.FirstOrDefaultAsync(o => o.Id == id && o.UserId == userId);
if (order == null) return null;
if (order.Status == "Shipped")
throw new Exception("Versendete Bestellung kann nicht storniert werden");
// lager zurückgeben
foreach (var item in order.OrderItems)
{
var product = await _db.Products.FindAsync(item.ProductId);
if(product != null)
{
product.Stock += item.Quantity;
}
}
order.Status = "Cancelled";
await _db.SaveChangesAsync();
return order;
}
// Update status (Admin)
public async Task<Order?> UpdatesStatusAsync(int id, string status)
{
var order = await _db.Orders.FindAsync(id);
if(order == null) return null;
order.Status = status;
await _db.SaveChangesAsync();
return order;
}
} }
} }

View File

@@ -0,0 +1,53 @@
using Microsoft.EntityFrameworkCore;
using ShopAPI.Data;
using ShopAPI.DTOs;
using ShopAPI.Models;
namespace ShopAPI.Services
{
public class PaymentService
{
public readonly AppDbContext _db;
public PaymentService(AppDbContext db)
{
_db = db;
}
// Pay Order
public async Task<Payment> PayAsync(int orderId, int uderId, PaymentDto dto)
{
var order = await _db.Orders
.Include(o => o.OrderItems)
.FirstOrDefaultAsync(o => o.Id == orderId && o.UserId == uderId);
if(order == null)
throw new Exception("Order not found");
if(order.Status == "Cancelled")
throw new Exception("Stornierte Bestellung kann nciht bezahlt werden");
// Prüfen ob schon bezahlt
var existing = await _db.Payments.FirstOrDefaultAsync(p => p.OrderId == orderId);
if(existing != null)
throw new Exception("Bestellung wurde bereits bezahlt");
// Gesamtbetrag berechnen
var amount = order.OrderItems.Sum(oi => oi.Quantity * oi.UnitPrice);
var payment = new Payment
{
OrderId = orderId,
Amount = amount,
Method = dto.Method,
Status = "Paid",
PaidAt = DateTime.UtcNow
};
order.Status = "Paid";
_db.Payments.Add(payment);
await _db.SaveChangesAsync();
return payment;
}
}
}

View File

@@ -15,7 +15,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("ShopAPI")] [assembly: System.Reflection.AssemblyCompanyAttribute("ShopAPI")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+384d7e0287f40c1ce9e61d3123377dc5917ef6b6")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+d297055cc4ab7e3c8a93d2b98beca4da08aba908")]
[assembly: System.Reflection.AssemblyProductAttribute("ShopAPI")] [assembly: System.Reflection.AssemblyProductAttribute("ShopAPI")]
[assembly: System.Reflection.AssemblyTitleAttribute("ShopAPI")] [assembly: System.Reflection.AssemblyTitleAttribute("ShopAPI")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -1 +1 @@
e04359f0bf9216ce6ea4f06fefbd8b64b5dbf14e78bc9a52d8c538097c953810 4ace5e3dd2679b7f8d6ba402a9a151576ba9bd2f4940c60938bf7c3a5fa4b2f7

View File

@@ -1 +1 @@
76e1eaf859bca5d672b1fac1789f7abe6bda99f7cc154bb78899e5780554f7a0 4ae0a6b08c2111e583c69b63b26d342f1e34844cf7b3ca861744608079c9e142

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"UbUS1vR6wGATGwMhgD7zMeswN1vTEJ19YTFurt+qa38=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["qXK9XBukmOKR4bC0/OSWUQmD7NVDnODrTNbVfODoSOA=","RsfaIcSdxUsyxkHT0X7TBDvWAE\u002BekQ7N\u002BCKYPxhpGog=","q4x5tw5ZdEVI3wN3ORljilxzvA3pVT3snlXNH\u002Bh3MZw=","hqxvnAagyda0d6U9WqiUEHNFsi/9qmMo9aedinq8ZQA=","TRUWKVmfrqI3Da5OlFteES1pBI/d\u002Bef88\u002BcbmmwH4mw=","AMewKZACPMF34A9M/c\u002BnjYkuddQPOVXhAdpXf\u002BloS2s=","GrwxjTxxr29ICyUcOpweOHua4V5l\u002BtKseEVsI30NOeU="],"CachedAssets":{},"CachedCopyCandidates":{}} {"GlobalPropertiesHash":"UbUS1vR6wGATGwMhgD7zMeswN1vTEJ19YTFurt+qa38=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["qXK9XBukmOKR4bC0/OSWUQmD7NVDnODrTNbVfODoSOA=","RsfaIcSdxUsyxkHT0X7TBDvWAE\u002BekQ7N\u002BCKYPxhpGog=","mwxpKH5s5WhH59E7QrRxfDY8/18lLYY3KNsyd1uFNcs=","q4x5tw5ZdEVI3wN3ORljilxzvA3pVT3snlXNH\u002Bh3MZw=","hqxvnAagyda0d6U9WqiUEHNFsi/9qmMo9aedinq8ZQA=","TRUWKVmfrqI3Da5OlFteES1pBI/d\u002Bef88\u002BcbmmwH4mw=","AMewKZACPMF34A9M/c\u002BnjYkuddQPOVXhAdpXf\u002BloS2s=","jLjjNkMAcR6ICPvLUJeO8hvccCstVj2PSPKA7Fc04j0="],"CachedAssets":{},"CachedCopyCandidates":{}}

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"8Bes/LQh4hq2du9moDLYcP3dWOcm8118awBX2grZ2jA=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["qXK9XBukmOKR4bC0/OSWUQmD7NVDnODrTNbVfODoSOA=","RsfaIcSdxUsyxkHT0X7TBDvWAE\u002BekQ7N\u002BCKYPxhpGog=","q4x5tw5ZdEVI3wN3ORljilxzvA3pVT3snlXNH\u002Bh3MZw=","hqxvnAagyda0d6U9WqiUEHNFsi/9qmMo9aedinq8ZQA=","TRUWKVmfrqI3Da5OlFteES1pBI/d\u002Bef88\u002BcbmmwH4mw=","AMewKZACPMF34A9M/c\u002BnjYkuddQPOVXhAdpXf\u002BloS2s=","GrwxjTxxr29ICyUcOpweOHua4V5l\u002BtKseEVsI30NOeU="],"CachedAssets":{},"CachedCopyCandidates":{}} {"GlobalPropertiesHash":"8Bes/LQh4hq2du9moDLYcP3dWOcm8118awBX2grZ2jA=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["qXK9XBukmOKR4bC0/OSWUQmD7NVDnODrTNbVfODoSOA=","RsfaIcSdxUsyxkHT0X7TBDvWAE\u002BekQ7N\u002BCKYPxhpGog=","mwxpKH5s5WhH59E7QrRxfDY8/18lLYY3KNsyd1uFNcs=","q4x5tw5ZdEVI3wN3ORljilxzvA3pVT3snlXNH\u002Bh3MZw=","hqxvnAagyda0d6U9WqiUEHNFsi/9qmMo9aedinq8ZQA=","TRUWKVmfrqI3Da5OlFteES1pBI/d\u002Bef88\u002BcbmmwH4mw=","AMewKZACPMF34A9M/c\u002BnjYkuddQPOVXhAdpXf\u002BloS2s=","jLjjNkMAcR6ICPvLUJeO8hvccCstVj2PSPKA7Fc04j0="],"CachedAssets":{},"CachedCopyCandidates":{}}