diff --git a/src/Application/Abstractions/Data/IApplicationDbContext.cs b/src/Application/Abstractions/Data/IApplicationDbContext.cs index d66e1e9..c5bdcd7 100644 --- a/src/Application/Abstractions/Data/IApplicationDbContext.cs +++ b/src/Application/Abstractions/Data/IApplicationDbContext.cs @@ -1,15 +1,19 @@ -using Domain.Applications; +using Domain.Applications; +using Domain.Areas; using Domain.AuditLogs; using Domain.Businesses; using Domain.BusinessMembers; using Domain.Countries; using Domain.Customers; +using Domain.Districts; using Domain.EmailVerification; +using Domain.Localities; using Domain.MfaLogs; using Domain.MfaSettings; using Domain.Otps; using Domain.PasswordResets; using Domain.Permissions; +using Domain.Regions; using Domain.RolePermissions; using Domain.Roles; using Domain.SmtpConfigs; @@ -45,6 +49,10 @@ public interface IApplicationDbContext DbSet Otp { get; } DbSet SmtpConfig { get; } DbSet Countries { get; } + DbSet Regions { get; } + DbSet Districts { get; } + DbSet Areas { get; } + DbSet Localities { get; } EntityEntry Entry(object entity); Task SaveChangesAsync(CancellationToken cancellationToken = default); } diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index b62bead..a99ee03 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -22,4 +22,8 @@ + + + + diff --git a/src/Application/Applications/Delete/DeleteApplicationCommandHandler.cs b/src/Application/Applications/Delete/DeleteApplicationCommandHandler.cs index 55f649a..cdcecc0 100644 --- a/src/Application/Applications/Delete/DeleteApplicationCommandHandler.cs +++ b/src/Application/Applications/Delete/DeleteApplicationCommandHandler.cs @@ -7,6 +7,7 @@ using SharedKernel; namespace Application.Applications.Delete; + public sealed class DeleteApplicationCommandHandler : ICommandHandler { diff --git a/src/Application/Areas/Create/CreateAreaCommand.cs b/src/Application/Areas/Create/CreateAreaCommand.cs new file mode 100644 index 0000000..c9fbb0b --- /dev/null +++ b/src/Application/Areas/Create/CreateAreaCommand.cs @@ -0,0 +1,12 @@ +using Application.Abstractions.Messaging; +using Domain.Areas; + +namespace Application.Areas.Create; + +public sealed record CreateAreaCommand( + Guid CountryId, + Guid DistrictId, + string Name, + Area.AreaType Type, + bool IsActive +) : ICommand; diff --git a/src/Application/Areas/Create/CreateAreaCommandHandler .cs b/src/Application/Areas/Create/CreateAreaCommandHandler .cs new file mode 100644 index 0000000..696250f --- /dev/null +++ b/src/Application/Areas/Create/CreateAreaCommandHandler .cs @@ -0,0 +1,37 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Domain; +using Domain.Areas; +using SharedKernel; + +namespace Application.Areas.Create; + +public sealed class CreateAreaCommandHandler + : ICommandHandler +{ + private readonly IApplicationDbContext _context; + + public CreateAreaCommandHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task> Handle( + CreateAreaCommand command, + CancellationToken cancellationToken) + { + var area = new Area + { + CountryId = command.CountryId, + DistrictId = command.DistrictId, + Name = command.Name, + Type = command.Type, + IsActive = command.IsActive + }; + + await _context.Areas.AddAsync(area, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + + return Result.Success(area.Id); + } +} diff --git a/src/Application/Areas/Create/CreateAreaValidator .cs b/src/Application/Areas/Create/CreateAreaValidator .cs new file mode 100644 index 0000000..6296ec8 --- /dev/null +++ b/src/Application/Areas/Create/CreateAreaValidator .cs @@ -0,0 +1,35 @@ +using System; +using FluentValidation; + +namespace Application.Areas.Create; + + public class CreateAreaValidator : AbstractValidator + { + public CreateAreaValidator() + { + RuleFor(x => x.CountryId) + .NotEmpty() + .WithMessage("CountryId is required.") + .NotEqual(Guid.Empty) + .WithMessage("CountryId cannot be empty GUID."); + + RuleFor(x => x.DistrictId) + .NotEmpty() + .WithMessage("ID is required.") + .NotEqual(Guid.Empty) + .WithMessage("DistrictId cannot be empty GUID."); + + RuleFor(x => x.Name) + .NotEmpty() + .WithMessage("Name is required.") + .MaximumLength(255) + .WithMessage("Name must not exceed 255 characters."); + + RuleFor(x => x.Type) + .IsInEnum() + .WithMessage( + "Invalid area type. Valid values: 1=Upazila, 2=City, 3=Thana, 4=Municipality, 5=Township." + ); + } + } + diff --git a/src/Application/Areas/Delete/DeleteAreaCommand.cs b/src/Application/Areas/Delete/DeleteAreaCommand.cs new file mode 100644 index 0000000..b632a99 --- /dev/null +++ b/src/Application/Areas/Delete/DeleteAreaCommand.cs @@ -0,0 +1,5 @@ +using Application.Abstractions.Messaging; + +namespace Application.Areas.Delete; + +public sealed record DeleteAreaCommand(Guid Id) : ICommand; diff --git a/src/Application/Areas/Delete/DeleteAreaCommandHandler.cs b/src/Application/Areas/Delete/DeleteAreaCommandHandler.cs new file mode 100644 index 0000000..f77a5b2 --- /dev/null +++ b/src/Application/Areas/Delete/DeleteAreaCommandHandler.cs @@ -0,0 +1,34 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Domain.Areas; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Areas.Delete; + +public sealed class DeleteAreaCommandHandler + : ICommandHandler +{ + private readonly IApplicationDbContext _context; + + public DeleteAreaCommandHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(DeleteAreaCommand command, CancellationToken cancellationToken) + { + Area? area = await _context.Areas + .FirstOrDefaultAsync(a => a.Id == command.Id, cancellationToken); + + if (area is null) + { + return Result.Failure("Area not found."); + } + + _context.Areas.Remove(area); + await _context.SaveChangesAsync(cancellationToken); + + return Result.Success(); + } +} diff --git a/src/Application/Areas/Delete/DeleteAreaValidator .cs b/src/Application/Areas/Delete/DeleteAreaValidator .cs new file mode 100644 index 0000000..f2961c5 --- /dev/null +++ b/src/Application/Areas/Delete/DeleteAreaValidator .cs @@ -0,0 +1,16 @@ +using System; +using FluentValidation; + +namespace Application.Areas.Delete; + +public class DeleteAreaValidator : AbstractValidator +{ + public DeleteAreaValidator() + { + RuleFor(x => x.Id) + .NotEmpty() + .WithMessage("ID is required.") + .NotEqual(Guid.Empty) + .WithMessage("ID cannot be empty GUID."); + } +} diff --git a/src/Application/Areas/Get/AreaResponse.cs b/src/Application/Areas/Get/AreaResponse.cs new file mode 100644 index 0000000..7667c55 --- /dev/null +++ b/src/Application/Areas/Get/AreaResponse.cs @@ -0,0 +1,11 @@ +namespace Application.Areas.Get; + +public sealed record AreaResponse( + Guid Id, + Guid CountryId, + Guid DistrictId, + string Name, + int Type, + string TypeName, + bool IsActive +); diff --git a/src/Application/Areas/Get/GetAreaByIdQuery.cs b/src/Application/Areas/Get/GetAreaByIdQuery.cs new file mode 100644 index 0000000..8638d5b --- /dev/null +++ b/src/Application/Areas/Get/GetAreaByIdQuery.cs @@ -0,0 +1,6 @@ +using System; +using Application.Abstractions.Messaging; + +namespace Application.Areas.Get; + +public sealed record GetAreaByIdQuery(Guid Id) : IQuery; diff --git a/src/Application/Areas/Get/GetAreaByIdQueryHandler.cs b/src/Application/Areas/Get/GetAreaByIdQueryHandler.cs new file mode 100644 index 0000000..7def816 --- /dev/null +++ b/src/Application/Areas/Get/GetAreaByIdQueryHandler.cs @@ -0,0 +1,41 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Areas.Get; + +public sealed class GetAreaByIdQueryHandler + : IQueryHandler +{ + private readonly IApplicationDbContext _context; + + public GetAreaByIdQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task> Handle( + GetAreaByIdQuery query, + CancellationToken cancellationToken) + { + AreaResponse? area = await _context.Areas.Where(a => a.Id == query.Id) + .Select(a => new AreaResponse( + a.Id, + a.CountryId, + a.DistrictId, + a.Name, + (int)a.Type, + a.Type.ToString(), + a.IsActive + )) + .FirstOrDefaultAsync(cancellationToken); + + if (area is null) + { + return Result.Failure("Area not found."); + } + + return Result.Success(area); + } +} diff --git a/src/Application/Areas/GetAll/GetAllAreasQuery.cs b/src/Application/Areas/GetAll/GetAllAreasQuery.cs new file mode 100644 index 0000000..5fee8b3 --- /dev/null +++ b/src/Application/Areas/GetAll/GetAllAreasQuery.cs @@ -0,0 +1,6 @@ +using Application.Abstractions.Messaging; +using Application.Areas.Get; + +namespace Application.Areas.GetAll; + +public sealed record GetAllAreasQuery() : IQuery>; diff --git a/src/Application/Areas/GetAll/GetAllAreasQueryHandler.cs b/src/Application/Areas/GetAll/GetAllAreasQueryHandler.cs new file mode 100644 index 0000000..1661705 --- /dev/null +++ b/src/Application/Areas/GetAll/GetAllAreasQueryHandler.cs @@ -0,0 +1,38 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Application.Areas.Get; +using Application.Areas.GetAll; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Areas.GetAll; + +public sealed class GetAllAreasQueryHandler + : IQueryHandler> +{ + private readonly IApplicationDbContext _context; + + public GetAllAreasQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task>> Handle( + GetAllAreasQuery query, + CancellationToken cancellationToken) + { + List areas = await _context.Areas + .Select(a => new AreaResponse( + a.Id, + a.CountryId, + a.DistrictId, + a.Name, + (int)a.Type, + a.Type.ToString(), + a.IsActive + )) + .ToListAsync(cancellationToken); + + return Result.Success(areas); + } +} diff --git a/src/Application/Areas/Update/UpdateAreaCommand.cs b/src/Application/Areas/Update/UpdateAreaCommand.cs new file mode 100644 index 0000000..7ff2eab --- /dev/null +++ b/src/Application/Areas/Update/UpdateAreaCommand.cs @@ -0,0 +1,13 @@ +using Application.Abstractions.Messaging; +using Domain.Areas; + +namespace Application.Areas.Update; + +public sealed record UpdateAreaCommand( + Guid Id, + Guid CountryId, + Guid DistrictId, + string Name, + Area.AreaType Type, + bool IsActive +) : ICommand; diff --git a/src/Application/Areas/Update/UpdateAreaCommandHandler.cs b/src/Application/Areas/Update/UpdateAreaCommandHandler.cs new file mode 100644 index 0000000..3c7d16d --- /dev/null +++ b/src/Application/Areas/Update/UpdateAreaCommandHandler.cs @@ -0,0 +1,55 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Domain.Areas; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Areas.Update; + +public sealed class UpdateAreaCommandHandler + : ICommandHandler +{ + private readonly IApplicationDbContext _context; + + public UpdateAreaCommandHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task> Handle( + UpdateAreaCommand command, + CancellationToken cancellationToken) + { + Area? area = await _context.Areas + .FirstOrDefaultAsync(a => a.Id == command.Id, cancellationToken); + + if (area is null) + { + return Result.Failure("Area not found."); + } + + // Check if area name is unique within the same district (excluding current area) + bool areaNameExists = await _context.Areas + .AnyAsync(a => + a.DistrictId == command.DistrictId && + a.Name == command.Name && + a.Id != command.Id, + cancellationToken); + + if (areaNameExists) + { + return Result.Failure("Area name already exists in this district."); + } + + // Update properties + area.CountryId = command.CountryId; + area.DistrictId = command.DistrictId; + area.Name = command.Name; + area.Type = command.Type; + area.IsActive = command.IsActive; + + await _context.SaveChangesAsync(cancellationToken); + + return Result.Success(area.Id); + } +} diff --git a/src/Application/Areas/Update/UpdateAreaValidator .cs b/src/Application/Areas/Update/UpdateAreaValidator .cs new file mode 100644 index 0000000..2dacf04 --- /dev/null +++ b/src/Application/Areas/Update/UpdateAreaValidator .cs @@ -0,0 +1,36 @@ +using System; +using FluentValidation; + +namespace Application.Areas.Update; + +public class UpdateAreaValidator : AbstractValidator +{ + public UpdateAreaValidator() + { + + RuleFor(x => x.CountryId) + .NotEmpty() + .WithMessage("CountryId is required.") + .NotEqual(Guid.Empty) + .WithMessage("CountryId cannot be empty GUID."); + RuleFor(x => x.DistrictId) + .NotEmpty() + .WithMessage("ID is required.") + .NotEqual(Guid.Empty) + .WithMessage("DistrictId cannot be empty GUID."); + + RuleFor(a => a.Name) + .NotEmpty() + .WithMessage("Name is required.") + .MaximumLength(255) + .WithMessage("Name must not exceed 255 characters."); + + RuleFor(a => a.Type) + .IsInEnum() + .WithMessage("Invalid area type."); + + RuleFor(a => a.IsActive) + .NotNull() + .WithMessage("IsActive is required."); + } +} diff --git a/src/Application/AuditLogs/Get/GetAuditLogQuery.cs b/src/Application/AuditLogs/Get/GetAuditLogQuery.cs index 773ea02..f2ffc55 100644 --- a/src/Application/AuditLogs/Get/GetAuditLogQuery.cs +++ b/src/Application/AuditLogs/Get/GetAuditLogQuery.cs @@ -1,5 +1,4 @@ - -using Application.Abstractions.Messaging; +using Application.Abstractions.Messaging; namespace Application.AuditLogs.Get; public sealed record GetAuditLogQuery() diff --git a/src/Application/AuditLogs/Update/UpdateAuditLogCommandHandler.cs b/src/Application/AuditLogs/Update/UpdateAuditLogCommandHandler.cs index 65198de..3695c87 100644 --- a/src/Application/AuditLogs/Update/UpdateAuditLogCommandHandler.cs +++ b/src/Application/AuditLogs/Update/UpdateAuditLogCommandHandler.cs @@ -1,7 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Application.Abstractions.Data; +using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Domain.AuditLogs; using Microsoft.EntityFrameworkCore; diff --git a/src/Application/AuditLogs/Update/UpdateAuditLogValidator.cs b/src/Application/AuditLogs/Update/UpdateAuditLogValidator.cs index c894c57..c38d078 100644 --- a/src/Application/AuditLogs/Update/UpdateAuditLogValidator.cs +++ b/src/Application/AuditLogs/Update/UpdateAuditLogValidator.cs @@ -1,5 +1,4 @@ - -using FluentValidation; +using FluentValidation; namespace Application.AuditLogs.Update; public sealed class UpdateAuditLogValidator : AbstractValidator diff --git a/src/Application/Districts/Create/CreateDistrictCommand.cs b/src/Application/Districts/Create/CreateDistrictCommand.cs new file mode 100644 index 0000000..c4f7444 --- /dev/null +++ b/src/Application/Districts/Create/CreateDistrictCommand.cs @@ -0,0 +1,14 @@ +using System; +using Application.Abstractions.Messaging; + +namespace Application.Districts.Create; + +public sealed class CreateDistrictCommand : ICommand +{ + public Guid CountryId { get; set; } + public Guid RegionId { get; set; } + public string Name { get; set; } + public bool IsActive { get; set; } = true; + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/Application/Districts/Create/CreateDistrictCommandHandler.cs b/src/Application/Districts/Create/CreateDistrictCommandHandler.cs new file mode 100644 index 0000000..9d5d1d4 --- /dev/null +++ b/src/Application/Districts/Create/CreateDistrictCommandHandler.cs @@ -0,0 +1,27 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Domain.Districts; +using SharedKernel; + +namespace Application.Districts.Create; + +internal sealed class CreateDistrictCommandHandler(IApplicationDbContext context) + : ICommandHandler +{ + public async Task> Handle(CreateDistrictCommand command, CancellationToken cancellationToken) + { + var district = new District + { + CountryId = command.CountryId, + RegionId = command.RegionId, + Name = command.Name, + IsActive = command.IsActive, + CreatedAt = command.CreatedAt + }; + + await context.Districts.AddAsync(district, cancellationToken); + await context.SaveChangesAsync(cancellationToken); + + return Result.Success(district.Id); + } +} diff --git a/src/Application/Districts/Create/CreateDistrictValidator.cs b/src/Application/Districts/Create/CreateDistrictValidator.cs new file mode 100644 index 0000000..634065c --- /dev/null +++ b/src/Application/Districts/Create/CreateDistrictValidator.cs @@ -0,0 +1,27 @@ +using FluentValidation; + +namespace Application.Districts.Create; + +public sealed class CreateDistrictValidator : AbstractValidator +{ + public CreateDistrictValidator() + { + RuleFor(d => d.CountryId) + .NotEmpty() + .WithMessage("CountryId is required."); + + RuleFor(d => d.RegionId) + .NotEmpty() + .WithMessage("RegionId is required."); + + RuleFor(d => d.Name) + .NotEmpty() + .WithMessage("Name is required.") + .MaximumLength(200) + .WithMessage("Name cannot exceed 200 characters."); + + RuleFor(d => d.CreatedAt) + .NotEmpty() + .WithMessage("CreatedAt is required."); + } +} diff --git a/src/Application/Districts/Delete/DeleteDistrictCommand.cs b/src/Application/Districts/Delete/DeleteDistrictCommand.cs new file mode 100644 index 0000000..a9cb75e --- /dev/null +++ b/src/Application/Districts/Delete/DeleteDistrictCommand.cs @@ -0,0 +1,6 @@ +using System; +using Application.Abstractions.Messaging; + +namespace Application.Districts.Delete; + +public sealed record DeleteDistrictCommand(Guid Id) : ICommand; diff --git a/src/Application/Districts/Delete/DeleteDistrictCommandHandler.cs b/src/Application/Districts/Delete/DeleteDistrictCommandHandler.cs new file mode 100644 index 0000000..dd2f089 --- /dev/null +++ b/src/Application/Districts/Delete/DeleteDistrictCommandHandler.cs @@ -0,0 +1,35 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Domain.Districts; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Districts.Delete; + +internal sealed class DeleteDistrictCommandHandler + : ICommandHandler +{ + private readonly IApplicationDbContext _context; + + public DeleteDistrictCommandHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(DeleteDistrictCommand command, CancellationToken cancellationToken) + { + District? district = await _context.Districts + .SingleOrDefaultAsync(x => x.Id == command.Id, cancellationToken); + + if (district is null) + { + return Result.Failure(DistrictErrors.NotFound(command.Id)); + } + + _context.Districts.Remove(district); + + await _context.SaveChangesAsync(cancellationToken); + + return Result.Success(); + } +} diff --git a/src/Application/Districts/Delete/DeleteDistrictCommandValidator.cs b/src/Application/Districts/Delete/DeleteDistrictCommandValidator.cs new file mode 100644 index 0000000..b47e812 --- /dev/null +++ b/src/Application/Districts/Delete/DeleteDistrictCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; + +namespace Application.Districts.Delete; + +public sealed class DeleteDistrictCommandValidator + : AbstractValidator +{ + public DeleteDistrictCommandValidator() + { + RuleFor(x => x.Id) + .NotEmpty() + .WithMessage("District Id is required."); + } +} diff --git a/src/Application/Districts/Get/DistrictResponse.cs b/src/Application/Districts/Get/DistrictResponse.cs new file mode 100644 index 0000000..1e6970a --- /dev/null +++ b/src/Application/Districts/Get/DistrictResponse.cs @@ -0,0 +1,14 @@ +using System; + +namespace Application.Districts.Get; + +public sealed class DistrictResponse +{ + public Guid Id { get; set; } + public Guid CountryId { get; set; } + public Guid RegionId { get; set; } + public string Name { get; set; } = string.Empty; + public bool IsActive { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } +} diff --git a/src/Application/Districts/Get/GetDistrictQuery.cs b/src/Application/Districts/Get/GetDistrictQuery.cs new file mode 100644 index 0000000..16aa078 --- /dev/null +++ b/src/Application/Districts/Get/GetDistrictQuery.cs @@ -0,0 +1,6 @@ +using Application.Abstractions.Messaging; + +namespace Application.Districts.Get; + +public sealed record GetDistrictQuery() + : IQuery>; diff --git a/src/Application/Districts/Get/GetDistrictQueryHandler.cs b/src/Application/Districts/Get/GetDistrictQueryHandler.cs new file mode 100644 index 0000000..5e0ffc3 --- /dev/null +++ b/src/Application/Districts/Get/GetDistrictQueryHandler.cs @@ -0,0 +1,32 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Districts.Get; + +internal sealed class GetDistrictQueryHandler( + IApplicationDbContext context) + : IQueryHandler> +{ + public async Task>> Handle( + GetDistrictQuery query, + CancellationToken cancellationToken) + { + List districts = await context.Districts + .OrderByDescending(x => x.CreatedAt) + .Select(x => new DistrictResponse + { + Id = x.Id, + CountryId = x.CountryId, + RegionId = x.RegionId, + Name = x.Name, + IsActive = x.IsActive, + CreatedAt = x.CreatedAt, + UpdatedAt = x.UpdatedAt + }) + .ToListAsync(cancellationToken); + + return districts; + } +} diff --git a/src/Application/Districts/GetById/GetDistrictByIdQuery.cs b/src/Application/Districts/GetById/GetDistrictByIdQuery.cs new file mode 100644 index 0000000..2fca7db --- /dev/null +++ b/src/Application/Districts/GetById/GetDistrictByIdQuery.cs @@ -0,0 +1,8 @@ +using System; +using Application.Abstractions.Messaging; +using Application.Districts.Get; + +namespace Application.Districts.GetById; + +public sealed record GetDistrictByIdQuery(Guid Id) + : IQuery; diff --git a/src/Application/Districts/GetById/GetDistrictByIdQueryHandler.cs b/src/Application/Districts/GetById/GetDistrictByIdQueryHandler.cs new file mode 100644 index 0000000..c157a6f --- /dev/null +++ b/src/Application/Districts/GetById/GetDistrictByIdQueryHandler.cs @@ -0,0 +1,46 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Application.Districts.Get; +using Domain.Districts; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Districts.GetById; + +internal sealed class GetDistrictByIdQueryHandler + : IQueryHandler +{ + private readonly IApplicationDbContext _context; + + public GetDistrictByIdQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task> Handle( + GetDistrictByIdQuery query, + CancellationToken cancellationToken) + { + District? district = await _context.Districts + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == query.Id, cancellationToken); + + if (district is null) + { + return Result.Failure(DistrictErrors.NotFound(query.Id)); + } + + var response = new DistrictResponse + { + Id = district.Id, + CountryId = district.CountryId, + RegionId = district.RegionId, + Name = district.Name, + IsActive = district.IsActive, + CreatedAt = district.CreatedAt, + UpdatedAt = district.UpdatedAt + }; + + return response; + } +} diff --git a/src/Application/Districts/Update/UpdateDistrictCommand.cs b/src/Application/Districts/Update/UpdateDistrictCommand.cs new file mode 100644 index 0000000..08cfc3f --- /dev/null +++ b/src/Application/Districts/Update/UpdateDistrictCommand.cs @@ -0,0 +1,12 @@ +using System; +using Application.Abstractions.Messaging; + +namespace Application.Districts.Update; + +public sealed record UpdateDistrictCommand( + Guid DistrictId, + Guid RegionId, + Guid CountryId, + string Name, + bool IsActive +) : ICommand; diff --git a/src/Application/Districts/Update/UpdateDistrictCommandHandler.cs b/src/Application/Districts/Update/UpdateDistrictCommandHandler.cs new file mode 100644 index 0000000..fb5dab4 --- /dev/null +++ b/src/Application/Districts/Update/UpdateDistrictCommandHandler.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Domain.Districts; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Districts.Update; + +internal sealed class UpdateDistrictCommandHandler( + IApplicationDbContext context +) : ICommandHandler +{ + public async Task Handle(UpdateDistrictCommand command, CancellationToken cancellationToken) + { + District? district = await context.Districts + .SingleOrDefaultAsync(d => d.Id == command.DistrictId, cancellationToken); + + if (district is null) + { + return Result.Failure(Error.NotFound( + "District.NotFound", + $"District with Id {command.DistrictId} not found.")); + } + + // Update fields + district.RegionId = command.RegionId; + district.CountryId = command.CountryId; + district.Name = command.Name; + district.IsActive = command.IsActive; + district.UpdatedAt = DateTime.UtcNow; + + await context.SaveChangesAsync(cancellationToken); + + return Result.Success(); + } +} diff --git a/src/Application/Districts/Update/UpdateDistrictValidator.cs b/src/Application/Districts/Update/UpdateDistrictValidator.cs new file mode 100644 index 0000000..8460ad7 --- /dev/null +++ b/src/Application/Districts/Update/UpdateDistrictValidator.cs @@ -0,0 +1,27 @@ +using FluentValidation; + +namespace Application.Districts.Update; + +public sealed class UpdateDistrictValidator : AbstractValidator +{ + public UpdateDistrictValidator() + { + RuleFor(d => d.DistrictId) + .NotEmpty() + .WithMessage("DistrictId is required."); + + RuleFor(d => d.RegionId) + .NotEmpty() + .WithMessage("RegionId is required."); + + RuleFor(d => d.Name) + .NotEmpty() + .WithMessage("District Name is required.") + .MaximumLength(255) + .WithMessage("District Name cannot exceed 255 characters."); + + RuleFor(d => d.IsActive) + .NotNull() + .WithMessage("IsActive value is required."); + } +} diff --git a/src/Application/Localities/Create/CreateLocalityCommand.cs b/src/Application/Localities/Create/CreateLocalityCommand.cs new file mode 100644 index 0000000..df4b7ae --- /dev/null +++ b/src/Application/Localities/Create/CreateLocalityCommand.cs @@ -0,0 +1,12 @@ +using Application.Abstractions.Messaging; +using Domain.Localities; + +namespace Application.Localities.Create; + +public sealed record CreateLocalityCommand( + Guid CountryId, + Guid AreaId, + string Name, + Locality.LocalityType Type, + bool IsActive +) : ICommand; diff --git a/src/Application/Localities/Create/CreateLocalityCommandHandler .cs b/src/Application/Localities/Create/CreateLocalityCommandHandler .cs new file mode 100644 index 0000000..ba9a8e0 --- /dev/null +++ b/src/Application/Localities/Create/CreateLocalityCommandHandler .cs @@ -0,0 +1,36 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Domain.Localities; +using SharedKernel; + +namespace Application.Localities.Create; + +public sealed class CreateLocalityCommandHandler + : ICommandHandler +{ + private readonly IApplicationDbContext _context; + + public CreateLocalityCommandHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task> Handle( + CreateLocalityCommand command, + CancellationToken cancellationToken) + { + var locality = new Locality + { + CountryId = command.CountryId, + AreaId = command.AreaId, + Name = command.Name, + Type = command.Type, + IsActive = command.IsActive + }; + + await _context.Localities.AddAsync(locality, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + + return Result.Success(locality.Id); + } +} diff --git a/src/Application/Localities/Create/CreateLocalityValidator .cs b/src/Application/Localities/Create/CreateLocalityValidator .cs new file mode 100644 index 0000000..37036ef --- /dev/null +++ b/src/Application/Localities/Create/CreateLocalityValidator .cs @@ -0,0 +1,32 @@ +using System; +using FluentValidation; + +namespace Application.Localities.Create; + +public class CreateLocalityValidator : AbstractValidator +{ + public CreateLocalityValidator() + { + + RuleFor(x => x.CountryId) + .NotEmpty() + .WithMessage("CountryId is required.") + .NotEqual(Guid.Empty) + .WithMessage("CountryId cannot be empty GUID."); + RuleFor(x => x.AreaId) + .NotEmpty() + .WithMessage("AreaId is required.") + .NotEqual(Guid.Empty) + .WithMessage("AreaId cannot be empty GUID."); + + RuleFor(l => l.Name) + .NotEmpty() + .WithMessage("Name is required.") + .MaximumLength(255) + .WithMessage("Name must not exceed 255 characters."); + + RuleFor(l => l.Type) + .IsInEnum() + .WithMessage("Invalid locality type."); + } +} diff --git a/src/Application/Localities/Delete/DeleteLocalityCommand.cs b/src/Application/Localities/Delete/DeleteLocalityCommand.cs new file mode 100644 index 0000000..76fea7c --- /dev/null +++ b/src/Application/Localities/Delete/DeleteLocalityCommand.cs @@ -0,0 +1,5 @@ +using Application.Abstractions.Messaging; + +namespace Application.Localities.Delete; + +public sealed record DeleteLocalityCommand(Guid Id) : ICommand; diff --git a/src/Application/Localities/Delete/DeleteLocalityCommandHandler.cs b/src/Application/Localities/Delete/DeleteLocalityCommandHandler.cs new file mode 100644 index 0000000..1ecb5b7 --- /dev/null +++ b/src/Application/Localities/Delete/DeleteLocalityCommandHandler.cs @@ -0,0 +1,34 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Domain.Localities; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Localities.Delete; + +public sealed class DeleteLocalityCommandHandler + : ICommandHandler +{ + private readonly IApplicationDbContext _context; + + public DeleteLocalityCommandHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(DeleteLocalityCommand command, CancellationToken cancellationToken) + { + Locality? locality = await _context.Localities + .FirstOrDefaultAsync(l => l.Id == command.Id, cancellationToken); + + if (locality is null) + { + return Result.Failure("Locality not found."); + } + + _context.Localities.Remove(locality); + await _context.SaveChangesAsync(cancellationToken); + + return Result.Success(); + } +} diff --git a/src/Application/Localities/Delete/DeleteLocalityValidator .cs b/src/Application/Localities/Delete/DeleteLocalityValidator .cs new file mode 100644 index 0000000..198ff98 --- /dev/null +++ b/src/Application/Localities/Delete/DeleteLocalityValidator .cs @@ -0,0 +1,15 @@ +using FluentValidation; + +namespace Application.Localities.Delete; + +public class DeleteLocalityValidator : AbstractValidator +{ + public DeleteLocalityValidator() + { + RuleFor(x => x.Id) + .NotEmpty() + .WithMessage("ID is required.") + .NotEqual(Guid.Empty) + .WithMessage("ID cannot be empty GUID."); + } +} diff --git a/src/Application/Localities/Get/GetLocalityByIdQuery.cs b/src/Application/Localities/Get/GetLocalityByIdQuery.cs new file mode 100644 index 0000000..c83acce --- /dev/null +++ b/src/Application/Localities/Get/GetLocalityByIdQuery.cs @@ -0,0 +1,5 @@ +using Application.Abstractions.Messaging; + +namespace Application.Localities.Get; + +public sealed record GetLocalityByIdQuery(Guid Id) : IQuery; diff --git a/src/Application/Localities/Get/GetLocalityByIdQueryHandler.cs b/src/Application/Localities/Get/GetLocalityByIdQueryHandler.cs new file mode 100644 index 0000000..9565e53 --- /dev/null +++ b/src/Application/Localities/Get/GetLocalityByIdQueryHandler.cs @@ -0,0 +1,42 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Localities.Get; + +public sealed class GetLocalityByIdQueryHandler + : IQueryHandler +{ + private readonly IApplicationDbContext _context; + + public GetLocalityByIdQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task> Handle( + GetLocalityByIdQuery query, + CancellationToken cancellationToken) + { + LocalityResponse? locality = await _context.Localities + .Where(l => l.Id == query.Id) + .Select(l => new LocalityResponse( + l.Id, + l.CountryId, + l.AreaId, + l.Name, + (int)l.Type, + l.Type.ToString(), + l.IsActive + )) + .FirstOrDefaultAsync(cancellationToken); + + if (locality is null) + { + return Result.Failure("Locality not found."); + } + + return Result.Success(locality); + } +} diff --git a/src/Application/Localities/Get/LocalityResponse.cs b/src/Application/Localities/Get/LocalityResponse.cs new file mode 100644 index 0000000..65b0a23 --- /dev/null +++ b/src/Application/Localities/Get/LocalityResponse.cs @@ -0,0 +1,11 @@ +namespace Application.Localities.Get; + +public sealed record LocalityResponse( + Guid Id, + Guid CountryId, + Guid AreaId, + string Name, + int Type, + string TypeName, + bool IsActive +); diff --git a/src/Application/Localities/GetAll/GetAllLocalitiesQuery.cs b/src/Application/Localities/GetAll/GetAllLocalitiesQuery.cs new file mode 100644 index 0000000..9f7cc39 --- /dev/null +++ b/src/Application/Localities/GetAll/GetAllLocalitiesQuery.cs @@ -0,0 +1,6 @@ +using Application.Abstractions.Messaging; +using Application.Localities.Get; + +namespace Application.Localities.GetAll; + +public sealed record GetAllLocalitiesQuery() : IQuery>; diff --git a/src/Application/Localities/GetAll/GetAllLocalitiesQueryHandler.cs b/src/Application/Localities/GetAll/GetAllLocalitiesQueryHandler.cs new file mode 100644 index 0000000..ef3443e --- /dev/null +++ b/src/Application/Localities/GetAll/GetAllLocalitiesQueryHandler.cs @@ -0,0 +1,37 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Application.Localities.Get; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Localities.GetAll; + +public sealed class GetAllLocalitiesQueryHandler + : IQueryHandler> +{ + private readonly IApplicationDbContext _context; + + public GetAllLocalitiesQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task>> Handle( + GetAllLocalitiesQuery query, + CancellationToken cancellationToken) + { + List localities = await _context.Localities + .Select(l => new LocalityResponse( + l.Id, + l.CountryId, + l.AreaId, + l.Name, + (int)l.Type, + l.Type.ToString(), + l.IsActive + )) + .ToListAsync(cancellationToken); + + return Result.Success(localities); + } +} diff --git a/src/Application/Localities/Update/UpdateLocalityCommand.cs b/src/Application/Localities/Update/UpdateLocalityCommand.cs new file mode 100644 index 0000000..40a4c6b --- /dev/null +++ b/src/Application/Localities/Update/UpdateLocalityCommand.cs @@ -0,0 +1,13 @@ +using Application.Abstractions.Messaging; +using Domain.Localities; + +namespace Application.Localities.Update; + +public sealed record UpdateLocalityCommand( + Guid Id, + Guid CountryId, + Guid AreaId, + string Name, + Locality.LocalityType Type, + bool IsActive +) : ICommand; diff --git a/src/Application/Localities/Update/UpdateLocalityCommandHandler.cs b/src/Application/Localities/Update/UpdateLocalityCommandHandler.cs new file mode 100644 index 0000000..a91cb2b --- /dev/null +++ b/src/Application/Localities/Update/UpdateLocalityCommandHandler.cs @@ -0,0 +1,55 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Domain.Localities; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Localities.Update; + +public sealed class UpdateLocalityCommandHandler + : ICommandHandler +{ + private readonly IApplicationDbContext _context; + + public UpdateLocalityCommandHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task> Handle( + UpdateLocalityCommand command, + CancellationToken cancellationToken) + { + Locality? locality = await _context.Localities + .FirstOrDefaultAsync(l => l.Id == command.Id, cancellationToken); + + if (locality is null) + { + return Result.Failure("Locality not found."); + } + + // Check if locality name is unique within the same area (excluding current locality) + bool localityNameExists = await _context.Localities + .AnyAsync(l => + l.AreaId == command.AreaId && + l.Name == command.Name && + l.Id != command.Id, + cancellationToken); + + if (localityNameExists) + { + return Result.Failure("Locality name already exists in this area."); + } + + // Update properties + locality.CountryId = command.CountryId; + locality.AreaId = command.AreaId; + locality.Name = command.Name; + locality.Type = command.Type; + locality.IsActive = command.IsActive; + + await _context.SaveChangesAsync(cancellationToken); + + return Result.Success(locality.Id); + } +} diff --git a/src/Application/Localities/Update/UpdateLocalityValidator .cs b/src/Application/Localities/Update/UpdateLocalityValidator .cs new file mode 100644 index 0000000..91c7c50 --- /dev/null +++ b/src/Application/Localities/Update/UpdateLocalityValidator .cs @@ -0,0 +1,35 @@ +using FluentValidation; + +namespace Application.Localities.Update; + +public class UpdateLocalityValidator : AbstractValidator +{ + public UpdateLocalityValidator() + { + + RuleFor(x => x.CountryId) + .NotEmpty() + .WithMessage("CountryId is required.") + .NotEqual(Guid.Empty) + .WithMessage("CountryId cannot be empty GUID."); + RuleFor(x => x.AreaId) + .NotEmpty() + .WithMessage("AreaId is required.") + .NotEqual(Guid.Empty) + .WithMessage("AreaId cannot be empty GUID."); + + RuleFor(l => l.Name) + .NotEmpty() + .WithMessage("Name is required.") + .MaximumLength(255) + .WithMessage("Name must not exceed 255 characters."); + + RuleFor(l => l.Type) + .IsInEnum() + .WithMessage("Invalid locality type."); + + RuleFor(l => l.IsActive) + .NotNull() + .WithMessage("IsActive is required."); + } +} diff --git a/src/Application/Regions/Create/CreateRegionCommand.cs b/src/Application/Regions/Create/CreateRegionCommand.cs new file mode 100644 index 0000000..eac1dae --- /dev/null +++ b/src/Application/Regions/Create/CreateRegionCommand.cs @@ -0,0 +1,14 @@ +using System; +using Application.Abstractions.Messaging; + +namespace Application.Regions.Create; + +public sealed class CreateRegionCommand : ICommand +{ + public Guid CountryId { get; set; } + public string Name { get; set; } + public string RegionType { get; set; } + public bool IsActive { get; set; } = true; + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/Application/Regions/Create/CreateRegionCommandHandler.cs b/src/Application/Regions/Create/CreateRegionCommandHandler.cs new file mode 100644 index 0000000..37f80f0 --- /dev/null +++ b/src/Application/Regions/Create/CreateRegionCommandHandler.cs @@ -0,0 +1,27 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Domain.Regions; +using SharedKernel; + +namespace Application.Regions.Create; + +internal sealed class CreateRegionCommandHandler(IApplicationDbContext context) + : ICommandHandler +{ + public async Task> Handle(CreateRegionCommand command, CancellationToken cancellationToken) + { + var region = new Region + { + CountryId = command.CountryId, + Name = command.Name, + RegionType = command.RegionType, + IsActive = command.IsActive, + CreatedAt = command.CreatedAt + }; + + await context.Regions.AddAsync(region, cancellationToken); + await context.SaveChangesAsync(cancellationToken); + + return Result.Success(region.Id); + } +} diff --git a/src/Application/Regions/Create/CreateRegionValidator.cs b/src/Application/Regions/Create/CreateRegionValidator.cs new file mode 100644 index 0000000..4ee667c --- /dev/null +++ b/src/Application/Regions/Create/CreateRegionValidator.cs @@ -0,0 +1,29 @@ +using FluentValidation; + +namespace Application.Regions.Create; + +public sealed class CreateRegionValidator : AbstractValidator +{ + public CreateRegionValidator() + { + RuleFor(r => r.CountryId) + .NotEmpty() + .WithMessage("CountryId is required."); + + RuleFor(r => r.Name) + .NotEmpty() + .WithMessage("Name is required.") + .MaximumLength(200) + .WithMessage("Name cannot exceed 200 characters."); + + RuleFor(r => r.RegionType) + .NotEmpty() + .WithMessage("RegionType is required.") + .MaximumLength(100) + .WithMessage("RegionType cannot exceed 100 characters."); + + RuleFor(r => r.CreatedAt) + .NotEmpty() + .WithMessage("CreatedAt is required."); + } +} diff --git a/src/Application/Regions/Delete/DeleteRegionCommand.cs b/src/Application/Regions/Delete/DeleteRegionCommand.cs new file mode 100644 index 0000000..2fbe5d7 --- /dev/null +++ b/src/Application/Regions/Delete/DeleteRegionCommand.cs @@ -0,0 +1,6 @@ +using System; +using Application.Abstractions.Messaging; + +namespace Application.Regions.Delete; + +public sealed record DeleteRegionCommand(Guid Id) : ICommand; diff --git a/src/Application/Regions/Delete/DeleteRegionCommandHandler.cs b/src/Application/Regions/Delete/DeleteRegionCommandHandler.cs new file mode 100644 index 0000000..5a31614 --- /dev/null +++ b/src/Application/Regions/Delete/DeleteRegionCommandHandler.cs @@ -0,0 +1,35 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Domain.Regions; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Regions.Delete; + +internal sealed class DeleteRegionCommandHandler + : ICommandHandler +{ + private readonly IApplicationDbContext _context; + + public DeleteRegionCommandHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(DeleteRegionCommand command, CancellationToken cancellationToken) + { + Region? region = await _context.Regions + .SingleOrDefaultAsync(x => x.Id == command.Id, cancellationToken); + + if (region is null) + { + return Result.Failure(RegionErrors.NotFound(command.Id)); + } + + _context.Regions.Remove(region); + + await _context.SaveChangesAsync(cancellationToken); + + return Result.Success(); + } +} diff --git a/src/Application/Regions/Delete/DeleteRegionCommandValidator.cs b/src/Application/Regions/Delete/DeleteRegionCommandValidator.cs new file mode 100644 index 0000000..8a4b35c --- /dev/null +++ b/src/Application/Regions/Delete/DeleteRegionCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; + +namespace Application.Regions.Delete; + +public sealed class DeleteRegionCommandValidator + : AbstractValidator +{ + public DeleteRegionCommandValidator() + { + RuleFor(x => x.Id) + .NotEmpty() + .WithMessage("Region Id is required."); + } +} diff --git a/src/Application/Regions/Get/GetRegionQuery.cs b/src/Application/Regions/Get/GetRegionQuery.cs new file mode 100644 index 0000000..e0cf505 --- /dev/null +++ b/src/Application/Regions/Get/GetRegionQuery.cs @@ -0,0 +1,6 @@ +using Application.Abstractions.Messaging; + +namespace Application.Regions.Get; + +public sealed record GetRegionQuery() + : IQuery>; diff --git a/src/Application/Regions/Get/GetRegionQueryHandler.cs b/src/Application/Regions/Get/GetRegionQueryHandler.cs new file mode 100644 index 0000000..2c33d9d --- /dev/null +++ b/src/Application/Regions/Get/GetRegionQueryHandler.cs @@ -0,0 +1,34 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Regions.Get; + +internal sealed class GetRegionQueryHandler( + IApplicationDbContext context) + : IQueryHandler> +{ + public async Task>> Handle( + GetRegionQuery query, + CancellationToken cancellationToken) + { + + + List regions = await context.Regions + .OrderByDescending(x => x.CreatedAt) + .Select(x => new RegionResponse + { + Id = x.Id, + CountryId = x.CountryId, + Name = x.Name, + RegionType = x.RegionType, + IsActive = x.IsActive, + CreatedAt = x.CreatedAt, + UpdatedAt = x.UpdatedAt + }) + .ToListAsync(cancellationToken); + + return regions; + } +} diff --git a/src/Application/Regions/Get/RegionResponse.cs b/src/Application/Regions/Get/RegionResponse.cs new file mode 100644 index 0000000..431907d --- /dev/null +++ b/src/Application/Regions/Get/RegionResponse.cs @@ -0,0 +1,14 @@ +using System; + +namespace Application.Regions.Get; + +public sealed class RegionResponse +{ + public Guid Id { get; set; } + public Guid CountryId { get; set; } + public string Name { get; set; } = string.Empty; + public string RegionType { get; set; } = string.Empty; + public bool IsActive { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } +} diff --git a/src/Application/Regions/GetById/GetRegionByIdQuery.cs b/src/Application/Regions/GetById/GetRegionByIdQuery.cs new file mode 100644 index 0000000..4161200 --- /dev/null +++ b/src/Application/Regions/GetById/GetRegionByIdQuery.cs @@ -0,0 +1,8 @@ +using System; +using Application.Abstractions.Messaging; +using Application.Regions.Get; + +namespace Application.Regions.GetById; + +public sealed record GetRegionByIdQuery(Guid Id) + : IQuery; diff --git a/src/Application/Regions/GetById/GetRegionByIdQueryHandler.cs b/src/Application/Regions/GetById/GetRegionByIdQueryHandler.cs new file mode 100644 index 0000000..b4b8b10 --- /dev/null +++ b/src/Application/Regions/GetById/GetRegionByIdQueryHandler.cs @@ -0,0 +1,46 @@ +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Application.Regions.Get; +using Domain.Regions; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Regions.GetById; + +internal sealed class GetRegionByIdQueryHandler + : IQueryHandler +{ + private readonly IApplicationDbContext _context; + + public GetRegionByIdQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task> Handle( + GetRegionByIdQuery query, + CancellationToken cancellationToken) + { + Region? region = await _context.Regions + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == query.Id, cancellationToken); + + if (region is null) + { + return Result.Failure(RegionErrors.NotFound(query.Id)); + } + + var response = new RegionResponse + { + Id = region.Id, + CountryId = region.CountryId, + Name = region.Name, + RegionType = region.RegionType, + IsActive = region.IsActive, + CreatedAt = region.CreatedAt, + UpdatedAt = region.UpdatedAt + }; + + return response; + } +} diff --git a/src/Application/Regions/Update/UpdateRegionCommand.cs b/src/Application/Regions/Update/UpdateRegionCommand.cs new file mode 100644 index 0000000..a994cdc --- /dev/null +++ b/src/Application/Regions/Update/UpdateRegionCommand.cs @@ -0,0 +1,12 @@ +using System; +using Application.Abstractions.Messaging; + +namespace Application.Regions.Update; + +public sealed record UpdateRegionCommand( + Guid RegionId, + Guid CountryId, + string Name, + string RegionType, + bool IsActive +) : ICommand; diff --git a/src/Application/Regions/Update/UpdateRegionCommandHandler.cs b/src/Application/Regions/Update/UpdateRegionCommandHandler.cs new file mode 100644 index 0000000..8c335a3 --- /dev/null +++ b/src/Application/Regions/Update/UpdateRegionCommandHandler.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Application.Abstractions.Data; +using Application.Abstractions.Messaging; +using Domain.Regions; +using Microsoft.EntityFrameworkCore; +using SharedKernel; + +namespace Application.Regions.Update; + +internal sealed class UpdateRegionCommandHandler( + IApplicationDbContext context +) : ICommandHandler +{ + public async Task Handle(UpdateRegionCommand command, CancellationToken cancellationToken) + { + Region? region = await context.Regions + .SingleOrDefaultAsync(r => r.Id == command.RegionId, cancellationToken); + + if (region is null) + { + return Result.Failure(Error.NotFound( + "Region.NotFound", + $"Region with Id {command.RegionId} not found.")); + } + + // Update fields + region.CountryId = command.CountryId; + region.Name = command.Name; + region.RegionType = command.RegionType; + region.IsActive = command.IsActive; + region.UpdatedAt = DateTime.UtcNow; + + await context.SaveChangesAsync(cancellationToken); + + return Result.Success(); + } +} diff --git a/src/Application/Regions/Update/UpdateRegionValidator.cs b/src/Application/Regions/Update/UpdateRegionValidator.cs new file mode 100644 index 0000000..c91a492 --- /dev/null +++ b/src/Application/Regions/Update/UpdateRegionValidator.cs @@ -0,0 +1,33 @@ +using FluentValidation; + +namespace Application.Regions.Update; + +public sealed class UpdateRegionValidator : AbstractValidator +{ + public UpdateRegionValidator() + { + RuleFor(r => r.RegionId) + .NotEmpty() + .WithMessage("RegionId is required."); + + RuleFor(r => r.CountryId) + .NotEmpty() + .WithMessage("CountryId is required."); + + RuleFor(r => r.Name) + .NotEmpty() + .WithMessage("Region Name is required.") + .MaximumLength(255) + .WithMessage("Region Name cannot exceed 255 characters."); + + RuleFor(r => r.RegionType) + .NotEmpty() + .WithMessage("RegionType is required.") + .MaximumLength(100) + .WithMessage("RegionType cannot exceed 100 characters."); + + RuleFor(r => r.IsActive) + .NotNull() + .WithMessage("IsActive value is required."); + } +} diff --git a/src/Domain/Areas/Area.cs b/src/Domain/Areas/Area.cs new file mode 100644 index 0000000..b6e6e02 --- /dev/null +++ b/src/Domain/Areas/Area.cs @@ -0,0 +1,25 @@ +namespace Domain.Areas; + +public sealed class Area +{ + public Guid Id { get; set; } + + public Guid CountryId { get; set; } + + public Guid DistrictId { get; set; } + + public string Name { get; set; } = string.Empty; + + public AreaType Type { get; set; } + + public bool IsActive { get; set; } + + public enum AreaType + { + Upazila = 1, + City = 2, + Thana = 3, + Municipality = 4, + Township = 5 + } +} diff --git a/src/Domain/AuditLogs/AuditLog.cs b/src/Domain/AuditLogs/AuditLog.cs index b9f0ad3..4d25cc2 100644 --- a/src/Domain/AuditLogs/AuditLog.cs +++ b/src/Domain/AuditLogs/AuditLog.cs @@ -1,5 +1,4 @@ -using System; -using Domain.Users; +using Domain.Users; using SharedKernel; namespace Domain.AuditLogs; diff --git a/src/Domain/Districts/District.cs b/src/Domain/Districts/District.cs new file mode 100644 index 0000000..30abe21 --- /dev/null +++ b/src/Domain/Districts/District.cs @@ -0,0 +1,20 @@ +using System; +using Domain.Countries; +using Domain.Regions; +using SharedKernel; + +namespace Domain.Districts; + +public class District : Entity +{ + public Guid Id { get; set; } + public Guid CountryId { get; set; } + public Guid RegionId { get; set; } + + public string Name { get; set; } + public bool IsActive { get; set; } + public Country Country { get; set; } + public Region Region { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } +} diff --git a/src/Domain/Districts/DistrictErrors.cs b/src/Domain/Districts/DistrictErrors.cs new file mode 100644 index 0000000..5979590 --- /dev/null +++ b/src/Domain/Districts/DistrictErrors.cs @@ -0,0 +1,13 @@ +using System; +using SharedKernel; + +namespace Domain.Districts; + +public static class DistrictErrors +{ + public static Error NotFound(Guid id) => + new Error( + "District.NotFound", + $"District with Id '{id}' was not found.", + ErrorType.NotFound); +} diff --git a/src/Domain/Localities/Locality.cs b/src/Domain/Localities/Locality.cs new file mode 100644 index 0000000..f15fb73 --- /dev/null +++ b/src/Domain/Localities/Locality.cs @@ -0,0 +1,28 @@ +namespace Domain.Localities; + +public sealed class Locality +{ + public Guid Id { get; set; } + + public Guid CountryId { get; set; } + + public Guid AreaId { get; set; } + + public string Name { get; set; } = string.Empty; + + public LocalityType Type { get; set; } + + public bool IsActive { get; set; } + + public enum LocalityType + { + Union = 1, + Ward = 2, + Neighborhood = 3, + Village = 4, + Town = 5, + Parish = 6, + Suburb = 7, + Hamlet = 8 + } +} diff --git a/src/Domain/Regions/Region.cs b/src/Domain/Regions/Region.cs new file mode 100644 index 0000000..86af95f --- /dev/null +++ b/src/Domain/Regions/Region.cs @@ -0,0 +1,18 @@ +using System; +using Domain.Countries; +using SharedKernel; + +namespace Domain.Regions; + +public class Region : Entity +{ + public Guid Id { get; set; } + public Guid CountryId { get; set; } + + public string Name { get; set; } + public string RegionType { get; set; } + public bool IsActive { get; set; } + public Country Country { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } +} diff --git a/src/Domain/Regions/RegionErrors.cs b/src/Domain/Regions/RegionErrors.cs new file mode 100644 index 0000000..e867e63 --- /dev/null +++ b/src/Domain/Regions/RegionErrors.cs @@ -0,0 +1,13 @@ +using System; +using SharedKernel; + +namespace Domain.Regions; + +public static class RegionErrors +{ + public static Error NotFound(Guid id) => + new Error( + "Region.NotFound", + $"Region with Id '{id}' was not found.", + ErrorType.NotFound); +} diff --git a/src/Infrastructure/Areas/AreaConfiguration .cs b/src/Infrastructure/Areas/AreaConfiguration .cs new file mode 100644 index 0000000..4f02f1d --- /dev/null +++ b/src/Infrastructure/Areas/AreaConfiguration .cs @@ -0,0 +1,48 @@ +using Domain; +using Domain.Areas; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Areas; + +internal sealed class AreaConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + + builder.HasKey(a => a.Id); + + builder.Property(a => a.Id) + .ValueGeneratedOnAdd(); + + builder.Property(a => a.Name) + .IsRequired() + .HasMaxLength(255); + + builder.Property(a => a.CountryId) + .IsRequired(); + + builder.Property(a => a.DistrictId) + .IsRequired(); + + builder.Property(a => a.Type) + .IsRequired() + .HasConversion(); + + builder.Property(a => a.IsActive) + .IsRequired() + .HasDefaultValue(true); + + // Add indexes + builder.HasIndex(a => a.DistrictId); + builder.HasIndex(a => a.CountryId); + builder.HasIndex(a => a.Name); + builder.HasIndex(a => a.Type); + + // Foreign key constraint (assuming districts table exists) + builder.HasOne() + .WithMany() + .HasForeignKey(a => a.DistrictId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/src/Infrastructure/Database/ApplicationDbContext.cs b/src/Infrastructure/Database/ApplicationDbContext.cs index 349f4bf..8392188 100644 --- a/src/Infrastructure/Database/ApplicationDbContext.cs +++ b/src/Infrastructure/Database/ApplicationDbContext.cs @@ -1,16 +1,20 @@ using Application.Abstractions.Data; using Domain.Applications; +using Domain.Areas; using Domain.AuditLogs; using Domain.Businesses; using Domain.BusinessMembers; using Domain.Countries; using Domain.Customers; +using Domain.Districts; using Domain.EmailVerification; +using Domain.Localities; using Domain.MfaLogs; using Domain.MfaSettings; using Domain.Otps; using Domain.PasswordResets; using Domain.Permissions; +using Domain.Regions; using Domain.RolePermissions; using Domain.Roles; using Domain.SmtpConfigs; @@ -64,7 +68,10 @@ public sealed class ApplicationDbContext( public DbSet Otp { get; set; } public DbSet SmtpConfig { get; set; } public DbSet Countries { get; set; } - + public DbSet Regions { get; set; } + public DbSet Districts { get; set; } + public DbSet Areas { get; set; } + public DbSet Localities { get; set; } public new EntityEntry Entry(object entity) => base.Entry(entity); protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/src/Infrastructure/Database/Migrations/20251209091628_AddDistricts.Designer.cs b/src/Infrastructure/Database/Migrations/20251209091628_AddDistricts.Designer.cs new file mode 100644 index 0000000..f5f5e49 --- /dev/null +++ b/src/Infrastructure/Database/Migrations/20251209091628_AddDistricts.Designer.cs @@ -0,0 +1,1113 @@ +// +using System; +using System.Collections.Generic; +using Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251209091628_AddDistricts")] + partial class AddDistricts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("public") + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Applications.Applicationapply", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApiBaseUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("api_base_url"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("client_secret"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RedirectUri") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("redirect_uri"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_applications"); + + b.HasIndex("ClientId") + .IsUnique() + .HasDatabaseName("ix_applications_client_id"); + + b.ToTable("applications", "public"); + }); + + modelBuilder.Entity("Domain.AuditLogs.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("action"); + + b.Property("BusinessId") + .HasColumnType("uuid") + .HasColumnName("business_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_logs"); + + b.HasIndex("BusinessId") + .HasDatabaseName("ix_audit_logs_business_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_audit_logs_user_id"); + + b.ToTable("audit_logs", "public"); + }); + + modelBuilder.Entity("Domain.BusinessMembers.BusinessMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("BusinessId") + .HasColumnType("uuid") + .HasColumnName("business_id"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_at"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_business_members"); + + b.HasIndex("BusinessId") + .HasDatabaseName("ix_business_members_business_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_business_members_role_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_business_members_user_id"); + + b.ToTable("business_members", "public"); + }); + + modelBuilder.Entity("Domain.Businesses.Business", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("BusinessName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("business_name"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("IndustryType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("industry_type"); + + b.Property("LogoUrl") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("logo_url"); + + b.Property("OwnerUserId") + .HasColumnType("uuid") + .HasColumnName("owner_user_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_businesses"); + + b.HasIndex("OwnerUserId") + .HasDatabaseName("ix_businesses_owner_user_id"); + + b.ToTable("businesses", "public"); + }); + + modelBuilder.Entity("Domain.Countries.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Capital") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("capital"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("name"); + + b.Property("PhoneCode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("phone_code"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.ToTable("countries", "public"); + }); + + modelBuilder.Entity("Domain.Customers.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("text") + .HasColumnName("address"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_customers"); + + b.ToTable("customers", "public"); + }); + + modelBuilder.Entity("Domain.Districts.District", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CountryId") + .HasColumnType("uuid") + .HasColumnName("country_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("NOW()"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_districts"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_districts_country_id"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_districts_region_id"); + + b.ToTable("districts", "public"); + }); + + modelBuilder.Entity("Domain.EmailVerification.EmailVerifications", b => + { + b.Property("EvId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("ev_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("token"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_at"); + + b.HasKey("EvId") + .HasName("pk_email_verifications"); + + b.ToTable("email_verifications", "public"); + }); + + modelBuilder.Entity("Domain.MfaLogs.MfaLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("NOW()"); + + b.Property("Device") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("device"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("ip_address"); + + b.Property("LoginTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("login_time"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_mfa_logs"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_mfa_logs_user_id"); + + b.ToTable("mfa_logs", "public"); + }); + + modelBuilder.Entity("Domain.MfaSettings.MfaSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("BackupCodes") + .HasColumnType("text") + .HasColumnName("backup_codes"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("enabled"); + + b.Property("Method") + .HasColumnType("integer") + .HasColumnName("method"); + + b.Property("SecretKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("secret_key"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_mfa_settings"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_mfa_settings_user_id"); + + b.ToTable("mfa_settings", "public"); + }); + + modelBuilder.Entity("Domain.Otps.Otp", b => + { + b.Property("OtpId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("otp_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Delay") + .HasColumnType("interval") + .HasColumnName("delay"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("IsExpired") + .HasColumnType("boolean") + .HasColumnName("is_expired"); + + b.Property("OtpToken") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("otp_token"); + + b.HasKey("OtpId") + .HasName("pk_otp"); + + b.ToTable("otp", "public"); + }); + + modelBuilder.Entity("Domain.PasswordResets.PasswordReset", b => + { + b.Property("PrId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("pr_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("token"); + + b.Property("Used") + .HasColumnType("boolean") + .HasColumnName("used"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("PrId") + .HasName("pk_password_reset"); + + b.ToTable("password_reset", "public"); + }); + + modelBuilder.Entity("Domain.Permissions.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.HasKey("Id") + .HasName("pk_permissions"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_permissions_code"); + + b.ToTable("permissions", "public"); + }); + + modelBuilder.Entity("Domain.Regions.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CountryId") + .HasColumnType("uuid") + .HasColumnName("country_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("NOW()"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RegionType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("region_type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "public"); + }); + + modelBuilder.Entity("Domain.RolePermissions.RolePermission", b => + { + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.Property("PermissionId") + .HasColumnType("uuid") + .HasColumnName("permission_id"); + + b.HasKey("RoleId", "PermissionId") + .HasName("pk_role_permissions"); + + b.HasIndex("PermissionId") + .HasDatabaseName("ix_role_permissions_permission_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_role_permissions_role_id"); + + b.ToTable("role_permissions", "public"); + }); + + modelBuilder.Entity("Domain.Roles.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("RoleName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("RoleName") + .IsUnique() + .HasDatabaseName("ix_roles_role_name"); + + b.ToTable("roles", "public"); + }); + + modelBuilder.Entity("Domain.SmtpConfigs.SmtpConfig", b => + { + b.Property("SmtpId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("smtp_id"); + + b.Property("EnableSsl") + .HasColumnType("boolean") + .HasColumnName("enable_ssl"); + + b.Property("Host") + .IsRequired() + .HasColumnType("text") + .HasColumnName("host"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("password"); + + b.Property("Port") + .HasColumnType("integer") + .HasColumnName("port"); + + b.Property("SenderEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("sender_email"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("username"); + + b.HasKey("SmtpId") + .HasName("pk_smtp_config"); + + b.ToTable("smtp_config", "public"); + }); + + modelBuilder.Entity("Domain.Todos.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("due_date"); + + b.Property("IsCompleted") + .HasColumnType("boolean") + .HasColumnName("is_completed"); + + b.PrimitiveCollection>("Labels") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("labels"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_todo_items"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_todo_items_user_id"); + + b.ToTable("todo_items", "public"); + }); + + modelBuilder.Entity("Domain.Token.Tokens", b => + { + b.Property("TokenId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("token_id"); + + b.Property("Accesstoken") + .IsRequired() + .HasColumnType("text") + .HasColumnName("accesstoken"); + + b.Property("AppId") + .HasColumnType("uuid") + .HasColumnName("app_id"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("issued_at"); + + b.Property("Refreshtoken") + .IsRequired() + .HasColumnType("text") + .HasColumnName("refreshtoken"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("TokenId") + .HasName("pk_tokens"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tokens_user_id"); + + b.ToTable("tokens", "public"); + }); + + modelBuilder.Entity("Domain.UserLoginHistories.UserLoginHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Browser") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("browser"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("city"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("country"); + + b.Property("Device") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("device"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("ip_address"); + + b.Property("LogInTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("log_in_time"); + + b.Property("LogoutTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("logout_time"); + + b.Property("OS") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("os"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("status"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_login_history"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_login_history_user_id"); + + b.ToTable("user_login_history", "public"); + }); + + modelBuilder.Entity("Domain.UserProfiles.UserProfile", b => + { + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("address"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("city"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("country"); + + b.Property("DateOfBirth") + .HasColumnType("date") + .HasColumnName("date_of_birth"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("postal_code"); + + b.Property("ProfileImageUrl") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("profile_image_url"); + + b.HasKey("UserId") + .HasName("pk_user_profile"); + + b.ToTable("user_profile", "public"); + }); + + modelBuilder.Entity("Domain.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("email"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("full_name"); + + b.Property("IsEmailVerified") + .HasColumnType("boolean") + .HasColumnName("is_email_verified"); + + b.Property("IsMFAEnabled") + .HasColumnType("boolean") + .HasColumnName("is_mfa_enabled"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("password_hash"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_users_email"); + + b.ToTable("users", "public"); + }); + + modelBuilder.Entity("Domain.AuditLogs.AuditLog", b => + { + b.HasOne("Domain.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audit_logs_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.BusinessMembers.BusinessMember", b => + { + b.HasOne("Domain.Businesses.Business", null) + .WithMany() + .HasForeignKey("BusinessId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_business_members_businesses_business_id"); + + b.HasOne("Domain.Roles.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_business_members_roles_role_id"); + + b.HasOne("Domain.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_business_members_users_user_id"); + }); + + modelBuilder.Entity("Domain.Businesses.Business", b => + { + b.HasOne("Domain.Users.User", null) + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_businesses_users_owner_user_id"); + }); + + modelBuilder.Entity("Domain.Districts.District", b => + { + b.HasOne("Domain.Countries.Country", "Country") + .WithMany() + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_districts_countries_country_id"); + + b.HasOne("Domain.Regions.Region", "Region") + .WithMany() + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_districts_regions_region_id"); + + b.Navigation("Country"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Domain.MfaSettings.MfaSetting", b => + { + b.HasOne("Domain.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_mfa_settings_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Regions.Region", b => + { + b.HasOne("Domain.Countries.Country", "Country") + .WithMany() + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_countries_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("Domain.RolePermissions.RolePermission", b => + { + b.HasOne("Domain.Permissions.Permission", "Permission") + .WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_role_permissions_permissions_permission_id"); + + b.HasOne("Domain.Roles.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_role_permissions_roles_role_id"); + + b.Navigation("Permission"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Domain.Todos.TodoItem", b => + { + b.HasOne("Domain.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_todo_items_users_user_id"); + }); + + modelBuilder.Entity("Domain.Token.Tokens", b => + { + b.HasOne("Domain.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_users_user_id"); + }); + + modelBuilder.Entity("Domain.UserLoginHistories.UserLoginHistory", b => + { + b.HasOne("Domain.Users.User", "User") + .WithMany("LoginHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_login_history_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.UserProfiles.UserProfile", b => + { + b.HasOne("Domain.Users.User", "User") + .WithOne("Profile") + .HasForeignKey("Domain.UserProfiles.UserProfile", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Users.User", b => + { + b.Navigation("LoginHistories"); + + b.Navigation("Profile"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Database/Migrations/20251209091628_AddDistricts.cs b/src/Infrastructure/Database/Migrations/20251209091628_AddDistricts.cs new file mode 100644 index 0000000..76e3ce9 --- /dev/null +++ b/src/Infrastructure/Database/Migrations/20251209091628_AddDistricts.cs @@ -0,0 +1,102 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + /// + public partial class AddDistricts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "regions", + schema: "public", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + country_id = table.Column(type: "uuid", nullable: false), + name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + region_type = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + is_active = table.Column(type: "boolean", nullable: false, defaultValue: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_regions", x => x.id); + table.ForeignKey( + name: "fk_regions_countries_country_id", + column: x => x.country_id, + principalSchema: "public", + principalTable: "countries", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "districts", + schema: "public", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + country_id = table.Column(type: "uuid", nullable: false), + region_id = table.Column(type: "uuid", nullable: false), + name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + is_active = table.Column(type: "boolean", nullable: false, defaultValue: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()"), + updated_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_districts", x => x.id); + table.ForeignKey( + name: "fk_districts_countries_country_id", + column: x => x.country_id, + principalSchema: "public", + principalTable: "countries", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_districts_regions_region_id", + column: x => x.region_id, + principalSchema: "public", + principalTable: "regions", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_districts_country_id", + schema: "public", + table: "districts", + column: "country_id"); + + migrationBuilder.CreateIndex( + name: "ix_districts_region_id", + schema: "public", + table: "districts", + column: "region_id"); + + migrationBuilder.CreateIndex( + name: "ix_regions_country_id", + schema: "public", + table: "regions", + column: "country_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "districts", + schema: "public"); + + migrationBuilder.DropTable( + name: "regions", + schema: "public"); + } + } +} diff --git a/src/Infrastructure/Database/Migrations/20251209123317_InitialDbArea.Designer.cs b/src/Infrastructure/Database/Migrations/20251209123317_InitialDbArea.Designer.cs new file mode 100644 index 0000000..78b0331 --- /dev/null +++ b/src/Infrastructure/Database/Migrations/20251209123317_InitialDbArea.Designer.cs @@ -0,0 +1,1247 @@ +// +using System; +using System.Collections.Generic; +using Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251209123317_InitialDbArea")] + partial class InitialDbArea + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("public") + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Applications.Applicationapply", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApiBaseUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("api_base_url"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("client_secret"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RedirectUri") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("redirect_uri"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_applications"); + + b.HasIndex("ClientId") + .IsUnique() + .HasDatabaseName("ix_applications_client_id"); + + b.ToTable("applications", "public"); + }); + + modelBuilder.Entity("Domain.Areas.Area", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CountryId") + .HasColumnType("uuid") + .HasColumnName("country_id"); + + b.Property("DistrictId") + .HasColumnType("uuid") + .HasColumnName("district_id"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("name"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_areas"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_areas_country_id"); + + b.HasIndex("DistrictId") + .HasDatabaseName("ix_areas_district_id"); + + b.HasIndex("Name") + .HasDatabaseName("ix_areas_name"); + + b.HasIndex("Type") + .HasDatabaseName("ix_areas_type"); + + b.ToTable("areas", "public"); + }); + + modelBuilder.Entity("Domain.AuditLogs.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("action"); + + b.Property("BusinessId") + .HasColumnType("uuid") + .HasColumnName("business_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_logs"); + + b.HasIndex("BusinessId") + .HasDatabaseName("ix_audit_logs_business_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_audit_logs_user_id"); + + b.ToTable("audit_logs", "public"); + }); + + modelBuilder.Entity("Domain.BusinessMembers.BusinessMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("BusinessId") + .HasColumnType("uuid") + .HasColumnName("business_id"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_at"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_business_members"); + + b.HasIndex("BusinessId") + .HasDatabaseName("ix_business_members_business_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_business_members_role_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_business_members_user_id"); + + b.ToTable("business_members", "public"); + }); + + modelBuilder.Entity("Domain.Businesses.Business", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("BusinessName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("business_name"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("IndustryType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("industry_type"); + + b.Property("LogoUrl") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("logo_url"); + + b.Property("OwnerUserId") + .HasColumnType("uuid") + .HasColumnName("owner_user_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_businesses"); + + b.HasIndex("OwnerUserId") + .HasDatabaseName("ix_businesses_owner_user_id"); + + b.ToTable("businesses", "public"); + }); + + modelBuilder.Entity("Domain.Countries.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Capital") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("capital"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("name"); + + b.Property("PhoneCode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("phone_code"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.ToTable("countries", "public"); + }); + + modelBuilder.Entity("Domain.Customers.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("text") + .HasColumnName("address"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_customers"); + + b.ToTable("customers", "public"); + }); + + modelBuilder.Entity("Domain.Districts.District", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CountryId") + .HasColumnType("uuid") + .HasColumnName("country_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("NOW()"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_districts"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_districts_country_id"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_districts_region_id"); + + b.ToTable("districts", "public"); + }); + + modelBuilder.Entity("Domain.EmailVerification.EmailVerifications", b => + { + b.Property("EvId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("ev_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("token"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_at"); + + b.HasKey("EvId") + .HasName("pk_email_verifications"); + + b.ToTable("email_verifications", "public"); + }); + + modelBuilder.Entity("Domain.Localities.Locality", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AreaId") + .HasColumnType("uuid") + .HasColumnName("area_id"); + + b.Property("CountryId") + .HasColumnType("uuid") + .HasColumnName("country_id"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("name"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_localities"); + + b.HasIndex("AreaId") + .HasDatabaseName("ix_localities_area_id"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_localities_country_id"); + + b.HasIndex("IsActive") + .HasDatabaseName("ix_localities_is_active"); + + b.HasIndex("Name") + .HasDatabaseName("ix_localities_name"); + + b.HasIndex("Type") + .HasDatabaseName("ix_localities_type"); + + b.HasIndex("AreaId", "IsActive") + .HasDatabaseName("ix_localities_area_id_is_active"); + + b.HasIndex("AreaId", "Name") + .IsUnique() + .HasDatabaseName("ix_localities_area_id_name"); + + b.HasIndex("AreaId", "Type", "IsActive") + .HasDatabaseName("ix_localities_area_id_type_is_active"); + + b.HasIndex("CountryId", "AreaId", "IsActive") + .HasDatabaseName("ix_localities_country_id_area_id_is_active"); + + b.ToTable("localities", "public"); + }); + + modelBuilder.Entity("Domain.MfaLogs.MfaLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("NOW()"); + + b.Property("Device") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("device"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("ip_address"); + + b.Property("LoginTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("login_time"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_mfa_logs"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_mfa_logs_user_id"); + + b.ToTable("mfa_logs", "public"); + }); + + modelBuilder.Entity("Domain.MfaSettings.MfaSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("BackupCodes") + .HasColumnType("text") + .HasColumnName("backup_codes"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("enabled"); + + b.Property("Method") + .HasColumnType("integer") + .HasColumnName("method"); + + b.Property("SecretKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("secret_key"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_mfa_settings"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_mfa_settings_user_id"); + + b.ToTable("mfa_settings", "public"); + }); + + modelBuilder.Entity("Domain.Otps.Otp", b => + { + b.Property("OtpId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("otp_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Delay") + .HasColumnType("interval") + .HasColumnName("delay"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("IsExpired") + .HasColumnType("boolean") + .HasColumnName("is_expired"); + + b.Property("OtpToken") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("otp_token"); + + b.HasKey("OtpId") + .HasName("pk_otp"); + + b.ToTable("otp", "public"); + }); + + modelBuilder.Entity("Domain.PasswordResets.PasswordReset", b => + { + b.Property("PrId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("pr_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("token"); + + b.Property("Used") + .HasColumnType("boolean") + .HasColumnName("used"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("PrId") + .HasName("pk_password_reset"); + + b.ToTable("password_reset", "public"); + }); + + modelBuilder.Entity("Domain.Permissions.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("code"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.HasKey("Id") + .HasName("pk_permissions"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_permissions_code"); + + b.ToTable("permissions", "public"); + }); + + modelBuilder.Entity("Domain.Regions.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CountryId") + .HasColumnType("uuid") + .HasColumnName("country_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("NOW()"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RegionType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("region_type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "public"); + }); + + modelBuilder.Entity("Domain.RolePermissions.RolePermission", b => + { + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.Property("PermissionId") + .HasColumnType("uuid") + .HasColumnName("permission_id"); + + b.HasKey("RoleId", "PermissionId") + .HasName("pk_role_permissions"); + + b.HasIndex("PermissionId") + .HasDatabaseName("ix_role_permissions_permission_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_role_permissions_role_id"); + + b.ToTable("role_permissions", "public"); + }); + + modelBuilder.Entity("Domain.Roles.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("RoleName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("RoleName") + .IsUnique() + .HasDatabaseName("ix_roles_role_name"); + + b.ToTable("roles", "public"); + }); + + modelBuilder.Entity("Domain.SmtpConfigs.SmtpConfig", b => + { + b.Property("SmtpId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("smtp_id"); + + b.Property("EnableSsl") + .HasColumnType("boolean") + .HasColumnName("enable_ssl"); + + b.Property("Host") + .IsRequired() + .HasColumnType("text") + .HasColumnName("host"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("password"); + + b.Property("Port") + .HasColumnType("integer") + .HasColumnName("port"); + + b.Property("SenderEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("sender_email"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("username"); + + b.HasKey("SmtpId") + .HasName("pk_smtp_config"); + + b.ToTable("smtp_config", "public"); + }); + + modelBuilder.Entity("Domain.Todos.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("due_date"); + + b.Property("IsCompleted") + .HasColumnType("boolean") + .HasColumnName("is_completed"); + + b.PrimitiveCollection>("Labels") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("labels"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_todo_items"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_todo_items_user_id"); + + b.ToTable("todo_items", "public"); + }); + + modelBuilder.Entity("Domain.Token.Tokens", b => + { + b.Property("TokenId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("token_id"); + + b.Property("Accesstoken") + .IsRequired() + .HasColumnType("text") + .HasColumnName("accesstoken"); + + b.Property("AppId") + .HasColumnType("uuid") + .HasColumnName("app_id"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("issued_at"); + + b.Property("Refreshtoken") + .IsRequired() + .HasColumnType("text") + .HasColumnName("refreshtoken"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("TokenId") + .HasName("pk_tokens"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tokens_user_id"); + + b.ToTable("tokens", "public"); + }); + + modelBuilder.Entity("Domain.UserLoginHistories.UserLoginHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Browser") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("browser"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("city"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("country"); + + b.Property("Device") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("device"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("ip_address"); + + b.Property("LogInTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("log_in_time"); + + b.Property("LogoutTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("logout_time"); + + b.Property("OS") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("os"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("status"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_login_history"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_login_history_user_id"); + + b.ToTable("user_login_history", "public"); + }); + + modelBuilder.Entity("Domain.UserProfiles.UserProfile", b => + { + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("address"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("city"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("country"); + + b.Property("DateOfBirth") + .HasColumnType("date") + .HasColumnName("date_of_birth"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("postal_code"); + + b.Property("ProfileImageUrl") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("profile_image_url"); + + b.HasKey("UserId") + .HasName("pk_user_profile"); + + b.ToTable("user_profile", "public"); + }); + + modelBuilder.Entity("Domain.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("email"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("full_name"); + + b.Property("IsEmailVerified") + .HasColumnType("boolean") + .HasColumnName("is_email_verified"); + + b.Property("IsMFAEnabled") + .HasColumnType("boolean") + .HasColumnName("is_mfa_enabled"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("password_hash"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_users_email"); + + b.ToTable("users", "public"); + }); + + modelBuilder.Entity("Domain.Areas.Area", b => + { + b.HasOne("Domain.Districts.District", null) + .WithMany() + .HasForeignKey("DistrictId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_areas_districts_district_id"); + }); + + modelBuilder.Entity("Domain.AuditLogs.AuditLog", b => + { + b.HasOne("Domain.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audit_logs_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.BusinessMembers.BusinessMember", b => + { + b.HasOne("Domain.Businesses.Business", null) + .WithMany() + .HasForeignKey("BusinessId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_business_members_businesses_business_id"); + + b.HasOne("Domain.Roles.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_business_members_roles_role_id"); + + b.HasOne("Domain.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_business_members_users_user_id"); + }); + + modelBuilder.Entity("Domain.Businesses.Business", b => + { + b.HasOne("Domain.Users.User", null) + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_businesses_users_owner_user_id"); + }); + + modelBuilder.Entity("Domain.Districts.District", b => + { + b.HasOne("Domain.Countries.Country", "Country") + .WithMany() + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_districts_countries_country_id"); + + b.HasOne("Domain.Regions.Region", "Region") + .WithMany() + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_districts_regions_region_id"); + + b.Navigation("Country"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Domain.Localities.Locality", b => + { + b.HasOne("Domain.Areas.Area", null) + .WithMany() + .HasForeignKey("AreaId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_localities_areas_area_id"); + }); + + modelBuilder.Entity("Domain.MfaSettings.MfaSetting", b => + { + b.HasOne("Domain.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_mfa_settings_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Regions.Region", b => + { + b.HasOne("Domain.Countries.Country", "Country") + .WithMany() + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_countries_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("Domain.RolePermissions.RolePermission", b => + { + b.HasOne("Domain.Permissions.Permission", "Permission") + .WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_role_permissions_permissions_permission_id"); + + b.HasOne("Domain.Roles.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_role_permissions_roles_role_id"); + + b.Navigation("Permission"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Domain.Todos.TodoItem", b => + { + b.HasOne("Domain.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_todo_items_users_user_id"); + }); + + modelBuilder.Entity("Domain.Token.Tokens", b => + { + b.HasOne("Domain.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_users_user_id"); + }); + + modelBuilder.Entity("Domain.UserLoginHistories.UserLoginHistory", b => + { + b.HasOne("Domain.Users.User", "User") + .WithMany("LoginHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_login_history_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.UserProfiles.UserProfile", b => + { + b.HasOne("Domain.Users.User", "User") + .WithOne("Profile") + .HasForeignKey("Domain.UserProfiles.UserProfile", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Users.User", b => + { + b.Navigation("LoginHistories"); + + b.Navigation("Profile"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Database/Migrations/20251209123317_InitialDbArea.cs b/src/Infrastructure/Database/Migrations/20251209123317_InitialDbArea.cs new file mode 100644 index 0000000..fd5dad3 --- /dev/null +++ b/src/Infrastructure/Database/Migrations/20251209123317_InitialDbArea.cs @@ -0,0 +1,154 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + /// + public partial class InitialDbArea : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "areas", + schema: "public", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + country_id = table.Column(type: "uuid", nullable: false), + district_id = table.Column(type: "uuid", nullable: false), + name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + type = table.Column(type: "integer", nullable: false), + is_active = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("pk_areas", x => x.id); + table.ForeignKey( + name: "fk_areas_districts_district_id", + column: x => x.district_id, + principalSchema: "public", + principalTable: "districts", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "localities", + schema: "public", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + country_id = table.Column(type: "uuid", nullable: false), + area_id = table.Column(type: "uuid", nullable: false), + name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + type = table.Column(type: "integer", nullable: false), + is_active = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("pk_localities", x => x.id); + table.ForeignKey( + name: "fk_localities_areas_area_id", + column: x => x.area_id, + principalSchema: "public", + principalTable: "areas", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "ix_areas_country_id", + schema: "public", + table: "areas", + column: "country_id"); + + migrationBuilder.CreateIndex( + name: "ix_areas_district_id", + schema: "public", + table: "areas", + column: "district_id"); + + migrationBuilder.CreateIndex( + name: "ix_areas_name", + schema: "public", + table: "areas", + column: "name"); + + migrationBuilder.CreateIndex( + name: "ix_areas_type", + schema: "public", + table: "areas", + column: "type"); + + migrationBuilder.CreateIndex( + name: "ix_localities_area_id", + schema: "public", + table: "localities", + column: "area_id"); + + migrationBuilder.CreateIndex( + name: "ix_localities_area_id_is_active", + schema: "public", + table: "localities", + columns: ["area_id", "is_active"]); + + migrationBuilder.CreateIndex( + name: "ix_localities_area_id_name", + schema: "public", + table: "localities", + columns: ["area_id", "name"], + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_localities_area_id_type_is_active", + schema: "public", + table: "localities", + columns: ["area_id", "type", "is_active"]); + + migrationBuilder.CreateIndex( + name: "ix_localities_country_id", + schema: "public", + table: "localities", + column: "country_id"); + + migrationBuilder.CreateIndex( + name: "ix_localities_country_id_area_id_is_active", + schema: "public", + table: "localities", + columns: ["country_id", "area_id", "is_active"]); + + migrationBuilder.CreateIndex( + name: "ix_localities_is_active", + schema: "public", + table: "localities", + column: "is_active"); + + migrationBuilder.CreateIndex( + name: "ix_localities_name", + schema: "public", + table: "localities", + column: "name"); + + migrationBuilder.CreateIndex( + name: "ix_localities_type", + schema: "public", + table: "localities", + column: "type"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "localities", + schema: "public"); + + migrationBuilder.DropTable( + name: "areas", + schema: "public"); + } + } +} diff --git a/src/Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs index 3597ac0..e129419 100644 --- a/src/Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs @@ -75,6 +75,55 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("applications", "public"); }); + modelBuilder.Entity("Domain.Areas.Area", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CountryId") + .HasColumnType("uuid") + .HasColumnName("country_id"); + + b.Property("DistrictId") + .HasColumnType("uuid") + .HasColumnName("district_id"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("name"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_areas"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_areas_country_id"); + + b.HasIndex("DistrictId") + .HasDatabaseName("ix_areas_district_id"); + + b.HasIndex("Name") + .HasDatabaseName("ix_areas_name"); + + b.HasIndex("Type") + .HasDatabaseName("ix_areas_type"); + + b.ToTable("areas", "public"); + }); + modelBuilder.Entity("Domain.AuditLogs.AuditLog", b => { b.Property("Id") @@ -270,6 +319,55 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("customers", "public"); }); + modelBuilder.Entity("Domain.Districts.District", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CountryId") + .HasColumnType("uuid") + .HasColumnName("country_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("NOW()"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("uuid") + .HasColumnName("region_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_districts"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_districts_country_id"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_districts_region_id"); + + b.ToTable("districts", "public"); + }); + modelBuilder.Entity("Domain.EmailVerification.EmailVerifications", b => { b.Property("EvId") @@ -301,6 +399,71 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("email_verifications", "public"); }); + modelBuilder.Entity("Domain.Localities.Locality", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AreaId") + .HasColumnType("uuid") + .HasColumnName("area_id"); + + b.Property("CountryId") + .HasColumnType("uuid") + .HasColumnName("country_id"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("name"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_localities"); + + b.HasIndex("AreaId") + .HasDatabaseName("ix_localities_area_id"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_localities_country_id"); + + b.HasIndex("IsActive") + .HasDatabaseName("ix_localities_is_active"); + + b.HasIndex("Name") + .HasDatabaseName("ix_localities_name"); + + b.HasIndex("Type") + .HasDatabaseName("ix_localities_type"); + + b.HasIndex("AreaId", "IsActive") + .HasDatabaseName("ix_localities_area_id_is_active"); + + b.HasIndex("AreaId", "Name") + .IsUnique() + .HasDatabaseName("ix_localities_area_id_name"); + + b.HasIndex("AreaId", "Type", "IsActive") + .HasDatabaseName("ix_localities_area_id_type_is_active"); + + b.HasIndex("CountryId", "AreaId", "IsActive") + .HasDatabaseName("ix_localities_country_id_area_id_is_active"); + + b.ToTable("localities", "public"); + }); + modelBuilder.Entity("Domain.MfaLogs.MfaLog", b => { b.Property("Id") @@ -487,6 +650,54 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("permissions", "public"); }); + modelBuilder.Entity("Domain.Regions.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CountryId") + .HasColumnType("uuid") + .HasColumnName("country_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("NOW()"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("RegionType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("region_type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "public"); + }); + modelBuilder.Entity("Domain.RolePermissions.RolePermission", b => { b.Property("RoleId") @@ -845,6 +1056,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("users", "public"); }); + modelBuilder.Entity("Domain.Areas.Area", b => + { + b.HasOne("Domain.Districts.District", null) + .WithMany() + .HasForeignKey("DistrictId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_areas_districts_district_id"); + }); + modelBuilder.Entity("Domain.AuditLogs.AuditLog", b => { b.HasOne("Domain.Users.User", "User") @@ -891,6 +1112,37 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasConstraintName("fk_businesses_users_owner_user_id"); }); + modelBuilder.Entity("Domain.Districts.District", b => + { + b.HasOne("Domain.Countries.Country", "Country") + .WithMany() + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_districts_countries_country_id"); + + b.HasOne("Domain.Regions.Region", "Region") + .WithMany() + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_districts_regions_region_id"); + + b.Navigation("Country"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Domain.Localities.Locality", b => + { + b.HasOne("Domain.Areas.Area", null) + .WithMany() + .HasForeignKey("AreaId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_localities_areas_area_id"); + }); + modelBuilder.Entity("Domain.MfaSettings.MfaSetting", b => { b.HasOne("Domain.Users.User", "User") @@ -903,6 +1155,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Domain.Regions.Region", b => + { + b.HasOne("Domain.Countries.Country", "Country") + .WithMany() + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_countries_country_id"); + + b.Navigation("Country"); + }); + modelBuilder.Entity("Domain.RolePermissions.RolePermission", b => { b.HasOne("Domain.Permissions.Permission", "Permission") diff --git a/src/Infrastructure/Districts/DistrictConfiguration.cs b/src/Infrastructure/Districts/DistrictConfiguration.cs new file mode 100644 index 0000000..39e9774 --- /dev/null +++ b/src/Infrastructure/Districts/DistrictConfiguration.cs @@ -0,0 +1,31 @@ +using Domain.Districts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Districts; + +public class DistrictConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(d => d.Id); + builder.HasIndex(d => d.CountryId); + builder.HasIndex(d => d.RegionId); + + builder.Property(d => d.Id).IsRequired(); + builder.Property(d => d.CountryId).IsRequired(); + builder.Property(d => d.RegionId).IsRequired(); + builder.Property(d => d.Name).IsRequired().HasMaxLength(200); + builder.Property(d => d.IsActive).IsRequired().HasDefaultValue(true); + builder.Property(d => d.CreatedAt).IsRequired().HasDefaultValueSql("NOW()"); + builder.Property(d => d.UpdatedAt).IsRequired(false); + + builder.HasOne(d => d.Country) + .WithMany() + .HasForeignKey(d => d.CountryId); + + builder.HasOne(d => d.Region) + .WithMany() + .HasForeignKey(d => d.RegionId); + } +} diff --git a/src/Infrastructure/Localities/LocalityConfiguration .cs b/src/Infrastructure/Localities/LocalityConfiguration .cs new file mode 100644 index 0000000..ba989e7 --- /dev/null +++ b/src/Infrastructure/Localities/LocalityConfiguration .cs @@ -0,0 +1,57 @@ +using Domain.Areas; +using Domain.Localities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Localities; + +internal sealed class LocalityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + // Primary Key + builder.HasKey(l => l.Id); + + // Properties configuration + builder.Property(l => l.Id) + .ValueGeneratedOnAdd(); + + builder.Property(l => l.Name) + .HasMaxLength(255) + .IsRequired(); + + builder.Property(l => l.CountryId) + .IsRequired(); + + builder.Property(l => l.AreaId) + .IsRequired(); + + builder.Property(l => l.Type) + .IsRequired() + .HasConversion(); + + builder.Property(l => l.IsActive) + .IsRequired() + .HasDefaultValue(true); + + // Indexes for performance + builder.HasIndex(l => l.Name); + builder.HasIndex(l => l.CountryId); + builder.HasIndex(l => l.AreaId); + builder.HasIndex(l => l.Type); + builder.HasIndex(l => l.IsActive); + + // Composite indexes for common query patterns + builder.HasIndex(l => new { l.AreaId, l.IsActive }); + builder.HasIndex(l => new { l.CountryId, l.AreaId, l.IsActive }); + builder.HasIndex(l => new { l.AreaId, l.Type, l.IsActive }); + + builder.HasIndex(l => new { l.AreaId, l.Name }) + .IsUnique(); + + builder.HasOne() + .WithMany() + .HasForeignKey(l => l.AreaId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/src/Infrastructure/Regions/RegionConfiguration.cs b/src/Infrastructure/Regions/RegionConfiguration.cs new file mode 100644 index 0000000..0c97d4c --- /dev/null +++ b/src/Infrastructure/Regions/RegionConfiguration.cs @@ -0,0 +1,26 @@ +using Domain.Regions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Regions; + +public class RegionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(r => r.Id); + builder.HasIndex(r => r.CountryId); + + builder.Property(r => r.Id).IsRequired(); + builder.Property(r => r.CountryId).IsRequired(); + builder.Property(r => r.Name).IsRequired().HasMaxLength(200); + builder.Property(r => r.RegionType).IsRequired().HasMaxLength(100); + builder.Property(r => r.IsActive).IsRequired().HasDefaultValue(true); + builder.Property(r => r.CreatedAt).IsRequired().HasDefaultValueSql("NOW()"); + builder.Property(r => r.UpdatedAt).IsRequired(false); + + builder.HasOne(r => r.Country) + .WithMany() + .HasForeignKey(r => r.CountryId); + } +} diff --git a/src/Web.Api/Endpoints/Areas/Create .cs b/src/Web.Api/Endpoints/Areas/Create .cs new file mode 100644 index 0000000..40d1da2 --- /dev/null +++ b/src/Web.Api/Endpoints/Areas/Create .cs @@ -0,0 +1,48 @@ +using Application.Abstractions.Messaging; +using Application.Areas.Create; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Areas; + +internal sealed class Create : IEndpoint +{ + public sealed record Request( + Guid CountryId, + Guid DistrictId, + string Name, + int Type, + bool IsActive + ); + + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPost("areas", async ( + Request request, + ICommandHandler handler, + CancellationToken cancellationToken) => + { + if (!Enum.IsDefined(typeof(Domain.Areas.Area.AreaType), request.Type)) + { + return Results.BadRequest("Invalid area type. Use 1=Upazila, 2=City, 3=Thana, 4=Municipality, 5=Township"); + } + + var areaType = (Domain.Areas.Area.AreaType)request.Type; + + var command = new CreateAreaCommand( + request.CountryId, + request.DistrictId, + request.Name, + areaType, + request.IsActive + ); + + Result result = await handler.Handle(command, cancellationToken); + + return result.Match(Results.Ok, CustomResults.Problem); + }) + .WithTags(Tags.Areas) + .RequireAuthorization(); + } +} diff --git a/src/Web.Api/Endpoints/Areas/Delete.cs b/src/Web.Api/Endpoints/Areas/Delete.cs new file mode 100644 index 0000000..90b368e --- /dev/null +++ b/src/Web.Api/Endpoints/Areas/Delete.cs @@ -0,0 +1,29 @@ +using Application.Abstractions.Messaging; +using Application.Areas.Delete; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Areas; + +internal sealed class Delete : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapDelete("areas/{id:Guid}", async ( + Guid id, + ICommandHandler handler, + CancellationToken cancellationToken) => + { + var command = new DeleteAreaCommand(id); + + Result result = await handler.Handle(command, cancellationToken); + + return result.Match( + () => Results.Ok(new { Message = "Area deleted successfully." }), + CustomResults.Problem); + }) + .WithTags(Tags.Areas) + .RequireAuthorization(); + } +} diff --git a/src/Web.Api/Endpoints/Areas/GetAll.cs b/src/Web.Api/Endpoints/Areas/GetAll.cs new file mode 100644 index 0000000..eac44c3 --- /dev/null +++ b/src/Web.Api/Endpoints/Areas/GetAll.cs @@ -0,0 +1,26 @@ +using Application.Abstractions.Messaging; +using Application.Areas.Get; +using Application.Areas.GetAll; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Areas; + +internal sealed class GetAll : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapGet("areas", async ( + IQueryHandler> handler, + CancellationToken cancellationToken) => + { + var query = new GetAllAreasQuery(); + Result> result = await handler.Handle(query, cancellationToken); + return result.Match(Results.Ok, CustomResults.Problem); + }) + .WithTags(Tags.Areas) + .RequireAuthorization(); + } +} + diff --git a/src/Web.Api/Endpoints/Areas/GetById .cs b/src/Web.Api/Endpoints/Areas/GetById .cs new file mode 100644 index 0000000..703702c --- /dev/null +++ b/src/Web.Api/Endpoints/Areas/GetById .cs @@ -0,0 +1,27 @@ +using Application.Abstractions.Messaging; +using Application.Areas.Get; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Areas; + +internal sealed class GetById : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapGet("areas/{id:Guid}", async ( + Guid id, + IQueryHandler handler, + CancellationToken cancellationToken) => + { + var query = new GetAreaByIdQuery(id); + + Result result = await handler.Handle(query, cancellationToken); + + return result.Match(Results.Ok, CustomResults.Problem); + }) + .WithTags(Tags.Areas) + .RequireAuthorization(); + } +} diff --git a/src/Web.Api/Endpoints/Areas/Update .cs b/src/Web.Api/Endpoints/Areas/Update .cs new file mode 100644 index 0000000..9f1a32f --- /dev/null +++ b/src/Web.Api/Endpoints/Areas/Update .cs @@ -0,0 +1,52 @@ +using Application.Abstractions.Messaging; +using Application.Areas.Update; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Areas; + +internal sealed class Update : IEndpoint +{ + public sealed record Request( + Guid CountryId, + Guid DistrictId, + string Name, + int Type, // 1=Upazila, 2=City, 3=Thana, 4=Municipality, 5=Township + bool IsActive + ); + + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPut("areas/{id:Guid}", async ( + Guid id, + Request request, + ICommandHandler handler, + CancellationToken cancellationToken) => + { + if (!Enum.IsDefined(typeof(Domain.Areas.Area.AreaType), request.Type)) + { + return Results.BadRequest("Invalid area type. Use 1=Upazila, 2=City, 3=Thana, 4=Municipality, 5=Township"); + } + + var areaType = (Domain.Areas.Area.AreaType)request.Type; + + var command = new UpdateAreaCommand( + id, + request.CountryId, + request.DistrictId, + request.Name, + areaType, + request.IsActive + ); + + Result result = await handler.Handle(command, cancellationToken); + + return result.Match( + areaId => Results.Ok(new { Id = areaId, Message = "Area updated successfully." }), + CustomResults.Problem); + }) + .WithTags(Tags.Areas) + .RequireAuthorization(); + } +} diff --git a/src/Web.Api/Endpoints/Districts/Create.cs b/src/Web.Api/Endpoints/Districts/Create.cs new file mode 100644 index 0000000..057b937 --- /dev/null +++ b/src/Web.Api/Endpoints/Districts/Create.cs @@ -0,0 +1,44 @@ +using Application.Abstractions.Messaging; +using Application.Districts.Create; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Districts; + +internal sealed class Create : IEndpoint +{ + public sealed class Request + { + public Guid CountryId { get; set; } + public Guid RegionId { get; set; } + public string Name { get; set; } + public bool IsActive { get; set; } = true; + } + + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPost("Districts", async ( + Request request, + ICommandHandler handler, + CancellationToken cancellationToken) => + { + var command = new CreateDistrictCommand + { + CountryId = request.CountryId, + RegionId = request.RegionId, + Name = request.Name, + IsActive = request.IsActive, + CreatedAt = DateTime.UtcNow + }; + + Result result = await handler.Handle(command, cancellationToken); + + return result.Match(Results.Ok, CustomResults.Problem); + }) + .WithTags(Tags.Districts) + .RequireAuthorization() + .WithSummary("Create a new District") + .WithDescription("Creates a new district with given name, region, and active status."); + } +} diff --git a/src/Web.Api/Endpoints/Districts/Delete.cs b/src/Web.Api/Endpoints/Districts/Delete.cs new file mode 100644 index 0000000..708c55f --- /dev/null +++ b/src/Web.Api/Endpoints/Districts/Delete.cs @@ -0,0 +1,32 @@ +using Application.Abstractions.Messaging; +using Application.Districts.Delete; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Districts; + +internal sealed class Delete : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapDelete("Districts/{id:guid}", async ( + Guid id, + ICommandHandler handler, + CancellationToken cancellationToken) => + { + var command = new DeleteDistrictCommand(id); + + Result result = await handler.Handle(command, cancellationToken); + + return result.Match( + Results.NoContent, + error => CustomResults.Problem(error) + ); + }) + .WithTags(Tags.Districts) + .RequireAuthorization() + .WithSummary("Delete District") + .WithDescription("Deletes a district by Id."); + } +} diff --git a/src/Web.Api/Endpoints/Districts/Get.cs b/src/Web.Api/Endpoints/Districts/Get.cs new file mode 100644 index 0000000..33282f7 --- /dev/null +++ b/src/Web.Api/Endpoints/Districts/Get.cs @@ -0,0 +1,29 @@ +using Application.Abstractions.Messaging; +using Application.Districts.Get; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Districts; + +internal sealed class Get : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapGet("Districts", async ( + IQueryHandler> handler, + CancellationToken cancellationToken) => + { + var query = new GetDistrictQuery(); + + Result> result = + await handler.Handle(query, cancellationToken); + + return result.Match(Results.Ok, CustomResults.Problem); + }) + .WithTags(Tags.Districts) + .RequireAuthorization() + .WithSummary("Get all Districts") + .WithDescription("Returns list of all districts"); + } +} diff --git a/src/Web.Api/Endpoints/Districts/GetById.cs b/src/Web.Api/Endpoints/Districts/GetById.cs new file mode 100644 index 0000000..e9b1ff8 --- /dev/null +++ b/src/Web.Api/Endpoints/Districts/GetById.cs @@ -0,0 +1,31 @@ +using Application.Abstractions.Messaging; +using Application.Districts.Get; +using Application.Districts.GetById; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Districts; + +internal sealed class GetById : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapGet("Districts/{id:guid}", async ( + Guid id, + IQueryHandler handler, + CancellationToken cancellationToken) => + { + var query = new GetDistrictByIdQuery(id); + + Result result = + await handler.Handle(query, cancellationToken); + + return result.Match(Results.Ok, CustomResults.Problem); + }) + .WithTags(Tags.Districts) + .RequireAuthorization() + .WithSummary("Get District by Id") + .WithDescription("Returns a district by Id"); + } +} diff --git a/src/Web.Api/Endpoints/Districts/Update.cs b/src/Web.Api/Endpoints/Districts/Update.cs new file mode 100644 index 0000000..6df5096 --- /dev/null +++ b/src/Web.Api/Endpoints/Districts/Update.cs @@ -0,0 +1,46 @@ +using Application.Abstractions.Messaging; +using Application.Districts.Update; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Districts; + +internal sealed class Update : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPut("Districts/{id:guid}", async ( + Guid id, + UpdateDistrictRequest request, + ICommandHandler handler, + CancellationToken cancellationToken) => + { + var command = new UpdateDistrictCommand( + DistrictId: id, + CountryId: request.CountryId, + RegionId: request.RegionId, + Name: request.Name, + IsActive: request.IsActive + ); + + Result result = await handler.Handle(command, cancellationToken); + + return result.Match( + Results.NoContent, + error => CustomResults.Problem(error) + ); + }) + .WithTags(Tags.Districts) + .RequireAuthorization() + .WithSummary("Update District") + .WithDescription("Updates district fields"); + } +} + +public sealed record UpdateDistrictRequest( + Guid CountryId, + Guid RegionId, + string Name, + bool IsActive +); diff --git a/src/Web.Api/Endpoints/Localities/Create.cs b/src/Web.Api/Endpoints/Localities/Create.cs new file mode 100644 index 0000000..d9865f2 --- /dev/null +++ b/src/Web.Api/Endpoints/Localities/Create.cs @@ -0,0 +1,50 @@ +using Application.Abstractions.Messaging; +using Application.Localities.Create; +using Domain.Localities; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Localities; + +internal sealed class Create : IEndpoint +{ + public sealed record Request( + Guid CountryId, + Guid AreaId, + string Name, + int Type, + bool IsActive + ); + + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPost("localities", async ( + Request request, + ICommandHandler handler, + CancellationToken cancellationToken) => + { + // Correct enum access: Locality.LocalityType (nested enum) + if (!Enum.IsDefined(typeof(Locality.LocalityType), request.Type)) + { + return Results.BadRequest("Invalid locality type."); + } + + var localityType = (Locality.LocalityType)request.Type; + + var command = new CreateLocalityCommand( + request.CountryId, + request.AreaId, + request.Name, + localityType, + request.IsActive + ); + + Result result = await handler.Handle(command, cancellationToken); + + return result.Match(Results.Ok, CustomResults.Problem); + }) + .WithTags(Tags.Localities) + .RequireAuthorization(); + } +} diff --git a/src/Web.Api/Endpoints/Localities/Delete .cs b/src/Web.Api/Endpoints/Localities/Delete .cs new file mode 100644 index 0000000..3270784 --- /dev/null +++ b/src/Web.Api/Endpoints/Localities/Delete .cs @@ -0,0 +1,29 @@ +using Application.Abstractions.Messaging; +using Application.Localities.Delete; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Localities; + +internal sealed class Delete : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapDelete("localities/{id:Guid}", async ( + Guid id, + ICommandHandler handler, + CancellationToken cancellationToken) => + { + var command = new DeleteLocalityCommand(id); + + Result result = await handler.Handle(command, cancellationToken); + + return result.Match( + () => Results.Ok(new { Message = "Locality deleted successfully." }), + CustomResults.Problem); + }) + .WithTags(Tags.Localities) + .RequireAuthorization(); + } +} diff --git a/src/Web.Api/Endpoints/Localities/GetAll.cs b/src/Web.Api/Endpoints/Localities/GetAll.cs new file mode 100644 index 0000000..786cbb4 --- /dev/null +++ b/src/Web.Api/Endpoints/Localities/GetAll.cs @@ -0,0 +1,25 @@ +using Application.Abstractions.Messaging; +using Application.Localities.Get; +using Application.Localities.GetAll; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Localities; + +internal sealed class GetAll : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapGet("localities", async ( + IQueryHandler> handler, + CancellationToken cancellationToken) => + { + var query = new GetAllLocalitiesQuery(); + Result> result = await handler.Handle(query, cancellationToken); + return result.Match(Results.Ok, CustomResults.Problem); + }) + .WithTags(Tags.Localities) + .RequireAuthorization(); + } +} diff --git a/src/Web.Api/Endpoints/Localities/GetById .cs b/src/Web.Api/Endpoints/Localities/GetById .cs new file mode 100644 index 0000000..a0ded68 --- /dev/null +++ b/src/Web.Api/Endpoints/Localities/GetById .cs @@ -0,0 +1,28 @@ +using Application.Abstractions.Messaging; +using Application.Localities.Get; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Localities; + +internal sealed class GetById : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapGet("localities/{id:Guid}", async ( + Guid id, + IQueryHandler handler, + CancellationToken cancellationToken) => + { + var query = new GetLocalityByIdQuery(id); + + Result result = await handler.Handle(query, cancellationToken); + + return result.Match(Results.Ok, CustomResults.Problem); + }) + .WithTags(Tags.Localities) + .RequireAuthorization(); + } +} + diff --git a/src/Web.Api/Endpoints/Localities/Update .cs b/src/Web.Api/Endpoints/Localities/Update .cs new file mode 100644 index 0000000..274bf95 --- /dev/null +++ b/src/Web.Api/Endpoints/Localities/Update .cs @@ -0,0 +1,52 @@ +using Application.Abstractions.Messaging; +using Application.Localities.Update; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Localities; + +internal sealed class Update : IEndpoint +{ + public sealed record Request( + Guid CountryId, + Guid AreaId, + string Name, + int Type, // 1=Union, 2=Ward, 3=Neighborhood, etc. + bool IsActive + ); + + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPut("localities/{id:Guid}", async ( + Guid id, + Request request, + ICommandHandler handler, + CancellationToken cancellationToken) => + { + if (!Enum.IsDefined(typeof(Domain.Localities.Locality.LocalityType), request.Type)) + { + return Results.BadRequest("Invalid locality type."); + } + + var localityType = (Domain.Localities.Locality.LocalityType)request.Type; + + var command = new UpdateLocalityCommand( + id, + request.CountryId, + request.AreaId, + request.Name, + localityType, + request.IsActive + ); + + Result result = await handler.Handle(command, cancellationToken); + + return result.Match( + localityId => Results.Ok(new { Id = localityId, Message = "Locality updated successfully." }), + CustomResults.Problem); + }) + .WithTags(Tags.Localities) + .RequireAuthorization(); + } +} diff --git a/src/Web.Api/Endpoints/Regions/Create.cs b/src/Web.Api/Endpoints/Regions/Create.cs new file mode 100644 index 0000000..5b5e470 --- /dev/null +++ b/src/Web.Api/Endpoints/Regions/Create.cs @@ -0,0 +1,45 @@ +using System; +using Application.Abstractions.Messaging; +using Application.Regions.Create; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Regions; + +internal sealed class Create : IEndpoint +{ + public sealed class Request + { + public Guid CountryId { get; set; } + public string Name { get; set; } + public string RegionType { get; set; } + public bool IsActive { get; set; } = true; + } + + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPost("Regions", async ( + Request request, + ICommandHandler handler, + CancellationToken cancellationToken) => + { + var command = new CreateRegionCommand + { + CountryId = request.CountryId, + Name = request.Name, + RegionType = request.RegionType, + IsActive = request.IsActive, + CreatedAt = DateTime.UtcNow + }; + + Result result = await handler.Handle(command, cancellationToken); + + return result.Match(Results.Ok, CustomResults.Problem); + }) + .WithTags(Tags.Regions) + .WithSummary("Creates a new region") + .WithDescription("This endpoint creates a new region with the provided details. Requires authentication.") + .RequireAuthorization(); + } +} diff --git a/src/Web.Api/Endpoints/Regions/Delete.cs b/src/Web.Api/Endpoints/Regions/Delete.cs new file mode 100644 index 0000000..ca796a0 --- /dev/null +++ b/src/Web.Api/Endpoints/Regions/Delete.cs @@ -0,0 +1,32 @@ +using Application.Abstractions.Messaging; +using Application.Regions.Delete; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Regions; + +internal sealed class Delete : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapDelete("Regions/{id:guid}", async ( + Guid id, + ICommandHandler handler, + CancellationToken cancellationToken) => + { + var command = new DeleteRegionCommand(id); + + Result result = await handler.Handle(command, cancellationToken); + + return result.Match( + Results.NoContent, + error => CustomResults.Problem(error) + ); + }) + .WithTags(Tags.Regions) + .RequireAuthorization() + .WithSummary("Delete Region") + .WithDescription("Deletes a region by Id."); + } +} diff --git a/src/Web.Api/Endpoints/Regions/Get.cs b/src/Web.Api/Endpoints/Regions/Get.cs new file mode 100644 index 0000000..baf8d01 --- /dev/null +++ b/src/Web.Api/Endpoints/Regions/Get.cs @@ -0,0 +1,29 @@ +using Application.Abstractions.Messaging; +using Application.Regions.Get; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Regions; + +internal sealed class Get : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapGet("Regions", async ( + IQueryHandler> handler, + CancellationToken cancellationToken) => + { + var query = new GetRegionQuery(); + + Result> result = + await handler.Handle(query, cancellationToken); + + return result.Match(Results.Ok, CustomResults.Problem); + }) + .WithTags(Tags.Regions) + .RequireAuthorization() + .WithSummary("Get all Regions") + .WithDescription("Returns list of all regions"); + } +} diff --git a/src/Web.Api/Endpoints/Regions/GetById.cs b/src/Web.Api/Endpoints/Regions/GetById.cs new file mode 100644 index 0000000..62e7657 --- /dev/null +++ b/src/Web.Api/Endpoints/Regions/GetById.cs @@ -0,0 +1,31 @@ +using Application.Abstractions.Messaging; +using Application.Regions.Get; +using Application.Regions.GetById; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Regions; + +internal sealed class GetById : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapGet("Regions/{id:guid}", async ( + Guid id, + IQueryHandler handler, + CancellationToken cancellationToken) => + { + var query = new GetRegionByIdQuery(id); + + Result result = + await handler.Handle(query, cancellationToken); + + return result.Match(Results.Ok, CustomResults.Problem); + }) + .WithTags(Tags.Regions) + .RequireAuthorization() + .WithSummary("Get Region by Id") + .WithDescription("Returns a region by Id"); + } +} diff --git a/src/Web.Api/Endpoints/Regions/Update.cs b/src/Web.Api/Endpoints/Regions/Update.cs new file mode 100644 index 0000000..0622c68 --- /dev/null +++ b/src/Web.Api/Endpoints/Regions/Update.cs @@ -0,0 +1,46 @@ +using Application.Abstractions.Messaging; +using Application.Regions.Update; +using SharedKernel; +using Web.Api.Extensions; +using Web.Api.Infrastructure; + +namespace Web.Api.Endpoints.Regions; + +internal sealed class Update : IEndpoint +{ + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapPut("Regions/{id:guid}", async ( + Guid id, + UpdateRegionRequest request, + ICommandHandler handler, + CancellationToken cancellationToken) => + { + var command = new UpdateRegionCommand( + RegionId: id, + CountryId: request.CountryId, + Name: request.Name, + RegionType: request.RegionType, + IsActive: request.IsActive + ); + + Result result = await handler.Handle(command, cancellationToken); + + return result.Match( + Results.NoContent, + error => CustomResults.Problem(error) + ); + }) + .WithTags(Tags.Regions) + .RequireAuthorization() + .WithSummary("Update Region") + .WithDescription("Updates region fields"); + } +} + +public sealed record UpdateRegionRequest( + Guid CountryId, + string Name, + string RegionType, + bool IsActive +); diff --git a/src/Web.Api/Endpoints/Tags.cs b/src/Web.Api/Endpoints/Tags.cs index 8364623..8d27dfd 100644 --- a/src/Web.Api/Endpoints/Tags.cs +++ b/src/Web.Api/Endpoints/Tags.cs @@ -20,4 +20,8 @@ public static class Tags public const string Otp = "Otp"; public const string SendMail = "Sent Email"; public const string Countries = "Countries"; + public const string Regions = "Regions"; + public const string Districts = "Districts"; + public const string Areas = "Areas"; + public const string Localities = "Localities"; }