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() + { + + } +}