Реализован модуль каталога, миграции, сваггер
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -136,3 +136,7 @@ src/Modules/Collaboration/Domain/bin/
|
||||
src/Modules/Collaboration/Presentation/bin/
|
||||
|
||||
src/Modules/Identity/Tests/obj/
|
||||
|
||||
src/Modules/Catalog/Tests/bin/
|
||||
|
||||
src/Modules/Catalog/Tests/obj/
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.76.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -2,6 +2,8 @@ using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Nashel.Modules.Catalog.Infrastructure;
|
||||
using Nashel.Modules.Catalog.Presentation.Endpoints;
|
||||
using Nashel.Modules.Identity.Infrastructure;
|
||||
using Nashel.Modules.Identity.Presentation.Endpoints;
|
||||
|
||||
@@ -41,7 +43,6 @@ builder.Services.AddSwaggerGen(options =>
|
||||
var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml");
|
||||
foreach (var xmlFile in xmlFiles)
|
||||
{
|
||||
// Исключаем системные сборки, берем только наши, если нужно, или просто все
|
||||
options.IncludeXmlComments(xmlFile);
|
||||
}
|
||||
});
|
||||
@@ -49,6 +50,9 @@ builder.Services.AddSwaggerGen(options =>
|
||||
// Регистрация модуля Identity
|
||||
builder.Services.AddIdentityModule(builder.Configuration);
|
||||
|
||||
// Регистрация модуля Catalog
|
||||
builder.Services.AddCatalogModule(builder.Configuration);
|
||||
|
||||
// Настройка аутентификации
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
@@ -83,7 +87,8 @@ if (app.Environment.IsDevelopment())
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// Маппинг эндпоинтов модуля Identity
|
||||
// Маппинг эндпоинтов
|
||||
app.MapIdentityEndpoints();
|
||||
app.MapCatalogEndpoints();
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using System.Text.Json;
|
||||
using MediatR;
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Команда создания категории.
|
||||
/// </summary>
|
||||
public record CreateCategoryCommand : IRequest<Guid>
|
||||
{
|
||||
/// <summary>
|
||||
/// Название категории.
|
||||
/// </summary>
|
||||
public string Title { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// URL-friendly идентификатор (слаг).
|
||||
/// </summary>
|
||||
public string Slug { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// ID родительской категории (null для корневых).
|
||||
/// </summary>
|
||||
public Guid? ParentId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Шаблон характеристик (JSON).
|
||||
/// </summary>
|
||||
public JsonDocument? AttributeSchema { get; init; }
|
||||
|
||||
public CreateCategoryCommand(string title, string slug, Guid? parentId, JsonDocument? attributeSchema)
|
||||
{
|
||||
Title = title;
|
||||
Slug = slug;
|
||||
ParentId = parentId;
|
||||
AttributeSchema = attributeSchema;
|
||||
}
|
||||
|
||||
public CreateCategoryCommand() { } // For deserialization
|
||||
}
|
||||
|
||||
public class CreateCategoryCommandHandler : IRequestHandler<CreateCategoryCommand, Guid>
|
||||
{
|
||||
private readonly ICategoryRepository _repository;
|
||||
|
||||
public CreateCategoryCommandHandler(ICategoryRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<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} не найдена.");
|
||||
}
|
||||
}
|
||||
|
||||
var category = new Category(
|
||||
request.Title,
|
||||
request.Slug,
|
||||
request.ParentId,
|
||||
request.AttributeSchema
|
||||
);
|
||||
|
||||
await _repository.AddAsync(category, cancellationToken);
|
||||
|
||||
return category.Id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.Text.Json;
|
||||
using MediatR;
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
using Nashel.Modules.Catalog.Domain.ValueObjects;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Команда создания услуги (оффера).
|
||||
/// </summary>
|
||||
public record CreateOfferCommand : IRequest<Guid>
|
||||
{
|
||||
/// <summary>
|
||||
/// ID владельца (пользователя/исполнителя).
|
||||
/// </summary>
|
||||
public Guid OwnerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// ID категории услуги.
|
||||
/// </summary>
|
||||
public Guid CategoryId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Заголовок объявления.
|
||||
/// </summary>
|
||||
public string Title { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Полное описание услуги.
|
||||
/// </summary>
|
||||
public string Description { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Цена услуги.
|
||||
/// </summary>
|
||||
public Price Price { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Характеристики услуги (JSON).
|
||||
/// </summary>
|
||||
public JsonDocument? Attributes { get; init; }
|
||||
|
||||
public CreateOfferCommand(Guid ownerId, Guid categoryId, string title, string description, Price price, JsonDocument? attributes)
|
||||
{
|
||||
OwnerId = ownerId;
|
||||
CategoryId = categoryId;
|
||||
Title = title;
|
||||
Description = description;
|
||||
Price = price;
|
||||
Attributes = attributes;
|
||||
}
|
||||
|
||||
public CreateOfferCommand() { }
|
||||
}
|
||||
|
||||
public class CreateOfferCommandHandler : IRequestHandler<CreateOfferCommand, Guid>
|
||||
{
|
||||
private readonly IOfferRepository _repository;
|
||||
|
||||
public CreateOfferCommandHandler(IOfferRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<Guid> Handle(CreateOfferCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var offer = new Offer(
|
||||
request.OwnerId,
|
||||
request.CategoryId,
|
||||
request.Title,
|
||||
request.Description,
|
||||
request.Price,
|
||||
request.Attributes
|
||||
);
|
||||
|
||||
await _repository.AddAsync(offer, cancellationToken);
|
||||
|
||||
return offer.Id;
|
||||
}
|
||||
}
|
||||
29
src/Modules/Catalog/Application/Common/Dtos.cs
Normal file
29
src/Modules/Catalog/Application/Common/Dtos.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Text.Json;
|
||||
using Nashel.Modules.Catalog.Domain.ValueObjects;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Application.Common;
|
||||
|
||||
/// <summary>
|
||||
/// DTO категории.
|
||||
/// </summary>
|
||||
public record CategoryDto(
|
||||
Guid Id,
|
||||
string Title,
|
||||
string Slug,
|
||||
Guid? ParentId,
|
||||
JsonDocument? AttributeSchema
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// DTO услуги/оффера.
|
||||
/// </summary>
|
||||
public record OfferDto(
|
||||
Guid Id,
|
||||
Guid OwnerId,
|
||||
Guid CategoryId,
|
||||
string Title,
|
||||
string Description,
|
||||
Price Price,
|
||||
JsonDocument? Attributes,
|
||||
bool IsActive
|
||||
);
|
||||
@@ -11,5 +11,7 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Text.Json;
|
||||
using MediatR;
|
||||
using Nashel.Modules.Catalog.Application.Common;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Запрос дерева категорий.
|
||||
/// </summary>
|
||||
public record GetCategoriesQuery() : IRequest<List<CategoryDto>>;
|
||||
|
||||
public class GetCategoriesQueryHandler : IRequestHandler<GetCategoriesQuery, List<CategoryDto>>
|
||||
{
|
||||
private readonly ICategoryRepository _repository;
|
||||
|
||||
public GetCategoriesQueryHandler(ICategoryRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<List<CategoryDto>> Handle(GetCategoriesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var rawCategories = await _repository.GetAllAsync(cancellationToken);
|
||||
|
||||
// Преобразование в DTO (для дерева логика нужна сложнее, но пока плоский список для старта)
|
||||
// Если нужно дерево: нужно иметь DTO с List<CategoryDto> Children
|
||||
// Для MVP возвращаем плоский список, фронтенд сам строит дерево по ParentId
|
||||
|
||||
return rawCategories.Select(c => new CategoryDto(
|
||||
c.Id,
|
||||
c.Title,
|
||||
c.Slug,
|
||||
c.ParentId,
|
||||
c.AttributeSchema
|
||||
)).ToList();
|
||||
}
|
||||
}
|
||||
37
src/Modules/Catalog/Application/Queries/GetOfferByIdQuery.cs
Normal file
37
src/Modules/Catalog/Application/Queries/GetOfferByIdQuery.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using MediatR;
|
||||
using Nashel.Modules.Catalog.Application.Common;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Запрос деталей услуги по ID.
|
||||
/// </summary>
|
||||
public record GetOfferByIdQuery(Guid Id) : IRequest<OfferDto?>;
|
||||
|
||||
public class GetOfferByIdQueryHandler : IRequestHandler<GetOfferByIdQuery, OfferDto?>
|
||||
{
|
||||
private readonly IOfferRepository _repository;
|
||||
|
||||
public GetOfferByIdQueryHandler(IOfferRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<OfferDto?> Handle(GetOfferByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var offer = await _repository.GetByIdAsync(request.Id, cancellationToken);
|
||||
if (offer == null) return null;
|
||||
|
||||
return new OfferDto(
|
||||
offer.Id,
|
||||
offer.OwnerId,
|
||||
offer.CategoryId,
|
||||
offer.Title,
|
||||
offer.Description,
|
||||
offer.Price,
|
||||
offer.Attributes,
|
||||
offer.IsActive
|
||||
);
|
||||
}
|
||||
}
|
||||
50
src/Modules/Catalog/Domain/Aggregates/Category.cs
Normal file
50
src/Modules/Catalog/Domain/Aggregates/Category.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
|
||||
/// <summary>
|
||||
/// Категория услуг.
|
||||
/// </summary>
|
||||
public class Category
|
||||
{
|
||||
/// <summary>
|
||||
/// Уникальный идентификатор.
|
||||
/// </summary>
|
||||
public Guid Id { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Название категории.
|
||||
/// </summary>
|
||||
public string Title { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL-friendly идентификатор (слаг).
|
||||
/// </summary>
|
||||
public string Slug { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID родительской категории (null для корневых).
|
||||
/// </summary>
|
||||
public Guid? ParentId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Шаблон характеристик (JSON).
|
||||
/// </summary>
|
||||
public JsonDocument? AttributeSchema { get; private set; }
|
||||
|
||||
// Конструктор по умолчанию для EF Core
|
||||
private Category() { }
|
||||
|
||||
/// <summary>
|
||||
/// Создает новую категорию.
|
||||
/// </summary>
|
||||
public Category(string title, string slug, Guid? parentId, JsonDocument? attributeSchema = null)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
Title = title;
|
||||
Slug = slug;
|
||||
ParentId = parentId;
|
||||
AttributeSchema = attributeSchema;
|
||||
}
|
||||
}
|
||||
68
src/Modules/Catalog/Domain/Aggregates/Offer.cs
Normal file
68
src/Modules/Catalog/Domain/Aggregates/Offer.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.Text.Json;
|
||||
using Nashel.Modules.Catalog.Domain.ValueObjects;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
|
||||
/// <summary>
|
||||
/// Услуга или оффер исполнителя.
|
||||
/// </summary>
|
||||
public class Offer
|
||||
{
|
||||
/// <summary>
|
||||
/// Уникальный идентификатор оффера.
|
||||
/// </summary>
|
||||
public Guid Id { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID владельца (пользователя/исполнителя).
|
||||
/// </summary>
|
||||
public Guid OwnerId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID услуги/категории.
|
||||
/// </summary>
|
||||
public Guid CategoryId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Заголовок объявления.
|
||||
/// </summary>
|
||||
public string Title { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Полное описание.
|
||||
/// </summary>
|
||||
public string? Description { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Цена (сумма, валюта, тип оплаты).
|
||||
/// </summary>
|
||||
public Price Price { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Характеристики (JSONB).
|
||||
/// </summary>
|
||||
public JsonDocument? Attributes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Активно ли объявление.
|
||||
/// </summary>
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
// Конструктор по умолчанию для EF Core
|
||||
private Offer() { }
|
||||
|
||||
/// <summary>
|
||||
/// Создает новый оффер.
|
||||
/// </summary>
|
||||
public Offer(Guid ownerId, Guid categoryId, string title, string description, Price price, JsonDocument? attributes = null)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
OwnerId = ownerId;
|
||||
CategoryId = categoryId;
|
||||
Title = title;
|
||||
Description = description;
|
||||
Price = price;
|
||||
Attributes = attributes;
|
||||
IsActive = true;
|
||||
}
|
||||
}
|
||||
22
src/Modules/Catalog/Domain/Enums/OfferType.cs
Normal file
22
src/Modules/Catalog/Domain/Enums/OfferType.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace Nashel.Modules.Catalog.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Тип оплаты услуги.
|
||||
/// </summary>
|
||||
public enum OfferType
|
||||
{
|
||||
/// <summary>
|
||||
/// Фиксированная цена.
|
||||
/// </summary>
|
||||
Fixed,
|
||||
|
||||
/// <summary>
|
||||
/// Почасовая оплата.
|
||||
/// </summary>
|
||||
Hourly,
|
||||
|
||||
/// <summary>
|
||||
/// Договорная цена.
|
||||
/// </summary>
|
||||
Negotiable
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Domain.Repositories;
|
||||
|
||||
public interface ICategoryRepository
|
||||
{
|
||||
Task<Category?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<List<Category>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
Task AddAsync(Category category, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Domain.Repositories;
|
||||
|
||||
public interface IOfferRepository
|
||||
{
|
||||
Task<Offer?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task AddAsync(Offer offer, CancellationToken cancellationToken = default);
|
||||
}
|
||||
35
src/Modules/Catalog/Domain/ValueObjects/Price.cs
Normal file
35
src/Modules/Catalog/Domain/ValueObjects/Price.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Nashel.Modules.Catalog.Domain.Enums;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Значение цены услуги.
|
||||
/// </summary>
|
||||
public record Price
|
||||
{
|
||||
/// <summary>
|
||||
/// Сумма.
|
||||
/// </summary>
|
||||
public decimal Amount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Валюта (по умолчанию RUB).
|
||||
/// </summary>
|
||||
public string Currency { get; init; } = "RUB";
|
||||
|
||||
/// <summary>
|
||||
/// Тип оплаты (фиксированная, почасовая, договорная).
|
||||
/// </summary>
|
||||
public OfferType Type { get; init; }
|
||||
|
||||
// Конструктор по умолчанию для EF Core и сериализации
|
||||
public Price() { }
|
||||
|
||||
public Price(decimal amount, OfferType type, string currency = "RUB")
|
||||
{
|
||||
Amount = amount;
|
||||
Type = type;
|
||||
Currency = currency;
|
||||
}
|
||||
}
|
||||
34
src/Modules/Catalog/Infrastructure/DependencyInjection.cs
Normal file
34
src/Modules/Catalog/Infrastructure/DependencyInjection.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Nashel.Modules.Catalog.Application.Commands;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
using Nashel.Modules.Catalog.Infrastructure.Persistence;
|
||||
using Nashel.Modules.Catalog.Infrastructure.Repositories;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Infrastructure;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddCatalogModule(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// 1. Регистрация DbContext
|
||||
services.AddDbContext<CatalogDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(configuration.GetConnectionString("DefaultConnection"));
|
||||
});
|
||||
|
||||
// 2. Репозитории
|
||||
services.AddScoped<ICategoryRepository, CategoryRepository>();
|
||||
services.AddScoped<IOfferRepository, OfferRepository>();
|
||||
|
||||
// 3. MediatR (сканируем сборку Application)
|
||||
services.AddMediatR(cfg =>
|
||||
{
|
||||
cfg.RegisterServicesFromAssembly(typeof(CreateCategoryCommand).Assembly);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Infrastructure.Persistence;
|
||||
|
||||
public class CatalogDbContext : DbContext
|
||||
{
|
||||
public DbSet<Category> Categories { get; set; } = null!;
|
||||
public DbSet<Offer> Offers { get; set; } = null!;
|
||||
|
||||
public CatalogDbContext(DbContextOptions<CatalogDbContext> options) : base(options) { }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasPostgresExtension("postgis"); // Если нужно для чего-то еще, но вообще JSONB встроен.
|
||||
// Но лучше просто не трогать расширения если не уверены.
|
||||
// Для JSONB ничего особенного не нужно, кроме HasColumnType("jsonb").
|
||||
|
||||
modelBuilder.Entity<Category>(entity =>
|
||||
{
|
||||
entity.ToTable("Categories", "catalog");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.HasIndex(e => e.Slug).IsUnique();
|
||||
|
||||
// Self-referencing
|
||||
entity.HasOne<Category>()
|
||||
.WithMany() // Навигационное свойство Children не добавлено в доменную модель явно, но связь есть
|
||||
.HasForeignKey(e => e.ParentId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
entity.Property(e => e.AttributeSchema)
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Offer>(entity =>
|
||||
{
|
||||
entity.ToTable("Offers", "catalog");
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.Property(e => e.Attributes)
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
// GIN Index
|
||||
entity.HasIndex(e => e.Attributes)
|
||||
.HasMethod("gin");
|
||||
|
||||
entity.OwnsOne(e => e.Price, price =>
|
||||
{
|
||||
price.Property(p => p.Amount).HasColumnName("PriceAmount");
|
||||
price.Property(p => p.Currency).HasColumnName("PriceCurrency").HasMaxLength(3);
|
||||
price.Property(p => p.Type).HasColumnName("PriceType"); // Enum as int by default
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
137
src/Modules/Catalog/Infrastructure/Persistence/Migrations/20260210114511_InitialCreate.Designer.cs
generated
Normal file
137
src/Modules/Catalog/Infrastructure/Persistence/Migrations/20260210114511_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,137 @@
|
||||
// <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("20260210114511_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <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<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.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<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid>("OwnerId")
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Nashel.Modules.Catalog.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "catalog");
|
||||
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("Npgsql:PostgresExtension:postgis", ",,");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Categories",
|
||||
schema: "catalog",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Title = table.Column<string>(type: "text", nullable: false),
|
||||
Slug = table.Column<string>(type: "text", nullable: false),
|
||||
ParentId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
AttributeSchema = table.Column<JsonDocument>(type: "jsonb", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Categories", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Categories_Categories_ParentId",
|
||||
column: x => x.ParentId,
|
||||
principalSchema: "catalog",
|
||||
principalTable: "Categories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Offers",
|
||||
schema: "catalog",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
OwnerId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CategoryId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Title = table.Column<string>(type: "text", nullable: false),
|
||||
Description = table.Column<string>(type: "text", nullable: true),
|
||||
PriceAmount = table.Column<decimal>(type: "numeric", nullable: false),
|
||||
PriceCurrency = table.Column<string>(type: "character varying(3)", maxLength: 3, nullable: false),
|
||||
PriceType = table.Column<int>(type: "integer", nullable: false),
|
||||
Attributes = table.Column<JsonDocument>(type: "jsonb", nullable: true),
|
||||
IsActive = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Offers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Categories_ParentId",
|
||||
schema: "catalog",
|
||||
table: "Categories",
|
||||
column: "ParentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Categories_Slug",
|
||||
schema: "catalog",
|
||||
table: "Categories",
|
||||
column: "Slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Offers_Attributes",
|
||||
schema: "catalog",
|
||||
table: "Offers",
|
||||
column: "Attributes")
|
||||
.Annotation("Npgsql:IndexMethod", "gin");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Categories",
|
||||
schema: "catalog");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Offers",
|
||||
schema: "catalog");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
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))]
|
||||
partial class CatalogDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Category", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<JsonDocument>("AttributeSchema")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.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<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid>("OwnerId")
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
using Nashel.Modules.Catalog.Infrastructure.Persistence;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Infrastructure.Repositories;
|
||||
|
||||
public class CategoryRepository : ICategoryRepository
|
||||
{
|
||||
private readonly CatalogDbContext _context;
|
||||
|
||||
public CategoryRepository(CatalogDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Category?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Categories
|
||||
.Include(c => c.AttributeSchema) // Not needed as it's a property now, but sometimes needed if navigation property (JsonDocument is property)
|
||||
.FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Category>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Categories.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task AddAsync(Category category, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.Categories.AddAsync(category, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
using Nashel.Modules.Catalog.Infrastructure.Persistence;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Infrastructure.Repositories;
|
||||
|
||||
public class OfferRepository : IOfferRepository
|
||||
{
|
||||
private readonly CatalogDbContext _context;
|
||||
|
||||
public OfferRepository(CatalogDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Offer?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Offers
|
||||
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task AddAsync(Offer offer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.Offers.AddAsync(offer, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Nashel.Modules.Catalog.Application.Commands;
|
||||
using Nashel.Modules.Catalog.Application.Queries;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Presentation.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// API Каталога (Категории и Услуги).
|
||||
/// </summary>
|
||||
public static class CatalogEndpoints
|
||||
{
|
||||
public static void MapCatalogEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var catalogGroup = app.MapGroup("/api/catalog").WithTags("Catalog");
|
||||
|
||||
// --- Категории ---
|
||||
|
||||
catalogGroup.MapPost("/categories", async ([FromBody] CreateCategoryCommand command, ISender sender) =>
|
||||
{
|
||||
var id = await sender.Send(command);
|
||||
return Results.Ok(id);
|
||||
})
|
||||
.WithName("CreateCategory")
|
||||
.WithSummary("Создать категорию (Admin)");
|
||||
|
||||
catalogGroup.MapGet("/categories", async (ISender sender) =>
|
||||
{
|
||||
var result = await sender.Send(new GetCategoriesQuery());
|
||||
return Results.Ok(result);
|
||||
})
|
||||
.WithName("GetCategories")
|
||||
.WithSummary("Получить все категории (плоский список с ParentId)");
|
||||
|
||||
// --- Услуги (Offers) ---
|
||||
|
||||
catalogGroup.MapPost("/offers", async ([FromBody] CreateOfferCommand command, ISender sender) =>
|
||||
{
|
||||
var id = await sender.Send(command);
|
||||
return Results.Ok(id);
|
||||
})
|
||||
.WithName("CreateOffer")
|
||||
.WithSummary("Создать оффер/услугу");
|
||||
|
||||
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();
|
||||
})
|
||||
.WithName("GetOfferById")
|
||||
.WithSummary("Получить детали услуги по ID");
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,10 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,44 @@
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using Nashel.Modules.Catalog.Application.Commands;
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Tests.Application;
|
||||
|
||||
public class CreateCategoryCommandHandlerTests
|
||||
{
|
||||
private readonly Mock<ICategoryRepository> _categoryRepositoryMock;
|
||||
private readonly CreateCategoryCommandHandler _handler;
|
||||
|
||||
public CreateCategoryCommandHandlerTests()
|
||||
{
|
||||
_categoryRepositoryMock = new Mock<ICategoryRepository>();
|
||||
_handler = new CreateCategoryCommandHandler(_categoryRepositoryMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldCreateCategory_WhenCommandIsValid()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateCategoryCommand(
|
||||
"Test Category",
|
||||
"test-category",
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(result); // Guid should be returned
|
||||
_categoryRepositoryMock.Verify(r => r.AddAsync(It.Is<Category>(c =>
|
||||
c.Title == command.Title &&
|
||||
c.Slug == command.Slug &&
|
||||
c.ParentId == command.ParentId &&
|
||||
c.AttributeSchema == command.AttributeSchema
|
||||
), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using Nashel.Modules.Catalog.Application.Commands;
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
using Nashel.Modules.Catalog.Domain.Enums;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
using Nashel.Modules.Catalog.Domain.ValueObjects;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Tests.Application;
|
||||
|
||||
public class CreateOfferCommandHandlerTests
|
||||
{
|
||||
private readonly Mock<IOfferRepository> _offerRepositoryMock;
|
||||
private readonly CreateOfferCommandHandler _handler;
|
||||
|
||||
public CreateOfferCommandHandlerTests()
|
||||
{
|
||||
_offerRepositoryMock = new Mock<IOfferRepository>();
|
||||
_handler = new CreateOfferCommandHandler(_offerRepositoryMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldCreateOffer_WhenCommandIsValid()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateOfferCommand(
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
"Test Offer",
|
||||
"Test Description",
|
||||
new Price(100, OfferType.Fixed, "RUB"),
|
||||
JsonDocument.Parse("{}")
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(result); // Guid should be returned
|
||||
_offerRepositoryMock.Verify(r => r.AddAsync(It.Is<Offer>(o =>
|
||||
o.Title == command.Title &&
|
||||
o.OwnerId == command.OwnerId &&
|
||||
o.CategoryId == command.CategoryId &&
|
||||
o.Description == command.Description &&
|
||||
o.Price.Amount == command.Price.Amount &&
|
||||
o.Price.Type == command.Price.Type &&
|
||||
o.Price.Currency == command.Price.Currency &&
|
||||
o.Attributes == command.Attributes
|
||||
), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using Nashel.Modules.Catalog.Application.Common;
|
||||
using Nashel.Modules.Catalog.Application.Queries;
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Tests.Application;
|
||||
|
||||
public class GetCategoriesQueryHandlerTests
|
||||
{
|
||||
private readonly Mock<ICategoryRepository> _categoryRepositoryMock;
|
||||
private readonly GetCategoriesQueryHandler _handler;
|
||||
|
||||
public GetCategoriesQueryHandlerTests()
|
||||
{
|
||||
_categoryRepositoryMock = new Mock<ICategoryRepository>();
|
||||
_handler = new GetCategoriesQueryHandler(_categoryRepositoryMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldReturnCategories_WhenCategoriesExist()
|
||||
{
|
||||
// Arrange
|
||||
var categories = new List<Category>
|
||||
{
|
||||
new Category("Category 1", "category-1", null),
|
||||
new Category("Category 2", "category-2", null)
|
||||
};
|
||||
_categoryRepositoryMock.Setup(r => r.GetAllAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(categories);
|
||||
|
||||
var query = new GetCategoriesQuery();
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Should().HaveCount(2);
|
||||
|
||||
var expectedDto1 = new CategoryDto(categories[0].Id, categories[0].Title, categories[0].Slug, categories[0].ParentId, categories[0].AttributeSchema);
|
||||
var expectedDto2 = new CategoryDto(categories[1].Id, categories[1].Title, categories[1].Slug, categories[1].ParentId, categories[1].AttributeSchema);
|
||||
|
||||
result.Should().ContainEquivalentOf(expectedDto1);
|
||||
result.Should().ContainEquivalentOf(expectedDto2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldReturnEmptyList_WhenNoCategoriesExist()
|
||||
{
|
||||
// Arrange
|
||||
_categoryRepositoryMock.Setup(r => r.GetAllAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Category>());
|
||||
|
||||
var query = new GetCategoriesQuery();
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using Nashel.Modules.Catalog.Application.Common;
|
||||
using Nashel.Modules.Catalog.Application.Queries;
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
using Nashel.Modules.Catalog.Domain.ValueObjects;
|
||||
using Nashel.Modules.Catalog.Domain.Enums;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Tests.Application;
|
||||
|
||||
public class GetOfferByIdQueryHandlerTests
|
||||
{
|
||||
private readonly Mock<IOfferRepository> _offerRepositoryMock;
|
||||
private readonly GetOfferByIdQueryHandler _handler;
|
||||
|
||||
public GetOfferByIdQueryHandlerTests()
|
||||
{
|
||||
_offerRepositoryMock = new Mock<IOfferRepository>();
|
||||
_handler = new GetOfferByIdQueryHandler(_offerRepositoryMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldReturnOffer_WhenOfferExists()
|
||||
{
|
||||
// Arrange
|
||||
var offerId = Guid.NewGuid();
|
||||
var offer = new Offer(
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
"Test Offer",
|
||||
"Test Description",
|
||||
new Price(100, OfferType.Fixed, "RUB"),
|
||||
JsonDocument.Parse("{}")
|
||||
);
|
||||
|
||||
// Reflection hack to set the ID as it's private set in constructor (Guid.NewGuid())
|
||||
// But wait, the constructor sets a new GUID.
|
||||
// We need to mock the repository to return the offer when GetByIdAsync is called with the offer's generated ID.
|
||||
// Or cleaner: modify the repository setup to ignore the specific ID argument or capture it?
|
||||
// No, let's just use the ID generated by the offer.
|
||||
|
||||
_offerRepositoryMock.Setup(r => r.GetByIdAsync(offer.Id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(offer);
|
||||
|
||||
var query = new GetOfferByIdQuery(offer.Id);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Id.Should().Be(offer.Id);
|
||||
result.Title.Should().Be("Test Offer");
|
||||
result.Description.Should().Be("Test Description");
|
||||
result.Price.Should().BeEquivalentTo(offer.Price);
|
||||
result.OwnerId.Should().Be(offer.OwnerId);
|
||||
result.CategoryId.Should().Be(offer.CategoryId);
|
||||
result.Attributes.Should().Be(offer.Attributes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldReturnNull_WhenOfferDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
_offerRepositoryMock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((Offer?)null);
|
||||
|
||||
var query = new GetOfferByIdQuery(Guid.NewGuid());
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
}
|
||||
40
src/Modules/Catalog/Tests/Domain/CategoryTests.cs
Normal file
40
src/Modules/Catalog/Tests/Domain/CategoryTests.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using FluentAssertions;
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Tests.Domain;
|
||||
|
||||
public class CategoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ShouldSetPropertiesCorrectly()
|
||||
{
|
||||
// Assemble
|
||||
var title = "Test Category";
|
||||
var slug = "test-category";
|
||||
var parentId = Guid.NewGuid();
|
||||
var jsonDoc = JsonDocument.Parse("{}");
|
||||
|
||||
// Act
|
||||
var category = new Category(title, slug, parentId, jsonDoc);
|
||||
|
||||
// Assert
|
||||
category.Id.Should().NotBeEmpty();
|
||||
category.Title.Should().Be(title);
|
||||
category.Slug.Should().Be(slug);
|
||||
category.ParentId.Should().Be(parentId);
|
||||
category.AttributeSchema.Should().Be(jsonDoc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ShouldSetDefaultValues_WhenOptionalParametersAreNull()
|
||||
{
|
||||
// Act
|
||||
var category = new Category("Test", "test", null, null);
|
||||
|
||||
// Assert
|
||||
category.ParentId.Should().BeNull();
|
||||
category.AttributeSchema.Should().BeNull();
|
||||
}
|
||||
}
|
||||
36
src/Modules/Catalog/Tests/Domain/OfferTests.cs
Normal file
36
src/Modules/Catalog/Tests/Domain/OfferTests.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using FluentAssertions;
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
using Nashel.Modules.Catalog.Domain.ValueObjects;
|
||||
using Nashel.Modules.Catalog.Domain.Enums;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Tests.Domain;
|
||||
|
||||
public class OfferTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ShouldSetPropertiesCorrectly()
|
||||
{
|
||||
// Assemble
|
||||
var ownerId = Guid.NewGuid();
|
||||
var categoryId = Guid.NewGuid();
|
||||
var title = "Test Offer";
|
||||
var description = "Test Description";
|
||||
var price = new Price(100, OfferType.Fixed, "RUB");
|
||||
var attributes = JsonDocument.Parse("{}");
|
||||
|
||||
// Act
|
||||
var offer = new Offer(ownerId, categoryId, title, description, price, attributes);
|
||||
|
||||
// Assert
|
||||
offer.Id.Should().NotBeEmpty();
|
||||
offer.OwnerId.Should().Be(ownerId);
|
||||
offer.CategoryId.Should().Be(categoryId);
|
||||
offer.Title.Should().Be(title);
|
||||
offer.Description.Should().Be(description);
|
||||
offer.Price.Should().Be(price);
|
||||
offer.Attributes.Should().Be(attributes);
|
||||
offer.IsActive.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
58
src/Modules/Catalog/Tests/Domain/PriceTests.cs
Normal file
58
src/Modules/Catalog/Tests/Domain/PriceTests.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using FluentAssertions;
|
||||
using Nashel.Modules.Catalog.Domain.Enums;
|
||||
using Nashel.Modules.Catalog.Domain.ValueObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Tests.Domain;
|
||||
|
||||
public class PriceTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ShouldSetPropertiesCorrectly()
|
||||
{
|
||||
// Assemble
|
||||
var amount = 100m;
|
||||
var type = OfferType.Fixed;
|
||||
var currency = "USD";
|
||||
|
||||
// Act
|
||||
var price = new Price(amount, type, currency);
|
||||
|
||||
// Assert
|
||||
price.Amount.Should().Be(amount);
|
||||
price.Type.Should().Be(type);
|
||||
price.Currency.Should().Be(currency);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ShouldSetDefaultCurrency_WhenNotProvided()
|
||||
{
|
||||
// Act
|
||||
var price = new Price(100m, OfferType.Fixed);
|
||||
|
||||
// Assert
|
||||
price.Currency.Should().Be("RUB");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equality_ShouldBeTrue_WhenValuesAreSame()
|
||||
{
|
||||
// Arrange
|
||||
var price1 = new Price(100m, OfferType.Fixed, "USD");
|
||||
var price2 = new Price(100m, OfferType.Fixed, "USD");
|
||||
|
||||
// Act & Assert
|
||||
price1.Should().Be(price2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equality_ShouldBeFalse_WhenValuesAreDifferent()
|
||||
{
|
||||
// Arrange
|
||||
var price1 = new Price(100m, OfferType.Fixed, "USD");
|
||||
var price2 = new Price(200m, OfferType.Fixed, "USD");
|
||||
|
||||
// Act & Assert
|
||||
price1.Should().NotBe(price2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.5.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Domain\Nashel.Modules.Catalog.Domain.csproj" />
|
||||
<ProjectReference Include="..\Application\Nashel.Modules.Catalog.Application.csproj" />
|
||||
<ProjectReference Include="..\..\..\BuildingBlocks\Nashel.BuildingBlocks.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -6,7 +6,29 @@ using Nashel.Modules.Identity.Domain.Services;
|
||||
|
||||
namespace Nashel.Modules.Identity.Application.Commands;
|
||||
|
||||
public record LoginCommand(string Phone, string Password) : IRequest<string>; // Возвращает JWT
|
||||
/// <summary>
|
||||
/// Команда входа пользователя.
|
||||
/// </summary>
|
||||
public record LoginCommand : IRequest<string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Номер телефона.
|
||||
/// </summary>
|
||||
public string Phone { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Пароль.
|
||||
/// </summary>
|
||||
public string Password { get; init; } = default!;
|
||||
|
||||
public LoginCommand(string phone, string password)
|
||||
{
|
||||
Phone = phone;
|
||||
Password = password;
|
||||
}
|
||||
|
||||
public LoginCommand() { }
|
||||
} // Возвращает JWT
|
||||
|
||||
public class LoginCommandHandler : IRequestHandler<LoginCommand, string>
|
||||
{
|
||||
|
||||
@@ -5,7 +5,29 @@ using Nashel.Modules.Identity.Domain.Repositories;
|
||||
|
||||
namespace Nashel.Modules.Identity.Application.Commands;
|
||||
|
||||
public record RegisterUserCommand(string Phone, string Password) : IRequest<Guid>;
|
||||
/// <summary>
|
||||
/// Команда регистрации нового пользователя.
|
||||
/// </summary>
|
||||
public record RegisterUserCommand : IRequest<Guid>
|
||||
{
|
||||
/// <summary>
|
||||
/// Номер телефона пользователя.
|
||||
/// </summary>
|
||||
public string Phone { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Пароль пользователя.
|
||||
/// </summary>
|
||||
public string Password { get; init; } = default!;
|
||||
|
||||
public RegisterUserCommand(string phone, string password)
|
||||
{
|
||||
Phone = phone;
|
||||
Password = password;
|
||||
}
|
||||
|
||||
public RegisterUserCommand() { }
|
||||
}
|
||||
|
||||
public class RegisterUserCommandHandler : IRequestHandler<RegisterUserCommand, Guid>
|
||||
{
|
||||
|
||||
@@ -11,5 +11,7 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -9,6 +9,8 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
57
src/Modules/Identity/Infrastructure/Persistence/Migrations/20260210114447_InitialCreate.Designer.cs
generated
Normal file
57
src/Modules/Identity/Infrastructure/Persistence/Migrations/20260210114447_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,57 @@
|
||||
// <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("20260210114447_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <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<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");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "identity");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Accounts",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Phone = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "text", nullable: false),
|
||||
Roles = table.Column<string>(type: "jsonb", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Accounts", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Accounts_Phone",
|
||||
schema: "identity",
|
||||
table: "Accounts",
|
||||
column: "Phone",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Accounts",
|
||||
schema: "identity");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
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))]
|
||||
partial class IdentityDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(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<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");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user