Модуль заказов
This commit is contained in:
@@ -146,3 +146,7 @@ src/Modules/Geo/Tests/bin/
|
||||
src/Modules/Geo/Tests/obj/
|
||||
|
||||
src/Modules/Geo/Presentation/bin/
|
||||
|
||||
src/Modules/Order/Tests/bin/
|
||||
|
||||
src/Modules/Order/Tests/obj/
|
||||
|
||||
+38
@@ -77,6 +77,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0AB3BF05
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nashel.Modules.Identity.Tests", "src\Modules\Identity\Tests\Nashel.Modules.Identity.Tests.csproj", "{401E3B50-A6B8-4AA1-9DD3-41F9EC78FABD}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{EC447DCF-ABFA-6E24-52A5-D7FD48A5C558}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Order", "Order", "{2C821288-5B9D-778E-74EE-053FE87F223E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{7D435AA2-42F6-BE60-ABDD-B2A72FA358F7}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{06FD8EF1-B9DE-B088-FC77-09DC201E1B21}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{159DA5B4-03CA-95B4-A9E1-B46977641578}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Presentation", "Presentation", "{D0003F1F-EE71-3913-8EC0-5F1638807A8D}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{FB27511E-CBA6-E343-8CE0-54AAD98AFC9F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nashel.Modules.Order.Tests", "src\Modules\Order\Tests\Nashel.Modules.Order.Tests.csproj", "{DC1E99E0-6B92-4FBD-B74B-68D202961FB7}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -411,6 +429,18 @@ Global
|
||||
{401E3B50-A6B8-4AA1-9DD3-41F9EC78FABD}.Release|x64.Build.0 = Release|Any CPU
|
||||
{401E3B50-A6B8-4AA1-9DD3-41F9EC78FABD}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{401E3B50-A6B8-4AA1-9DD3-41F9EC78FABD}.Release|x86.Build.0 = Release|Any CPU
|
||||
{DC1E99E0-6B92-4FBD-B74B-68D202961FB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DC1E99E0-6B92-4FBD-B74B-68D202961FB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DC1E99E0-6B92-4FBD-B74B-68D202961FB7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{DC1E99E0-6B92-4FBD-B74B-68D202961FB7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{DC1E99E0-6B92-4FBD-B74B-68D202961FB7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{DC1E99E0-6B92-4FBD-B74B-68D202961FB7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{DC1E99E0-6B92-4FBD-B74B-68D202961FB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DC1E99E0-6B92-4FBD-B74B-68D202961FB7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DC1E99E0-6B92-4FBD-B74B-68D202961FB7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{DC1E99E0-6B92-4FBD-B74B-68D202961FB7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{DC1E99E0-6B92-4FBD-B74B-68D202961FB7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{DC1E99E0-6B92-4FBD-B74B-68D202961FB7}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -449,5 +479,13 @@ Global
|
||||
{3266C089-BF45-8FB5-CBFB-520E7D591FF0} = {CA253913-39DE-BFD0-C9A3-4B7EC6FBDF17}
|
||||
{6153DBFE-E924-7D0F-FF5C-84EC91E8B397} = {CA253913-39DE-BFD0-C9A3-4B7EC6FBDF17}
|
||||
{401E3B50-A6B8-4AA1-9DD3-41F9EC78FABD} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
{EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{2C821288-5B9D-778E-74EE-053FE87F223E} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558}
|
||||
{7D435AA2-42F6-BE60-ABDD-B2A72FA358F7} = {2C821288-5B9D-778E-74EE-053FE87F223E}
|
||||
{06FD8EF1-B9DE-B088-FC77-09DC201E1B21} = {2C821288-5B9D-778E-74EE-053FE87F223E}
|
||||
{159DA5B4-03CA-95B4-A9E1-B46977641578} = {2C821288-5B9D-778E-74EE-053FE87F223E}
|
||||
{D0003F1F-EE71-3913-8EC0-5F1638807A8D} = {2C821288-5B9D-778E-74EE-053FE87F223E}
|
||||
{FB27511E-CBA6-E343-8CE0-54AAD98AFC9F} = {2C821288-5B9D-778E-74EE-053FE87F223E}
|
||||
{DC1E99E0-6B92-4FBD-B74B-68D202961FB7} = {FB27511E-CBA6-E343-8CE0-54AAD98AFC9F}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.OpenApi" Version="1.6.14" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -8,6 +8,8 @@ using Nashel.Modules.Geo.Infrastructure;
|
||||
using Nashel.Modules.Geo.Presentation;
|
||||
using Nashel.Modules.Identity.Infrastructure;
|
||||
using Nashel.Modules.Identity.Presentation.Endpoints;
|
||||
using Nashel.Modules.Order.Infrastructure;
|
||||
using Nashel.Modules.Order.Presentation;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -58,6 +60,9 @@ builder.Services.AddCatalogModule(builder.Configuration);
|
||||
// Регистрация модуля Geo
|
||||
builder.Services.AddGeoModule(builder.Configuration);
|
||||
|
||||
// Регистрация модуля Order
|
||||
builder.Services.AddOrderModule(builder.Configuration);
|
||||
|
||||
// Настройка аутентификации
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
@@ -96,5 +101,6 @@ app.UseAuthorization();
|
||||
app.MapIdentityEndpoints();
|
||||
app.MapCatalogEndpoints();
|
||||
app.MapGeoEndpoints();
|
||||
app.MapOrderEndpoints();
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Nashel.Modules.Geo.Tests;
|
||||
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Nashel.Modules.Identity.Tests;
|
||||
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using MediatR;
|
||||
using Nashel.Modules.Order.Domain.Repositories;
|
||||
using Nashel.Modules.Order.Domain.ValueObjects;
|
||||
|
||||
namespace Nashel.Modules.Order.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Команда добавления отклика на заказ.
|
||||
/// </summary>
|
||||
public record AddApplicationCommand : IRequest<Unit>
|
||||
{
|
||||
public Guid OrderId { get; init; }
|
||||
public Guid PerformerId { get; init; }
|
||||
public decimal Amount { get; init; }
|
||||
public string Currency { get; init; } = "RUB";
|
||||
public string Comment { get; init; } = string.Empty;
|
||||
|
||||
public AddApplicationCommand(Guid orderId, Guid performerId, decimal amount, string currency, string comment)
|
||||
{
|
||||
OrderId = orderId;
|
||||
PerformerId = performerId;
|
||||
Amount = amount;
|
||||
Currency = currency;
|
||||
Comment = comment;
|
||||
}
|
||||
public AddApplicationCommand() { }
|
||||
}
|
||||
|
||||
public class AddApplicationHandler : IRequestHandler<AddApplicationCommand, Unit>
|
||||
{
|
||||
private readonly IOrderRepository _repository;
|
||||
|
||||
public AddApplicationHandler(IOrderRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(AddApplicationCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var order = await _repository.GetByIdAsync(request.OrderId, cancellationToken);
|
||||
if (order == null)
|
||||
{
|
||||
throw new KeyNotFoundException($"Order {request.OrderId} not found");
|
||||
}
|
||||
|
||||
var price = new Money(request.Amount, request.Currency);
|
||||
order.AddApplication(request.PerformerId, price, request.Comment);
|
||||
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using MediatR;
|
||||
using Nashel.Modules.Order.Domain.Repositories;
|
||||
|
||||
namespace Nashel.Modules.Order.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Команда отмены заказа.
|
||||
/// </summary>
|
||||
public record CancelOrderCommand(Guid OrderId) : IRequest<Unit>
|
||||
{
|
||||
public CancelOrderCommand() : this(Guid.Empty) { }
|
||||
}
|
||||
|
||||
public class CancelOrderHandler : IRequestHandler<CancelOrderCommand, Unit>
|
||||
{
|
||||
private readonly IOrderRepository _repository;
|
||||
|
||||
public CancelOrderHandler(IOrderRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(CancelOrderCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var order = await _repository.GetByIdAsync(request.OrderId, cancellationToken);
|
||||
if (order == null)
|
||||
{
|
||||
throw new KeyNotFoundException($"Order {request.OrderId} not found");
|
||||
}
|
||||
|
||||
order.Cancel();
|
||||
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using MediatR;
|
||||
using Nashel.Modules.Order.Domain.Enums;
|
||||
using Nashel.Modules.Order.Domain.Repositories;
|
||||
using Nashel.Modules.Order.Domain.ValueObjects;
|
||||
using NetTopologySuite.Geometries;
|
||||
|
||||
namespace Nashel.Modules.Order.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Команда на создание заказа.
|
||||
/// </summary>
|
||||
public record CreateOrderCommand(
|
||||
Guid CustomerId,
|
||||
Guid ServiceId,
|
||||
OrderType Type,
|
||||
string Address,
|
||||
double Latitude,
|
||||
double Longitude,
|
||||
DateTime? Deadline,
|
||||
Guid? PerformerId
|
||||
) : IRequest<Guid>;
|
||||
|
||||
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
|
||||
{
|
||||
private readonly IOrderRepository _repository;
|
||||
|
||||
public CreateOrderHandler(IOrderRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Создаем точку (WGS84)
|
||||
var point = new Point(request.Longitude, request.Latitude) { SRID = 4326 };
|
||||
var location = new OrderLocation(request.Address, point);
|
||||
|
||||
Nashel.Modules.Order.Domain.Aggregates.Order order;
|
||||
|
||||
if (request.Type == OrderType.Direct)
|
||||
{
|
||||
if (request.PerformerId == null)
|
||||
{
|
||||
throw new ArgumentException("PerformerId обязателен для прямого заказа.");
|
||||
}
|
||||
|
||||
order = Nashel.Modules.Order.Domain.Aggregates.Order.CreateDirect(
|
||||
request.CustomerId,
|
||||
request.ServiceId,
|
||||
request.PerformerId.Value,
|
||||
location,
|
||||
request.Deadline
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
order = Nashel.Modules.Order.Domain.Aggregates.Order.CreatePublic(
|
||||
request.CustomerId,
|
||||
request.ServiceId,
|
||||
location,
|
||||
request.Deadline
|
||||
);
|
||||
}
|
||||
|
||||
await _repository.AddAsync(order, cancellationToken);
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return order.Id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using MediatR;
|
||||
using Nashel.Modules.Order.Application.Services;
|
||||
using Nashel.Modules.Order.Domain.Repositories;
|
||||
|
||||
namespace Nashel.Modules.Order.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Команда выбора исполнителя.
|
||||
/// </summary>
|
||||
public record SelectPerformerCommand : IRequest<Unit>
|
||||
{
|
||||
/// <summary>
|
||||
/// Идентификатор заказа.
|
||||
/// </summary>
|
||||
public Guid OrderId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Идентификатор исполнителя.
|
||||
/// </summary>
|
||||
public Guid PerformerId { get; init; }
|
||||
|
||||
public SelectPerformerCommand(Guid orderId, Guid performerId)
|
||||
{
|
||||
OrderId = orderId;
|
||||
PerformerId = performerId;
|
||||
}
|
||||
|
||||
public SelectPerformerCommand() { }
|
||||
}
|
||||
|
||||
|
||||
public class SelectPerformerHandler : IRequestHandler<SelectPerformerCommand, Unit>
|
||||
{
|
||||
private readonly IOrderRepository _repository;
|
||||
private readonly ISlaTimerService _slaTimerService;
|
||||
|
||||
public SelectPerformerHandler(IOrderRepository repository, ISlaTimerService slaTimerService)
|
||||
{
|
||||
_repository = repository;
|
||||
_slaTimerService = slaTimerService;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(SelectPerformerCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var order = await _repository.GetByIdAsync(request.OrderId, cancellationToken);
|
||||
if (order == null)
|
||||
{
|
||||
throw new KeyNotFoundException($"Order {request.OrderId} not found");
|
||||
}
|
||||
|
||||
order.SelectPerformer(request.PerformerId);
|
||||
|
||||
// Запускаем SLA таймер на 60 минут
|
||||
await _slaTimerService.ScheduleAutoCancel(order.Id, TimeSpan.FromMinutes(60));
|
||||
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Nashel.Modules.Order.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Сервис управления SLA-таймерами.
|
||||
/// </summary>
|
||||
public interface ISlaTimerService
|
||||
{
|
||||
/// <summary>
|
||||
/// Запланировать автоматическую отмену заказа через заданное время.
|
||||
/// </summary>
|
||||
Task ScheduleAutoCancel(Guid orderId, TimeSpan delay);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using Nashel.BuildingBlocks.Domain;
|
||||
using Nashel.Modules.Order.Domain.Entities;
|
||||
using Nashel.Modules.Order.Domain.Enums;
|
||||
using Nashel.Modules.Order.Domain.ValueObjects;
|
||||
|
||||
namespace Nashel.Modules.Order.Domain.Aggregates;
|
||||
|
||||
/// <summary>
|
||||
/// Агрегат Заказ.
|
||||
/// </summary>
|
||||
public class Order : AggregateRoot<Guid>
|
||||
{
|
||||
public Guid CustomerId { get; private set; }
|
||||
public Guid ServiceId { get; private set; }
|
||||
public OrderType Type { get; private set; }
|
||||
public OrderStatus Status { get; private set; }
|
||||
public Guid? SelectedPerformerId { get; private set; }
|
||||
public DateTime? Deadline { get; private set; }
|
||||
public OrderLocation Location { get; private set; } = null!;
|
||||
|
||||
// Optimistic Concurrency
|
||||
public uint RowVersion { get; private set; }
|
||||
|
||||
private readonly List<OrderApplication> _applications = new();
|
||||
public IReadOnlyCollection<OrderApplication> Applications => _applications.AsReadOnly();
|
||||
|
||||
private Order() { }
|
||||
|
||||
/// <summary>
|
||||
/// Создание прямого заказа.
|
||||
/// </summary>
|
||||
public static Order CreateDirect(Guid customerId, Guid serviceId, Guid performerId, OrderLocation location, DateTime? deadline)
|
||||
{
|
||||
return new Order
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CustomerId = customerId,
|
||||
ServiceId = serviceId,
|
||||
Type = OrderType.Direct,
|
||||
Status = OrderStatus.PendingAcceptance, // Сразу ждет подтверждения
|
||||
SelectedPerformerId = performerId,
|
||||
Location = location,
|
||||
Deadline = deadline
|
||||
};
|
||||
// Для прямого заказа отклик можно создать косвенно или считать цену договорной.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создание публичной заявки.
|
||||
/// </summary>
|
||||
public static Order CreatePublic(Guid customerId, Guid serviceId, OrderLocation location, DateTime? deadline)
|
||||
{
|
||||
return new Order
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CustomerId = customerId,
|
||||
ServiceId = serviceId,
|
||||
Type = OrderType.PublicJob,
|
||||
Status = OrderStatus.Published, // Публикуется для поиска
|
||||
Location = location,
|
||||
Deadline = deadline
|
||||
};
|
||||
}
|
||||
|
||||
public void AddApplication(Guid performerId, Money price, string comment)
|
||||
{
|
||||
if (Status != OrderStatus.Published)
|
||||
{
|
||||
throw new InvalidOperationException("Нельзя оставить отклик на заказ, который не опубликован.");
|
||||
}
|
||||
|
||||
if (_applications.Any(a => a.PerformerId == performerId))
|
||||
{
|
||||
throw new InvalidOperationException("Исполнитель уже оставил отклик на этот заказ.");
|
||||
}
|
||||
|
||||
_applications.Add(new OrderApplication(performerId, price, comment));
|
||||
}
|
||||
|
||||
public void SelectPerformer(Guid performerId)
|
||||
{
|
||||
if (Status != OrderStatus.Published)
|
||||
{
|
||||
throw new InvalidOperationException("Выбрать исполнителя можно только для опубликованного заказа.");
|
||||
}
|
||||
|
||||
var app = _applications.FirstOrDefault(a => a.PerformerId == performerId);
|
||||
if (app == null)
|
||||
{
|
||||
throw new InvalidOperationException("Выбранный исполнитель не оставлял отклик.");
|
||||
}
|
||||
|
||||
SelectedPerformerId = performerId;
|
||||
Status = OrderStatus.PendingAcceptance;
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
Status = OrderStatus.Cancelled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Nashel.BuildingBlocks.Domain;
|
||||
using Nashel.Modules.Order.Domain.ValueObjects;
|
||||
|
||||
namespace Nashel.Modules.Order.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Отклик на заказ.
|
||||
/// </summary>
|
||||
public class OrderApplication : Entity<Guid>
|
||||
{
|
||||
public Guid PerformerId { get; private set; }
|
||||
public Money Price { get; private set; } = null!;
|
||||
public string Comment { get; private set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
|
||||
private OrderApplication() { }
|
||||
|
||||
public OrderApplication(Guid performerId, Money price, string comment)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
PerformerId = performerId;
|
||||
Price = price;
|
||||
Comment = comment;
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace Nashel.Modules.Order.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Статус заказа.
|
||||
/// </summary>
|
||||
public enum OrderStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Создан.
|
||||
/// </summary>
|
||||
Created,
|
||||
/// <summary>
|
||||
/// Опубликован (поиск исполнителей).
|
||||
/// </summary>
|
||||
Published,
|
||||
/// <summary>
|
||||
/// Ожидает подтверждения (выбран исполнитель).
|
||||
/// </summary>
|
||||
PendingAcceptance,
|
||||
/// <summary>
|
||||
/// В работе.
|
||||
/// </summary>
|
||||
InProgress,
|
||||
/// <summary>
|
||||
/// Завершен.
|
||||
/// </summary>
|
||||
Completed,
|
||||
/// <summary>
|
||||
/// Отменен.
|
||||
/// </summary>
|
||||
Cancelled
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Nashel.Modules.Order.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Тип заказа.
|
||||
/// </summary>
|
||||
public enum OrderType
|
||||
{
|
||||
/// <summary>
|
||||
/// Прямой заказ.
|
||||
/// </summary>
|
||||
Direct,
|
||||
/// <summary>
|
||||
/// Публичная заявка.
|
||||
/// </summary>
|
||||
PublicJob
|
||||
}
|
||||
@@ -4,6 +4,10 @@
|
||||
<ProjectReference Include="..\..\..\BuildingBlocks\Nashel.BuildingBlocks.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Nashel.Modules.Order.Domain</RootNamespace>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using OrderAggregate = Nashel.Modules.Order.Domain.Aggregates.Order;
|
||||
|
||||
namespace Nashel.Modules.Order.Domain.Repositories;
|
||||
|
||||
public interface IOrderRepository
|
||||
{
|
||||
Task<OrderAggregate?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task AddAsync(OrderAggregate order, CancellationToken cancellationToken = default);
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Nashel.Modules.Order.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Денежная сумма.
|
||||
/// </summary>
|
||||
public record Money(decimal Amount, string Currency)
|
||||
{
|
||||
public static Money Zero(string currency = "RUB") => new(0m, currency);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using NetTopologySuite.Geometries;
|
||||
|
||||
namespace Nashel.Modules.Order.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Локация заказа.
|
||||
/// </summary>
|
||||
public record OrderLocation(string Address, Point Point);
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Nashel.Modules.Order.Application.Commands;
|
||||
using Nashel.Modules.Order.Application.Services;
|
||||
using Nashel.Modules.Order.Domain.Repositories;
|
||||
using Nashel.Modules.Order.Infrastructure.Persistence;
|
||||
using Nashel.Modules.Order.Infrastructure.Persistence.Repositories;
|
||||
using Nashel.Modules.Order.Infrastructure.Services;
|
||||
using Npgsql;
|
||||
|
||||
namespace Nashel.Modules.Order.Infrastructure;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddOrderModule(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection");
|
||||
|
||||
services.AddDbContext<OrderDbContext>(options =>
|
||||
options.UseNpgsql(
|
||||
connectionString,
|
||||
o => o.UseNetTopologySuite()
|
||||
));
|
||||
|
||||
services.AddScoped<IOrderRepository, OrderRepository>();
|
||||
services.AddScoped<ISlaTimerService, DummySlaTimerService>();
|
||||
|
||||
services.AddMediatR(cfg =>
|
||||
{
|
||||
cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
// <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("20260210192734_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <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<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.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,93 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NetTopologySuite.Geometries;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Nashel.Modules.Order.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "ordering");
|
||||
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("Npgsql:PostgresExtension:postgis", ",,");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Orders",
|
||||
schema: "ordering",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CustomerId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ServiceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Type = table.Column<string>(type: "text", nullable: false),
|
||||
Status = table.Column<string>(type: "text", nullable: false),
|
||||
SelectedPerformerId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
Deadline = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
Address = table.Column<string>(type: "text", nullable: false),
|
||||
Location_Point = table.Column<Point>(type: "geography (point)", nullable: false),
|
||||
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Orders", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OrderApplications",
|
||||
schema: "ordering",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PerformerId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PriceAmount = table.Column<decimal>(type: "numeric", nullable: false),
|
||||
PriceCurrency = table.Column<string>(type: "character varying(3)", maxLength: 3, nullable: false),
|
||||
Comment = table.Column<string>(type: "text", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
OrderId = table.Column<Guid>(type: "uuid", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OrderApplications", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OrderApplications_Orders_OrderId",
|
||||
column: x => x.OrderId,
|
||||
principalSchema: "ordering",
|
||||
principalTable: "Orders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OrderApplications_OrderId",
|
||||
schema: "ordering",
|
||||
table: "OrderApplications",
|
||||
column: "OrderId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Orders_Location_Point",
|
||||
schema: "ordering",
|
||||
table: "Orders",
|
||||
column: "Location_Point")
|
||||
.Annotation("Npgsql:IndexMethod", "gist");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "OrderApplications",
|
||||
schema: "ordering");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Orders",
|
||||
schema: "ordering");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
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))]
|
||||
partial class OrderDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(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<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.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NetTopologySuite.IO.PostGis" Version="2.1.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\BuildingBlocks\Nashel.BuildingBlocks.csproj" />
|
||||
<ProjectReference Include="..\Application\Nashel.Modules.Order.Application.csproj" />
|
||||
<ProjectReference Include="..\Domain\Nashel.Modules.Order.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Nashel.Modules.Order.Infrastructure</RootNamespace>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,54 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Nashel.Modules.Order.Domain.Aggregates;
|
||||
using Nashel.Modules.Order.Domain.Entities;
|
||||
|
||||
namespace Nashel.Modules.Order.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class OrderConfiguration : IEntityTypeConfiguration<Nashel.Modules.Order.Domain.Aggregates.Order>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Nashel.Modules.Order.Domain.Aggregates.Order> builder)
|
||||
{
|
||||
builder.ToTable("Orders", "ordering");
|
||||
|
||||
builder.HasKey(o => o.Id);
|
||||
|
||||
// Optimistic Concurrency (xmin mapping for uint)
|
||||
builder.Property(o => o.RowVersion)
|
||||
.IsRowVersion();
|
||||
|
||||
builder.Property(o => o.Type)
|
||||
.HasConversion<string>();
|
||||
|
||||
builder.Property(o => o.Status)
|
||||
.HasConversion<string>();
|
||||
|
||||
builder.OwnsOne(o => o.Location, nav =>
|
||||
{
|
||||
nav.Property(l => l.Address).HasColumnName("Address");
|
||||
nav.Property(l => l.Point).HasColumnType("geography (point)");
|
||||
nav.HasIndex(l => l.Point).HasMethod("gist");
|
||||
});
|
||||
|
||||
builder.HasMany(o => o.Applications)
|
||||
.WithOne()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
public class OrderApplicationConfiguration : IEntityTypeConfiguration<OrderApplication>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<OrderApplication> builder)
|
||||
{
|
||||
builder.ToTable("OrderApplications", "ordering");
|
||||
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.OwnsOne(x => x.Price, money =>
|
||||
{
|
||||
money.Property(m => m.Amount).HasColumnName("PriceAmount");
|
||||
money.Property(m => m.Currency).HasColumnName("PriceCurrency").HasMaxLength(3);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Nashel.Modules.Order.Domain.Aggregates;
|
||||
using Nashel.Modules.Order.Domain.Entities;
|
||||
using Nashel.Modules.Order.Infrastructure.Persistence.Configurations;
|
||||
|
||||
namespace Nashel.Modules.Order.Infrastructure.Persistence;
|
||||
|
||||
public class OrderDbContext : DbContext
|
||||
{
|
||||
public OrderDbContext(DbContextOptions<OrderDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<Nashel.Modules.Order.Domain.Aggregates.Order> Orders { get; set; } = null!;
|
||||
public DbSet<OrderApplication> OrderApplications { get; set; } = null!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasDefaultSchema("ordering");
|
||||
modelBuilder.HasPostgresExtension("postgis");
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(OrderConfiguration).Assembly);
|
||||
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Nashel.Modules.Order.Domain.Repositories;
|
||||
using Nashel.Modules.Order.Infrastructure.Persistence;
|
||||
using OrderAggregate = Nashel.Modules.Order.Domain.Aggregates.Order;
|
||||
|
||||
namespace Nashel.Modules.Order.Infrastructure.Persistence.Repositories;
|
||||
|
||||
public class OrderRepository : IOrderRepository
|
||||
{
|
||||
private readonly OrderDbContext _context;
|
||||
|
||||
public OrderRepository(OrderDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task AddAsync(OrderAggregate order, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.Orders.AddAsync(order, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<OrderAggregate?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Orders
|
||||
.Include(o => o.Applications)
|
||||
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Nashel.Modules.Order.Application.Services;
|
||||
|
||||
namespace Nashel.Modules.Order.Infrastructure.Services;
|
||||
|
||||
public class DummySlaTimerService : ISlaTimerService
|
||||
{
|
||||
private readonly ILogger<DummySlaTimerService> _logger;
|
||||
|
||||
public DummySlaTimerService(ILogger<DummySlaTimerService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task ScheduleAutoCancel(Guid orderId, TimeSpan delay)
|
||||
{
|
||||
_logger.LogInformation("SLA Timer scheduled for Order {OrderId} in {Delay}.", orderId, delay);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Application\Nashel.Modules.Order.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Nashel.Modules.Order.Presentation</RootNamespace>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediatR" Version="14.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Application\Nashel.Modules.Order.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,87 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Nashel.Modules.Order.Application.Commands;
|
||||
using Nashel.Modules.Order.Domain.Enums;
|
||||
|
||||
namespace Nashel.Modules.Order.Presentation;
|
||||
|
||||
public static class OrderEndpoints
|
||||
{
|
||||
public static void MapOrderEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("/api/orders").WithTags("Orders");
|
||||
|
||||
group.MapPost("/", async (CreateOrderRequest request, ISender sender) =>
|
||||
{
|
||||
var command = new CreateOrderCommand(
|
||||
request.CustomerId,
|
||||
request.ServiceId,
|
||||
request.Type,
|
||||
request.Address,
|
||||
request.Latitude,
|
||||
request.Longitude,
|
||||
request.Deadline,
|
||||
request.PerformerId
|
||||
);
|
||||
var orderId = await sender.Send(command);
|
||||
return Results.Ok(orderId);
|
||||
})
|
||||
.WithName("CreateOrder")
|
||||
.WithOpenApi(operation => new(operation) { Summary = "Создание нового заказа.", Description = "Создает прямой или публичный заказ." });
|
||||
|
||||
group.MapPost("/{id:guid}/applications", async (Guid id, AddApplicationRequest request, ISender sender) =>
|
||||
{
|
||||
var command = new AddApplicationCommand(
|
||||
id,
|
||||
request.PerformerId,
|
||||
request.Amount,
|
||||
request.Currency,
|
||||
request.Comment
|
||||
);
|
||||
await sender.Send(command);
|
||||
return Results.Ok();
|
||||
})
|
||||
.WithName("AddOrderApplication")
|
||||
.WithOpenApi(operation => new(operation) { Summary = "Добавление отклика на заказ." });
|
||||
|
||||
group.MapPost("/{id:guid}/select-performer", async (Guid id, SelectPerformerRequest request, ISender sender) =>
|
||||
{
|
||||
var command = new SelectPerformerCommand(id, request.PerformerId);
|
||||
await sender.Send(command);
|
||||
return Results.Ok();
|
||||
})
|
||||
.WithName("SelectPerformer")
|
||||
.WithOpenApi(operation => new(operation) { Summary = "Выбор исполнителя для заказа." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Запрос на создание заказа.
|
||||
/// </summary>
|
||||
public record CreateOrderRequest(
|
||||
Guid CustomerId,
|
||||
Guid ServiceId,
|
||||
OrderType Type,
|
||||
string Address,
|
||||
double Latitude,
|
||||
double Longitude,
|
||||
DateTime? Deadline,
|
||||
Guid? PerformerId
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Запрос на добавление отклика.
|
||||
/// </summary>
|
||||
public record AddApplicationRequest(
|
||||
Guid PerformerId,
|
||||
decimal Amount,
|
||||
string Currency,
|
||||
string Comment
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Запрос на выбор исполнителя.
|
||||
/// </summary>
|
||||
public record SelectPerformerRequest(Guid PerformerId);
|
||||
@@ -0,0 +1,61 @@
|
||||
using FluentAssertions;
|
||||
using MediatR;
|
||||
using NSubstitute;
|
||||
using Nashel.Modules.Order.Application.Commands;
|
||||
using Nashel.Modules.Order.Application.Services;
|
||||
using Nashel.Modules.Order.Domain.Enums;
|
||||
using Nashel.Modules.Order.Domain.Repositories;
|
||||
using Nashel.Modules.Order.Domain.ValueObjects;
|
||||
using NetTopologySuite.Geometries;
|
||||
using Xunit;
|
||||
|
||||
namespace Nashel.Modules.Order.Tests.Application;
|
||||
|
||||
public class SelectPerformerHandlerTests
|
||||
{
|
||||
private readonly IOrderRepository _repository;
|
||||
private readonly ISlaTimerService _slaTimerService;
|
||||
private readonly SelectPerformerHandler _handler;
|
||||
|
||||
public SelectPerformerHandlerTests()
|
||||
{
|
||||
_repository = Substitute.For<IOrderRepository>();
|
||||
_slaTimerService = Substitute.For<ISlaTimerService>();
|
||||
_handler = new SelectPerformerHandler(_repository, _slaTimerService);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Should_Call_SlaTimer()
|
||||
{
|
||||
// Arrange
|
||||
var customerId = Guid.NewGuid();
|
||||
var serviceId = Guid.NewGuid();
|
||||
var performerId = Guid.NewGuid();
|
||||
var location = new OrderLocation("Test St", new Point(10, 20));
|
||||
|
||||
// Создаем публичный заказ (Status = Published)
|
||||
var order = Nashel.Modules.Order.Domain.Aggregates.Order.CreatePublic(
|
||||
customerId, serviceId, location, null);
|
||||
|
||||
// Добавляем отклик исполнителя
|
||||
order.AddApplication(performerId, new Money(100, "RUB"), "Comment");
|
||||
|
||||
// Mock Repository
|
||||
_repository.GetByIdAsync(order.Id, Arg.Any<CancellationToken>()).Returns(order);
|
||||
|
||||
var command = new SelectPerformerCommand(order.Id, performerId);
|
||||
|
||||
// Act
|
||||
await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
// Проверяем изменение статуса
|
||||
order.Status.Should().Be(OrderStatus.PendingAcceptance);
|
||||
|
||||
// Проверяем вызов SLA таймера (60 минут)
|
||||
await _slaTimerService.Received(1).ScheduleAutoCancel(order.Id, Arg.Is<TimeSpan>(t => t.TotalMinutes == 60));
|
||||
|
||||
// Проверяем сохранение в БД
|
||||
await _repository.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using FluentAssertions;
|
||||
using Nashel.Modules.Order.Domain.Enums;
|
||||
using Nashel.Modules.Order.Domain.ValueObjects;
|
||||
using NetTopologySuite.Geometries;
|
||||
using Xunit;
|
||||
|
||||
namespace Nashel.Modules.Order.Tests.Domain;
|
||||
|
||||
public class OrderAggregateTests
|
||||
{
|
||||
[Fact]
|
||||
public void CreateDirect_Should_SetStatus_PendingAcceptance()
|
||||
{
|
||||
// Arrange
|
||||
var customerId = Guid.NewGuid();
|
||||
var serviceId = Guid.NewGuid();
|
||||
var performerId = Guid.NewGuid();
|
||||
var location = new OrderLocation("Test St", new Point(10, 20));
|
||||
|
||||
// Act
|
||||
var order = Nashel.Modules.Order.Domain.Aggregates.Order.CreateDirect(
|
||||
customerId, serviceId, performerId, location, DateTime.UtcNow.AddDays(1));
|
||||
|
||||
// Assert
|
||||
order.Status.Should().Be(OrderStatus.PendingAcceptance);
|
||||
order.SelectedPerformerId.Should().Be(performerId);
|
||||
order.Type.Should().Be(OrderType.Direct);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddApplication_Should_Throw_If_OrderNotPublished()
|
||||
{
|
||||
// Arrange
|
||||
var customerId = Guid.NewGuid();
|
||||
var serviceId = Guid.NewGuid();
|
||||
var performerId = Guid.NewGuid();
|
||||
var location = new OrderLocation("Test St", new Point(10, 20));
|
||||
// Direct order created in PendingAcceptance
|
||||
var order = Nashel.Modules.Order.Domain.Aggregates.Order.CreateDirect(
|
||||
customerId, serviceId, performerId, location, null);
|
||||
|
||||
// Act
|
||||
var action = () => order.AddApplication(Guid.NewGuid(), new Money(100, "RUB"), "Comment");
|
||||
|
||||
// Assert
|
||||
action.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("Нельзя оставить отклик на заказ, который не опубликован.*"); // Regex match
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectPerformer_Should_ChangeStatus_To_Pending()
|
||||
{
|
||||
// Arrange
|
||||
var customerId = Guid.NewGuid();
|
||||
var serviceId = Guid.NewGuid();
|
||||
var location = new OrderLocation("Test St", new Point(10, 20));
|
||||
var order = Nashel.Modules.Order.Domain.Aggregates.Order.CreatePublic(
|
||||
customerId, serviceId, location, null);
|
||||
|
||||
var performerId = Guid.NewGuid();
|
||||
order.AddApplication(performerId, new Money(100, "RUB"), "Comment");
|
||||
|
||||
// Act
|
||||
order.SelectPerformer(performerId);
|
||||
|
||||
// Assert
|
||||
order.Status.Should().Be(OrderStatus.PendingAcceptance);
|
||||
order.SelectedPerformerId.Should().Be(performerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions">
|
||||
<Version>8.8.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk">
|
||||
<Version>18.0.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NetTopologySuite">
|
||||
<Version>2.6.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NSubstitute">
|
||||
<Version>5.3.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="xunit">
|
||||
<Version>2.9.3</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<Version>3.1.5</Version>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Domain\Nashel.Modules.Order.Domain.csproj" />
|
||||
<ProjectReference Include="..\Application\Nashel.Modules.Order.Application.csproj" />
|
||||
<ProjectReference Include="..\Infrastructure\Nashel.Modules.Order.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Implicit using of FluentAssertions might be useful but explicit is safer -->
|
||||
</Project>
|
||||
Reference in New Issue
Block a user