add account creation when adding a company

This commit is contained in:
cuqmbr 2025-05-29 11:56:34 +03:00
parent bb309d7c20
commit 9ccd0bb68d
Signed by: cuqmbr
GPG Key ID: 0AA446880C766199
20 changed files with 1617 additions and 11 deletions

View File

@ -11,4 +11,11 @@ public record AddCompanyCommand : IRequest<CompanyDto>
public string ContactEmail { get; set; } public string ContactEmail { get; set; }
public string ContactPhoneNumber { get; set; } public string ContactPhoneNumber { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
} }

View File

@ -1,8 +1,12 @@
using MediatR; using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Domain.Entities;
using AutoMapper; using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions; using cuqmbr.TravelGuide.Application.Common.Exceptions;
using cuqmbr.TravelGuide.Application.Common.Services;
using cuqmbr.TravelGuide.Domain.Entities;
using cuqmbr.TravelGuide.Domain.Enums;
using System.Security.Cryptography;
using System.Text;
namespace cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany; namespace cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany;
@ -11,13 +15,14 @@ public class AddCompanyCommandHandler :
{ {
private readonly UnitOfWork _unitOfWork; private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly PasswordHasherService _passwordHasher;
public AddCompanyCommandHandler( public AddCompanyCommandHandler(UnitOfWork unitOfWork, IMapper mapper,
UnitOfWork unitOfWork, PasswordHasherService passwordHasher)
IMapper mapper)
{ {
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_mapper = mapper; _mapper = mapper;
_passwordHasher = passwordHasher;
} }
public async Task<CompanyDto> Handle( public async Task<CompanyDto> Handle(
@ -33,12 +38,51 @@ public class AddCompanyCommandHandler :
"Company with given name already exists."); "Company with given name already exists.");
} }
// Create new account for employee
var account = await _unitOfWork.AccountRepository.GetOneAsync(
e => e.Email == request.Email,
cancellationToken);
if (account != null)
{
throw new DuplicateEntityException();
}
var role = (await _unitOfWork.RoleRepository.GetPageAsync(
1, IdentityRole.Enumerations.Count(), cancellationToken))
.Items
.First(r => r.Value.Equals(IdentityRole.CompanyOwner));
var salt = RandomNumberGenerator.GetBytes(128 / 8);
var hash = await _passwordHasher.HashAsync(
Encoding.UTF8.GetBytes(request.Password),
salt, cancellationToken);
var saltBase64 = Convert.ToBase64String(salt);
var hashBase64 = Convert.ToBase64String(hash);
account = new Account()
{
Username = request.Username,
Email = request.Email,
PasswordHash = hashBase64,
PasswordSalt = saltBase64,
AccountRoles = new AccountRole[] { new() { RoleId = role.Id } }
};
account = await _unitOfWork.AccountRepository.AddOneAsync(
account, cancellationToken);
entity = new Company() entity = new Company()
{ {
Name = request.Name, Name = request.Name,
LegalAddress = request.LegalAddress, LegalAddress = request.LegalAddress,
ContactEmail = request.ContactEmail, ContactEmail = request.ContactEmail,
ContactPhoneNumber = request.ContactPhoneNumber ContactPhoneNumber = request.ContactPhoneNumber,
Account = account
}; };
entity = await _unitOfWork.CompanyRepository.AddOneAsync( entity = await _unitOfWork.CompanyRepository.AddOneAsync(

View File

@ -54,5 +54,46 @@ public class AddCompanyCommandValidator : AbstractValidator<AddCompanyCommand>
cultureService.Culture, cultureService.Culture,
localizer["FluentValidation.MaximumLength"], localizer["FluentValidation.MaximumLength"],
64)); 64));
RuleFor(v => v.Username)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MinimumLength(1)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MinimumLength"],
1))
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32))
.IsUsername()
.WithMessage(localizer["FluentValidation.IsUsername"]);
RuleFor(v => v.Email)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.IsEmail()
.WithMessage(localizer["FluentValidation.IsEmail"]);
RuleFor(v => v.Password)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MinimumLength(8)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MinimumLength"],
8))
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
} }
} }

View File

@ -18,7 +18,7 @@ public class DeleteCompanyCommandHandler : IRequestHandler<DeleteCompanyCommand>
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var entity = await _unitOfWork.CompanyRepository.GetOneAsync( var entity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken); e => e.Guid == request.Guid, e => e.Account, cancellationToken);
if (entity == null) if (entity == null)
{ {
@ -28,6 +28,9 @@ public class DeleteCompanyCommandHandler : IRequestHandler<DeleteCompanyCommand>
await _unitOfWork.CompanyRepository.DeleteOneAsync( await _unitOfWork.CompanyRepository.DeleteOneAsync(
entity, cancellationToken); entity, cancellationToken);
await _unitOfWork.AccountRepository.DeleteOneAsync(
entity.Account, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken); await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose(); _unitOfWork.Dispose();
} }

View File

@ -31,10 +31,14 @@ public class UpdateCompanyCommandHandler :
throw new NotFoundException(); throw new NotFoundException();
} }
var account = await _unitOfWork.AccountRepository.GetOneAsync(
a => a.Id == entity.AccountId, cancellationToken);
entity.Name = request.Name; entity.Name = request.Name;
entity.LegalAddress = request.LegalAddress; entity.LegalAddress = request.LegalAddress;
entity.ContactEmail = request.ContactEmail; entity.ContactEmail = request.ContactEmail;
entity.ContactPhoneNumber = request.ContactPhoneNumber; entity.ContactPhoneNumber = request.ContactPhoneNumber;
entity.Account = account;
entity = await _unitOfWork.CompanyRepository.UpdateOneAsync( entity = await _unitOfWork.CompanyRepository.UpdateOneAsync(
entity, cancellationToken); entity, cancellationToken);

View File

@ -0,0 +1,21 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Companies;
public sealed class CompanyAccountDto : IMapFrom<Account>
{
public Guid Uuid { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<Account, CompanyAccountDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid));
}
}

View File

@ -15,6 +15,8 @@ public sealed class CompanyDto : IMapFrom<Company>
public string ContactPhoneNumber { get; set; } public string ContactPhoneNumber { get; set; }
public CompanyAccountDto Account { get; set; }
public void Mapping(MappingProfile profile) public void Mapping(MappingProfile profile)
{ {
profile.CreateMap<Company, CompanyDto>() profile.CreateMap<Company, CompanyDto>()

View File

@ -33,6 +33,19 @@ public class GetCompaniesPageQueryHandler :
request.PageNumber, request.PageSize, request.PageNumber, request.PageSize,
cancellationToken); cancellationToken);
// Hydrate companies
var accountIds = paginatedList.Items.Select(e => e.AccountId);
var accounts = await _unitOfWork.AccountRepository.GetPageAsync(
e => accountIds.Contains(e.Id),
1, paginatedList.Items.Count, cancellationToken);
foreach (var company in paginatedList.Items)
{
company.Account =
accounts.Items.First(a => a.Id == company.AccountId);
}
var mappedItems = _mapper var mappedItems = _mapper
.ProjectTo<CompanyDto>(paginatedList.Items.AsQueryable()); .ProjectTo<CompanyDto>(paginatedList.Items.AsQueryable());

View File

@ -26,13 +26,18 @@ public class GetCompanyQueryHandler :
var entity = await _unitOfWork.CompanyRepository.GetOneAsync( var entity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken); e => e.Guid == request.Guid, cancellationToken);
_unitOfWork.Dispose();
if (entity == null) if (entity == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
var account = await _unitOfWork.AccountRepository.GetOneAsync(
e => e.Id == entity.AccountId, cancellationToken);
entity.Account = account;
_unitOfWork.Dispose();
return _mapper.Map<CompanyDto>(entity); return _mapper.Map<CompanyDto>(entity);
} }
} }

View File

@ -9,4 +9,11 @@ public sealed class AddCompanyViewModel
public string ContactEmail { get; set; } public string ContactEmail { get; set; }
public string ContactPhoneNumber { get; set; } public string ContactPhoneNumber { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
} }

View File

@ -68,7 +68,6 @@ public class AddEmployeeCommandHandler :
throw new DuplicateEntityException(); throw new DuplicateEntityException();
} }
var role = (await _unitOfWork.RoleRepository.GetPageAsync( var role = (await _unitOfWork.RoleRepository.GetPageAsync(
1, IdentityRole.Enumerations.Count(), cancellationToken)) 1, IdentityRole.Enumerations.Count(), cancellationToken))
.Items .Items

View File

@ -5,7 +5,17 @@ namespace cuqmbr.TravelGuide.Application.Employees;
public sealed class EmployeeAccountDto : IMapFrom<Account> public sealed class EmployeeAccountDto : IMapFrom<Account>
{ {
public Guid Uuid { get; set; }
public string Username { get; set; } public string Username { get; set; }
public string Email { get; set; } public string Email { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<Account, EmployeeAccountDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid));
}
} }

View File

@ -61,8 +61,9 @@ public class GetEmployeesPageQueryHandler :
companies.Items.First(c => c.Id == employee.CompanyId); companies.Items.First(c => c.Id == employee.CompanyId);
} }
var accountIds = paginatedList.Items.Select(e => e.AccountId);
var accounts = await _unitOfWork.AccountRepository.GetPageAsync( var accounts = await _unitOfWork.AccountRepository.GetPageAsync(
e => paginatedList.Items.Select(e => e.AccountId).Contains(e.Id), e => accountIds.Contains(e.Id),
1, paginatedList.Items.Count, cancellationToken); 1, paginatedList.Items.Count, cancellationToken);
foreach (var employee in paginatedList.Items) foreach (var employee in paginatedList.Items)

View File

@ -16,4 +16,6 @@ public sealed class Account : EntityBase
public Employee? Employee { get; set; } public Employee? Employee { get; set; }
public Company? Company { get; set; }
} }

View File

@ -14,4 +14,9 @@ public sealed class Company : EntityBase
public ICollection<Employee> Employees { get; set; } public ICollection<Employee> Employees { get; set; }
public ICollection<Vehicle> Vehicles { get; set; } public ICollection<Vehicle> Vehicles { get; set; }
public long AccountId { get; set; }
public Account Account { get; set; }
} }

View File

@ -52,6 +52,9 @@ public class CompaniesController : ControllerBase
LegalAddress = viewModel.LegalAddress, LegalAddress = viewModel.LegalAddress,
ContactEmail = viewModel.ContactEmail, ContactEmail = viewModel.ContactEmail,
ContactPhoneNumber = viewModel.ContactPhoneNumber, ContactPhoneNumber = viewModel.ContactPhoneNumber,
Username = viewModel.Username,
Email = viewModel.Email,
Password = viewModel.Password
}, },
cancellationToken)); cancellationToken));
} }

View File

@ -38,5 +38,29 @@ public class CompanyConfiguration : BaseConfiguration<Company>
.HasColumnName("contact_phone_number") .HasColumnName("contact_phone_number")
.HasColumnType("varchar(64)") .HasColumnType("varchar(64)")
.IsRequired(true); .IsRequired(true);
builder
.Property(c => c.AccountId)
.HasColumnName("account_id")
.HasColumnType("bigint")
.IsRequired(true);
builder
.HasOne(c => c.Account)
.WithOne(a => a.Company)
.HasForeignKey<Company>(c => c.AccountId)
.HasConstraintName(
"fk_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(c => c.AccountId).Metadata.GetColumnName()}")
.OnDelete(DeleteBehavior.Cascade);
builder
.HasIndex(c => c.AccountId)
.HasDatabaseName(
"ix_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(c => c.AccountId).Metadata.GetColumnName()}");
} }
} }

View File

@ -0,0 +1,58 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Persistence.PostgreSql.Migrations
{
/// <inheritdoc />
public partial class Add_navigation_from_Company_to_Account : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "account_id",
schema: "application",
table: "companies",
type: "bigint",
nullable: false,
defaultValue: 0L);
migrationBuilder.CreateIndex(
name: "ix_companies_account_id",
schema: "application",
table: "companies",
column: "account_id",
unique: true);
migrationBuilder.AddForeignKey(
name: "fk_companies_account_id",
schema: "application",
table: "companies",
column: "account_id",
principalSchema: "application",
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_companies_account_id",
schema: "application",
table: "companies");
migrationBuilder.DropIndex(
name: "ix_companies_account_id",
schema: "application",
table: "companies");
migrationBuilder.DropColumn(
name: "account_id",
schema: "application",
table: "companies");
}
}
}

View File

@ -235,6 +235,10 @@ namespace Persistence.PostgreSql.Migrations
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "companies_id_sequence"); NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "companies_id_sequence");
b.Property<long>("AccountId")
.HasColumnType("bigint")
.HasColumnName("account_id");
b.Property<string>("ContactEmail") b.Property<string>("ContactEmail")
.IsRequired() .IsRequired()
.HasColumnType("varchar(256)") .HasColumnType("varchar(256)")
@ -265,6 +269,10 @@ namespace Persistence.PostgreSql.Migrations
b.HasAlternateKey("Guid") b.HasAlternateKey("Guid")
.HasName("altk_companies_uuid"); .HasName("altk_companies_uuid");
b.HasIndex("AccountId")
.IsUnique()
.HasDatabaseName("ix_companies_account_id");
b.ToTable("companies", "application"); b.ToTable("companies", "application");
}); });
@ -1036,6 +1044,18 @@ namespace Persistence.PostgreSql.Migrations
b.Navigation("Region"); b.Navigation("Region");
}); });
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b =>
{
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account")
.WithOne("Company")
.HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Company", "AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_companies_account_id");
b.Navigation("Account");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b =>
{ {
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account")
@ -1230,6 +1250,8 @@ namespace Persistence.PostgreSql.Migrations
{ {
b.Navigation("AccountRoles"); b.Navigation("AccountRoles");
b.Navigation("Company");
b.Navigation("Employee"); b.Navigation("Employee");
b.Navigation("RefreshTokens"); b.Navigation("RefreshTokens");