Услуги
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0-preview AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Устанавливаем инструмент для миграций EF Core
|
||||
RUN dotnet tool install --global dotnet-ef --version 9.0.0
|
||||
ENV PATH="${PATH}:/root/.dotnet/tools"
|
||||
|
||||
# Копируем решение и проекты
|
||||
COPY ["Nashel.sln", "./"]
|
||||
COPY ["src/", "src/"]
|
||||
|
||||
# Восстанавливаем зависимости
|
||||
RUN dotnet restore "Nashel.sln"
|
||||
|
||||
WORKDIR "/src/src/Host"
|
||||
ENTRYPOINT ["sh", "-c", "\
|
||||
echo 'Running migrations...' && \
|
||||
export PATH=\"$PATH:/root/.dotnet/tools\" && \
|
||||
dotnet ef database update --project ../Modules/Identity/Infrastructure --startup-project . --context IdentityDbContext && \
|
||||
dotnet ef database update --project ../Modules/Catalog/Infrastructure --startup-project . --context CatalogDbContext && \
|
||||
dotnet ef database update --project ../Modules/Collaboration/Infrastructure --startup-project . --context CollaborationDbContext && \
|
||||
dotnet ef database update --project ../Modules/Order/Infrastructure --startup-project . --context OrderDbContext && \
|
||||
dotnet ef database update --project ../Modules/Reputation/Infrastructure --startup-project . --context ReputationDbContext && \
|
||||
dotnet ef database update --project ../Modules/Geo/Infrastructure --startup-project . --context GeoDbContext && \
|
||||
echo 'All migrations completed successfully!'\
|
||||
"]
|
||||
+1
-3
@@ -2,7 +2,7 @@ services:
|
||||
migrations:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: Dockerfile.migrations
|
||||
container_name: nashel-migrations
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
@@ -10,8 +10,6 @@ services:
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Nashel.BuildingBlocks.Domain;
|
||||
|
||||
public class Result<T>
|
||||
{
|
||||
public bool IsSuccess { get; }
|
||||
public T? Value { get; }
|
||||
public string? Error { get; }
|
||||
|
||||
protected Result(bool isSuccess, T? value, string? error)
|
||||
{
|
||||
IsSuccess = isSuccess;
|
||||
Value = value;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public static Result<T> Success(T value) => new Result<T>(true, value, null);
|
||||
public static Result<T> Failure(string error) => new Result<T>(false, default, error);
|
||||
}
|
||||
|
||||
public class Result : Result<object>
|
||||
{
|
||||
protected Result(bool isSuccess, string? error) : base(isSuccess, null, error)
|
||||
{
|
||||
}
|
||||
|
||||
public static Result Success() => new Result(true, null);
|
||||
public static new Result Failure(string error) => new Result(false, error);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using MediatR;
|
||||
using Nashel.BuildingBlocks.Domain;
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
|
||||
@@ -8,17 +9,17 @@ namespace Nashel.Modules.Catalog.Application.Commands;
|
||||
/// <summary>
|
||||
/// Команда создания категории.
|
||||
/// </summary>
|
||||
public record CreateCategoryCommand : IRequest<Guid>
|
||||
public record CreateCategoryCommand : IRequest<Result<Guid>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Название категории.
|
||||
/// </summary>
|
||||
public string Title { get; init; }
|
||||
public string Name { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// URL-friendly идентификатор (слаг).
|
||||
/// </summary>
|
||||
public string Slug { get; init; }
|
||||
public string Slug { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// ID родительской категории (null для корневых).
|
||||
@@ -30,9 +31,9 @@ public record CreateCategoryCommand : IRequest<Guid>
|
||||
/// </summary>
|
||||
public JsonDocument? AttributeSchema { get; init; }
|
||||
|
||||
public CreateCategoryCommand(string title, string slug, Guid? parentId, JsonDocument? attributeSchema)
|
||||
public CreateCategoryCommand(string name, string slug, Guid? parentId, JsonDocument? attributeSchema)
|
||||
{
|
||||
Title = title;
|
||||
Name = name;
|
||||
Slug = slug;
|
||||
ParentId = parentId;
|
||||
AttributeSchema = attributeSchema;
|
||||
@@ -41,7 +42,7 @@ public record CreateCategoryCommand : IRequest<Guid>
|
||||
public CreateCategoryCommand() { } // For deserialization
|
||||
}
|
||||
|
||||
public class CreateCategoryCommandHandler : IRequestHandler<CreateCategoryCommand, Guid>
|
||||
public class CreateCategoryCommandHandler : IRequestHandler<CreateCategoryCommand, Result<Guid>>
|
||||
{
|
||||
private readonly ICategoryRepository _repository;
|
||||
|
||||
@@ -50,19 +51,19 @@ public class CreateCategoryCommandHandler : IRequestHandler<CreateCategoryComman
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<Guid> Handle(CreateCategoryCommand request, CancellationToken cancellationToken)
|
||||
public async Task<Result<Guid>> Handle(CreateCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.ParentId.HasValue)
|
||||
{
|
||||
var parentCategory = await _repository.GetByIdAsync(request.ParentId.Value, cancellationToken);
|
||||
if (parentCategory == null)
|
||||
{
|
||||
throw new ApplicationException($"Родительская категория с ID {request.ParentId} не найдена.");
|
||||
return Result<Guid>.Failure($"Родительская категория с ID {request.ParentId} не найдена.");
|
||||
}
|
||||
}
|
||||
|
||||
var category = new Category(
|
||||
request.Title,
|
||||
request.Name,
|
||||
request.Slug,
|
||||
request.ParentId,
|
||||
request.AttributeSchema
|
||||
@@ -70,6 +71,6 @@ public class CreateCategoryCommandHandler : IRequestHandler<CreateCategoryComman
|
||||
|
||||
await _repository.AddAsync(category, cancellationToken);
|
||||
|
||||
return category.Id;
|
||||
return Result<Guid>.Success(category.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System.Text.Json;
|
||||
using MediatR;
|
||||
using MediatR;
|
||||
using Nashel.BuildingBlocks.Application.Abstractions;
|
||||
using Nashel.BuildingBlocks.Domain;
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
using Nashel.Modules.Catalog.Domain.ValueObjects;
|
||||
@@ -9,16 +12,8 @@ namespace Nashel.Modules.Catalog.Application.Commands;
|
||||
/// <summary>
|
||||
/// Команда создания услуги (оффера).
|
||||
/// </summary>
|
||||
public record CreateOfferCommand : IRequest<Guid>
|
||||
public record CreateOfferCommand : IRequest<Result<Guid>>
|
||||
{
|
||||
/// <summary>
|
||||
/// ID владельца (пользователя/исполнителя).
|
||||
/// </summary>
|
||||
public Guid OwnerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// ID категории услуги.
|
||||
/// </summary>
|
||||
public Guid CategoryId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
@@ -41,9 +36,8 @@ public record CreateOfferCommand : IRequest<Guid>
|
||||
/// </summary>
|
||||
public JsonDocument? Attributes { get; init; }
|
||||
|
||||
public CreateOfferCommand(Guid ownerId, Guid categoryId, string title, string description, Price price, JsonDocument? attributes)
|
||||
public CreateOfferCommand(Guid categoryId, string title, string description, Price price, JsonDocument? attributes)
|
||||
{
|
||||
OwnerId = ownerId;
|
||||
CategoryId = categoryId;
|
||||
Title = title;
|
||||
Description = description;
|
||||
@@ -54,19 +48,24 @@ public record CreateOfferCommand : IRequest<Guid>
|
||||
public CreateOfferCommand() { }
|
||||
}
|
||||
|
||||
public class CreateOfferCommandHandler : IRequestHandler<CreateOfferCommand, Guid>
|
||||
public class CreateOfferCommandHandler : IRequestHandler<CreateOfferCommand, Result<Guid>>
|
||||
{
|
||||
private readonly IOfferRepository _repository;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
|
||||
public CreateOfferCommandHandler(IOfferRepository repository)
|
||||
public CreateOfferCommandHandler(IOfferRepository repository, ICurrentUserService currentUserService)
|
||||
{
|
||||
_repository = repository;
|
||||
_currentUserService = currentUserService;
|
||||
}
|
||||
|
||||
public async Task<Guid> Handle(CreateOfferCommand request, CancellationToken cancellationToken)
|
||||
public async Task<Result<Guid>> Handle(CreateOfferCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = _currentUserService.UserId;
|
||||
if (userId == null) return Result<Guid>.Failure("Неавторизован");
|
||||
|
||||
var offer = new Offer(
|
||||
request.OwnerId,
|
||||
userId.Value,
|
||||
request.CategoryId,
|
||||
request.Title,
|
||||
request.Description,
|
||||
@@ -76,6 +75,6 @@ public class CreateOfferCommandHandler : IRequestHandler<CreateOfferCommand, Gui
|
||||
|
||||
await _repository.AddAsync(offer, cancellationToken);
|
||||
|
||||
return offer.Id;
|
||||
return Result<Guid>.Success(offer.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public record CategoryDto
|
||||
/// <summary>
|
||||
/// Название категории.
|
||||
/// </summary>
|
||||
public string Title { get; init; } = default!;
|
||||
public string Name { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// URL-совместимый идентификатор (slug).
|
||||
@@ -33,10 +33,10 @@ public record CategoryDto
|
||||
/// </summary>
|
||||
public JsonDocument? AttributeSchema { get; init; }
|
||||
|
||||
public CategoryDto(Guid id, string title, string slug, Guid? parentId, JsonDocument? attributeSchema)
|
||||
public CategoryDto(Guid id, string name, string slug, Guid? parentId, JsonDocument? attributeSchema)
|
||||
{
|
||||
Id = id;
|
||||
Title = title;
|
||||
Name = name;
|
||||
Slug = slug;
|
||||
ParentId = parentId;
|
||||
AttributeSchema = attributeSchema;
|
||||
@@ -58,7 +58,7 @@ public record OfferDto
|
||||
/// <summary>
|
||||
/// Идентификатор владельца (исполнителя).
|
||||
/// </summary>
|
||||
public Guid OwnerId { get; init; }
|
||||
public Guid PerformerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Идентификатор категории.
|
||||
@@ -90,10 +90,10 @@ public record OfferDto
|
||||
/// </summary>
|
||||
public bool IsActive { get; init; }
|
||||
|
||||
public OfferDto(Guid id, Guid ownerId, Guid categoryId, string title, string description, Price price, JsonDocument? attributes, bool isActive)
|
||||
public OfferDto(Guid id, Guid performerId, Guid categoryId, string title, string description, Price price, JsonDocument? attributes, bool isActive)
|
||||
{
|
||||
Id = id;
|
||||
OwnerId = ownerId;
|
||||
PerformerId = performerId;
|
||||
CategoryId = categoryId;
|
||||
Title = title;
|
||||
Description = description;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using MediatR;
|
||||
using Nashel.BuildingBlocks.Domain;
|
||||
using Nashel.Modules.Catalog.Application.Common;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
|
||||
@@ -8,9 +9,9 @@ namespace Nashel.Modules.Catalog.Application.Queries;
|
||||
/// <summary>
|
||||
/// Запрос дерева категорий.
|
||||
/// </summary>
|
||||
public record GetCategoriesQuery() : IRequest<List<CategoryDto>>;
|
||||
public record GetCategoriesQuery() : IRequest<Result<List<CategoryDto>>>;
|
||||
|
||||
public class GetCategoriesQueryHandler : IRequestHandler<GetCategoriesQuery, List<CategoryDto>>
|
||||
public class GetCategoriesQueryHandler : IRequestHandler<GetCategoriesQuery, Result<List<CategoryDto>>>
|
||||
{
|
||||
private readonly ICategoryRepository _repository;
|
||||
|
||||
@@ -19,7 +20,7 @@ public class GetCategoriesQueryHandler : IRequestHandler<GetCategoriesQuery, Lis
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<List<CategoryDto>> Handle(GetCategoriesQuery request, CancellationToken cancellationToken)
|
||||
public async Task<Result<List<CategoryDto>>> Handle(GetCategoriesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var rawCategories = await _repository.GetAllAsync(cancellationToken);
|
||||
|
||||
@@ -27,12 +28,14 @@ public class GetCategoriesQueryHandler : IRequestHandler<GetCategoriesQuery, Lis
|
||||
// Если нужно дерево: нужно иметь DTO с List<CategoryDto> Children
|
||||
// Для MVP возвращаем плоский список, фронтенд сам строит дерево по ParentId
|
||||
|
||||
return rawCategories.Select(c => new CategoryDto(
|
||||
var list = rawCategories.Select(c => new CategoryDto(
|
||||
c.Id,
|
||||
c.Title,
|
||||
c.Name,
|
||||
c.Slug,
|
||||
c.ParentId,
|
||||
c.AttributeSchema
|
||||
)).ToList();
|
||||
|
||||
return Result<List<CategoryDto>>.Success(list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using MediatR;
|
||||
using Nashel.BuildingBlocks.Application.Abstractions;
|
||||
using Nashel.BuildingBlocks.Domain;
|
||||
using Nashel.Modules.Catalog.Application.Common;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Запрос получения услуг текущего пользователя.
|
||||
/// </summary>
|
||||
public record GetMyOffersQuery() : IRequest<Result<List<OfferDto>>>;
|
||||
|
||||
public class GetMyOffersQueryHandler : IRequestHandler<GetMyOffersQuery, Result<List<OfferDto>>>
|
||||
{
|
||||
private readonly IOfferRepository _repository;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
|
||||
public GetMyOffersQueryHandler(IOfferRepository repository, ICurrentUserService currentUserService)
|
||||
{
|
||||
_repository = repository;
|
||||
_currentUserService = currentUserService;
|
||||
}
|
||||
|
||||
public async Task<Result<List<OfferDto>>> Handle(GetMyOffersQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = _currentUserService.UserId;
|
||||
if (userId == null) return Result<List<OfferDto>>.Failure("Неавторизован");
|
||||
|
||||
var offers = await _repository.GetByPerformerIdAsync(userId.Value, cancellationToken);
|
||||
|
||||
var list = offers.Select(offer => new OfferDto(
|
||||
offer.Id,
|
||||
offer.PerformerId,
|
||||
offer.CategoryId,
|
||||
offer.Title,
|
||||
offer.Description ?? string.Empty,
|
||||
offer.Price,
|
||||
offer.Attributes,
|
||||
offer.IsActive
|
||||
)).ToList();
|
||||
|
||||
return Result<List<OfferDto>>.Success(list);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using MediatR;
|
||||
using Nashel.BuildingBlocks.Domain;
|
||||
using Nashel.Modules.Catalog.Application.Common;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
|
||||
@@ -7,9 +8,9 @@ namespace Nashel.Modules.Catalog.Application.Queries;
|
||||
/// <summary>
|
||||
/// Запрос деталей услуги по ID.
|
||||
/// </summary>
|
||||
public record GetOfferByIdQuery(Guid Id) : IRequest<OfferDto?>;
|
||||
public record GetOfferByIdQuery(Guid Id) : IRequest<Result<OfferDto>>;
|
||||
|
||||
public class GetOfferByIdQueryHandler : IRequestHandler<GetOfferByIdQuery, OfferDto?>
|
||||
public class GetOfferByIdQueryHandler : IRequestHandler<GetOfferByIdQuery, Result<OfferDto>>
|
||||
{
|
||||
private readonly IOfferRepository _repository;
|
||||
|
||||
@@ -18,20 +19,22 @@ public class GetOfferByIdQueryHandler : IRequestHandler<GetOfferByIdQuery, Offer
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<OfferDto?> Handle(GetOfferByIdQuery request, CancellationToken cancellationToken)
|
||||
public async Task<Result<OfferDto>> Handle(GetOfferByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var offer = await _repository.GetByIdAsync(request.Id, cancellationToken);
|
||||
if (offer == null) return null;
|
||||
if (offer == null) return Result<OfferDto>.Failure("Offer not found");
|
||||
|
||||
return new OfferDto(
|
||||
var dto = new OfferDto(
|
||||
offer.Id,
|
||||
offer.OwnerId,
|
||||
offer.PerformerId,
|
||||
offer.CategoryId,
|
||||
offer.Title,
|
||||
offer.Description,
|
||||
offer.Description ?? string.Empty,
|
||||
offer.Price,
|
||||
offer.Attributes,
|
||||
offer.IsActive
|
||||
);
|
||||
|
||||
return Result<OfferDto>.Success(dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public class Category
|
||||
/// <summary>
|
||||
/// Название категории.
|
||||
/// </summary>
|
||||
public string Title { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL-friendly идентификатор (слаг).
|
||||
@@ -39,10 +39,10 @@ public class Category
|
||||
/// <summary>
|
||||
/// Создает новую категорию.
|
||||
/// </summary>
|
||||
public Category(string title, string slug, Guid? parentId, JsonDocument? attributeSchema = null)
|
||||
public Category(string name, string slug, Guid? parentId, JsonDocument? attributeSchema = null)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
Title = title;
|
||||
Name = name;
|
||||
Slug = slug;
|
||||
ParentId = parentId;
|
||||
AttributeSchema = attributeSchema;
|
||||
|
||||
@@ -16,7 +16,7 @@ public class Offer
|
||||
/// <summary>
|
||||
/// ID владельца (пользователя/исполнителя).
|
||||
/// </summary>
|
||||
public Guid OwnerId { get; private set; }
|
||||
public Guid PerformerId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID услуги/категории.
|
||||
@@ -48,21 +48,27 @@ public class Offer
|
||||
/// </summary>
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Дата создания.
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; private set; }
|
||||
|
||||
// Конструктор по умолчанию для EF Core
|
||||
private Offer() { }
|
||||
|
||||
/// <summary>
|
||||
/// Создает новый оффер.
|
||||
/// </summary>
|
||||
public Offer(Guid ownerId, Guid categoryId, string title, string description, Price price, JsonDocument? attributes = null)
|
||||
public Offer(Guid performerId, Guid categoryId, string title, string description, Price price, JsonDocument? attributes = null)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
OwnerId = ownerId;
|
||||
PerformerId = performerId;
|
||||
CategoryId = categoryId;
|
||||
Title = title;
|
||||
Description = description;
|
||||
Price = price;
|
||||
Attributes = attributes;
|
||||
IsActive = true;
|
||||
CreatedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ namespace Nashel.Modules.Catalog.Domain.Repositories;
|
||||
public interface IOfferRepository
|
||||
{
|
||||
Task<Offer?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<List<Offer>> GetByPerformerIdAsync(Guid performerId, CancellationToken cancellationToken = default);
|
||||
Task AddAsync(Offer offer, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
src/Modules/Catalog/Infrastructure/Persistence/Migrations/20260302095500_CatalogRefactor.Designer.cs
Generated
+140
@@ -0,0 +1,140 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Nashel.Modules.Catalog.Infrastructure.Persistence;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Nashel.Modules.Catalog.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(CatalogDbContext))]
|
||||
[Migration("20260302095500_CatalogRefactor")]
|
||||
partial class CatalogRefactor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Category", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<JsonDocument>("AttributeSchema")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Categories", "catalog");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Offer", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<JsonDocument>("Attributes")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid>("PerformerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Attributes");
|
||||
|
||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Attributes"), "gin");
|
||||
|
||||
b.ToTable("Offers", "catalog");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Category", b =>
|
||||
{
|
||||
b.HasOne("Nashel.Modules.Catalog.Domain.Aggregates.Category", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Offer", b =>
|
||||
{
|
||||
b.OwnsOne("Nashel.Modules.Catalog.Domain.ValueObjects.Price", "Price", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("OfferId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("PriceAmount");
|
||||
|
||||
b1.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(3)
|
||||
.HasColumnType("character varying(3)")
|
||||
.HasColumnName("PriceCurrency");
|
||||
|
||||
b1.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("PriceType");
|
||||
|
||||
b1.HasKey("OfferId");
|
||||
|
||||
b1.ToTable("Offers", "catalog");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("OfferId");
|
||||
});
|
||||
|
||||
b.Navigation("Price")
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Nashel.Modules.Catalog.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class CatalogRefactor : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "OwnerId",
|
||||
schema: "catalog",
|
||||
table: "Offers",
|
||||
newName: "PerformerId");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Title",
|
||||
schema: "catalog",
|
||||
table: "Categories",
|
||||
newName: "Name");
|
||||
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "CreatedAt",
|
||||
schema: "catalog",
|
||||
table: "Offers",
|
||||
type: "timestamp with time zone",
|
||||
nullable: false,
|
||||
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedAt",
|
||||
schema: "catalog",
|
||||
table: "Offers");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "PerformerId",
|
||||
schema: "catalog",
|
||||
table: "Offers",
|
||||
newName: "OwnerId");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Name",
|
||||
schema: "catalog",
|
||||
table: "Categories",
|
||||
newName: "Title");
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-5
@@ -33,6 +33,10 @@ namespace Nashel.Modules.Catalog.Infrastructure.Persistence.Migrations
|
||||
b.Property<JsonDocument>("AttributeSchema")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
@@ -40,10 +44,6 @@ namespace Nashel.Modules.Catalog.Infrastructure.Persistence.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
@@ -66,13 +66,16 @@ namespace Nashel.Modules.Catalog.Infrastructure.Persistence.Migrations
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid>("OwnerId")
|
||||
b.Property<Guid>("PerformerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
|
||||
@@ -20,6 +20,13 @@ public class OfferRepository : IOfferRepository
|
||||
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Offer>> GetByPerformerIdAsync(Guid performerId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Offers
|
||||
.Where(o => o.PerformerId == performerId)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task AddAsync(Offer offer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.Offers.AddAsync(offer, cancellationToken);
|
||||
|
||||
@@ -21,8 +21,8 @@ public static class CatalogEndpoints
|
||||
|
||||
catalogGroup.MapPost("/categories", async ([FromBody] CreateCategoryCommand command, ISender sender) =>
|
||||
{
|
||||
var id = await sender.Send(command);
|
||||
return Results.Ok(id);
|
||||
var result = await sender.Send(command);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error);
|
||||
})
|
||||
.WithName("CreateCategory")
|
||||
.WithOpenApi(operation => new(operation) { Summary = "Создать категорию (Admin)", Description = "Создает новую категорию. Требуются права администратора." });
|
||||
@@ -30,17 +30,27 @@ public static class CatalogEndpoints
|
||||
catalogGroup.MapGet("/categories", async (ISender sender) =>
|
||||
{
|
||||
var result = await sender.Send(new GetCategoriesQuery());
|
||||
return Results.Ok(result);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error);
|
||||
})
|
||||
.WithName("GetCategories")
|
||||
.WithOpenApi(operation => new(operation) { Summary = "Получить все категории", Description = "Возвращает плоский список категорий с указанием ParentId для иерархии." });
|
||||
|
||||
// --- Услуги (Offers) ---
|
||||
|
||||
catalogGroup.MapPost("/offers", async ([FromBody] CreateOfferCommand command, ISender sender) =>
|
||||
var protectedOffersGroup = catalogGroup.MapGroup("").RequireAuthorization();
|
||||
|
||||
protectedOffersGroup.MapGet("/offers/my", async (ISender sender) =>
|
||||
{
|
||||
var id = await sender.Send(command);
|
||||
return Results.Ok(id);
|
||||
var result = await sender.Send(new GetMyOffersQuery());
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error);
|
||||
})
|
||||
.WithName("GetMyOffers")
|
||||
.WithOpenApi(operation => new(operation) { Summary = "Получить мои услуги", Description = "Возвращает список услуг текущего пользователя." });
|
||||
|
||||
protectedOffersGroup.MapPost("/offers", async ([FromBody] CreateOfferCommand command, ISender sender) =>
|
||||
{
|
||||
var result = await sender.Send(command);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error);
|
||||
})
|
||||
.WithName("CreateOffer")
|
||||
.WithOpenApi(operation => new(operation) { Summary = "Создать оффер/услугу", Description = "Создает новое предложение услуги в указанной категории." });
|
||||
@@ -48,7 +58,7 @@ public static class CatalogEndpoints
|
||||
catalogGroup.MapGet("/offers/{id:guid}", async (Guid id, ISender sender) =>
|
||||
{
|
||||
var result = await sender.Send(new GetOfferByIdQuery(id));
|
||||
return result is not null ? Results.Ok(result) : Results.NotFound();
|
||||
return result.IsSuccess && result.Value is not null ? Results.Ok(result.Value) : Results.NotFound();
|
||||
})
|
||||
.WithName("GetOfferById")
|
||||
.WithOpenApi(operation => new(operation) { Summary = "Получить детали услуги по ID", Description = "Возвращает полную информацию об услуге." });
|
||||
|
||||
@@ -24,12 +24,12 @@ public record RegisterUserCommand : IRequest<Guid>
|
||||
/// <summary>
|
||||
/// Имя.
|
||||
/// </summary>
|
||||
public string FirstName { get; init; } = default!;
|
||||
public string? FirstName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Фамилия.
|
||||
/// </summary>
|
||||
public string LastName { get; init; } = default!;
|
||||
public string? LastName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Отчество (необязательно).
|
||||
@@ -97,8 +97,8 @@ public class RegisterUserCommandHandler : IRequestHandler<RegisterUserCommand, G
|
||||
existingAccount.Roles.Clear();
|
||||
existingAccount.Roles.Add(Role.User);
|
||||
|
||||
// Добавляем роль только для Company
|
||||
if (request.Role == Role.Company)
|
||||
// Добавляем запрошенную роль (кроме User)
|
||||
if (request.Role != Role.User)
|
||||
{
|
||||
existingAccount.Roles.Add(request.Role);
|
||||
}
|
||||
@@ -116,8 +116,8 @@ public class RegisterUserCommandHandler : IRequestHandler<RegisterUserCommand, G
|
||||
var passwordHashNew = _passwordHasher.HashPassword(request.Password);
|
||||
var account = Account.Create(request.Phone, passwordHashNew);
|
||||
|
||||
// Добавляем роль только для Company. Newbie не добавляем - она добавляется через BecomePerformerCommand
|
||||
if (request.Role == Role.Company)
|
||||
// Добавляем запрошенную роль (кроме User, так как он добавляется по умолчанию)
|
||||
if (request.Role != Role.User)
|
||||
{
|
||||
account.Roles.Add(request.Role);
|
||||
}
|
||||
|
||||
@@ -22,15 +22,15 @@ public record ProfileResponse(
|
||||
Guid Id,
|
||||
string Phone,
|
||||
List<string> Roles,
|
||||
string FirstName,
|
||||
string LastName,
|
||||
string? FirstName,
|
||||
string? LastName,
|
||||
string? Patronymic,
|
||||
string? CompanyName,
|
||||
string? Inn,
|
||||
string? Description,
|
||||
string? Location,
|
||||
string? CurrentLocation,
|
||||
string FullName,
|
||||
string? FullName,
|
||||
string? AvatarUrl,
|
||||
List<string> Competencies,
|
||||
WorkScheduleResponse? WorkSchedule);
|
||||
@@ -60,10 +60,13 @@ public class GetProfileQueryHandler : IRequestHandler<GetProfileQuery, ProfileRe
|
||||
throw new Exception("Account not found");
|
||||
}
|
||||
|
||||
var fullName = $"{account.Profile.FirstName} {account.Profile.LastName}";
|
||||
if (!string.IsNullOrEmpty(account.Profile.Patronymic))
|
||||
var names = new[] { account.Profile.FirstName, account.Profile.LastName, account.Profile.Patronymic }
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x));
|
||||
var fullName = string.Join(" ", names);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fullName))
|
||||
{
|
||||
fullName += $" {account.Profile.Patronymic}";
|
||||
fullName = account.Profile.CompanyName ?? "";
|
||||
}
|
||||
|
||||
// Парсинг WorkSchedule
|
||||
|
||||
@@ -17,12 +17,12 @@ public class RegisterUserCommandValidator : AbstractValidator<RegisterUserComman
|
||||
.MinimumLength(6).WithMessage("Пароль должен быть не менее 6 символов");
|
||||
|
||||
RuleFor(x => x.FirstName)
|
||||
.NotEmpty().WithMessage("Имя обязательно")
|
||||
.MinimumLength(2).WithMessage("Имя должно содержать минимум 2 символа");
|
||||
.NotEmpty().WithMessage("Имя обязательно").When(x => x.Role != Role.Company)
|
||||
.MinimumLength(2).WithMessage("Имя должно содержать минимум 2 символа").When(x => x.Role != Role.Company && !string.IsNullOrEmpty(x.FirstName));
|
||||
|
||||
RuleFor(x => x.LastName)
|
||||
.NotEmpty().WithMessage("Фамилия обязательна")
|
||||
.MinimumLength(2).WithMessage("Фамилия должна содержать минимум 2 символа");
|
||||
.NotEmpty().WithMessage("Фамилия обязательна").When(x => x.Role != Role.Company)
|
||||
.MinimumLength(2).WithMessage("Фамилия должна содержать минимум 2 символа").When(x => x.Role != Role.Company && !string.IsNullOrEmpty(x.LastName));
|
||||
|
||||
RuleFor(x => x.CompanyName)
|
||||
.NotEmpty().WithMessage("Название компании обязательно для юридических лиц")
|
||||
|
||||
@@ -81,17 +81,17 @@ public class Account : AggregateRoot<Guid>
|
||||
|
||||
public void BecomePerformer()
|
||||
{
|
||||
if (!Roles.Contains(Role.Newbie) && !Roles.Contains(Role.Master))
|
||||
if (!Roles.Contains(Role.Candidate) && !Roles.Contains(Role.Master))
|
||||
{
|
||||
Roles.Add(Role.Newbie);
|
||||
Roles.Add(Role.Candidate);
|
||||
}
|
||||
}
|
||||
|
||||
public void PromoteToMaster()
|
||||
{
|
||||
if (Roles.Contains(Role.Newbie))
|
||||
if (Roles.Contains(Role.Candidate))
|
||||
{
|
||||
Roles.Remove(Role.Newbie);
|
||||
Roles.Remove(Role.Candidate);
|
||||
}
|
||||
|
||||
if (!Roles.Contains(Role.Master))
|
||||
@@ -102,6 +102,10 @@ public class Account : AggregateRoot<Guid>
|
||||
|
||||
public void UpdatePerformerData(string description, IEnumerable<Competency> competencies, string? location, string? currentLocation, WorkSchedule? workSchedule)
|
||||
{
|
||||
if (Profile == null)
|
||||
{
|
||||
throw new InvalidOperationException("Profile not found");
|
||||
}
|
||||
Profile.UpdatePerformerData(description, competencies, location, currentLocation, workSchedule);
|
||||
}
|
||||
|
||||
@@ -112,6 +116,6 @@ public class Account : AggregateRoot<Guid>
|
||||
|
||||
public bool IsPerformer()
|
||||
{
|
||||
return Roles.Contains(Role.Newbie) || Roles.Contains(Role.Master);
|
||||
return Roles.Contains(Role.Candidate) || Roles.Contains(Role.Master);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ public class UserProfile : Entity<Guid>
|
||||
private const int MaxDescriptionLength = 2048;
|
||||
private readonly List<Competency> _competencies = new();
|
||||
|
||||
public string FirstName { get; private set; }
|
||||
public string LastName { get; private set; }
|
||||
public string? FirstName { get; private set; }
|
||||
public string? LastName { get; private set; }
|
||||
public string? Patronymic { get; private set; }
|
||||
public string? CompanyName { get; private set; }
|
||||
public string? Inn { get; private set; }
|
||||
@@ -24,7 +24,7 @@ public class UserProfile : Entity<Guid>
|
||||
// EF Core constructor
|
||||
private UserProfile() { }
|
||||
|
||||
private UserProfile(Guid id, string firstName, string lastName, string? patronymic, string? companyName, string? inn, string? description)
|
||||
private UserProfile(Guid id, string? firstName, string? lastName, string? patronymic, string? companyName, string? inn, string? description)
|
||||
{
|
||||
Id = id;
|
||||
FirstName = firstName;
|
||||
@@ -35,7 +35,7 @@ public class UserProfile : Entity<Guid>
|
||||
Description = description;
|
||||
}
|
||||
|
||||
public static UserProfile Create(Guid id, string firstName, string lastName, string? patronymic, string? companyName, string? inn, string? description)
|
||||
public static UserProfile Create(Guid id, string? firstName, string? lastName, string? patronymic, string? companyName, string? inn, string? description)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(description) && description.Length > MaxDescriptionLength)
|
||||
throw new ArgumentException($"Описание не может превышать {MaxDescriptionLength} символов", nameof(description));
|
||||
@@ -43,7 +43,7 @@ public class UserProfile : Entity<Guid>
|
||||
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, string? location = null, string? currentLocation = null)
|
||||
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));
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace Nashel.Modules.Identity.Domain.Enums;
|
||||
public enum Role
|
||||
{
|
||||
User = 0,
|
||||
Newbie,
|
||||
Candidate,
|
||||
Master,
|
||||
Company,
|
||||
|
||||
-2
@@ -15,11 +15,9 @@ public class UserProfileConfiguration : IEntityTypeConfiguration<UserProfile>
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.FirstName)
|
||||
.IsRequired()
|
||||
.HasMaxLength(100);
|
||||
|
||||
builder.Property(x => x.LastName)
|
||||
.IsRequired()
|
||||
.HasMaxLength(100);
|
||||
|
||||
builder.Property(x => x.Patronymic)
|
||||
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
// <auto-generated />
|
||||
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("20260302184149_RemoveNewbieRole")]
|
||||
partial class RemoveNewbieRole
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("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<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AvatarUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("CompanyName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("CurrentLocation")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Inn")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("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<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsAlwaysReady")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("WorkingDays")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("WorkSchedules", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("UserProfileCompetencies", b =>
|
||||
{
|
||||
b.Property<Guid>("UserProfileId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveNewbieRole : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "DeletedAt",
|
||||
schema: "identity",
|
||||
table: "Accounts",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsDeleted",
|
||||
schema: "identity",
|
||||
table: "Accounts",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeletedAt",
|
||||
schema: "identity",
|
||||
table: "Accounts");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsDeleted",
|
||||
schema: "identity",
|
||||
table: "Accounts");
|
||||
}
|
||||
}
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
// <auto-generated />
|
||||
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("20260302192325_MakeNamesOptional")]
|
||||
partial class MakeNamesOptional
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("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<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AvatarUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("CompanyName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("CurrentLocation")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Inn")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("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<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsAlwaysReady")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("WorkingDays")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("WorkSchedules", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("UserProfileCompetencies", b =>
|
||||
{
|
||||
b.Property<Guid>("UserProfileId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MakeNamesOptional : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "LastName",
|
||||
schema: "identity",
|
||||
table: "UserProfiles",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(100)",
|
||||
oldMaxLength: 100);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "FirstName",
|
||||
schema: "identity",
|
||||
table: "UserProfiles",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(100)",
|
||||
oldMaxLength: 100);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "LastName",
|
||||
schema: "identity",
|
||||
table: "UserProfiles",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(100)",
|
||||
oldMaxLength: 100,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "FirstName",
|
||||
schema: "identity",
|
||||
table: "UserProfiles",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(100)",
|
||||
oldMaxLength: 100,
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -28,6 +28,14 @@ namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
@@ -70,7 +78,6 @@ namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
@@ -79,7 +86,6 @@ namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ public class BecomePerformerCommandHandlerTests
|
||||
{
|
||||
// Arrange
|
||||
var account = Account.Create("123", "pass"); // Has User role only
|
||||
var profile = UserProfile.Create(account.Id, "TestName", "TestSurname", null, null, null, null);
|
||||
account.SetProfile(profile);
|
||||
var userId = account.Id;
|
||||
|
||||
_mockUserService.Setup(s => s.UserId).Returns(userId);
|
||||
@@ -40,7 +42,8 @@ public class BecomePerformerCommandHandlerTests
|
||||
.ReturnsAsync(account);
|
||||
|
||||
// Act
|
||||
await _handler.Handle(new BecomePerformerCommand(), CancellationToken.None);
|
||||
var command = new BecomePerformerCommand { Description = new string('x', 51) };
|
||||
await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
account.Roles.Should().Contain(Role.Candidate);
|
||||
|
||||
@@ -26,7 +26,7 @@ public class RegisterUserCommandHandlerTests
|
||||
{
|
||||
// Arrange
|
||||
var phone = "123";
|
||||
_mockRepo.Setup(r => r.GetByPhoneAsync(phone, It.IsAny<CancellationToken>()))
|
||||
_mockRepo.Setup(r => r.GetByPhoneIncludingDeletedAsync(phone, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((Account?)null);
|
||||
|
||||
_mockPasswordHasher.Setup(p => p.HashPassword("pass"))
|
||||
@@ -51,10 +51,10 @@ public class RegisterUserCommandHandlerTests
|
||||
{
|
||||
// Arrange
|
||||
var phone = "123";
|
||||
_mockRepo.Setup(r => r.GetByPhoneAsync(phone, It.IsAny<CancellationToken>()))
|
||||
_mockRepo.Setup(r => r.GetByPhoneIncludingDeletedAsync(phone, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Account.Create(phone, "pass"));
|
||||
|
||||
// Act
|
||||
// Assert
|
||||
var act = async () => await _handler.Handle(new RegisterUserCommand
|
||||
{
|
||||
Phone = phone,
|
||||
@@ -63,8 +63,8 @@ public class RegisterUserCommandHandlerTests
|
||||
LastName = "Ivanov"
|
||||
}, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<Exception>().WithMessage("Пользователь уже существует");
|
||||
var ex = await Assert.ThrowsAsync<Exception>(act);
|
||||
ex.Message.Should().Be("Пользователь с таким номером телефона уже существует");
|
||||
_mockRepo.Verify(r => r.AddAsync(It.IsAny<Account>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user