Реализация и тесты Geo модуля
This commit is contained in:
@@ -140,3 +140,9 @@ src/Modules/Identity/Tests/obj/
|
|||||||
src/Modules/Catalog/Tests/bin/
|
src/Modules/Catalog/Tests/bin/
|
||||||
|
|
||||||
src/Modules/Catalog/Tests/obj/
|
src/Modules/Catalog/Tests/obj/
|
||||||
|
|
||||||
|
src/Modules/Geo/Tests/bin/
|
||||||
|
|
||||||
|
src/Modules/Geo/Tests/obj/
|
||||||
|
|
||||||
|
src/Modules/Geo/Presentation/bin/
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ using Microsoft.IdentityModel.Tokens;
|
|||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Nashel.Modules.Catalog.Infrastructure;
|
using Nashel.Modules.Catalog.Infrastructure;
|
||||||
using Nashel.Modules.Catalog.Presentation.Endpoints;
|
using Nashel.Modules.Catalog.Presentation.Endpoints;
|
||||||
|
using Nashel.Modules.Geo.Infrastructure;
|
||||||
|
using Nashel.Modules.Geo.Presentation;
|
||||||
using Nashel.Modules.Identity.Infrastructure;
|
using Nashel.Modules.Identity.Infrastructure;
|
||||||
using Nashel.Modules.Identity.Presentation.Endpoints;
|
using Nashel.Modules.Identity.Presentation.Endpoints;
|
||||||
|
|
||||||
@@ -53,6 +55,9 @@ builder.Services.AddIdentityModule(builder.Configuration);
|
|||||||
// Регистрация модуля Catalog
|
// Регистрация модуля Catalog
|
||||||
builder.Services.AddCatalogModule(builder.Configuration);
|
builder.Services.AddCatalogModule(builder.Configuration);
|
||||||
|
|
||||||
|
// Регистрация модуля Geo
|
||||||
|
builder.Services.AddGeoModule(builder.Configuration);
|
||||||
|
|
||||||
// Настройка аутентификации
|
// Настройка аутентификации
|
||||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
.AddJwtBearer(options =>
|
.AddJwtBearer(options =>
|
||||||
@@ -90,5 +95,6 @@ app.UseAuthorization();
|
|||||||
// Маппинг эндпоинтов
|
// Маппинг эндпоинтов
|
||||||
app.MapIdentityEndpoints();
|
app.MapIdentityEndpoints();
|
||||||
app.MapCatalogEndpoints();
|
app.MapCatalogEndpoints();
|
||||||
|
app.MapGeoEndpoints();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Nashel.Modules.Geo.Domain.Aggregates;
|
||||||
|
using Nashel.Modules.Geo.Domain.Enums;
|
||||||
|
using Nashel.Modules.Geo.Domain.Repositories;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Geo.Application.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Команда обновления координат и статуса.
|
||||||
|
/// </summary>
|
||||||
|
public record UpdateGeoCommand : IRequest<Unit>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Идентификатор исполнителя.
|
||||||
|
/// </summary>
|
||||||
|
public Guid PerformerId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Широта.
|
||||||
|
/// </summary>
|
||||||
|
public double Latitude { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Долгота.
|
||||||
|
/// </summary>
|
||||||
|
public double Longitude { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Новый статус.
|
||||||
|
/// </summary>
|
||||||
|
public PerformerStatus Status { get; init; }
|
||||||
|
|
||||||
|
public UpdateGeoCommand(Guid performerId, double latitude, double longitude, PerformerStatus status)
|
||||||
|
{
|
||||||
|
PerformerId = performerId;
|
||||||
|
Latitude = latitude;
|
||||||
|
Longitude = longitude;
|
||||||
|
Status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UpdateGeoCommand() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateGeoCommandHandler : IRequestHandler<UpdateGeoCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IGeoRepository _repository;
|
||||||
|
|
||||||
|
public UpdateGeoCommandHandler(IGeoRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(UpdateGeoCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var liveStatus = await _repository.GetByPerformerIdAsync(request.PerformerId, cancellationToken);
|
||||||
|
bool isNew = false;
|
||||||
|
|
||||||
|
if (liveStatus == null)
|
||||||
|
{
|
||||||
|
liveStatus = new LiveStatus(request.PerformerId, request.Latitude, request.Longitude);
|
||||||
|
isNew = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
liveStatus.UpdateLocation(request.Latitude, request.Longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (request.Status)
|
||||||
|
{
|
||||||
|
case PerformerStatus.Available:
|
||||||
|
liveStatus.GoOnline();
|
||||||
|
break;
|
||||||
|
case PerformerStatus.Busy:
|
||||||
|
// Установим дефолтное время занятости 1 час, если не указано иное
|
||||||
|
liveStatus.SetBusy(DateTime.UtcNow.AddHours(1));
|
||||||
|
break;
|
||||||
|
case PerformerStatus.DayOff:
|
||||||
|
liveStatus.GoOffline();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNew)
|
||||||
|
{
|
||||||
|
await _repository.AddAsync(liveStatus, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _repository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,5 +11,7 @@
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Nashel.Modules.Geo.Domain.Repositories;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Geo.Application.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Запрос на поиск исполнителей в радиусе.
|
||||||
|
/// </summary>
|
||||||
|
public record SearchNearbyQuery : IRequest<List<Guid>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Широта центра поиска.
|
||||||
|
/// </summary>
|
||||||
|
public double Latitude { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Долгота центра поиска.
|
||||||
|
/// </summary>
|
||||||
|
public double Longitude { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Радиус поиска (в метрах).
|
||||||
|
/// </summary>
|
||||||
|
public double RadiusInMeters { get; init; }
|
||||||
|
|
||||||
|
public SearchNearbyQuery(double latitude, double longitude, double radiusInMeters)
|
||||||
|
{
|
||||||
|
Latitude = latitude;
|
||||||
|
Longitude = longitude;
|
||||||
|
RadiusInMeters = radiusInMeters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchNearbyQuery() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SearchNearbyQueryHandler : IRequestHandler<SearchNearbyQuery, List<Guid>>
|
||||||
|
{
|
||||||
|
private readonly IGeoRepository _repository;
|
||||||
|
|
||||||
|
public SearchNearbyQueryHandler(IGeoRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Guid>> Handle(SearchNearbyQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _repository.GetNearbyAsync(request.Latitude, request.Longitude, request.RadiusInMeters, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using Nashel.BuildingBlocks.Domain;
|
||||||
|
using Nashel.Modules.Geo.Domain.Enums;
|
||||||
|
using NetTopologySuite.Geometries;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Geo.Domain.Aggregates;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Агрегат, хранящий текущее статус и локацию исполнителя.
|
||||||
|
/// </summary>
|
||||||
|
public class LiveStatus : AggregateRoot<Guid>
|
||||||
|
{
|
||||||
|
private LiveStatus() { } // EF Core
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает новый статус.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="performerId">ID исполнителя.</param>
|
||||||
|
/// <param name="latitude">Широта (Lat).</param>
|
||||||
|
/// <param name="longitude">Долгота (Lon).</param>
|
||||||
|
public LiveStatus(Guid performerId, double latitude, double longitude)
|
||||||
|
{
|
||||||
|
Id = performerId;
|
||||||
|
Location = new Point(longitude, latitude) { SRID = 4326 };
|
||||||
|
Status = PerformerStatus.Available;
|
||||||
|
LastUpdated = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Геопозиция исполнителя. SRID 4326 (WGS 84).
|
||||||
|
/// </summary>
|
||||||
|
public Point Location { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Статус доступности.
|
||||||
|
/// </summary>
|
||||||
|
public PerformerStatus Status { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Время, до которого исполнитель занят (UTC).
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? BusyUntil { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Время последнего обновления (UTC).
|
||||||
|
/// </summary>
|
||||||
|
public DateTime LastUpdated { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обновляет координаты.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lat">Широта.</param>
|
||||||
|
/// <param name="lon">Долгота.</param>
|
||||||
|
public void UpdateLocation(double lat, double lon)
|
||||||
|
{
|
||||||
|
Location = new Point(lon, lat) { SRID = 4326 };
|
||||||
|
LastUpdated = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Устанавливает статус "Занят" до указанного времени.
|
||||||
|
/// </summary>
|
||||||
|
public void SetBusy(DateTime until)
|
||||||
|
{
|
||||||
|
Status = PerformerStatus.Busy;
|
||||||
|
BusyUntil = until;
|
||||||
|
LastUpdated = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Переводит в статус "Доступен".
|
||||||
|
/// </summary>
|
||||||
|
public void GoOnline()
|
||||||
|
{
|
||||||
|
Status = PerformerStatus.Available;
|
||||||
|
BusyUntil = null;
|
||||||
|
LastUpdated = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Переводит в статус "Выходной" (офлайн).
|
||||||
|
/// </summary>
|
||||||
|
public void GoOffline()
|
||||||
|
{
|
||||||
|
Status = PerformerStatus.DayOff;
|
||||||
|
BusyUntil = null;
|
||||||
|
LastUpdated = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Nashel.Modules.Geo.Domain.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Статус доступности исполнителя.
|
||||||
|
/// </summary>
|
||||||
|
public enum PerformerStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Доступен для новых заказов.
|
||||||
|
/// </summary>
|
||||||
|
Available,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Занят выполнением заказа.
|
||||||
|
/// </summary>
|
||||||
|
Busy,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Выходной или не на смене.
|
||||||
|
/// </summary>
|
||||||
|
DayOff
|
||||||
|
}
|
||||||
@@ -4,11 +4,18 @@
|
|||||||
<ProjectReference Include="..\..\..\BuildingBlocks\Nashel.BuildingBlocks.csproj" />
|
<ProjectReference Include="..\..\..\BuildingBlocks\Nashel.BuildingBlocks.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
|
||||||
|
<PackageReference Include="NetTopologySuite.IO.PostGis" Version="2.1.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<RootNamespace>Nashel.Modules.Geo.Domain</RootNamespace>
|
<RootNamespace>Nashel.Modules.Geo.Domain</RootNamespace>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Nashel.Modules.Geo.Domain.Aggregates;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Geo.Domain.Repositories;
|
||||||
|
|
||||||
|
public interface IGeoRepository
|
||||||
|
{
|
||||||
|
Task<LiveStatus?> GetByPerformerIdAsync(Guid performerId, CancellationToken cancellationToken = default);
|
||||||
|
Task AddAsync(LiveStatus liveStatus, CancellationToken cancellationToken = default);
|
||||||
|
Task<List<Guid>> GetNearbyAsync(double lat, double lon, double radiusInMeters, CancellationToken cancellationToken = default);
|
||||||
|
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Nashel.Modules.Geo.Application.Commands;
|
||||||
|
using Nashel.Modules.Geo.Domain.Repositories;
|
||||||
|
using Nashel.Modules.Geo.Infrastructure.Persistence;
|
||||||
|
using Nashel.Modules.Geo.Infrastructure.Persistence.Repositories;
|
||||||
|
using Npgsql;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Geo.Infrastructure;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddGeoModule(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.AddDbContext<GeoDbContext>(options =>
|
||||||
|
options.UseNpgsql(
|
||||||
|
configuration.GetConnectionString("DefaultConnection"),
|
||||||
|
x => x.UseNetTopologySuite()
|
||||||
|
));
|
||||||
|
|
||||||
|
services.AddScoped<IGeoRepository, GeoRepository>();
|
||||||
|
|
||||||
|
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(UpdateGeoCommand).Assembly));
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Nashel.Modules.Geo.Infrastructure.Persistence;
|
||||||
|
using NetTopologySuite.Geometries;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Geo.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(GeoDbContext))]
|
||||||
|
[Migration("20260210181606_InitialCreate")]
|
||||||
|
partial class InitialCreate
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.2")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Geo.Domain.Aggregates.LiveStatus", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("BusyUntil")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Point>("Location")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("geography (point)");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Location");
|
||||||
|
|
||||||
|
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Location"), "gist");
|
||||||
|
|
||||||
|
b.ToTable("LiveStatuses", "geo");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using NetTopologySuite.Geometries;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Geo.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialCreate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.EnsureSchema(
|
||||||
|
name: "geo");
|
||||||
|
|
||||||
|
migrationBuilder.AlterDatabase()
|
||||||
|
.Annotation("Npgsql:PostgresExtension:postgis", ",,");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "LiveStatuses",
|
||||||
|
schema: "geo",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Location = table.Column<Point>(type: "geography (point)", nullable: false),
|
||||||
|
Status = table.Column<string>(type: "text", nullable: false),
|
||||||
|
BusyUntil = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
LastUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_LiveStatuses", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_LiveStatuses_Location",
|
||||||
|
schema: "geo",
|
||||||
|
table: "LiveStatuses",
|
||||||
|
column: "Location")
|
||||||
|
.Annotation("Npgsql:IndexMethod", "gist");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "LiveStatuses",
|
||||||
|
schema: "geo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Nashel.Modules.Geo.Infrastructure.Persistence;
|
||||||
|
using NetTopologySuite.Geometries;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Geo.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(GeoDbContext))]
|
||||||
|
partial class GeoDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.2")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Geo.Domain.Aggregates.LiveStatus", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("BusyUntil")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Point>("Location")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("geography (point)");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Location");
|
||||||
|
|
||||||
|
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Location"), "gist");
|
||||||
|
|
||||||
|
b.ToTable("LiveStatuses", "geo");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<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" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<RootNamespace>Nashel.Modules.Geo.Infrastructure</RootNamespace>
|
<RootNamespace>Nashel.Modules.Geo.Infrastructure</RootNamespace>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using Nashel.Modules.Geo.Domain.Aggregates;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Geo.Infrastructure.Persistence.Configurations;
|
||||||
|
|
||||||
|
public class LiveStatusConfiguration : IEntityTypeConfiguration<LiveStatus>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<LiveStatus> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("LiveStatuses", "geo");
|
||||||
|
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
|
||||||
|
// Маппинг Location в geography(point)
|
||||||
|
builder.Property(x => x.Location)
|
||||||
|
.HasColumnType("geography (point)")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
// Пространственный индекс (GiST)
|
||||||
|
builder.HasIndex(x => x.Location)
|
||||||
|
.HasMethod("gist");
|
||||||
|
|
||||||
|
builder.Property(x => x.Status)
|
||||||
|
.HasConversion<string>();
|
||||||
|
|
||||||
|
builder.Property(x => x.BusyUntil)
|
||||||
|
.IsRequired(false);
|
||||||
|
|
||||||
|
builder.Property(x => x.LastUpdated)
|
||||||
|
.IsRequired();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Nashel.Modules.Geo.Domain.Aggregates;
|
||||||
|
using Nashel.Modules.Geo.Infrastructure.Persistence.Configurations;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Geo.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
public class GeoDbContext : DbContext
|
||||||
|
{
|
||||||
|
public GeoDbContext(DbContextOptions<GeoDbContext> options) : base(options) { }
|
||||||
|
|
||||||
|
public DbSet<LiveStatus> LiveStatuses { get; set; }
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.HasPostgresExtension("postgis");
|
||||||
|
modelBuilder.ApplyConfiguration(new LiveStatusConfiguration());
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Nashel.Modules.Geo.Domain.Aggregates;
|
||||||
|
using Nashel.Modules.Geo.Domain.Enums;
|
||||||
|
using Nashel.Modules.Geo.Domain.Repositories;
|
||||||
|
using Nashel.Modules.Geo.Infrastructure.Persistence;
|
||||||
|
using NetTopologySuite.Geometries;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Geo.Infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
|
public class GeoRepository : IGeoRepository
|
||||||
|
{
|
||||||
|
private readonly GeoDbContext _context;
|
||||||
|
|
||||||
|
public GeoRepository(GeoDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LiveStatus?> GetByPerformerIdAsync(Guid performerId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.LiveStatuses
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == performerId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddAsync(LiveStatus liveStatus, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _context.LiveStatuses.AddAsync(liveStatus, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Guid>> GetNearbyAsync(double lat, double lon, double radiusInMeters, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var location = new Point(lon, lat) { SRID = 4326 };
|
||||||
|
return await _context.LiveStatuses
|
||||||
|
.Where(x => x.Status == PerformerStatus.Available && x.Location.IsWithinDistance(location, radiusInMeters))
|
||||||
|
.Select(x => x.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Nashel.Modules.Geo.Application.Commands;
|
||||||
|
using Nashel.Modules.Geo.Application.Queries;
|
||||||
|
using Nashel.Modules.Geo.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Geo.Presentation;
|
||||||
|
|
||||||
|
public static class GeoEndpoints
|
||||||
|
{
|
||||||
|
public static void MapGeoEndpoints(this IEndpointRouteBuilder app)
|
||||||
|
{
|
||||||
|
var group = app.MapGroup("api/geo").WithTags("Geo");
|
||||||
|
|
||||||
|
// PATCH /api/geo/status
|
||||||
|
group.MapPatch("status", async (
|
||||||
|
[FromBody] UpdateGeoRequest request,
|
||||||
|
ISender sender,
|
||||||
|
CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
var command = new UpdateGeoCommand(request.PerformerId, request.Latitude, request.Longitude, request.Status);
|
||||||
|
await sender.Send(command, ct);
|
||||||
|
return Results.Ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/geo/nearby
|
||||||
|
group.MapGet("nearby", async (
|
||||||
|
[FromQuery] double lat,
|
||||||
|
[FromQuery] double lon,
|
||||||
|
[FromQuery] double radius,
|
||||||
|
ISender sender,
|
||||||
|
CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
var result = await sender.Send(new SearchNearbyQuery(lat, lon, radius), ct);
|
||||||
|
return Results.Ok(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Запрос на обновление геопозиции и статуса исполнителя.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="PerformerId">Идентификатор исполнителя.</param>
|
||||||
|
/// <param name="Latitude">Широта.</param>
|
||||||
|
/// <param name="Longitude">Долгота.</param>
|
||||||
|
/// <param name="Status">Новый статус (0 = Available, 1 = Busy, 2 = DayOff).</param>
|
||||||
|
public record UpdateGeoRequest(Guid PerformerId, double Latitude, double Longitude, PerformerStatus Status);
|
||||||
|
}
|
||||||
@@ -7,5 +7,11 @@
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
-418
@@ -1,418 +0,0 @@
|
|||||||
{
|
|
||||||
"runtimeTarget": {
|
|
||||||
"name": ".NETCoreApp,Version=v10.0",
|
|
||||||
"signature": ""
|
|
||||||
},
|
|
||||||
"compilationOptions": {},
|
|
||||||
"targets": {
|
|
||||||
".NETCoreApp,Version=v10.0": {
|
|
||||||
"Nashel.Modules.Geo.Presentation/1.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Nashel.Modules.Geo.Application": "1.0.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"Nashel.Modules.Geo.Presentation.dll": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"FluentValidation/12.1.1": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net8.0/FluentValidation.dll": {
|
|
||||||
"assemblyVersion": "12.0.0.0",
|
|
||||||
"fileVersion": "12.1.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MediatR/14.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"MediatR.Contracts": "2.0.1",
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.2",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.2",
|
|
||||||
"Microsoft.IdentityModel.JsonWebTokens": "8.14.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/MediatR.dll": {
|
|
||||||
"assemblyVersion": "14.0.0.0",
|
|
||||||
"fileVersion": "14.0.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MediatR.Contracts/2.0.1": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/netstandard2.0/MediatR.Contracts.dll": {
|
|
||||||
"assemblyVersion": "2.0.1.0",
|
|
||||||
"fileVersion": "2.0.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore/10.0.2": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.EntityFrameworkCore.Abstractions": "10.0.2",
|
|
||||||
"Microsoft.Extensions.Caching.Memory": "10.0.2",
|
|
||||||
"Microsoft.Extensions.Logging": "10.0.2"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.EntityFrameworkCore.dll": {
|
|
||||||
"assemblyVersion": "10.0.2.0",
|
|
||||||
"fileVersion": "10.0.225.61305"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Abstractions/10.0.2": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.EntityFrameworkCore.Abstractions.dll": {
|
|
||||||
"assemblyVersion": "10.0.2.0",
|
|
||||||
"fileVersion": "10.0.225.61305"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Relational/10.0.2": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.EntityFrameworkCore": "10.0.2",
|
|
||||||
"Microsoft.Extensions.Caching.Memory": "10.0.2",
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.2",
|
|
||||||
"Microsoft.Extensions.Logging": "10.0.2"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.EntityFrameworkCore.Relational.dll": {
|
|
||||||
"assemblyVersion": "10.0.2.0",
|
|
||||||
"fileVersion": "10.0.225.61305"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Caching.Abstractions/10.0.2": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Primitives": "10.0.2"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.Extensions.Caching.Abstractions.dll": {
|
|
||||||
"assemblyVersion": "10.0.0.0",
|
|
||||||
"fileVersion": "10.0.225.61305"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Caching.Memory/10.0.2": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Caching.Abstractions": "10.0.2",
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.2",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.2",
|
|
||||||
"Microsoft.Extensions.Options": "10.0.2",
|
|
||||||
"Microsoft.Extensions.Primitives": "10.0.2"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.Extensions.Caching.Memory.dll": {
|
|
||||||
"assemblyVersion": "10.0.0.0",
|
|
||||||
"fileVersion": "10.0.225.61305"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions/10.0.2": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Primitives": "10.0.2"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.Extensions.Configuration.Abstractions.dll": {
|
|
||||||
"assemblyVersion": "10.0.0.0",
|
|
||||||
"fileVersion": "10.0.225.61305"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.DependencyInjection/10.0.2": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.2"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.Extensions.DependencyInjection.dll": {
|
|
||||||
"assemblyVersion": "10.0.0.0",
|
|
||||||
"fileVersion": "10.0.225.61305"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions/10.0.2": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
|
|
||||||
"assemblyVersion": "10.0.0.0",
|
|
||||||
"fileVersion": "10.0.225.61305"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Logging/10.0.2": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection": "10.0.2",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.2",
|
|
||||||
"Microsoft.Extensions.Options": "10.0.2"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.Extensions.Logging.dll": {
|
|
||||||
"assemblyVersion": "10.0.0.0",
|
|
||||||
"fileVersion": "10.0.225.61305"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions/10.0.2": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.2"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.Extensions.Logging.Abstractions.dll": {
|
|
||||||
"assemblyVersion": "10.0.0.0",
|
|
||||||
"fileVersion": "10.0.225.61305"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Options/10.0.2": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.2",
|
|
||||||
"Microsoft.Extensions.Primitives": "10.0.2"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.Extensions.Options.dll": {
|
|
||||||
"assemblyVersion": "10.0.0.0",
|
|
||||||
"fileVersion": "10.0.225.61305"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Primitives/10.0.2": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.Extensions.Primitives.dll": {
|
|
||||||
"assemblyVersion": "10.0.0.0",
|
|
||||||
"fileVersion": "10.0.225.61305"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.Abstractions/8.14.0": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net9.0/Microsoft.IdentityModel.Abstractions.dll": {
|
|
||||||
"assemblyVersion": "8.14.0.0",
|
|
||||||
"fileVersion": "8.14.0.60815"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.JsonWebTokens/8.14.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.IdentityModel.Tokens": "8.14.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net9.0/Microsoft.IdentityModel.JsonWebTokens.dll": {
|
|
||||||
"assemblyVersion": "8.14.0.0",
|
|
||||||
"fileVersion": "8.14.0.60815"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.Logging/8.14.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.IdentityModel.Abstractions": "8.14.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net9.0/Microsoft.IdentityModel.Logging.dll": {
|
|
||||||
"assemblyVersion": "8.14.0.0",
|
|
||||||
"fileVersion": "8.14.0.60815"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.Tokens/8.14.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.2",
|
|
||||||
"Microsoft.IdentityModel.Logging": "8.14.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net9.0/Microsoft.IdentityModel.Tokens.dll": {
|
|
||||||
"assemblyVersion": "8.14.0.0",
|
|
||||||
"fileVersion": "8.14.0.60815"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Nashel.BuildingBlocks/1.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"FluentValidation": "12.1.1",
|
|
||||||
"MediatR": "14.0.0",
|
|
||||||
"Microsoft.EntityFrameworkCore.Relational": "10.0.2"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"Nashel.BuildingBlocks.dll": {
|
|
||||||
"assemblyVersion": "1.0.0.0",
|
|
||||||
"fileVersion": "1.0.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Nashel.Modules.Geo.Application/1.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"MediatR": "14.0.0",
|
|
||||||
"Nashel.BuildingBlocks": "1.0.0",
|
|
||||||
"Nashel.Modules.Geo.Domain": "1.0.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"Nashel.Modules.Geo.Application.dll": {
|
|
||||||
"assemblyVersion": "1.0.0.0",
|
|
||||||
"fileVersion": "1.0.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Nashel.Modules.Geo.Domain/1.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Nashel.BuildingBlocks": "1.0.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"Nashel.Modules.Geo.Domain.dll": {
|
|
||||||
"assemblyVersion": "1.0.0.0",
|
|
||||||
"fileVersion": "1.0.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"libraries": {
|
|
||||||
"Nashel.Modules.Geo.Presentation/1.0.0": {
|
|
||||||
"type": "project",
|
|
||||||
"serviceable": false,
|
|
||||||
"sha512": ""
|
|
||||||
},
|
|
||||||
"FluentValidation/12.1.1": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==",
|
|
||||||
"path": "fluentvalidation/12.1.1",
|
|
||||||
"hashPath": "fluentvalidation.12.1.1.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"MediatR/14.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-r5fwUO6NBvOFKaiMRx/gRrD1MEHHOio5yEdzSLs2OMeD2e9ZKnZaBQM6A6vVBEWJF4VY41vplXmDMOY1YvpqNA==",
|
|
||||||
"path": "mediatr/14.0.0",
|
|
||||||
"hashPath": "mediatr.14.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"MediatR.Contracts/2.0.1": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==",
|
|
||||||
"path": "mediatr.contracts/2.0.1",
|
|
||||||
"hashPath": "mediatr.contracts.2.0.1.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore/10.0.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-d3+XKbLSHPCu3vwpXECoXcFbvGKmAhEeUmc1xy2czmuPnEF7rZN2HP5ZGMwCMbAKk4B01+nS4HixSMo2Vf/Y9g==",
|
|
||||||
"path": "microsoft.entityframeworkcore/10.0.2",
|
|
||||||
"hashPath": "microsoft.entityframeworkcore.10.0.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Abstractions/10.0.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-BzAwIU5mYeOmnKbEXrkwx7feW2V+zUTrK/kRonSib94tjvc0/iRj2a4N6YGXRhTNjaFP3tvCMIDaX1vIFF6dkg==",
|
|
||||||
"path": "microsoft.entityframeworkcore.abstractions/10.0.2",
|
|
||||||
"hashPath": "microsoft.entityframeworkcore.abstractions.10.0.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Relational/10.0.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-1fUeyNmqDNfMogJ2ut7OKO57/WGjjkHMYeX51SpA3PwP7ftbx8g/Z3fbErD+1q14DILrqJfsszYsYhGssBRfDg==",
|
|
||||||
"path": "microsoft.entityframeworkcore.relational/10.0.2",
|
|
||||||
"hashPath": "microsoft.entityframeworkcore.relational.10.0.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Caching.Abstractions/10.0.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-WIRPDa/qoKHmJhTAPCO/zLu9kRLQ2Fd6HD5tzgdXJ3xGEVXDHP6FvakKJjynwKrVDld8H4G4tcbW53wuC/wxMQ==",
|
|
||||||
"path": "microsoft.extensions.caching.abstractions/10.0.2",
|
|
||||||
"hashPath": "microsoft.extensions.caching.abstractions.10.0.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Caching.Memory/10.0.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-MkdPYdtsu0Ta4m9Di4XnWVdO9u+wi1LtvisoR1EteIxsXWO/+3iyAPH6RZbw2lBlWZu9lastbl2YsHVIaL9j+g==",
|
|
||||||
"path": "microsoft.extensions.caching.memory/10.0.2",
|
|
||||||
"hashPath": "microsoft.extensions.caching.memory.10.0.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions/10.0.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-KC5PslaTDnTuTvyke0KYAVBYdZ7IVTsU3JhHe69BpEbHLcj1YThP3bIGtZNOkZfast2AuLnul5lk4rZKxAdUGQ==",
|
|
||||||
"path": "microsoft.extensions.configuration.abstractions/10.0.2",
|
|
||||||
"hashPath": "microsoft.extensions.configuration.abstractions.10.0.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.DependencyInjection/10.0.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-J/Zmp6fY93JbaiZ11ckWvcyxMPjD6XVwIHQXBjryTBgn7O6O20HYg9uVLFcZlNfgH78MnreE/7EH+hjfzn7VyA==",
|
|
||||||
"path": "microsoft.extensions.dependencyinjection/10.0.2",
|
|
||||||
"hashPath": "microsoft.extensions.dependencyinjection.10.0.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions/10.0.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-zOIurr59+kUf9vNcsUkCvKWZv+fPosUZXURZesYkJCvl0EzTc9F7maAO4Cd2WEV7ZJJ0AZrFQvuH6Npph9wdBw==",
|
|
||||||
"path": "microsoft.extensions.dependencyinjection.abstractions/10.0.2",
|
|
||||||
"hashPath": "microsoft.extensions.dependencyinjection.abstractions.10.0.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Logging/10.0.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-a0EWuBs6D3d7XMGroDXm+WsAi5CVVfjOJvyxurzWnuhBN9CO+1qHKcrKV1JK7H/T4ZtHIoVCOX/YyWM8K87qtw==",
|
|
||||||
"path": "microsoft.extensions.logging/10.0.2",
|
|
||||||
"hashPath": "microsoft.extensions.logging.10.0.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions/10.0.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-RZkez/JjpnO+MZ6efKkSynN6ZztLpw3WbxNzjLCPBd97wWj1S9ZYPWi0nmT4kWBRa6atHsdM1ydGkUr8GudyDQ==",
|
|
||||||
"path": "microsoft.extensions.logging.abstractions/10.0.2",
|
|
||||||
"hashPath": "microsoft.extensions.logging.abstractions.10.0.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Options/10.0.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-1De2LJjmxdqopI5AYC5dIhoZQ79AR5ayywxNF1rXrXFtKQfbQOV9+n/IsZBa7qWlr0MqoGpW8+OY2v/57udZOA==",
|
|
||||||
"path": "microsoft.extensions.options/10.0.2",
|
|
||||||
"hashPath": "microsoft.extensions.options.10.0.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Primitives/10.0.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-QmSiO+oLBEooGgB3i0GRXyeYRDHjllqt3k365jwfZlYWhvSHA3UL2NEVV5m8aZa041eIlblo6KMI5txvTMpTwA==",
|
|
||||||
"path": "microsoft.extensions.primitives/10.0.2",
|
|
||||||
"hashPath": "microsoft.extensions.primitives.10.0.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.Abstractions/8.14.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-iwbCpSjD3ehfTwBhtSNEtKPK0ICun6ov7Ibx6ISNA9bfwIyzI2Siwyi9eJFCJBwxowK9xcA1mj+jBWiigeqgcQ==",
|
|
||||||
"path": "microsoft.identitymodel.abstractions/8.14.0",
|
|
||||||
"hashPath": "microsoft.identitymodel.abstractions.8.14.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.JsonWebTokens/8.14.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-4jOpiA4THdtpLyMdAb24dtj7+6GmvhOhxf5XHLYWmPKF8ApEnApal1UnJsKO4HxUWRXDA6C4WQVfYyqsRhpNpQ==",
|
|
||||||
"path": "microsoft.identitymodel.jsonwebtokens/8.14.0",
|
|
||||||
"hashPath": "microsoft.identitymodel.jsonwebtokens.8.14.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.Logging/8.14.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-eqqnemdW38CKZEHS6diA50BV94QICozDZEvSrsvN3SJXUFwVB9gy+/oz76gldP7nZliA16IglXjXTCTdmU/Ejg==",
|
|
||||||
"path": "microsoft.identitymodel.logging/8.14.0",
|
|
||||||
"hashPath": "microsoft.identitymodel.logging.8.14.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.Tokens/8.14.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-lKIZiBiGd36k02TCdMHp1KlNWisyIvQxcYJvIkz7P4gSQ9zi8dgh6S5Grj8NNG7HWYIPfQymGyoZ6JB5d1Lo1g==",
|
|
||||||
"path": "microsoft.identitymodel.tokens/8.14.0",
|
|
||||||
"hashPath": "microsoft.identitymodel.tokens.8.14.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Nashel.BuildingBlocks/1.0.0": {
|
|
||||||
"type": "project",
|
|
||||||
"serviceable": false,
|
|
||||||
"sha512": ""
|
|
||||||
},
|
|
||||||
"Nashel.Modules.Geo.Application/1.0.0": {
|
|
||||||
"type": "project",
|
|
||||||
"serviceable": false,
|
|
||||||
"sha512": ""
|
|
||||||
},
|
|
||||||
"Nashel.Modules.Geo.Domain/1.0.0": {
|
|
||||||
"type": "project",
|
|
||||||
"serviceable": false,
|
|
||||||
"sha512": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using Nashel.Modules.Geo.Application.Commands;
|
||||||
|
using Nashel.Modules.Geo.Domain.Aggregates;
|
||||||
|
using Nashel.Modules.Geo.Domain.Enums;
|
||||||
|
using Nashel.Modules.Geo.Domain.Repositories;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Geo.Tests.Application;
|
||||||
|
|
||||||
|
public class UpdateGeoCommandHandlerTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IGeoRepository> _repositoryMock;
|
||||||
|
private readonly UpdateGeoCommandHandler _handler;
|
||||||
|
|
||||||
|
public UpdateGeoCommandHandlerTests()
|
||||||
|
{
|
||||||
|
_repositoryMock = new Mock<IGeoRepository>();
|
||||||
|
_handler = new UpdateGeoCommandHandler(_repositoryMock.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_Should_CreateNew_When_RecordDoesNotExist()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new UpdateGeoCommand(Guid.NewGuid(), 10.0, 20.0, PerformerStatus.Available);
|
||||||
|
|
||||||
|
_repositoryMock.Setup(x => x.GetByPerformerIdAsync(command.PerformerId, It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync((LiveStatus?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_repositoryMock.Verify(x => x.AddAsync(
|
||||||
|
It.Is<LiveStatus>(s =>
|
||||||
|
s.Id == command.PerformerId &&
|
||||||
|
s.Location.Y == command.Latitude &&
|
||||||
|
s.Location.X == command.Longitude),
|
||||||
|
It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
|
||||||
|
_repositoryMock.Verify(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_Should_UpdateExisting_When_RecordExists()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var performerId = Guid.NewGuid();
|
||||||
|
var existingStatus = new LiveStatus(performerId, 10.0, 20.0);
|
||||||
|
var command = new UpdateGeoCommand(performerId, 15.0, 25.0, PerformerStatus.Busy);
|
||||||
|
|
||||||
|
_repositoryMock.Setup(x => x.GetByPerformerIdAsync(performerId, It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(existingStatus);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
existingStatus.Location.Y.Should().Be(15.0);
|
||||||
|
existingStatus.Location.X.Should().Be(25.0);
|
||||||
|
existingStatus.Status.Should().Be(PerformerStatus.Busy);
|
||||||
|
|
||||||
|
_repositoryMock.Verify(x => x.AddAsync(It.IsAny<LiveStatus>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
|
_repositoryMock.Verify(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Nashel.Modules.Geo.Domain.Aggregates;
|
||||||
|
using Nashel.Modules.Geo.Domain.Enums;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Geo.Tests.Domain;
|
||||||
|
|
||||||
|
public class LiveStatusTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void UpdateLocation_Should_ChangeCoordinates_And_UpdateTimestamp()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var performerId = Guid.NewGuid();
|
||||||
|
var liveStatus = new LiveStatus(performerId, 10.0, 20.0);
|
||||||
|
var originalTimestamp = liveStatus.LastUpdated;
|
||||||
|
|
||||||
|
Thread.Sleep(50); // Small delay
|
||||||
|
|
||||||
|
// Act
|
||||||
|
liveStatus.UpdateLocation(15.0, 25.0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
liveStatus.Location.Y.Should().Be(15.0); // Latitude
|
||||||
|
liveStatus.Location.X.Should().Be(25.0); // Longitude
|
||||||
|
liveStatus.LastUpdated.Should().BeAfter(originalTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetBusy_Should_ChangeStatus_To_Busy()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var liveStatus = new LiveStatus(Guid.NewGuid(), 10.0, 20.0);
|
||||||
|
var busyUntil = DateTime.UtcNow.AddHours(2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
liveStatus.SetBusy(busyUntil);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
liveStatus.Status.Should().Be(PerformerStatus.Busy);
|
||||||
|
liveStatus.BusyUntil.Should().Be(busyUntil);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.8.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Domain\Nashel.Modules.Geo.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\Application\Nashel.Modules.Geo.Application.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Nashel.Modules.Geo.Tests;
|
||||||
|
|
||||||
|
public class UnitTest1
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Test1()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user