Fixed erratic order behavior

This commit is contained in:
lumijiez
2025-06-26 00:16:26 +03:00
parent c21c01c432
commit 266aa529fa
27 changed files with 1381 additions and 1179 deletions

View File

@@ -1,13 +1,14 @@
using AutoMapper;
using Imprink.Application.Dtos;
using Imprink.Application.Services;
using Imprink.Domain.Entities;
using MediatR;
using Microsoft.Extensions.Logging;
namespace Imprink.Application.Commands.Addresses;
public class CreateAddressCommand : IRequest<AddressDto>
{
public string UserId { get; set; } = null!;
public string AddressType { get; set; } = null!;
public string? FirstName { get; set; }
public string? LastName { get; set; }
@@ -27,7 +28,12 @@ public class CreateAddressCommand : IRequest<AddressDto>
public bool IsActive { get; set; } = true;
}
public class CreateAddressHandler(IUnitOfWork uw, IMapper mapper) : IRequestHandler<CreateAddressCommand, AddressDto>
public class CreateAddressHandler(
IUnitOfWork uw,
IMapper mapper,
ICurrentUserService userService,
ILogger<CreateAddressHandler> logger)
: IRequestHandler<CreateAddressCommand, AddressDto>
{
public async Task<AddressDto> Handle(CreateAddressCommand request, CancellationToken cancellationToken)
{
@@ -35,6 +41,10 @@ public class CreateAddressHandler(IUnitOfWork uw, IMapper mapper) : IRequestHand
{
var address = mapper.Map<Address>(request);
address.UserId = userService.GetCurrentUserId()!;
address.CreatedAt = DateTime.UtcNow;
address.ModifiedAt = DateTime.UtcNow;
if (address.IsDefault)
{
var currentDefault = await uw.AddressRepository.GetDefaultByUserIdAsync(address.UserId, cancellationToken);
@@ -46,6 +56,8 @@ public class CreateAddressHandler(IUnitOfWork uw, IMapper mapper) : IRequestHand
}
var createdAddress = await uw.AddressRepository.AddAsync(address, cancellationToken);
await uw.SaveAsync(cancellationToken);
return mapper.Map<AddressDto>(createdAddress);
}, cancellationToken);
}

View File

@@ -1,5 +1,7 @@
using AutoMapper;
using Imprink.Application.Dtos;
using Imprink.Application.Exceptions;
using Imprink.Application.Services;
using Imprink.Domain.Entities;
using MediatR;
@@ -7,7 +9,6 @@ namespace Imprink.Application.Commands.Orders;
public class CreateOrderCommand : IRequest<OrderDto>
{
public string UserId { get; set; } = null!;
public decimal Amount { get; set; }
public int Quantity { get; set; }
public Guid ProductId { get; set; }
@@ -15,29 +16,30 @@ public class CreateOrderCommand : IRequest<OrderDto>
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 string[]? OriginalImageUrls { get; set; } = [];
public string? CustomizationImageUrl { get; set; } = null!;
public string? CustomizationDescription { get; set; } = null!;
public Guid AddressId { get; set; }
}
public class CreateOrderHandler(IUnitOfWork uw, IMapper mapper) : IRequestHandler<CreateOrderCommand, OrderDto>
public class CreateOrderHandler(IUnitOfWork uw, IMapper mapper, ICurrentUserService userService) : IRequestHandler<CreateOrderCommand, OrderDto>
{
public async Task<OrderDto> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
return await uw.TransactAsync(async () =>
{
var sourceAddress = await uw.AddressRepository.GetByIdAndUserIdAsync(request.AddressId, request.UserId, cancellationToken);
var sourceAddress = await uw.AddressRepository.GetByIdAndUserIdAsync(request.AddressId, userService.GetCurrentUserId()!, cancellationToken);
if (sourceAddress == null)
{
throw new ArgumentException($"Address with ID {request.AddressId} not found for user {request.UserId}");
throw new NotFoundException($"Address with ID {request.AddressId} not found for user {userService.GetCurrentUserId()!}");
}
var order = mapper.Map<Order>(request);
order.UserId = userService.GetCurrentUserId()!;
order.OrderDate = DateTime.UtcNow;
order.OrderStatusId = 1;
order.ShippingStatusId = 1;
order.OrderStatusId = 0;
order.ShippingStatusId = 0;
var createdOrder = await uw.OrderRepository.AddAsync(order, cancellationToken);
@@ -70,6 +72,8 @@ public class CreateOrderHandler(IUnitOfWork uw, IMapper mapper) : IRequestHandle
{
createdOrder.ProductVariant = await uw.ProductVariantRepository.GetByIdAsync(createdOrder.ProductVariantId.Value, cancellationToken);
}
await uw.SaveAsync(cancellationToken);
return mapper.Map<OrderDto>(createdOrder);
}, cancellationToken);

View File

@@ -21,6 +21,4 @@ public class AddressDto
public string? Instructions { get; set; }
public bool IsDefault { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}

View File

@@ -11,13 +11,11 @@ public class OrderDto
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; }
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 string? CustomizationImageUrl { get; set; }
public string[]? OriginalImageUrls { get; set; } = [];
public string? CustomizationDescription { get; set; } = null!;
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }

View File

@@ -0,0 +1,23 @@
using AutoMapper;
using Imprink.Application.Commands.Addresses;
using Imprink.Application.Dtos;
using Imprink.Domain.Entities;
namespace Imprink.Application.Mappings;
public class AddressMappingProfile : Profile
{
public AddressMappingProfile()
{
CreateMap<CreateAddressCommand, Address>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
.ForMember(dest => dest.ModifiedAt, opt => opt.Ignore())
.ForMember(dest => dest.User, opt => opt.Ignore());
CreateMap<Address, AddressDto>();
CreateMap<AddressDto, Address>()
.ForMember(dest => dest.User, opt => opt.Ignore());
}
}

View File

@@ -0,0 +1,38 @@
using AutoMapper;
using Imprink.Application.Dtos;
using Imprink.Domain.Entities;
namespace Imprink.Application.Mappings;
public class OrderAddressMappingProfile : Profile
{
public OrderAddressMappingProfile()
{
CreateMap<OrderAddress, OrderAddressDto>();
CreateMap<OrderAddressDto, OrderAddress>()
.ForMember(dest => dest.Order, opt => opt.Ignore());
CreateMap<Address, OrderAddress>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.OrderId, opt => opt.Ignore())
.ForMember(dest => dest.Order, opt => opt.Ignore())
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
.ForMember(dest => dest.ModifiedAt, opt => opt.Ignore())
.ForMember(dest => dest.AddressType, opt => opt.MapFrom(src => src.AddressType))
.ForMember(dest => dest.FirstName, opt => opt.MapFrom(src => src.FirstName))
.ForMember(dest => dest.LastName, opt => opt.MapFrom(src => src.LastName))
.ForMember(dest => dest.Company, opt => opt.MapFrom(src => src.Company))
.ForMember(dest => dest.AddressLine1, opt => opt.MapFrom(src => src.AddressLine1))
.ForMember(dest => dest.AddressLine2, opt => opt.MapFrom(src => src.AddressLine2))
.ForMember(dest => dest.ApartmentNumber, opt => opt.MapFrom(src => src.ApartmentNumber))
.ForMember(dest => dest.BuildingNumber, opt => opt.MapFrom(src => src.BuildingNumber))
.ForMember(dest => dest.Floor, opt => opt.MapFrom(src => src.Floor))
.ForMember(dest => dest.City, opt => opt.MapFrom(src => src.City))
.ForMember(dest => dest.State, opt => opt.MapFrom(src => src.State))
.ForMember(dest => dest.PostalCode, opt => opt.MapFrom(src => src.PostalCode))
.ForMember(dest => dest.Country, opt => opt.MapFrom(src => src.Country))
.ForMember(dest => dest.PhoneNumber, opt => opt.MapFrom(src => src.PhoneNumber))
.ForMember(dest => dest.Instructions, opt => opt.MapFrom(src => src.Instructions));
}
}

View File

@@ -0,0 +1,36 @@
using AutoMapper;
using Imprink.Application.Commands.Orders;
using Imprink.Application.Dtos;
using Imprink.Domain.Entities;
namespace Imprink.Application.Mappings;
public class OrderMappingProfile : Profile
{
public OrderMappingProfile()
{
CreateMap<CreateOrderCommand, Order>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.OrderDate, opt => opt.Ignore())
.ForMember(dest => dest.OrderStatusId, opt => opt.Ignore())
.ForMember(dest => dest.ShippingStatusId, opt => opt.Ignore())
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
.ForMember(dest => dest.ModifiedAt, opt => opt.Ignore())
.ForMember(dest => dest.OrderStatus, opt => opt.Ignore())
.ForMember(dest => dest.User, opt => opt.Ignore())
.ForMember(dest => dest.ShippingStatus, opt => opt.Ignore())
.ForMember(dest => dest.OrderAddress, opt => opt.Ignore())
.ForMember(dest => dest.Product, opt => opt.Ignore())
.ForMember(dest => dest.ProductVariant, opt => opt.Ignore());
CreateMap<Order, OrderDto>();
CreateMap<OrderDto, Order>()
.ForMember(dest => dest.OrderStatus, opt => opt.Ignore())
.ForMember(dest => dest.User, opt => opt.Ignore())
.ForMember(dest => dest.ShippingStatus, opt => opt.Ignore())
.ForMember(dest => dest.OrderAddress, opt => opt.Ignore())
.ForMember(dest => dest.Product, opt => opt.Ignore())
.ForMember(dest => dest.ProductVariant, opt => opt.Ignore());
}
}

View File

@@ -0,0 +1,15 @@
using AutoMapper;
using Imprink.Application.Dtos;
using Imprink.Domain.Entities;
namespace Imprink.Application.Mappings;
public class OrderStatusMappingProfile : Profile
{
public OrderStatusMappingProfile()
{
CreateMap<OrderStatus, OrderStatusDto>();
CreateMap<OrderStatusDto, OrderStatus>();
}
}

View File

@@ -0,0 +1,15 @@
using AutoMapper;
using Imprink.Application.Dtos;
using Imprink.Domain.Entities;
namespace Imprink.Application.Mappings;
public class ShippingStatusMappingProfile : Profile
{
public ShippingStatusMappingProfile()
{
CreateMap<ShippingStatus, ShippingStatusDto>();
CreateMap<ShippingStatusDto, ShippingStatus>();
}
}

View File

@@ -2,7 +2,7 @@ namespace Imprink.Domain.Entities;
public class Order : EntityBase
{
public string UserId { get; set; } = null!;
public required string UserId { get; set; }
public DateTime OrderDate { get; set; }
public decimal Amount { get; set; }
public int Quantity { get; set; }
@@ -10,18 +10,17 @@ public class Order : EntityBase
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; }
public string? MerchantId { get; set; }
public string? ComposingImageUrl { get; set; }
public string? CustomizationImageUrl { get; set; }
public string[] OriginalImageUrls { get; set; } = [];
public string CustomizationImageUrl { get; set; } = null!;
public string CustomizationDescription { get; set; } = null!;
public string? CustomizationDescription { get; set; }
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 Product Product { get; set; } = null!;
public ProductVariant? ProductVariant { get; set; }
public virtual OrderStatus OrderStatus { get; set; } = null!;
public virtual User User { get; set; } = null!;
public virtual User? Merchant { get; set; }
public virtual ShippingStatus ShippingStatus { get; set; } = null!;
public virtual OrderAddress OrderAddress { get; set; } = null!;
public virtual Product Product { get; set; } = null!;
public virtual ProductVariant? ProductVariant { get; set; }
}

View File

@@ -18,6 +18,6 @@ public class OrderAddress : EntityBase
public required string Country { get; set; }
public string? PhoneNumber { get; set; }
public string? Instructions { get; set; }
public virtual required Order Order { get; set; }
public virtual Order Order { get; set; } = null!;
}

View File

@@ -6,7 +6,7 @@ public interface IAddressRepository
{
Task<IEnumerable<Address>> GetByUserIdAsync(string userId, CancellationToken cancellationToken = default);
Task<IEnumerable<Address>> GetActiveByUserIdAsync(string userId, CancellationToken cancellationToken = default);
Task<Address?> GetDefaultByUserIdAsync(string userId, CancellationToken cancellationToken = default);
Task<Address?> GetDefaultByUserIdAsync(string? userId, CancellationToken cancellationToken = default);
Task<IEnumerable<Address>> GetByUserIdAndTypeAsync(string userId, string addressType, CancellationToken cancellationToken = default);
Task<Address?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<Address?> GetByIdAndUserIdAsync(Guid id, string userId, CancellationToken cancellationToken = default);
@@ -14,7 +14,7 @@ public interface IAddressRepository
Task<Address> UpdateAsync(Address address, CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default);
Task<bool> DeleteByUserIdAsync(Guid id, string userId, CancellationToken cancellationToken = default);
Task SetDefaultAddressAsync(string userId, Guid addressId, 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<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default);

View File

@@ -7,7 +7,6 @@ public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<Order?> GetByIdWithDetailsAsync(Guid id, CancellationToken cancellationToken = default);
Task<Order?> GetByOrderNumberAsync(string orderNumber, CancellationToken cancellationToken = default);
Task<IEnumerable<Order>> GetByUserIdAsync(string userId, CancellationToken cancellationToken = default);
Task<IEnumerable<Order>> GetByUserIdWithDetailsAsync(string userId, CancellationToken cancellationToken = default);
Task<IEnumerable<Order>> GetByMerchantIdAsync(string merchantId, CancellationToken cancellationToken = default);
@@ -19,9 +18,6 @@ public interface IOrderRepository
Task<Order> UpdateAsync(Order order, CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default);
Task<bool> IsOrderNumberUniqueAsync(string orderNumber, CancellationToken cancellationToken = default);
Task<bool> IsOrderNumberUniqueAsync(string orderNumber, Guid excludeOrderId, CancellationToken cancellationToken = default);
Task<string> 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);

View File

@@ -75,7 +75,6 @@ public class AddressConfiguration : EntityBaseConfiguration<Address>
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)
@@ -87,4 +86,4 @@ public class AddressConfiguration : EntityBaseConfiguration<Address>
builder.HasIndex(a => new { a.UserId, a.IsDefault })
.HasDatabaseName("IX_Address_User_Default");
}
}
}

View File

@@ -2,7 +2,7 @@ using Imprink.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Imprink.Infrastructure;
namespace Imprink.Infrastructure.Configuration;
public class EntityBaseConfiguration<T> : IEntityTypeConfiguration<T> where T : EntityBase
{
@@ -12,20 +12,16 @@ public class EntityBaseConfiguration<T> : IEntityTypeConfiguration<T> where T :
builder.Property(e => e.Id)
.HasDefaultValueSql("NEWID()");
builder.Property(e => e.CreatedAt)
.IsRequired();
builder.Property(e => e.CreatedAt);
builder.Property(e => e.ModifiedAt)
.IsRequired()
.HasDefaultValueSql("GETUTCDATE()");
builder.Property(e => e.CreatedBy)
.IsRequired()
.HasMaxLength(450);
builder.Property(e => e.ModifiedBy)
.IsRequired()
.HasMaxLength(450);
builder.HasIndex(e => e.CreatedAt)

View File

@@ -35,19 +35,12 @@ public class OrderConfiguration : EntityBaseConfiguration<Order>
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.MerchantId)
.HasMaxLength(450);
builder.Property(o => o.ComposingImageUrl)
.HasMaxLength(1000);
builder.Property(o => o.OriginalImageUrls)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
@@ -55,11 +48,9 @@ public class OrderConfiguration : EntityBaseConfiguration<Order>
.HasColumnType("nvarchar(max)");
builder.Property(o => o.CustomizationImageUrl)
.IsRequired()
.HasMaxLength(1000);
builder.Property(o => o.CustomizationDescription)
.IsRequired()
.HasMaxLength(2000);
builder.HasOne(o => o.OrderStatus)
@@ -80,13 +71,11 @@ public class OrderConfiguration : EntityBaseConfiguration<Order>
builder.HasOne(o => o.User)
.WithMany(u => u.Orders)
.HasForeignKey(o => o.UserId)
.HasPrincipalKey(u => u.Id)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne<User>()
builder.HasOne(o => o.Merchant)
.WithMany(u => u.MerchantOrders)
.HasForeignKey(o => o.MerchantId)
.HasPrincipalKey(u => u.Id)
.OnDelete(DeleteBehavior.SetNull);
builder.HasOne(o => o.Product)
@@ -102,10 +91,6 @@ public class OrderConfiguration : EntityBaseConfiguration<Order>
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");
@@ -133,4 +118,4 @@ public class OrderConfiguration : EntityBaseConfiguration<Order>
builder.HasIndex(o => new { o.ProductId, o.OrderDate })
.HasDatabaseName("IX_Order_Product_Date");
}
}
}

View File

@@ -34,18 +34,6 @@ public class ProductConfiguration : EntityBaseConfiguration<Product>
builder.Property(p => p.CategoryId)
.IsRequired(false);
builder.Property(c => c.CreatedAt)
.IsRequired(false);
builder.Property(c => c.CreatedBy)
.IsRequired(false);
builder.Property(c => c.ModifiedAt)
.IsRequired(false);
builder.Property(c => c.ModifiedBy)
.IsRequired(false);
builder.HasOne(p => p.Category)
.WithMany(c => c.Products)

View File

@@ -37,18 +37,6 @@ public class ProductVariantConfiguration : EntityBaseConfiguration<ProductVarian
builder.Property(pv => pv.IsActive)
.IsRequired()
.HasDefaultValue(true);
builder.Property(c => c.CreatedAt)
.IsRequired(false);
builder.Property(c => c.CreatedBy)
.IsRequired(false);
builder.Property(c => c.ModifiedAt)
.IsRequired(false);
builder.Property(c => c.ModifiedBy)
.IsRequired(false);
builder.HasOne(pv => pv.Product)
.WithMany(p => p.ProductVariants)

View File

@@ -8,6 +8,8 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.HasKey(u => u.Id);
builder.Property(u => u.Id)
.HasMaxLength(450)
.ValueGeneratedNever();
@@ -47,12 +49,6 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
builder.HasIndex(u => u.IsActive)
.HasDatabaseName("IX_User_IsActive");
builder.HasMany(u => u.Addresses)
.WithOne()
.HasForeignKey(a => a.UserId)
.HasPrincipalKey(u => u.Id)
.OnDelete(DeleteBehavior.Cascade);
builder.Ignore(u => u.DefaultAddress);
builder.Ignore(u => u.Roles);
}

View File

@@ -84,7 +84,7 @@ namespace Imprink.Infrastructure.Migrations
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Nickname = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
EmailVerified = table.Column<bool>(type: "bit", maxLength: 100, nullable: false),
EmailVerified = table.Column<bool>(type: "bit", nullable: false),
FirstName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
LastName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
PhoneNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
@@ -130,17 +130,26 @@ namespace Imprink.Infrastructure.Migrations
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWID()"),
UserId = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false),
AddressType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Street = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
FirstName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
LastName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Company = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
AddressLine1 = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
AddressLine2 = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
ApartmentNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
BuildingNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
Floor = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
City = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
State = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
PostalCode = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
Country = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
PhoneNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
Instructions = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
IsDefault = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
ModifiedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"),
CreatedBy = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false),
ModifiedBy = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false)
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
ModifiedAt = table.Column<DateTime>(type: "datetime2", nullable: true, defaultValueSql: "GETUTCDATE()"),
CreatedBy = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: true),
ModifiedBy = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: true)
},
constraints: table =>
{
@@ -153,46 +162,6 @@ namespace Imprink.Infrastructure.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Orders",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWID()"),
UserId = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false),
OrderDate = table.Column<DateTime>(type: "datetime2", nullable: false),
TotalPrice = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
OrderStatusId = table.Column<int>(type: "int", nullable: false),
ShippingStatusId = table.Column<int>(type: "int", nullable: false),
OrderNumber = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Notes = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
ModifiedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"),
CreatedBy = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false),
ModifiedBy = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Orders", x => x.Id);
table.ForeignKey(
name: "FK_Orders_OrderStatuses_OrderStatusId",
column: x => x.OrderStatusId,
principalTable: "OrderStatuses",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Orders_ShippingStatuses_ShippingStatusId",
column: x => x.ShippingStatusId,
principalTable: "ShippingStatuses",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Orders_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "UserRole",
columns: table => new
@@ -246,21 +215,95 @@ namespace Imprink.Infrastructure.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Orders",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWID()"),
UserId = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false),
OrderDate = table.Column<DateTime>(type: "datetime2", nullable: false),
Amount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
Quantity = table.Column<int>(type: "int", nullable: false, defaultValue: 1),
ProductId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ProductVariantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
OrderStatusId = table.Column<int>(type: "int", nullable: false),
ShippingStatusId = table.Column<int>(type: "int", nullable: false),
Notes = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
MerchantId = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: true),
CustomizationImageUrl = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
OriginalImageUrls = table.Column<string>(type: "nvarchar(max)", nullable: false),
CustomizationDescription = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
ModifiedAt = table.Column<DateTime>(type: "datetime2", nullable: true, defaultValueSql: "GETUTCDATE()"),
CreatedBy = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: true),
ModifiedBy = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Orders", x => x.Id);
table.ForeignKey(
name: "FK_Orders_OrderStatuses_OrderStatusId",
column: x => x.OrderStatusId,
principalTable: "OrderStatuses",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Orders_ProductVariants_ProductVariantId",
column: x => x.ProductVariantId,
principalTable: "ProductVariants",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Orders_Products_ProductId",
column: x => x.ProductId,
principalTable: "Products",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Orders_ShippingStatuses_ShippingStatusId",
column: x => x.ShippingStatusId,
principalTable: "ShippingStatuses",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Orders_Users_MerchantId",
column: x => x.MerchantId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_Orders_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "OrderAddresses",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWID()"),
OrderId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Street = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
AddressType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
FirstName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
LastName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Company = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
AddressLine1 = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
AddressLine2 = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
ApartmentNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
BuildingNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
Floor = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
City = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
State = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
PostalCode = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
Country = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
ModifiedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"),
CreatedBy = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false),
ModifiedBy = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false)
PhoneNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
Instructions = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
ModifiedAt = table.Column<DateTime>(type: "datetime2", nullable: true, defaultValueSql: "GETUTCDATE()"),
CreatedBy = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: true),
ModifiedBy = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: true)
},
constraints: table =>
{
@@ -273,47 +316,6 @@ namespace Imprink.Infrastructure.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "OrderItems",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWID()"),
OrderId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ProductId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ProductVariantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Quantity = table.Column<int>(type: "int", nullable: false, defaultValue: 1),
UnitPrice = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
TotalPrice = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
CustomizationImageUrl = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
CustomizationDescription = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
ModifiedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"),
CreatedBy = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false),
ModifiedBy = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OrderItems", x => x.Id);
table.ForeignKey(
name: "FK_OrderItems_Orders_OrderId",
column: x => x.OrderId,
principalTable: "Orders",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_OrderItems_ProductVariants_ProductVariantId",
column: x => x.ProductVariantId,
principalTable: "ProductVariants",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_OrderItems_Products_ProductId",
column: x => x.ProductId,
principalTable: "Products",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.InsertData(
table: "Categories",
columns: new[] { "Id", "CreatedAt", "CreatedBy", "Description", "ImageUrl", "IsActive", "ModifiedAt", "ModifiedBy", "Name", "ParentCategoryId", "SortOrder" },
@@ -446,41 +448,6 @@ namespace Imprink.Infrastructure.Migrations
column: "OrderId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OrderItem_CreatedAt",
table: "OrderItems",
column: "CreatedAt");
migrationBuilder.CreateIndex(
name: "IX_OrderItem_CreatedBy",
table: "OrderItems",
column: "CreatedBy");
migrationBuilder.CreateIndex(
name: "IX_OrderItem_ModifiedAt",
table: "OrderItems",
column: "ModifiedAt");
migrationBuilder.CreateIndex(
name: "IX_OrderItem_Order_Product",
table: "OrderItems",
columns: new[] { "OrderId", "ProductId" });
migrationBuilder.CreateIndex(
name: "IX_OrderItem_OrderId",
table: "OrderItems",
column: "OrderId");
migrationBuilder.CreateIndex(
name: "IX_OrderItem_ProductId",
table: "OrderItems",
column: "ProductId");
migrationBuilder.CreateIndex(
name: "IX_OrderItem_ProductVariantId",
table: "OrderItems",
column: "ProductVariantId");
migrationBuilder.CreateIndex(
name: "IX_Order_CreatedAt",
table: "Orders",
@@ -491,6 +458,16 @@ namespace Imprink.Infrastructure.Migrations
table: "Orders",
column: "CreatedBy");
migrationBuilder.CreateIndex(
name: "IX_Order_Merchant_Date",
table: "Orders",
columns: new[] { "MerchantId", "OrderDate" });
migrationBuilder.CreateIndex(
name: "IX_Order_MerchantId",
table: "Orders",
column: "MerchantId");
migrationBuilder.CreateIndex(
name: "IX_Order_ModifiedAt",
table: "Orders",
@@ -501,17 +478,26 @@ namespace Imprink.Infrastructure.Migrations
table: "Orders",
column: "OrderDate");
migrationBuilder.CreateIndex(
name: "IX_Order_OrderNumber",
table: "Orders",
column: "OrderNumber",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Order_OrderStatusId",
table: "Orders",
column: "OrderStatusId");
migrationBuilder.CreateIndex(
name: "IX_Order_Product_Date",
table: "Orders",
columns: new[] { "ProductId", "OrderDate" });
migrationBuilder.CreateIndex(
name: "IX_Order_ProductId",
table: "Orders",
column: "ProductId");
migrationBuilder.CreateIndex(
name: "IX_Order_ProductVariantId",
table: "Orders",
column: "ProductVariantId");
migrationBuilder.CreateIndex(
name: "IX_Order_ShippingStatusId",
table: "Orders",
@@ -659,24 +645,21 @@ namespace Imprink.Infrastructure.Migrations
migrationBuilder.DropTable(
name: "OrderAddresses");
migrationBuilder.DropTable(
name: "OrderItems");
migrationBuilder.DropTable(
name: "UserRole");
migrationBuilder.DropTable(
name: "Orders");
migrationBuilder.DropTable(
name: "ProductVariants");
migrationBuilder.DropTable(
name: "Roles");
migrationBuilder.DropTable(
name: "OrderStatuses");
migrationBuilder.DropTable(
name: "ProductVariants");
migrationBuilder.DropTable(
name: "ShippingStatuses");

View File

@@ -25,7 +25,7 @@ public class AddressRepository(ApplicationDbContext context) : IAddressRepositor
.ToListAsync(cancellationToken);
}
public async Task<Address?> GetDefaultByUserIdAsync(string userId, CancellationToken cancellationToken = default)
public async Task<Address?> GetDefaultByUserIdAsync(string? userId, CancellationToken cancellationToken = default)
{
return await context.Addresses
.FirstOrDefaultAsync(a => a.UserId == userId && a.IsDefault && a.IsActive, cancellationToken);
@@ -103,7 +103,7 @@ public class AddressRepository(ApplicationDbContext context) : IAddressRepositor
return true;
}
public async Task SetDefaultAddressAsync(string userId, Guid addressId, CancellationToken cancellationToken = default)
public async Task SetDefaultAddressAsync(string? userId, Guid addressId, CancellationToken cancellationToken = default)
{
await UnsetDefaultAddressesAsync(userId, cancellationToken);
@@ -154,7 +154,7 @@ public class AddressRepository(ApplicationDbContext context) : IAddressRepositor
.AnyAsync(a => a.Id == id && a.UserId == userId, cancellationToken);
}
private async Task UnsetDefaultAddressesAsync(string userId, CancellationToken cancellationToken = default)
private async Task UnsetDefaultAddressesAsync(string? userId, CancellationToken cancellationToken = default)
{
var defaultAddresses = await context.Addresses
.Where(a => a.UserId == userId && a.IsDefault)

View File

@@ -25,12 +25,6 @@ public class OrderRepository(ApplicationDbContext context) : IOrderRepository
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
}
public async Task<Order?> GetByOrderNumberAsync(string orderNumber, CancellationToken cancellationToken = default)
{
return await context.Orders
.FirstOrDefaultAsync(o => o.OrderNumber == orderNumber, cancellationToken);
}
public async Task<IEnumerable<Order>> GetByUserIdAsync(string userId, CancellationToken cancellationToken = default)
{
return await context.Orders
@@ -134,37 +128,6 @@ public class OrderRepository(ApplicationDbContext context) : IOrderRepository
.AnyAsync(o => o.Id == id, cancellationToken);
}
public async Task<bool> IsOrderNumberUniqueAsync(string orderNumber, CancellationToken cancellationToken = default)
{
return !await context.Orders
.AnyAsync(o => o.OrderNumber == orderNumber, cancellationToken);
}
public async Task<bool> IsOrderNumberUniqueAsync(string orderNumber, Guid excludeOrderId, CancellationToken cancellationToken = default)
{
return !await context.Orders
.AnyAsync(o => o.OrderNumber == orderNumber && o.Id != excludeOrderId, cancellationToken);
}
public async Task<string> GenerateOrderNumberAsync(CancellationToken cancellationToken = default)
{
string orderNumber;
bool isUnique;
do
{
// 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 orderNumber;
}
public async Task UpdateStatusAsync(Guid orderId, int statusId, CancellationToken cancellationToken = default)
{
var order = await context.Orders

View File

@@ -52,7 +52,6 @@ public class UnitOfWork(
try
{
var result = await operation();
await SaveAsync(cancellationToken);
await CommitTransactionAsync(cancellationToken);
return result;
}
@@ -69,7 +68,6 @@ public class UnitOfWork(
try
{
await operation();
await SaveAsync(cancellationToken);
await CommitTransactionAsync(cancellationToken);
}
catch

View File

@@ -0,0 +1,60 @@
using Imprink.Application.Commands.Addresses;
using Imprink.Application.Dtos;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Imprink.WebApi.Controllers;
[ApiController]
[Route("/api/addresses")]
public class AddressesController(IMediator mediator) : ControllerBase
{
[HttpGet("{id:guid}")]
[Authorize]
public async Task<ActionResult<AddressDto>> GetAddressById(
Guid id,
[FromQuery] string? userId = null,
CancellationToken cancellationToken = default)
{
var result = await mediator.Send(new GetAddressByIdQuery
{
Id = id,
UserId = userId
}, cancellationToken);
if (result == null)
return NotFound();
return Ok(result);
}
[HttpGet("user/{userId}")]
[Authorize]
public async Task<ActionResult<IEnumerable<AddressDto>>> GetAddressesByUserId(
string userId,
[FromQuery] bool activeOnly = false,
[FromQuery] string? addressType = null,
CancellationToken cancellationToken = default)
{
var result = await mediator.Send(new GetAddressesByUserIdQuery
{
UserId = userId,
ActiveOnly = activeOnly,
AddressType = addressType
}, cancellationToken);
return Ok(result);
}
[HttpPost]
[Authorize]
public async Task<ActionResult<AddressDto>> CreateAddress(
[FromBody] CreateAddressCommand command,
CancellationToken cancellationToken = default)
{
var result = await mediator.Send(command, cancellationToken);
return CreatedAtAction(nameof(GetAddressById), new { id = result.Id }, result);
}
}

View File

@@ -0,0 +1,74 @@
using Imprink.Application.Commands.Orders;
using Imprink.Application.Dtos;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Imprink.WebApi.Controllers;
[ApiController]
[Route("/api/orders")]
public class OrdersController(IMediator mediator) : ControllerBase
{
[HttpGet("{id:guid}")]
[Authorize]
public async Task<ActionResult<OrderDto>> GetOrderById(
Guid id,
[FromQuery] bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var result = await mediator.Send(new GetOrderByIdQuery
{
Id = id,
IncludeDetails = includeDetails
}, cancellationToken);
if (result == null)
return NotFound();
return Ok(result);
}
[HttpGet("user/{userId}")]
[Authorize]
public async Task<ActionResult<IEnumerable<OrderDto>>> GetOrdersByUserId(
string userId,
[FromQuery] bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var result = await mediator.Send(new GetOrdersByUserIdQuery
{
UserId = userId,
IncludeDetails = includeDetails
}, cancellationToken);
return Ok(result);
}
[HttpGet("merchant/{merchantId}")]
[Authorize(Roles = "Admin,Merchant")]
public async Task<ActionResult<IEnumerable<OrderDto>>> GetOrdersByMerchantId(
string merchantId,
[FromQuery] bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var result = await mediator.Send(new GetOrdersByMerchantIdQuery
{
MerchantId = merchantId,
IncludeDetails = includeDetails
}, cancellationToken);
return Ok(result);
}
[HttpPost]
[Authorize]
public async Task<ActionResult<OrderDto>> CreateOrder(
[FromBody] CreateOrderCommand command,
CancellationToken cancellationToken = default)
{
var result = await mediator.Send(command, cancellationToken);
return CreatedAtAction(nameof(GetOrderById), new { id = result.Id }, result);
}
}