diff --git a/src/Imprink.Application/Validation/Models/OrderFilterParametersValidator.cs b/src/Imprink.Application/Validation/Models/OrderFilterParametersValidator.cs new file mode 100644 index 0000000..0efcdad --- /dev/null +++ b/src/Imprink.Application/Validation/Models/OrderFilterParametersValidator.cs @@ -0,0 +1,69 @@ +using FluentValidation; +using Imprink.Domain.Models; + +namespace Imprink.Application.Validation.Models; + +public class OrderFilterParametersValidator : AbstractValidator +{ + public OrderFilterParametersValidator() + { + RuleFor(x => x.PageNumber) + .GreaterThan(0).WithMessage("PageNumber must be greater than 0."); + + RuleFor(x => x.PageSize) + .InclusiveBetween(1, 100).WithMessage("PageSize must be between 1 and 100."); + + RuleFor(x => x.UserId) + .Length(1, 450).WithMessage("UserId length must be between 1 and 450 characters.") + .When(x => !string.IsNullOrWhiteSpace(x.UserId)); + + RuleFor(x => x.OrderNumber) + .Length(1, 50).WithMessage("OrderNumber length must be between 1 and 50 characters.") + .When(x => !string.IsNullOrWhiteSpace(x.OrderNumber)); + + RuleFor(x => x.OrderStatusId) + .GreaterThan(0).When(x => x.OrderStatusId.HasValue) + .WithMessage("OrderStatusId must be greater than 0."); + + RuleFor(x => x.ShippingStatusId) + .GreaterThan(0).When(x => x.ShippingStatusId.HasValue) + .WithMessage("ShippingStatusId must be greater than 0."); + + RuleFor(x => x.StartDate) + .LessThanOrEqualTo(DateTime.UtcNow.AddDays(1)).When(x => x.StartDate.HasValue) + .WithMessage("StartDate cannot be in the future."); + + RuleFor(x => x.EndDate) + .LessThanOrEqualTo(DateTime.UtcNow.AddDays(1)).When(x => x.EndDate.HasValue) + .WithMessage("EndDate cannot be in the future."); + + RuleFor(x => x) + .Must(x => !x.StartDate.HasValue || !x.EndDate.HasValue || x.StartDate <= x.EndDate) + .WithMessage("StartDate cannot be greater than EndDate."); + + RuleFor(x => x.MinTotalPrice) + .GreaterThanOrEqualTo(0).When(x => x.MinTotalPrice.HasValue) + .WithMessage("MinTotalPrice cannot be negative."); + + RuleFor(x => x.MaxTotalPrice) + .GreaterThanOrEqualTo(0).When(x => x.MaxTotalPrice.HasValue) + .WithMessage("MaxTotalPrice cannot be negative."); + + RuleFor(x => x) + .Must(x => !x.MinTotalPrice.HasValue || !x.MaxTotalPrice.HasValue || x.MinTotalPrice <= x.MaxTotalPrice) + .WithMessage("MinTotalPrice cannot be greater than MaxTotalPrice."); + + RuleFor(x => x.SortBy) + .NotEmpty().WithMessage("SortBy is required.") + .Must(value => AllowedSortColumns.Contains(value, StringComparer.OrdinalIgnoreCase)) + .WithMessage("SortBy must be one of: OrderDate, TotalPrice, OrderNumber."); + + RuleFor(x => x.SortDirection) + .NotEmpty().WithMessage("SortDirection is required.") + .Must(value => value.Equals("ASC", StringComparison.OrdinalIgnoreCase) + || value.Equals("DESC", StringComparison.OrdinalIgnoreCase)) + .WithMessage("SortDirection must be 'ASC' or 'DESC'."); + } + + private static readonly string[] AllowedSortColumns = ["OrderDate", "TotalPrice", "OrderNumber"]; +} \ No newline at end of file diff --git a/src/Imprink.Application/Validation/Products/GetProductsQueryValidator.cs b/src/Imprink.Application/Validation/Products/GetProductsQueryValidator.cs index 21a6b37..9026474 100644 --- a/src/Imprink.Application/Validation/Products/GetProductsQueryValidator.cs +++ b/src/Imprink.Application/Validation/Products/GetProductsQueryValidator.cs @@ -1,6 +1,5 @@ using FluentValidation; using Imprink.Application.Domains.Products; -using Imprink.Application.Products; using Imprink.Application.Validation.Models; namespace Imprink.Application.Validation.Products; diff --git a/src/Imprink.Domain/Imprink.Domain.csproj b/src/Imprink.Domain/Imprink.Domain.csproj index 1584c08..3302df9 100644 --- a/src/Imprink.Domain/Imprink.Domain.csproj +++ b/src/Imprink.Domain/Imprink.Domain.csproj @@ -10,8 +10,4 @@ - - - - diff --git a/src/Imprink.Domain/Models/OrderFilterParameters.cs b/src/Imprink.Domain/Models/OrderFilterParameters.cs new file mode 100644 index 0000000..f917a8e --- /dev/null +++ b/src/Imprink.Domain/Models/OrderFilterParameters.cs @@ -0,0 +1,46 @@ +namespace Imprink.Domain.Models; + +public class OrderFilterParameters +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string? UserId { get; set; } + + public string? OrderNumber { get; set; } + + public int? OrderStatusId { get; set; } + + public int? ShippingStatusId { get; set; } + + public DateTime? StartDate { get; set; } + + public DateTime? EndDate { get; set; } + + public decimal? MinTotalPrice { get; set; } + + public decimal? MaxTotalPrice { get; set; } + + public string SortBy { get; set; } = "OrderDate"; + + public string SortDirection { get; set; } = "DESC"; + + public bool IsValidDateRange() + { + if (StartDate.HasValue && EndDate.HasValue) + { + return StartDate.Value <= EndDate.Value; + } + return true; + } + + public bool IsValidPriceRange() + { + if (MinTotalPrice.HasValue && MaxTotalPrice.HasValue) + { + return MinTotalPrice.Value <= MaxTotalPrice.Value; + } + return true; + } +} \ No newline at end of file diff --git a/src/Imprink.Domain/Repositories/Orders/IOrderItemRepository.cs b/src/Imprink.Domain/Repositories/Orders/IOrderItemRepository.cs new file mode 100644 index 0000000..d3699d9 --- /dev/null +++ b/src/Imprink.Domain/Repositories/Orders/IOrderItemRepository.cs @@ -0,0 +1,27 @@ +using Imprink.Domain.Entities.Orders; + +namespace Imprink.Domain.Repositories.Orders; + +public interface IOrderItemRepository +{ + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdWithProductAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdWithVariantAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdFullAsync(Guid id, CancellationToken cancellationToken = default); + Task> GetByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default); + Task> GetByOrderIdWithProductsAsync(Guid orderId, CancellationToken cancellationToken = default); + Task> GetByProductIdAsync(Guid productId, CancellationToken cancellationToken = default); + Task> GetByProductVariantIdAsync(Guid productVariantId, CancellationToken cancellationToken = default); + Task> GetCustomizedItemsAsync(CancellationToken cancellationToken = default); + Task> GetByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default); + Task AddAsync(OrderItem orderItem, CancellationToken cancellationToken = default); + Task> AddRangeAsync(IEnumerable orderItems, CancellationToken cancellationToken = default); + Task UpdateAsync(OrderItem orderItem, CancellationToken cancellationToken = default); + Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); + Task DeleteByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default); + Task ExistsAsync(Guid id, CancellationToken cancellationToken = default); + Task GetTotalValueByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default); + Task GetQuantityByProductIdAsync(Guid productId, CancellationToken cancellationToken = default); + Task GetQuantityByVariantIdAsync(Guid productVariantId, CancellationToken cancellationToken = default); + Task> GetProductSalesCountAsync(DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Imprink.Domain/Repositories/Orders/IOrderRepository.cs b/src/Imprink.Domain/Repositories/Orders/IOrderRepository.cs new file mode 100644 index 0000000..c45ae48 --- /dev/null +++ b/src/Imprink.Domain/Repositories/Orders/IOrderRepository.cs @@ -0,0 +1,27 @@ +using Imprink.Domain.Entities.Orders; +using Imprink.Domain.Models; + +namespace Imprink.Domain.Repositories.Orders; + +public interface IOrderRepository +{ + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdWithItemsAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdWithAddressAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdFullAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByOrderNumberAsync(string orderNumber, CancellationToken cancellationToken = default); + Task> GetPagedAsync(OrderFilterParameters filterParameters, CancellationToken cancellationToken = default); + Task> GetByUserIdAsync(string userId, CancellationToken cancellationToken = default); + Task> GetByUserIdPagedAsync(string userId, int pageNumber, int pageSize, CancellationToken cancellationToken = default); + Task> GetByOrderStatusAsync(int orderStatusId, CancellationToken cancellationToken = default); + Task> GetByShippingStatusAsync(int shippingStatusId, CancellationToken cancellationToken = default); + Task> GetByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default); + Task AddAsync(Order order, CancellationToken cancellationToken = default); + Task UpdateAsync(Order order, CancellationToken cancellationToken = default); + Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); + Task ExistsAsync(Guid id, CancellationToken cancellationToken = default); + Task OrderNumberExistsAsync(string orderNumber, Guid? excludeId = null, CancellationToken cancellationToken = default); + Task GetTotalRevenueAsync(CancellationToken cancellationToken = default); + Task GetTotalRevenueByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default); + Task GetOrderCountByStatusAsync(int orderStatusId, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Imprink.Infrastructure/Imprink.Infrastructure.csproj b/src/Imprink.Infrastructure/Imprink.Infrastructure.csproj index 8bf4ad7..8650de1 100644 --- a/src/Imprink.Infrastructure/Imprink.Infrastructure.csproj +++ b/src/Imprink.Infrastructure/Imprink.Infrastructure.csproj @@ -25,7 +25,6 @@ - diff --git a/src/Imprink.Infrastructure/Repositories/Orders/OrderItemRepository.cs b/src/Imprink.Infrastructure/Repositories/Orders/OrderItemRepository.cs new file mode 100644 index 0000000..ed9e91f --- /dev/null +++ b/src/Imprink.Infrastructure/Repositories/Orders/OrderItemRepository.cs @@ -0,0 +1,204 @@ +using Imprink.Domain.Entities.Orders; +using Imprink.Domain.Repositories.Orders; +using Imprink.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace Imprink.Infrastructure.Repositories.Orders; + +public class OrderItemRepository(ApplicationDbContext context) : IOrderItemRepository +{ + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .FirstOrDefaultAsync(oi => oi.Id == id, cancellationToken); + } + + public async Task GetByIdWithProductAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.Product) + .ThenInclude(p => p.Category) + .FirstOrDefaultAsync(oi => oi.Id == id, cancellationToken); + } + + public async Task GetByIdWithVariantAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.ProductVariant) + .FirstOrDefaultAsync(oi => oi.Id == id, cancellationToken); + } + + public async Task GetByIdFullAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.Order) + .ThenInclude(o => o.User) + .Include(oi => oi.Product) + .ThenInclude(p => p.Category) + .Include(oi => oi.ProductVariant) + .FirstOrDefaultAsync(oi => oi.Id == id, cancellationToken); + } + + public async Task> GetByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Where(oi => oi.OrderId == orderId) + .OrderBy(oi => oi.CreatedAt) + .ToListAsync(cancellationToken); + } + + public async Task> GetByOrderIdWithProductsAsync(Guid orderId, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.Product) + .ThenInclude(p => p.Category) + .Include(oi => oi.ProductVariant) + .Where(oi => oi.OrderId == orderId) + .OrderBy(oi => oi.CreatedAt) + .ToListAsync(cancellationToken); + } + + public async Task> GetByProductIdAsync(Guid productId, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.Order) + .Where(oi => oi.ProductId == productId) + .OrderByDescending(oi => oi.CreatedAt) + .ToListAsync(cancellationToken); + } + + public async Task> GetByProductVariantIdAsync(Guid productVariantId, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.Order) + .Where(oi => oi.ProductVariantId == productVariantId) + .OrderByDescending(oi => oi.CreatedAt) + .ToListAsync(cancellationToken); + } + + public async Task> GetCustomizedItemsAsync(CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.Product) + .Include(oi => oi.Order) + .Where(oi => !string.IsNullOrEmpty(oi.CustomizationImageUrl) || !string.IsNullOrEmpty(oi.CustomizationDescription)) + .OrderByDescending(oi => oi.CreatedAt) + .ToListAsync(cancellationToken); + } + + public async Task> GetByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.Order) + .Include(oi => oi.Product) + .Where(oi => oi.Order.OrderDate >= startDate && oi.Order.OrderDate <= endDate) + .OrderByDescending(oi => oi.Order.OrderDate) + .ToListAsync(cancellationToken); + } + + public Task AddAsync(OrderItem orderItem, CancellationToken cancellationToken = default) + { + orderItem.Id = Guid.NewGuid(); + orderItem.CreatedAt = DateTime.UtcNow; + orderItem.ModifiedAt = DateTime.UtcNow; + + context.OrderItems.Add(orderItem); + return Task.FromResult(orderItem); + } + + public Task> AddRangeAsync(IEnumerable orderItems, CancellationToken cancellationToken = default) + { + var items = orderItems.ToList(); + var utcNow = DateTime.UtcNow; + + foreach (var item in items) + { + item.Id = Guid.NewGuid(); + item.CreatedAt = utcNow; + item.ModifiedAt = utcNow; + } + + context.OrderItems.AddRange(items); + return Task.FromResult>(items); + } + + public Task UpdateAsync(OrderItem orderItem, CancellationToken cancellationToken = default) + { + orderItem.ModifiedAt = DateTime.UtcNow; + context.OrderItems.Update(orderItem); + return Task.FromResult(orderItem); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var orderItem = await GetByIdAsync(id, cancellationToken); + if (orderItem != null) + { + context.OrderItems.Remove(orderItem); + } + } + + public async Task DeleteByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default) + { + var orderItems = await context.OrderItems + .Where(oi => oi.OrderId == orderId) + .ToListAsync(cancellationToken); + + if (orderItems.Any()) + { + context.OrderItems.RemoveRange(orderItems); + } + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .AnyAsync(oi => oi.Id == id, cancellationToken); + } + + public async Task GetTotalValueByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Where(oi => oi.OrderId == orderId) + .SumAsync(oi => oi.TotalPrice, cancellationToken); + } + + public async Task GetQuantityByProductIdAsync(Guid productId, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Where(oi => oi.ProductId == productId) + .SumAsync(oi => oi.Quantity, cancellationToken); + } + + public async Task GetQuantityByVariantIdAsync(Guid productVariantId, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Where(oi => oi.ProductVariantId == productVariantId) + .SumAsync(oi => oi.Quantity, cancellationToken); + } + + public async Task> GetProductSalesCountAsync( + DateTime? startDate = null, + DateTime? endDate = null, + CancellationToken cancellationToken = default) + { + var query = context.OrderItems + .Include(oi => oi.Order) + .AsQueryable(); + + if (startDate.HasValue) + { + query = query.Where(oi => oi.Order.OrderDate >= startDate.Value); + } + + if (endDate.HasValue) + { + query = query.Where(oi => oi.Order.OrderDate <= endDate.Value); + } + + return await query + .GroupBy(oi => oi.ProductId) + .Select(g => new { ProductId = g.Key, TotalQuantity = g.Sum(oi => oi.Quantity) }) + .ToDictionaryAsync(x => x.ProductId, x => x.TotalQuantity, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Imprink.Infrastructure/Repositories/Orders/OrderRepository.cs b/src/Imprink.Infrastructure/Repositories/Orders/OrderRepository.cs new file mode 100644 index 0000000..b285a94 --- /dev/null +++ b/src/Imprink.Infrastructure/Repositories/Orders/OrderRepository.cs @@ -0,0 +1,265 @@ +using Imprink.Domain.Entities.Orders; +using Imprink.Domain.Models; +using Imprink.Domain.Repositories.Orders; +using Imprink.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace Imprink.Infrastructure.Repositories.Orders; + +public class OrderRepository(ApplicationDbContext context) : IOrderRepository +{ + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Orders + .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); + } + + public async Task GetByIdWithItemsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.OrderItems) + .ThenInclude(oi => oi.Product) + .Include(o => o.OrderItems) + .ThenInclude(oi => oi.ProductVariant) + .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); + } + + public async Task GetByIdWithAddressAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.OrderAddress) + .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); + } + + public async Task GetByIdFullAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.User) + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .Include(o => o.OrderAddress) + .Include(o => o.OrderItems) + .ThenInclude(oi => oi.Product) + .ThenInclude(p => p.Category) + .Include(o => o.OrderItems) + .ThenInclude(oi => oi.ProductVariant) + .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); + } + + public async Task GetByOrderNumberAsync(string orderNumber, CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .FirstOrDefaultAsync(o => o.OrderNumber == orderNumber, cancellationToken); + } + + public async Task> GetPagedAsync( + OrderFilterParameters filterParameters, + CancellationToken cancellationToken = default) + { + var query = context.Orders + .Include(o => o.User) + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .AsQueryable(); + + if (!string.IsNullOrEmpty(filterParameters.UserId)) + { + query = query.Where(o => o.UserId == filterParameters.UserId); + } + + if (!string.IsNullOrEmpty(filterParameters.OrderNumber)) + { + query = query.Where(o => o.OrderNumber.Contains(filterParameters.OrderNumber)); + } + + if (filterParameters.OrderStatusId.HasValue) + { + query = query.Where(o => o.OrderStatusId == filterParameters.OrderStatusId.Value); + } + + if (filterParameters.ShippingStatusId.HasValue) + { + query = query.Where(o => o.ShippingStatusId == filterParameters.ShippingStatusId.Value); + } + + if (filterParameters.StartDate.HasValue) + { + query = query.Where(o => o.OrderDate >= filterParameters.StartDate.Value); + } + + if (filterParameters.EndDate.HasValue) + { + query = query.Where(o => o.OrderDate <= filterParameters.EndDate.Value); + } + + if (filterParameters.MinTotalPrice.HasValue) + { + query = query.Where(o => o.TotalPrice >= filterParameters.MinTotalPrice.Value); + } + + if (filterParameters.MaxTotalPrice.HasValue) + { + query = query.Where(o => o.TotalPrice <= filterParameters.MaxTotalPrice.Value); + } + + query = filterParameters.SortBy.ToLower() switch + { + "orderdate" => filterParameters.SortDirection.Equals("DESC", StringComparison.CurrentCultureIgnoreCase) + ? query.OrderByDescending(o => o.OrderDate) + : query.OrderBy(o => o.OrderDate), + "totalprice" => filterParameters.SortDirection.Equals("DESC", StringComparison.CurrentCultureIgnoreCase) + ? query.OrderByDescending(o => o.TotalPrice) + : query.OrderBy(o => o.TotalPrice), + "ordernumber" => filterParameters.SortDirection.Equals("DESC", StringComparison.CurrentCultureIgnoreCase) + ? query.OrderByDescending(o => o.OrderNumber) + : query.OrderBy(o => o.OrderNumber), + _ => query.OrderByDescending(o => o.OrderDate) + }; + + var totalCount = await query.CountAsync(cancellationToken); + + var items = await query + .Skip((filterParameters.PageNumber - 1) * filterParameters.PageSize) + .Take(filterParameters.PageSize) + .ToListAsync(cancellationToken); + + return new PagedResult + { + Items = items, + TotalCount = totalCount, + PageNumber = filterParameters.PageNumber, + PageSize = filterParameters.PageSize + }; + } + + public async Task> GetByUserIdAsync(string userId, CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .Where(o => o.UserId == userId) + .OrderByDescending(o => o.OrderDate) + .ToListAsync(cancellationToken); + } + + public async Task> GetByUserIdPagedAsync( + string userId, + int pageNumber, + int pageSize, + CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .Where(o => o.UserId == userId) + .OrderByDescending(o => o.OrderDate) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + } + + public async Task> GetByOrderStatusAsync(int orderStatusId, CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.User) + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .Where(o => o.OrderStatusId == orderStatusId) + .OrderByDescending(o => o.OrderDate) + .ToListAsync(cancellationToken); + } + + public async Task> GetByShippingStatusAsync(int shippingStatusId, CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.User) + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .Where(o => o.ShippingStatusId == shippingStatusId) + .OrderByDescending(o => o.OrderDate) + .ToListAsync(cancellationToken); + } + + public async Task> GetByDateRangeAsync( + DateTime startDate, + DateTime endDate, + CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.User) + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .Where(o => o.OrderDate >= startDate && o.OrderDate <= endDate) + .OrderByDescending(o => o.OrderDate) + .ToListAsync(cancellationToken); + } + + public Task AddAsync(Order order, CancellationToken cancellationToken = default) + { + order.Id = Guid.NewGuid(); + order.CreatedAt = DateTime.UtcNow; + order.ModifiedAt = DateTime.UtcNow; + + context.Orders.Add(order); + return Task.FromResult(order); + } + + public Task UpdateAsync(Order order, CancellationToken cancellationToken = default) + { + order.ModifiedAt = DateTime.UtcNow; + context.Orders.Update(order); + return Task.FromResult(order); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var order = await GetByIdAsync(id, cancellationToken); + if (order != null) + { + context.Orders.Remove(order); + } + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Orders + .AnyAsync(o => o.Id == id, cancellationToken); + } + + public async Task OrderNumberExistsAsync(string orderNumber, Guid? excludeId = null, CancellationToken cancellationToken = default) + { + var query = context.Orders.Where(o => o.OrderNumber == orderNumber); + + if (excludeId.HasValue) + { + query = query.Where(o => o.Id != excludeId.Value); + } + + return await query.AnyAsync(cancellationToken); + } + + public async Task GetTotalRevenueAsync(CancellationToken cancellationToken = default) + { + return await context.Orders + .Where(o => o.OrderStatusId != 5) // Assuming 5 is cancelled status + .SumAsync(o => o.TotalPrice, cancellationToken); + } + + public async Task GetTotalRevenueByDateRangeAsync( + DateTime startDate, + DateTime endDate, + CancellationToken cancellationToken = default) + { + return await context.Orders + .Where(o => o.OrderDate >= startDate && o.OrderDate <= endDate && o.OrderStatusId != 5) + .SumAsync(o => o.TotalPrice, cancellationToken); + } + + public async Task GetOrderCountByStatusAsync(int orderStatusId, CancellationToken cancellationToken = default) + { + return await context.Orders + .CountAsync(o => o.OrderStatusId == orderStatusId, cancellationToken); + } +} \ No newline at end of file