Услуги
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:
|
migrations:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile.migrations
|
||||||
container_name: nashel-migrations
|
container_name: nashel-migrations
|
||||||
environment:
|
environment:
|
||||||
- ASPNETCORE_ENVIRONMENT=Development
|
- ASPNETCORE_ENVIRONMENT=Development
|
||||||
@@ -10,8 +10,6 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
command: >
|
|
||||||
sh -c "cd /app/src/Host && dotnet ef database update --project ../Modules/Identity/Infrastructure --startup-project . --context IdentityDbContext"
|
|
||||||
networks:
|
networks:
|
||||||
- nashel-network
|
- 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 System.Text.Json;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using Nashel.BuildingBlocks.Domain;
|
||||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||||
|
|
||||||
@@ -8,17 +9,17 @@ namespace Nashel.Modules.Catalog.Application.Commands;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Команда создания категории.
|
/// Команда создания категории.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record CreateCategoryCommand : IRequest<Guid>
|
public record CreateCategoryCommand : IRequest<Result<Guid>>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Название категории.
|
/// Название категории.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Title { get; init; }
|
public string Name { get; init; } = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// URL-friendly идентификатор (слаг).
|
/// URL-friendly идентификатор (слаг).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Slug { get; init; }
|
public string Slug { get; init; } = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ID родительской категории (null для корневых).
|
/// ID родительской категории (null для корневых).
|
||||||
@@ -30,9 +31,9 @@ public record CreateCategoryCommand : IRequest<Guid>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public JsonDocument? AttributeSchema { get; init; }
|
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;
|
Slug = slug;
|
||||||
ParentId = parentId;
|
ParentId = parentId;
|
||||||
AttributeSchema = attributeSchema;
|
AttributeSchema = attributeSchema;
|
||||||
@@ -41,7 +42,7 @@ public record CreateCategoryCommand : IRequest<Guid>
|
|||||||
public CreateCategoryCommand() { } // For deserialization
|
public CreateCategoryCommand() { } // For deserialization
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateCategoryCommandHandler : IRequestHandler<CreateCategoryCommand, Guid>
|
public class CreateCategoryCommandHandler : IRequestHandler<CreateCategoryCommand, Result<Guid>>
|
||||||
{
|
{
|
||||||
private readonly ICategoryRepository _repository;
|
private readonly ICategoryRepository _repository;
|
||||||
|
|
||||||
@@ -50,19 +51,19 @@ public class CreateCategoryCommandHandler : IRequestHandler<CreateCategoryComman
|
|||||||
_repository = repository;
|
_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)
|
if (request.ParentId.HasValue)
|
||||||
{
|
{
|
||||||
var parentCategory = await _repository.GetByIdAsync(request.ParentId.Value, cancellationToken);
|
var parentCategory = await _repository.GetByIdAsync(request.ParentId.Value, cancellationToken);
|
||||||
if (parentCategory == null)
|
if (parentCategory == null)
|
||||||
{
|
{
|
||||||
throw new ApplicationException($"Родительская категория с ID {request.ParentId} не найдена.");
|
return Result<Guid>.Failure($"Родительская категория с ID {request.ParentId} не найдена.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var category = new Category(
|
var category = new Category(
|
||||||
request.Title,
|
request.Name,
|
||||||
request.Slug,
|
request.Slug,
|
||||||
request.ParentId,
|
request.ParentId,
|
||||||
request.AttributeSchema
|
request.AttributeSchema
|
||||||
@@ -70,6 +71,6 @@ public class CreateCategoryCommandHandler : IRequestHandler<CreateCategoryComman
|
|||||||
|
|
||||||
await _repository.AddAsync(category, cancellationToken);
|
await _repository.AddAsync(category, cancellationToken);
|
||||||
|
|
||||||
return category.Id;
|
return Result<Guid>.Success(category.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using MediatR;
|
||||||
|
using Nashel.BuildingBlocks.Application.Abstractions;
|
||||||
|
using Nashel.BuildingBlocks.Domain;
|
||||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||||
using Nashel.Modules.Catalog.Domain.ValueObjects;
|
using Nashel.Modules.Catalog.Domain.ValueObjects;
|
||||||
@@ -9,16 +12,8 @@ namespace Nashel.Modules.Catalog.Application.Commands;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Команда создания услуги (оффера).
|
/// Команда создания услуги (оффера).
|
||||||
/// </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; }
|
public Guid CategoryId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -41,9 +36,8 @@ public record CreateOfferCommand : IRequest<Guid>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public JsonDocument? Attributes { get; init; }
|
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;
|
CategoryId = categoryId;
|
||||||
Title = title;
|
Title = title;
|
||||||
Description = description;
|
Description = description;
|
||||||
@@ -54,19 +48,24 @@ public record CreateOfferCommand : IRequest<Guid>
|
|||||||
public CreateOfferCommand() { }
|
public CreateOfferCommand() { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateOfferCommandHandler : IRequestHandler<CreateOfferCommand, Guid>
|
public class CreateOfferCommandHandler : IRequestHandler<CreateOfferCommand, Result<Guid>>
|
||||||
{
|
{
|
||||||
private readonly IOfferRepository _repository;
|
private readonly IOfferRepository _repository;
|
||||||
|
private readonly ICurrentUserService _currentUserService;
|
||||||
|
|
||||||
public CreateOfferCommandHandler(IOfferRepository repository)
|
public CreateOfferCommandHandler(IOfferRepository repository, ICurrentUserService currentUserService)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_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(
|
var offer = new Offer(
|
||||||
request.OwnerId,
|
userId.Value,
|
||||||
request.CategoryId,
|
request.CategoryId,
|
||||||
request.Title,
|
request.Title,
|
||||||
request.Description,
|
request.Description,
|
||||||
@@ -76,6 +75,6 @@ public class CreateOfferCommandHandler : IRequestHandler<CreateOfferCommand, Gui
|
|||||||
|
|
||||||
await _repository.AddAsync(offer, cancellationToken);
|
await _repository.AddAsync(offer, cancellationToken);
|
||||||
|
|
||||||
return offer.Id;
|
return Result<Guid>.Success(offer.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public record CategoryDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Название категории.
|
/// Название категории.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Title { get; init; } = default!;
|
public string Name { get; private set; } = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// URL-совместимый идентификатор (slug).
|
/// URL-совместимый идентификатор (slug).
|
||||||
@@ -33,10 +33,10 @@ public record CategoryDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public JsonDocument? AttributeSchema { get; init; }
|
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;
|
Id = id;
|
||||||
Title = title;
|
Name = name;
|
||||||
Slug = slug;
|
Slug = slug;
|
||||||
ParentId = parentId;
|
ParentId = parentId;
|
||||||
AttributeSchema = attributeSchema;
|
AttributeSchema = attributeSchema;
|
||||||
@@ -58,7 +58,7 @@ public record OfferDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Идентификатор владельца (исполнителя).
|
/// Идентификатор владельца (исполнителя).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid OwnerId { get; init; }
|
public Guid PerformerId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Идентификатор категории.
|
/// Идентификатор категории.
|
||||||
@@ -90,10 +90,10 @@ public record OfferDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsActive { get; init; }
|
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;
|
Id = id;
|
||||||
OwnerId = ownerId;
|
PerformerId = performerId;
|
||||||
CategoryId = categoryId;
|
CategoryId = categoryId;
|
||||||
Title = title;
|
Title = title;
|
||||||
Description = description;
|
Description = description;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using Nashel.BuildingBlocks.Domain;
|
||||||
using Nashel.Modules.Catalog.Application.Common;
|
using Nashel.Modules.Catalog.Application.Common;
|
||||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||||
|
|
||||||
@@ -8,9 +9,9 @@ namespace Nashel.Modules.Catalog.Application.Queries;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Запрос дерева категорий.
|
/// Запрос дерева категорий.
|
||||||
/// </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;
|
private readonly ICategoryRepository _repository;
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ public class GetCategoriesQueryHandler : IRequestHandler<GetCategoriesQuery, Lis
|
|||||||
_repository = repository;
|
_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);
|
var rawCategories = await _repository.GetAllAsync(cancellationToken);
|
||||||
|
|
||||||
@@ -27,12 +28,14 @@ public class GetCategoriesQueryHandler : IRequestHandler<GetCategoriesQuery, Lis
|
|||||||
// Если нужно дерево: нужно иметь DTO с List<CategoryDto> Children
|
// Если нужно дерево: нужно иметь DTO с List<CategoryDto> Children
|
||||||
// Для MVP возвращаем плоский список, фронтенд сам строит дерево по ParentId
|
// Для MVP возвращаем плоский список, фронтенд сам строит дерево по ParentId
|
||||||
|
|
||||||
return rawCategories.Select(c => new CategoryDto(
|
var list = rawCategories.Select(c => new CategoryDto(
|
||||||
c.Id,
|
c.Id,
|
||||||
c.Title,
|
c.Name,
|
||||||
c.Slug,
|
c.Slug,
|
||||||
c.ParentId,
|
c.ParentId,
|
||||||
c.AttributeSchema
|
c.AttributeSchema
|
||||||
)).ToList();
|
)).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 MediatR;
|
||||||
|
using Nashel.BuildingBlocks.Domain;
|
||||||
using Nashel.Modules.Catalog.Application.Common;
|
using Nashel.Modules.Catalog.Application.Common;
|
||||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||||
|
|
||||||
@@ -7,9 +8,9 @@ namespace Nashel.Modules.Catalog.Application.Queries;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Запрос деталей услуги по ID.
|
/// Запрос деталей услуги по ID.
|
||||||
/// </summary>
|
/// </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;
|
private readonly IOfferRepository _repository;
|
||||||
|
|
||||||
@@ -18,20 +19,22 @@ public class GetOfferByIdQueryHandler : IRequestHandler<GetOfferByIdQuery, Offer
|
|||||||
_repository = repository;
|
_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);
|
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.Id,
|
||||||
offer.OwnerId,
|
offer.PerformerId,
|
||||||
offer.CategoryId,
|
offer.CategoryId,
|
||||||
offer.Title,
|
offer.Title,
|
||||||
offer.Description,
|
offer.Description ?? string.Empty,
|
||||||
offer.Price,
|
offer.Price,
|
||||||
offer.Attributes,
|
offer.Attributes,
|
||||||
offer.IsActive
|
offer.IsActive
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return Result<OfferDto>.Success(dto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class Category
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Название категории.
|
/// Название категории.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Title { get; private set; }
|
public string Name { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// URL-friendly идентификатор (слаг).
|
/// URL-friendly идентификатор (слаг).
|
||||||
@@ -39,10 +39,10 @@ public class Category
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает новую категорию.
|
/// Создает новую категорию.
|
||||||
/// </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();
|
Id = Guid.NewGuid();
|
||||||
Title = title;
|
Name = name;
|
||||||
Slug = slug;
|
Slug = slug;
|
||||||
ParentId = parentId;
|
ParentId = parentId;
|
||||||
AttributeSchema = attributeSchema;
|
AttributeSchema = attributeSchema;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class Offer
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ID владельца (пользователя/исполнителя).
|
/// ID владельца (пользователя/исполнителя).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid OwnerId { get; private set; }
|
public Guid PerformerId { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ID услуги/категории.
|
/// ID услуги/категории.
|
||||||
@@ -48,21 +48,27 @@ public class Offer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsActive { get; private set; }
|
public bool IsActive { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Дата создания.
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset CreatedAt { get; private set; }
|
||||||
|
|
||||||
// Конструктор по умолчанию для EF Core
|
// Конструктор по умолчанию для EF Core
|
||||||
private Offer() { }
|
private Offer() { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает новый оффер.
|
/// Создает новый оффер.
|
||||||
/// </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();
|
Id = Guid.NewGuid();
|
||||||
OwnerId = ownerId;
|
PerformerId = performerId;
|
||||||
CategoryId = categoryId;
|
CategoryId = categoryId;
|
||||||
Title = title;
|
Title = title;
|
||||||
Description = description;
|
Description = description;
|
||||||
Price = price;
|
Price = price;
|
||||||
Attributes = attributes;
|
Attributes = attributes;
|
||||||
IsActive = true;
|
IsActive = true;
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ namespace Nashel.Modules.Catalog.Domain.Repositories;
|
|||||||
public interface IOfferRepository
|
public interface IOfferRepository
|
||||||
{
|
{
|
||||||
Task<Offer?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
Task<Offer?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||||
|
Task<List<Offer>> GetByPerformerIdAsync(Guid performerId, CancellationToken cancellationToken = default);
|
||||||
Task AddAsync(Offer offer, 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")
|
b.Property<JsonDocument>("AttributeSchema")
|
||||||
.HasColumnType("jsonb");
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<Guid?>("ParentId")
|
b.Property<Guid?>("ParentId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
@@ -40,10 +44,6 @@ namespace Nashel.Modules.Catalog.Infrastructure.Persistence.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("ParentId");
|
b.HasIndex("ParentId");
|
||||||
@@ -66,13 +66,16 @@ namespace Nashel.Modules.Catalog.Infrastructure.Persistence.Migrations
|
|||||||
b.Property<Guid>("CategoryId")
|
b.Property<Guid>("CategoryId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<bool>("IsActive")
|
b.Property<bool>("IsActive")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<Guid>("OwnerId")
|
b.Property<Guid>("PerformerId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ public class OfferRepository : IOfferRepository
|
|||||||
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
|
.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)
|
public async Task AddAsync(Offer offer, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _context.Offers.AddAsync(offer, cancellationToken);
|
await _context.Offers.AddAsync(offer, cancellationToken);
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ public static class CatalogEndpoints
|
|||||||
|
|
||||||
catalogGroup.MapPost("/categories", async ([FromBody] CreateCategoryCommand command, ISender sender) =>
|
catalogGroup.MapPost("/categories", async ([FromBody] CreateCategoryCommand command, ISender sender) =>
|
||||||
{
|
{
|
||||||
var id = await sender.Send(command);
|
var result = await sender.Send(command);
|
||||||
return Results.Ok(id);
|
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error);
|
||||||
})
|
})
|
||||||
.WithName("CreateCategory")
|
.WithName("CreateCategory")
|
||||||
.WithOpenApi(operation => new(operation) { Summary = "Создать категорию (Admin)", Description = "Создает новую категорию. Требуются права администратора." });
|
.WithOpenApi(operation => new(operation) { Summary = "Создать категорию (Admin)", Description = "Создает новую категорию. Требуются права администратора." });
|
||||||
@@ -30,17 +30,27 @@ public static class CatalogEndpoints
|
|||||||
catalogGroup.MapGet("/categories", async (ISender sender) =>
|
catalogGroup.MapGet("/categories", async (ISender sender) =>
|
||||||
{
|
{
|
||||||
var result = await sender.Send(new GetCategoriesQuery());
|
var result = await sender.Send(new GetCategoriesQuery());
|
||||||
return Results.Ok(result);
|
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error);
|
||||||
})
|
})
|
||||||
.WithName("GetCategories")
|
.WithName("GetCategories")
|
||||||
.WithOpenApi(operation => new(operation) { Summary = "Получить все категории", Description = "Возвращает плоский список категорий с указанием ParentId для иерархии." });
|
.WithOpenApi(operation => new(operation) { Summary = "Получить все категории", Description = "Возвращает плоский список категорий с указанием ParentId для иерархии." });
|
||||||
|
|
||||||
// --- Услуги (Offers) ---
|
// --- Услуги (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);
|
var result = await sender.Send(new GetMyOffersQuery());
|
||||||
return Results.Ok(id);
|
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")
|
.WithName("CreateOffer")
|
||||||
.WithOpenApi(operation => new(operation) { Summary = "Создать оффер/услугу", Description = "Создает новое предложение услуги в указанной категории." });
|
.WithOpenApi(operation => new(operation) { Summary = "Создать оффер/услугу", Description = "Создает новое предложение услуги в указанной категории." });
|
||||||
@@ -48,7 +58,7 @@ public static class CatalogEndpoints
|
|||||||
catalogGroup.MapGet("/offers/{id:guid}", async (Guid id, ISender sender) =>
|
catalogGroup.MapGet("/offers/{id:guid}", async (Guid id, ISender sender) =>
|
||||||
{
|
{
|
||||||
var result = await sender.Send(new GetOfferByIdQuery(id));
|
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")
|
.WithName("GetOfferById")
|
||||||
.WithOpenApi(operation => new(operation) { Summary = "Получить детали услуги по ID", Description = "Возвращает полную информацию об услуге." });
|
.WithOpenApi(operation => new(operation) { Summary = "Получить детали услуги по ID", Description = "Возвращает полную информацию об услуге." });
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ public record RegisterUserCommand : IRequest<Guid>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Имя.
|
/// Имя.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string FirstName { get; init; } = default!;
|
public string? FirstName { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Фамилия.
|
/// Фамилия.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string LastName { get; init; } = default!;
|
public string? LastName { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Отчество (необязательно).
|
/// Отчество (необязательно).
|
||||||
@@ -97,8 +97,8 @@ public class RegisterUserCommandHandler : IRequestHandler<RegisterUserCommand, G
|
|||||||
existingAccount.Roles.Clear();
|
existingAccount.Roles.Clear();
|
||||||
existingAccount.Roles.Add(Role.User);
|
existingAccount.Roles.Add(Role.User);
|
||||||
|
|
||||||
// Добавляем роль только для Company
|
// Добавляем запрошенную роль (кроме User)
|
||||||
if (request.Role == Role.Company)
|
if (request.Role != Role.User)
|
||||||
{
|
{
|
||||||
existingAccount.Roles.Add(request.Role);
|
existingAccount.Roles.Add(request.Role);
|
||||||
}
|
}
|
||||||
@@ -116,8 +116,8 @@ public class RegisterUserCommandHandler : IRequestHandler<RegisterUserCommand, G
|
|||||||
var passwordHashNew = _passwordHasher.HashPassword(request.Password);
|
var passwordHashNew = _passwordHasher.HashPassword(request.Password);
|
||||||
var account = Account.Create(request.Phone, passwordHashNew);
|
var account = Account.Create(request.Phone, passwordHashNew);
|
||||||
|
|
||||||
// Добавляем роль только для Company. Newbie не добавляем - она добавляется через BecomePerformerCommand
|
// Добавляем запрошенную роль (кроме User, так как он добавляется по умолчанию)
|
||||||
if (request.Role == Role.Company)
|
if (request.Role != Role.User)
|
||||||
{
|
{
|
||||||
account.Roles.Add(request.Role);
|
account.Roles.Add(request.Role);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,15 +22,15 @@ public record ProfileResponse(
|
|||||||
Guid Id,
|
Guid Id,
|
||||||
string Phone,
|
string Phone,
|
||||||
List<string> Roles,
|
List<string> Roles,
|
||||||
string FirstName,
|
string? FirstName,
|
||||||
string LastName,
|
string? LastName,
|
||||||
string? Patronymic,
|
string? Patronymic,
|
||||||
string? CompanyName,
|
string? CompanyName,
|
||||||
string? Inn,
|
string? Inn,
|
||||||
string? Description,
|
string? Description,
|
||||||
string? Location,
|
string? Location,
|
||||||
string? CurrentLocation,
|
string? CurrentLocation,
|
||||||
string FullName,
|
string? FullName,
|
||||||
string? AvatarUrl,
|
string? AvatarUrl,
|
||||||
List<string> Competencies,
|
List<string> Competencies,
|
||||||
WorkScheduleResponse? WorkSchedule);
|
WorkScheduleResponse? WorkSchedule);
|
||||||
@@ -60,10 +60,13 @@ public class GetProfileQueryHandler : IRequestHandler<GetProfileQuery, ProfileRe
|
|||||||
throw new Exception("Account not found");
|
throw new Exception("Account not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
var fullName = $"{account.Profile.FirstName} {account.Profile.LastName}";
|
var names = new[] { account.Profile.FirstName, account.Profile.LastName, account.Profile.Patronymic }
|
||||||
if (!string.IsNullOrEmpty(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
|
// Парсинг WorkSchedule
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ public class RegisterUserCommandValidator : AbstractValidator<RegisterUserComman
|
|||||||
.MinimumLength(6).WithMessage("Пароль должен быть не менее 6 символов");
|
.MinimumLength(6).WithMessage("Пароль должен быть не менее 6 символов");
|
||||||
|
|
||||||
RuleFor(x => x.FirstName)
|
RuleFor(x => x.FirstName)
|
||||||
.NotEmpty().WithMessage("Имя обязательно")
|
.NotEmpty().WithMessage("Имя обязательно").When(x => x.Role != Role.Company)
|
||||||
.MinimumLength(2).WithMessage("Имя должно содержать минимум 2 символа");
|
.MinimumLength(2).WithMessage("Имя должно содержать минимум 2 символа").When(x => x.Role != Role.Company && !string.IsNullOrEmpty(x.FirstName));
|
||||||
|
|
||||||
RuleFor(x => x.LastName)
|
RuleFor(x => x.LastName)
|
||||||
.NotEmpty().WithMessage("Фамилия обязательна")
|
.NotEmpty().WithMessage("Фамилия обязательна").When(x => x.Role != Role.Company)
|
||||||
.MinimumLength(2).WithMessage("Фамилия должна содержать минимум 2 символа");
|
.MinimumLength(2).WithMessage("Фамилия должна содержать минимум 2 символа").When(x => x.Role != Role.Company && !string.IsNullOrEmpty(x.LastName));
|
||||||
|
|
||||||
RuleFor(x => x.CompanyName)
|
RuleFor(x => x.CompanyName)
|
||||||
.NotEmpty().WithMessage("Название компании обязательно для юридических лиц")
|
.NotEmpty().WithMessage("Название компании обязательно для юридических лиц")
|
||||||
|
|||||||
@@ -81,17 +81,17 @@ public class Account : AggregateRoot<Guid>
|
|||||||
|
|
||||||
public void BecomePerformer()
|
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()
|
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))
|
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)
|
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);
|
Profile.UpdatePerformerData(description, competencies, location, currentLocation, workSchedule);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +116,6 @@ public class Account : AggregateRoot<Guid>
|
|||||||
|
|
||||||
public bool IsPerformer()
|
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 const int MaxDescriptionLength = 2048;
|
||||||
private readonly List<Competency> _competencies = new();
|
private readonly List<Competency> _competencies = new();
|
||||||
|
|
||||||
public string FirstName { get; private set; }
|
public string? FirstName { get; private set; }
|
||||||
public string LastName { get; private set; }
|
public string? LastName { get; private set; }
|
||||||
public string? Patronymic { get; private set; }
|
public string? Patronymic { get; private set; }
|
||||||
public string? CompanyName { get; private set; }
|
public string? CompanyName { get; private set; }
|
||||||
public string? Inn { get; private set; }
|
public string? Inn { get; private set; }
|
||||||
@@ -24,7 +24,7 @@ public class UserProfile : Entity<Guid>
|
|||||||
// EF Core constructor
|
// EF Core constructor
|
||||||
private UserProfile() { }
|
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;
|
Id = id;
|
||||||
FirstName = firstName;
|
FirstName = firstName;
|
||||||
@@ -35,7 +35,7 @@ public class UserProfile : Entity<Guid>
|
|||||||
Description = description;
|
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)
|
if (!string.IsNullOrEmpty(description) && description.Length > MaxDescriptionLength)
|
||||||
throw new ArgumentException($"Описание не может превышать {MaxDescriptionLength} символов", nameof(description));
|
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);
|
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)
|
if (!string.IsNullOrEmpty(description) && description.Length > MaxDescriptionLength)
|
||||||
throw new ArgumentException($"Описание не может превышать {MaxDescriptionLength} символов", nameof(description));
|
throw new ArgumentException($"Описание не может превышать {MaxDescriptionLength} символов", nameof(description));
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ namespace Nashel.Modules.Identity.Domain.Enums;
|
|||||||
public enum Role
|
public enum Role
|
||||||
{
|
{
|
||||||
User = 0,
|
User = 0,
|
||||||
Newbie,
|
|
||||||
Candidate,
|
Candidate,
|
||||||
Master,
|
Master,
|
||||||
Company,
|
Company,
|
||||||
|
|||||||
-2
@@ -15,11 +15,9 @@ public class UserProfileConfiguration : IEntityTypeConfiguration<UserProfile>
|
|||||||
builder.HasKey(x => x.Id);
|
builder.HasKey(x => x.Id);
|
||||||
|
|
||||||
builder.Property(x => x.FirstName)
|
builder.Property(x => x.FirstName)
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100);
|
.HasMaxLength(100);
|
||||||
|
|
||||||
builder.Property(x => x.LastName)
|
builder.Property(x => x.LastName)
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100);
|
.HasMaxLength(100);
|
||||||
|
|
||||||
builder.Property(x => x.Patronymic)
|
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()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
b.Property<string>("PasswordHash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
@@ -70,7 +78,6 @@ namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
|
|||||||
.HasColumnType("character varying(2048)");
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
b.Property<string>("FirstName")
|
b.Property<string>("FirstName")
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
.HasColumnType("character varying(100)");
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
@@ -79,7 +86,6 @@ namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
|
|||||||
.HasColumnType("character varying(10)");
|
.HasColumnType("character varying(10)");
|
||||||
|
|
||||||
b.Property<string>("LastName")
|
b.Property<string>("LastName")
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
.HasColumnType("character varying(100)");
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ public class BecomePerformerCommandHandlerTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var account = Account.Create("123", "pass"); // Has User role only
|
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;
|
var userId = account.Id;
|
||||||
|
|
||||||
_mockUserService.Setup(s => s.UserId).Returns(userId);
|
_mockUserService.Setup(s => s.UserId).Returns(userId);
|
||||||
@@ -40,7 +42,8 @@ public class BecomePerformerCommandHandlerTests
|
|||||||
.ReturnsAsync(account);
|
.ReturnsAsync(account);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await _handler.Handle(new BecomePerformerCommand(), CancellationToken.None);
|
var command = new BecomePerformerCommand { Description = new string('x', 51) };
|
||||||
|
await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
account.Roles.Should().Contain(Role.Candidate);
|
account.Roles.Should().Contain(Role.Candidate);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public class RegisterUserCommandHandlerTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var phone = "123";
|
var phone = "123";
|
||||||
_mockRepo.Setup(r => r.GetByPhoneAsync(phone, It.IsAny<CancellationToken>()))
|
_mockRepo.Setup(r => r.GetByPhoneIncludingDeletedAsync(phone, It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync((Account?)null);
|
.ReturnsAsync((Account?)null);
|
||||||
|
|
||||||
_mockPasswordHasher.Setup(p => p.HashPassword("pass"))
|
_mockPasswordHasher.Setup(p => p.HashPassword("pass"))
|
||||||
@@ -51,10 +51,10 @@ public class RegisterUserCommandHandlerTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var phone = "123";
|
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"));
|
.ReturnsAsync(Account.Create(phone, "pass"));
|
||||||
|
|
||||||
// Act
|
// Assert
|
||||||
var act = async () => await _handler.Handle(new RegisterUserCommand
|
var act = async () => await _handler.Handle(new RegisterUserCommand
|
||||||
{
|
{
|
||||||
Phone = phone,
|
Phone = phone,
|
||||||
@@ -63,8 +63,8 @@ public class RegisterUserCommandHandlerTests
|
|||||||
LastName = "Ivanov"
|
LastName = "Ivanov"
|
||||||
}, CancellationToken.None);
|
}, CancellationToken.None);
|
||||||
|
|
||||||
// Assert
|
var ex = await Assert.ThrowsAsync<Exception>(act);
|
||||||
await act.Should().ThrowAsync<Exception>().WithMessage("Пользователь уже существует");
|
ex.Message.Should().Be("Пользователь с таким номером телефона уже существует");
|
||||||
_mockRepo.Verify(r => r.AddAsync(It.IsAny<Account>(), It.IsAny<CancellationToken>()), Times.Never);
|
_mockRepo.Verify(r => r.AddAsync(It.IsAny<Account>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user