Features Added: - Standard e-commerce properties (Price, Weight, shipping fields) - Order management with Create/Edit views and shipping information - ShippingRates system for weight-based shipping calculations - Comprehensive test coverage with JWT authentication tests - Sample data seeder with 5 orders demonstrating full workflow - Photo upload functionality for products - Multi-cryptocurrency payment support (BTC, XMR, USDT, etc.) Database Changes: - Added ShippingRates table - Added shipping fields to Orders (Name, Address, City, PostCode, Country) - Renamed properties to standard names (BasePrice to Price, ProductWeight to Weight) - Added UpdatedAt timestamps to models UI Improvements: - Added Create/Edit views for Orders - Added ShippingRates management UI - Updated navigation menu with Shipping option - Enhanced Order Details view with shipping information Sample Data: - 3 Categories (Electronics, Clothing, Books) - 5 Products with various prices - 5 Shipping rates (Royal Mail options) - 5 Orders in different statuses (Pending to Delivered) - 3 Crypto payments demonstrating payment flow Security: - All API endpoints secured with JWT authentication - No public endpoints - client apps must authenticate - Privacy-focused design with minimal data collection Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
219 lines
7.3 KiB
C#
219 lines
7.3 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using LittleShop.Data;
|
|
using LittleShop.Models;
|
|
using LittleShop.DTOs;
|
|
using LittleShop.Enums;
|
|
|
|
namespace LittleShop.Services;
|
|
|
|
public class OrderService : IOrderService
|
|
{
|
|
private readonly LittleShopContext _context;
|
|
private readonly ILogger<OrderService> _logger;
|
|
|
|
public OrderService(LittleShopContext context, ILogger<OrderService> logger)
|
|
{
|
|
_context = context;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<IEnumerable<OrderDto>> GetAllOrdersAsync()
|
|
{
|
|
var orders = await _context.Orders
|
|
.Include(o => o.Items)
|
|
.ThenInclude(oi => oi.Product)
|
|
.Include(o => o.Payments)
|
|
.OrderByDescending(o => o.CreatedAt)
|
|
.ToListAsync();
|
|
|
|
return orders.Select(MapToDto);
|
|
}
|
|
|
|
public async Task<IEnumerable<OrderDto>> GetOrdersByIdentityAsync(string identityReference)
|
|
{
|
|
var orders = await _context.Orders
|
|
.Include(o => o.Items)
|
|
.ThenInclude(oi => oi.Product)
|
|
.Include(o => o.Payments)
|
|
.Where(o => o.IdentityReference == identityReference)
|
|
.OrderByDescending(o => o.CreatedAt)
|
|
.ToListAsync();
|
|
|
|
return orders.Select(MapToDto);
|
|
}
|
|
|
|
public async Task<OrderDto?> GetOrderByIdAsync(Guid id)
|
|
{
|
|
var order = await _context.Orders
|
|
.Include(o => o.Items)
|
|
.ThenInclude(oi => oi.Product)
|
|
.Include(o => o.Payments)
|
|
.FirstOrDefaultAsync(o => o.Id == id);
|
|
|
|
return order == null ? null : MapToDto(order);
|
|
}
|
|
|
|
public async Task<OrderDto> CreateOrderAsync(CreateOrderDto createOrderDto)
|
|
{
|
|
using var transaction = await _context.Database.BeginTransactionAsync();
|
|
|
|
try
|
|
{
|
|
var order = new Order
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
IdentityReference = createOrderDto.IdentityReference,
|
|
Status = OrderStatus.PendingPayment,
|
|
TotalAmount = 0,
|
|
Currency = "GBP",
|
|
ShippingName = createOrderDto.ShippingName,
|
|
ShippingAddress = createOrderDto.ShippingAddress,
|
|
ShippingCity = createOrderDto.ShippingCity,
|
|
ShippingPostCode = createOrderDto.ShippingPostCode,
|
|
ShippingCountry = createOrderDto.ShippingCountry,
|
|
Notes = createOrderDto.Notes,
|
|
CreatedAt = DateTime.UtcNow,
|
|
UpdatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
_context.Orders.Add(order);
|
|
|
|
decimal totalAmount = 0;
|
|
foreach (var itemDto in createOrderDto.Items)
|
|
{
|
|
var product = await _context.Products.FindAsync(itemDto.ProductId);
|
|
if (product == null || !product.IsActive)
|
|
{
|
|
throw new ArgumentException($"Product {itemDto.ProductId} not found or inactive");
|
|
}
|
|
|
|
var orderItem = new OrderItem
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
OrderId = order.Id,
|
|
ProductId = itemDto.ProductId,
|
|
Quantity = itemDto.Quantity,
|
|
UnitPrice = product.Price,
|
|
TotalPrice = product.Price * itemDto.Quantity
|
|
};
|
|
|
|
_context.OrderItems.Add(orderItem);
|
|
totalAmount += orderItem.TotalPrice;
|
|
}
|
|
|
|
order.TotalAmount = totalAmount;
|
|
await _context.SaveChangesAsync();
|
|
await transaction.CommitAsync();
|
|
|
|
_logger.LogInformation("Created order {OrderId} for identity {Identity} with total {Total}",
|
|
order.Id, createOrderDto.IdentityReference, totalAmount);
|
|
|
|
// Reload order with includes
|
|
var createdOrder = await GetOrderByIdAsync(order.Id);
|
|
return createdOrder!;
|
|
}
|
|
catch
|
|
{
|
|
await transaction.RollbackAsync();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<bool> UpdateOrderStatusAsync(Guid id, UpdateOrderStatusDto updateOrderStatusDto)
|
|
{
|
|
var order = await _context.Orders.FindAsync(id);
|
|
if (order == null) return false;
|
|
|
|
order.Status = updateOrderStatusDto.Status;
|
|
|
|
if (!string.IsNullOrEmpty(updateOrderStatusDto.TrackingNumber))
|
|
{
|
|
order.TrackingNumber = updateOrderStatusDto.TrackingNumber;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(updateOrderStatusDto.Notes))
|
|
{
|
|
order.Notes = updateOrderStatusDto.Notes;
|
|
}
|
|
|
|
if (updateOrderStatusDto.Status == OrderStatus.Shipped && order.ShippedAt == null)
|
|
{
|
|
order.ShippedAt = DateTime.UtcNow;
|
|
}
|
|
|
|
order.UpdatedAt = DateTime.UtcNow;
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("Updated order {OrderId} status to {Status}", id, updateOrderStatusDto.Status);
|
|
|
|
return true;
|
|
}
|
|
|
|
public async Task<bool> CancelOrderAsync(Guid id, string identityReference)
|
|
{
|
|
var order = await _context.Orders.FindAsync(id);
|
|
if (order == null || order.IdentityReference != identityReference)
|
|
return false;
|
|
|
|
if (order.Status != OrderStatus.PendingPayment)
|
|
{
|
|
return false; // Can only cancel pending orders
|
|
}
|
|
|
|
order.Status = OrderStatus.Cancelled;
|
|
order.UpdatedAt = DateTime.UtcNow;
|
|
await _context.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("Cancelled order {OrderId} by identity {Identity}", id, identityReference);
|
|
|
|
return true;
|
|
}
|
|
|
|
private static OrderDto MapToDto(Order order)
|
|
{
|
|
return new OrderDto
|
|
{
|
|
Id = order.Id,
|
|
IdentityReference = order.IdentityReference,
|
|
Status = order.Status,
|
|
TotalAmount = order.TotalAmount,
|
|
Currency = order.Currency,
|
|
ShippingName = order.ShippingName,
|
|
ShippingAddress = order.ShippingAddress,
|
|
ShippingCity = order.ShippingCity,
|
|
ShippingPostCode = order.ShippingPostCode,
|
|
ShippingCountry = order.ShippingCountry,
|
|
Notes = order.Notes,
|
|
TrackingNumber = order.TrackingNumber,
|
|
CreatedAt = order.CreatedAt,
|
|
UpdatedAt = order.UpdatedAt,
|
|
PaidAt = order.PaidAt,
|
|
ShippedAt = order.ShippedAt,
|
|
Items = order.Items.Select(oi => new OrderItemDto
|
|
{
|
|
Id = oi.Id,
|
|
ProductId = oi.ProductId,
|
|
ProductName = oi.Product.Name,
|
|
Quantity = oi.Quantity,
|
|
UnitPrice = oi.UnitPrice,
|
|
TotalPrice = oi.TotalPrice
|
|
}).ToList(),
|
|
Payments = order.Payments.Select(cp => new CryptoPaymentDto
|
|
{
|
|
Id = cp.Id,
|
|
OrderId = cp.OrderId,
|
|
Currency = cp.Currency,
|
|
WalletAddress = cp.WalletAddress,
|
|
RequiredAmount = cp.RequiredAmount,
|
|
PaidAmount = cp.PaidAmount,
|
|
Status = cp.Status,
|
|
BTCPayInvoiceId = cp.BTCPayInvoiceId,
|
|
TransactionHash = cp.TransactionHash,
|
|
CreatedAt = cp.CreatedAt,
|
|
PaidAt = cp.PaidAt,
|
|
ExpiresAt = cp.ExpiresAt
|
|
}).ToList()
|
|
};
|
|
}
|
|
} |