littleshop/LittleShop/Services/OrderService.cs
2025-08-27 18:02:39 +01:00

278 lines
10 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;
private readonly ICustomerService _customerService;
public OrderService(LittleShopContext context, ILogger<OrderService> logger, ICustomerService customerService)
{
_context = context;
_logger = logger;
_customerService = customerService;
}
public async Task<IEnumerable<OrderDto>> GetAllOrdersAsync()
{
var orders = await _context.Orders
.Include(o => o.Customer)
.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.Customer)
.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.Customer)
.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
{
// Handle customer creation/linking during checkout
Guid? customerId = null;
string? identityReference = null;
if (createOrderDto.CustomerInfo != null)
{
// Create customer during checkout process
var customer = await _customerService.GetOrCreateCustomerAsync(
createOrderDto.CustomerInfo.TelegramUserId,
createOrderDto.CustomerInfo.TelegramDisplayName,
createOrderDto.CustomerInfo.TelegramUsername,
createOrderDto.CustomerInfo.TelegramFirstName,
createOrderDto.CustomerInfo.TelegramLastName);
customerId = customer?.Id;
}
else if (createOrderDto.CustomerId.HasValue)
{
// Order for existing customer
customerId = createOrderDto.CustomerId;
}
else
{
// Anonymous order (legacy support)
identityReference = createOrderDto.IdentityReference;
}
var order = new Order
{
Id = Guid.NewGuid(),
CustomerId = customerId,
IdentityReference = 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();
if (customerId.HasValue)
{
_logger.LogInformation("Created order {OrderId} for customer {CustomerId} with total {Total}",
order.Id, customerId.Value, totalAmount);
}
else
{
_logger.LogInformation("Created order {OrderId} for identity {Identity} with total {Total}",
order.Id, 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,
CustomerId = order.CustomerId,
IdentityReference = order.IdentityReference,
Status = order.Status,
Customer = order.Customer != null ? new CustomerSummaryDto
{
Id = order.Customer.Id,
DisplayName = !string.IsNullOrEmpty(order.Customer.TelegramDisplayName) ? order.Customer.TelegramDisplayName :
!string.IsNullOrEmpty(order.Customer.TelegramUsername) ? $"@{order.Customer.TelegramUsername}" :
$"{order.Customer.TelegramFirstName} {order.Customer.TelegramLastName}".Trim(),
TelegramUsername = order.Customer.TelegramUsername,
TotalOrders = order.Customer.TotalOrders,
TotalSpent = order.Customer.TotalSpent,
CustomerType = order.Customer.TotalOrders == 0 ? "New" :
order.Customer.TotalOrders == 1 ? "First-time" :
order.Customer.TotalOrders < 5 ? "Regular" :
order.Customer.TotalOrders < 20 ? "Loyal" : "VIP",
RiskScore = order.Customer.RiskScore,
LastActiveAt = order.Customer.LastActiveAt,
IsBlocked = order.Customer.IsBlocked
} : null,
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()
};
}
}