feat: Implement new API endpoints, application logic, and domain repositories across FnbEngine, BookingService, and OrderService, alongside minor infrastructure updates.
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
// EN: Command to cancel an order.
|
||||
// VI: Command hủy order.
|
||||
|
||||
using MediatR;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to cancel an order with reason.
|
||||
/// VI: Command hủy order với lý do.
|
||||
/// </summary>
|
||||
public record CancelOrderCommand(
|
||||
Guid OrderId,
|
||||
Guid ShopId,
|
||||
string Reason
|
||||
) : IRequest<CancelOrderResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of cancel order command.
|
||||
/// VI: Kết quả command hủy order.
|
||||
/// </summary>
|
||||
public record CancelOrderResult(
|
||||
bool Success,
|
||||
string Status
|
||||
);
|
||||
@@ -0,0 +1,65 @@
|
||||
// EN: Handler for CancelOrderCommand.
|
||||
// VI: Handler cho CancelOrderCommand.
|
||||
|
||||
using MediatR;
|
||||
using OrderService.Domain.AggregatesModel.OrderAggregate;
|
||||
using OrderService.Domain.Exceptions;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for cancelling an order.
|
||||
/// VI: Handler hủy order.
|
||||
/// </summary>
|
||||
public class CancelOrderCommandHandler : IRequestHandler<CancelOrderCommand, CancelOrderResult>
|
||||
{
|
||||
private readonly IOrderRepository _orderRepository;
|
||||
private readonly ILogger<CancelOrderCommandHandler> _logger;
|
||||
|
||||
public CancelOrderCommandHandler(
|
||||
IOrderRepository orderRepository,
|
||||
ILogger<CancelOrderCommandHandler> logger)
|
||||
{
|
||||
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<CancelOrderResult> Handle(
|
||||
CancelOrderCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Cancelling order {OrderId} / VI: Hủy order {OrderId}",
|
||||
request.OrderId);
|
||||
|
||||
// EN: Load order
|
||||
// VI: Load order
|
||||
var order = await _orderRepository.GetByIdAsync(request.OrderId, cancellationToken);
|
||||
if (order == null)
|
||||
{
|
||||
throw new DomainException($"Order not found: {request.OrderId}");
|
||||
}
|
||||
|
||||
// EN: Verify shop ownership
|
||||
// VI: Xác minh quyền sở hữu shop
|
||||
if (order.ShopId != request.ShopId)
|
||||
{
|
||||
throw new DomainException("Order does not belong to this shop");
|
||||
}
|
||||
|
||||
// EN: Cancel order (domain validates status)
|
||||
// VI: Hủy order (domain validates status)
|
||||
order.Cancel(request.Reason);
|
||||
|
||||
// EN: Save changes (domain events will be dispatched)
|
||||
// VI: Lưu thay đổi (domain events sẽ được dispatch)
|
||||
await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"EN: Order cancelled successfully / VI: Hủy order thành công: {OrderId}, Reason: {Reason}",
|
||||
order.Id,
|
||||
request.Reason);
|
||||
|
||||
return new CancelOrderResult(true, order.Status.Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// EN: Command to complete an order.
|
||||
// VI: Command hoàn thành order.
|
||||
|
||||
using MediatR;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to mark order as completed.
|
||||
/// VI: Command đánh dấu order hoàn thành.
|
||||
/// </summary>
|
||||
public record CompleteOrderCommand(
|
||||
Guid OrderId,
|
||||
Guid ShopId
|
||||
) : IRequest<CompleteOrderResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of complete order command.
|
||||
/// VI: Kết quả command hoàn thành order.
|
||||
/// </summary>
|
||||
public record CompleteOrderResult(
|
||||
bool Success,
|
||||
string Status
|
||||
);
|
||||
@@ -0,0 +1,64 @@
|
||||
// EN: Handler for CompleteOrderCommand.
|
||||
// VI: Handler cho CompleteOrderCommand.
|
||||
|
||||
using MediatR;
|
||||
using OrderService.Domain.AggregatesModel.OrderAggregate;
|
||||
using OrderService.Domain.Exceptions;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for completing an order.
|
||||
/// VI: Handler hoàn thành order.
|
||||
/// </summary>
|
||||
public class CompleteOrderCommandHandler : IRequestHandler<CompleteOrderCommand, CompleteOrderResult>
|
||||
{
|
||||
private readonly IOrderRepository _orderRepository;
|
||||
private readonly ILogger<CompleteOrderCommandHandler> _logger;
|
||||
|
||||
public CompleteOrderCommandHandler(
|
||||
IOrderRepository orderRepository,
|
||||
ILogger<CompleteOrderCommandHandler> logger)
|
||||
{
|
||||
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<CompleteOrderResult> Handle(
|
||||
CompleteOrderCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Completing order {OrderId} / VI: Hoàn thành order {OrderId}",
|
||||
request.OrderId);
|
||||
|
||||
// EN: Load order
|
||||
// VI: Load order
|
||||
var order = await _orderRepository.GetByIdAsync(request.OrderId, cancellationToken);
|
||||
if (order == null)
|
||||
{
|
||||
throw new DomainException($"Order not found: {request.OrderId}");
|
||||
}
|
||||
|
||||
// EN: Verify shop ownership
|
||||
// VI: Xác minh quyền sở hữu shop
|
||||
if (order.ShopId != request.ShopId)
|
||||
{
|
||||
throw new DomainException("Order does not belong to this shop");
|
||||
}
|
||||
|
||||
// EN: Complete order (domain validates status)
|
||||
// VI: Hoàn thành order (domain validates status)
|
||||
order.MarkAsCompleted();
|
||||
|
||||
// EN: Save changes (domain events will be dispatched)
|
||||
// VI: Lưu thay đổi (domain events sẽ được dispatch)
|
||||
await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"EN: Order completed successfully / VI: Hoàn thành order thành công: {OrderId}",
|
||||
order.Id);
|
||||
|
||||
return new CompleteOrderResult(true, order.Status.Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// EN: Command to create a new order.
|
||||
// VI: Command tạo order mới.
|
||||
|
||||
using MediatR;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to create a new order with validation through strategies.
|
||||
/// VI: Command tạo order mới với validation qua strategies.
|
||||
/// </summary>
|
||||
public record CreateOrderCommand(
|
||||
Guid ShopId,
|
||||
Guid? CustomerId,
|
||||
List<OrderItemRequest> Items
|
||||
) : IRequest<CreateOrderResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Order item request.
|
||||
/// VI: Yêu cầu order item.
|
||||
/// </summary>
|
||||
public record OrderItemRequest(
|
||||
Guid ProductId,
|
||||
string ProductName,
|
||||
string ProductType,
|
||||
int Quantity,
|
||||
decimal UnitPrice
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of create order command.
|
||||
/// VI: Kết quả command tạo order.
|
||||
/// </summary>
|
||||
public record CreateOrderResult(
|
||||
Guid OrderId,
|
||||
decimal TotalAmount,
|
||||
string Status
|
||||
);
|
||||
@@ -0,0 +1,99 @@
|
||||
// EN: Handler for CreateOrderCommand.
|
||||
// VI: Handler cho CreateOrderCommand.
|
||||
|
||||
using MediatR;
|
||||
using OrderService.Domain.AggregatesModel.OrderAggregate;
|
||||
using OrderService.Domain.Strategies;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for creating a new order with strategy-based validation.
|
||||
/// VI: Handler tạo order mới với validation qua strategy.
|
||||
/// </summary>
|
||||
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, CreateOrderResult>
|
||||
{
|
||||
private readonly IOrderRepository _orderRepository;
|
||||
private readonly IEnumerable<ILineItemStrategy> _strategies;
|
||||
private readonly ILogger<CreateOrderCommandHandler> _logger;
|
||||
|
||||
public CreateOrderCommandHandler(
|
||||
IOrderRepository orderRepository,
|
||||
IEnumerable<ILineItemStrategy> strategies,
|
||||
ILogger<CreateOrderCommandHandler> logger)
|
||||
{
|
||||
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
|
||||
_strategies = strategies ?? throw new ArgumentNullException(nameof(strategies));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<CreateOrderResult> Handle(
|
||||
CreateOrderCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Creating order for shop {ShopId} / VI: Tạo order cho shop {ShopId}",
|
||||
request.ShopId);
|
||||
|
||||
// EN: Create order aggregate
|
||||
// VI: Tạo order aggregate
|
||||
var order = new Order(request.ShopId, request.CustomerId);
|
||||
|
||||
// EN: Add items to order
|
||||
// VI: Thêm items vào order
|
||||
foreach (var itemRequest in request.Items)
|
||||
{
|
||||
var orderItem = new OrderItem(
|
||||
itemRequest.ProductId,
|
||||
itemRequest.ProductName,
|
||||
itemRequest.ProductType,
|
||||
itemRequest.Quantity,
|
||||
itemRequest.UnitPrice);
|
||||
|
||||
order.AddItem(orderItem);
|
||||
}
|
||||
|
||||
// EN: Validate all items through their strategies
|
||||
// VI: Validate tất cả items qua strategies của chúng
|
||||
foreach (var item in order.Items)
|
||||
{
|
||||
var strategy = GetStrategy(item.ProductType);
|
||||
var isValid = await strategy.ValidateAsync(item, request.ShopId, cancellationToken);
|
||||
|
||||
if (!isValid)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Validation failed for item {item.ProductName} (type: {item.ProductType})");
|
||||
}
|
||||
}
|
||||
|
||||
// EN: Mark order as validated after all items pass validation
|
||||
// VI: Đánh dấu order là validated sau khi tất cả items pass validation
|
||||
order.MarkAsValidated();
|
||||
|
||||
// EN: Save order
|
||||
// VI: Lưu order
|
||||
_orderRepository.Add(order);
|
||||
await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"EN: Order created successfully / VI: Tạo order thành công: {OrderId}",
|
||||
order.Id);
|
||||
|
||||
return new CreateOrderResult(
|
||||
order.Id,
|
||||
order.TotalAmount,
|
||||
order.Status.Name);
|
||||
}
|
||||
|
||||
private ILineItemStrategy GetStrategy(string productType)
|
||||
{
|
||||
var strategy = _strategies.FirstOrDefault(s => s.SupportedType == productType);
|
||||
if (strategy == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"EN: No strategy found for product type / VI: Không tìm thấy strategy cho loại sản phẩm: {productType}");
|
||||
}
|
||||
return strategy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// EN: Command to pay for an order.
|
||||
// VI: Command thanh toán order.
|
||||
|
||||
using MediatR;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to process payment and execute order.
|
||||
/// VI: Command xử lý thanh toán và thực thi order.
|
||||
/// </summary>
|
||||
public record PayOrderCommand(
|
||||
Guid OrderId,
|
||||
Guid ShopId
|
||||
) : IRequest<PayOrderResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of pay order command.
|
||||
/// VI: Kết quả command thanh toán order.
|
||||
/// </summary>
|
||||
public record PayOrderResult(
|
||||
bool Success,
|
||||
string Status
|
||||
);
|
||||
@@ -0,0 +1,96 @@
|
||||
// EN: Handler for PayOrderCommand.
|
||||
// VI: Handler cho PayOrderCommand.
|
||||
|
||||
using MediatR;
|
||||
using OrderService.Domain.AggregatesModel.OrderAggregate;
|
||||
using OrderService.Domain.Exceptions;
|
||||
using OrderService.Domain.Strategies;
|
||||
|
||||
namespace OrderService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for processing payment and executing order items via strategies.
|
||||
/// VI: Handler xử lý thanh toán và thực thi order items qua strategies.
|
||||
/// </summary>
|
||||
public class PayOrderCommandHandler : IRequestHandler<PayOrderCommand, PayOrderResult>
|
||||
{
|
||||
private readonly IOrderRepository _orderRepository;
|
||||
private readonly IEnumerable<ILineItemStrategy> _strategies;
|
||||
private readonly ILogger<PayOrderCommandHandler> _logger;
|
||||
|
||||
public PayOrderCommandHandler(
|
||||
IOrderRepository orderRepository,
|
||||
IEnumerable<ILineItemStrategy> strategies,
|
||||
ILogger<PayOrderCommandHandler> logger)
|
||||
{
|
||||
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
|
||||
_strategies = strategies ?? throw new ArgumentNullException(nameof(strategies));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<PayOrderResult> Handle(
|
||||
PayOrderCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Processing payment for order {OrderId} / VI: Xử lý thanh toán cho order {OrderId}",
|
||||
request.OrderId);
|
||||
|
||||
// EN: Load order
|
||||
// VI: Load order
|
||||
var order = await _orderRepository.GetByIdAsync(request.OrderId, cancellationToken);
|
||||
if (order == null)
|
||||
{
|
||||
throw new DomainException($"Order not found: {request.OrderId}");
|
||||
}
|
||||
|
||||
// EN: Verify shop ownership
|
||||
// VI: Xác minh quyền sở hữu shop
|
||||
if (order.ShopId != request.ShopId)
|
||||
{
|
||||
throw new DomainException("Order does not belong to this shop");
|
||||
}
|
||||
|
||||
// EN: Mark order as paid (will validate status internally)
|
||||
// VI: Đánh dấu order là đã thanh toán (sẽ validate status bên trong)
|
||||
order.MarkAsPaid();
|
||||
|
||||
// EN: Execute all items via their strategies
|
||||
// VI: Thực thi tất cả items qua strategies của chúng
|
||||
foreach (var item in order.Items)
|
||||
{
|
||||
var strategy = GetStrategy(item.ProductType);
|
||||
await strategy.ExecuteAsync(item, request.ShopId, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"EN: Executed item {ProductName} via {StrategyType} / VI: Đã thực thi item {ProductName} qua {StrategyType}",
|
||||
item.ProductName,
|
||||
strategy.GetType().Name);
|
||||
}
|
||||
|
||||
// EN: Mark order as processing
|
||||
// VI: Đánh dấu order là đang xử lý
|
||||
order.MarkAsProcessing();
|
||||
|
||||
// EN: Save changes
|
||||
// VI: Lưu thay đổi
|
||||
await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"EN: Payment processed successfully / VI: Xử lý thanh toán thành công: {OrderId}",
|
||||
order.Id);
|
||||
|
||||
return new PayOrderResult(true, order.Status.Name);
|
||||
}
|
||||
|
||||
private ILineItemStrategy GetStrategy(string productType)
|
||||
{
|
||||
var strategy = _strategies.FirstOrDefault(s => s.SupportedType == productType);
|
||||
if (strategy == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"EN: No strategy found for product type / VI: Không tìm thấy strategy cho loại sản phẩm: {productType}");
|
||||
}
|
||||
return strategy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// EN: DTOs for Order Service.
|
||||
// VI: DTOs cho Order Service.
|
||||
|
||||
namespace OrderService.API.Application.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Complete order details with items.
|
||||
/// VI: Chi tiết order đầy đủ với items.
|
||||
/// </summary>
|
||||
public record OrderDto(
|
||||
Guid Id,
|
||||
Guid ShopId,
|
||||
Guid? CustomerId,
|
||||
string Status,
|
||||
decimal TotalAmount,
|
||||
DateTime CreatedAt,
|
||||
DateTime? UpdatedAt,
|
||||
List<OrderItemDto> Items
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Order item details.
|
||||
/// VI: Chi tiết order item.
|
||||
/// </summary>
|
||||
public record OrderItemDto(
|
||||
Guid Id,
|
||||
Guid ProductId,
|
||||
string ProductName,
|
||||
string ProductType,
|
||||
int Quantity,
|
||||
decimal UnitPrice,
|
||||
decimal TotalPrice,
|
||||
string Status
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Order summary for list views.
|
||||
/// VI: Tóm tắt order cho danh sách.
|
||||
/// </summary>
|
||||
public record OrderSummaryDto(
|
||||
Guid Id,
|
||||
Guid ShopId,
|
||||
Guid? CustomerId,
|
||||
string Status,
|
||||
decimal TotalAmount,
|
||||
int ItemCount,
|
||||
DateTime CreatedAt
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Paged result wrapper.
|
||||
/// VI: Wrapper kết quả phân trang.
|
||||
/// </summary>
|
||||
public record PagedResult<T>(
|
||||
List<T> Items,
|
||||
int TotalCount,
|
||||
int PageSize,
|
||||
int CurrentPage
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: Total number of pages.
|
||||
/// VI: Tổng số trang.
|
||||
/// </summary>
|
||||
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Whether there are more pages.
|
||||
/// VI: Có thêm trang hay không.
|
||||
/// </summary>
|
||||
public bool HasNextPage => CurrentPage < TotalPages;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Whether there are previous pages.
|
||||
/// VI: Có trang trước hay không.
|
||||
/// </summary>
|
||||
public bool HasPreviousPage => CurrentPage > 1;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// EN: Query to get order by ID.
|
||||
// VI: Query lấy order theo ID.
|
||||
|
||||
using MediatR;
|
||||
using OrderService.API.Application.DTOs;
|
||||
|
||||
namespace OrderService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to get order details by ID.
|
||||
/// VI: Query lấy chi tiết order theo ID.
|
||||
/// </summary>
|
||||
public record GetOrderByIdQuery(
|
||||
Guid OrderId,
|
||||
Guid ShopId
|
||||
) : IRequest<OrderDto?>;
|
||||
@@ -0,0 +1,102 @@
|
||||
// EN: Handler for GetOrderByIdQuery using Dapper.
|
||||
// VI: Handler cho GetOrderByIdQuery dùng Dapper.
|
||||
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using MediatR;
|
||||
using OrderService.API.Application.DTOs;
|
||||
|
||||
namespace OrderService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for getting order by ID using optimized Dapper query.
|
||||
/// VI: Handler lấy order theo ID dùng Dapper query tối ưu.
|
||||
/// </summary>
|
||||
public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, OrderDto?>
|
||||
{
|
||||
private readonly IDbConnection _connection;
|
||||
private readonly ILogger<GetOrderByIdQueryHandler> _logger;
|
||||
|
||||
public GetOrderByIdQueryHandler(
|
||||
IDbConnection connection,
|
||||
ILogger<GetOrderByIdQueryHandler> logger)
|
||||
{
|
||||
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<OrderDto?> Handle(
|
||||
GetOrderByIdQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string orderSql = @"
|
||||
SELECT
|
||||
o.id AS Id,
|
||||
o.shop_id AS ShopId,
|
||||
o.customer_id AS CustomerId,
|
||||
os.name AS Status,
|
||||
o.total_amount AS TotalAmount,
|
||||
o.created_at AS CreatedAt,
|
||||
o.updated_at AS UpdatedAt
|
||||
FROM orders o
|
||||
INNER JOIN order_statuses os ON o.status_id = os.id
|
||||
WHERE o.id = @OrderId AND o.shop_id = @ShopId";
|
||||
|
||||
const string itemsSql = @"
|
||||
SELECT
|
||||
oi.id AS Id,
|
||||
oi.product_id AS ProductId,
|
||||
oi.product_name AS ProductName,
|
||||
oi.product_type AS ProductType,
|
||||
oi.quantity AS Quantity,
|
||||
oi.unit_price AS UnitPrice,
|
||||
(oi.quantity * oi.unit_price) AS TotalPrice,
|
||||
oi.status AS Status
|
||||
FROM order_items oi
|
||||
WHERE oi.order_id = @OrderId
|
||||
ORDER BY oi.id";
|
||||
|
||||
// EN: Query order header
|
||||
// VI: Query order header
|
||||
var order = await _connection.QueryFirstOrDefaultAsync<OrderDtoRecord>(
|
||||
orderSql,
|
||||
new { request.OrderId, request.ShopId });
|
||||
|
||||
if (order == null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"EN: Order not found / VI: Không tìm thấy order: {OrderId}",
|
||||
request.OrderId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// EN: Query order items
|
||||
// VI: Query order items
|
||||
var items = await _connection.QueryAsync<OrderItemDto>(
|
||||
itemsSql,
|
||||
new { request.OrderId });
|
||||
|
||||
return new OrderDto(
|
||||
order.Id,
|
||||
order.ShopId,
|
||||
order.CustomerId,
|
||||
order.Status,
|
||||
order.TotalAmount,
|
||||
order.CreatedAt,
|
||||
order.UpdatedAt,
|
||||
items.ToList()
|
||||
);
|
||||
}
|
||||
|
||||
// EN: Helper record for Dapper mapping
|
||||
// VI: Record helper cho Dapper mapping
|
||||
private record OrderDtoRecord(
|
||||
Guid Id,
|
||||
Guid ShopId,
|
||||
Guid? CustomerId,
|
||||
string Status,
|
||||
decimal TotalAmount,
|
||||
DateTime CreatedAt,
|
||||
DateTime? UpdatedAt
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// EN: Query to get orders by customer.
|
||||
// VI: Query lấy orders theo khách hàng.
|
||||
|
||||
using MediatR;
|
||||
using OrderService.API.Application.DTOs;
|
||||
|
||||
namespace OrderService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to get orders for a customer with pagination.
|
||||
/// VI: Query lấy orders cho khách hàng có phân trang.
|
||||
/// </summary>
|
||||
public record GetOrdersByCustomerQuery(
|
||||
Guid CustomerId,
|
||||
int Page = 1,
|
||||
int PageSize = 20
|
||||
) : IRequest<PagedResult<OrderSummaryDto>>;
|
||||
@@ -0,0 +1,70 @@
|
||||
// EN: Handler for GetOrdersByCustomerQuery using Dapper.
|
||||
// VI: Handler cho GetOrdersByCustomerQuery dùng Dapper.
|
||||
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using MediatR;
|
||||
using OrderService.API.Application.DTOs;
|
||||
|
||||
namespace OrderService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for getting customer orders using Dapper.
|
||||
/// VI: Handler lấy orders của khách hàng dùng Dapper.
|
||||
/// </summary>
|
||||
public class GetOrdersByCustomerQueryHandler : IRequestHandler<GetOrdersByCustomerQuery, PagedResult<OrderSummaryDto>>
|
||||
{
|
||||
private readonly IDbConnection _connection;
|
||||
private readonly ILogger<GetOrdersByCustomerQueryHandler> _logger;
|
||||
|
||||
public GetOrdersByCustomerQueryHandler(
|
||||
IDbConnection connection,
|
||||
ILogger<GetOrdersByCustomerQueryHandler> logger)
|
||||
{
|
||||
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<PagedResult<OrderSummaryDto>> Handle(
|
||||
GetOrdersByCustomerQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// EN: Get total count
|
||||
// VI: Lấy tổng số
|
||||
const string countSql = "SELECT COUNT(*) FROM orders WHERE customer_id = @CustomerId";
|
||||
var totalCount = await _connection.ExecuteScalarAsync<int>(countSql, new { request.CustomerId });
|
||||
|
||||
// EN: Get paged data
|
||||
// VI: Lấy dữ liệu phân trang
|
||||
const string sql = @"
|
||||
SELECT
|
||||
o.id AS Id,
|
||||
o.shop_id AS ShopId,
|
||||
o.customer_id AS CustomerId,
|
||||
os.name AS Status,
|
||||
o.total_amount AS TotalAmount,
|
||||
o.created_at AS CreatedAt,
|
||||
(SELECT COUNT(*) FROM order_items WHERE order_id = o.id) AS ItemCount
|
||||
FROM orders o
|
||||
INNER JOIN order_statuses os ON o.status_id = os.id
|
||||
WHERE o.customer_id = @CustomerId
|
||||
ORDER BY o.created_at DESC
|
||||
LIMIT @PageSize OFFSET @Offset";
|
||||
|
||||
var parameters = new
|
||||
{
|
||||
request.CustomerId,
|
||||
request.PageSize,
|
||||
Offset = (request.Page - 1) * request.PageSize
|
||||
};
|
||||
|
||||
var orders = await _connection.QueryAsync<OrderSummaryDto>(sql, parameters);
|
||||
|
||||
return new PagedResult<OrderSummaryDto>(
|
||||
orders.ToList(),
|
||||
totalCount,
|
||||
request.PageSize,
|
||||
request.Page
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// EN: Query to list orders by shop.
|
||||
// VI: Query liệt kê orders theo shop.
|
||||
|
||||
using MediatR;
|
||||
using OrderService.API.Application.DTOs;
|
||||
|
||||
namespace OrderService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to list orders by shop with filtering and pagination.
|
||||
/// VI: Query liệt kê orders theo shop có lọc và phân trang.
|
||||
/// </summary>
|
||||
public record ListOrdersByShopQuery(
|
||||
Guid ShopId,
|
||||
string? Status,
|
||||
DateTime? FromDate,
|
||||
DateTime? ToDate,
|
||||
int Page = 1,
|
||||
int PageSize = 20
|
||||
) : IRequest<PagedResult<OrderSummaryDto>>;
|
||||
@@ -0,0 +1,100 @@
|
||||
// EN: Handler for ListOrdersByShopQuery using Dapper.
|
||||
// VI: Handler cho ListOrdersByShopQuery dùng Dapper.
|
||||
|
||||
using System.Data;
|
||||
using System.Text;
|
||||
using Dapper;
|
||||
using MediatR;
|
||||
using OrderService.API.Application.DTOs;
|
||||
|
||||
namespace OrderService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for listing orders by shop with filtering using Dapper.
|
||||
/// VI: Handler liệt kê orders theo shop với lọc dùng Dapper.
|
||||
/// </summary>
|
||||
public class ListOrdersByShopQueryHandler : IRequestHandler<ListOrdersByShopQuery, PagedResult<OrderSummaryDto>>
|
||||
{
|
||||
private readonly IDbConnection _connection;
|
||||
private readonly ILogger<ListOrdersByShopQueryHandler> _logger;
|
||||
|
||||
public ListOrdersByShopQueryHandler(
|
||||
IDbConnection connection,
|
||||
ILogger<ListOrdersByShopQueryHandler> logger)
|
||||
{
|
||||
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<PagedResult<OrderSummaryDto>> Handle(
|
||||
ListOrdersByShopQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var (whereClause, parameters) = BuildWhereClause(request);
|
||||
|
||||
// EN: Get total count
|
||||
// VI: Lấy tổng số
|
||||
var countSql = $"SELECT COUNT(*) FROM orders o INNER JOIN order_statuses os ON o.status_id = os.id {whereClause}";
|
||||
var totalCount = await _connection.ExecuteScalarAsync<int>(countSql, parameters);
|
||||
|
||||
// EN: Get paged data
|
||||
// VI: Lấy dữ liệu phân trang
|
||||
var sql = $@"
|
||||
SELECT
|
||||
o.id AS Id,
|
||||
o.shop_id AS ShopId,
|
||||
o.customer_id AS CustomerId,
|
||||
os.name AS Status,
|
||||
o.total_amount AS TotalAmount,
|
||||
o.created_at AS CreatedAt,
|
||||
(SELECT COUNT(*) FROM order_items WHERE order_id = o.id) AS ItemCount
|
||||
FROM orders o
|
||||
INNER JOIN order_statuses os ON o.status_id = os.id
|
||||
{whereClause}
|
||||
ORDER BY o.created_at DESC
|
||||
LIMIT @PageSize OFFSET @Offset";
|
||||
|
||||
parameters.Add("PageSize", request.PageSize);
|
||||
parameters.Add("Offset", (request.Page - 1) * request.PageSize);
|
||||
|
||||
var orders = await _connection.QueryAsync<OrderSummaryDto>(sql, parameters);
|
||||
|
||||
return new PagedResult<OrderSummaryDto>(
|
||||
orders.ToList(),
|
||||
totalCount,
|
||||
request.PageSize,
|
||||
request.Page
|
||||
);
|
||||
}
|
||||
|
||||
private static (string whereClause, DynamicParameters parameters) BuildWhereClause(ListOrdersByShopQuery request)
|
||||
{
|
||||
var conditions = new List<string> { "o.shop_id = @ShopId" };
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add("ShopId", request.ShopId);
|
||||
|
||||
if (!string.IsNullOrEmpty(request.Status))
|
||||
{
|
||||
conditions.Add("os.name = @Status");
|
||||
parameters.Add("Status", request.Status);
|
||||
}
|
||||
|
||||
if (request.FromDate.HasValue)
|
||||
{
|
||||
conditions.Add("o.created_at >= @FromDate");
|
||||
parameters.Add("FromDate", request.FromDate.Value);
|
||||
}
|
||||
|
||||
if (request.ToDate.HasValue)
|
||||
{
|
||||
conditions.Add("o.created_at <= @ToDate");
|
||||
parameters.Add("ToDate", request.ToDate.Value);
|
||||
}
|
||||
|
||||
var whereClause = conditions.Count > 0
|
||||
? "WHERE " + string.Join(" AND ", conditions)
|
||||
: string.Empty;
|
||||
|
||||
return (whereClause, parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// EN: Strategy for PreparedFood products (F&B).
|
||||
// VI: Strategy cho sản phẩm PreparedFood (F&B).
|
||||
|
||||
using OrderService.Domain.AggregatesModel.OrderAggregate;
|
||||
using OrderService.Domain.Strategies;
|
||||
using OrderService.API.Infrastructure.HttpClients;
|
||||
|
||||
namespace OrderService.API.Application.Strategies;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Strategy for PreparedFood products - routes to F&B Engine.
|
||||
/// VI: Strategy cho sản phẩm PreparedFood - route đến F&B Engine.
|
||||
/// </summary>
|
||||
public class FnbStrategy : ILineItemStrategy
|
||||
{
|
||||
private readonly FnbEngineClient _fnbClient;
|
||||
private readonly ILogger<FnbStrategy> _logger;
|
||||
|
||||
public string SupportedType => "PreparedFood";
|
||||
|
||||
public FnbStrategy(
|
||||
FnbEngineClient fnbClient,
|
||||
ILogger<FnbStrategy> logger)
|
||||
{
|
||||
_fnbClient = fnbClient ?? throw new ArgumentNullException(nameof(fnbClient));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public Task<bool> ValidateAsync(
|
||||
OrderItem item,
|
||||
Guid shopId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Validating PreparedFood product / VI: Validate sản phẩm PreparedFood: {ProductName}",
|
||||
item.ProductName);
|
||||
|
||||
// EN: F&B items are always valid (no inventory check)
|
||||
// VI: F&B items luôn hợp lệ (không cần kiểm tra kho)
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(
|
||||
OrderItem item,
|
||||
Guid shopId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Executing PreparedFood product / VI: Thực thi sản phẩm PreparedFood: {ProductName}",
|
||||
item.ProductName);
|
||||
|
||||
// EN: Create kitchen ticket
|
||||
// VI: Tạo phiếu bếp
|
||||
var ticketId = await _fnbClient.CreateKitchenTicketAsync(
|
||||
item.ProductId,
|
||||
item.ProductName,
|
||||
shopId,
|
||||
item.Quantity,
|
||||
null, // Notes can be added later
|
||||
cancellationToken);
|
||||
|
||||
if (ticketId == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"EN: Failed to create kitchen ticket / VI: Tạo phiếu bếp thất bại: {item.ProductName}");
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"EN: Kitchen ticket created / VI: Phiếu bếp đã tạo: {ProductName}, TicketId: {TicketId}",
|
||||
item.ProductName,
|
||||
ticketId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// EN: Strategy for Physical products (Retail).
|
||||
// VI: Strategy cho sản phẩm Physical (Bán lẻ).
|
||||
|
||||
using OrderService.Domain.AggregatesModel.OrderAggregate;
|
||||
using OrderService.Domain.Strategies;
|
||||
using OrderService.API.Infrastructure.HttpClients;
|
||||
|
||||
namespace OrderService.API.Application.Strategies;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Strategy for Physical products - routes to Inventory Service.
|
||||
/// VI: Strategy cho sản phẩm Physical - route đến Inventory Service.
|
||||
/// </summary>
|
||||
public class RetailStrategy : ILineItemStrategy
|
||||
{
|
||||
private readonly InventoryServiceClient _inventoryClient;
|
||||
private readonly ILogger<RetailStrategy> _logger;
|
||||
|
||||
public string SupportedType => "Physical";
|
||||
|
||||
public RetailStrategy(
|
||||
InventoryServiceClient inventoryClient,
|
||||
ILogger<RetailStrategy> logger)
|
||||
{
|
||||
_inventoryClient = inventoryClient ?? throw new ArgumentNullException(nameof(inventoryClient));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateAsync(
|
||||
OrderItem item,
|
||||
Guid shopId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Validating Physical product / VI: Validate sản phẩm Physical: {ProductName}",
|
||||
item.ProductName);
|
||||
|
||||
// EN: Check inventory availability
|
||||
// VI: Kiểm tra hàng tồn kho
|
||||
var isAvailable = await _inventoryClient.CheckStockAsync(
|
||||
item.ProductId,
|
||||
shopId,
|
||||
item.Quantity,
|
||||
cancellationToken);
|
||||
|
||||
if (!isAvailable)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"EN: Insufficient stock / VI: Không đủ hàng: {ProductName}, Quantity: {Quantity}",
|
||||
item.ProductName,
|
||||
item.Quantity);
|
||||
}
|
||||
|
||||
return isAvailable;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(
|
||||
OrderItem item,
|
||||
Guid shopId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Executing Physical product / VI: Thực thi sản phẩm Physical: {ProductName}",
|
||||
item.ProductName);
|
||||
|
||||
// EN: Deduct inventory
|
||||
// VI: Trừ hàng tồn kho
|
||||
var success = await _inventoryClient.DeductStockAsync(
|
||||
item.ProductId,
|
||||
shopId,
|
||||
item.Quantity,
|
||||
cancellationToken);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"EN: Failed to deduct stock / VI: Trừ kho thất bại: {item.ProductName}");
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"EN: Stock deducted successfully / VI: Trừ kho thành công: {ProductName}",
|
||||
item.ProductName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// EN: Strategy for Service products.
|
||||
// VI: Strategy cho sản phẩm Service.
|
||||
|
||||
using OrderService.Domain.AggregatesModel.OrderAggregate;
|
||||
using OrderService.Domain.Strategies;
|
||||
using OrderService.API.Infrastructure.HttpClients;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace OrderService.API.Application.Strategies;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Strategy for Service products - routes to Booking Service.
|
||||
/// VI: Strategy cho sản phẩm Service - route đến Booking Service.
|
||||
/// </summary>
|
||||
public class ServiceStrategy : ILineItemStrategy
|
||||
{
|
||||
private readonly BookingServiceClient _bookingClient;
|
||||
private readonly ILogger<ServiceStrategy> _logger;
|
||||
|
||||
public string SupportedType => "Service";
|
||||
|
||||
public ServiceStrategy(
|
||||
BookingServiceClient bookingClient,
|
||||
ILogger<ServiceStrategy> logger)
|
||||
{
|
||||
_bookingClient = bookingClient ?? throw new ArgumentNullException(nameof(bookingClient));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateAsync(
|
||||
OrderItem item,
|
||||
Guid shopId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Validating Service product / VI: Validate sản phẩm Service: {ProductName}",
|
||||
item.ProductName);
|
||||
|
||||
// EN: Parse metadata for appointment details
|
||||
// VI: Parse metadata cho chi tiết đặt lịch
|
||||
var metadata = ParseMetadata(item.Metadata);
|
||||
if (metadata == null)
|
||||
{
|
||||
_logger.LogWarning("EN: Invalid metadata for Service item / VI: Metadata không hợp lệ cho Service item");
|
||||
return false;
|
||||
}
|
||||
|
||||
// EN: Check availability
|
||||
// VI: Kiểm tra lịch trống
|
||||
var isAvailable = await _bookingClient.CheckAvailabilityAsync(
|
||||
item.ProductId,
|
||||
shopId,
|
||||
metadata.StartTime,
|
||||
metadata.DurationMinutes,
|
||||
cancellationToken);
|
||||
|
||||
if (!isAvailable)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"EN: Time slot not available / VI: Lịch không trống: {ProductName}, Time: {StartTime}",
|
||||
item.ProductName,
|
||||
metadata.StartTime);
|
||||
}
|
||||
|
||||
return isAvailable;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(
|
||||
OrderItem item,
|
||||
Guid shopId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Executing Service product / VI: Thực thi sản phẩm Service: {ProductName}",
|
||||
item.ProductName);
|
||||
|
||||
// EN: Parse metadata
|
||||
// VI: Parse metadata
|
||||
var metadata = ParseMetadata(item.Metadata);
|
||||
if (metadata == null)
|
||||
{
|
||||
throw new InvalidOperationException("EN: Invalid metadata / VI: Metadata không hợp lệ");
|
||||
}
|
||||
|
||||
// EN: Create appointment
|
||||
// VI: Tạo đặt lịch
|
||||
var appointmentId = await _bookingClient.CreateAppointmentAsync(
|
||||
item.ProductId,
|
||||
shopId,
|
||||
null, // Customer ID can be null for walk-in
|
||||
metadata.StartTime,
|
||||
metadata.DurationMinutes,
|
||||
cancellationToken);
|
||||
|
||||
if (appointmentId == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"EN: Failed to create appointment / VI: Tạo đặt lịch thất bại: {item.ProductName}");
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"EN: Appointment created / VI: Đặt lịch thành công: {ProductName}, AppointmentId: {AppointmentId}",
|
||||
item.ProductName,
|
||||
appointmentId);
|
||||
}
|
||||
|
||||
private ServiceMetadata? ParseMetadata(string? metadataJson)
|
||||
{
|
||||
if (string.IsNullOrEmpty(metadataJson))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<ServiceMetadata>(metadataJson);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Metadata for Service items.
|
||||
/// VI: Metadata cho Service items.
|
||||
/// </summary>
|
||||
public record ServiceMetadata(
|
||||
DateTime StartTime,
|
||||
int DurationMinutes
|
||||
);
|
||||
@@ -0,0 +1,56 @@
|
||||
// EN: Factory for resolving line item strategies.
|
||||
// VI: Factory để resolve line item strategies.
|
||||
|
||||
using OrderService.Domain.Strategies;
|
||||
|
||||
namespace OrderService.API.Application.Strategies;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Factory for getting the appropriate strategy based on product type.
|
||||
/// VI: Factory lấy strategy phù hợp dựa trên loại sản phẩm.
|
||||
/// </summary>
|
||||
public class StrategyFactory
|
||||
{
|
||||
private readonly IEnumerable<ILineItemStrategy> _strategies;
|
||||
private readonly ILogger<StrategyFactory> _logger;
|
||||
|
||||
public StrategyFactory(
|
||||
IEnumerable<ILineItemStrategy> strategies,
|
||||
ILogger<StrategyFactory> logger)
|
||||
{
|
||||
_strategies = strategies ?? throw new ArgumentNullException(nameof(strategies));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get strategy by product type.
|
||||
/// VI: Lấy strategy theo loại sản phẩm.
|
||||
/// </summary>
|
||||
public ILineItemStrategy GetStrategy(string productType)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"EN: Getting strategy for product type / VI: Lấy strategy cho loại sản phẩm: {ProductType}",
|
||||
productType);
|
||||
|
||||
var strategy = _strategies.FirstOrDefault(s => s.SupportedType.Equals(productType, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (strategy == null)
|
||||
{
|
||||
var availableTypes = string.Join(", ", _strategies.Select(s => s.SupportedType));
|
||||
throw new InvalidOperationException(
|
||||
$"EN: No strategy found for product type: {productType}. Available types: {availableTypes} / " +
|
||||
$"VI: Không tìm thấy strategy cho loại sản phẩm: {productType}. Các loại có sẵn: {availableTypes}");
|
||||
}
|
||||
|
||||
return strategy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get all available product types.
|
||||
/// VI: Lấy tất cả loại sản phẩm có sẵn.
|
||||
/// </summary>
|
||||
public IEnumerable<string> GetAvailableTypes()
|
||||
{
|
||||
return _strategies.Select(s => s.SupportedType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// EN: Validator for CancelOrderCommand.
|
||||
// VI: Validator cho CancelOrderCommand.
|
||||
|
||||
using FluentValidation;
|
||||
|
||||
namespace OrderService.API.Application.Validations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validator for CancelOrderCommand.
|
||||
/// VI: Validator cho CancelOrderCommand.
|
||||
/// </summary>
|
||||
public class CancelOrderCommandValidator : AbstractValidator<Commands.CancelOrderCommand>
|
||||
{
|
||||
public CancelOrderCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.OrderId)
|
||||
.NotEmpty()
|
||||
.WithMessage("EN: Order ID is required / VI: Order ID là bắt buộc");
|
||||
|
||||
RuleFor(x => x.ShopId)
|
||||
.NotEmpty()
|
||||
.WithMessage("EN: Shop ID is required / VI: Shop ID là bắt buộc");
|
||||
|
||||
RuleFor(x => x.Reason)
|
||||
.NotEmpty()
|
||||
.WithMessage("EN: Cancellation reason is required / VI: Lý do hủy là bắt buộc")
|
||||
.MaximumLength(500)
|
||||
.WithMessage("EN: Reason must not exceed 500 characters / VI: Lý do không được vượt quá 500 ký tự");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// EN: Validator for CreateOrderCommand.
|
||||
// VI: Validator cho CreateOrderCommand.
|
||||
|
||||
using FluentValidation;
|
||||
|
||||
namespace OrderService.API.Application.Validations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validator for CreateOrderCommand.
|
||||
/// VI: Validator cho CreateOrderCommand.
|
||||
/// </summary>
|
||||
public class CreateOrderCommandValidator : AbstractValidator<Commands.CreateOrderCommand>
|
||||
{
|
||||
public CreateOrderCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.ShopId)
|
||||
.NotEmpty()
|
||||
.WithMessage("EN: Shop ID is required / VI: Shop ID là bắt buộc");
|
||||
|
||||
RuleFor(x => x.Items)
|
||||
.NotEmpty()
|
||||
.WithMessage("EN: Order must have at least one item / VI: Order phải có ít nhất một item")
|
||||
.Must(items => items != null && items.Count > 0)
|
||||
.WithMessage("EN: Order must have at least one item / VI: Order phải có ít nhất một item");
|
||||
|
||||
RuleForEach(x => x.Items).SetValidator(new OrderItemRequestValidator());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validator for OrderItemRequest.
|
||||
/// VI: Validator cho OrderItemRequest.
|
||||
/// </summary>
|
||||
public class OrderItemRequestValidator : AbstractValidator<Commands.OrderItemRequest>
|
||||
{
|
||||
public OrderItemRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.ProductId)
|
||||
.NotEmpty()
|
||||
.WithMessage("EN: Product ID is required / VI: Product ID là bắt buộc");
|
||||
|
||||
RuleFor(x => x.ProductName)
|
||||
.NotEmpty()
|
||||
.WithMessage("EN: Product name is required / VI: Tên sản phẩm là bắt buộc")
|
||||
.MaximumLength(255)
|
||||
.WithMessage("EN: Product name must not exceed 255 characters / VI: Tên sản phẩm không được vượt quá 255 ký tự");
|
||||
|
||||
RuleFor(x => x.ProductType)
|
||||
.NotEmpty()
|
||||
.WithMessage("EN: Product type is required / VI: Loại sản phẩm là bắt buộc")
|
||||
.Must(type => new[] { "Physical", "Service", "PreparedFood" }.Contains(type))
|
||||
.WithMessage("EN: Invalid product type / VI: Loại sản phẩm không hợp lệ");
|
||||
|
||||
RuleFor(x => x.Quantity)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("EN: Quantity must be greater than 0 / VI: Số lượng phải lớn hơn 0");
|
||||
|
||||
RuleFor(x => x.UnitPrice)
|
||||
.GreaterThanOrEqualTo(0)
|
||||
.WithMessage("EN: Unit price must be greater than or equal to 0 / VI: Đơn giá phải lớn hơn hoặc bằng 0");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// EN: Validator for PayOrderCommand.
|
||||
// VI: Validator cho PayOrderCommand.
|
||||
|
||||
using FluentValidation;
|
||||
|
||||
namespace OrderService.API.Application.Validations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validator for PayOrderCommand.
|
||||
/// VI: Validator cho PayOrderCommand.
|
||||
/// </summary>
|
||||
public class PayOrderCommandValidator : AbstractValidator<Commands.PayOrderCommand>
|
||||
{
|
||||
public PayOrderCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.OrderId)
|
||||
.NotEmpty()
|
||||
.WithMessage("EN: Order ID is required / VI: Order ID là bắt buộc");
|
||||
|
||||
RuleFor(x => x.ShopId)
|
||||
.NotEmpty()
|
||||
.WithMessage("EN: Shop ID is required / VI: Shop ID là bắt buộc");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// EN: Admin Orders REST API Controller.
|
||||
// VI: Controller REST API cho Admin Orders.
|
||||
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OrderService.API.Application.DTOs;
|
||||
using OrderService.API.Application.Queries;
|
||||
|
||||
namespace OrderService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Admin Orders API Controller for administrative order management.
|
||||
/// VI: Controller API Admin Orders cho quản lý đơn hàng dạng admin.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/admin/orders")]
|
||||
public class AdminOrdersController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<AdminOrdersController> _logger;
|
||||
|
||||
public AdminOrdersController(
|
||||
IMediator mediator,
|
||||
ILogger<AdminOrdersController> logger)
|
||||
{
|
||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: List all orders with advanced filtering (Admin only).
|
||||
/// VI: Liệt kê tất cả orders với lọc nâng cao (chỉ Admin).
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResult<OrderSummaryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResult<OrderSummaryDto>>> ListAllOrders(
|
||||
[FromQuery] Guid? shopId = null,
|
||||
[FromQuery] Guid? customerId = null,
|
||||
[FromQuery] string? status = null,
|
||||
[FromQuery] DateTime? fromDate = null,
|
||||
[FromQuery] DateTime? toDate = null,
|
||||
[FromQuery] decimal? minAmount = null,
|
||||
[FromQuery] decimal? maxAmount = null,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// EN: For now, if shopId is provided, use ListOrdersByShopQuery
|
||||
// VI: Tạm thời, nếu shopId được cung cấp, dùng ListOrdersByShopQuery
|
||||
// TODO: Create AdminListOrdersQuery with more filters
|
||||
if (shopId.HasValue)
|
||||
{
|
||||
var query = new ListOrdersByShopQuery(
|
||||
shopId.Value,
|
||||
status,
|
||||
fromDate,
|
||||
toDate,
|
||||
page,
|
||||
pageSize);
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// EN: If customerId is provided
|
||||
// VI: Nếu customerId được cung cấp
|
||||
if (customerId.HasValue)
|
||||
{
|
||||
var query = new GetOrdersByCustomerQuery(customerId.Value, page, pageSize);
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// EN: Return empty for now
|
||||
// VI: Trả về rỗng tạm thời
|
||||
return Ok(new PagedResult<OrderSummaryDto>(
|
||||
new List<OrderSummaryDto>(),
|
||||
0,
|
||||
pageSize,
|
||||
page));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get order statistics (Admin only).
|
||||
/// VI: Lấy thống kê orders (chỉ Admin).
|
||||
/// </summary>
|
||||
[HttpGet("stats")]
|
||||
[ProducesResponseType(typeof(OrderStatsDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<OrderStatsDto>> GetOrderStats(
|
||||
[FromQuery] Guid? shopId = null,
|
||||
[FromQuery] DateTime? fromDate = null,
|
||||
[FromQuery] DateTime? toDate = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation("EN: Getting order stats / VI: Lấy thống kê orders");
|
||||
|
||||
// TODO: Implement GetOrderStatsQuery
|
||||
// EN: Return mock data for now
|
||||
// VI: Trả về dữ liệu giả tạm thời
|
||||
var stats = new OrderStatsDto(
|
||||
TotalOrders: 0,
|
||||
TotalRevenue: 0,
|
||||
AverageOrderValue: 0,
|
||||
OrdersByStatus: new Dictionary<string, int>()
|
||||
);
|
||||
|
||||
return Ok(stats);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Export orders to CSV (Admin only).
|
||||
/// VI: Xuất orders ra CSV (chỉ Admin).
|
||||
/// </summary>
|
||||
[HttpGet("export")]
|
||||
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> ExportOrders(
|
||||
[FromQuery] Guid? shopId = null,
|
||||
[FromQuery] DateTime? fromDate = null,
|
||||
[FromQuery] DateTime? toDate = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation("EN: Exporting orders / VI: Xuất orders");
|
||||
|
||||
// TODO: Implement export functionality
|
||||
// EN: Return empty CSV for now
|
||||
// VI: Trả về CSV rỗng tạm thời
|
||||
var csv = "OrderId,ShopId,CustomerId,Status,TotalAmount,CreatedAt\n";
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(csv);
|
||||
|
||||
return File(bytes, "text/csv", $"orders_{DateTime.UtcNow:yyyyMMdd}.csv");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Order statistics DTO.
|
||||
/// VI: DTO thống kê orders.
|
||||
/// </summary>
|
||||
public record OrderStatsDto(
|
||||
int TotalOrders,
|
||||
decimal TotalRevenue,
|
||||
decimal AverageOrderValue,
|
||||
Dictionary<string, int> OrdersByStatus
|
||||
);
|
||||
@@ -0,0 +1,199 @@
|
||||
// EN: Orders REST API Controller.
|
||||
// VI: Controller REST API cho Orders.
|
||||
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OrderService.API.Application.Commands;
|
||||
using OrderService.API.Application.DTOs;
|
||||
using OrderService.API.Application.Queries;
|
||||
|
||||
namespace OrderService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Orders API Controller for order management.
|
||||
/// VI: Controller API Orders cho quản lý đơn hàng.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/orders")]
|
||||
public class OrdersController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<OrdersController> _logger;
|
||||
|
||||
public OrdersController(
|
||||
IMediator mediator,
|
||||
ILogger<OrdersController> logger)
|
||||
{
|
||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create a new order.
|
||||
/// VI: Tạo order mới.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(CreateOrderResult), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<CreateOrderResult>> CreateOrder(
|
||||
[FromBody] CreateOrderCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Creating order for shop {ShopId} / VI: Tạo order cho shop {ShopId}",
|
||||
command.ShopId);
|
||||
|
||||
var result = await _mediator.Send(command, cancellationToken);
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetOrderById),
|
||||
new { id = result.OrderId },
|
||||
result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get order by ID.
|
||||
/// VI: Lấy order theo ID.
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<OrderDto>> GetOrderById(
|
||||
Guid id,
|
||||
[FromQuery] Guid shopId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new GetOrderByIdQuery(id, shopId);
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound(new { Message = $"Order with ID {id} not found" });
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: List orders by shop with filtering and pagination.
|
||||
/// VI: Liệt kê orders theo shop có lọc và phân trang.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResult<OrderSummaryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResult<OrderSummaryDto>>> ListOrdersByShop(
|
||||
[FromQuery] Guid shopId,
|
||||
[FromQuery] string? status = null,
|
||||
[FromQuery] DateTime? fromDate = null,
|
||||
[FromQuery] DateTime? toDate = null,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new ListOrdersByShopQuery(
|
||||
shopId,
|
||||
status,
|
||||
fromDate,
|
||||
toDate,
|
||||
page,
|
||||
pageSize);
|
||||
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Process payment for an order.
|
||||
/// VI: Xử lý thanh toán cho order.
|
||||
/// </summary>
|
||||
[HttpPost("{id}/pay")]
|
||||
[ProducesResponseType(typeof(PayOrderResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<PayOrderResult>> PayOrder(
|
||||
Guid id,
|
||||
[FromQuery] Guid shopId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Processing payment for order {OrderId} / VI: Xử lý thanh toán cho order {OrderId}",
|
||||
id);
|
||||
|
||||
var command = new PayOrderCommand(id, shopId);
|
||||
var result = await _mediator.Send(command, cancellationToken);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Cancel an order.
|
||||
/// VI: Hủy order.
|
||||
/// </summary>
|
||||
[HttpPost("{id}/cancel")]
|
||||
[ProducesResponseType(typeof(CancelOrderResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CancelOrderResult>> CancelOrder(
|
||||
Guid id,
|
||||
[FromQuery] Guid shopId,
|
||||
[FromBody] CancelOrderRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Cancelling order {OrderId} / VI: Hủy order {OrderId}",
|
||||
id);
|
||||
|
||||
var command = new CancelOrderCommand(id, shopId, request.Reason);
|
||||
var result = await _mediator.Send(command, cancellationToken);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Complete an order.
|
||||
/// VI: Hoàn thành order.
|
||||
/// </summary>
|
||||
[HttpPost("{id}/complete")]
|
||||
[ProducesResponseType(typeof(CompleteOrderResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CompleteOrderResult>> CompleteOrder(
|
||||
Guid id,
|
||||
[FromQuery] Guid shopId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Completing order {OrderId} / VI: Hoàn thành order {OrderId}",
|
||||
id);
|
||||
|
||||
var command = new CompleteOrderCommand(id, shopId);
|
||||
var result = await _mediator.Send(command, cancellationToken);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get orders by customer.
|
||||
/// VI: Lấy orders theo khách hàng.
|
||||
/// </summary>
|
||||
[HttpGet("customer/{customerId}")]
|
||||
[ProducesResponseType(typeof(PagedResult<OrderSummaryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResult<OrderSummaryDto>>> GetOrdersByCustomer(
|
||||
Guid customerId,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new GetOrdersByCustomerQuery(customerId, page, pageSize);
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request to cancel an order.
|
||||
/// VI: Yêu cầu hủy order.
|
||||
/// </summary>
|
||||
public record CancelOrderRequest(string Reason);
|
||||
@@ -0,0 +1,112 @@
|
||||
// EN: HTTP client for Booking Service.
|
||||
// VI: HTTP client cho Booking Service.
|
||||
|
||||
namespace OrderService.API.Infrastructure.HttpClients;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Client for calling Booking Service for appointment operations.
|
||||
/// VI: Client gọi Booking Service để thực hiện thao tác đặt lịch.
|
||||
/// </summary>
|
||||
public class BookingServiceClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<BookingServiceClient> _logger;
|
||||
|
||||
public BookingServiceClient(
|
||||
HttpClient httpClient,
|
||||
ILogger<BookingServiceClient> logger)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Check availability for service appointment.
|
||||
/// VI: Kiểm tra lịch trống cho dịch vụ.
|
||||
/// </summary>
|
||||
public async Task<bool> CheckAvailabilityAsync(
|
||||
Guid serviceId,
|
||||
Guid shopId,
|
||||
DateTime startTime,
|
||||
int durationMinutes,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Checking availability / VI: Kiểm tra lịch trống: Service={ServiceId}, Time={StartTime}",
|
||||
serviceId, startTime);
|
||||
|
||||
var response = await _httpClient.GetAsync(
|
||||
$"/api/v1/bookings/check-availability?serviceId={serviceId}&shopId={shopId}&startTime={startTime:O}&duration={durationMinutes}",
|
||||
cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<AvailabilityCheckResult>(cancellationToken);
|
||||
return result?.IsAvailable ?? false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "EN: Error checking availability / VI: Lỗi khi kiểm tra lịch");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create appointment reservation.
|
||||
/// VI: Tạo đặt lịch.
|
||||
/// </summary>
|
||||
public async Task<Guid?> CreateAppointmentAsync(
|
||||
Guid serviceId,
|
||||
Guid shopId,
|
||||
Guid? customerId,
|
||||
DateTime startTime,
|
||||
int durationMinutes,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Creating appointment / VI: Tạo đặt lịch: Service={ServiceId}, Time={StartTime}",
|
||||
serviceId, startTime);
|
||||
|
||||
var request = new CreateAppointmentRequest(
|
||||
serviceId,
|
||||
shopId,
|
||||
customerId,
|
||||
startTime,
|
||||
durationMinutes);
|
||||
|
||||
var response = await _httpClient.PostAsJsonAsync(
|
||||
"/api/v1/bookings/appointments",
|
||||
request,
|
||||
cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<CreateAppointmentResult>(cancellationToken);
|
||||
return result?.AppointmentId;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "EN: Error creating appointment / VI: Lỗi khi tạo đặt lịch");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record AvailabilityCheckResult(bool IsAvailable);
|
||||
public record CreateAppointmentRequest(
|
||||
Guid ServiceId,
|
||||
Guid ShopId,
|
||||
Guid? CustomerId,
|
||||
DateTime StartTime,
|
||||
int DurationMinutes);
|
||||
public record CreateAppointmentResult(Guid AppointmentId);
|
||||
@@ -0,0 +1,69 @@
|
||||
// EN: HTTP client for Catalog Service.
|
||||
// VI: HTTP client cho Catalog Service.
|
||||
|
||||
namespace OrderService.API.Infrastructure.HttpClients;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Client for calling Catalog Service to get product details.
|
||||
/// VI: Client gọi Catalog Service để lấy chi tiết sản phẩm.
|
||||
/// </summary>
|
||||
public class CatalogServiceClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<CatalogServiceClient> _logger;
|
||||
|
||||
public CatalogServiceClient(
|
||||
HttpClient httpClient,
|
||||
ILogger<CatalogServiceClient> logger)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get product by ID.
|
||||
/// VI: Lấy sản phẩm theo ID.
|
||||
/// </summary>
|
||||
public async Task<ProductDto?> GetProductByIdAsync(Guid productId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Getting product from Catalog Service / VI: Lấy sản phẩm từ Catalog Service: {ProductId}",
|
||||
productId);
|
||||
|
||||
var response = await _httpClient.GetAsync(
|
||||
$"/api/v1/products/{productId}",
|
||||
cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"EN: Product not found in Catalog Service / VI: Không tìm thấy sản phẩm trong Catalog Service: {ProductId}",
|
||||
productId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<ProductDto>(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"EN: Error calling Catalog Service / VI: Lỗi khi gọi Catalog Service: {ProductId}",
|
||||
productId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Product DTO from Catalog Service.
|
||||
/// VI: DTO sản phẩm từ Catalog Service.
|
||||
/// </summary>
|
||||
public record ProductDto(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string Type,
|
||||
decimal Price,
|
||||
bool IsActive
|
||||
);
|
||||
@@ -0,0 +1,77 @@
|
||||
// EN: HTTP client for F&B Engine.
|
||||
// VI: HTTP client cho F&B Engine.
|
||||
|
||||
namespace OrderService.API.Infrastructure.HttpClients;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Client for calling F&B Engine for kitchen operations.
|
||||
/// VI: Client gọi F&B Engine để thực hiện thao tác bếp.
|
||||
/// </summary>
|
||||
public class FnbEngineClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<FnbEngineClient> _logger;
|
||||
|
||||
public FnbEngineClient(
|
||||
HttpClient httpClient,
|
||||
ILogger<FnbEngineClient> logger)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create kitchen ticket for prepared food.
|
||||
/// VI: Tạo phiếu bếp cho món ăn.
|
||||
/// </summary>
|
||||
public async Task<Guid?> CreateKitchenTicketAsync(
|
||||
Guid productId,
|
||||
string productName,
|
||||
Guid shopId,
|
||||
int quantity,
|
||||
string? notes,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Creating kitchen ticket / VI: Tạo phiếu bếp: {ProductName}, Quantity: {Quantity}",
|
||||
productName, quantity);
|
||||
|
||||
var request = new CreateKitchenTicketRequest(
|
||||
productId,
|
||||
productName,
|
||||
shopId,
|
||||
quantity,
|
||||
notes);
|
||||
|
||||
var response = await _httpClient.PostAsJsonAsync(
|
||||
"/api/v1/fnb/kitchen-tickets",
|
||||
request,
|
||||
cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("EN: Failed to create kitchen ticket / VI: Tạo phiếu bếp thất bại");
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<CreateKitchenTicketResult>(cancellationToken);
|
||||
return result?.TicketId;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "EN: Error creating kitchen ticket / VI: Lỗi khi tạo phiếu bếp");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record CreateKitchenTicketRequest(
|
||||
Guid ProductId,
|
||||
string ProductName,
|
||||
Guid ShopId,
|
||||
int Quantity,
|
||||
string? Notes);
|
||||
|
||||
public record CreateKitchenTicketResult(Guid TicketId);
|
||||
@@ -0,0 +1,92 @@
|
||||
// EN: HTTP client for Inventory Service.
|
||||
// VI: HTTP client cho Inventory Service.
|
||||
|
||||
namespace OrderService.API.Infrastructure.HttpClients;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Client for calling Inventory Service for stock operations.
|
||||
/// VI: Client gọi Inventory Service để thực hiện thao tác kho.
|
||||
/// </summary>
|
||||
public class InventoryServiceClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<InventoryServiceClient> _logger;
|
||||
|
||||
public InventoryServiceClient(
|
||||
HttpClient httpClient,
|
||||
ILogger<InventoryServiceClient> logger)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Check if stock is available.
|
||||
/// VI: Kiểm tra hàng còn trong kho.
|
||||
/// </summary>
|
||||
public async Task<bool> CheckStockAsync(
|
||||
Guid productId,
|
||||
Guid shopId,
|
||||
int quantity,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Checking stock / VI: Kiểm tra kho: Product={ProductId}, Shop={ShopId}, Quantity={Quantity}",
|
||||
productId, shopId, quantity);
|
||||
|
||||
var response = await _httpClient.GetAsync(
|
||||
$"/api/v1/inventory/check?productId={productId}&shopId={shopId}&quantity={quantity}",
|
||||
cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("EN: Stock check failed / VI: Kiểm tra kho thất bại");
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<StockCheckResult>(cancellationToken);
|
||||
return result?.IsAvailable ?? false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "EN: Error checking stock / VI: Lỗi khi kiểm tra kho");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Deduct stock from inventory.
|
||||
/// VI: Trừ hàng khỏi kho.
|
||||
/// </summary>
|
||||
public async Task<bool> DeductStockAsync(
|
||||
Guid productId,
|
||||
Guid shopId,
|
||||
int quantity,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"EN: Deducting stock / VI: Trừ kho: Product={ProductId}, Shop={ShopId}, Quantity={Quantity}",
|
||||
productId, shopId, quantity);
|
||||
|
||||
var request = new DeductStockRequest(productId, shopId, quantity);
|
||||
var response = await _httpClient.PostAsJsonAsync(
|
||||
"/api/v1/inventory/deduct",
|
||||
request,
|
||||
cancellationToken);
|
||||
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "EN: Error deducting stock / VI: Lỗi khi trừ kho");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record StockCheckResult(bool IsAvailable, int AvailableQuantity);
|
||||
public record DeductStockRequest(Guid ProductId, Guid ShopId, int Quantity);
|
||||
@@ -1,8 +1,15 @@
|
||||
using System.Data;
|
||||
using Asp.Versioning;
|
||||
using FluentValidation;
|
||||
using Hellang.Middleware.ProblemDetails;
|
||||
using Npgsql;
|
||||
using OrderService.API.Application.Behaviors;
|
||||
using OrderService.API.Application.Strategies;
|
||||
using OrderService.API.Infrastructure.HttpClients;
|
||||
using OrderService.Domain.Strategies;
|
||||
using OrderService.Infrastructure;
|
||||
using Polly;
|
||||
using Polly.Extensions.Http;
|
||||
using Serilog;
|
||||
|
||||
// EN: Configure Serilog early / VI: Cấu hình Serilog sớm
|
||||
@@ -26,6 +33,62 @@ try
|
||||
// EN: Add Infrastructure services / VI: Thêm Infrastructure services
|
||||
builder.Services.AddInfrastructure(builder.Configuration);
|
||||
|
||||
// EN: Add Dapper IDbConnection / VI: Thêm Dapper IDbConnection
|
||||
builder.Services.AddTransient<IDbConnection>(sp =>
|
||||
{
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
|
||||
?? builder.Configuration["DATABASE_URL"]
|
||||
?? throw new InvalidOperationException("Database connection string is required");
|
||||
return new NpgsqlConnection(connectionString);
|
||||
});
|
||||
|
||||
// EN: Add HTTP Clients with Polly / VI: Thêm HTTP Clients với Polly
|
||||
var retryPolicy = HttpPolicyExtensions
|
||||
.HandleTransientHttpError()
|
||||
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
|
||||
|
||||
var circuitBreakerPolicy = HttpPolicyExtensions
|
||||
.HandleTransientHttpError()
|
||||
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
|
||||
|
||||
builder.Services.AddHttpClient<CatalogServiceClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(builder.Configuration["Services:CatalogService"] ?? "http://catalog-service-net:8080");
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
})
|
||||
.AddPolicyHandler(retryPolicy)
|
||||
.AddPolicyHandler(circuitBreakerPolicy);
|
||||
|
||||
builder.Services.AddHttpClient<InventoryServiceClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(builder.Configuration["Services:InventoryService"] ?? "http://inventory-service-net:8080");
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
})
|
||||
.AddPolicyHandler(retryPolicy)
|
||||
.AddPolicyHandler(circuitBreakerPolicy);
|
||||
|
||||
builder.Services.AddHttpClient<BookingServiceClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(builder.Configuration["Services:BookingService"] ?? "http://booking-service-net:8080");
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
})
|
||||
.AddPolicyHandler(retryPolicy)
|
||||
.AddPolicyHandler(circuitBreakerPolicy);
|
||||
|
||||
builder.Services.AddHttpClient<FnbEngineClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(builder.Configuration["Services:FnbEngine"] ?? "http://fnb-engine-net:8080");
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
})
|
||||
.AddPolicyHandler(retryPolicy)
|
||||
.AddPolicyHandler(circuitBreakerPolicy);
|
||||
|
||||
// EN: Register Strategies / VI: Đăng ký Strategies
|
||||
builder.Services.AddTransient<ILineItemStrategy, RetailStrategy>();
|
||||
builder.Services.AddTransient<ILineItemStrategy, ServiceStrategy>();
|
||||
builder.Services.AddTransient<ILineItemStrategy, FnbStrategy>();
|
||||
builder.Services.AddSingleton<StrategyFactory>();
|
||||
|
||||
// EN: Add MediatR with behaviors / VI: Thêm MediatR với behaviors
|
||||
builder.Services.AddMediatR(cfg =>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user