Реализация и тесты Geo модуля
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -140,3 +140,9 @@ src/Modules/Identity/Tests/obj/
|
||||
src/Modules/Catalog/Tests/bin/
|
||||
|
||||
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 Nashel.Modules.Catalog.Infrastructure;
|
||||
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.Presentation.Endpoints;
|
||||
|
||||
@@ -53,6 +55,9 @@ builder.Services.AddIdentityModule(builder.Configuration);
|
||||
// Регистрация модуля Catalog
|
||||
builder.Services.AddCatalogModule(builder.Configuration);
|
||||
|
||||
// Регистрация модуля Geo
|
||||
builder.Services.AddGeoModule(builder.Configuration);
|
||||
|
||||
// Настройка аутентификации
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
@@ -90,5 +95,6 @@ app.UseAuthorization();
|
||||
// Маппинг эндпоинтов
|
||||
app.MapIdentityEndpoints();
|
||||
app.MapCatalogEndpoints();
|
||||
app.MapGeoEndpoints();
|
||||
|
||||
app.Run();
|
||||
|
||||
91
src/Modules/Geo/Application/Commands/UpdateGeoCommand.cs
Normal file
91
src/Modules/Geo/Application/Commands/UpdateGeoCommand.cs
Normal file
@@ -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>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
49
src/Modules/Geo/Application/Queries/SearchNearbyQuery.cs
Normal file
49
src/Modules/Geo/Application/Queries/SearchNearbyQuery.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
88
src/Modules/Geo/Domain/Aggregates/LiveStatus.cs
Normal file
88
src/Modules/Geo/Domain/Aggregates/LiveStatus.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
22
src/Modules/Geo/Domain/Enums/PerformerStatus.cs
Normal file
22
src/Modules/Geo/Domain/Enums/PerformerStatus.cs
Normal file
@@ -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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
|
||||
<PackageReference Include="NetTopologySuite.IO.PostGis" Version="2.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Nashel.Modules.Geo.Domain</RootNamespace>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
11
src/Modules/Geo/Domain/Repositories/IGeoRepository.cs
Normal file
11
src/Modules/Geo/Domain/Repositories/IGeoRepository.cs
Normal file
@@ -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);
|
||||
}
|
||||
28
src/Modules/Geo/Infrastructure/DependencyInjection.cs
Normal file
28
src/Modules/Geo/Infrastructure/DependencyInjection.cs
Normal file
@@ -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
src/Modules/Geo/Infrastructure/Migrations/20260210181606_InitialCreate.Designer.cs
generated
Normal file
61
src/Modules/Geo/Infrastructure/Migrations/20260210181606_InitialCreate.Designer.cs
generated
Normal file
@@ -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>
|
||||
<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>
|
||||
<PropertyGroup>
|
||||
<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();
|
||||
}
|
||||
}
|
||||
19
src/Modules/Geo/Infrastructure/Persistence/GeoDbContext.cs
Normal file
19
src/Modules/Geo/Infrastructure/Persistence/GeoDbContext.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
50
src/Modules/Geo/Presentation/GeoEndpoints.cs
Normal file
50
src/Modules/Geo/Presentation/GeoEndpoints.cs
Normal file
@@ -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>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
43
src/Modules/Geo/Tests/Domain/LiveStatusTests.cs
Normal file
43
src/Modules/Geo/Tests/Domain/LiveStatusTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
29
src/Modules/Geo/Tests/Nashel.Modules.Geo.Tests.csproj
Normal file
29
src/Modules/Geo/Tests/Nashel.Modules.Geo.Tests.csproj
Normal file
@@ -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>
|
||||
10
src/Modules/Geo/Tests/UnitTest1.cs
Normal file
10
src/Modules/Geo/Tests/UnitTest1.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Nashel.Modules.Geo.Tests;
|
||||
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user