diff --git a/.gitignore b/.gitignore
index f1de997..e369b34 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
diff --git a/src/Host/Program.cs b/src/Host/Program.cs
index 0eb2cfc..6ef9258 100644
--- a/src/Host/Program.cs
+++ b/src/Host/Program.cs
@@ -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();
diff --git a/src/Modules/Geo/Application/Commands/UpdateGeoCommand.cs b/src/Modules/Geo/Application/Commands/UpdateGeoCommand.cs
new file mode 100644
index 0000000..dbdda88
--- /dev/null
+++ b/src/Modules/Geo/Application/Commands/UpdateGeoCommand.cs
@@ -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;
+
+///
+/// Команда обновления координат и статуса.
+///
+public record UpdateGeoCommand : IRequest
+{
+ ///
+ /// Идентификатор исполнителя.
+ ///
+ public Guid PerformerId { get; init; }
+
+ ///
+ /// Широта.
+ ///
+ public double Latitude { get; init; }
+
+ ///
+ /// Долгота.
+ ///
+ public double Longitude { get; init; }
+
+ ///
+ /// Новый статус.
+ ///
+ 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
+{
+ private readonly IGeoRepository _repository;
+
+ public UpdateGeoCommandHandler(IGeoRepository repository)
+ {
+ _repository = repository;
+ }
+
+ public async Task 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;
+ }
+}
diff --git a/src/Modules/Geo/Application/Nashel.Modules.Geo.Application.csproj b/src/Modules/Geo/Application/Nashel.Modules.Geo.Application.csproj
index 98a93c7..754b5cc 100644
--- a/src/Modules/Geo/Application/Nashel.Modules.Geo.Application.csproj
+++ b/src/Modules/Geo/Application/Nashel.Modules.Geo.Application.csproj
@@ -11,5 +11,7 @@
net10.0
enable
enable
+ true
+ $(NoWarn);1591
\ No newline at end of file
diff --git a/src/Modules/Geo/Application/Queries/SearchNearbyQuery.cs b/src/Modules/Geo/Application/Queries/SearchNearbyQuery.cs
new file mode 100644
index 0000000..1c183a1
--- /dev/null
+++ b/src/Modules/Geo/Application/Queries/SearchNearbyQuery.cs
@@ -0,0 +1,49 @@
+using MediatR;
+using Nashel.Modules.Geo.Domain.Repositories;
+
+namespace Nashel.Modules.Geo.Application.Queries;
+
+///
+/// Запрос на поиск исполнителей в радиусе.
+///
+public record SearchNearbyQuery : IRequest>
+{
+ ///
+ /// Широта центра поиска.
+ ///
+ public double Latitude { get; init; }
+
+ ///
+ /// Долгота центра поиска.
+ ///
+ public double Longitude { get; init; }
+
+ ///
+ /// Радиус поиска (в метрах).
+ ///
+ 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>
+{
+ private readonly IGeoRepository _repository;
+
+ public SearchNearbyQueryHandler(IGeoRepository repository)
+ {
+ _repository = repository;
+ }
+
+ public async Task> Handle(SearchNearbyQuery request, CancellationToken cancellationToken)
+ {
+ return await _repository.GetNearbyAsync(request.Latitude, request.Longitude, request.RadiusInMeters, cancellationToken);
+ }
+}
diff --git a/src/Modules/Geo/Domain/Aggregates/LiveStatus.cs b/src/Modules/Geo/Domain/Aggregates/LiveStatus.cs
new file mode 100644
index 0000000..22317c0
--- /dev/null
+++ b/src/Modules/Geo/Domain/Aggregates/LiveStatus.cs
@@ -0,0 +1,88 @@
+using Nashel.BuildingBlocks.Domain;
+using Nashel.Modules.Geo.Domain.Enums;
+using NetTopologySuite.Geometries;
+
+namespace Nashel.Modules.Geo.Domain.Aggregates;
+
+///
+/// Агрегат, хранящий текущее статус и локацию исполнителя.
+///
+public class LiveStatus : AggregateRoot
+{
+ private LiveStatus() { } // EF Core
+
+ ///
+ /// Создает новый статус.
+ ///
+ /// ID исполнителя.
+ /// Широта (Lat).
+ /// Долгота (Lon).
+ public LiveStatus(Guid performerId, double latitude, double longitude)
+ {
+ Id = performerId;
+ Location = new Point(longitude, latitude) { SRID = 4326 };
+ Status = PerformerStatus.Available;
+ LastUpdated = DateTime.UtcNow;
+ }
+
+ ///
+ /// Геопозиция исполнителя. SRID 4326 (WGS 84).
+ ///
+ public Point Location { get; private set; }
+
+ ///
+ /// Статус доступности.
+ ///
+ public PerformerStatus Status { get; private set; }
+
+ ///
+ /// Время, до которого исполнитель занят (UTC).
+ ///
+ public DateTime? BusyUntil { get; private set; }
+
+ ///
+ /// Время последнего обновления (UTC).
+ ///
+ public DateTime LastUpdated { get; private set; }
+
+ ///
+ /// Обновляет координаты.
+ ///
+ /// Широта.
+ /// Долгота.
+ public void UpdateLocation(double lat, double lon)
+ {
+ Location = new Point(lon, lat) { SRID = 4326 };
+ LastUpdated = DateTime.UtcNow;
+ }
+
+ ///
+ /// Устанавливает статус "Занят" до указанного времени.
+ ///
+ public void SetBusy(DateTime until)
+ {
+ Status = PerformerStatus.Busy;
+ BusyUntil = until;
+ LastUpdated = DateTime.UtcNow;
+ }
+
+ ///
+ /// Переводит в статус "Доступен".
+ ///
+ public void GoOnline()
+ {
+ Status = PerformerStatus.Available;
+ BusyUntil = null;
+ LastUpdated = DateTime.UtcNow;
+ }
+
+ ///
+ /// Переводит в статус "Выходной" (офлайн).
+ ///
+ public void GoOffline()
+ {
+ Status = PerformerStatus.DayOff;
+ BusyUntil = null;
+ LastUpdated = DateTime.UtcNow;
+ }
+}
diff --git a/src/Modules/Geo/Domain/Enums/PerformerStatus.cs b/src/Modules/Geo/Domain/Enums/PerformerStatus.cs
new file mode 100644
index 0000000..cd42df2
--- /dev/null
+++ b/src/Modules/Geo/Domain/Enums/PerformerStatus.cs
@@ -0,0 +1,22 @@
+namespace Nashel.Modules.Geo.Domain.Enums;
+
+///
+/// Статус доступности исполнителя.
+///
+public enum PerformerStatus
+{
+ ///
+ /// Доступен для новых заказов.
+ ///
+ Available,
+
+ ///
+ /// Занят выполнением заказа.
+ ///
+ Busy,
+
+ ///
+ /// Выходной или не на смене.
+ ///
+ DayOff
+}
diff --git a/src/Modules/Geo/Domain/Nashel.Modules.Geo.Domain.csproj b/src/Modules/Geo/Domain/Nashel.Modules.Geo.Domain.csproj
index 11edb14..8cb9dc4 100644
--- a/src/Modules/Geo/Domain/Nashel.Modules.Geo.Domain.csproj
+++ b/src/Modules/Geo/Domain/Nashel.Modules.Geo.Domain.csproj
@@ -4,11 +4,18 @@
+
+
+
+
+
Nashel.Modules.Geo.Domain
net10.0
enable
enable
+ true
+ $(NoWarn);1591
diff --git a/src/Modules/Geo/Domain/Repositories/IGeoRepository.cs b/src/Modules/Geo/Domain/Repositories/IGeoRepository.cs
new file mode 100644
index 0000000..dac0c10
--- /dev/null
+++ b/src/Modules/Geo/Domain/Repositories/IGeoRepository.cs
@@ -0,0 +1,11 @@
+using Nashel.Modules.Geo.Domain.Aggregates;
+
+namespace Nashel.Modules.Geo.Domain.Repositories;
+
+public interface IGeoRepository
+{
+ Task GetByPerformerIdAsync(Guid performerId, CancellationToken cancellationToken = default);
+ Task AddAsync(LiveStatus liveStatus, CancellationToken cancellationToken = default);
+ Task> GetNearbyAsync(double lat, double lon, double radiusInMeters, CancellationToken cancellationToken = default);
+ Task SaveChangesAsync(CancellationToken cancellationToken = default);
+}
diff --git a/src/Modules/Geo/Infrastructure/DependencyInjection.cs b/src/Modules/Geo/Infrastructure/DependencyInjection.cs
new file mode 100644
index 0000000..99b1124
--- /dev/null
+++ b/src/Modules/Geo/Infrastructure/DependencyInjection.cs
@@ -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(options =>
+ options.UseNpgsql(
+ configuration.GetConnectionString("DefaultConnection"),
+ x => x.UseNetTopologySuite()
+ ));
+
+ services.AddScoped();
+
+ services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(UpdateGeoCommand).Assembly));
+
+ return services;
+ }
+}
diff --git a/src/Modules/Geo/Infrastructure/Migrations/20260210181606_InitialCreate.Designer.cs b/src/Modules/Geo/Infrastructure/Migrations/20260210181606_InitialCreate.Designer.cs
new file mode 100644
index 0000000..fc2cceb
--- /dev/null
+++ b/src/Modules/Geo/Infrastructure/Migrations/20260210181606_InitialCreate.Designer.cs
@@ -0,0 +1,61 @@
+//
+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
+ {
+ ///
+ 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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("BusyUntil")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LastUpdated")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Location")
+ .IsRequired()
+ .HasColumnType("geography (point)");
+
+ b.Property("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
+ }
+ }
+}
diff --git a/src/Modules/Geo/Infrastructure/Migrations/20260210181606_InitialCreate.cs b/src/Modules/Geo/Infrastructure/Migrations/20260210181606_InitialCreate.cs
new file mode 100644
index 0000000..fda8bfd
--- /dev/null
+++ b/src/Modules/Geo/Infrastructure/Migrations/20260210181606_InitialCreate.cs
@@ -0,0 +1,53 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using NetTopologySuite.Geometries;
+
+#nullable disable
+
+namespace Nashel.Modules.Geo.Infrastructure.Migrations
+{
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ 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(type: "uuid", nullable: false),
+ Location = table.Column(type: "geography (point)", nullable: false),
+ Status = table.Column(type: "text", nullable: false),
+ BusyUntil = table.Column(type: "timestamp with time zone", nullable: true),
+ LastUpdated = table.Column(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");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "LiveStatuses",
+ schema: "geo");
+ }
+ }
+}
diff --git a/src/Modules/Geo/Infrastructure/Migrations/GeoDbContextModelSnapshot.cs b/src/Modules/Geo/Infrastructure/Migrations/GeoDbContextModelSnapshot.cs
new file mode 100644
index 0000000..5f51e65
--- /dev/null
+++ b/src/Modules/Geo/Infrastructure/Migrations/GeoDbContextModelSnapshot.cs
@@ -0,0 +1,58 @@
+//
+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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("BusyUntil")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LastUpdated")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Location")
+ .IsRequired()
+ .HasColumnType("geography (point)");
+
+ b.Property("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
+ }
+ }
+}
diff --git a/src/Modules/Geo/Infrastructure/Nashel.Modules.Geo.Infrastructure.csproj b/src/Modules/Geo/Infrastructure/Nashel.Modules.Geo.Infrastructure.csproj
index 058c067..71e285d 100644
--- a/src/Modules/Geo/Infrastructure/Nashel.Modules.Geo.Infrastructure.csproj
+++ b/src/Modules/Geo/Infrastructure/Nashel.Modules.Geo.Infrastructure.csproj
@@ -7,6 +7,7 @@
+
Nashel.Modules.Geo.Infrastructure
diff --git a/src/Modules/Geo/Infrastructure/Persistence/Configurations/LiveStatusConfiguration.cs b/src/Modules/Geo/Infrastructure/Persistence/Configurations/LiveStatusConfiguration.cs
new file mode 100644
index 0000000..4b117e9
--- /dev/null
+++ b/src/Modules/Geo/Infrastructure/Persistence/Configurations/LiveStatusConfiguration.cs
@@ -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
+{
+ public void Configure(EntityTypeBuilder 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();
+
+ builder.Property(x => x.BusyUntil)
+ .IsRequired(false);
+
+ builder.Property(x => x.LastUpdated)
+ .IsRequired();
+ }
+}
diff --git a/src/Modules/Geo/Infrastructure/Persistence/GeoDbContext.cs b/src/Modules/Geo/Infrastructure/Persistence/GeoDbContext.cs
new file mode 100644
index 0000000..df9daaa
--- /dev/null
+++ b/src/Modules/Geo/Infrastructure/Persistence/GeoDbContext.cs
@@ -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 options) : base(options) { }
+
+ public DbSet LiveStatuses { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.HasPostgresExtension("postgis");
+ modelBuilder.ApplyConfiguration(new LiveStatusConfiguration());
+ base.OnModelCreating(modelBuilder);
+ }
+}
diff --git a/src/Modules/Geo/Infrastructure/Persistence/Repositories/GeoRepository.cs b/src/Modules/Geo/Infrastructure/Persistence/Repositories/GeoRepository.cs
new file mode 100644
index 0000000..2f97d61
--- /dev/null
+++ b/src/Modules/Geo/Infrastructure/Persistence/Repositories/GeoRepository.cs
@@ -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 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> 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);
+ }
+}
diff --git a/src/Modules/Geo/Presentation/GeoEndpoints.cs b/src/Modules/Geo/Presentation/GeoEndpoints.cs
new file mode 100644
index 0000000..ab75910
--- /dev/null
+++ b/src/Modules/Geo/Presentation/GeoEndpoints.cs
@@ -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);
+ });
+ }
+
+ ///
+ /// Запрос на обновление геопозиции и статуса исполнителя.
+ ///
+ /// Идентификатор исполнителя.
+ /// Широта.
+ /// Долгота.
+ /// Новый статус (0 = Available, 1 = Busy, 2 = DayOff).
+ public record UpdateGeoRequest(Guid PerformerId, double Latitude, double Longitude, PerformerStatus Status);
+}
diff --git a/src/Modules/Geo/Presentation/Nashel.Modules.Geo.Presentation.csproj b/src/Modules/Geo/Presentation/Nashel.Modules.Geo.Presentation.csproj
index e4c7427..4174f35 100644
--- a/src/Modules/Geo/Presentation/Nashel.Modules.Geo.Presentation.csproj
+++ b/src/Modules/Geo/Presentation/Nashel.Modules.Geo.Presentation.csproj
@@ -7,5 +7,11 @@
net10.0
enable
enable
+ true
+ $(NoWarn);1591
+
+
+
+
\ No newline at end of file
diff --git a/src/Modules/Geo/Presentation/bin/Debug/net10.0/Nashel.Modules.Geo.Presentation.deps.json b/src/Modules/Geo/Presentation/bin/Debug/net10.0/Nashel.Modules.Geo.Presentation.deps.json
deleted file mode 100644
index 5716f32..0000000
--- a/src/Modules/Geo/Presentation/bin/Debug/net10.0/Nashel.Modules.Geo.Presentation.deps.json
+++ /dev/null
@@ -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": ""
- }
- }
-}
\ No newline at end of file
diff --git a/src/Modules/Geo/Tests/Application/UpdateGeoCommandHandlerTests.cs b/src/Modules/Geo/Tests/Application/UpdateGeoCommandHandlerTests.cs
new file mode 100644
index 0000000..bc870e7
--- /dev/null
+++ b/src/Modules/Geo/Tests/Application/UpdateGeoCommandHandlerTests.cs
@@ -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 _repositoryMock;
+ private readonly UpdateGeoCommandHandler _handler;
+
+ public UpdateGeoCommandHandlerTests()
+ {
+ _repositoryMock = new Mock();
+ _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()))
+ .ReturnsAsync((LiveStatus?)null);
+
+ // Act
+ await _handler.Handle(command, CancellationToken.None);
+
+ // Assert
+ _repositoryMock.Verify(x => x.AddAsync(
+ It.Is(s =>
+ s.Id == command.PerformerId &&
+ s.Location.Y == command.Latitude &&
+ s.Location.X == command.Longitude),
+ It.IsAny()), Times.Once);
+
+ _repositoryMock.Verify(x => x.SaveChangesAsync(It.IsAny()), 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()))
+ .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(), It.IsAny()), Times.Never);
+ _repositoryMock.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Once);
+ }
+}
diff --git a/src/Modules/Geo/Tests/Domain/LiveStatusTests.cs b/src/Modules/Geo/Tests/Domain/LiveStatusTests.cs
new file mode 100644
index 0000000..803afa4
--- /dev/null
+++ b/src/Modules/Geo/Tests/Domain/LiveStatusTests.cs
@@ -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);
+ }
+}
diff --git a/src/Modules/Geo/Tests/Nashel.Modules.Geo.Tests.csproj b/src/Modules/Geo/Tests/Nashel.Modules.Geo.Tests.csproj
new file mode 100644
index 0000000..51f6f97
--- /dev/null
+++ b/src/Modules/Geo/Tests/Nashel.Modules.Geo.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Modules/Geo/Tests/UnitTest1.cs b/src/Modules/Geo/Tests/UnitTest1.cs
new file mode 100644
index 0000000..cae23f6
--- /dev/null
+++ b/src/Modules/Geo/Tests/UnitTest1.cs
@@ -0,0 +1,10 @@
+namespace Nashel.Modules.Geo.Tests;
+
+public class UnitTest1
+{
+ [Fact]
+ public void Test1()
+ {
+
+ }
+}