Merge pull request #15 from bytegrip/dev
Dev
This commit was merged in pull request #15.
This commit is contained in:
63
src/Imprink.Application/Commands/Addresses/CreateAddress.cs
Normal file
63
src/Imprink.Application/Commands/Addresses/CreateAddress.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
26
src/Imprink.Application/Commands/Addresses/GetMyAddresses.cs
Normal file
26
src/Imprink.Application/Commands/Addresses/GetMyAddresses.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
95
src/Imprink.Application/Commands/Orders/CreateOrder.cs
Normal file
95
src/Imprink.Application/Commands/Orders/CreateOrder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
36
src/Imprink.Application/Commands/Orders/GetOrderById.cs
Normal file
36
src/Imprink.Application/Commands/Orders/GetOrderById.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
38
src/Imprink.Application/Commands/Orders/GetOrdersByUserId.cs
Normal file
38
src/Imprink.Application/Commands/Orders/GetOrdersByUserId.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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 () =>
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
51
src/Imprink.Application/Commands/Products/GetProductById.cs
Normal file
51
src/Imprink.Application/Commands/Products/GetProductById.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
@@ -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
|
||||
{
|
||||
@@ -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);
|
||||
22
src/Imprink.Application/Commands/Users/GetAllRoles.cs
Normal file
22
src/Imprink.Application/Commands/Users/GetAllRoles.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.");
|
||||
@@ -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.");
|
||||
@@ -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);
|
||||
@@ -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");
|
||||
24
src/Imprink.Application/Dtos/AddressDto.cs
Normal file
24
src/Imprink.Application/Dtos/AddressDto.cs
Normal 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; }
|
||||
}
|
||||
24
src/Imprink.Application/Dtos/OrderAddressDto.cs
Normal file
24
src/Imprink.Application/Dtos/OrderAddressDto.cs
Normal 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; }
|
||||
}
|
||||
28
src/Imprink.Application/Dtos/OrderDto.cs
Normal file
28
src/Imprink.Application/Dtos/OrderDto.cs
Normal 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; }
|
||||
}
|
||||
8
src/Imprink.Application/Dtos/OrderStatusDto.cs
Normal file
8
src/Imprink.Application/Dtos/OrderStatusDto.cs
Normal 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; }
|
||||
}
|
||||
8
src/Imprink.Application/Dtos/ShippingStatusDto.cs
Normal file
8
src/Imprink.Application/Dtos/ShippingStatusDto.cs
Normal 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; }
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -19,8 +19,4 @@
|
||||
<ProjectReference Include="..\Imprink.Domain\Imprink.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Commands\Orders\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
23
src/Imprink.Application/Mappings/AddressMappingProfile.cs
Normal file
23
src/Imprink.Application/Mappings/AddressMappingProfile.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
36
src/Imprink.Application/Mappings/OrderMappingProfile.cs
Normal file
36
src/Imprink.Application/Mappings/OrderMappingProfile.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,5 @@ namespace Imprink.Application.Services;
|
||||
|
||||
public interface ICurrentUserService
|
||||
{
|
||||
string? GetCurrentUserId();
|
||||
string GetCurrentUserId();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
22
src/Imprink.Domain/Repositories/IAddressRepository.cs
Normal file
22
src/Imprink.Domain/Repositories/IAddressRepository.cs
Normal 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);
|
||||
}
|
||||
16
src/Imprink.Domain/Repositories/IOrderAddressRepository.cs
Normal file
16
src/Imprink.Domain/Repositories/IOrderAddressRepository.cs
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using Imprink.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
@@ -5,7 +6,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
namespace Imprink.Infrastructure.Configuration;
|
||||
|
||||
public class OrderConfiguration : EntityBaseConfiguration<Order>
|
||||
{
|
||||
{
|
||||
public override void Configure(EntityTypeBuilder<Order> builder)
|
||||
{
|
||||
base.Configure(builder);
|
||||
@@ -17,23 +18,41 @@ public class OrderConfiguration : EntityBaseConfiguration<Order>
|
||||
builder.Property(o => o.OrderDate)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(o => o.TotalPrice)
|
||||
builder.Property(o => o.Amount)
|
||||
.IsRequired()
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
builder.Property(o => o.Quantity)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(1);
|
||||
|
||||
builder.Property(o => o.ProductId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(o => o.OrderStatusId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(o => o.ShippingStatusId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(o => o.OrderNumber)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50);
|
||||
|
||||
builder.Property(o => o.Notes)
|
||||
.HasMaxLength(1000);
|
||||
|
||||
builder.Property(o => o.MerchantId)
|
||||
.HasMaxLength(450);
|
||||
|
||||
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.Property(o => o.CustomizationImageUrl)
|
||||
.HasMaxLength(1000);
|
||||
|
||||
builder.Property(o => o.CustomizationDescription)
|
||||
.HasMaxLength(2000);
|
||||
|
||||
builder.HasOne(o => o.OrderStatus)
|
||||
.WithMany(os => os.Orders)
|
||||
.HasForeignKey(o => o.OrderStatusId)
|
||||
@@ -52,16 +71,26 @@ public class OrderConfiguration : EntityBaseConfiguration<Order>
|
||||
builder.HasOne(o => o.User)
|
||||
.WithMany(u => u.Orders)
|
||||
.HasForeignKey(o => o.UserId)
|
||||
.HasPrincipalKey(u => u.Id)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(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.OrderNumber)
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Order_OrderNumber");
|
||||
|
||||
builder.HasIndex(o => o.OrderDate)
|
||||
.HasDatabaseName("IX_Order_OrderDate");
|
||||
|
||||
@@ -71,7 +100,22 @@ public class OrderConfiguration : EntityBaseConfiguration<Order>
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
168
src/Imprink.Infrastructure/Repositories/AddressRepository.cs
Normal file
168
src/Imprink.Infrastructure/Repositories/AddressRepository.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
var order = await GetByIdAsync(id, cancellationToken);
|
||||
if (order != null)
|
||||
public async Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
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 UpdateShippingStatusAsync(Guid orderId, int shippingStatusId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var order = await context.Orders
|
||||
.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken);
|
||||
|
||||
if (order != null)
|
||||
{
|
||||
order.ShippingStatusId = shippingStatusId;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<decimal> GetTotalRevenueAsync(CancellationToken cancellationToken = default)
|
||||
public async Task AssignMerchantAsync(Guid orderId, string merchantId, 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.MerchantId = merchantId;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<decimal> GetTotalRevenueByDateRangeAsync(
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
CancellationToken cancellationToken = default)
|
||||
public async Task UnassignMerchantAsync(Guid orderId, 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);
|
||||
|
||||
public async Task<int> GetOrderCountByStatusAsync(int orderStatusId, CancellationToken cancellationToken = default)
|
||||
if (order != null)
|
||||
{
|
||||
return await context.Orders
|
||||
.CountAsync(o => o.OrderStatusId == orderStatusId, cancellationToken);
|
||||
order.MerchantId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
49
src/Imprink.WebApi/Controllers/AddressesController.cs
Normal file
49
src/Imprink.WebApi/Controllers/AddressesController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
74
src/Imprink.WebApi/Controllers/OrdersController.cs
Normal file
74
src/Imprink.WebApi/Controllers/OrdersController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
800
ui/src/app/builder/[id]/page.tsx
Normal file
800
ui/src/app/builder/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user