Merge pull request #15 from bytegrip/dev

Dev
This commit was merged in pull request #15.
This commit is contained in:
Daniel
2025-06-26 03:06:57 +03:00
committed by GitHub
82 changed files with 3618 additions and 1782 deletions

View File

@@ -0,0 +1,63 @@
using AutoMapper;
using Imprink.Application.Dtos;
using Imprink.Application.Services;
using Imprink.Domain.Entities;
using MediatR;
namespace Imprink.Application.Commands.Addresses;
public class CreateAddressCommand : IRequest<AddressDto>
{
public string AddressType { get; set; } = null!;
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Company { get; set; }
public string AddressLine1 { get; set; } = null!;
public string? AddressLine2 { get; set; }
public string? ApartmentNumber { get; set; }
public string? BuildingNumber { get; set; }
public string? Floor { get; set; }
public string City { get; set; } = null!;
public string State { get; set; } = null!;
public string PostalCode { get; set; } = null!;
public string Country { get; set; } = null!;
public string? PhoneNumber { get; set; }
public string? Instructions { get; set; }
public bool IsDefault { get; set; }
public bool IsActive { get; set; } = true;
}
public class CreateAddress(
IUnitOfWork uw,
IMapper mapper,
ICurrentUserService userService)
: IRequestHandler<CreateAddressCommand, AddressDto>
{
public async Task<AddressDto> Handle(
CreateAddressCommand request,
CancellationToken cancellationToken)
{
return await uw.TransactAsync(async () =>
{
var address = mapper.Map<Address>(request);
address.UserId = userService.GetCurrentUserId();
if (address.IsDefault)
{
var currentDefault = await uw.AddressRepository
.GetDefaultByUserIdAsync(address.UserId, cancellationToken);
if (currentDefault != null)
{
currentDefault.IsDefault = false;
await uw.AddressRepository.UpdateAsync(currentDefault, cancellationToken);
}
}
var createdAddress = await uw.AddressRepository.AddAsync(address, cancellationToken);
await uw.SaveAsync(cancellationToken);
return mapper.Map<AddressDto>(createdAddress);
}, cancellationToken);
}
}

View File

@@ -0,0 +1,43 @@
using AutoMapper;
using Imprink.Application.Dtos;
using Imprink.Domain.Entities;
using MediatR;
namespace Imprink.Application.Commands.Addresses;
public class GetAddressesByUserIdQuery : IRequest<IEnumerable<AddressDto>>
{
public string UserId { get; set; } = null!;
public bool ActiveOnly { get; set; }
public string? AddressType { get; set; }
}
public class GetAddressesByUserId(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<GetAddressesByUserIdQuery, IEnumerable<AddressDto>>
{
public async Task<IEnumerable<AddressDto>> Handle(
GetAddressesByUserIdQuery request,
CancellationToken cancellationToken)
{
IEnumerable<Address> addresses;
if (!string.IsNullOrEmpty(request.AddressType))
{
addresses = await uw.AddressRepository
.GetByUserIdAndTypeAsync(request.UserId, request.AddressType, cancellationToken);
}
else if (request.ActiveOnly)
{
addresses = await uw.AddressRepository
.GetActiveByUserIdAsync(request.UserId, cancellationToken);
}
else
{
addresses = await uw.AddressRepository.GetByUserIdAsync(request.UserId, cancellationToken);
}
return mapper.Map<IEnumerable<AddressDto>>(addresses);
}
}

View File

@@ -0,0 +1,26 @@
using AutoMapper;
using Imprink.Application.Dtos;
using Imprink.Application.Services;
using Imprink.Domain.Entities;
using MediatR;
namespace Imprink.Application.Commands.Addresses;
public class GetMyAddressesQuery : IRequest<IEnumerable<AddressDto?>>;
public class GetMyAddresses(
IUnitOfWork uw,
IMapper mapper,
ICurrentUserService userService)
: IRequestHandler<GetMyAddressesQuery, IEnumerable<AddressDto?>>
{
public async Task<IEnumerable<AddressDto?>> Handle(
GetMyAddressesQuery request,
CancellationToken cancellationToken)
{
IEnumerable<Address?> addresses = await uw.AddressRepository
.GetByUserIdAsync(userService.GetCurrentUserId(), cancellationToken);
return mapper.Map<IEnumerable<AddressDto>>(addresses);
}
}

View File

@@ -14,9 +14,13 @@ public class CreateCategoryCommand : IRequest<CategoryDto>
public Guid? ParentCategoryId { get; set; }
}
public class CreateCategoryHandler(IUnitOfWork unitOfWork) : IRequestHandler<CreateCategoryCommand, CategoryDto>
public class CreateCategory(
IUnitOfWork unitOfWork)
: IRequestHandler<CreateCategoryCommand, CategoryDto>
{
public async Task<CategoryDto> Handle(CreateCategoryCommand request, CancellationToken cancellationToken)
public async Task<CategoryDto> Handle(
CreateCategoryCommand request,
CancellationToken cancellationToken)
{
await unitOfWork.BeginTransactionAsync(cancellationToken);
@@ -32,7 +36,9 @@ public class CreateCategoryHandler(IUnitOfWork unitOfWork) : IRequestHandler<Cre
ParentCategoryId = request.ParentCategoryId
};
var createdCategory = await unitOfWork.CategoryRepository.AddAsync(category, cancellationToken);
var createdCategory = await unitOfWork
.CategoryRepository.AddAsync(category, cancellationToken);
await unitOfWork.SaveAsync(cancellationToken);
await unitOfWork.CommitTransactionAsync(cancellationToken);

View File

@@ -7,15 +7,21 @@ public class DeleteCategoryCommand : IRequest<bool>
public Guid Id { get; init; }
}
public class DeleteCategoryHandler(IUnitOfWork unitOfWork) : IRequestHandler<DeleteCategoryCommand, bool>
public class DeleteCategory(
IUnitOfWork unitOfWork)
: IRequestHandler<DeleteCategoryCommand, bool>
{
public async Task<bool> Handle(DeleteCategoryCommand request, CancellationToken cancellationToken)
public async Task<bool> Handle(
DeleteCategoryCommand request,
CancellationToken cancellationToken)
{
await unitOfWork.BeginTransactionAsync(cancellationToken);
try
{
var exists = await unitOfWork.CategoryRepository.ExistsAsync(request.Id, cancellationToken);
var exists = await unitOfWork.CategoryRepository
.ExistsAsync(request.Id, cancellationToken);
if (!exists)
{
await unitOfWork.RollbackTransactionAsync(cancellationToken);

View File

@@ -10,10 +10,13 @@ public class GetCategoriesQuery : IRequest<IEnumerable<CategoryDto>>
public bool RootCategoriesOnly { get; set; } = false;
}
public class GetCategoriesHandler(IUnitOfWork unitOfWork)
public class GetCategories(
IUnitOfWork unitOfWork)
: IRequestHandler<GetCategoriesQuery, IEnumerable<CategoryDto>>
{
public async Task<IEnumerable<CategoryDto>> Handle(GetCategoriesQuery request, CancellationToken cancellationToken)
public async Task<IEnumerable<CategoryDto>> Handle(
GetCategoriesQuery request,
CancellationToken cancellationToken)
{
IEnumerable<Category> categories;

View File

@@ -15,15 +15,20 @@ public class UpdateCategoryCommand : IRequest<CategoryDto>
public Guid? ParentCategoryId { get; set; }
}
public class UpdateCategoryHandler(IUnitOfWork unitOfWork) : IRequestHandler<UpdateCategoryCommand, CategoryDto>
public class UpdateCategory(
IUnitOfWork unitOfWork)
: IRequestHandler<UpdateCategoryCommand, CategoryDto>
{
public async Task<CategoryDto> Handle(UpdateCategoryCommand request, CancellationToken cancellationToken)
public async Task<CategoryDto> Handle(
UpdateCategoryCommand request,
CancellationToken cancellationToken)
{
await unitOfWork.BeginTransactionAsync(cancellationToken);
try
{
var existingCategory = await unitOfWork.CategoryRepository.GetByIdAsync(request.Id, cancellationToken);
var existingCategory = await unitOfWork.CategoryRepository
.GetByIdAsync(request.Id, cancellationToken);
if (existingCategory == null)
{
@@ -37,7 +42,9 @@ public class UpdateCategoryHandler(IUnitOfWork unitOfWork) : IRequestHandler<Upd
existingCategory.IsActive = request.IsActive;
existingCategory.ParentCategoryId = request.ParentCategoryId;
var updatedCategory = await unitOfWork.CategoryRepository.UpdateAsync(existingCategory, cancellationToken);
var updatedCategory = await unitOfWork.CategoryRepository
.UpdateAsync(existingCategory, cancellationToken);
await unitOfWork.CommitTransactionAsync(cancellationToken);
return new CategoryDto

View File

@@ -0,0 +1,95 @@
using AutoMapper;
using Imprink.Application.Dtos;
using Imprink.Application.Exceptions;
using Imprink.Application.Services;
using Imprink.Domain.Entities;
using MediatR;
namespace Imprink.Application.Commands.Orders;
public class CreateOrderCommand : IRequest<OrderDto>
{
public int Quantity { get; set; }
public Guid ProductId { get; set; }
public Guid ProductVariantId { get; set; }
public string[]? OriginalImageUrls { get; set; } = [];
public string? CustomizationImageUrl { get; set; } = null!;
public string? CustomizationDescription { get; set; } = null!;
public Guid AddressId { get; set; }
}
public class CreateOrder(
IUnitOfWork uw,
IMapper mapper,
ICurrentUserService userService)
: IRequestHandler<CreateOrderCommand, OrderDto>
{
public async Task<OrderDto> Handle(
CreateOrderCommand request,
CancellationToken cancellationToken)
{
return await uw.TransactAsync(async () =>
{
var userId = userService.GetCurrentUserId()!;
var sourceAddress = await uw.AddressRepository
.GetByIdAndUserIdAsync(request.AddressId, userId, cancellationToken);
if (sourceAddress == null)
throw new NotFoundException($"Address {request.AddressId} not found for {userId}");
var order = mapper.Map<Order>(request);
order.UserId = userService.GetCurrentUserId();
order.OrderDate = DateTime.UtcNow;
order.OrderStatusId = 0;
order.ShippingStatusId = 0;
var variant = uw.ProductVariantRepository
.GetByIdAsync(request.ProductVariantId, cancellationToken).Result;
if (variant == null)
throw new NotFoundException("Product variant not found");
order.Amount = variant.Price * request.Quantity;
var createdOrder = await uw.OrderRepository.AddAsync(order, cancellationToken);
var orderAddress = new OrderAddress
{
OrderId = createdOrder.Id,
AddressType = sourceAddress.AddressType,
FirstName = sourceAddress.FirstName,
LastName = sourceAddress.LastName,
Company = sourceAddress.Company,
AddressLine1 = sourceAddress.AddressLine1,
AddressLine2 = sourceAddress.AddressLine2,
ApartmentNumber = sourceAddress.ApartmentNumber,
BuildingNumber = sourceAddress.BuildingNumber,
Floor = sourceAddress.Floor,
City = sourceAddress.City,
State = sourceAddress.State,
PostalCode = sourceAddress.PostalCode,
Country = sourceAddress.Country,
PhoneNumber = sourceAddress.PhoneNumber,
Instructions = sourceAddress.Instructions,
Order = createdOrder
};
await uw.OrderAddressRepository.AddAsync(orderAddress, cancellationToken);
createdOrder.Product = (await uw.ProductRepository
.GetByIdAsync(createdOrder.ProductId, cancellationToken))!;
if (!createdOrder.ProductVariantId.HasValue)
throw new NotFoundException("Product variant not found");
createdOrder.ProductVariant = await uw.ProductVariantRepository
.GetByIdAsync(createdOrder.ProductVariantId.Value, cancellationToken);
await uw.SaveAsync(cancellationToken);
return mapper.Map<OrderDto>(createdOrder);
}, cancellationToken);
}
}

View File

@@ -0,0 +1,36 @@
using AutoMapper;
using Imprink.Application.Dtos;
using Imprink.Domain.Entities;
using MediatR;
namespace Imprink.Application.Commands.Orders;
public class GetOrderByIdQuery : IRequest<OrderDto?>
{
public Guid Id { get; set; }
public bool IncludeDetails { get; set; }
}
public class GetOrderById(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<GetOrderByIdQuery, OrderDto?>
{
public async Task<OrderDto?> Handle(
GetOrderByIdQuery request,
CancellationToken cancellationToken)
{
Order? order;
if (request.IncludeDetails)
{
order = await uw.OrderRepository.GetByIdWithDetailsAsync(request.Id, cancellationToken);
}
else
{
order = await uw.OrderRepository.GetByIdAsync(request.Id, cancellationToken);
}
return order != null ? mapper.Map<OrderDto>(order) : null;
}
}

View File

@@ -0,0 +1,38 @@
using AutoMapper;
using Imprink.Application.Dtos;
using Imprink.Domain.Entities;
using MediatR;
namespace Imprink.Application.Commands.Orders;
public class GetOrdersByMerchantIdQuery : IRequest<IEnumerable<OrderDto>>
{
public string MerchantId { get; set; } = null!;
public bool IncludeDetails { get; set; }
}
public class GetOrdersByMerchantId(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<GetOrdersByMerchantIdQuery, IEnumerable<OrderDto>>
{
public async Task<IEnumerable<OrderDto>> Handle(
GetOrdersByMerchantIdQuery request,
CancellationToken cancellationToken)
{
IEnumerable<Order> orders;
if (request.IncludeDetails)
{
orders = await uw.OrderRepository
.GetByMerchantIdWithDetailsAsync(request.MerchantId, cancellationToken);
}
else
{
orders = await uw.OrderRepository
.GetByMerchantIdAsync(request.MerchantId, cancellationToken);
}
return mapper.Map<IEnumerable<OrderDto>>(orders);
}
}

View File

@@ -0,0 +1,38 @@
using AutoMapper;
using Imprink.Application.Dtos;
using Imprink.Domain.Entities;
using MediatR;
namespace Imprink.Application.Commands.Orders;
public class GetOrdersByUserIdQuery : IRequest<IEnumerable<OrderDto>>
{
public string UserId { get; set; } = null!;
public bool IncludeDetails { get; set; }
}
public class GetOrdersByUserId(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<GetOrdersByUserIdQuery, IEnumerable<OrderDto>>
{
public async Task<IEnumerable<OrderDto>> Handle(
GetOrdersByUserIdQuery request,
CancellationToken cancellationToken)
{
IEnumerable<Order> orders;
if (request.IncludeDetails)
{
orders = await uw.OrderRepository
.GetByUserIdWithDetailsAsync(request.UserId, cancellationToken);
}
else
{
orders = await uw.OrderRepository
.GetByUserIdAsync(request.UserId, cancellationToken);
}
return mapper.Map<IEnumerable<OrderDto>>(orders);
}
}

View File

@@ -17,10 +17,14 @@ public class CreateProductVariantCommand : IRequest<ProductVariantDto>
public bool IsActive { get; set; } = true;
}
public class CreateProductVariantHandler(IUnitOfWork unitOfWork, IMapper mapper)
public class CreateProductVariant(
IUnitOfWork unitOfWork,
IMapper mapper)
: IRequestHandler<CreateProductVariantCommand, ProductVariantDto>
{
public async Task<ProductVariantDto> Handle(CreateProductVariantCommand request, CancellationToken cancellationToken)
public async Task<ProductVariantDto> Handle(
CreateProductVariantCommand request,
CancellationToken cancellationToken)
{
await unitOfWork.BeginTransactionAsync(cancellationToken);
@@ -30,7 +34,8 @@ public class CreateProductVariantHandler(IUnitOfWork unitOfWork, IMapper mapper)
productVariant.Product = null!;
var createdVariant = await unitOfWork.ProductVariantRepository.AddAsync(productVariant, cancellationToken);
var createdVariant = await unitOfWork.ProductVariantRepository
.AddAsync(productVariant, cancellationToken);
await unitOfWork.SaveAsync(cancellationToken);
await unitOfWork.CommitTransactionAsync(cancellationToken);

View File

@@ -7,15 +7,20 @@ public class DeleteProductVariantCommand : IRequest<bool>
public Guid Id { get; set; }
}
public class DeleteProductVariantHandler(IUnitOfWork unitOfWork) : IRequestHandler<DeleteProductVariantCommand, bool>
public class DeleteProductVariant(
IUnitOfWork unitOfWork)
: IRequestHandler<DeleteProductVariantCommand, bool>
{
public async Task<bool> Handle(DeleteProductVariantCommand request, CancellationToken cancellationToken)
public async Task<bool> Handle(
DeleteProductVariantCommand request,
CancellationToken cancellationToken)
{
await unitOfWork.BeginTransactionAsync(cancellationToken);
try
{
var exists = await unitOfWork.ProductVariantRepository.ExistsAsync(request.Id, cancellationToken);
var exists = await unitOfWork.ProductVariantRepository
.ExistsAsync(request.Id, cancellationToken);
if (!exists)
{

View File

@@ -2,7 +2,6 @@ using AutoMapper;
using Imprink.Application.Dtos;
using Imprink.Domain.Entities;
using MediatR;
using Microsoft.Extensions.Logging;
namespace Imprink.Application.Commands.ProductVariants;
@@ -13,10 +12,14 @@ public class GetProductVariantsQuery : IRequest<IEnumerable<ProductVariantDto>>
public bool InStockOnly { get; set; } = false;
}
public class GetProductVariantsHandler(IUnitOfWork unitOfWork, IMapper mapper, ILogger<GetProductVariantsHandler> logger)
public class GetProductVariants(
IUnitOfWork unitOfWork,
IMapper mapper)
: IRequestHandler<GetProductVariantsQuery, IEnumerable<ProductVariantDto>>
{
public async Task<IEnumerable<ProductVariantDto>> Handle(GetProductVariantsQuery request, CancellationToken cancellationToken)
public async Task<IEnumerable<ProductVariantDto>> Handle(
GetProductVariantsQuery request,
CancellationToken cancellationToken)
{
IEnumerable<ProductVariant> variants;
@@ -24,15 +27,18 @@ public class GetProductVariantsHandler(IUnitOfWork unitOfWork, IMapper mapper, I
{
if (request.InStockOnly)
{
variants = await unitOfWork.ProductVariantRepository.GetInStockByProductIdAsync(request.ProductId.Value, cancellationToken);
variants = await unitOfWork.ProductVariantRepository
.GetInStockByProductIdAsync(request.ProductId.Value, cancellationToken);
}
else if (request.IsActive.HasValue && request.IsActive.Value)
{
variants = await unitOfWork.ProductVariantRepository.GetActiveByProductIdAsync(request.ProductId.Value, cancellationToken);
variants = await unitOfWork.ProductVariantRepository
.GetActiveByProductIdAsync(request.ProductId.Value, cancellationToken);
}
else
{
variants = await unitOfWork.ProductVariantRepository.GetByProductIdAsync(request.ProductId.Value, cancellationToken);
variants = await unitOfWork.ProductVariantRepository
.GetByProductIdAsync(request.ProductId.Value, cancellationToken);
}
}
else

View File

@@ -18,23 +18,29 @@ public class UpdateProductVariantCommand : IRequest<ProductVariantDto>
public bool IsActive { get; set; }
}
public class UpdateProductVariantHandler(IUnitOfWork unitOfWork, IMapper mapper)
public class UpdateProductVariant(
IUnitOfWork unitOfWork,
IMapper mapper)
: IRequestHandler<UpdateProductVariantCommand, ProductVariantDto>
{
public async Task<ProductVariantDto> Handle(UpdateProductVariantCommand request, CancellationToken cancellationToken)
public async Task<ProductVariantDto> Handle(
UpdateProductVariantCommand request,
CancellationToken cancellationToken)
{
await unitOfWork.BeginTransactionAsync(cancellationToken);
try
{
var existingVariant = await unitOfWork.ProductVariantRepository.GetByIdAsync(request.Id, cancellationToken);
var existingVariant = await unitOfWork.ProductVariantRepository
.GetByIdAsync(request.Id, cancellationToken);
if (existingVariant == null)
throw new NotFoundException($"Product variant with ID {request.Id} not found.");
mapper.Map(request, existingVariant);
var updatedVariant = await unitOfWork.ProductVariantRepository.UpdateAsync(existingVariant, cancellationToken);
var updatedVariant = await unitOfWork.ProductVariantRepository
.UpdateAsync(existingVariant, cancellationToken);
await unitOfWork.SaveAsync(cancellationToken);
await unitOfWork.CommitTransactionAsync(cancellationToken);

View File

@@ -16,9 +16,14 @@ public class CreateProductCommand : IRequest<ProductDto>
public Guid? CategoryId { get; set; }
}
public class CreateProductHandler(IUnitOfWork uw, IMapper mapper) : IRequestHandler<CreateProductCommand, ProductDto>
public class CreateProduct(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<CreateProductCommand, ProductDto>
{
public async Task<ProductDto> Handle(CreateProductCommand request, CancellationToken cancellationToken)
public async Task<ProductDto> Handle(
CreateProductCommand request,
CancellationToken cancellationToken)
{
return await uw.TransactAsync(async () =>
{

View File

@@ -8,13 +8,18 @@ public class DeleteProductCommand : IRequest
public Guid Id { get; set; }
}
public class DeleteProductHandler(IUnitOfWork uw) : IRequestHandler<DeleteProductCommand>
public class DeleteProduct(
IUnitOfWork uw)
: IRequestHandler<DeleteProductCommand>
{
public async Task Handle(DeleteProductCommand request, CancellationToken cancellationToken)
public async Task Handle(
DeleteProductCommand request,
CancellationToken cancellationToken)
{
await uw.TransactAsync(async () =>
{
var exists = await uw.ProductRepository.ExistsAsync(request.Id, cancellationToken);
var exists = await uw.ProductRepository
.ExistsAsync(request.Id, cancellationToken);
if (!exists)
{

View File

@@ -0,0 +1,51 @@
using Imprink.Application.Dtos;
using MediatR;
namespace Imprink.Application.Commands.Products;
public class GetProductByIdQuery : IRequest<ProductDto?>
{
public Guid ProductId { get; set; }
}
public class GetProductById(
IUnitOfWork unitOfWork)
: IRequestHandler<GetProductByIdQuery, ProductDto?>
{
public async Task<ProductDto?> Handle(
GetProductByIdQuery request,
CancellationToken cancellationToken)
{
var product = await unitOfWork.ProductRepository
.GetByIdWithCategoryAsync(request.ProductId, cancellationToken);
if (product == null)
return null;
return new ProductDto
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
BasePrice = product.BasePrice,
IsCustomizable = product.IsCustomizable,
IsActive = product.IsActive,
ImageUrl = product.ImageUrl,
CategoryId = product.CategoryId,
Category = new CategoryDto
{
Id = product.Category.Id,
Name = product.Category.Name,
Description = product.Category.Description,
ImageUrl = product.Category.ImageUrl,
SortOrder = product.Category.SortOrder,
IsActive = product.Category.IsActive,
ParentCategoryId = product.Category.ParentCategoryId,
CreatedAt = product.Category.CreatedAt,
ModifiedAt = product.Category.ModifiedAt
},
CreatedAt = product.CreatedAt,
ModifiedAt = product.ModifiedAt
};
}
}

View File

@@ -9,11 +9,16 @@ public class GetProductsQuery : IRequest<PagedResultDto<ProductDto>>
public ProductFilterParameters FilterParameters { get; set; } = new();
}
public class GetProductsHandler(IUnitOfWork unitOfWork) : IRequestHandler<GetProductsQuery, PagedResultDto<ProductDto>>
public class GetProducts(
IUnitOfWork unitOfWork)
: IRequestHandler<GetProductsQuery, PagedResultDto<ProductDto>>
{
public async Task<PagedResultDto<ProductDto>> Handle(GetProductsQuery request, CancellationToken cancellationToken)
public async Task<PagedResultDto<ProductDto>> Handle(
GetProductsQuery request,
CancellationToken cancellationToken)
{
var pagedResult = await unitOfWork.ProductRepository.GetPagedAsync(request.FilterParameters, cancellationToken);
var pagedResult = await unitOfWork.ProductRepository
.GetPagedAsync(request.FilterParameters, cancellationToken);
var productDtos = pagedResult.Items.Select(p => new ProductDto
{

View File

@@ -4,7 +4,7 @@ using MediatR;
namespace Imprink.Application.Commands.Products;
public class UpdateProductCommand : IRequest<ProductDto>
public class UpdateProduct : IRequest<ProductDto>
{
public Guid Id { get; set; }
public string Name { get; set; } = null!;
@@ -16,15 +16,20 @@ public class UpdateProductCommand : IRequest<ProductDto>
public Guid? CategoryId { get; set; }
}
public class UpdateProductHandler(IUnitOfWork unitOfWork) : IRequestHandler<UpdateProductCommand, ProductDto>
public class UpdateProductHandler(
IUnitOfWork unitOfWork)
: IRequestHandler<UpdateProduct, ProductDto>
{
public async Task<ProductDto> Handle(UpdateProductCommand request, CancellationToken cancellationToken)
public async Task<ProductDto> Handle(
UpdateProduct request,
CancellationToken cancellationToken)
{
await unitOfWork.BeginTransactionAsync(cancellationToken);
try
{
var existingProduct = await unitOfWork.ProductRepository.GetByIdAsync(request.Id, cancellationToken);
var existingProduct = await unitOfWork.ProductRepository
.GetByIdAsync(request.Id, cancellationToken);
if (existingProduct == null)
throw new NotFoundException($"Product with ID {request.Id} not found.");
@@ -37,7 +42,8 @@ public class UpdateProductHandler(IUnitOfWork unitOfWork) : IRequestHandler<Upda
existingProduct.ImageUrl = request.ImageUrl;
existingProduct.CategoryId = request.CategoryId;
var updatedProduct = await unitOfWork.ProductRepository.UpdateAsync(existingProduct, cancellationToken);
var updatedProduct = await unitOfWork.ProductRepository
.UpdateAsync(existingProduct, cancellationToken);
var categoryDto = new CategoryDto
{

View File

@@ -7,9 +7,14 @@ namespace Imprink.Application.Commands.Users;
public record DeleteUserRoleCommand(string Sub, Guid RoleId) : IRequest<UserRoleDto?>;
public class DeleteUserRoleHandler(IUnitOfWork uw, IMapper mapper) : IRequestHandler<DeleteUserRoleCommand, UserRoleDto?>
public class DeleteUserRole(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<DeleteUserRoleCommand, UserRoleDto?>
{
public async Task<UserRoleDto?> Handle(DeleteUserRoleCommand request, CancellationToken cancellationToken)
public async Task<UserRoleDto?> Handle(
DeleteUserRoleCommand request,
CancellationToken cancellationToken)
{
await uw.BeginTransactionAsync(cancellationToken);
@@ -18,12 +23,14 @@ public class DeleteUserRoleHandler(IUnitOfWork uw, IMapper mapper) : IRequestHan
if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken))
throw new NotFoundException("User with ID: " + request.Sub + " does not exist.");
var existingUserRole = await uw.UserRoleRepository.GetUserRoleAsync(request.Sub, request.RoleId, cancellationToken);
var existingUserRole = await uw.UserRoleRepository
.GetUserRoleAsync(request.Sub, request.RoleId, cancellationToken);
if (existingUserRole == null)
throw new NotFoundException($"User role not found for user {request.Sub} and role {request.RoleId}");
var removedRole = await uw.UserRoleRepository.RemoveUserRoleAsync(existingUserRole, cancellationToken);
var removedRole = await uw.UserRoleRepository
.RemoveUserRoleAsync(existingUserRole, cancellationToken);
await uw.SaveAsync(cancellationToken);
await uw.CommitTransactionAsync(cancellationToken);

View File

@@ -0,0 +1,22 @@
using AutoMapper;
using Imprink.Application.Dtos;
using MediatR;
namespace Imprink.Application.Commands.Users;
public record GetAllRolesCommand : IRequest<IEnumerable<RoleDto>>;
public class GetAllRoles(
IUnitOfWork uw,
IMapper mapper): IRequestHandler<GetAllRolesCommand, IEnumerable<RoleDto>>
{
public async Task<IEnumerable<RoleDto>> Handle(
GetAllRolesCommand request,
CancellationToken cancellationToken)
{
var roles = await uw.RoleRepository
.GetAllRolesAsync(cancellationToken);
return mapper.Map<IEnumerable<RoleDto>>(roles);
}
}

View File

@@ -1,17 +0,0 @@
using AutoMapper;
using Imprink.Application.Dtos;
using MediatR;
namespace Imprink.Application.Commands.Users;
public record GetAllRolesCommand : IRequest<IEnumerable<RoleDto>>;
public class GetAllRolesHandler(IUnitOfWork uw, IMapper mapper): IRequestHandler<GetAllRolesCommand, IEnumerable<RoleDto>>
{
public async Task<IEnumerable<RoleDto>> Handle(GetAllRolesCommand request, CancellationToken cancellationToken)
{
var roles = await uw.RoleRepository.GetAllRolesAsync(cancellationToken);
return mapper.Map<IEnumerable<RoleDto>>(roles);
}
}

View File

@@ -7,14 +7,20 @@ namespace Imprink.Application.Commands.Users;
public record GetUserRolesCommand(string Sub) : IRequest<IEnumerable<RoleDto>>;
public class GetUserRolesHandler(IUnitOfWork uw, IMapper mapper): IRequestHandler<GetUserRolesCommand, IEnumerable<RoleDto>>
public class GetUserRoles(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<GetUserRolesCommand, IEnumerable<RoleDto>>
{
public async Task<IEnumerable<RoleDto>> Handle(GetUserRolesCommand request, CancellationToken cancellationToken)
public async Task<IEnumerable<RoleDto>> Handle(
GetUserRolesCommand request,
CancellationToken cancellationToken)
{
if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken))
throw new NotFoundException("User with ID: " + request.Sub + " does not exist.");
var roles = await uw.UserRoleRepository.GetUserRolesAsync(request.Sub, cancellationToken);
var roles = await uw.UserRoleRepository
.GetUserRolesAsync(request.Sub, cancellationToken);
return mapper.Map<IEnumerable<RoleDto>>(roles);
}

View File

@@ -8,9 +8,15 @@ namespace Imprink.Application.Commands.Users;
public record SetUserFullNameCommand(string FirstName, string LastName) : IRequest<UserDto?>;
public class SetUserFullNameHandler(IUnitOfWork uw, IMapper mapper, ICurrentUserService userService) : IRequestHandler<SetUserFullNameCommand, UserDto?>
public class SetUserFullName(
IUnitOfWork uw,
IMapper mapper,
ICurrentUserService userService)
: IRequestHandler<SetUserFullNameCommand, UserDto?>
{
public async Task<UserDto?> Handle(SetUserFullNameCommand request, CancellationToken cancellationToken)
public async Task<UserDto?> Handle(
SetUserFullNameCommand request,
CancellationToken cancellationToken)
{
await uw.BeginTransactionAsync(cancellationToken);
@@ -21,7 +27,8 @@ public class SetUserFullNameHandler(IUnitOfWork uw, IMapper mapper, ICurrentUser
if (currentUser == null)
throw new NotFoundException("User token could not be accessed.");
var user = await uw.UserRepository.SetUserFullNameAsync(currentUser, request.FirstName, request.LastName, cancellationToken);
var user = await uw.UserRepository
.SetUserFullNameAsync(currentUser, request.FirstName, request.LastName, cancellationToken);
if (user == null)
throw new DataUpdateException("User name could not be updated.");

View File

@@ -8,9 +8,15 @@ namespace Imprink.Application.Commands.Users;
public record SetUserPhoneCommand(string PhoneNumber) : IRequest<UserDto?>;
public class SetUserPhoneHandler(IUnitOfWork uw, IMapper mapper, ICurrentUserService userService) : IRequestHandler<SetUserPhoneCommand, UserDto?>
public class SetUserPhone(
IUnitOfWork uw,
IMapper mapper,
ICurrentUserService userService)
: IRequestHandler<SetUserPhoneCommand, UserDto?>
{
public async Task<UserDto?> Handle(SetUserPhoneCommand request, CancellationToken cancellationToken)
public async Task<UserDto?> Handle(
SetUserPhoneCommand request,
CancellationToken cancellationToken)
{
await uw.BeginTransactionAsync(cancellationToken);
@@ -21,7 +27,8 @@ public class SetUserPhoneHandler(IUnitOfWork uw, IMapper mapper, ICurrentUserSer
if (currentUser == null)
throw new NotFoundException("User token could not be accessed.");
var user = await uw.UserRepository.SetUserPhoneAsync(currentUser, request.PhoneNumber, cancellationToken);
var user = await uw.UserRepository
.SetUserPhoneAsync(currentUser, request.PhoneNumber, cancellationToken);
if (user == null)
throw new DataUpdateException("User phone could not be updated.");

View File

@@ -8,9 +8,14 @@ namespace Imprink.Application.Commands.Users;
public record SetUserRoleCommand(string Sub, Guid RoleId) : IRequest<UserRoleDto?>;
public class SetUserRoleHandler(IUnitOfWork uw, IMapper mapper) : IRequestHandler<SetUserRoleCommand, UserRoleDto?>
public class SetUserRole(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<SetUserRoleCommand, UserRoleDto?>
{
public async Task<UserRoleDto?> Handle(SetUserRoleCommand request, CancellationToken cancellationToken)
public async Task<UserRoleDto?> Handle(
SetUserRoleCommand request,
CancellationToken cancellationToken)
{
await uw.BeginTransactionAsync(cancellationToken);
@@ -25,7 +30,8 @@ public class SetUserRoleHandler(IUnitOfWork uw, IMapper mapper) : IRequestHandle
RoleId = request.RoleId
};
var addedRole = await uw.UserRoleRepository.AddUserRoleAsync(userRole, cancellationToken);
var addedRole = await uw.UserRoleRepository
.AddUserRoleAsync(userRole, cancellationToken);
await uw.SaveAsync(cancellationToken);
await uw.CommitTransactionAsync(cancellationToken);

View File

@@ -7,15 +7,21 @@ namespace Imprink.Application.Commands.Users;
public record SyncUserCommand(Auth0User User) : IRequest<UserDto?>;
public class SyncUserHandler(IUnitOfWork uw, IMapper mapper): IRequestHandler<SyncUserCommand, UserDto?>
public class SyncUser(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<SyncUserCommand, UserDto?>
{
public async Task<UserDto?> Handle(SyncUserCommand request, CancellationToken cancellationToken)
public async Task<UserDto?> Handle(
SyncUserCommand request,
CancellationToken cancellationToken)
{
await uw.BeginTransactionAsync(cancellationToken);
try
{
var user = await uw.UserRepository.UpdateOrCreateUserAsync(request.User, cancellationToken);
var user = await uw.UserRepository
.UpdateOrCreateUserAsync(request.User, cancellationToken);
if (user == null)
throw new Exception("User exists but could not be updated");

View File

@@ -0,0 +1,24 @@
namespace Imprink.Application.Dtos;
public class AddressDto
{
public Guid Id { get; set; }
public string UserId { get; set; } = null!;
public string AddressType { get; set; } = null!;
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Company { get; set; }
public string AddressLine1 { get; set; } = null!;
public string? AddressLine2 { get; set; }
public string? ApartmentNumber { get; set; }
public string? BuildingNumber { get; set; }
public string? Floor { get; set; }
public string City { get; set; } = null!;
public string State { get; set; } = null!;
public string PostalCode { get; set; } = null!;
public string Country { get; set; } = null!;
public string? PhoneNumber { get; set; }
public string? Instructions { get; set; }
public bool IsDefault { get; set; }
public bool IsActive { get; set; }
}

View File

@@ -0,0 +1,24 @@
namespace Imprink.Application.Dtos;
public class OrderAddressDto
{
public Guid Id { get; set; }
public Guid OrderId { get; set; }
public string AddressType { get; set; } = null!;
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Company { get; set; }
public string AddressLine1 { get; set; } = null!;
public string? AddressLine2 { get; set; }
public string? ApartmentNumber { get; set; }
public string? BuildingNumber { get; set; }
public string? Floor { get; set; }
public string City { get; set; } = null!;
public string State { get; set; } = null!;
public string PostalCode { get; set; } = null!;
public string Country { get; set; } = null!;
public string? PhoneNumber { get; set; }
public string? Instructions { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}

View File

@@ -0,0 +1,28 @@
namespace Imprink.Application.Dtos;
public class OrderDto
{
public Guid Id { get; set; }
public string UserId { get; set; } = null!;
public DateTime OrderDate { 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? Notes { get; set; }
public string? MerchantId { get; set; }
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; }
public OrderStatusDto? OrderStatus { get; set; }
public UserDto? User { get; set; }
public ShippingStatusDto? ShippingStatus { get; set; }
public OrderAddressDto? OrderAddress { get; set; }
public ProductDto? Product { get; set; }
public ProductVariantDto? ProductVariant { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace Imprink.Application.Dtos;
public class OrderStatusDto
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public string? Description { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace Imprink.Application.Dtos;
public class ShippingStatusDto
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public string? Description { get; set; }
}

View File

@@ -11,7 +11,8 @@ public interface IUnitOfWork
public IUserRoleRepository UserRoleRepository { get; }
public IRoleRepository RoleRepository { get; }
public IOrderRepository OrderRepository { get; }
public IOrderItemRepository OrderItemRepository { get; }
public IAddressRepository AddressRepository { get; }
public IOrderAddressRepository OrderAddressRepository { get; }
Task SaveAsync(CancellationToken cancellationToken = default);
Task BeginTransactionAsync(CancellationToken cancellationToken = default);

View File

@@ -19,8 +19,4 @@
<ProjectReference Include="..\Imprink.Domain\Imprink.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Commands\Orders\" />
</ItemGroup>
</Project>

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

@@ -16,14 +16,12 @@ 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<ProductVariant, ProductVariantDto>();
CreateMap<ProductVariantDto, ProductVariant>()
.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<Product, ProductDto>();
@@ -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<Category, CategoryDto>();
}

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,5 +2,5 @@ namespace Imprink.Application.Services;
public interface ICurrentUserService
{
string? GetCurrentUserId();
string GetCurrentUserId();
}

View File

@@ -0,0 +1,117 @@
using FluentValidation;
using Imprink.Application.Commands.Addresses;
namespace Imprink.Application.Validation.Addresses;
public class CreateAddressCommandValidator : AbstractValidator<CreateAddressCommand>
{
public CreateAddressCommandValidator()
{
RuleFor(x => x.AddressType)
.NotEmpty()
.WithMessage("Address type is required.")
.MaximumLength(50)
.WithMessage("Address type must not exceed 50 characters.")
.Must(BeValidAddressType)
.WithMessage("Address type must be one of: Home, Work, Billing, Shipping, Other.");
RuleFor(x => x.FirstName)
.MaximumLength(100)
.WithMessage("First name must not exceed 100 characters.")
.Matches(@"^[a-zA-Z\s\-'\.]*$")
.When(x => !string.IsNullOrEmpty(x.FirstName))
.WithMessage("First name can only contain letters, spaces, hyphens, apostrophes, and periods.");
RuleFor(x => x.LastName)
.MaximumLength(100)
.WithMessage("Last name must not exceed 100 characters.")
.Matches(@"^[a-zA-Z\s\-'\.]*$")
.When(x => !string.IsNullOrEmpty(x.LastName))
.WithMessage("Last name can only contain letters, spaces, hyphens, apostrophes, and periods.");
RuleFor(x => x.Company)
.MaximumLength(200)
.WithMessage("Company name must not exceed 200 characters.");
RuleFor(x => x.AddressLine1)
.NotEmpty()
.WithMessage("Address line 1 is required.")
.MaximumLength(255)
.WithMessage("Address line 1 must not exceed 255 characters.");
RuleFor(x => x.AddressLine2)
.MaximumLength(255)
.WithMessage("Address line 2 must not exceed 255 characters.");
RuleFor(x => x.ApartmentNumber)
.MaximumLength(20)
.WithMessage("Apartment number must not exceed 20 characters.");
RuleFor(x => x.BuildingNumber)
.MaximumLength(20)
.WithMessage("Building number must not exceed 20 characters.");
RuleFor(x => x.Floor)
.MaximumLength(20)
.WithMessage("Floor must not exceed 20 characters.");
RuleFor(x => x.City)
.NotEmpty()
.WithMessage("City is required.")
.MaximumLength(100)
.WithMessage("City must not exceed 100 characters.")
.Matches(@"^[a-zA-Z\s\-'\.]*$")
.WithMessage("City can only contain letters, spaces, hyphens, apostrophes, and periods.");
RuleFor(x => x.State)
.NotEmpty()
.WithMessage("State is required.")
.MaximumLength(100)
.WithMessage("State must not exceed 100 characters.");
RuleFor(x => x.PostalCode)
.NotEmpty()
.WithMessage("Postal code is required.")
.MaximumLength(20)
.WithMessage("Postal code must not exceed 20 characters.")
.Matches(@"^[a-zA-Z0-9\s\-]*$")
.WithMessage("Postal code can only contain letters, numbers, spaces, and hyphens.");
RuleFor(x => x.Country)
.NotEmpty()
.WithMessage("Country is required.")
.MaximumLength(100)
.WithMessage("Country must not exceed 100 characters.")
.Matches(@"^[a-zA-Z\s\-'\.]*$")
.WithMessage("Country can only contain letters, spaces, hyphens, apostrophes, and periods.");
RuleFor(x => x.PhoneNumber)
.MaximumLength(20)
.WithMessage("Phone number must not exceed 20 characters.")
.Matches(@"^[\+]?[0-9\s\-\(\)\.]*$")
.When(x => !string.IsNullOrEmpty(x.PhoneNumber))
.WithMessage("Phone number format is invalid. Use numbers, spaces, hyphens, parentheses, periods, and optional + prefix.");
RuleFor(x => x.Instructions)
.MaximumLength(500)
.WithMessage("Instructions must not exceed 500 characters.");
RuleFor(x => x)
.Must(HaveNameOrCompany)
.WithMessage("Either first name and last name, or company name must be provided.")
.WithName("Address");
}
private static bool BeValidAddressType(string addressType)
{
var validTypes = new[] { "Home", "Work", "Billing", "Shipping", "Other" };
return validTypes.Contains(addressType, StringComparer.OrdinalIgnoreCase);
}
private static bool HaveNameOrCompany(CreateAddressCommand command)
{
var hasName = !string.IsNullOrWhiteSpace(command.FirstName) && !string.IsNullOrWhiteSpace(command.LastName);
var hasCompany = !string.IsNullOrWhiteSpace(command.Company);
return hasName || hasCompany;
}
}

View File

@@ -0,0 +1,69 @@
using FluentValidation;
using Imprink.Application.Commands.Orders;
namespace Imprink.Application.Validation.Orders;
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator()
{
RuleFor(x => x.Quantity)
.GreaterThan(0)
.WithMessage("Quantity must be greater than 0.")
.LessThanOrEqualTo(1000)
.WithMessage("Quantity cannot exceed 1000 items per order.");
RuleFor(x => x.ProductId)
.NotEmpty()
.WithMessage("Product ID is required.");
RuleFor(x => x.ProductVariantId)
.NotEmpty()
.WithMessage("Product variant ID is required.");
RuleFor(x => x.AddressId)
.NotEmpty()
.WithMessage("Address ID is required.");
RuleFor(x => x.CustomizationImageUrl)
.MaximumLength(2048)
.WithMessage("Customization image URL must not exceed 2048 characters.")
.Must(BeValidUrl)
.When(x => !string.IsNullOrEmpty(x.CustomizationImageUrl))
.WithMessage("Customization image URL must be a valid URL.");
RuleFor(x => x.CustomizationDescription)
.MaximumLength(2000)
.WithMessage("Customization description must not exceed 2000 characters.");
RuleFor(x => x.OriginalImageUrls)
.Must(HaveValidImageUrls)
.When(x => x.OriginalImageUrls != null && x.OriginalImageUrls.Length > 0)
.WithMessage("All original image URLs must be valid URLs.")
.Must(x => x == null || x.Length <= 10)
.WithMessage("Cannot have more than 10 original images.");
RuleForEach(x => x.OriginalImageUrls)
.MaximumLength(2048)
.WithMessage("Each original image URL must not exceed 2048 characters.")
.Must(BeValidUrl)
.WithMessage("Each original image URL must be a valid URL.");
}
private static bool BeValidUrl(string url)
{
if (string.IsNullOrEmpty(url))
return true;
return Uri.TryCreate(url, UriKind.Absolute, out var result)
&& (result.Scheme == Uri.UriSchemeHttp || result.Scheme == Uri.UriSchemeHttps);
}
private static bool HaveValidImageUrls(string[]? urls)
{
if (urls == null || urls.Length == 0)
return true;
return urls.All(url => !string.IsNullOrEmpty(url) && BeValidUrl(url));
}
}

View File

@@ -3,7 +3,7 @@ using Imprink.Application.Commands.Products;
namespace Imprink.Application.Validation.Products;
public class UpdateProductCommandValidator : AbstractValidator<UpdateProductCommand>
public class UpdateProductCommandValidator : AbstractValidator<UpdateProduct>
{
public UpdateProductCommandValidator()
{

View File

@@ -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!;
}

View File

@@ -2,17 +2,25 @@ 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 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? CustomizationImageUrl { get; set; }
public string[] OriginalImageUrls { get; set; } = [];
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 virtual ICollection<OrderItem> OrderItems { get; set; } = new List<OrderItem>();
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

@@ -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; }
public virtual Order Order { get; set; } = null!;
}

View File

@@ -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!;
}

View File

@@ -12,5 +12,5 @@ public class Product : EntityBase
public virtual required Category Category { get; set; }
public virtual ICollection<ProductVariant> ProductVariants { get; set; } = new List<ProductVariant>();
public virtual ICollection<OrderItem> OrderItems { get; set; } = new List<OrderItem>();
public virtual ICollection<Order> Orders { get; set; } = new List<Order>();
}

View File

@@ -12,5 +12,5 @@ public class ProductVariant : EntityBase
public bool IsActive { get; set; }
public virtual required Product Product { get; set; }
public virtual ICollection<OrderItem> OrderItems { get; set; } = new List<OrderItem>();
public virtual ICollection<Order> Orders { get; set; } = new List<Order>();
}

View File

@@ -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<Address> Addresses { get; set; } = new List<Address>();
public virtual ICollection<UserRole> UserRoles { get; set; } = new List<UserRole>();
public virtual ICollection<Order> Orders { get; set; } = new List<Order>();
public virtual ICollection<Order> MerchantOrders { get; set; } = new List<Order>();
public Address? DefaultAddress => Addresses.FirstOrDefault(a => a is { IsDefault: true, IsActive: true });
public IEnumerable<Role> Roles => UserRoles.Select(ur => ur.Role);

View File

@@ -0,0 +1,22 @@
using Imprink.Domain.Entities;
namespace Imprink.Domain.Repositories;
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<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);
Task<Address> AddAsync(Address address, CancellationToken cancellationToken = default);
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 DeactivateAddressAsync(Guid addressId, CancellationToken cancellationToken = default);
Task ActivateAddressAsync(Guid addressId, CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default);
Task<bool> IsUserAddressAsync(Guid id, string userId, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,16 @@
using Imprink.Domain.Entities;
namespace Imprink.Domain.Repositories;
public interface IOrderAddressRepository
{
Task<OrderAddress?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<OrderAddress?> GetByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default);
Task<IEnumerable<OrderAddress>> GetByOrderIdsAsync(IEnumerable<Guid> orderIds, CancellationToken cancellationToken = default);
Task<OrderAddress> AddAsync(OrderAddress orderAddress, CancellationToken cancellationToken = default);
Task<OrderAddress> UpdateAsync(OrderAddress orderAddress, CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default);
Task<bool> DeleteByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default);
Task<bool> ExistsByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default);
}

View File

@@ -1,27 +0,0 @@
using Imprink.Domain.Entities;
namespace Imprink.Domain.Repositories;
public interface IOrderItemRepository
{
Task<OrderItem?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<OrderItem?> GetByIdWithProductAsync(Guid id, CancellationToken cancellationToken = default);
Task<OrderItem?> GetByIdWithVariantAsync(Guid id, CancellationToken cancellationToken = default);
Task<OrderItem?> GetByIdFullAsync(Guid id, CancellationToken cancellationToken = default);
Task<IEnumerable<OrderItem>> GetByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default);
Task<IEnumerable<OrderItem>> GetByOrderIdWithProductsAsync(Guid orderId, CancellationToken cancellationToken = default);
Task<IEnumerable<OrderItem>> GetByProductIdAsync(Guid productId, CancellationToken cancellationToken = default);
Task<IEnumerable<OrderItem>> GetByProductVariantIdAsync(Guid productVariantId, CancellationToken cancellationToken = default);
Task<IEnumerable<OrderItem>> GetCustomizedItemsAsync(CancellationToken cancellationToken = default);
Task<IEnumerable<OrderItem>> GetByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default);
Task<OrderItem> AddAsync(OrderItem orderItem, CancellationToken cancellationToken = default);
Task<IEnumerable<OrderItem>> AddRangeAsync(IEnumerable<OrderItem> orderItems, CancellationToken cancellationToken = default);
Task<OrderItem> UpdateAsync(OrderItem orderItem, CancellationToken cancellationToken = default);
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
Task DeleteByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default);
Task<decimal> GetTotalValueByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default);
Task<int> GetQuantityByProductIdAsync(Guid productId, CancellationToken cancellationToken = default);
Task<int> GetQuantityByVariantIdAsync(Guid productVariantId, CancellationToken cancellationToken = default);
Task<Dictionary<Guid, int>> GetProductSalesCountAsync(DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default);
}

View File

@@ -6,22 +6,20 @@ namespace Imprink.Domain.Repositories;
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<Order?> GetByIdWithItemsAsync(Guid id, CancellationToken cancellationToken = default);
Task<Order?> GetByIdWithAddressAsync(Guid id, CancellationToken cancellationToken = default);
Task<Order?> GetByIdFullAsync(Guid id, CancellationToken cancellationToken = default);
Task<Order?> GetByOrderNumberAsync(string orderNumber, CancellationToken cancellationToken = default);
Task<PagedResult<Order>> GetPagedAsync(OrderFilterParameters filterParameters, CancellationToken cancellationToken = default);
Task<Order?> GetByIdWithDetailsAsync(Guid id, CancellationToken cancellationToken = default);
Task<IEnumerable<Order>> GetByUserIdAsync(string userId, CancellationToken cancellationToken = default);
Task<IEnumerable<Order>> GetByUserIdPagedAsync(string userId, int pageNumber, int pageSize, CancellationToken cancellationToken = default);
Task<IEnumerable<Order>> GetByOrderStatusAsync(int orderStatusId, CancellationToken cancellationToken = default);
Task<IEnumerable<Order>> GetByShippingStatusAsync(int shippingStatusId, CancellationToken cancellationToken = default);
Task<IEnumerable<Order>> GetByUserIdWithDetailsAsync(string userId, CancellationToken cancellationToken = default);
Task<IEnumerable<Order>> GetByMerchantIdAsync(string merchantId, CancellationToken cancellationToken = default);
Task<IEnumerable<Order>> GetByMerchantIdWithDetailsAsync(string merchantId, CancellationToken cancellationToken = default);
Task<IEnumerable<Order>> GetByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default);
Task<IEnumerable<Order>> GetByStatusAsync(int statusId, CancellationToken cancellationToken = default);
Task<IEnumerable<Order>> GetByShippingStatusAsync(int shippingStatusId, CancellationToken cancellationToken = default);
Task<Order> AddAsync(Order order, CancellationToken cancellationToken = default);
Task<Order> UpdateAsync(Order order, CancellationToken cancellationToken = default);
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default);
Task<bool> OrderNumberExistsAsync(string orderNumber, Guid? excludeId = null, CancellationToken cancellationToken = default);
Task<decimal> GetTotalRevenueAsync(CancellationToken cancellationToken = default);
Task<decimal> GetTotalRevenueByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default);
Task<int> GetOrderCountByStatusAsync(int orderStatusId, 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);
}

View File

@@ -18,10 +18,31 @@ public class AddressConfiguration : EntityBaseConfiguration<Address>
.IsRequired()
.HasMaxLength(50);
builder.Property(a => a.Street)
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.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()
.HasMaxLength(100);
@@ -37,6 +58,12 @@ public class AddressConfiguration : EntityBaseConfiguration<Address>
.IsRequired()
.HasMaxLength(100);
builder.Property(a => a.PhoneNumber)
.HasMaxLength(20);
builder.Property(a => a.Instructions)
.HasMaxLength(500);
builder.Property(a => a.IsDefault)
.IsRequired()
.HasDefaultValue(false);
@@ -45,6 +72,11 @@ public class AddressConfiguration : EntityBaseConfiguration<Address>
.IsRequired()
.HasDefaultValue(true);
builder.HasOne(a => a.User)
.WithMany(u => u.Addresses)
.HasForeignKey(a => a.UserId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasIndex(a => a.UserId)
.HasDatabaseName("IX_Address_UserId");

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
{
@@ -13,19 +13,15 @@ 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

@@ -13,10 +13,35 @@ public class OrderAddressConfiguration : EntityBaseConfiguration<OrderAddress>
builder.Property(oa => oa.OrderId)
.IsRequired();
builder.Property(oa => oa.Street)
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.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()
.HasMaxLength(100);
@@ -32,6 +57,12 @@ public class OrderAddressConfiguration : EntityBaseConfiguration<OrderAddress>
.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");

View File

@@ -1,3 +1,4 @@
using System.Text.Json;
using Imprink.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
@@ -5,73 +6,116 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Imprink.Infrastructure.Configuration;
public class OrderConfiguration : EntityBaseConfiguration<Order>
{
public override void Configure(EntityTypeBuilder<Order> builder)
{
public override void Configure(EntityTypeBuilder<Order> builder)
{
base.Configure(builder);
base.Configure(builder);
builder.Property(o => o.UserId)
.IsRequired()
.HasMaxLength(450);
builder.Property(o => o.UserId)
.IsRequired()
.HasMaxLength(450);
builder.Property(o => o.OrderDate)
.IsRequired();
builder.Property(o => o.OrderDate)
.IsRequired();
builder.Property(o => o.TotalPrice)
.IsRequired()
.HasColumnType("decimal(18,2)");
builder.Property(o => o.Amount)
.IsRequired()
.HasColumnType("decimal(18,2)");
builder.Property(o => o.OrderStatusId)
.IsRequired();
builder.Property(o => o.Quantity)
.IsRequired()
.HasDefaultValue(1);
builder.Property(o => o.ShippingStatusId)
.IsRequired();
builder.Property(o => o.ProductId)
.IsRequired();
builder.Property(o => o.OrderNumber)
.IsRequired()
.HasMaxLength(50);
builder.Property(o => o.OrderStatusId)
.IsRequired();
builder.Property(o => o.Notes)
.HasMaxLength(1000);
builder.Property(o => o.ShippingStatusId)
.IsRequired();
builder.HasOne(o => o.OrderStatus)
.WithMany(os => os.Orders)
.HasForeignKey(o => o.OrderStatusId)
.OnDelete(DeleteBehavior.Restrict);
builder.Property(o => o.Notes)
.HasMaxLength(1000);
builder.HasOne(o => o.ShippingStatus)
.WithMany(ss => ss.Orders)
.HasForeignKey(o => o.ShippingStatusId)
.OnDelete(DeleteBehavior.Restrict);
builder.Property(o => o.MerchantId)
.HasMaxLength(450);
builder.HasOne(o => o.OrderAddress)
.WithOne(oa => oa.Order)
.HasForeignKey<OrderAddress>(oa => oa.OrderId)
.OnDelete(DeleteBehavior.Cascade);
builder.Property(o => o.OriginalImageUrls)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
v => JsonSerializer.Deserialize<string[]>(v, (JsonSerializerOptions?)null) ?? Array.Empty<string>())
.HasColumnType("nvarchar(max)");
builder.HasOne(o => o.User)
.WithMany(u => u.Orders)
.HasForeignKey(o => o.UserId)
.HasPrincipalKey(u => u.Id)
.OnDelete(DeleteBehavior.Restrict);
builder.Property(o => o.CustomizationImageUrl)
.HasMaxLength(1000);
builder.HasIndex(o => o.UserId)
.HasDatabaseName("IX_Order_UserId");
builder.Property(o => o.CustomizationDescription)
.HasMaxLength(2000);
builder.HasIndex(o => o.OrderNumber)
.IsUnique()
.HasDatabaseName("IX_Order_OrderNumber");
builder.HasOne(o => o.OrderStatus)
.WithMany(os => os.Orders)
.HasForeignKey(o => o.OrderStatusId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(o => o.OrderDate)
.HasDatabaseName("IX_Order_OrderDate");
builder.HasOne(o => o.ShippingStatus)
.WithMany(ss => ss.Orders)
.HasForeignKey(o => o.ShippingStatusId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(o => o.OrderStatusId)
.HasDatabaseName("IX_Order_OrderStatusId");
builder.HasOne(o => o.OrderAddress)
.WithOne(oa => oa.Order)
.HasForeignKey<OrderAddress>(oa => oa.OrderId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasIndex(o => o.ShippingStatusId)
.HasDatabaseName("IX_Order_ShippingStatusId");
builder.HasOne(o => o.User)
.WithMany(u => u.Orders)
.HasForeignKey(o => o.UserId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(o => new { o.UserId, o.OrderDate })
.HasDatabaseName("IX_Order_User_Date");
}
builder.HasOne(o => o.Merchant)
.WithMany(u => u.MerchantOrders)
.HasForeignKey(o => o.MerchantId)
.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.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");
}
}

View File

@@ -1,64 +0,0 @@
using Imprink.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Imprink.Infrastructure.Configuration;
public class OrderItemConfiguration : EntityBaseConfiguration<OrderItem>
{
public override void Configure(EntityTypeBuilder<OrderItem> 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");
}
}

View File

@@ -35,18 +35,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)
.HasForeignKey(p => p.CategoryId)

View File

@@ -38,18 +38,6 @@ public class ProductVariantConfiguration : EntityBaseConfiguration<ProductVarian
.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)
.HasForeignKey(pv => pv.ProductId)

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();
@@ -25,8 +27,7 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
.HasMaxLength(100);
builder.Property(u => u.EmailVerified)
.IsRequired()
.HasMaxLength(100);
.IsRequired();
builder.Property(u => u.FirstName)
.HasMaxLength(100);
@@ -48,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

@@ -11,8 +11,6 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
public DbSet<Product> Products { get; set; }
public DbSet<ProductVariant> ProductVariants { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<OrderItem> OrderItems { get; set; }
public DbSet<OrderAddress> OrderAddresses { get; set; }
public DbSet<Address> Addresses { get; set; }
public DbSet<OrderStatus> OrderStatuses { get; set; }
public DbSet<ShippingStatus> ShippingStatuses { get; set; }
@@ -20,6 +18,7 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
public DbSet<UserRole> UserRole { get; set; }
public DbSet<Role> Roles { get; set; }
public DbSet<Category> Categories { get; set; }
public DbSet<OrderAddress> OrderAddresses { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -28,7 +27,6 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> 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());

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

@@ -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<IEnumerable<Address>> 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<IEnumerable<Address>> 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<Address?> GetDefaultByUserIdAsync(string? userId, CancellationToken cancellationToken = default)
{
return await context.Addresses
.FirstOrDefaultAsync(a => a.UserId == userId && a.IsDefault && a.IsActive, cancellationToken);
}
public async Task<IEnumerable<Address>> 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<Address?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
return await context.Addresses
.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
}
public async Task<Address?> GetByIdAndUserIdAsync(Guid id, string userId, CancellationToken cancellationToken = default)
{
return await context.Addresses
.FirstOrDefaultAsync(a => a.Id == id && a.UserId == userId, cancellationToken);
}
public async Task<Address> AddAsync(Address address, CancellationToken cancellationToken = default)
{
if (address.IsDefault)
{
await UnsetDefaultAddressesAsync(address.UserId, cancellationToken);
}
context.Addresses.Add(address);
return address;
}
public async Task<Address> 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<bool> 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<bool> 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<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default)
{
return await context.Addresses
.AnyAsync(a => a.Id == id, cancellationToken);
}
public async Task<bool> 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;
}
}
}

View File

@@ -0,0 +1,77 @@
using Imprink.Domain.Entities;
using Imprink.Domain.Repositories;
using Imprink.Infrastructure.Database;
using Microsoft.EntityFrameworkCore;
namespace Imprink.Infrastructure.Repositories;
public class OrderAddressRepository(ApplicationDbContext context) : IOrderAddressRepository
{
public async Task<OrderAddress?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
return await context.OrderAddresses
.Include(oa => oa.Order)
.FirstOrDefaultAsync(oa => oa.Id == id, cancellationToken);
}
public async Task<OrderAddress?> GetByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default)
{
return await context.OrderAddresses
.Include(oa => oa.Order)
.FirstOrDefaultAsync(oa => oa.OrderId == orderId, cancellationToken);
}
public async Task<IEnumerable<OrderAddress>> GetByOrderIdsAsync(IEnumerable<Guid> orderIds, CancellationToken cancellationToken = default)
{
return await context.OrderAddresses
.Include(oa => oa.Order)
.Where(oa => orderIds.Contains(oa.OrderId))
.ToListAsync(cancellationToken);
}
public async Task<OrderAddress> AddAsync(OrderAddress orderAddress, CancellationToken cancellationToken = default)
{
var entry = await context.OrderAddresses.AddAsync(orderAddress, cancellationToken);
await context.SaveChangesAsync(cancellationToken);
return entry.Entity;
}
public async Task<OrderAddress> UpdateAsync(OrderAddress orderAddress, CancellationToken cancellationToken = default)
{
context.OrderAddresses.Update(orderAddress);
await context.SaveChangesAsync(cancellationToken);
return orderAddress;
}
public async Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default)
{
var orderAddress = await context.OrderAddresses.FindAsync([id], cancellationToken);
if (orderAddress == null)
return false;
context.OrderAddresses.Remove(orderAddress);
await context.SaveChangesAsync(cancellationToken);
return true;
}
public async Task<bool> DeleteByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default)
{
var orderAddress = await context.OrderAddresses.FirstOrDefaultAsync(oa => oa.OrderId == orderId, cancellationToken);
if (orderAddress == null)
return false;
context.OrderAddresses.Remove(orderAddress);
return true;
}
public async Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default)
{
return await context.OrderAddresses.AnyAsync(oa => oa.Id == id, cancellationToken);
}
public async Task<bool> ExistsByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default)
{
return await context.OrderAddresses.AnyAsync(oa => oa.OrderId == orderId, cancellationToken);
}
}

View File

@@ -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<OrderItem?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
return await context.OrderItems
.FirstOrDefaultAsync(oi => oi.Id == id, cancellationToken);
}
public async Task<OrderItem?> 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<OrderItem?> GetByIdWithVariantAsync(Guid id, CancellationToken cancellationToken = default)
{
return await context.OrderItems
.Include(oi => oi.ProductVariant)
.FirstOrDefaultAsync(oi => oi.Id == id, cancellationToken);
}
public async Task<OrderItem?> 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<IEnumerable<OrderItem>> GetByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default)
{
return await context.OrderItems
.Where(oi => oi.OrderId == orderId)
.OrderBy(oi => oi.CreatedAt)
.ToListAsync(cancellationToken);
}
public async Task<IEnumerable<OrderItem>> 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<IEnumerable<OrderItem>> 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<IEnumerable<OrderItem>> 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<IEnumerable<OrderItem>> 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<IEnumerable<OrderItem>> 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<OrderItem> 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<IEnumerable<OrderItem>> AddRangeAsync(IEnumerable<OrderItem> 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<IEnumerable<OrderItem>>(items);
}
public Task<OrderItem> 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<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default)
{
return await context.OrderItems
.AnyAsync(oi => oi.Id == id, cancellationToken);
}
public async Task<decimal> GetTotalValueByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default)
{
return await context.OrderItems
.Where(oi => oi.OrderId == orderId)
.SumAsync(oi => oi.TotalPrice, cancellationToken);
}
public async Task<int> GetQuantityByProductIdAsync(Guid productId, CancellationToken cancellationToken = default)
{
return await context.OrderItems
.Where(oi => oi.ProductId == productId)
.SumAsync(oi => oi.Quantity, cancellationToken);
}
public async Task<int> GetQuantityByVariantIdAsync(Guid productVariantId, CancellationToken cancellationToken = default)
{
return await context.OrderItems
.Where(oi => oi.ProductVariantId == productVariantId)
.SumAsync(oi => oi.Quantity, cancellationToken);
}
public async Task<Dictionary<Guid, int>> 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);
}
}

View File

@@ -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,73 @@ public class OrderRepository(ApplicationDbContext context) : IOrderRepository
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
}
public async Task<Order?> GetByIdWithItemsAsync(Guid id, CancellationToken cancellationToken = default)
public async Task<Order?> 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<Order?> GetByIdWithAddressAsync(Guid id, CancellationToken cancellationToken = default)
{
return await context.Orders
.Include(o => o.OrderAddress)
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
}
public async Task<Order?> GetByIdFullAsync(Guid id, CancellationToken cancellationToken = default)
{
return await context.Orders
.Include(o => o.User)
.Include(o => o.OrderStatus)
.Include(o => o.ShippingStatus)
.Include(o => o.OrderAddress)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.ThenInclude(p => p.Category)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.ProductVariant)
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
}
public async Task<Order?> 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<PagedResult<Order>> GetPagedAsync(
OrderFilterParameters filterParameters,
CancellationToken cancellationToken = default)
{
var query = context.Orders
.Include(o => o.Product)
.Include(o => o.ProductVariant)
.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<Order>
{
Items = items,
TotalCount = totalCount,
PageNumber = filterParameters.PageNumber,
PageSize = filterParameters.PageSize
};
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
}
public async Task<IEnumerable<Order>> 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<IEnumerable<Order>> GetByUserIdPagedAsync(
string userId,
int pageNumber,
int pageSize,
CancellationToken cancellationToken = default)
public async Task<IEnumerable<Order>> 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<IEnumerable<Order>> GetByOrderStatusAsync(int orderStatusId, CancellationToken cancellationToken = default)
public async Task<IEnumerable<Order>> GetByMerchantIdAsync(string merchantId, CancellationToken cancellationToken = default)
{
return await context.Orders
.Where(o => o.MerchantId == merchantId)
.OrderByDescending(o => o.OrderDate)
.ToListAsync(cancellationToken);
}
public async Task<IEnumerable<Order>> 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<IEnumerable<Order>> 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<IEnumerable<Order>> GetByStatusAsync(int statusId, CancellationToken cancellationToken = default)
{
return await context.Orders
.Where(o => o.OrderStatusId == statusId)
.OrderByDescending(o => o.OrderDate)
.ToListAsync(cancellationToken);
}
@@ -174,52 +87,39 @@ public class OrderRepository(ApplicationDbContext context) : IOrderRepository
public async Task<IEnumerable<Order>> 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<IEnumerable<Order>> 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<Order> 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<Order> UpdateAsync(Order order, CancellationToken cancellationToken = default)
public async Task<Order> 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<bool> 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<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default)
@@ -228,38 +128,47 @@ public class OrderRepository(ApplicationDbContext context) : IOrderRepository
.AnyAsync(o => o.Id == id, cancellationToken);
}
public async Task<bool> OrderNumberExistsAsync(string orderNumber, Guid? excludeId = null, CancellationToken cancellationToken = default)
public async Task UpdateStatusAsync(Guid orderId, int statusId, CancellationToken cancellationToken = default)
{
var query = context.Orders.Where(o => o.OrderNumber == orderNumber);
var order = await context.Orders
.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken);
if (excludeId.HasValue)
if (order != null)
{
query = query.Where(o => o.Id != excludeId.Value);
order.OrderStatusId = statusId;
}
return await query.AnyAsync(cancellationToken);
}
public async Task<decimal> GetTotalRevenueAsync(CancellationToken cancellationToken = default)
public async Task UpdateShippingStatusAsync(Guid orderId, int shippingStatusId, 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.ShippingStatusId = shippingStatusId;
}
}
public async Task<decimal> GetTotalRevenueByDateRangeAsync(
DateTime startDate,
DateTime endDate,
CancellationToken cancellationToken = default)
public async Task AssignMerchantAsync(Guid orderId, string merchantId, 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.MerchantId = merchantId;
}
}
public async Task<int> GetOrderCountByStatusAsync(int orderStatusId, CancellationToken cancellationToken = default)
public async Task UnassignMerchantAsync(Guid orderId, 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 = null;
}
}
}

View File

@@ -13,7 +13,8 @@ public class UnitOfWork(
IUserRoleRepository userRoleRepository,
IRoleRepository roleRepository,
IOrderRepository orderRepository,
IOrderItemRepository orderItemRepository) : IUnitOfWork
IAddressRepository addressRepository,
IOrderAddressRepository orderAddressRepository) : IUnitOfWork
{
public IProductRepository ProductRepository => productRepository;
public IProductVariantRepository ProductVariantRepository => productVariantRepository;
@@ -22,7 +23,8 @@ public class UnitOfWork(
public IUserRoleRepository UserRoleRepository => userRoleRepository;
public IRoleRepository RoleRepository => roleRepository;
public IOrderRepository OrderRepository => orderRepository;
public IOrderItemRepository OrderItemRepository => orderItemRepository;
public IAddressRepository AddressRepository => addressRepository;
public IOrderAddressRepository OrderAddressRepository => orderAddressRepository;
public async Task SaveAsync(CancellationToken cancellationToken = default)
{
@@ -50,7 +52,6 @@ public class UnitOfWork(
try
{
var result = await operation();
await SaveAsync(cancellationToken);
await CommitTransactionAsync(cancellationToken);
return result;
}
@@ -67,7 +68,6 @@ public class UnitOfWork(
try
{
await operation();
await SaveAsync(cancellationToken);
await CommitTransactionAsync(cancellationToken);
}
catch

View File

@@ -0,0 +1,49 @@
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("me")]
[Authorize]
public async Task<ActionResult<IEnumerable<AddressDto?>>> GetMyAddresses(CancellationToken cancellationToken = default)
{
var result = await mediator.Send(new GetMyAddressesQuery(), cancellationToken);
return Ok(result);
}
[HttpGet("user/{userId}")]
[Authorize(Roles = "Admin")]
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(CreateAddress), 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);
}
}

View File

@@ -10,11 +10,12 @@ namespace Imprink.WebApi.Controllers;
[Route("/api/products/variants")]
public class ProductVariantsController(IMediator mediator) : ControllerBase
{
[HttpGet]
[HttpGet("{id:guid}")]
[AllowAnonymous]
public async Task<ActionResult<IEnumerable<ProductVariantDto>>> GetProductVariants(
[FromQuery] GetProductVariantsQuery query)
Guid id)
{
var query = new GetProductVariantsQuery { ProductId = id };
return Ok(await mediator.Send(query));
}

View File

@@ -21,6 +21,18 @@ public class ProductsController(IMediator mediator) : ControllerBase
return Ok(result);
}
[HttpGet("{id:guid}")]
[AllowAnonymous]
public async Task<ActionResult<ProductDto>> GetProductById(
Guid id,
CancellationToken cancellationToken)
{
var result = await mediator
.Send(new GetProductByIdQuery { ProductId = id}, cancellationToken);
return Ok(result);
}
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<ActionResult<PagedResultDto<ProductDto>>> CreateProduct(
@@ -34,7 +46,7 @@ public class ProductsController(IMediator mediator) : ControllerBase
[Authorize(Roles = "Admin")]
public async Task<ActionResult<ProductDto>> UpdateProduct(
Guid id,
[FromBody] UpdateProductCommand command)
[FromBody] UpdateProduct command)
{
if (id != command.Id) return BadRequest("ID mismatch");

View File

@@ -20,7 +20,8 @@ public static class StartupApplicationExtensions
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUserRoleRepository, UserRoleRepository>();
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IOrderItemRepository, OrderItemRepository>();
services.AddScoped<IOrderAddressRepository, OrderAddressRepository>();
services.AddScoped<IAddressRepository, AddressRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<ICurrentUserService, CurrentUserService>();
services.AddScoped<Seeder>();

View File

@@ -24,7 +24,7 @@ public static class Startup
services.AddDatabaseContexts(builder.Configuration);
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CreateProductHandler).Assembly));
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CreateProduct).Assembly));
services.AddValidatorsFromAssembly(typeof(Auth0UserValidator).Assembly);

View File

@@ -15,7 +15,7 @@ public class CreateCategoryHandlerIntegrationTest : IDisposable
{
private readonly ApplicationDbContext _context;
private readonly IServiceProvider _serviceProvider;
private readonly CreateCategoryHandler _handler;
private readonly CreateCategory _handler;
private readonly SqliteConnection _connection;
public CreateCategoryHandlerIntegrationTest()
@@ -35,14 +35,14 @@ public class CreateCategoryHandlerIntegrationTest : IDisposable
services.AddScoped<IUserRoleRepository, UserRoleRepository>();
services.AddScoped<IRoleRepository, RoleRepository>();
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IOrderItemRepository, OrderItemRepository>();
services.AddScoped<IAddressRepository, AddressRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<CreateCategoryHandler>();
services.AddScoped<CreateCategory>();
_serviceProvider = services.BuildServiceProvider();
_context = _serviceProvider.GetRequiredService<ApplicationDbContext>();
_handler = _serviceProvider.GetRequiredService<CreateCategoryHandler>();
_handler = _serviceProvider.GetRequiredService<CreateCategory>();
_context.Database.EnsureCreated();
}
@@ -216,7 +216,7 @@ public class CreateCategoryHandlerIntegrationTest : IDisposable
var initialCount = await _context.Categories.CountAsync();
var handler = _serviceProvider.GetRequiredService<CreateCategoryHandler>();
var handler = _serviceProvider.GetRequiredService<CreateCategory>();
var result = await handler.Handle(command, CancellationToken.None);

View File

@@ -6,13 +6,13 @@ using Moq;
namespace Imprink.Application.Tests;
public class UpdateCategoryHandlerTests
public class UpdateCategoryTests
{
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
private readonly Mock<ICategoryRepository> _categoryRepositoryMock;
private readonly UpdateCategoryHandler _handler;
private readonly UpdateCategory _handler;
public UpdateCategoryHandlerTests()
public UpdateCategoryTests()
{
_unitOfWorkMock = new Mock<IUnitOfWork>();
_categoryRepositoryMock = new Mock<ICategoryRepository>();
@@ -20,7 +20,7 @@ public class UpdateCategoryHandlerTests
_unitOfWorkMock.Setup(x => x.CategoryRepository)
.Returns(_categoryRepositoryMock.Object);
_handler = new UpdateCategoryHandler(_unitOfWorkMock.Object);
_handler = new UpdateCategory(_unitOfWorkMock.Object);
}
[Fact]

View File

@@ -0,0 +1,800 @@
'use client';
import React, { useState, useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation';
import clientApi from '@/lib/clientApi';
import {
Box,
Button,
Card,
CardContent,
CardMedia,
Typography,
Stepper,
Step,
StepLabel,
Grid,
TextField,
IconButton,
Radio,
RadioGroup,
FormControlLabel,
FormControl,
FormLabel,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Chip,
Container,
Paper,
Fade,
CircularProgress
} from '@mui/material';
import {
Add as AddIcon,
Remove as RemoveIcon,
LocationOn as LocationIcon,
AddLocation as AddLocationIcon
} from '@mui/icons-material';
interface Category {
id: string;
name: string;
description: string;
imageUrl: string;
sortOrder: number;
isActive: boolean;
parentCategoryId: string;
createdAt: string;
modifiedAt: string;
}
interface Product {
id: string;
name: string;
description: string;
basePrice: number;
isCustomizable: boolean;
isActive: boolean;
imageUrl: string;
categoryId: string;
category: Category;
createdAt: string;
modifiedAt: string;
}
interface Variant {
id: string;
productId: string;
size: string;
color: string;
price: number;
imageUrl: string;
sku: string;
stockQuantity: number;
isActive: boolean;
product: Product;
createdAt: string;
modifiedAt: string;
}
interface Address {
id: string;
userId: string;
addressType: string;
firstName: string;
lastName: string;
company: string;
addressLine1: string;
addressLine2: string;
apartmentNumber: string;
buildingNumber: string;
floor: string;
city: string;
state: string;
postalCode: string;
country: string;
phoneNumber: string;
instructions: string;
isDefault: boolean;
isActive: boolean;
}
interface NewAddress {
addressType: string;
firstName: string;
lastName: string;
company: string;
addressLine1: string;
addressLine2: string;
apartmentNumber: string;
buildingNumber: string;
floor: string;
city: string;
state: string;
postalCode: string;
country: string;
phoneNumber: string;
instructions: string;
isDefault: boolean;
isActive: boolean;
}
const steps = ['Product Details', 'Select Variant', 'Choose Quantity', 'Delivery Address', 'Review & Order'];
export default function OrderBuilder() {
const router = useRouter();
const params = useParams();
const productId = params.id as string;
const [activeStep, setActiveStep] = useState(0);
const [loading, setLoading] = useState(false);
const [product, setProduct] = useState<Product | null>(null);
const [variants, setVariants] = useState<Variant[]>([]);
const [addresses, setAddresses] = useState<Address[]>([]);
const [selectedVariant, setSelectedVariant] = useState<Variant | null>(null);
const [quantity, setQuantity] = useState(1);
const [selectedAddress, setSelectedAddress] = useState<Address | null>(null);
const [showAddressDialog, setShowAddressDialog] = useState(false);
const [newAddress, setNewAddress] = useState<NewAddress>({
addressType: 'Home',
firstName: '',
lastName: '',
company: '',
addressLine1: '',
addressLine2: '',
apartmentNumber: '',
buildingNumber: '',
floor: '',
city: '',
state: '',
postalCode: '',
country: '',
phoneNumber: '',
instructions: '',
isDefault: false,
isActive: true
});
useEffect(() => {
if (productId) {
loadProduct();
}
}, [productId]);
const loadProduct = async () => {
setLoading(true);
try {
const productData = await clientApi.get(`/products/${productId}`);
setProduct(productData.data);
} catch (error) {
console.error('Failed to load product:', error);
} finally {
setLoading(false);
}
};
const loadVariants = async () => {
setLoading(true);
try {
const variantsData = await clientApi.get(`/products/variants/${productId}`);
setVariants(variantsData.data);
} catch (error) {
console.error('Failed to load variants:', error);
} finally {
setLoading(false);
}
};
const loadAddresses = async () => {
setLoading(true);
try {
const addressesData = await clientApi.get('/addresses/me');
setAddresses(addressesData.data);
if (addressesData.data.length > 0) {
const defaultAddress = addressesData.data.find((addr: Address) => addr.isDefault) || addressesData.data[0];
setSelectedAddress(defaultAddress);
}
} catch (error) {
console.error('Failed to load addresses:', error);
} finally {
setLoading(false);
}
};
const handleNext = () => {
if (activeStep === 0 && product) {
loadVariants();
} else if (activeStep === 2) {
loadAddresses();
}
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleQuantityChange = (delta: number) => {
const newQuantity = quantity + delta;
if (newQuantity >= 1) {
setQuantity(newQuantity);
}
};
const handleAddAddress = async () => {
try {
const addedAddress = await clientApi.post('/addresses', newAddress);
setAddresses([...addresses, addedAddress.data]);
setSelectedAddress(addedAddress.data);
setShowAddressDialog(false);
setNewAddress({
addressType: 'shipping',
firstName: '',
lastName: '',
company: '',
addressLine1: '',
addressLine2: '',
apartmentNumber: '',
buildingNumber: '',
floor: '',
city: '',
state: '',
postalCode: '',
country: '',
phoneNumber: '',
instructions: '',
isDefault: false,
isActive: true
});
} catch (error) {
console.error('Failed to add address:', error);
}
};
const handlePlaceOrder = async () => {
if (!selectedVariant || !selectedAddress) return;
const orderData = {
productId: product!.id,
productVariantId: selectedVariant.id,
quantity: quantity,
addressId: selectedAddress.id,
totalPrice: selectedVariant.price * quantity
};
try {
setLoading(true);
await clientApi.post('/orders', orderData);
router.push('/orders/success');
} catch (error) {
console.error('Failed to place order:', error);
} finally {
setLoading(false);
}
};
const getTotalPrice = () => {
if (!selectedVariant) return 0;
return selectedVariant.price * quantity;
};
const canProceed = () => {
switch (activeStep) {
case 0: return product !== null;
case 1: return selectedVariant !== null;
case 2: return quantity > 0;
case 3: return selectedAddress !== null;
default: return true;
}
};
const renderStepContent = () => {
switch (activeStep) {
case 0:
return (
<Fade in={true}>
<Box>
{product && (
<Card>
<CardMedia
component="img"
height="400"
image={product.imageUrl}
alt={product.name}
/>
<CardContent>
<Typography variant="h4" gutterBottom>
{product.name}
</Typography>
<Typography variant="body1" color="text.secondary" paragraph>
{product.description}
</Typography>
<Typography variant="h5" color="primary">
${product.basePrice.toFixed(2)}
</Typography>
<Box mt={2}>
<Chip label={product.category.name} color="primary" variant="outlined" />
{product.isCustomizable && <Chip label="Customizable" color="secondary" sx={{ ml: 1 }} />}
</Box>
</CardContent>
</Card>
)}
</Box>
</Fade>
);
case 1:
return (
<Fade in={true}>
<Box>
<Typography variant="h5" gutterBottom>
Select Variant
</Typography>
<Grid container spacing={3}>
{variants.map((variant) => (
<Grid size={{ xs:12, sm:6, md:4 }} key={variant.id}>
<Card
sx={{
cursor: 'pointer',
border: selectedVariant?.id === variant.id ? 2 : 1,
borderColor: selectedVariant?.id === variant.id ? 'primary.main' : 'grey.300'
}}
onClick={() => setSelectedVariant(variant)}
>
<CardMedia
component="img"
height="200"
image={variant.imageUrl}
alt={`${variant.size} - ${variant.color}`}
/>
<CardContent>
<Typography variant="h6">
{variant.size} - {variant.color}
</Typography>
<Typography variant="body2" color="text.secondary">
SKU: {variant.sku}
</Typography>
<Typography variant="h6" color="primary">
${variant.price.toFixed(2)}
</Typography>
<Typography variant="body2" color={variant.stockQuantity > 0 ? 'success.main' : 'error.main'}>
{variant.stockQuantity > 0 ? `${variant.stockQuantity} in stock` : 'Out of stock'}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Box>
</Fade>
);
case 2:
return (
<Fade in={true}>
<Box>
<Typography variant="h5" gutterBottom>
Choose Quantity
</Typography>
{selectedVariant && (
<Card>
<CardContent>
<Grid container spacing={3} alignItems="center">
<Grid size={{ xs:12, md:6 }}>
<Box display="flex" alignItems="center" gap={2}>
<img
src={selectedVariant.imageUrl}
alt={selectedVariant.size}
style={{ width: 80, height: 80, objectFit: 'cover', borderRadius: 8 }}
/>
<Box>
<Typography variant="h6">
{selectedVariant.product.name}
</Typography>
<Typography variant="body2" color="text.secondary">
{selectedVariant.size} - {selectedVariant.color}
</Typography>
<Typography variant="h6" color="primary">
${selectedVariant.price.toFixed(2)} each
</Typography>
</Box>
</Box>
</Grid>
<Grid size={{ xs:12, md:6 }}>
<Box display="flex" alignItems="center" justifyContent="center" gap={2}>
<IconButton
onClick={() => handleQuantityChange(-1)}
disabled={quantity <= 1}
>
<RemoveIcon />
</IconButton>
<TextField
value={quantity}
onChange={(e) => {
const val = parseInt(e.target.value) || 1;
if (val >= 1) setQuantity(val);
}}
inputProps={{
style: { textAlign: 'center', fontSize: '1.2rem' },
min: 1
}}
sx={{ width: 80 }}
/>
<IconButton onClick={() => handleQuantityChange(1)}>
<AddIcon />
</IconButton>
</Box>
</Grid>
</Grid>
<Box mt={3} textAlign="center">
<Typography variant="h4" color="primary">
Total: ${getTotalPrice().toFixed(2)}
</Typography>
</Box>
</CardContent>
</Card>
)}
</Box>
</Fade>
);
case 3:
return (
<Fade in={true}>
<Box>
<Typography variant="h5" gutterBottom>
Select Delivery Address
</Typography>
<FormControl component="fieldset" sx={{ width: '100%' }}>
<RadioGroup
value={selectedAddress?.id || ''}
onChange={(e) => {
const addr = addresses.find(a => a.id === e.target.value);
setSelectedAddress(addr || null);
}}
>
<Grid container spacing={2}>
{addresses.map((address) => (
<Grid size={{ xs:12, md:6 }} key={address.id}>
<Card sx={{ position: 'relative' }}>
<CardContent>
<FormControlLabel
value={address.id}
control={<Radio />}
label=""
sx={{ position: 'absolute', top: 8, right: 8 }}
/>
<Box display="flex" alignItems="flex-start" gap={1} mb={1}>
<LocationIcon color="primary" />
<Typography variant="h6">
{address.firstName} {address.lastName}
</Typography>
{address.isDefault && <Chip label="Default" size="small" color="primary" />}
</Box>
{address.company && (
<Typography variant="body2" color="text.secondary">
{address.company}
</Typography>
)}
<Typography variant="body2">
{address.addressLine1}
</Typography>
{address.addressLine2 && (
<Typography variant="body2">
{address.addressLine2}
</Typography>
)}
<Typography variant="body2">
{address.city}, {address.state} {address.postalCode}
</Typography>
<Typography variant="body2">
{address.country}
</Typography>
{address.phoneNumber && (
<Typography variant="body2" color="text.secondary">
Phone: {address.phoneNumber}
</Typography>
)}
</CardContent>
</Card>
</Grid>
))}
<Grid size={{ xs:12, md:6 }}>
<Card
sx={{
cursor: 'pointer',
border: '2px dashed',
borderColor: 'primary.main',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: 200
}}
onClick={() => setShowAddressDialog(true)}
>
<CardContent>
<Box textAlign="center">
<AddLocationIcon sx={{ fontSize: 48, color: 'primary.main', mb: 1 }} />
<Typography variant="h6" color="primary">
Add New Address
</Typography>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</RadioGroup>
</FormControl>
</Box>
</Fade>
);
case 4:
return (
<Fade in={true}>
<Box>
<Typography variant="h5" gutterBottom>
Review Your Order
</Typography>
<Grid container spacing={3}>
<Grid size={{ xs:12, md:8 }}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Order Summary
</Typography>
{selectedVariant && (
<Box display="flex" alignItems="center" gap={2} mb={2}>
<img
src={selectedVariant.imageUrl}
alt={selectedVariant.size}
style={{ width: 60, height: 60, objectFit: 'cover', borderRadius: 8 }}
/>
<Box flex={1}>
<Typography variant="subtitle1">
{selectedVariant.product.name}
</Typography>
<Typography variant="body2" color="text.secondary">
{selectedVariant.size} - {selectedVariant.color}
</Typography>
<Typography variant="body2">
Quantity: {quantity}
</Typography>
</Box>
<Typography variant="h6">
${getTotalPrice().toFixed(2)}
</Typography>
</Box>
)}
</CardContent>
</Card>
</Grid>
<Grid size={{ xs:12, md:4 }}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Delivery Address
</Typography>
{selectedAddress && (
<Box>
<Typography variant="subtitle2">
{selectedAddress.firstName} {selectedAddress.lastName}
</Typography>
{selectedAddress.company && (
<Typography variant="body2" color="text.secondary">
{selectedAddress.company}
</Typography>
)}
<Typography variant="body2">
{selectedAddress.addressLine1}
</Typography>
{selectedAddress.addressLine2 && (
<Typography variant="body2">
{selectedAddress.addressLine2}
</Typography>
)}
<Typography variant="body2">
{selectedAddress.city}, {selectedAddress.state} {selectedAddress.postalCode}
</Typography>
<Typography variant="body2">
{selectedAddress.country}
</Typography>
</Box>
)}
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
</Fade>
);
default:
return null;
}
};
if (loading && !product) {
return (
<Container maxWidth="lg" sx={{ mt: 4, display: 'flex', justifyContent: 'center' }}>
<CircularProgress />
</Container>
);
}
return (
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
<Paper sx={{ p: 3 }}>
<Stepper activeStep={activeStep} sx={{ mb: 4 }}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
<Box sx={{ mb: 4 }}>
{renderStepContent()}
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Button
color="inherit"
disabled={activeStep === 0}
onClick={handleBack}
sx={{ mr: 1 }}
>
Back
</Button>
<Button
variant="contained"
onClick={activeStep === steps.length - 1 ? handlePlaceOrder : handleNext}
disabled={!canProceed() || loading}
>
{loading ? (
<CircularProgress size={24} />
) : activeStep === steps.length - 1 ? (
'Place Order'
) : (
'Next'
)}
</Button>
</Box>
</Paper>
<Dialog open={showAddressDialog} onClose={() => setShowAddressDialog(false)} maxWidth="md" fullWidth>
<DialogTitle>Add New Address</DialogTitle>
<DialogContent>
<Grid container spacing={2} sx={{ mt: 1 }}>
<Grid size={{ xs:12, sm:6 }}>
<TextField
fullWidth
label="First Name"
value={newAddress.firstName}
onChange={(e) => setNewAddress({...newAddress, firstName: e.target.value})}
/>
</Grid>
<Grid size={{ xs:12, sm:6 }}>
<TextField
fullWidth
label="Last Name"
value={newAddress.lastName}
onChange={(e) => setNewAddress({...newAddress, lastName: e.target.value})}
/>
</Grid>
<Grid size={{ xs:12, sm:6 }}>
<TextField
fullWidth
label="Company (Optional)"
value={newAddress.company}
onChange={(e) => setNewAddress({...newAddress, company: e.target.value})}
/>
</Grid>
<Grid size={{ xs:12, sm:6 }}>
<TextField
fullWidth
label="Address Line 1"
value={newAddress.addressLine1}
onChange={(e) => setNewAddress({...newAddress, addressLine1: e.target.value})}
/>
</Grid>
<Grid size={{ xs:12, sm:6 }}>
<TextField
fullWidth
label="Address Line 2 (Optional)"
value={newAddress.addressLine2}
onChange={(e) => setNewAddress({...newAddress, addressLine2: e.target.value})}
/>
</Grid>
<Grid size={{ xs:12, sm:6 }}>
<TextField
fullWidth
label="Apartment #"
value={newAddress.apartmentNumber}
onChange={(e) => setNewAddress({...newAddress, apartmentNumber: e.target.value})}
/>
</Grid>
<Grid size={{ xs:12, sm:6 }}>
<TextField
fullWidth
label="Building #"
value={newAddress.buildingNumber}
onChange={(e) => setNewAddress({...newAddress, buildingNumber: e.target.value})}
/>
</Grid>
<Grid size={{ xs:12, sm:6 }}>
<TextField
fullWidth
label="Floor"
value={newAddress.floor}
onChange={(e) => setNewAddress({...newAddress, floor: e.target.value})}
/>
</Grid>
<Grid size={{ xs:12, sm:6 }}>
<TextField
fullWidth
label="City"
value={newAddress.city}
onChange={(e) => setNewAddress({...newAddress, city: e.target.value})}
/>
</Grid>
<Grid size={{ xs:12, sm:6 }}>
<TextField
fullWidth
label="State"
value={newAddress.state}
onChange={(e) => setNewAddress({...newAddress, state: e.target.value})}
/>
</Grid>
<Grid size={{ xs:12, sm:6 }}>
<TextField
fullWidth
label="Postal Code"
value={newAddress.postalCode}
onChange={(e) => setNewAddress({...newAddress, postalCode: e.target.value})}
/>
</Grid>
<Grid size={{ xs:12, sm:6 }}>
<TextField
fullWidth
label="Country"
value={newAddress.country}
onChange={(e) => setNewAddress({...newAddress, country: e.target.value})}
/>
</Grid>
<Grid size={{ xs:12 }}>
<TextField
fullWidth
label="Phone Number"
value={newAddress.phoneNumber}
onChange={(e) => setNewAddress({...newAddress, phoneNumber: e.target.value})}
/>
</Grid>
<Grid size={{ xs:12 }}>
<TextField
fullWidth
multiline
rows={3}
label="Delivery Instructions (Optional)"
value={newAddress.instructions}
onChange={(e) => setNewAddress({...newAddress, instructions: e.target.value})}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowAddressDialog(false)}>Cancel</Button>
<Button
onClick={handleAddAddress}
variant="contained"
disabled={!newAddress.firstName || !newAddress.lastName || !newAddress.addressLine1 || !newAddress.city}
>
Add Address
</Button>
</DialogActions>
</Dialog>
</Container>
);
}