Создание заказов
This commit is contained in:
+7
-3
@@ -9,9 +9,6 @@ COPY ["src/", "src/"]
|
|||||||
# Restore dependencies
|
# Restore dependencies
|
||||||
RUN dotnet restore "Nashel.sln"
|
RUN dotnet restore "Nashel.sln"
|
||||||
|
|
||||||
# Copy the rest of the source code (already copied in previous step fundamentally, but ensuring context)
|
|
||||||
# Actually, copying src/ covers it.
|
|
||||||
|
|
||||||
# Publish the application
|
# Publish the application
|
||||||
WORKDIR "/src/src/Host"
|
WORKDIR "/src/src/Host"
|
||||||
RUN dotnet publish "Nashel.Host.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
RUN dotnet publish "Nashel.Host.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||||
@@ -19,6 +16,13 @@ RUN dotnet publish "Nashel.Host.csproj" -c Release -o /app/publish /p:UseAppHost
|
|||||||
# Final stage
|
# Final stage
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-preview AS final
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0-preview AS final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Устанавливаем зависимости Npgsql / Kerberos (нужны для PostgreSQL через Npgsql в .NET 10 preview)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libgssapi-krb5-2 \
|
||||||
|
krb5-user \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=build /app/publish .
|
COPY --from=build /app/publish .
|
||||||
# Copy wwwroot folder for static files (uploads)
|
# Copy wwwroot folder for static files (uploads)
|
||||||
COPY src/Host/wwwroot /app/wwwroot
|
COPY src/Host/wwwroot /app/wwwroot
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ using Nashel.Modules.Reputation.Presentation;
|
|||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Настройка JSON: принимаем строковые значения enum ("Direct" вместо 0)
|
||||||
|
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||||
|
{
|
||||||
|
options.SerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
|
||||||
|
});
|
||||||
|
|
||||||
// Добавление сервисов в контейнер.
|
// Добавление сервисов в контейнер.
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen(options =>
|
builder.Services.AddSwaggerGen(options =>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Nashel.Modules.Order.Domain.Repositories;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Order.Application.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Команда принятия заказа исполнителем.
|
||||||
|
/// </summary>
|
||||||
|
public record AcceptOrderCommand(Guid OrderId, Guid PerformerId) : IRequest<Unit>;
|
||||||
|
|
||||||
|
public class AcceptOrderHandler : IRequestHandler<AcceptOrderCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _repository;
|
||||||
|
|
||||||
|
public AcceptOrderHandler(IOrderRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(AcceptOrderCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var order = await _repository.GetByIdAsync(request.OrderId, cancellationToken);
|
||||||
|
if (order == null)
|
||||||
|
{
|
||||||
|
throw new KeyNotFoundException($"Заказ {request.OrderId} не найден.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что именно этот исполнитель принимает заказ
|
||||||
|
if (order.SelectedPerformerId != request.PerformerId)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("Этот заказ предназначен другому исполнителю.");
|
||||||
|
}
|
||||||
|
|
||||||
|
order.Accept();
|
||||||
|
|
||||||
|
await _repository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,9 @@ namespace Nashel.Modules.Order.Application.Commands;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Команда отмены заказа.
|
/// Команда отмены заказа.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record CancelOrderCommand(Guid OrderId) : IRequest<Unit>
|
public record CancelOrderCommand(Guid OrderId, Guid RequesterId, string? Reason = null) : IRequest<Unit>
|
||||||
{
|
{
|
||||||
public CancelOrderCommand() : this(Guid.Empty) { }
|
public CancelOrderCommand() : this(Guid.Empty, Guid.Empty, null) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CancelOrderHandler : IRequestHandler<CancelOrderCommand, Unit>
|
public class CancelOrderHandler : IRequestHandler<CancelOrderCommand, Unit>
|
||||||
@@ -25,10 +25,19 @@ public class CancelOrderHandler : IRequestHandler<CancelOrderCommand, Unit>
|
|||||||
var order = await _repository.GetByIdAsync(request.OrderId, cancellationToken);
|
var order = await _repository.GetByIdAsync(request.OrderId, cancellationToken);
|
||||||
if (order == null)
|
if (order == null)
|
||||||
{
|
{
|
||||||
throw new KeyNotFoundException($"Order {request.OrderId} not found");
|
throw new KeyNotFoundException($"Заказ {request.OrderId} не найден.");
|
||||||
}
|
}
|
||||||
|
|
||||||
order.Cancel();
|
// Отменить может заказчик или исполнитель
|
||||||
|
var isCustomer = order.CustomerId == request.RequesterId;
|
||||||
|
var isPerformer = order.SelectedPerformerId == request.RequesterId;
|
||||||
|
|
||||||
|
if (!isCustomer && !isPerformer)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("Только заказчик или исполнитель может отменить заказ.");
|
||||||
|
}
|
||||||
|
|
||||||
|
order.Cancel(request.Reason);
|
||||||
|
|
||||||
await _repository.SaveChangesAsync(cancellationToken);
|
await _repository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Nashel.Modules.Order.Domain.Repositories;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Order.Application.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Команда завершения заказа.
|
||||||
|
/// </summary>
|
||||||
|
public record CompleteOrderCommand(Guid OrderId, Guid RequesterId) : IRequest<Unit>;
|
||||||
|
|
||||||
|
public class CompleteOrderHandler : IRequestHandler<CompleteOrderCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _repository;
|
||||||
|
|
||||||
|
public CompleteOrderHandler(IOrderRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(CompleteOrderCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var order = await _repository.GetByIdAsync(request.OrderId, cancellationToken);
|
||||||
|
if (order == null)
|
||||||
|
{
|
||||||
|
throw new KeyNotFoundException($"Заказ {request.OrderId} не найден.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Завершить может заказчик или исполнитель
|
||||||
|
var isCustomer = order.CustomerId == request.RequesterId;
|
||||||
|
var isPerformer = order.SelectedPerformerId == request.RequesterId;
|
||||||
|
|
||||||
|
if (!isCustomer && !isPerformer)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("Только заказчик или исполнитель может завершить заказ.");
|
||||||
|
}
|
||||||
|
|
||||||
|
order.Complete();
|
||||||
|
|
||||||
|
await _repository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Nashel.Modules.Order.Domain.Enums;
|
using Nashel.Modules.Order.Domain.Enums;
|
||||||
using Nashel.Modules.Order.Domain.Repositories;
|
using Nashel.Modules.Order.Domain.Repositories;
|
||||||
using Nashel.Modules.Order.Domain.ValueObjects;
|
using Nashel.Modules.Order.Domain.ValueObjects;
|
||||||
@@ -11,26 +12,38 @@ namespace Nashel.Modules.Order.Application.Commands;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record CreateOrderCommand(
|
public record CreateOrderCommand(
|
||||||
Guid CustomerId,
|
Guid CustomerId,
|
||||||
|
string CustomerName,
|
||||||
Guid ServiceId,
|
Guid ServiceId,
|
||||||
|
string ServiceTitle,
|
||||||
|
decimal PriceAmount,
|
||||||
|
PriceType PriceType,
|
||||||
OrderType Type,
|
OrderType Type,
|
||||||
string Address,
|
string Address,
|
||||||
double Latitude,
|
double Latitude,
|
||||||
double Longitude,
|
double Longitude,
|
||||||
DateTime? Deadline,
|
DateTime? Deadline,
|
||||||
Guid? PerformerId
|
Guid? PerformerId,
|
||||||
|
string? PerformerName
|
||||||
) : IRequest<Guid>;
|
) : IRequest<Guid>;
|
||||||
|
|
||||||
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
|
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
|
||||||
{
|
{
|
||||||
private readonly IOrderRepository _repository;
|
private readonly IOrderRepository _repository;
|
||||||
|
private readonly ILogger<CreateOrderHandler> _logger;
|
||||||
|
|
||||||
public CreateOrderHandler(IOrderRepository repository)
|
public CreateOrderHandler(IOrderRepository repository, ILogger<CreateOrderHandler> logger)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
|
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Creating order for customer {CustomerId}, service {ServiceId}, type {Type}",
|
||||||
|
request.CustomerId, request.ServiceId, request.Type);
|
||||||
|
|
||||||
// Создаем точку (WGS84)
|
// Создаем точку (WGS84)
|
||||||
var point = new Point(request.Longitude, request.Latitude) { SRID = 4326 };
|
var point = new Point(request.Longitude, request.Latitude) { SRID = 4326 };
|
||||||
var location = new OrderLocation(request.Address, point);
|
var location = new OrderLocation(request.Address, point);
|
||||||
@@ -41,13 +54,29 @@ public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
|
|||||||
{
|
{
|
||||||
if (request.PerformerId == null)
|
if (request.PerformerId == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogError("PerformerId is null for direct order");
|
||||||
throw new ArgumentException("PerformerId обязателен для прямого заказа.");
|
throw new ArgumentException("PerformerId обязателен для прямого заказа.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ПРОВЕРКА НА ДУБЛИКАТ
|
||||||
|
var existing = await _repository.GetActiveOrderAsync(
|
||||||
|
request.CustomerId, request.PerformerId.Value, request.ServiceId, cancellationToken);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Duplicate active order found: {OrderId} for customer {CustomerId}, master {MasterId}",
|
||||||
|
existing.Id, request.CustomerId, request.PerformerId);
|
||||||
|
throw new InvalidOperationException("У вас уже есть активный заказ на эту услугу с данным мастером.");
|
||||||
|
}
|
||||||
|
|
||||||
order = Nashel.Modules.Order.Domain.Aggregates.Order.CreateDirect(
|
order = Nashel.Modules.Order.Domain.Aggregates.Order.CreateDirect(
|
||||||
request.CustomerId,
|
request.CustomerId,
|
||||||
|
request.CustomerName,
|
||||||
request.ServiceId,
|
request.ServiceId,
|
||||||
|
request.ServiceTitle,
|
||||||
|
request.PriceAmount,
|
||||||
|
request.PriceType,
|
||||||
request.PerformerId.Value,
|
request.PerformerId.Value,
|
||||||
|
request.PerformerName ?? "Мастер",
|
||||||
location,
|
location,
|
||||||
request.Deadline
|
request.Deadline
|
||||||
);
|
);
|
||||||
@@ -56,7 +85,11 @@ public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
|
|||||||
{
|
{
|
||||||
order = Nashel.Modules.Order.Domain.Aggregates.Order.CreatePublic(
|
order = Nashel.Modules.Order.Domain.Aggregates.Order.CreatePublic(
|
||||||
request.CustomerId,
|
request.CustomerId,
|
||||||
|
request.CustomerName,
|
||||||
request.ServiceId,
|
request.ServiceId,
|
||||||
|
request.ServiceTitle,
|
||||||
|
request.PriceAmount,
|
||||||
|
request.PriceType,
|
||||||
location,
|
location,
|
||||||
request.Deadline
|
request.Deadline
|
||||||
);
|
);
|
||||||
@@ -65,6 +98,13 @@ public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
|
|||||||
await _repository.AddAsync(order, cancellationToken);
|
await _repository.AddAsync(order, cancellationToken);
|
||||||
await _repository.SaveChangesAsync(cancellationToken);
|
await _repository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("Order created with ID: {OrderId}", order.Id);
|
||||||
return order.Id;
|
return order.Id;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error in CreateOrderHandler for customer {CustomerId}", request.CustomerId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Nashel.Modules.Order.Domain.Enums;
|
||||||
|
using Nashel.Modules.Order.Domain.Repositories;
|
||||||
|
using OrderAggregate = Nashel.Modules.Order.Domain.Aggregates.Order;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Order.Application.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DTO для передачи данных заказа на фронтенд.
|
||||||
|
/// </summary>
|
||||||
|
public record OrderDto(
|
||||||
|
Guid Id,
|
||||||
|
Guid CustomerId,
|
||||||
|
string CustomerName,
|
||||||
|
Guid? PerformerId,
|
||||||
|
string? PerformerName,
|
||||||
|
Guid ServiceId,
|
||||||
|
string ServiceTitle,
|
||||||
|
decimal PriceAmount,
|
||||||
|
string PriceType,
|
||||||
|
string Type,
|
||||||
|
string Status,
|
||||||
|
string Address,
|
||||||
|
DateTime CreatedAt,
|
||||||
|
DateTime? AcceptedAt,
|
||||||
|
DateTime? CompletedAt,
|
||||||
|
string? CancellationReason,
|
||||||
|
/// <summary>Секунды до истечения SLA. Null если не в статусе PendingAcceptance.</summary>
|
||||||
|
int? SlaSecondsLeft
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Запрос списка заказов, где пользователь является заказчиком.
|
||||||
|
/// </summary>
|
||||||
|
public record GetMyOrdersAsCustomerQuery(Guid UserId) : IRequest<IReadOnlyList<OrderDto>>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Запрос списка заказов, где пользователь является исполнителем/мастером.
|
||||||
|
/// </summary>
|
||||||
|
public record GetMyOrdersAsPerformerQuery(Guid UserId) : IRequest<IReadOnlyList<OrderDto>>;
|
||||||
|
|
||||||
|
// ─── Handlers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public class GetMyOrdersAsCustomerHandler : IRequestHandler<GetMyOrdersAsCustomerQuery, IReadOnlyList<OrderDto>>
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _repository;
|
||||||
|
|
||||||
|
public GetMyOrdersAsCustomerHandler(IOrderRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<OrderDto>> Handle(GetMyOrdersAsCustomerQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var orders = await _repository.GetByCustomerIdAsync(request.UserId, cancellationToken);
|
||||||
|
return orders.Select(OrderMapper.ToDto).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GetMyOrdersAsPerformerHandler : IRequestHandler<GetMyOrdersAsPerformerQuery, IReadOnlyList<OrderDto>>
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _repository;
|
||||||
|
|
||||||
|
public GetMyOrdersAsPerformerHandler(IOrderRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<OrderDto>> Handle(GetMyOrdersAsPerformerQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var orders = await _repository.GetByPerformerIdAsync(request.UserId, cancellationToken);
|
||||||
|
return orders.Select(OrderMapper.ToDto).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Mapper ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
internal static class OrderMapper
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan SlaTimeout = TimeSpan.FromMinutes(60);
|
||||||
|
|
||||||
|
public static OrderDto ToDto(OrderAggregate order)
|
||||||
|
{
|
||||||
|
int? slaSecondsLeft = null;
|
||||||
|
|
||||||
|
if (order.Status == OrderStatus.PendingAcceptance)
|
||||||
|
{
|
||||||
|
var slaDeadline = order.CreatedAt.Add(SlaTimeout);
|
||||||
|
var secondsLeft = (int)(slaDeadline - DateTime.UtcNow).TotalSeconds;
|
||||||
|
slaSecondsLeft = Math.Max(0, secondsLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OrderDto(
|
||||||
|
order.Id,
|
||||||
|
order.CustomerId,
|
||||||
|
order.CustomerName,
|
||||||
|
order.SelectedPerformerId,
|
||||||
|
order.PerformerName,
|
||||||
|
order.ServiceId,
|
||||||
|
order.ServiceTitle,
|
||||||
|
order.PriceAmount,
|
||||||
|
order.PriceType.ToString(),
|
||||||
|
order.Type.ToString(),
|
||||||
|
order.Status.ToString(),
|
||||||
|
order.Location?.Address ?? string.Empty,
|
||||||
|
order.CreatedAt,
|
||||||
|
order.AcceptedAt,
|
||||||
|
order.CompletedAt,
|
||||||
|
order.CancellationReason,
|
||||||
|
slaSecondsLeft
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,13 +11,30 @@ namespace Nashel.Modules.Order.Domain.Aggregates;
|
|||||||
public class Order : AggregateRoot<Guid>
|
public class Order : AggregateRoot<Guid>
|
||||||
{
|
{
|
||||||
public Guid CustomerId { get; private set; }
|
public Guid CustomerId { get; private set; }
|
||||||
|
public string CustomerName { get; private set; } = default!;
|
||||||
public Guid ServiceId { get; private set; }
|
public Guid ServiceId { get; private set; }
|
||||||
|
public string ServiceTitle { get; private set; } = default!;
|
||||||
|
public decimal PriceAmount { get; private set; }
|
||||||
|
public PriceType PriceType { get; private set; }
|
||||||
public OrderType Type { get; private set; }
|
public OrderType Type { get; private set; }
|
||||||
public OrderStatus Status { get; private set; }
|
public OrderStatus Status { get; private set; }
|
||||||
public Guid? SelectedPerformerId { get; private set; }
|
public Guid? SelectedPerformerId { get; private set; }
|
||||||
|
public string? PerformerName { get; private set; }
|
||||||
public DateTime? Deadline { get; private set; }
|
public DateTime? Deadline { get; private set; }
|
||||||
public OrderLocation Location { get; private set; } = null!;
|
public OrderLocation Location { get; private set; } = null!;
|
||||||
|
|
||||||
|
// SLA-таймер: время создания заказа
|
||||||
|
public DateTime CreatedAt { get; private set; }
|
||||||
|
|
||||||
|
// Время принятия заказа исполнителем
|
||||||
|
public DateTime? AcceptedAt { get; private set; }
|
||||||
|
|
||||||
|
// Время завершения заказа
|
||||||
|
public DateTime? CompletedAt { get; private set; }
|
||||||
|
|
||||||
|
// Причина отмены
|
||||||
|
public string? CancellationReason { get; private set; }
|
||||||
|
|
||||||
// Optimistic Concurrency
|
// Optimistic Concurrency
|
||||||
public uint RowVersion { get; private set; }
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
@@ -27,41 +44,140 @@ public class Order : AggregateRoot<Guid>
|
|||||||
private Order() { }
|
private Order() { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создание прямого заказа.
|
/// Создание прямого заказа (заказчик выбирает конкретного мастера).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Order CreateDirect(Guid customerId, Guid serviceId, Guid performerId, OrderLocation location, DateTime? deadline)
|
public static Order CreateDirect(
|
||||||
|
Guid customerId,
|
||||||
|
string customerName,
|
||||||
|
Guid serviceId,
|
||||||
|
string serviceTitle,
|
||||||
|
decimal priceAmount,
|
||||||
|
PriceType priceType,
|
||||||
|
Guid performerId,
|
||||||
|
string performerName,
|
||||||
|
OrderLocation location,
|
||||||
|
DateTime? deadline)
|
||||||
{
|
{
|
||||||
return new Order
|
return new Order
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
CustomerId = customerId,
|
CustomerId = customerId,
|
||||||
|
CustomerName = customerName,
|
||||||
ServiceId = serviceId,
|
ServiceId = serviceId,
|
||||||
|
ServiceTitle = serviceTitle,
|
||||||
|
PriceAmount = priceAmount,
|
||||||
|
PriceType = priceType,
|
||||||
Type = OrderType.Direct,
|
Type = OrderType.Direct,
|
||||||
Status = OrderStatus.PendingAcceptance, // Сразу ждет подтверждения
|
Status = OrderStatus.PendingAcceptance,
|
||||||
SelectedPerformerId = performerId,
|
SelectedPerformerId = performerId,
|
||||||
|
PerformerName = performerName,
|
||||||
Location = location,
|
Location = location,
|
||||||
Deadline = deadline
|
Deadline = deadline,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
// Для прямого заказа отклик можно создать косвенно или считать цену договорной.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создание публичной заявки.
|
/// Создание публичной заявки (любой мастер может откликнуться).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Order CreatePublic(Guid customerId, Guid serviceId, OrderLocation location, DateTime? deadline)
|
public static Order CreatePublic(
|
||||||
|
Guid customerId,
|
||||||
|
string customerName,
|
||||||
|
Guid serviceId,
|
||||||
|
string serviceTitle,
|
||||||
|
decimal priceAmount,
|
||||||
|
PriceType priceType,
|
||||||
|
OrderLocation location,
|
||||||
|
DateTime? deadline)
|
||||||
{
|
{
|
||||||
return new Order
|
return new Order
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
CustomerId = customerId,
|
CustomerId = customerId,
|
||||||
|
CustomerName = customerName,
|
||||||
ServiceId = serviceId,
|
ServiceId = serviceId,
|
||||||
|
ServiceTitle = serviceTitle,
|
||||||
|
PriceAmount = priceAmount,
|
||||||
|
PriceType = priceType,
|
||||||
Type = OrderType.PublicJob,
|
Type = OrderType.PublicJob,
|
||||||
Status = OrderStatus.Published, // Публикуется для поиска
|
Status = OrderStatus.Published,
|
||||||
Location = location,
|
Location = location,
|
||||||
Deadline = deadline
|
Deadline = deadline,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Исполнитель принимает заказ. Запускает рабочий процесс.
|
||||||
|
/// </summary>
|
||||||
|
public void Accept()
|
||||||
|
{
|
||||||
|
if (Status != OrderStatus.PendingAcceptance)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Принять заказ можно только если он ожидает подтверждения. Текущий статус: {Status}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Status = OrderStatus.InProgress;
|
||||||
|
AcceptedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Заказ завершён (инициируется любой из сторон по договорённости).
|
||||||
|
/// </summary>
|
||||||
|
public void Complete()
|
||||||
|
{
|
||||||
|
if (Status != OrderStatus.InProgress)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Завершить заказ можно только если он в работе. Текущий статус: {Status}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Status = OrderStatus.Completed;
|
||||||
|
CompletedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отмена заказа с указанием причины.
|
||||||
|
/// </summary>
|
||||||
|
public void Cancel(string? reason = null)
|
||||||
|
{
|
||||||
|
if (Status == OrderStatus.Completed)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Нельзя отменить уже завершённый заказ.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Status == OrderStatus.Cancelled)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Заказ уже отменён.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Status == OrderStatus.Expired)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Нельзя отменить заказ с истёкшим SLA-временем.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Status = OrderStatus.Cancelled;
|
||||||
|
CancellationReason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SLA-таймер истёк: мастер не принял заказ в течение 60 минут.
|
||||||
|
/// </summary>
|
||||||
|
public void Expire()
|
||||||
|
{
|
||||||
|
if (Status != OrderStatus.PendingAcceptance)
|
||||||
|
{
|
||||||
|
// Уже обработан (принят, отменён и т.д.) — ничего не делаем
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status = OrderStatus.Expired;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Добавление отклика исполнителя на публичный заказ.
|
||||||
|
/// </summary>
|
||||||
public void AddApplication(Guid performerId, Money price, string comment)
|
public void AddApplication(Guid performerId, Money price, string comment)
|
||||||
{
|
{
|
||||||
if (Status != OrderStatus.Published)
|
if (Status != OrderStatus.Published)
|
||||||
@@ -77,6 +193,9 @@ public class Order : AggregateRoot<Guid>
|
|||||||
_applications.Add(new OrderApplication(performerId, price, comment));
|
_applications.Add(new OrderApplication(performerId, price, comment));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Заказчик выбирает исполнителя из откликнувшихся.
|
||||||
|
/// </summary>
|
||||||
public void SelectPerformer(Guid performerId)
|
public void SelectPerformer(Guid performerId)
|
||||||
{
|
{
|
||||||
if (Status != OrderStatus.Published)
|
if (Status != OrderStatus.Published)
|
||||||
@@ -93,9 +212,4 @@ public class Order : AggregateRoot<Guid>
|
|||||||
SelectedPerformerId = performerId;
|
SelectedPerformerId = performerId;
|
||||||
Status = OrderStatus.PendingAcceptance;
|
Status = OrderStatus.PendingAcceptance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Cancel()
|
|
||||||
{
|
|
||||||
Status = OrderStatus.Cancelled;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,27 +6,37 @@ namespace Nashel.Modules.Order.Domain.Enums;
|
|||||||
public enum OrderStatus
|
public enum OrderStatus
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создан.
|
/// Создан (внутренний, промежуточный).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Created,
|
Created,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Опубликован (поиск исполнителей).
|
/// Опубликован (идёт поиск исполнителей для публичного заказа).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Published,
|
Published,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ожидает подтверждения (выбран исполнитель).
|
/// Ожидает подтверждения мастером (SLA-таймер 60 минут запущен).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
PendingAcceptance,
|
PendingAcceptance,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// В работе.
|
/// В работе (мастер принял заказ).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
InProgress,
|
InProgress,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Завершен.
|
/// Завершён успешно.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Completed,
|
Completed,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Отменен.
|
/// Отменён любой из сторон.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Cancelled
|
Cancelled,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Истекло время ожидания (мастер не принял заказ в течение 60 минут).
|
||||||
|
/// </summary>
|
||||||
|
Expired
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Nashel.Modules.Order.Domain.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тип оплаты в заказе.
|
||||||
|
/// </summary>
|
||||||
|
public enum PriceType
|
||||||
|
{
|
||||||
|
Fixed,
|
||||||
|
Hourly,
|
||||||
|
Negotiable
|
||||||
|
}
|
||||||
@@ -2,9 +2,29 @@ using OrderAggregate = Nashel.Modules.Order.Domain.Aggregates.Order;
|
|||||||
|
|
||||||
namespace Nashel.Modules.Order.Domain.Repositories;
|
namespace Nashel.Modules.Order.Domain.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Репозиторий заказов.
|
||||||
|
/// </summary>
|
||||||
public interface IOrderRepository
|
public interface IOrderRepository
|
||||||
{
|
{
|
||||||
Task<OrderAggregate?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
Task<OrderAggregate?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Заказы где пользователь является заказчиком.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<OrderAggregate>> GetByCustomerIdAsync(Guid customerId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Заказы где пользователь является исполнителем.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<OrderAggregate>> GetByPerformerIdAsync(Guid performerId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Заказы в статусе PendingAcceptance, у которых CreatedAt < cutoff (для SLA-таймера).
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<OrderAggregate>> GetExpiredPendingOrdersAsync(DateTime cutoff, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task AddAsync(OrderAggregate order, CancellationToken cancellationToken = default);
|
Task AddAsync(OrderAggregate order, CancellationToken cancellationToken = default);
|
||||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<OrderAggregate?> GetActiveOrderAsync(Guid customerId, Guid performerId, Guid serviceId, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Nashel.Modules.Order.Application.Commands;
|
using Nashel.Modules.Order.Application.Commands;
|
||||||
|
using Nashel.Modules.Order.Application.Queries;
|
||||||
using Nashel.Modules.Order.Application.Services;
|
using Nashel.Modules.Order.Application.Services;
|
||||||
using Nashel.Modules.Order.Domain.Repositories;
|
using Nashel.Modules.Order.Domain.Repositories;
|
||||||
using Nashel.Modules.Order.Infrastructure.Persistence;
|
using Nashel.Modules.Order.Infrastructure.Persistence;
|
||||||
using Nashel.Modules.Order.Infrastructure.Persistence.Repositories;
|
using Nashel.Modules.Order.Infrastructure.Persistence.Repositories;
|
||||||
using Nashel.Modules.Order.Infrastructure.Services;
|
using Nashel.Modules.Order.Infrastructure.Services;
|
||||||
using Npgsql;
|
|
||||||
|
|
||||||
namespace Nashel.Modules.Order.Infrastructure;
|
namespace Nashel.Modules.Order.Infrastructure;
|
||||||
|
|
||||||
@@ -24,11 +25,19 @@ public static class DependencyInjection
|
|||||||
));
|
));
|
||||||
|
|
||||||
services.AddScoped<IOrderRepository, OrderRepository>();
|
services.AddScoped<IOrderRepository, OrderRepository>();
|
||||||
|
|
||||||
|
// Регистрируем DummySlaTimerService для обратной совместимости (используется в SelectPerformerCommand)
|
||||||
services.AddScoped<ISlaTimerService, DummySlaTimerService>();
|
services.AddScoped<ISlaTimerService, DummySlaTimerService>();
|
||||||
|
|
||||||
|
// Регистрируем реальный SLA BackgroundService
|
||||||
|
services.AddHostedService<SlaBackgroundService>();
|
||||||
|
|
||||||
services.AddMediatR(cfg =>
|
services.AddMediatR(cfg =>
|
||||||
{
|
{
|
||||||
|
// Application: Commands
|
||||||
cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly);
|
cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly);
|
||||||
|
// Application: Queries
|
||||||
|
cfg.RegisterServicesFromAssembly(typeof(GetMyOrdersAsCustomerQuery).Assembly);
|
||||||
});
|
});
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Nashel.Modules.Order.Infrastructure.Persistence;
|
||||||
|
using NetTopologySuite.Geometries;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Order.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(OrderDbContext))]
|
||||||
|
[Migration("20260307182933_AddOrderSlaFields")]
|
||||||
|
partial class AddOrderSlaFields
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("ordering")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.2")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Order.Domain.Aggregates.Order", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("AcceptedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("CancellationReason")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("CompletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Deadline")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("xid")
|
||||||
|
.HasColumnName("xmin");
|
||||||
|
|
||||||
|
b.Property<Guid?>("SelectedPerformerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ServiceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Status", "CreatedAt");
|
||||||
|
|
||||||
|
b.ToTable("Orders", "ordering");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Order.Domain.Entities.OrderApplication", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Comment")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("OrderId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("PerformerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OrderId");
|
||||||
|
|
||||||
|
b.ToTable("OrderApplications", "ordering");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Order.Domain.Aggregates.Order", b =>
|
||||||
|
{
|
||||||
|
b.OwnsOne("Nashel.Modules.Order.Domain.ValueObjects.OrderLocation", "Location", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("OrderId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<string>("Address")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("Address");
|
||||||
|
|
||||||
|
b1.Property<Point>("Point")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("geography (point)");
|
||||||
|
|
||||||
|
b1.HasKey("OrderId");
|
||||||
|
|
||||||
|
b1.HasIndex("Point");
|
||||||
|
|
||||||
|
NpgsqlIndexBuilderExtensions.HasMethod(b1.HasIndex("Point"), "gist");
|
||||||
|
|
||||||
|
b1.ToTable("Orders", "ordering");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("OrderId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Location")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Order.Domain.Entities.OrderApplication", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Nashel.Modules.Order.Domain.Aggregates.Order", null)
|
||||||
|
.WithMany("Applications")
|
||||||
|
.HasForeignKey("OrderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.OwnsOne("Nashel.Modules.Order.Domain.ValueObjects.Money", "Price", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("OrderApplicationId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("numeric")
|
||||||
|
.HasColumnName("PriceAmount");
|
||||||
|
|
||||||
|
b1.Property<string>("Currency")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("character varying(3)")
|
||||||
|
.HasColumnName("PriceCurrency");
|
||||||
|
|
||||||
|
b1.HasKey("OrderApplicationId");
|
||||||
|
|
||||||
|
b1.ToTable("OrderApplications", "ordering");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("OrderApplicationId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Price")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Order.Domain.Aggregates.Order", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Applications");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Order.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddOrderSlaFields : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "AcceptedAt",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "CancellationReason",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders",
|
||||||
|
type: "character varying(512)",
|
||||||
|
maxLength: 512,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "CompletedAt",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "CreatedAt",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Orders_Status_CreatedAt",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders",
|
||||||
|
columns: new[] { "Status", "CreatedAt" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Orders_Status_CreatedAt",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AcceptedAt",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CancellationReason",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CompletedAt",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CreatedAt",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+199
@@ -0,0 +1,199 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Nashel.Modules.Order.Infrastructure.Persistence;
|
||||||
|
using NetTopologySuite.Geometries;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Order.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(OrderDbContext))]
|
||||||
|
[Migration("20260307192905_ExpandOrderDetails")]
|
||||||
|
partial class ExpandOrderDetails
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("ordering")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.2")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Order.Domain.Aggregates.Order", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("AcceptedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("CancellationReason")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("CompletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("CustomerName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Deadline")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("PerformerName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal>("PriceAmount")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<int>("PriceType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("xid")
|
||||||
|
.HasColumnName("xmin");
|
||||||
|
|
||||||
|
b.Property<Guid?>("SelectedPerformerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ServiceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ServiceTitle")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Status", "CreatedAt");
|
||||||
|
|
||||||
|
b.ToTable("Orders", "ordering");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Order.Domain.Entities.OrderApplication", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Comment")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("OrderId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("PerformerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OrderId");
|
||||||
|
|
||||||
|
b.ToTable("OrderApplications", "ordering");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Order.Domain.Aggregates.Order", b =>
|
||||||
|
{
|
||||||
|
b.OwnsOne("Nashel.Modules.Order.Domain.ValueObjects.OrderLocation", "Location", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("OrderId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<string>("Address")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("Address");
|
||||||
|
|
||||||
|
b1.Property<Point>("Point")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("geography (point)");
|
||||||
|
|
||||||
|
b1.HasKey("OrderId");
|
||||||
|
|
||||||
|
b1.HasIndex("Point");
|
||||||
|
|
||||||
|
NpgsqlIndexBuilderExtensions.HasMethod(b1.HasIndex("Point"), "gist");
|
||||||
|
|
||||||
|
b1.ToTable("Orders", "ordering");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("OrderId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Location")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Order.Domain.Entities.OrderApplication", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Nashel.Modules.Order.Domain.Aggregates.Order", null)
|
||||||
|
.WithMany("Applications")
|
||||||
|
.HasForeignKey("OrderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.OwnsOne("Nashel.Modules.Order.Domain.ValueObjects.Money", "Price", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("OrderApplicationId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("numeric")
|
||||||
|
.HasColumnName("PriceAmount");
|
||||||
|
|
||||||
|
b1.Property<string>("Currency")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("character varying(3)")
|
||||||
|
.HasColumnName("PriceCurrency");
|
||||||
|
|
||||||
|
b1.HasKey("OrderApplicationId");
|
||||||
|
|
||||||
|
b1.ToTable("OrderApplications", "ordering");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("OrderApplicationId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Price")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Order.Domain.Aggregates.Order", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Applications");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Order.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ExpandOrderDetails : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "CustomerName",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PerformerName",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "PriceAmount",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders",
|
||||||
|
type: "numeric",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0m);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "PriceType",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ServiceTitle",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CustomerName",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PerformerName",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PriceAmount",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PriceType",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ServiceTitle",
|
||||||
|
schema: "ordering",
|
||||||
|
table: "Orders");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+199
@@ -0,0 +1,199 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Nashel.Modules.Order.Infrastructure.Persistence;
|
||||||
|
using NetTopologySuite.Geometries;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Order.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(OrderDbContext))]
|
||||||
|
[Migration("20260307214623_UpdatePriceTypeToEnum")]
|
||||||
|
partial class UpdatePriceTypeToEnum
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("ordering")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.2")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Order.Domain.Aggregates.Order", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("AcceptedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("CancellationReason")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("CompletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("CustomerName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Deadline")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("PerformerName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal>("PriceAmount")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<int>("PriceType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("xid")
|
||||||
|
.HasColumnName("xmin");
|
||||||
|
|
||||||
|
b.Property<Guid?>("SelectedPerformerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ServiceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ServiceTitle")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Status", "CreatedAt");
|
||||||
|
|
||||||
|
b.ToTable("Orders", "ordering");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Order.Domain.Entities.OrderApplication", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Comment")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("OrderId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("PerformerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OrderId");
|
||||||
|
|
||||||
|
b.ToTable("OrderApplications", "ordering");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Order.Domain.Aggregates.Order", b =>
|
||||||
|
{
|
||||||
|
b.OwnsOne("Nashel.Modules.Order.Domain.ValueObjects.OrderLocation", "Location", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("OrderId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<string>("Address")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("Address");
|
||||||
|
|
||||||
|
b1.Property<Point>("Point")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("geography (point)");
|
||||||
|
|
||||||
|
b1.HasKey("OrderId");
|
||||||
|
|
||||||
|
b1.HasIndex("Point");
|
||||||
|
|
||||||
|
NpgsqlIndexBuilderExtensions.HasMethod(b1.HasIndex("Point"), "gist");
|
||||||
|
|
||||||
|
b1.ToTable("Orders", "ordering");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("OrderId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Location")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Order.Domain.Entities.OrderApplication", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Nashel.Modules.Order.Domain.Aggregates.Order", null)
|
||||||
|
.WithMany("Applications")
|
||||||
|
.HasForeignKey("OrderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.OwnsOne("Nashel.Modules.Order.Domain.ValueObjects.Money", "Price", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("OrderApplicationId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("numeric")
|
||||||
|
.HasColumnName("PriceAmount");
|
||||||
|
|
||||||
|
b1.Property<string>("Currency")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("character varying(3)")
|
||||||
|
.HasColumnName("PriceCurrency");
|
||||||
|
|
||||||
|
b1.HasKey("OrderApplicationId");
|
||||||
|
|
||||||
|
b1.ToTable("OrderApplications", "ordering");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("OrderApplicationId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Price")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Order.Domain.Aggregates.Order", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Applications");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Order.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class UpdatePriceTypeToEnum : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,12 +31,38 @@ namespace Nashel.Modules.Order.Infrastructure.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("AcceptedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("CancellationReason")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("CompletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<Guid>("CustomerId")
|
b.Property<Guid>("CustomerId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("CustomerName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<DateTime?>("Deadline")
|
b.Property<DateTime?>("Deadline")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("PerformerName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal>("PriceAmount")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<int>("PriceType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<uint>("RowVersion")
|
b.Property<uint>("RowVersion")
|
||||||
.IsConcurrencyToken()
|
.IsConcurrencyToken()
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
@@ -49,6 +75,10 @@ namespace Nashel.Modules.Order.Infrastructure.Migrations
|
|||||||
b.Property<Guid>("ServiceId")
|
b.Property<Guid>("ServiceId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ServiceTitle")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("Status")
|
b.Property<string>("Status")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
@@ -59,6 +89,8 @@ namespace Nashel.Modules.Order.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Status", "CreatedAt");
|
||||||
|
|
||||||
b.ToTable("Orders", "ordering");
|
b.ToTable("Orders", "ordering");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.2.25163.2" />
|
||||||
<PackageReference Include="NetTopologySuite.IO.PostGis" Version="2.1.0" />
|
<PackageReference Include="NetTopologySuite.IO.PostGis" Version="2.1.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="10.0.0" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="10.0.0" />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
using Nashel.Modules.Order.Domain.Aggregates;
|
|
||||||
using Nashel.Modules.Order.Domain.Entities;
|
using Nashel.Modules.Order.Domain.Entities;
|
||||||
|
|
||||||
namespace Nashel.Modules.Order.Infrastructure.Persistence.Configurations;
|
namespace Nashel.Modules.Order.Infrastructure.Persistence.Configurations;
|
||||||
@@ -13,9 +12,12 @@ public class OrderConfiguration : IEntityTypeConfiguration<Nashel.Modules.Order.
|
|||||||
|
|
||||||
builder.HasKey(o => o.Id);
|
builder.HasKey(o => o.Id);
|
||||||
|
|
||||||
// Optimistic Concurrency (xmin mapping for uint)
|
// Optimistic Concurrency через PostgreSQL системный столбец xmin
|
||||||
builder.Property(o => o.RowVersion)
|
builder.Property(o => o.RowVersion)
|
||||||
.IsRowVersion();
|
.HasColumnName("xmin")
|
||||||
|
.HasColumnType("xid")
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.IsConcurrencyToken();
|
||||||
|
|
||||||
builder.Property(o => o.Type)
|
builder.Property(o => o.Type)
|
||||||
.HasConversion<string>();
|
.HasConversion<string>();
|
||||||
@@ -23,6 +25,20 @@ public class OrderConfiguration : IEntityTypeConfiguration<Nashel.Modules.Order.
|
|||||||
builder.Property(o => o.Status)
|
builder.Property(o => o.Status)
|
||||||
.HasConversion<string>();
|
.HasConversion<string>();
|
||||||
|
|
||||||
|
// SLA-поля
|
||||||
|
builder.Property(o => o.CreatedAt)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(o => o.AcceptedAt)
|
||||||
|
.IsRequired(false);
|
||||||
|
|
||||||
|
builder.Property(o => o.CompletedAt)
|
||||||
|
.IsRequired(false);
|
||||||
|
|
||||||
|
builder.Property(o => o.CancellationReason)
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.IsRequired(false);
|
||||||
|
|
||||||
builder.OwnsOne(o => o.Location, nav =>
|
builder.OwnsOne(o => o.Location, nav =>
|
||||||
{
|
{
|
||||||
nav.Property(l => l.Address).HasColumnName("Address");
|
nav.Property(l => l.Address).HasColumnName("Address");
|
||||||
@@ -34,6 +50,9 @@ public class OrderConfiguration : IEntityTypeConfiguration<Nashel.Modules.Order.
|
|||||||
.WithOne()
|
.WithOne()
|
||||||
.HasForeignKey("OrderId")
|
.HasForeignKey("OrderId")
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
// Индекс для SLA-таймера (быстрый поиск просроченных заказов)
|
||||||
|
builder.HasIndex(o => new { o.Status, o.CreatedAt });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Nashel.Modules.Order.Domain.Enums;
|
||||||
using Nashel.Modules.Order.Domain.Repositories;
|
using Nashel.Modules.Order.Domain.Repositories;
|
||||||
using Nashel.Modules.Order.Infrastructure.Persistence;
|
using Nashel.Modules.Order.Infrastructure.Persistence;
|
||||||
using OrderAggregate = Nashel.Modules.Order.Domain.Aggregates.Order;
|
using OrderAggregate = Nashel.Modules.Order.Domain.Aggregates.Order;
|
||||||
@@ -26,8 +27,45 @@ public class OrderRepository : IOrderRepository
|
|||||||
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
|
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<OrderAggregate>> GetByCustomerIdAsync(Guid customerId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Orders
|
||||||
|
.Include(o => o.Applications)
|
||||||
|
.Where(o => o.CustomerId == customerId)
|
||||||
|
.OrderByDescending(o => o.CreatedAt)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<OrderAggregate>> GetByPerformerIdAsync(Guid performerId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Orders
|
||||||
|
.Include(o => o.Applications)
|
||||||
|
.Where(o => o.SelectedPerformerId == performerId || o.Applications.Any(a => a.PerformerId == performerId))
|
||||||
|
.OrderByDescending(o => o.CreatedAt)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<OrderAggregate>> GetExpiredPendingOrdersAsync(DateTime cutoff, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Orders
|
||||||
|
.Where(o => o.Status == OrderStatus.PendingAcceptance && o.CreatedAt < cutoff)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<OrderAggregate?> GetActiveOrderAsync(Guid customerId, Guid performerId, Guid serviceId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var activeStatuses = new[] { OrderStatus.PendingAcceptance, OrderStatus.InProgress };
|
||||||
|
|
||||||
|
return await _context.Orders
|
||||||
|
.Where(o => o.CustomerId == customerId &&
|
||||||
|
o.SelectedPerformerId == performerId &&
|
||||||
|
o.ServiceId == serviceId &&
|
||||||
|
activeStatuses.Contains(o.Status))
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Nashel.Modules.Order.Domain.Repositories;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Order.Infrastructure.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Фоновый сервис SLA-таймера.
|
||||||
|
/// Раз в 1 минуту проверяет заказы в статусе PendingAcceptance,
|
||||||
|
/// которые ожидают более 60 минут, и переводит их в статус Expired.
|
||||||
|
/// </summary>
|
||||||
|
public class SlaBackgroundService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly ILogger<SlaBackgroundService> _logger;
|
||||||
|
|
||||||
|
// Период проверки — 1 минута
|
||||||
|
private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
|
// SLA — 60 минут
|
||||||
|
private static readonly TimeSpan SlaTimeout = TimeSpan.FromMinutes(60);
|
||||||
|
|
||||||
|
public SlaBackgroundService(IServiceScopeFactory scopeFactory, ILogger<SlaBackgroundService> logger)
|
||||||
|
{
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("SLA BackgroundService запущен. Период проверки: {Interval}.", CheckInterval);
|
||||||
|
|
||||||
|
// Стартовая задержка — даём приложению полностью запуститься
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ProcessExpiredOrdersAsync(stoppingToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Ошибка при обработке просроченных заказов.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(CheckInterval, stoppingToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("SLA BackgroundService остановлен.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessExpiredOrdersAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Используем scoped DI (репозиторий — scoped сервис)
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var repository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
|
||||||
|
|
||||||
|
var cutoff = DateTime.UtcNow.Subtract(SlaTimeout);
|
||||||
|
var expiredOrders = await repository.GetExpiredPendingOrdersAsync(cutoff, cancellationToken);
|
||||||
|
|
||||||
|
if (expiredOrders.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("SLA-таймер: найдено {Count} просроченных заказов.", expiredOrders.Count);
|
||||||
|
|
||||||
|
foreach (var order in expiredOrders)
|
||||||
|
{
|
||||||
|
order.Expire();
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Заказ {OrderId} переведён в статус Expired (создан {CreatedAt:u}, просрочен на {OverdueMinutes} мин.).",
|
||||||
|
order.Id,
|
||||||
|
order.CreatedAt,
|
||||||
|
(int)(DateTime.UtcNow - order.CreatedAt - SlaTimeout).TotalMinutes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await repository.SaveChangesAsync(cancellationToken);
|
||||||
|
_logger.LogInformation("SLA-таймер: {Count} заказов успешно обновлено.", expiredOrders.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Nashel.Modules.Order.Application.Commands;
|
using Nashel.Modules.Order.Application.Commands;
|
||||||
|
using Nashel.Modules.Order.Application.Queries;
|
||||||
using Nashel.Modules.Order.Domain.Enums;
|
using Nashel.Modules.Order.Domain.Enums;
|
||||||
|
|
||||||
namespace Nashel.Modules.Order.Presentation;
|
namespace Nashel.Modules.Order.Presentation;
|
||||||
@@ -11,41 +15,125 @@ public static class OrderEndpoints
|
|||||||
{
|
{
|
||||||
public static void MapOrderEndpoints(this IEndpointRouteBuilder app)
|
public static void MapOrderEndpoints(this IEndpointRouteBuilder app)
|
||||||
{
|
{
|
||||||
var group = app.MapGroup("/api/orders").WithTags("Orders");
|
// ─── Создание заказа (временно без авторизации) ─────────────────────────────
|
||||||
|
// AllowAnonymous не перекрывает RequireAuthorization на группе в Minimal API,
|
||||||
group.MapPost("/", async (CreateOrderRequest request, ISender sender) =>
|
// поэтому создание вынесено в отдельную группу.
|
||||||
|
app.MapPost("/api/orders", async ([FromBody] CreateOrderRequest request, ISender sender) =>
|
||||||
{
|
{
|
||||||
var command = new CreateOrderCommand(
|
var command = new CreateOrderCommand(
|
||||||
request.CustomerId,
|
request.CustomerId,
|
||||||
|
request.CustomerName,
|
||||||
request.ServiceId,
|
request.ServiceId,
|
||||||
|
request.ServiceTitle,
|
||||||
|
request.PriceAmount,
|
||||||
|
request.PriceType,
|
||||||
request.Type,
|
request.Type,
|
||||||
request.Address,
|
request.Address,
|
||||||
request.Latitude,
|
request.Latitude,
|
||||||
request.Longitude,
|
request.Longitude,
|
||||||
request.Deadline,
|
request.Deadline,
|
||||||
request.PerformerId
|
request.PerformerId,
|
||||||
|
request.PerformerName
|
||||||
);
|
);
|
||||||
var orderId = await sender.Send(command);
|
var orderId = await sender.Send(command);
|
||||||
return Results.Ok(orderId);
|
return Results.Ok(orderId);
|
||||||
})
|
})
|
||||||
|
.WithTags("Заказы")
|
||||||
.WithName("CreateOrder")
|
.WithName("CreateOrder")
|
||||||
.WithOpenApi(operation => new(operation) { Summary = "Создание нового заказа.", Description = "Создает прямой или публичный заказ." });
|
.WithOpenApi(op => new(op)
|
||||||
|
{
|
||||||
|
Summary = "Создать заказ",
|
||||||
|
Description = "Создаёт прямой заказ (конкретному мастеру) или публичную заявку."
|
||||||
|
})
|
||||||
|
.AllowAnonymous();
|
||||||
|
|
||||||
|
var group = app.MapGroup("/api/orders")
|
||||||
|
.WithTags("Заказы")
|
||||||
|
.RequireAuthorization();
|
||||||
|
|
||||||
|
// ─── Получить мои заказы (как заказчик) ─────────────────────────────────────
|
||||||
|
group.MapGet("/as-customer/{userId:guid}", async (Guid userId, ISender sender) =>
|
||||||
|
{
|
||||||
|
var result = await sender.Send(new GetMyOrdersAsCustomerQuery(userId));
|
||||||
|
return Results.Ok(result);
|
||||||
|
})
|
||||||
|
.WithName("GetMyOrdersAsCustomer")
|
||||||
|
.WithOpenApi(op => new(op)
|
||||||
|
{
|
||||||
|
Summary = "Мои заказы (роль: Заказчик)",
|
||||||
|
Description = "Возвращает список всех заказов, где текущий пользователь является заказчиком, отсортированных по дате создания."
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Получить мои заказы (как исполнитель) ───────────────────────────────────
|
||||||
|
group.MapGet("/as-performer/{userId:guid}", async (Guid userId, ISender sender) =>
|
||||||
|
{
|
||||||
|
var result = await sender.Send(new GetMyOrdersAsPerformerQuery(userId));
|
||||||
|
return Results.Ok(result);
|
||||||
|
})
|
||||||
|
.WithName("GetMyOrdersAsPerformer")
|
||||||
|
.WithOpenApi(op => new(op)
|
||||||
|
{
|
||||||
|
Summary = "Мои заказы (роль: Исполнитель)",
|
||||||
|
Description = "Возвращает список всех заказов, где текущий пользователь является назначенным исполнителем."
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Принять заказ (мастер) ──────────────────────────────────────────────────
|
||||||
|
group.MapPost("/{id:guid}/accept", async (Guid id, AcceptOrderRequest request, ISender sender) =>
|
||||||
|
{
|
||||||
|
var command = new AcceptOrderCommand(id, request.PerformerId);
|
||||||
|
await sender.Send(command);
|
||||||
|
return Results.Ok();
|
||||||
|
})
|
||||||
|
.WithName("AcceptOrder")
|
||||||
|
.WithOpenApi(op => new(op)
|
||||||
|
{
|
||||||
|
Summary = "Принять заказ",
|
||||||
|
Description = "Исполнитель принимает заказ. Переводит заказ в статус «В работе»."
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Завершить заказ ─────────────────────────────────────────────────────────
|
||||||
|
group.MapPost("/{id:guid}/complete", async (Guid id, CompleteOrderRequest request, ISender sender) =>
|
||||||
|
{
|
||||||
|
var command = new CompleteOrderCommand(id, request.RequesterId);
|
||||||
|
await sender.Send(command);
|
||||||
|
return Results.Ok();
|
||||||
|
})
|
||||||
|
.WithName("CompleteOrder")
|
||||||
|
.WithOpenApi(op => new(op)
|
||||||
|
{
|
||||||
|
Summary = "Завершить заказ",
|
||||||
|
Description = "Заказчик или исполнитель завершает заказ. Переводит в статус «Завершён»."
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Отменить заказ ──────────────────────────────────────────────────────────
|
||||||
|
group.MapPost("/{id:guid}/cancel", async (Guid id, CancelOrderRequest request, ISender sender) =>
|
||||||
|
{
|
||||||
|
var command = new CancelOrderCommand(id, request.RequesterId, request.Reason);
|
||||||
|
await sender.Send(command);
|
||||||
|
return Results.Ok();
|
||||||
|
})
|
||||||
|
.WithName("CancelOrder")
|
||||||
|
.WithOpenApi(op => new(op)
|
||||||
|
{
|
||||||
|
Summary = "Отменить заказ",
|
||||||
|
Description = "Заказчик или исполнитель отменяет заказ с указанием причины."
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Добавить отклик (мастер на публичный заказ) ─────────────────────────────
|
||||||
group.MapPost("/{id:guid}/applications", async (Guid id, AddApplicationRequest request, ISender sender) =>
|
group.MapPost("/{id:guid}/applications", async (Guid id, AddApplicationRequest request, ISender sender) =>
|
||||||
{
|
{
|
||||||
var command = new AddApplicationCommand(
|
var command = new AddApplicationCommand(id, request.PerformerId, request.Amount, request.Currency, request.Comment);
|
||||||
id,
|
|
||||||
request.PerformerId,
|
|
||||||
request.Amount,
|
|
||||||
request.Currency,
|
|
||||||
request.Comment
|
|
||||||
);
|
|
||||||
await sender.Send(command);
|
await sender.Send(command);
|
||||||
return Results.Ok();
|
return Results.Ok();
|
||||||
})
|
})
|
||||||
.WithName("AddOrderApplication")
|
.WithName("AddOrderApplication")
|
||||||
.WithOpenApi(operation => new(operation) { Summary = "Добавление отклика на заказ.", Description = "Позволяет исполнителю откликнуться на заказ." });
|
.WithOpenApi(op => new(op)
|
||||||
|
{
|
||||||
|
Summary = "Откликнуться на заказ",
|
||||||
|
Description = "Исполнитель отправляет отклик на публичный заказ с указанием цены и комментария."
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Выбрать исполнителя из откликов (заказчик) ─────────────────────────────
|
||||||
group.MapPost("/{id:guid}/select-performer", async (Guid id, SelectPerformerRequest request, ISender sender) =>
|
group.MapPost("/{id:guid}/select-performer", async (Guid id, SelectPerformerRequest request, ISender sender) =>
|
||||||
{
|
{
|
||||||
var command = new SelectPerformerCommand(id, request.PerformerId);
|
var command = new SelectPerformerCommand(id, request.PerformerId);
|
||||||
@@ -53,94 +141,125 @@ public static class OrderEndpoints
|
|||||||
return Results.Ok();
|
return Results.Ok();
|
||||||
})
|
})
|
||||||
.WithName("SelectPerformer")
|
.WithName("SelectPerformer")
|
||||||
.WithOpenApi(operation => new(operation) { Summary = "Выбор исполнителя для заказа.", Description = "Позволяет заказчику выбрать конкретного исполнителя." });
|
.WithOpenApi(op => new(op)
|
||||||
|
{
|
||||||
|
Summary = "Выбрать исполнителя",
|
||||||
|
Description = "Заказчик выбирает конкретного исполнителя из откликнувшихся. Запускает SLA-таймер 60 минут."
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Request DTOs ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Запрос на создание заказа.
|
/// Запрос на создание заказа.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record CreateOrderRequest
|
public record CreateOrderRequest
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>Идентификатор заказчика.</summary>
|
||||||
/// Идентификатор заказчика.
|
|
||||||
/// </summary>
|
|
||||||
public Guid CustomerId { get; init; }
|
public Guid CustomerId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>ФИО заказчика.</summary>
|
||||||
/// Идентификатор услуги.
|
public string CustomerName { get; init; } = default!;
|
||||||
/// </summary>
|
|
||||||
|
/// <summary>Идентификатор услуги.</summary>
|
||||||
public Guid ServiceId { get; init; }
|
public Guid ServiceId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Название услуги.</summary>
|
||||||
/// Тип заказа (прямой, публичный).
|
public string ServiceTitle { get; init; } = default!;
|
||||||
/// </summary>
|
|
||||||
|
/// <summary>Сумма услуги.</summary>
|
||||||
|
public decimal PriceAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Тип цены (0=Фикс, 1=Почасовая, 2=Договорная).</summary>
|
||||||
|
public PriceType PriceType { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Тип заказа: Direct (прямой) или PublicJob (публичный).</summary>
|
||||||
public OrderType Type { get; init; }
|
public OrderType Type { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Адрес выполнения заказа (текстовый).</summary>
|
||||||
/// Текстовый адрес выполнения заказа.
|
|
||||||
/// </summary>
|
|
||||||
public string Address { get; init; } = default!;
|
public string Address { get; init; } = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Широта места выполнения.</summary>
|
||||||
/// Географическая широта места выполнения.
|
|
||||||
/// </summary>
|
|
||||||
public double Latitude { get; init; }
|
public double Latitude { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Долгота места выполнения.</summary>
|
||||||
/// Географическая долгота места выполнения.
|
|
||||||
/// </summary>
|
|
||||||
public double Longitude { get; init; }
|
public double Longitude { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Крайний срок выполнения (необязательно).</summary>
|
||||||
/// Крайний срок выполнения заказа (опционально).
|
|
||||||
/// </summary>
|
|
||||||
public DateTime? Deadline { get; init; }
|
public DateTime? Deadline { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Идентификатор исполнителя (обязателен для прямого заказа).</summary>
|
||||||
/// Идентификатор конкретного исполнителя (если прямой заказ).
|
|
||||||
/// </summary>
|
|
||||||
public Guid? PerformerId { get; init; }
|
public Guid? PerformerId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Имя исполнителя (обязателен для прямого заказа).</summary>
|
||||||
|
public string? PerformerName { get; init; }
|
||||||
|
|
||||||
public CreateOrderRequest() { }
|
public CreateOrderRequest() { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Запрос на добавление отклика.
|
/// Запрос принятия заказа исполнителем.
|
||||||
|
/// </summary>
|
||||||
|
public record AcceptOrderRequest
|
||||||
|
{
|
||||||
|
/// <summary>Идентификатор исполнителя, принимающего заказ.</summary>
|
||||||
|
public Guid PerformerId { get; init; }
|
||||||
|
|
||||||
|
public AcceptOrderRequest() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Запрос завершения заказа.
|
||||||
|
/// </summary>
|
||||||
|
public record CompleteOrderRequest
|
||||||
|
{
|
||||||
|
/// <summary>Идентификатор пользователя (заказчик или исполнитель).</summary>
|
||||||
|
public Guid RequesterId { get; init; }
|
||||||
|
|
||||||
|
public CompleteOrderRequest() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Запрос отмены заказа.
|
||||||
|
/// </summary>
|
||||||
|
public record CancelOrderRequest
|
||||||
|
{
|
||||||
|
/// <summary>Идентификатор пользователя, инициирующего отмену.</summary>
|
||||||
|
public Guid RequesterId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Причина отмены (необязательно).</summary>
|
||||||
|
public string? Reason { get; init; }
|
||||||
|
|
||||||
|
public CancelOrderRequest() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Запрос добавления отклика на заказ.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record AddApplicationRequest
|
public record AddApplicationRequest
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>Идентификатор исполнителя.</summary>
|
||||||
/// Идентификатор исполнителя.
|
|
||||||
/// </summary>
|
|
||||||
public Guid PerformerId { get; init; }
|
public Guid PerformerId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Предлагаемая сумма.</summary>
|
||||||
/// Сумма предложения.
|
|
||||||
/// </summary>
|
|
||||||
public decimal Amount { get; init; }
|
public decimal Amount { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Валюта (например, RUB).</summary>
|
||||||
/// Валюта предложения (например, RUB).
|
|
||||||
/// </summary>
|
|
||||||
public string Currency { get; init; } = "RUB";
|
public string Currency { get; init; } = "RUB";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Комментарий к отклику.</summary>
|
||||||
/// Комментарий к отклику.
|
|
||||||
/// </summary>
|
|
||||||
public string Comment { get; init; } = default!;
|
public string Comment { get; init; } = default!;
|
||||||
|
|
||||||
public AddApplicationRequest() { }
|
public AddApplicationRequest() { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Запрос на выбор исполнителя.
|
/// Запрос выбора исполнителя.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record SelectPerformerRequest
|
public record SelectPerformerRequest
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>Идентификатор выбранного исполнителя.</summary>
|
||||||
/// Идентификатор выбранного исполнителя.
|
|
||||||
/// </summary>
|
|
||||||
public Guid PerformerId { get; init; }
|
public Guid PerformerId { get; init; }
|
||||||
|
|
||||||
public SelectPerformerRequest() { }
|
public SelectPerformerRequest() { }
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public class SelectPerformerHandlerTests
|
|||||||
|
|
||||||
// Создаем публичный заказ (Status = Published)
|
// Создаем публичный заказ (Status = Published)
|
||||||
var order = Nashel.Modules.Order.Domain.Aggregates.Order.CreatePublic(
|
var order = Nashel.Modules.Order.Domain.Aggregates.Order.CreatePublic(
|
||||||
customerId, serviceId, location, null);
|
customerId, "Customer", serviceId, "Service", 100, 0, location, null);
|
||||||
|
|
||||||
// Добавляем отклик исполнителя
|
// Добавляем отклик исполнителя
|
||||||
order.AddApplication(performerId, new Money(100, "RUB"), "Comment");
|
order.AddApplication(performerId, new Money(100, "RUB"), "Comment");
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public class OrderAggregateTests
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var order = Nashel.Modules.Order.Domain.Aggregates.Order.CreateDirect(
|
var order = Nashel.Modules.Order.Domain.Aggregates.Order.CreateDirect(
|
||||||
customerId, serviceId, performerId, location, DateTime.UtcNow.AddDays(1));
|
customerId, "Customer", serviceId, "Service", 100, 0, performerId, "Performer", location, DateTime.UtcNow.AddDays(1));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
order.Status.Should().Be(OrderStatus.PendingAcceptance);
|
order.Status.Should().Be(OrderStatus.PendingAcceptance);
|
||||||
@@ -37,7 +37,7 @@ public class OrderAggregateTests
|
|||||||
var location = new OrderLocation("Test St", new Point(10, 20));
|
var location = new OrderLocation("Test St", new Point(10, 20));
|
||||||
// Direct order created in PendingAcceptance
|
// Direct order created in PendingAcceptance
|
||||||
var order = Nashel.Modules.Order.Domain.Aggregates.Order.CreateDirect(
|
var order = Nashel.Modules.Order.Domain.Aggregates.Order.CreateDirect(
|
||||||
customerId, serviceId, performerId, location, null);
|
customerId, "Customer", serviceId, "Service", 100, 0, performerId, "Performer", location, null);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var action = () => order.AddApplication(Guid.NewGuid(), new Money(100, "RUB"), "Comment");
|
var action = () => order.AddApplication(Guid.NewGuid(), new Money(100, "RUB"), "Comment");
|
||||||
@@ -55,7 +55,7 @@ public class OrderAggregateTests
|
|||||||
var serviceId = Guid.NewGuid();
|
var serviceId = Guid.NewGuid();
|
||||||
var location = new OrderLocation("Test St", new Point(10, 20));
|
var location = new OrderLocation("Test St", new Point(10, 20));
|
||||||
var order = Nashel.Modules.Order.Domain.Aggregates.Order.CreatePublic(
|
var order = Nashel.Modules.Order.Domain.Aggregates.Order.CreatePublic(
|
||||||
customerId, serviceId, location, null);
|
customerId, "Customer", serviceId, "Service", 100, 0, location, null);
|
||||||
|
|
||||||
var performerId = Guid.NewGuid();
|
var performerId = Guid.NewGuid();
|
||||||
order.AddApplication(performerId, new Money(100, "RUB"), "Comment");
|
order.AddApplication(performerId, new Money(100, "RUB"), "Comment");
|
||||||
|
|||||||
Reference in New Issue
Block a user