From c824e26cc0b64f3345c6b448e3d2b74751f989dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A5=D0=B0=D0=BB=D0=B8=D0=BC=D0=BE=D0=B2=20=D0=A0=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BC?= Date: Thu, 19 Feb 2026 21:51:17 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D1=82=D0=B0=D1=82=D1=8C=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8=D1=82=D0=B5=D0=BB=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 18 ++ src/Host/Program.cs | 25 +++ src/Host/Properties/launchSettings.json | 4 +- .../Commands/BecomePerformerCommand.cs | 71 ++++--- .../Commands/RegisterUserCommand.cs | 6 +- .../Commands/UpdateProfileCommand.cs | 6 +- .../Queries/GetProfile/GetProfileQuery.cs | 35 +-- .../Identity/Domain/Aggregates/Account.cs | 4 +- .../Identity/Domain/Aggregates/UserProfile.cs | 15 +- .../Identity/Domain/Entities/WorkSchedule.cs | 26 +-- .../Infrastructure/DependencyInjection.cs | 1 + .../WorkScheduleConfiguration.cs | 8 +- ...CurrentLocationAndRemoveIs24_7.Designer.cs | 199 ++++++++++++++++++ ...20252_AddCurrentLocationAndRemoveIs24_7.cs | 92 ++++++++ .../IdentityDbContextModelSnapshot.cs | 17 +- .../Repositories/AccountRepository.cs | 87 +++++++- .../Repositories/CompetencyRepository.cs | 34 +++ .../Endpoints/IdentityEndpoints.cs | 22 +- 18 files changed, 562 insertions(+), 108 deletions(-) create mode 100644 src/Modules/Identity/Infrastructure/Persistence/Migrations/20260216120252_AddCurrentLocationAndRemoveIs24_7.Designer.cs create mode 100644 src/Modules/Identity/Infrastructure/Persistence/Migrations/20260216120252_AddCurrentLocationAndRemoveIs24_7.cs create mode 100644 src/Modules/Identity/Infrastructure/Repositories/CompetencyRepository.cs diff --git a/docker-compose.yml b/docker-compose.yml index c6b98ca..cb9c675 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,20 @@ services: + migrations: + build: + context: . + dockerfile: Dockerfile + container_name: nashel-migrations + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ConnectionStrings__DefaultConnection=Host=db;Port=5432;Database=nashel;Username=postgres;Password=postgres + depends_on: + db: + condition: service_healthy + command: > + sh -c "cd /app/src/Host && dotnet ef database update --project ../Modules/Identity/Infrastructure --startup-project . --context IdentityDbContext" + networks: + - nashel-network + app: build: context: . @@ -14,6 +30,8 @@ services: depends_on: db: condition: service_healthy + migrations: + condition: service_completed_successfully networks: - nashel-network diff --git a/src/Host/Program.cs b/src/Host/Program.cs index 95622fa..0b968bf 100644 --- a/src/Host/Program.cs +++ b/src/Host/Program.cs @@ -104,6 +104,31 @@ builder.Services.AddCors(options => var app = builder.Build(); +// Middleware для обработки исключений +app.UseExceptionHandler(errorApp => +{ + errorApp.Run(async context => + { + context.Response.StatusCode = 500; + context.Response.ContentType = "application/json"; + + var exception = context.Features.Get(); + if (exception != null) + { + var error = new + { + Message = exception.Error.Message, + StackTrace = exception.Error.StackTrace + }; + + Console.WriteLine($"[ERROR] {exception.Error.Message}"); + Console.WriteLine(exception.Error.StackTrace); + + await context.Response.WriteAsJsonAsync(error); + } + }); +}); + // Настройка конвейера HTTP-запросов. if (app.Environment.IsDevelopment()) { diff --git a/src/Host/Properties/launchSettings.json b/src/Host/Properties/launchSettings.json index 2b8bdb9..656ddc3 100644 --- a/src/Host/Properties/launchSettings.json +++ b/src/Host/Properties/launchSettings.json @@ -5,10 +5,10 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "http://localhost:5232", + "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } -} +} \ No newline at end of file diff --git a/src/Modules/Identity/Application/Commands/BecomePerformerCommand.cs b/src/Modules/Identity/Application/Commands/BecomePerformerCommand.cs index 5b9b231..c54c8d1 100644 --- a/src/Modules/Identity/Application/Commands/BecomePerformerCommand.cs +++ b/src/Modules/Identity/Application/Commands/BecomePerformerCommand.cs @@ -9,80 +9,87 @@ public record BecomePerformerCommand : IRequest { public string Description { get; init; } = default!; public List CompetencyNames { get; init; } = default!; - public bool Is24_7 { get; init; } + public string? Location { get; init; } + public string? CurrentLocation { get; init; } public bool IsAlwaysReady { get; init; } - public List? WorkingDays { get; init; } - public string? WorkingHoursStart { get; init; } - public string? WorkingHoursEnd { get; init; } + public string? WorkingDays { get; init; } } public class BecomePerformerCommandHandler : IRequestHandler { private readonly IAccountRepository _accountRepository; private readonly ICurrentUserService _currentUserService; + private readonly ICompetencyRepository _competencyRepository; - public BecomePerformerCommandHandler(IAccountRepository accountRepository, ICurrentUserService currentUserService) + public BecomePerformerCommandHandler( + IAccountRepository accountRepository, + ICurrentUserService currentUserService, + ICompetencyRepository competencyRepository) { _accountRepository = accountRepository; _currentUserService = currentUserService; + _competencyRepository = competencyRepository; } public async Task Handle(BecomePerformerCommand request, CancellationToken cancellationToken) { + Console.WriteLine($"[BecomePerformerCommand] Starting to process request for user"); + var userId = _currentUserService.UserId; if (userId == null) { + Console.WriteLine("[BecomePerformerCommand] User is not authorized"); throw new Exception("Неавторизован"); } + Console.WriteLine($"[BecomePerformerCommand] UserId: {userId.Value}"); + var account = await _accountRepository.GetByIdAsync(userId.Value, cancellationToken); if (account == null) { + Console.WriteLine($"[BecomePerformerCommand] Account not found for userId: {userId.Value}"); throw new Exception("Аккаунт не найден"); } - // Создаем компетенции - var competencies = request.CompetencyNames - .Select(name => Competency.Create(name)) - .ToList(); + Console.WriteLine($"[BecomePerformerCommand] Account found: {account.Id}"); - // Создаем график работы - WorkSchedule? workSchedule = null; - if (request.Is24_7 || request.IsAlwaysReady || (request.WorkingDays != null && request.WorkingDays.Any())) + // Получаем или создаем компетенции + var competencies = new List(); + if (request.CompetencyNames != null) { - string? workingDaysJson = null; - string? workingHoursJson = null; - - if (request.WorkingDays != null && request.WorkingDays.Any()) + Console.WriteLine($"[BecomePerformerCommand] Processing {request.CompetencyNames.Count} competencies"); + foreach (var name in request.CompetencyNames) { - workingDaysJson = System.Text.Json.JsonSerializer.Serialize(request.WorkingDays); - } - - if (!string.IsNullOrEmpty(request.WorkingHoursStart) && !string.IsNullOrEmpty(request.WorkingHoursEnd)) - { - workingHoursJson = System.Text.Json.JsonSerializer.Serialize(new + var competency = await _competencyRepository.GetByNameAsync(name, cancellationToken); + if (competency == null) { - start = request.WorkingHoursStart, - end = request.WorkingHoursEnd - }); + Console.WriteLine($"[BecomePerformerCommand] Creating new competency: {name}"); + competency = Competency.Create(name); + await _competencyRepository.AddAsync(competency, cancellationToken); + } + competencies.Add(competency); } + } + WorkSchedule? workSchedule = null; + if (!request.IsAlwaysReady && !string.IsNullOrEmpty(request.WorkingDays)) + { + Console.WriteLine($"[BecomePerformerCommand] Creating work schedule"); workSchedule = WorkSchedule.Create( - request.Is24_7, request.IsAlwaysReady, - workingDaysJson, - workingHoursJson + request.WorkingDays ); } - // Обновляем данные исполнителя - account.UpdatePerformerData(request.Description, competencies, workSchedule); - - // Добавляем роль исполнителя + Console.WriteLine($"[BecomePerformerCommand] Updating performer data"); + account.UpdatePerformerData(request.Description, competencies, request.Location, request.CurrentLocation, workSchedule); account.BecomePerformer(); + Console.WriteLine($"[BecomePerformerCommand] Saving to database"); await _accountRepository.UpdateAsync(account, cancellationToken); + Console.WriteLine($"[BecomePerformerCommand] Successfully completed"); + return Unit.Value; } } diff --git a/src/Modules/Identity/Application/Commands/RegisterUserCommand.cs b/src/Modules/Identity/Application/Commands/RegisterUserCommand.cs index ccebf72..78bb18a 100644 --- a/src/Modules/Identity/Application/Commands/RegisterUserCommand.cs +++ b/src/Modules/Identity/Application/Commands/RegisterUserCommand.cs @@ -79,12 +79,12 @@ public class RegisterUserCommandHandler : IRequestHandler @@ -50,7 +52,9 @@ public class UpdateProfileCommandHandler : IRequestHandler request.Patronymic, request.CompanyName, request.Inn, - request.Description); + request.Description, + request.Location, + request.CurrentLocation); await _accountRepository.UpdateAsync(account, cancellationToken); } diff --git a/src/Modules/Identity/Application/Queries/GetProfile/GetProfileQuery.cs b/src/Modules/Identity/Application/Queries/GetProfile/GetProfileQuery.cs index 9099f4d..c6a2b74 100644 --- a/src/Modules/Identity/Application/Queries/GetProfile/GetProfileQuery.cs +++ b/src/Modules/Identity/Application/Queries/GetProfile/GetProfileQuery.cs @@ -7,13 +7,12 @@ namespace Nashel.Modules.Identity.Application.Queries.GetProfile; public record GetProfileQuery : IRequest; public record WorkScheduleResponse( - bool Is24_7, bool IsAlwaysReady, - List? WorkingDays, - string? WorkingHoursStart, - string? WorkingHoursEnd + Dictionary>? SchedulePerDay ); +public record TimePeriod(string Start, string End); + public record ProfileResponse( Guid Id, string Phone, @@ -24,6 +23,8 @@ public record ProfileResponse( string? CompanyName, string? Inn, string? Description, + string? Location, + string? CurrentLocation, string FullName, string? AvatarUrl, List Competencies, @@ -64,31 +65,15 @@ public class GetProfileQueryHandler : IRequestHandler>(account.Profile.WorkSchedule.WorkingDays) - : null; - - string? workingHoursStart = null; - string? workingHoursEnd = null; - if (account.Profile.WorkSchedule.WorkingHours != null) + Dictionary>? schedulePerDay = null; + if (account.Profile.WorkSchedule.WorkingDays != null) { - var workingHours = System.Text.Json.JsonSerializer.Deserialize(account.Profile.WorkSchedule.WorkingHours); - if (workingHours.TryGetProperty("start", out var start)) - { - workingHoursStart = start.GetString(); - } - if (workingHours.TryGetProperty("end", out var end)) - { - workingHoursEnd = end.GetString(); - } + schedulePerDay = System.Text.Json.JsonSerializer.Deserialize>>(account.Profile.WorkSchedule.WorkingDays); } workScheduleResponse = new WorkScheduleResponse( - account.Profile.WorkSchedule.Is24_7, account.Profile.WorkSchedule.IsAlwaysReady, - workingDays, - workingHoursStart, - workingHoursEnd + schedulePerDay ); } @@ -102,6 +87,8 @@ public class GetProfileQueryHandler : IRequestHandler c.Name).ToList(), diff --git a/src/Modules/Identity/Domain/Aggregates/Account.cs b/src/Modules/Identity/Domain/Aggregates/Account.cs index 6af3986..8c47643 100644 --- a/src/Modules/Identity/Domain/Aggregates/Account.cs +++ b/src/Modules/Identity/Domain/Aggregates/Account.cs @@ -60,9 +60,9 @@ public class Account : AggregateRoot } } - public void UpdatePerformerData(string description, IEnumerable competencies, WorkSchedule? workSchedule) + public void UpdatePerformerData(string description, IEnumerable competencies, string? location, string? currentLocation, WorkSchedule? workSchedule) { - Profile.UpdatePerformerData(description, competencies, workSchedule); + Profile.UpdatePerformerData(description, competencies, location, currentLocation, workSchedule); } public void UpdateWorkSchedule(WorkSchedule workSchedule) diff --git a/src/Modules/Identity/Domain/Aggregates/UserProfile.cs b/src/Modules/Identity/Domain/Aggregates/UserProfile.cs index fa917ec..57f964d 100644 --- a/src/Modules/Identity/Domain/Aggregates/UserProfile.cs +++ b/src/Modules/Identity/Domain/Aggregates/UserProfile.cs @@ -16,6 +16,8 @@ public class UserProfile : Entity public string? Inn { get; private set; } public string? Description { get; private set; } public string? AvatarUrl { get; private set; } + public string? Location { get; private set; } + public string? CurrentLocation { get; private set; } public IReadOnlyCollection Competencies => _competencies.AsReadOnly(); public WorkSchedule? WorkSchedule { get; private set; } @@ -41,7 +43,7 @@ public class UserProfile : Entity return new UserProfile(id, firstName, lastName, patronymic, companyName, inn, description); } - public void Update(string firstName, string lastName, string? patronymic, string? companyName, string? inn, string? description) + public void Update(string firstName, string lastName, string? patronymic, string? companyName, string? inn, string? description, string? location = null, string? currentLocation = null) { if (!string.IsNullOrEmpty(description) && description.Length > MaxDescriptionLength) throw new ArgumentException($"Описание не может превышать {MaxDescriptionLength} символов", nameof(description)); @@ -52,6 +54,8 @@ public class UserProfile : Entity CompanyName = companyName; Inn = inn; Description = description; + Location = location; + CurrentLocation = currentLocation; } public void UpdateAvatar(string? avatarUrl) @@ -70,7 +74,7 @@ public class UserProfile : Entity _competencies.AddRange(competencyList); } - public void UpdatePerformerData(string description, IEnumerable competencies, WorkSchedule? workSchedule) + public void UpdatePerformerData(string description, IEnumerable competencies, string? location, string? currentLocation, WorkSchedule? workSchedule) { if (string.IsNullOrWhiteSpace(description) || description.Length < 50) throw new ArgumentException($"Описание должно содержать минимум 50 символов", nameof(description)); @@ -79,6 +83,8 @@ public class UserProfile : Entity throw new ArgumentException($"Описание не может превышать {MaxDescriptionLength} символов", nameof(description)); Description = description; + Location = location; + CurrentLocation = currentLocation; var competencyList = competencies.ToList(); if (competencyList.Count > MaxCompetencies) @@ -90,6 +96,11 @@ public class UserProfile : Entity WorkSchedule = workSchedule; } + public void UpdateCurrentLocation(string? currentLocation) + { + CurrentLocation = currentLocation; + } + public void SetWorkSchedule(WorkSchedule workSchedule) { WorkSchedule = workSchedule; diff --git a/src/Modules/Identity/Domain/Entities/WorkSchedule.cs b/src/Modules/Identity/Domain/Entities/WorkSchedule.cs index 3712677..13728cd 100644 --- a/src/Modules/Identity/Domain/Entities/WorkSchedule.cs +++ b/src/Modules/Identity/Domain/Entities/WorkSchedule.cs @@ -3,47 +3,39 @@ namespace Nashel.Modules.Identity.Domain.Entities; public class WorkSchedule { public Guid Id { get; private set; } - public bool Is24_7 { get; private set; } public bool IsAlwaysReady { get; private set; } - public string? WorkingDays { get; private set; } // JSON-массив дней недели: ["Mon", "Tue", ...] - public string? WorkingHours { get; private set; } // JSON: {"start": "09:00", "end": "18:00"} + public string? WorkingDays { get; private set; } // JSON: {"Понедельник": [{"start": "09:00", "end": "18:00"}], ...} // EF Core constructor private WorkSchedule() { } - private WorkSchedule(bool is24_7, bool isAlwaysReady, string? workingDays, string? workingHours) + private WorkSchedule(bool isAlwaysReady, string? workingDays) { Id = Guid.NewGuid(); - Is24_7 = is24_7; IsAlwaysReady = isAlwaysReady; WorkingDays = workingDays; - WorkingHours = workingHours; } - public static WorkSchedule Create(bool is24_7, bool isAlwaysReady, string? workingDays, string? workingHours) + public static WorkSchedule Create(bool isAlwaysReady, string? workingDays) { - // Если 24/7 или Always Ready, то дни и часы не нужны - if (is24_7 || isAlwaysReady) + // Если Always Ready, то расписание не нужно + if (isAlwaysReady) { workingDays = null; - workingHours = null; } - return new WorkSchedule(is24_7, isAlwaysReady, workingDays, workingHours); + return new WorkSchedule(isAlwaysReady, workingDays); } - public void Update(bool is24_7, bool isAlwaysReady, string? workingDays, string? workingHours) + public void Update(bool isAlwaysReady, string? workingDays) { - // Если 24/7 или Always Ready, то дни и часы не нужны - if (is24_7 || isAlwaysReady) + // Если Always Ready, то расписание не нужно + if (isAlwaysReady) { workingDays = null; - workingHours = null; } - Is24_7 = is24_7; IsAlwaysReady = isAlwaysReady; WorkingDays = workingDays; - WorkingHours = workingHours; } } diff --git a/src/Modules/Identity/Infrastructure/DependencyInjection.cs b/src/Modules/Identity/Infrastructure/DependencyInjection.cs index 0c533e9..8cfef05 100644 --- a/src/Modules/Identity/Infrastructure/DependencyInjection.cs +++ b/src/Modules/Identity/Infrastructure/DependencyInjection.cs @@ -19,6 +19,7 @@ public static class DependencyInjection public static IServiceCollection AddIdentityModule(this IServiceCollection services, IConfiguration configuration) { services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Modules/Identity/Infrastructure/Persistence/Configurations/WorkScheduleConfiguration.cs b/src/Modules/Identity/Infrastructure/Persistence/Configurations/WorkScheduleConfiguration.cs index e43cd12..8d935f0 100644 --- a/src/Modules/Identity/Infrastructure/Persistence/Configurations/WorkScheduleConfiguration.cs +++ b/src/Modules/Identity/Infrastructure/Persistence/Configurations/WorkScheduleConfiguration.cs @@ -12,16 +12,10 @@ public class WorkScheduleConfiguration : IEntityTypeConfiguration builder.HasKey(x => x.Id); - builder.Property(x => x.Is24_7) - .IsRequired(); - builder.Property(x => x.IsAlwaysReady) .IsRequired(); builder.Property(x => x.WorkingDays) - .HasMaxLength(100); // JSON-массив дней недели - - builder.Property(x => x.WorkingHours) - .HasMaxLength(100); // JSON с часами работы + .HasMaxLength(2000); // JSON с расписанием для каждого дня } } diff --git a/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260216120252_AddCurrentLocationAndRemoveIs24_7.Designer.cs b/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260216120252_AddCurrentLocationAndRemoveIs24_7.Designer.cs new file mode 100644 index 0000000..ffc4ba5 --- /dev/null +++ b/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260216120252_AddCurrentLocationAndRemoveIs24_7.Designer.cs @@ -0,0 +1,199 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Nashel.Modules.Identity.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(IdentityDbContext))] + [Migration("20260216120252_AddCurrentLocationAndRemoveIs24_7")] + partial class AddCurrentLocationAndRemoveIs24_7 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("Accounts", "identity"); + }); + + modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CompanyName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CurrentLocation") + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Inn") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Location") + .HasColumnType("text"); + + b.Property("Patronymic") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.ToTable("UserProfiles", "identity"); + }); + + modelBuilder.Entity("Nashel.Modules.Identity.Domain.Entities.Competency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("IX_Competencies_Name"); + + b.ToTable("Competencies", "identity"); + }); + + modelBuilder.Entity("Nashel.Modules.Identity.Domain.Entities.WorkSchedule", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("IsAlwaysReady") + .HasColumnType("boolean"); + + b.Property("WorkingDays") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.HasKey("Id"); + + b.ToTable("WorkSchedules", "identity"); + }); + + modelBuilder.Entity("UserProfileCompetencies", b => + { + b.Property("UserProfileId") + .HasColumnType("uuid"); + + b.Property("CompetencyId") + .HasColumnType("uuid"); + + b.HasKey("UserProfileId", "CompetencyId"); + + b.HasIndex("CompetencyId"); + + b.ToTable("UserProfileCompetencies", "identity"); + }); + + modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", b => + { + b.HasOne("Nashel.Modules.Identity.Domain.Aggregates.Account", null) + .WithOne("Profile") + .HasForeignKey("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Nashel.Modules.Identity.Domain.Entities.WorkSchedule", b => + { + b.HasOne("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", null) + .WithOne("WorkSchedule") + .HasForeignKey("Nashel.Modules.Identity.Domain.Entities.WorkSchedule", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("UserProfileCompetencies", b => + { + b.HasOne("Nashel.Modules.Identity.Domain.Entities.Competency", null) + .WithMany() + .HasForeignKey("CompetencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", null) + .WithMany() + .HasForeignKey("UserProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.Account", b => + { + b.Navigation("Profile") + .IsRequired(); + }); + + modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", b => + { + b.Navigation("WorkSchedule"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260216120252_AddCurrentLocationAndRemoveIs24_7.cs b/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260216120252_AddCurrentLocationAndRemoveIs24_7.cs new file mode 100644 index 0000000..456210f --- /dev/null +++ b/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260216120252_AddCurrentLocationAndRemoveIs24_7.cs @@ -0,0 +1,92 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddCurrentLocationAndRemoveIs24_7 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Is24_7", + schema: "identity", + table: "WorkSchedules"); + + migrationBuilder.DropColumn( + name: "WorkingHours", + schema: "identity", + table: "WorkSchedules"); + + migrationBuilder.AlterColumn( + name: "WorkingDays", + schema: "identity", + table: "WorkSchedules", + type: "character varying(2000)", + maxLength: 2000, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.AddColumn( + name: "CurrentLocation", + schema: "identity", + table: "UserProfiles", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Location", + schema: "identity", + table: "UserProfiles", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CurrentLocation", + schema: "identity", + table: "UserProfiles"); + + migrationBuilder.DropColumn( + name: "Location", + schema: "identity", + table: "UserProfiles"); + + migrationBuilder.AlterColumn( + name: "WorkingDays", + schema: "identity", + table: "WorkSchedules", + type: "character varying(100)", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(2000)", + oldMaxLength: 2000, + oldNullable: true); + + migrationBuilder.AddColumn( + name: "Is24_7", + schema: "identity", + table: "WorkSchedules", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "WorkingHours", + schema: "identity", + table: "WorkSchedules", + type: "character varying(100)", + maxLength: 100, + nullable: true); + } + } +} diff --git a/src/Modules/Identity/Infrastructure/Persistence/Migrations/IdentityDbContextModelSnapshot.cs b/src/Modules/Identity/Infrastructure/Persistence/Migrations/IdentityDbContextModelSnapshot.cs index 9596a70..7c88abe 100644 --- a/src/Modules/Identity/Infrastructure/Persistence/Migrations/IdentityDbContextModelSnapshot.cs +++ b/src/Modules/Identity/Infrastructure/Persistence/Migrations/IdentityDbContextModelSnapshot.cs @@ -62,6 +62,9 @@ namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations .HasMaxLength(200) .HasColumnType("character varying(200)"); + b.Property("CurrentLocation") + .HasColumnType("text"); + b.Property("Description") .HasMaxLength(2048) .HasColumnType("character varying(2048)"); @@ -80,6 +83,9 @@ namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations .HasMaxLength(100) .HasColumnType("character varying(100)"); + b.Property("Location") + .HasColumnType("text"); + b.Property("Patronymic") .HasMaxLength(100) .HasColumnType("character varying(100)"); @@ -114,19 +120,12 @@ namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations b.Property("Id") .HasColumnType("uuid"); - b.Property("Is24_7") - .HasColumnType("boolean"); - b.Property("IsAlwaysReady") .HasColumnType("boolean"); b.Property("WorkingDays") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("WorkingHours") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.HasKey("Id"); diff --git a/src/Modules/Identity/Infrastructure/Repositories/AccountRepository.cs b/src/Modules/Identity/Infrastructure/Repositories/AccountRepository.cs index 41bbf39..b2528e4 100644 --- a/src/Modules/Identity/Infrastructure/Repositories/AccountRepository.cs +++ b/src/Modules/Identity/Infrastructure/Repositories/AccountRepository.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Nashel.Modules.Identity.Domain.Aggregates; +using Nashel.Modules.Identity.Domain.Entities; using Nashel.Modules.Identity.Domain.Repositories; using Nashel.Modules.Identity.Infrastructure.Persistence; @@ -20,6 +21,12 @@ public class AccountRepository : IAccountRepository if (account.Profile != null) { await _context.UserProfiles.AddAsync(account.Profile, cancellationToken); + + // Добавляем WorkSchedule если есть + if (account.Profile.WorkSchedule != null) + { + await _context.WorkSchedules.AddAsync(account.Profile.WorkSchedule, cancellationToken); + } } await _context.Accounts.AddAsync(account, cancellationToken); await _context.SaveChangesAsync(cancellationToken); @@ -30,6 +37,8 @@ public class AccountRepository : IAccountRepository return await _context.Accounts .Include(a => a.Profile) .ThenInclude(p => p.Competencies) + .Include(a => a.Profile) + .ThenInclude(p => p.WorkSchedule) .FirstOrDefaultAsync(a => a.Id == id, cancellationToken); } @@ -42,12 +51,88 @@ public class AccountRepository : IAccountRepository public async Task UpdateAsync(Account account, CancellationToken cancellationToken) { + Console.WriteLine($"[AccountRepository.UpdateAsync] Starting update for account: {account.Id}"); + // Явно обновляем профиль, чтобы EF Core сохранил изменения в таблицу UserProfiles if (account.Profile != null) { - _context.UserProfiles.Update(account.Profile); + Console.WriteLine($"[AccountRepository.UpdateAsync] Profile found: {account.Profile.Id}"); + + // Загружаем текущий профиль с компетенциями + var existingProfile = await _context.UserProfiles + .Include(p => p.Competencies) + .Include(p => p.WorkSchedule) + .FirstOrDefaultAsync(p => p.Id == account.Profile.Id, cancellationToken); + + if (existingProfile != null) + { + Console.WriteLine($"[AccountRepository.UpdateAsync] Existing profile loaded"); + + // Обновляем свойства профиля + existingProfile.Update( + account.Profile.FirstName, + account.Profile.LastName, + account.Profile.Patronymic, + account.Profile.CompanyName, + account.Profile.Inn, + account.Profile.Description, + account.Profile.Location, + account.Profile.CurrentLocation + ); + + // Обновляем компетенции через доменный метод + // Поскольку компетенции уже сохранены через CompetencyRepository, + // просто загружаем их из БД по Id для правильной работы EF Core + var competencies = new List(); + if (account.Profile.Competencies != null && account.Profile.Competencies.Any()) + { + var competencyIds = account.Profile.Competencies + .Select(c => c.Id) + .Distinct() + .ToList(); + + Console.WriteLine($"[AccountRepository.UpdateAsync] Loading {competencyIds.Count} competencies from DB"); + competencies = await _context.Competencies + .Where(c => competencyIds.Contains(c.Id)) + .ToListAsync(cancellationToken); + } + existingProfile.UpdateCompetencies(competencies); + + // Обновляем WorkSchedule + if (account.Profile.WorkSchedule != null) + { + if (existingProfile.WorkSchedule != null) + { + // Обновляем существующий WorkSchedule + existingProfile.WorkSchedule.Update( + account.Profile.WorkSchedule.IsAlwaysReady, + account.Profile.WorkSchedule.WorkingDays + ); + } + else + { + // Создаём новый WorkSchedule + var newSchedule = WorkSchedule.Create( + account.Profile.WorkSchedule.IsAlwaysReady, + account.Profile.WorkSchedule.WorkingDays + ); + // EF Core сам установит правильную связь через Id профиля + existingProfile.SetWorkSchedule(newSchedule); + _context.WorkSchedules.Add(newSchedule); + } + } + + _context.UserProfiles.Update(existingProfile); + } + else + { + Console.WriteLine($"[AccountRepository.UpdateAsync] Existing profile not found, using account profile"); + _context.UserProfiles.Update(account.Profile); + } } _context.Accounts.Update(account); + Console.WriteLine($"[AccountRepository.UpdateAsync] Saving changes to database"); await _context.SaveChangesAsync(cancellationToken); + Console.WriteLine($"[AccountRepository.UpdateAsync] Successfully completed"); } } diff --git a/src/Modules/Identity/Infrastructure/Repositories/CompetencyRepository.cs b/src/Modules/Identity/Infrastructure/Repositories/CompetencyRepository.cs new file mode 100644 index 0000000..99e06e7 --- /dev/null +++ b/src/Modules/Identity/Infrastructure/Repositories/CompetencyRepository.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore; +using Nashel.Modules.Identity.Domain.Entities; +using Nashel.Modules.Identity.Domain.Repositories; +using Nashel.Modules.Identity.Infrastructure.Persistence; + +namespace Nashel.Modules.Identity.Infrastructure.Repositories; + +public class CompetencyRepository : ICompetencyRepository +{ + private readonly IdentityDbContext _context; + + public CompetencyRepository(IdentityDbContext context) + { + _context = context; + } + + public async Task GetByNameAsync(string name, CancellationToken cancellationToken) + { + return await _context.Competencies + .FirstOrDefaultAsync(c => c.Name == name, cancellationToken); + } + + public async Task AddAsync(Competency competency, CancellationToken cancellationToken) + { + await _context.Competencies.AddAsync(competency, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task> GetAllAsync(CancellationToken cancellationToken) + { + return await _context.Competencies + .ToListAsync(cancellationToken); + } +} diff --git a/src/Modules/Identity/Presentation/Endpoints/IdentityEndpoints.cs b/src/Modules/Identity/Presentation/Endpoints/IdentityEndpoints.cs index 5114bc0..779655a 100644 --- a/src/Modules/Identity/Presentation/Endpoints/IdentityEndpoints.cs +++ b/src/Modules/Identity/Presentation/Endpoints/IdentityEndpoints.cs @@ -58,16 +58,23 @@ public static class IdentityEndpoints profileGroup.MapPost("/become-performer", async (BecomePerformerRequest request, ISender sender) => { + Console.WriteLine($"[BecomePerformerEndpoint] Request received"); + Console.WriteLine($"[BecomePerformerEndpoint] Description: {request.Description?.Substring(0, Math.Min(50, request.Description.Length))}..."); + Console.WriteLine($"[BecomePerformerEndpoint] Competencies count: {request.Competencies?.Count ?? 0}"); + Console.WriteLine($"[BecomePerformerEndpoint] Location: {request.Location}"); + Console.WriteLine($"[BecomePerformerEndpoint] IsAlwaysReady: {request.IsAlwaysReady}"); + await sender.Send(new BecomePerformerCommand { Description = request.Description, CompetencyNames = request.Competencies, - Is24_7 = request.Is24_7, + Location = request.Location, + CurrentLocation = request.CurrentLocation, IsAlwaysReady = request.IsAlwaysReady, - WorkingDays = request.WorkingDays, - WorkingHoursStart = request.WorkingHoursStart, - WorkingHoursEnd = request.WorkingHoursEnd + WorkingDays = request.WorkingDays }); + + Console.WriteLine($"[BecomePerformerEndpoint] Successfully completed"); return Results.Ok(); }) .WithName("BecomePerformer") @@ -107,9 +114,8 @@ public record UpdateCompetenciesRequest(List Competencies); public record BecomePerformerRequest( string Description, List Competencies, - bool Is24_7, + string? Location, + string? CurrentLocation, bool IsAlwaysReady, - List? WorkingDays, - string? WorkingHoursStart, - string? WorkingHoursEnd + string? WorkingDays );