From 3887e7bc44d838784129d89bf6c4fe4ec82b7b2c Mon Sep 17 00:00:00 2001 From: lumijiez <59575049+lumijiez@users.noreply.github.com> Date: Wed, 25 Jun 2025 22:20:50 +0300 Subject: [PATCH] Add address/order handling --- src/Imprink.Application/IUnitOfWork.cs | 2 +- .../Mappings/ProductMappingProfile.cs | 11 +- src/Imprink.Domain/Entities/Address.cs | 13 +- src/Imprink.Domain/Entities/Order.cs | 15 +- src/Imprink.Domain/Entities/OrderAddress.cs | 12 +- src/Imprink.Domain/Entities/OrderItem.cs | 17 -- src/Imprink.Domain/Entities/Product.cs | 2 +- src/Imprink.Domain/Entities/ProductVariant.cs | 2 +- src/Imprink.Domain/Entities/User.cs | 5 +- .../Repositories/IAddressRepository.cs | 22 ++ .../Repositories/IOrderItemRepository.cs | 27 -- .../Repositories/IOrderRepository.cs | 26 +- .../Configuration/AddressConfiguration.cs | 37 ++- .../OrderAddressConfiguration.cs | 33 +- .../Configuration/OrderConfiguration.cs | 187 ++++++++---- .../Configuration/OrderItemConfiguration.cs | 64 ---- .../Configuration/UserConfiguration.cs | 5 +- .../Database/ApplicationDbContext.cs | 2 - .../Repositories/AddressRepository.cs | 168 ++++++++++ .../Repositories/OrderItemRepository.cs | 204 ------------- .../Repositories/OrderRepository.cs | 288 +++++++----------- src/Imprink.Infrastructure/UnitOfWork.cs | 4 +- .../StartupApplicationExtensions.cs | 2 +- .../CreateCategoryHandlerIntegrationTest.cs | 2 +- 24 files changed, 562 insertions(+), 588 deletions(-) delete mode 100644 src/Imprink.Domain/Entities/OrderItem.cs create mode 100644 src/Imprink.Domain/Repositories/IAddressRepository.cs delete mode 100644 src/Imprink.Domain/Repositories/IOrderItemRepository.cs delete mode 100644 src/Imprink.Infrastructure/Configuration/OrderItemConfiguration.cs create mode 100644 src/Imprink.Infrastructure/Repositories/AddressRepository.cs delete mode 100644 src/Imprink.Infrastructure/Repositories/OrderItemRepository.cs diff --git a/src/Imprink.Application/IUnitOfWork.cs b/src/Imprink.Application/IUnitOfWork.cs index a3346a2..4655022 100644 --- a/src/Imprink.Application/IUnitOfWork.cs +++ b/src/Imprink.Application/IUnitOfWork.cs @@ -11,7 +11,7 @@ public interface IUnitOfWork public IUserRoleRepository UserRoleRepository { get; } public IRoleRepository RoleRepository { get; } public IOrderRepository OrderRepository { get; } - public IOrderItemRepository OrderItemRepository { get; } + public IAddressRepository AddressRepository { get; } Task SaveAsync(CancellationToken cancellationToken = default); Task BeginTransactionAsync(CancellationToken cancellationToken = default); diff --git a/src/Imprink.Application/Mappings/ProductMappingProfile.cs b/src/Imprink.Application/Mappings/ProductMappingProfile.cs index 389651f..ba17cc0 100644 --- a/src/Imprink.Application/Mappings/ProductMappingProfile.cs +++ b/src/Imprink.Application/Mappings/ProductMappingProfile.cs @@ -16,17 +16,15 @@ public class ProductMappingProfile: Profile .ForMember(dest => dest.ModifiedAt, opt => opt.Ignore()) .ForMember(dest => dest.CreatedBy, opt => opt.Ignore()) .ForMember(dest => dest.ModifiedBy, opt => opt.Ignore()) - .ForMember(dest => dest.Product, opt => opt.Ignore()) - .ForMember(dest => dest.OrderItems, opt => opt.Ignore()); + .ForMember(dest => dest.Product, opt => opt.Ignore()); CreateMap(); CreateMap() .ForMember(dest => dest.CreatedBy, opt => opt.Ignore()) - .ForMember(dest => dest.ModifiedBy, opt => opt.Ignore()) - .ForMember(dest => dest.OrderItems, opt => opt.Ignore()); + .ForMember(dest => dest.ModifiedBy, opt => opt.Ignore()); CreateMap(); - + CreateMap() .ForMember(dest => dest.Id, opt => opt.Ignore()) .ForMember(dest => dest.CreatedAt, opt => opt.Ignore()) @@ -34,8 +32,7 @@ public class ProductMappingProfile: Profile .ForMember(dest => dest.CreatedBy, opt => opt.Ignore()) .ForMember(dest => dest.ModifiedBy, opt => opt.Ignore()) .ForMember(dest => dest.Category, opt => opt.Ignore()) - .ForMember(dest => dest.ProductVariants, opt => opt.Ignore()) - .ForMember(dest => dest.OrderItems, opt => opt.Ignore()); + .ForMember(dest => dest.ProductVariants, opt => opt.Ignore()); CreateMap(); } diff --git a/src/Imprink.Domain/Entities/Address.cs b/src/Imprink.Domain/Entities/Address.cs index 857f331..42e592b 100644 --- a/src/Imprink.Domain/Entities/Address.cs +++ b/src/Imprink.Domain/Entities/Address.cs @@ -4,11 +4,22 @@ public class Address : EntityBase { public required string UserId { get; set; } public required string AddressType { get; set; } - public required string Street { get; set; } + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? Company { get; set; } + public required string AddressLine1 { get; set; } + public string? AddressLine2 { get; set; } + public string? ApartmentNumber { get; set; } + public string? BuildingNumber { get; set; } + public string? Floor { get; set; } public required string City { get; set; } public required string State { get; set; } public required string PostalCode { get; set; } public required string Country { get; set; } + public string? PhoneNumber { get; set; } + public string? Instructions { get; set; } public required bool IsDefault { get; set; } public required bool IsActive { get; set; } + + public virtual User User { get; set; } = null!; } \ No newline at end of file diff --git a/src/Imprink.Domain/Entities/Order.cs b/src/Imprink.Domain/Entities/Order.cs index 66d5e68..144d8b3 100644 --- a/src/Imprink.Domain/Entities/Order.cs +++ b/src/Imprink.Domain/Entities/Order.cs @@ -4,15 +4,24 @@ public class Order : EntityBase { public string UserId { get; set; } = null!; public DateTime OrderDate { get; set; } - public decimal TotalPrice { get; set; } + public decimal Amount { get; set; } + public int Quantity { get; set; } + public Guid ProductId { get; set; } + public Guid? ProductVariantId { get; set; } public int OrderStatusId { get; set; } public int ShippingStatusId { get; set; } public string OrderNumber { get; set; } = null!; - public string Notes { get; set; } = null!; + public string? Notes { get; set; } + public string? MerchantId { get; set; } + public string? ComposingImageUrl { get; set; } + public string[] OriginalImageUrls { get; set; } = []; + public string CustomizationImageUrl { get; set; } = null!; + public string CustomizationDescription { get; set; } = null!; public OrderStatus OrderStatus { get; set; } = null!; public User User { get; set; } = null!; public ShippingStatus ShippingStatus { get; set; } = null!; public OrderAddress OrderAddress { get; set; } = null!; - public virtual ICollection OrderItems { get; set; } = new List(); + public Product Product { get; set; } = null!; + public ProductVariant? ProductVariant { get; set; } } \ No newline at end of file diff --git a/src/Imprink.Domain/Entities/OrderAddress.cs b/src/Imprink.Domain/Entities/OrderAddress.cs index 1505f65..944fe37 100644 --- a/src/Imprink.Domain/Entities/OrderAddress.cs +++ b/src/Imprink.Domain/Entities/OrderAddress.cs @@ -3,11 +3,21 @@ namespace Imprink.Domain.Entities; public class OrderAddress : EntityBase { public Guid OrderId { get; set; } - public required string Street { get; set; } + public required string AddressType { get; set; } + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? Company { get; set; } + public required string AddressLine1 { get; set; } + public string? AddressLine2 { get; set; } + public string? ApartmentNumber { get; set; } + public string? BuildingNumber { get; set; } + public string? Floor { get; set; } public required string City { get; set; } public required string State { get; set; } public required string PostalCode { get; set; } public required string Country { get; set; } + public string? PhoneNumber { get; set; } + public string? Instructions { get; set; } public virtual required Order Order { get; set; } } \ No newline at end of file diff --git a/src/Imprink.Domain/Entities/OrderItem.cs b/src/Imprink.Domain/Entities/OrderItem.cs deleted file mode 100644 index 6d8b769..0000000 --- a/src/Imprink.Domain/Entities/OrderItem.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Imprink.Domain.Entities; - -public class OrderItem : EntityBase -{ - public Guid OrderId { get; set; } - public Guid ProductId { get; set; } - public Guid? ProductVariantId { get; set; } - public int Quantity { get; set; } - public decimal UnitPrice { get; set; } - public decimal TotalPrice { get; set; } - public string CustomizationImageUrl { get; set; } = null!; - public string CustomizationDescription { get; set; } = null!; - - public Order Order { get; set; } = null!; - public Product Product { get; set; } = null!; - public ProductVariant ProductVariant { get; set; } = null!; -} \ No newline at end of file diff --git a/src/Imprink.Domain/Entities/Product.cs b/src/Imprink.Domain/Entities/Product.cs index d640552..1476817 100644 --- a/src/Imprink.Domain/Entities/Product.cs +++ b/src/Imprink.Domain/Entities/Product.cs @@ -12,5 +12,5 @@ public class Product : EntityBase public virtual required Category Category { get; set; } public virtual ICollection ProductVariants { get; set; } = new List(); - public virtual ICollection OrderItems { get; set; } = new List(); + public virtual ICollection Orders { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Imprink.Domain/Entities/ProductVariant.cs b/src/Imprink.Domain/Entities/ProductVariant.cs index e9fee5b..4829f98 100644 --- a/src/Imprink.Domain/Entities/ProductVariant.cs +++ b/src/Imprink.Domain/Entities/ProductVariant.cs @@ -12,5 +12,5 @@ public class ProductVariant : EntityBase public bool IsActive { get; set; } public virtual required Product Product { get; set; } - public virtual ICollection OrderItems { get; set; } = new List(); + public virtual ICollection Orders { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Imprink.Domain/Entities/User.cs b/src/Imprink.Domain/Entities/User.cs index 59edbfa..3bfef13 100644 --- a/src/Imprink.Domain/Entities/User.cs +++ b/src/Imprink.Domain/Entities/User.cs @@ -8,14 +8,15 @@ public class User public required string Email { get; set; } public bool EmailVerified { get; set; } - public string? FirstName { get; set; } = null!; - public string? LastName { get; set; } = null!; + public string? FirstName { get; set; } + public string? LastName { get; set; } public string? PhoneNumber { get; set; } public required bool IsActive { get; set; } public virtual ICollection
Addresses { get; set; } = new List
(); public virtual ICollection UserRoles { get; set; } = new List(); public virtual ICollection Orders { get; set; } = new List(); + public virtual ICollection MerchantOrders { get; set; } = new List(); public Address? DefaultAddress => Addresses.FirstOrDefault(a => a is { IsDefault: true, IsActive: true }); public IEnumerable Roles => UserRoles.Select(ur => ur.Role); diff --git a/src/Imprink.Domain/Repositories/IAddressRepository.cs b/src/Imprink.Domain/Repositories/IAddressRepository.cs new file mode 100644 index 0000000..911061d --- /dev/null +++ b/src/Imprink.Domain/Repositories/IAddressRepository.cs @@ -0,0 +1,22 @@ +using Imprink.Domain.Entities; + +namespace Imprink.Domain.Repositories; + +public interface IAddressRepository +{ + Task> GetByUserIdAsync(string userId, CancellationToken cancellationToken = default); + Task> GetActiveByUserIdAsync(string userId, CancellationToken cancellationToken = default); + Task GetDefaultByUserIdAsync(string userId, CancellationToken cancellationToken = default); + Task> GetByUserIdAndTypeAsync(string userId, string addressType, CancellationToken cancellationToken = default); + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdAndUserIdAsync(Guid id, string userId, CancellationToken cancellationToken = default); + Task
AddAsync(Address address, CancellationToken cancellationToken = default); + Task
UpdateAsync(Address address, CancellationToken cancellationToken = default); + Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); + Task DeleteByUserIdAsync(Guid id, string userId, CancellationToken cancellationToken = default); + Task SetDefaultAddressAsync(string userId, Guid addressId, CancellationToken cancellationToken = default); + Task DeactivateAddressAsync(Guid addressId, CancellationToken cancellationToken = default); + Task ActivateAddressAsync(Guid addressId, CancellationToken cancellationToken = default); + Task ExistsAsync(Guid id, CancellationToken cancellationToken = default); + Task IsUserAddressAsync(Guid id, string userId, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Imprink.Domain/Repositories/IOrderItemRepository.cs b/src/Imprink.Domain/Repositories/IOrderItemRepository.cs deleted file mode 100644 index 7f8d123..0000000 --- a/src/Imprink.Domain/Repositories/IOrderItemRepository.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Imprink.Domain.Entities; - -namespace Imprink.Domain.Repositories; - -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/IOrderRepository.cs b/src/Imprink.Domain/Repositories/IOrderRepository.cs index 0f53fba..d5a95fb 100644 --- a/src/Imprink.Domain/Repositories/IOrderRepository.cs +++ b/src/Imprink.Domain/Repositories/IOrderRepository.cs @@ -6,22 +6,24 @@ namespace Imprink.Domain.Repositories; 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 GetByIdWithDetailsAsync(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> GetByUserIdWithDetailsAsync(string userId, CancellationToken cancellationToken = default); + Task> GetByMerchantIdAsync(string merchantId, CancellationToken cancellationToken = default); + Task> GetByMerchantIdWithDetailsAsync(string merchantId, CancellationToken cancellationToken = default); Task> GetByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default); + Task> GetByStatusAsync(int statusId, CancellationToken cancellationToken = default); + Task> GetByShippingStatusAsync(int shippingStatusId, 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 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); + Task IsOrderNumberUniqueAsync(string orderNumber, CancellationToken cancellationToken = default); + Task IsOrderNumberUniqueAsync(string orderNumber, Guid excludeOrderId, CancellationToken cancellationToken = default); + Task GenerateOrderNumberAsync(CancellationToken cancellationToken = default); + Task UpdateStatusAsync(Guid orderId, int statusId, CancellationToken cancellationToken = default); + Task UpdateShippingStatusAsync(Guid orderId, int shippingStatusId, CancellationToken cancellationToken = default); + Task AssignMerchantAsync(Guid orderId, string merchantId, CancellationToken cancellationToken = default); + Task UnassignMerchantAsync(Guid orderId, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Imprink.Infrastructure/Configuration/AddressConfiguration.cs b/src/Imprink.Infrastructure/Configuration/AddressConfiguration.cs index d1e4bbd..6a1bcce 100644 --- a/src/Imprink.Infrastructure/Configuration/AddressConfiguration.cs +++ b/src/Imprink.Infrastructure/Configuration/AddressConfiguration.cs @@ -17,10 +17,31 @@ public class AddressConfiguration : EntityBaseConfiguration
builder.Property(a => a.AddressType) .IsRequired() .HasMaxLength(50); + + builder.Property(a => a.FirstName) + .HasMaxLength(100); + + builder.Property(a => a.LastName) + .HasMaxLength(100); + + builder.Property(a => a.Company) + .HasMaxLength(200); - builder.Property(a => a.Street) + builder.Property(a => a.AddressLine1) .IsRequired() .HasMaxLength(200); + + builder.Property(a => a.AddressLine2) + .HasMaxLength(200); + + builder.Property(a => a.ApartmentNumber) + .HasMaxLength(20); + + builder.Property(a => a.BuildingNumber) + .HasMaxLength(20); + + builder.Property(a => a.Floor) + .HasMaxLength(20); builder.Property(a => a.City) .IsRequired() @@ -36,6 +57,12 @@ public class AddressConfiguration : EntityBaseConfiguration
builder.Property(a => a.Country) .IsRequired() .HasMaxLength(100); + + builder.Property(a => a.PhoneNumber) + .HasMaxLength(20); + + builder.Property(a => a.Instructions) + .HasMaxLength(500); builder.Property(a => a.IsDefault) .IsRequired() @@ -45,6 +72,12 @@ public class AddressConfiguration : EntityBaseConfiguration
.IsRequired() .HasDefaultValue(true); + builder.HasOne(a => a.User) + .WithMany(u => u.Addresses) + .HasForeignKey(a => a.UserId) + .HasPrincipalKey(u => u.Id) + .OnDelete(DeleteBehavior.Cascade); + builder.HasIndex(a => a.UserId) .HasDatabaseName("IX_Address_UserId"); @@ -54,4 +87,4 @@ public class AddressConfiguration : EntityBaseConfiguration
builder.HasIndex(a => new { a.UserId, a.IsDefault }) .HasDatabaseName("IX_Address_User_Default"); } -} \ No newline at end of file +} diff --git a/src/Imprink.Infrastructure/Configuration/OrderAddressConfiguration.cs b/src/Imprink.Infrastructure/Configuration/OrderAddressConfiguration.cs index 0fe6e64..c701bc7 100644 --- a/src/Imprink.Infrastructure/Configuration/OrderAddressConfiguration.cs +++ b/src/Imprink.Infrastructure/Configuration/OrderAddressConfiguration.cs @@ -12,10 +12,35 @@ public class OrderAddressConfiguration : EntityBaseConfiguration builder.Property(oa => oa.OrderId) .IsRequired(); + + builder.Property(oa => oa.AddressType) + .IsRequired() + .HasMaxLength(50); + + builder.Property(oa => oa.FirstName) + .HasMaxLength(100); + + builder.Property(oa => oa.LastName) + .HasMaxLength(100); + + builder.Property(oa => oa.Company) + .HasMaxLength(200); - builder.Property(oa => oa.Street) + builder.Property(oa => oa.AddressLine1) .IsRequired() .HasMaxLength(200); + + builder.Property(oa => oa.AddressLine2) + .HasMaxLength(200); + + builder.Property(oa => oa.ApartmentNumber) + .HasMaxLength(20); + + builder.Property(oa => oa.BuildingNumber) + .HasMaxLength(20); + + builder.Property(oa => oa.Floor) + .HasMaxLength(20); builder.Property(oa => oa.City) .IsRequired() @@ -32,6 +57,12 @@ public class OrderAddressConfiguration : EntityBaseConfiguration .IsRequired() .HasMaxLength(100); + builder.Property(oa => oa.PhoneNumber) + .HasMaxLength(20); + + builder.Property(oa => oa.Instructions) + .HasMaxLength(500); + builder.HasIndex(oa => oa.OrderId) .IsUnique() .HasDatabaseName("IX_OrderAddress_OrderId"); diff --git a/src/Imprink.Infrastructure/Configuration/OrderConfiguration.cs b/src/Imprink.Infrastructure/Configuration/OrderConfiguration.cs index e4a827b..0aebad5 100644 --- a/src/Imprink.Infrastructure/Configuration/OrderConfiguration.cs +++ b/src/Imprink.Infrastructure/Configuration/OrderConfiguration.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Imprink.Domain.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -5,73 +6,131 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Imprink.Infrastructure.Configuration; public class OrderConfiguration : EntityBaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) - { - base.Configure(builder); + base.Configure(builder); + + builder.Property(o => o.UserId) + .IsRequired() + .HasMaxLength(450); + + builder.Property(o => o.OrderDate) + .IsRequired(); - builder.Property(o => o.UserId) - .IsRequired() - .HasMaxLength(450); + builder.Property(o => o.Amount) + .IsRequired() + .HasColumnType("decimal(18,2)"); - builder.Property(o => o.OrderDate) - .IsRequired(); - - builder.Property(o => o.TotalPrice) - .IsRequired() - .HasColumnType("decimal(18,2)"); - - builder.Property(o => o.OrderStatusId) - .IsRequired(); - - builder.Property(o => o.ShippingStatusId) - .IsRequired(); - - builder.Property(o => o.OrderNumber) - .IsRequired() - .HasMaxLength(50); - - builder.Property(o => o.Notes) - .HasMaxLength(1000); + builder.Property(o => o.Quantity) + .IsRequired() + .HasDefaultValue(1); - builder.HasOne(o => o.OrderStatus) - .WithMany(os => os.Orders) - .HasForeignKey(o => o.OrderStatusId) - .OnDelete(DeleteBehavior.Restrict); - - builder.HasOne(o => o.ShippingStatus) - .WithMany(ss => ss.Orders) - .HasForeignKey(o => o.ShippingStatusId) - .OnDelete(DeleteBehavior.Restrict); - - builder.HasOne(o => o.OrderAddress) - .WithOne(oa => oa.Order) - .HasForeignKey(oa => oa.OrderId) - .OnDelete(DeleteBehavior.Cascade); + builder.Property(o => o.ProductId) + .IsRequired(); - builder.HasOne(o => o.User) - .WithMany(u => u.Orders) - .HasForeignKey(o => o.UserId) - .HasPrincipalKey(u => u.Id) - .OnDelete(DeleteBehavior.Restrict); + builder.Property(o => o.OrderStatusId) + .IsRequired(); + + builder.Property(o => o.ShippingStatusId) + .IsRequired(); + + builder.Property(o => o.OrderNumber) + .IsRequired() + .HasMaxLength(50); + + builder.Property(o => o.Notes) + .HasMaxLength(1000); - builder.HasIndex(o => o.UserId) - .HasDatabaseName("IX_Order_UserId"); - - builder.HasIndex(o => o.OrderNumber) - .IsUnique() - .HasDatabaseName("IX_Order_OrderNumber"); - - builder.HasIndex(o => o.OrderDate) - .HasDatabaseName("IX_Order_OrderDate"); - - builder.HasIndex(o => o.OrderStatusId) - .HasDatabaseName("IX_Order_OrderStatusId"); - - builder.HasIndex(o => o.ShippingStatusId) - .HasDatabaseName("IX_Order_ShippingStatusId"); - - builder.HasIndex(o => new { o.UserId, o.OrderDate }) - .HasDatabaseName("IX_Order_User_Date"); - } - } \ No newline at end of file + builder.Property(o => o.MerchantId) + .HasMaxLength(450); + + builder.Property(o => o.ComposingImageUrl) + .HasMaxLength(1000); + + builder.Property(o => o.OriginalImageUrls) + .HasConversion( + v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), + v => JsonSerializer.Deserialize(v, (JsonSerializerOptions?)null) ?? Array.Empty()) + .HasColumnType("nvarchar(max)"); + + builder.Property(o => o.CustomizationImageUrl) + .IsRequired() + .HasMaxLength(1000); + + builder.Property(o => o.CustomizationDescription) + .IsRequired() + .HasMaxLength(2000); + + builder.HasOne(o => o.OrderStatus) + .WithMany(os => os.Orders) + .HasForeignKey(o => o.OrderStatusId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(o => o.ShippingStatus) + .WithMany(ss => ss.Orders) + .HasForeignKey(o => o.ShippingStatusId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(o => o.OrderAddress) + .WithOne(oa => oa.Order) + .HasForeignKey(oa => oa.OrderId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(o => o.User) + .WithMany(u => u.Orders) + .HasForeignKey(o => o.UserId) + .HasPrincipalKey(u => u.Id) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne() + .WithMany(u => u.MerchantOrders) + .HasForeignKey(o => o.MerchantId) + .HasPrincipalKey(u => u.Id) + .OnDelete(DeleteBehavior.SetNull); + + builder.HasOne(o => o.Product) + .WithMany(p => p.Orders) + .HasForeignKey(o => o.ProductId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(o => o.ProductVariant) + .WithMany(pv => pv.Orders) + .HasForeignKey(o => o.ProductVariantId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasIndex(o => o.UserId) + .HasDatabaseName("IX_Order_UserId"); + + builder.HasIndex(o => o.OrderNumber) + .IsUnique() + .HasDatabaseName("IX_Order_OrderNumber"); + + builder.HasIndex(o => o.OrderDate) + .HasDatabaseName("IX_Order_OrderDate"); + + builder.HasIndex(o => o.OrderStatusId) + .HasDatabaseName("IX_Order_OrderStatusId"); + + builder.HasIndex(o => o.ShippingStatusId) + .HasDatabaseName("IX_Order_ShippingStatusId"); + + builder.HasIndex(o => o.MerchantId) + .HasDatabaseName("IX_Order_MerchantId"); + + builder.HasIndex(o => o.ProductId) + .HasDatabaseName("IX_Order_ProductId"); + + builder.HasIndex(o => o.ProductVariantId) + .HasDatabaseName("IX_Order_ProductVariantId"); + + builder.HasIndex(o => new { o.UserId, o.OrderDate }) + .HasDatabaseName("IX_Order_User_Date"); + + builder.HasIndex(o => new { o.MerchantId, o.OrderDate }) + .HasDatabaseName("IX_Order_Merchant_Date"); + + builder.HasIndex(o => new { o.ProductId, o.OrderDate }) + .HasDatabaseName("IX_Order_Product_Date"); + } +} diff --git a/src/Imprink.Infrastructure/Configuration/OrderItemConfiguration.cs b/src/Imprink.Infrastructure/Configuration/OrderItemConfiguration.cs deleted file mode 100644 index e95654e..0000000 --- a/src/Imprink.Infrastructure/Configuration/OrderItemConfiguration.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Imprink.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Imprink.Infrastructure.Configuration; - -public class OrderItemConfiguration : EntityBaseConfiguration - { - public override void Configure(EntityTypeBuilder builder) - { - base.Configure(builder); - - builder.Property(oi => oi.OrderId) - .IsRequired(); - - builder.Property(oi => oi.ProductId) - .IsRequired(); - - builder.Property(oi => oi.Quantity) - .IsRequired() - .HasDefaultValue(1); - - builder.Property(oi => oi.UnitPrice) - .IsRequired() - .HasColumnType("decimal(18,2)"); - - builder.Property(oi => oi.TotalPrice) - .IsRequired() - .HasColumnType("decimal(18,2)"); - - builder.Property(oi => oi.CustomizationImageUrl) - .HasMaxLength(500); - - builder.Property(oi => oi.CustomizationDescription) - .HasMaxLength(2000); - - builder.HasOne(oi => oi.Order) - .WithMany(o => o.OrderItems) - .HasForeignKey(oi => oi.OrderId) - .OnDelete(DeleteBehavior.Cascade); - - builder.HasOne(oi => oi.Product) - .WithMany(p => p.OrderItems) - .HasForeignKey(oi => oi.ProductId) - .OnDelete(DeleteBehavior.Restrict); - - builder.HasOne(oi => oi.ProductVariant) - .WithMany(pv => pv.OrderItems) - .HasForeignKey(oi => oi.ProductVariantId) - .OnDelete(DeleteBehavior.Restrict); - - builder.HasIndex(oi => oi.OrderId) - .HasDatabaseName("IX_OrderItem_OrderId"); - - builder.HasIndex(oi => oi.ProductId) - .HasDatabaseName("IX_OrderItem_ProductId"); - - builder.HasIndex(oi => oi.ProductVariantId) - .HasDatabaseName("IX_OrderItem_ProductVariantId"); - - builder.HasIndex(oi => new { oi.OrderId, oi.ProductId }) - .HasDatabaseName("IX_OrderItem_Order_Product"); - } - } \ No newline at end of file diff --git a/src/Imprink.Infrastructure/Configuration/UserConfiguration.cs b/src/Imprink.Infrastructure/Configuration/UserConfiguration.cs index 15b71bc..3725f6c 100644 --- a/src/Imprink.Infrastructure/Configuration/UserConfiguration.cs +++ b/src/Imprink.Infrastructure/Configuration/UserConfiguration.cs @@ -23,10 +23,9 @@ public class UserConfiguration : IEntityTypeConfiguration builder.Property(u => u.Nickname) .IsRequired() .HasMaxLength(100); - + builder.Property(u => u.EmailVerified) - .IsRequired() - .HasMaxLength(100); + .IsRequired(); builder.Property(u => u.FirstName) .HasMaxLength(100); diff --git a/src/Imprink.Infrastructure/Database/ApplicationDbContext.cs b/src/Imprink.Infrastructure/Database/ApplicationDbContext.cs index 844fe62..423bef6 100644 --- a/src/Imprink.Infrastructure/Database/ApplicationDbContext.cs +++ b/src/Imprink.Infrastructure/Database/ApplicationDbContext.cs @@ -11,7 +11,6 @@ public class ApplicationDbContext(DbContextOptions options public DbSet Products { get; set; } public DbSet ProductVariants { get; set; } public DbSet Orders { get; set; } - public DbSet OrderItems { get; set; } public DbSet OrderAddresses { get; set; } public DbSet
Addresses { get; set; } public DbSet OrderStatuses { get; set; } @@ -28,7 +27,6 @@ public class ApplicationDbContext(DbContextOptions options modelBuilder.ApplyConfiguration(new ProductConfiguration()); modelBuilder.ApplyConfiguration(new ProductVariantConfiguration()); modelBuilder.ApplyConfiguration(new OrderConfiguration()); - modelBuilder.ApplyConfiguration(new OrderItemConfiguration()); modelBuilder.ApplyConfiguration(new OrderAddressConfiguration()); modelBuilder.ApplyConfiguration(new AddressConfiguration()); modelBuilder.ApplyConfiguration(new OrderStatusConfiguration()); diff --git a/src/Imprink.Infrastructure/Repositories/AddressRepository.cs b/src/Imprink.Infrastructure/Repositories/AddressRepository.cs new file mode 100644 index 0000000..e30e420 --- /dev/null +++ b/src/Imprink.Infrastructure/Repositories/AddressRepository.cs @@ -0,0 +1,168 @@ +using Imprink.Domain.Entities; +using Imprink.Domain.Repositories; +using Imprink.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace Imprink.Infrastructure.Repositories; + +public class AddressRepository(ApplicationDbContext context) : IAddressRepository +{ + public async Task> GetByUserIdAsync(string userId, CancellationToken cancellationToken = default) + { + return await context.Addresses + .Where(a => a.UserId == userId) + .OrderByDescending(a => a.IsDefault) + .ThenBy(a => a.AddressType) + .ToListAsync(cancellationToken); + } + + public async Task> GetActiveByUserIdAsync(string userId, CancellationToken cancellationToken = default) + { + return await context.Addresses + .Where(a => a.UserId == userId && a.IsActive) + .OrderByDescending(a => a.IsDefault) + .ThenBy(a => a.AddressType) + .ToListAsync(cancellationToken); + } + + public async Task GetDefaultByUserIdAsync(string userId, CancellationToken cancellationToken = default) + { + return await context.Addresses + .FirstOrDefaultAsync(a => a.UserId == userId && a.IsDefault && a.IsActive, cancellationToken); + } + + public async Task> GetByUserIdAndTypeAsync(string userId, string addressType, CancellationToken cancellationToken = default) + { + return await context.Addresses + .Where(a => a.UserId == userId && a.AddressType == addressType && a.IsActive) + .OrderByDescending(a => a.IsDefault) + .ToListAsync(cancellationToken); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Addresses + .FirstOrDefaultAsync(a => a.Id == id, cancellationToken); + } + + public async Task GetByIdAndUserIdAsync(Guid id, string userId, CancellationToken cancellationToken = default) + { + return await context.Addresses + .FirstOrDefaultAsync(a => a.Id == id && a.UserId == userId, cancellationToken); + } + + public async Task
AddAsync(Address address, CancellationToken cancellationToken = default) + { + if (address.IsDefault) + { + await UnsetDefaultAddressesAsync(address.UserId, cancellationToken); + } + + context.Addresses.Add(address); + return address; + } + + public async Task
UpdateAsync(Address address, CancellationToken cancellationToken = default) + { + var existingAddress = await context.Addresses + .FirstOrDefaultAsync(a => a.Id == address.Id, cancellationToken); + + if (existingAddress == null) + throw new InvalidOperationException($"Address with ID {address.Id} not found"); + + if (address.IsDefault && !existingAddress.IsDefault) + { + await UnsetDefaultAddressesAsync(address.UserId, cancellationToken); + } + + context.Entry(existingAddress).CurrentValues.SetValues(address); + return existingAddress; + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var address = await context.Addresses + .FirstOrDefaultAsync(a => a.Id == id, cancellationToken); + + if (address == null) + return false; + + context.Addresses.Remove(address); + return true; + } + + public async Task DeleteByUserIdAsync(Guid id, string userId, CancellationToken cancellationToken = default) + { + var address = await context.Addresses + .FirstOrDefaultAsync(a => a.Id == id && a.UserId == userId, cancellationToken); + + if (address == null) + return false; + + context.Addresses.Remove(address); + return true; + } + + public async Task SetDefaultAddressAsync(string userId, Guid addressId, CancellationToken cancellationToken = default) + { + await UnsetDefaultAddressesAsync(userId, cancellationToken); + + var address = await context.Addresses + .FirstOrDefaultAsync(a => a.Id == addressId && a.UserId == userId, cancellationToken); + + if (address != null) + { + address.IsDefault = true; + } + } + + public async Task DeactivateAddressAsync(Guid addressId, CancellationToken cancellationToken = default) + { + var address = await context.Addresses + .FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken); + + if (address != null) + { + address.IsActive = false; + if (address.IsDefault) + { + address.IsDefault = false; + } + } + } + + public async Task ActivateAddressAsync(Guid addressId, CancellationToken cancellationToken = default) + { + var address = await context.Addresses + .FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken); + + if (address != null) + { + address.IsActive = true; + } + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Addresses + .AnyAsync(a => a.Id == id, cancellationToken); + } + + public async Task IsUserAddressAsync(Guid id, string userId, CancellationToken cancellationToken = default) + { + return await context.Addresses + .AnyAsync(a => a.Id == id && a.UserId == userId, cancellationToken); + } + + private async Task UnsetDefaultAddressesAsync(string userId, CancellationToken cancellationToken = default) + { + var defaultAddresses = await context.Addresses + .Where(a => a.UserId == userId && a.IsDefault) + .ToListAsync(cancellationToken); + + foreach (var addr in defaultAddresses) + { + addr.IsDefault = false; + } + } +} \ No newline at end of file diff --git a/src/Imprink.Infrastructure/Repositories/OrderItemRepository.cs b/src/Imprink.Infrastructure/Repositories/OrderItemRepository.cs deleted file mode 100644 index 916e457..0000000 --- a/src/Imprink.Infrastructure/Repositories/OrderItemRepository.cs +++ /dev/null @@ -1,204 +0,0 @@ -using Imprink.Domain.Entities; -using Imprink.Domain.Repositories; -using Imprink.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; - -namespace Imprink.Infrastructure.Repositories; - -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/OrderRepository.cs b/src/Imprink.Infrastructure/Repositories/OrderRepository.cs index 39a7655..1a11f67 100644 --- a/src/Imprink.Infrastructure/Repositories/OrderRepository.cs +++ b/src/Imprink.Infrastructure/Repositories/OrderRepository.cs @@ -1,5 +1,4 @@ using Imprink.Domain.Entities; -using Imprink.Domain.Models; using Imprink.Domain.Repositories; using Imprink.Infrastructure.Database; using Microsoft.EntityFrameworkCore; @@ -14,159 +13,79 @@ public class OrderRepository(ApplicationDbContext context) : IOrderRepository .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); } - public async Task GetByIdWithItemsAsync(Guid id, CancellationToken cancellationToken = default) + public async Task GetByIdWithDetailsAsync(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) + .Include(o => o.Product) + .Include(o => o.ProductVariant) + .Include(o => o.User) .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) + public async Task> GetByUserIdWithDetailsAsync(string userId, CancellationToken cancellationToken = default) { return await context.Orders .Include(o => o.OrderStatus) .Include(o => o.ShippingStatus) + .Include(o => o.OrderAddress) + .Include(o => o.Product) + .Include(o => o.ProductVariant) .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) + public async Task> GetByMerchantIdAsync(string merchantId, CancellationToken cancellationToken = default) { return await context.Orders + .Where(o => o.MerchantId == merchantId) + .OrderByDescending(o => o.OrderDate) + .ToListAsync(cancellationToken); + } + + public async Task> GetByMerchantIdWithDetailsAsync(string merchantId, CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .Include(o => o.OrderAddress) + .Include(o => o.Product) + .Include(o => o.ProductVariant) .Include(o => o.User) - .Include(o => o.OrderStatus) - .Include(o => o.ShippingStatus) - .Where(o => o.OrderStatusId == orderStatusId) + .Where(o => o.MerchantId == merchantId) + .OrderByDescending(o => o.OrderDate) + .ToListAsync(cancellationToken); + } + + public async Task> GetByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default) + { + return await context.Orders + .Where(o => o.OrderDate >= startDate && o.OrderDate <= endDate) + .OrderByDescending(o => o.OrderDate) + .ToListAsync(cancellationToken); + } + + public async Task> GetByStatusAsync(int statusId, CancellationToken cancellationToken = default) + { + return await context.Orders + .Where(o => o.OrderStatusId == statusId) .OrderByDescending(o => o.OrderDate) .ToListAsync(cancellationToken); } @@ -174,52 +93,39 @@ public class OrderRepository(ApplicationDbContext context) : IOrderRepository 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) + public async Task UpdateAsync(Order order, CancellationToken cancellationToken = default) { - order.ModifiedAt = DateTime.UtcNow; - context.Orders.Update(order); - return Task.FromResult(order); + var existingOrder = await context.Orders + .FirstOrDefaultAsync(o => o.Id == order.Id, cancellationToken); + + if (existingOrder == null) + throw new InvalidOperationException($"Order with ID {order.Id} not found"); + + context.Entry(existingOrder).CurrentValues.SetValues(order); + return existingOrder; } - public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) { - var order = await GetByIdAsync(id, cancellationToken); - if (order != null) - { - context.Orders.Remove(order); - } + var order = await context.Orders + .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); + + if (order == null) + return false; + + context.Orders.Remove(order); + return true; } public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) @@ -228,38 +134,78 @@ public class OrderRepository(ApplicationDbContext context) : IOrderRepository .AnyAsync(o => o.Id == id, cancellationToken); } - public async Task OrderNumberExistsAsync(string orderNumber, Guid? excludeId = null, CancellationToken cancellationToken = default) + public async Task IsOrderNumberUniqueAsync(string orderNumber, CancellationToken cancellationToken = default) { - var query = context.Orders.Where(o => o.OrderNumber == orderNumber); - - if (excludeId.HasValue) + return !await context.Orders + .AnyAsync(o => o.OrderNumber == orderNumber, cancellationToken); + } + + public async Task IsOrderNumberUniqueAsync(string orderNumber, Guid excludeOrderId, CancellationToken cancellationToken = default) + { + return !await context.Orders + .AnyAsync(o => o.OrderNumber == orderNumber && o.Id != excludeOrderId, cancellationToken); + } + + public async Task GenerateOrderNumberAsync(CancellationToken cancellationToken = default) + { + string orderNumber; + bool isUnique; + + do { - query = query.Where(o => o.Id != excludeId.Value); + // Generate order number format: ORD-YYYYMMDD-XXXXXX (where X is random) + var datePart = DateTime.UtcNow.ToString("yyyyMMdd"); + var randomPart = Random.Shared.Next(100000, 999999).ToString(); + orderNumber = $"ORD-{datePart}-{randomPart}"; + + isUnique = await IsOrderNumberUniqueAsync(orderNumber, cancellationToken); } + while (!isUnique); - return await query.AnyAsync(cancellationToken); + return orderNumber; } - public async Task GetTotalRevenueAsync(CancellationToken cancellationToken = default) + public async Task UpdateStatusAsync(Guid orderId, int statusId, CancellationToken cancellationToken = default) { - return await context.Orders - .Where(o => o.OrderStatusId != 5) // Assuming 5 is cancelled status - .SumAsync(o => o.TotalPrice, cancellationToken); + var order = await context.Orders + .FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken); + + if (order != null) + { + order.OrderStatusId = statusId; + } } - public async Task GetTotalRevenueByDateRangeAsync( - DateTime startDate, - DateTime endDate, - CancellationToken cancellationToken = default) + public async Task UpdateShippingStatusAsync(Guid orderId, int shippingStatusId, CancellationToken cancellationToken = default) { - return await context.Orders - .Where(o => o.OrderDate >= startDate && o.OrderDate <= endDate && o.OrderStatusId != 5) - .SumAsync(o => o.TotalPrice, cancellationToken); + var order = await context.Orders + .FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken); + + if (order != null) + { + order.ShippingStatusId = shippingStatusId; + } } - public async Task GetOrderCountByStatusAsync(int orderStatusId, CancellationToken cancellationToken = default) + public async Task AssignMerchantAsync(Guid orderId, string merchantId, CancellationToken cancellationToken = default) { - return await context.Orders - .CountAsync(o => o.OrderStatusId == orderStatusId, cancellationToken); + var order = await context.Orders + .FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken); + + if (order != null) + { + order.MerchantId = merchantId; + } + } + + public async Task UnassignMerchantAsync(Guid orderId, CancellationToken cancellationToken = default) + { + var order = await context.Orders + .FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken); + + if (order != null) + { + order.MerchantId = null; + } } } \ No newline at end of file diff --git a/src/Imprink.Infrastructure/UnitOfWork.cs b/src/Imprink.Infrastructure/UnitOfWork.cs index 22011f2..29ae434 100644 --- a/src/Imprink.Infrastructure/UnitOfWork.cs +++ b/src/Imprink.Infrastructure/UnitOfWork.cs @@ -13,7 +13,7 @@ public class UnitOfWork( IUserRoleRepository userRoleRepository, IRoleRepository roleRepository, IOrderRepository orderRepository, - IOrderItemRepository orderItemRepository) : IUnitOfWork + IAddressRepository addressRepository) : IUnitOfWork { public IProductRepository ProductRepository => productRepository; public IProductVariantRepository ProductVariantRepository => productVariantRepository; @@ -22,7 +22,7 @@ public class UnitOfWork( public IUserRoleRepository UserRoleRepository => userRoleRepository; public IRoleRepository RoleRepository => roleRepository; public IOrderRepository OrderRepository => orderRepository; - public IOrderItemRepository OrderItemRepository => orderItemRepository; + public IAddressRepository AddressRepository => addressRepository; public async Task SaveAsync(CancellationToken cancellationToken = default) { diff --git a/src/Imprink.WebApi/Extensions/StartupApplicationExtensions.cs b/src/Imprink.WebApi/Extensions/StartupApplicationExtensions.cs index 86ee554..48e12fb 100644 --- a/src/Imprink.WebApi/Extensions/StartupApplicationExtensions.cs +++ b/src/Imprink.WebApi/Extensions/StartupApplicationExtensions.cs @@ -20,7 +20,7 @@ public static class StartupApplicationExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/tests/Imprink.Application.Tests/CreateCategoryHandlerIntegrationTest.cs b/tests/Imprink.Application.Tests/CreateCategoryHandlerIntegrationTest.cs index a1e30c0..67430c4 100644 --- a/tests/Imprink.Application.Tests/CreateCategoryHandlerIntegrationTest.cs +++ b/tests/Imprink.Application.Tests/CreateCategoryHandlerIntegrationTest.cs @@ -35,7 +35,7 @@ public class CreateCategoryHandlerIntegrationTest : IDisposable services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped();