littleshop/LittleShop/Services/OrderService.cs
sysadmin a281bb2896 Implement complete e-commerce functionality with shipping and order management
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>
2025-08-20 17:37:24 +01:00

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()
};
}
}