using Microsoft.EntityFrameworkCore; using StoreService.Database.Entities; using StoreService.Models; using StoreService.Repositories; namespace StoreService.Services; /// /// Implements order creation, payment, redemption and price calculation logic. /// public class OrderService : IOrderService { #region Fields private readonly IUnitOfWork _uow; private readonly ILogger _logger; #endregion #region Ctor public OrderService(IUnitOfWork uow, ILogger logger) { _uow = uow; _logger = logger; } #endregion #region Public Methods /// public async Task CreateOrderAsync(CreateOrderRequest request, CancellationToken ct = default) { if (request.StoreItemIds.Count == 0) throw new ArgumentException("No store items specified", nameof(request.StoreItemIds)); // Ensure uniqueness var uniqueIds = request.StoreItemIds.Distinct().ToList(); // Load items with potential discount items and discounts for calculation. var items = await _uow.StoreItems .Get(i => uniqueIds.Contains(i.Id), includes: i => i.DiscountItems) .Include(i => i.DiscountItems) // ensure collection loaded .ToListAsync(ct); if (items.Count != uniqueIds.Count) { var missing = uniqueIds.Except(items.Select(i => i.Id)).ToArray(); throw new KeyNotFoundException($"Store items not found: {string.Join(", ", missing)}"); } // Preload discounts referenced by items for efficient calculation. var discountIds = items.SelectMany(i => i.DiscountItems.Select(di => di.StoreDiscountId)).Distinct().ToList(); var discounts = await _uow.StoreDiscounts .Get(d => discountIds.Contains(d.Id)) .ToListAsync(ct); var now = DateTime.UtcNow; var order = new StoreOrder { UserId = request.UserId, CostUpdateDate = now, OrderItems = new List() }; foreach (var item in items) { var applicableDiscounts = discounts .Where(d => item.DiscountItems.Any(di => di.StoreDiscountId == d.Id) && d.IsActive(now)) .ToList(); var calculatedPrice = CalculatePrice(item.ManaBuyPrice, applicableDiscounts); var orderItem = new StoreOrderItem { StoreItemId = item.Id, CalculatedPrice = calculatedPrice, AppliedDiscounts = applicableDiscounts.Select(d => new StoreOrderItemDiscount { StoreDiscountId = d.Id }).ToList() }; order.OrderItems.Add(orderItem); } await _uow.StoreOrders.AddAsync(order); await _uow.SaveChangesAsync(ct); _logger.LogInformation("Created order {OrderId} for user {UserId}", order.Id, order.UserId); return MapOrder(order); } /// public async Task GetOrderAsync(long id, CancellationToken ct = default) { var order = await _uow.StoreOrders .Get(o => o.Id == id, includes: o => o.OrderItems) .Include(o => o.OrderItems) .ThenInclude(oi => oi.AppliedDiscounts) .FirstOrDefaultAsync(ct); return order == null ? null : MapOrder(order); } /// public async Task PayOrderAsync(long id, CancellationToken ct = default) { var order = await _uow.StoreOrders .Get(o => o.Id == id, includes: o => o.OrderItems) .FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException($"Order {id} not found"); if (order.PaidDate != null) throw new InvalidOperationException("Order already paid"); order.PaidDate = DateTime.UtcNow; await _uow.SaveChangesAsync(ct); _logger.Information("Order {OrderId} paid", id); return await GetOrderRequiredAsync(id, ct); } /// public async Task RedeemOrderItemsAsync(long id, CancellationToken ct = default) { var order = await _uow.StoreOrders.Get(o => o.Id == id).FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException($"Order {id} not found"); if (order.PaidDate == null) throw new InvalidOperationException("Order not paid yet"); if (order.ItemsRedeemed) throw new InvalidOperationException("Order items already redeemed"); // TODO: integrate with external inventory service. For now we just toggle flag. order.ItemsRedeemed = true; await _uow.SaveChangesAsync(ct); _logger.Information("Order {OrderId} items redeemed", id); return await GetOrderRequiredAsync(id, ct); } #endregion #region Helpers private int CalculatePrice(int basePrice, IEnumerable discounts) { // Assumption: sum discount percentages, cap at 90% to avoid free unless explicitly 100%. Documented decision. var totalPercent = discounts.Sum(d => d.Percentage); if (totalPercent > 100m) totalPercent = 100m; // absolute cap var discounted = basePrice - (int)Math.Floor(basePrice * (decimal)totalPercent / 100m); return Math.Max(0, discounted); } private async Task GetOrderRequiredAsync(long id, CancellationToken ct) { var dto = await GetOrderAsync(id, ct); if (dto == null) throw new KeyNotFoundException($"Order {id} not found after update"); return dto; } private static OrderDto MapOrder(StoreOrder order) => new() { Id = order.Id, UserId = order.UserId, CostUpdateDate = order.CostUpdateDate, PaidDate = order.PaidDate, ItemsRedeemed = order.ItemsRedeemed, Items = order.OrderItems.Select(oi => new OrderItemDto { Id = oi.Id, StoreItemId = oi.StoreItemId, CalculatedPrice = oi.CalculatedPrice, AppliedDiscountIds = oi.AppliedDiscounts.Select(ad => ad.StoreDiscountId ?? 0).ToList() }).ToList() }; #endregion }