marco.pms.api/Marco.Pms.Services/Controllers/OrganizationController.cs

944 lines
49 KiB
C#

using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Organization;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.OrganizationModel;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Master;
using Marco.Pms.Model.ViewModels.Organization;
using Marco.Pms.Model.ViewModels.Projects;
using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Net;
namespace Marco.Pms.Services.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class OrganizationController : ControllerBase
{
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly IServiceScopeFactory _serviceScope;
private readonly UserHelper _userHelper;
private readonly Guid tenantId;
private readonly IMapper _mapper;
private readonly ILoggingService _logger;
private static readonly Guid PMCProvider = Guid.Parse("b1877a3b-8832-47b1-bbe3-dc7e98672f49");
private static readonly Guid ServiceProvider = Guid.Parse("5ee49bcd-b6d3-482f-9aaf-484afe04abec");
private static readonly Guid SubContractorProvider = Guid.Parse("a283356a-9b02-4029-afb7-e65c703efdd4");
private static readonly Guid superTenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26");
public OrganizationController(IDbContextFactory<ApplicationDbContext> dbContextFactory,
IServiceScopeFactory serviceScope,
UserHelper userHelper,
ILoggingService logger,
IMapper mapper)
{
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
_serviceScope = serviceScope ?? throw new ArgumentNullException(nameof(serviceScope));
_userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
tenantId = userHelper.GetTenantId();
}
#region =================================================================== Get Functions ===================================================================
[HttpGet("list")]
public async Task<IActionResult> GetOrganizarionListAsync([FromQuery] string? searchString, [FromQuery] double? sprid, [FromQuery] bool active = true,
[FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
{
_logger.LogDebug("Fetching organization list. SearchString: {SearchString}, SPRID: {SPRID}, Active: {Active}, Page: {PageNumber}, Size: {PageSize}",
searchString ?? "", sprid ?? 0, active, pageNumber, pageSize);
await using var _context = await _dbContextFactory.CreateDbContextAsync();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Base query filtering by active status
IQueryable<Organization> organizationQuery = _context.Organizations.Where(o => o.IsActive == active);
if (sprid.HasValue)
{
// Filter by SPRID if provided
organizationQuery = organizationQuery.Where(o => o.SPRID == sprid.Value);
_logger.LogDebug("Filtering organizations by SPRID: {SPRID}", sprid.Value);
}
else
{
// Get organization IDs mapped to current tenant that are active
var organizationIds = await _context.TenantOrgMappings
.Where(to => to.TenantId == tenantId && to.IsActive)
.Select(to => to.OrganizationId)
.ToListAsync();
organizationQuery = organizationQuery.Where(o => organizationIds.Contains(o.Id));
_logger.LogDebug("Filtering organizations by tenant's mapped IDs count: {Count}", organizationIds.Count);
if (!string.IsNullOrWhiteSpace(searchString))
{
// Filter by search string on organization name -- extend here if needed
organizationQuery = organizationQuery.Where(o => o.Name.Contains(searchString));
_logger.LogDebug("Filtering organizations by search string: {SearchString}", searchString);
}
}
// Get total count for pagination
var totalCount = await organizationQuery.CountAsync();
var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
// Fetch page of organizations sorted by name
var organizations = await organizationQuery
.OrderBy(o => o.Name)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
// Collect creator and updater employee IDs
var createdByIds = organizations.Where(o => o.CreatedById != null).Select(o => o.CreatedById!.Value).Distinct().ToList();
var updatedByIds = organizations.Where(o => o.UpdatedById != null).Select(o => o.UpdatedById!.Value).Distinct().ToList();
// Fetch corresponding employee details in one query
var employeeIds = createdByIds.Union(updatedByIds).ToList();
var employees = await _context.Employees.Where(e => employeeIds.Contains(e.Id)).ToListAsync();
// Map data to view models including created and updated by employees
var vm = organizations.Select(o =>
{
var orgVm = _mapper.Map<OrganizationVM>(o);
orgVm.CreatedBy = employees.Where(e => e.Id == o.CreatedById).Select(e => _mapper.Map<BasicEmployeeVM>(e)).FirstOrDefault();
orgVm.UpdatedBy = employees.Where(e => e.Id == o.UpdatedById).Select(e => _mapper.Map<BasicEmployeeVM>(e)).FirstOrDefault();
return orgVm;
}).ToList();
var response = new
{
CurrentPage = pageNumber,
TotalPages = totalPages,
TotalEntities = totalCount,
Data = vm,
};
_logger.LogInfo("Fetched {Count} organizations (Page {PageNumber} of {TotalPages})", vm.Count, pageNumber, totalPages);
return Ok(ApiResponse<object>.SuccessResponse(response, "Successfully fetched the organization list", 200));
}
[HttpGet("details/{id}")]
public async Task<IActionResult> GetOrganizationDetailsAsync(Guid id)
{
_logger.LogDebug("Started fetching details for OrganizationId: {OrganizationId}", id);
await using var _context = await _dbContextFactory.CreateDbContextAsync();
try
{
// Get the logged-in employee (for filter/permission checks)
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Fetch the organization entity by Id
var organization = await _context.Organizations.FirstOrDefaultAsync(o => o.Id == id);
if (organization == null)
{
_logger.LogWarning("Organization not found for OrganizationId: {OrganizationId}", id);
return NotFound(ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found", 404));
}
// Fetch CreatedBy employee (with JobRole)
var createdByTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Employees
.Include(e => e.JobRole)
.FirstOrDefaultAsync(e => e.Id == organization.CreatedById);
});
// Fetch UpdatedBy employee (with JobRole)
var updatedByTask = Task.Run(async () =>
{
if (organization.UpdatedById.HasValue)
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Employees
.Include(e => e.JobRole)
.FirstOrDefaultAsync(e => e.Id == organization.UpdatedById);
}
return null;
});
// Fetch the organization's service mappings and corresponding services
var orgServiceMappingTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.OrgServiceMappings
.Include(os => os.Service)
.Where(os => os.OrganizationId == id).ToListAsync();
});
// Fetch active employees in the organization
var employeeListTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Employees
.Where(e => e.OrganizationId == id && e.IsActive).ToListAsync();
});
await Task.WhenAll(createdByTask, updatedByTask, orgServiceMappingTask, employeeListTask);
var createdByEmployee = createdByTask.Result;
var updatedByEmployee = updatedByTask.Result;
var orgServiceMappings = orgServiceMappingTask.Result;
var employeeList = employeeListTask.Result;
var activeEmployeeCount = employeeList.Count;
var activeApplicationUserCount = employeeList.Count(e => e.HasApplicationAccess);
// Start query for projects mapped to this organization (including project and service info)
var baseProjectOrgMappingQuery = _context.ProjectOrgMappings
.Include(po => po.ProjectService)
.ThenInclude(ps => ps!.Service)
.Include(po => po.ProjectService)
.ThenInclude(ps => ps!.Project)
.Where(po => po.OrganizationId == id && po.ProjectService != null);
// If logged-in employee is not from the requested organization, restrict projects to those also mapped to their org
List<ProjectOrgMapping> projectOrgMappings;
if (loggedInEmployee.OrganizationId != id)
{
var projectIds = await _context.ProjectOrgMappings
.Include(po => po.ProjectService)
.Where(po => po.OrganizationId == loggedInEmployee.OrganizationId && po.ProjectService != null)
.Select(po => po.ProjectService!.ProjectId)
.ToListAsync();
projectOrgMappings = await baseProjectOrgMappingQuery
.Where(po => projectIds.Contains(po.ProjectService!.ProjectId))
.ToListAsync();
}
else
{
projectOrgMappings = await baseProjectOrgMappingQuery.ToListAsync();
}
// Map results to output view model
var response = _mapper.Map<OrganizationDetailsVM>(organization);
response.ActiveApplicationUserCount = activeApplicationUserCount;
response.ActiveEmployeeCount = activeEmployeeCount;
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(createdByEmployee);
response.UpdatedBy = _mapper.Map<BasicEmployeeVM>(updatedByEmployee);
response.Projects = _mapper.Map<List<ProjectServiceMappingVM>>(projectOrgMappings.Select(po => po.ProjectService).ToList());
_logger.LogInfo("Fetched organization details for OrganizationId: {OrganizationId}, Employee count: {EmployeeCount}, App user count: {AppUserCount}, Project count: {ProjectCount}",
id, activeEmployeeCount, activeApplicationUserCount, response.Projects.Count);
return Ok(ApiResponse<object>.SuccessResponse(response, "Successfully fetched the organization details", 200));
}
catch (DbUpdateException dbEx)
{
_logger.LogError(dbEx, "Database exception while fetching details for OrganizationId: {OrganizationId}", id);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500));
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception while fetching details for OrganizationId: {OrganizationId}", id);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "An internal exception occurred", 500));
}
}
#endregion
#region =================================================================== Post Functions ===================================================================
[HttpPost("create")]
public async Task<IActionResult> CreateOrganizationAsync([FromBody] CreateOrganizationDto model)
{
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScope.CreateScope(); // Create scope for scoped services
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Concurrent permission check and organization existence check
var hasPermissionTask = Task.Run(async () =>
{
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.AddOrganization, loggedInEmployee.Id);
});
var isPrimaryOrganizationTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Tenants.AnyAsync(t => t.OrganizationId == loggedInEmployee.OrganizationId);
});
await Task.WhenAll(hasPermissionTask, isPrimaryOrganizationTask);
bool hasPermission = hasPermissionTask.Result;
bool isPrimaryOrganization = isPrimaryOrganizationTask.Result;
// Check user access permission
if (!hasPermission && !isPrimaryOrganization)
{
_logger.LogWarning("User {EmployeeId} attempted to create a new organization without permission", loggedInEmployee.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to create new organization.", 403));
}
// Get last SPRID and increment for new organization
var lastOrganization = await _context.Organizations.OrderByDescending(sp => sp.SPRID).FirstOrDefaultAsync();
double lastSPRID = lastOrganization?.SPRID ?? 5400;
// Map DTO to entity and set defaults
Organization organization = _mapper.Map<Organization>(model);
organization.SPRID = lastSPRID + 1;
organization.CreatedAt = DateTime.UtcNow;
organization.CreatedById = loggedInEmployee.Id;
organization.IsActive = true;
_context.Organizations.Add(organization);
// Create mapping for organization tenant
var newOrganizationTenantMapping = new TenantOrgMapping
{
OrganizationId = organization.Id,
SPRID = organization.SPRID,
AssignedDate = DateTime.UtcNow,
IsActive = true,
AssignedById = loggedInEmployee.Id,
TenantId = tenantId
};
_context.TenantOrgMappings.Add(newOrganizationTenantMapping);
// Prepare user creation for identity
var user = new ApplicationUser
{
UserName = model.Email,
Email = model.Email,
EmailConfirmed = true
};
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
var emailSender = scope.ServiceProvider.GetRequiredService<IEmailSender>();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
// Create Identity user with a default password (recommend to improve password handling)
var result = await userManager.CreateAsync(user, "User@123");
if (!result.Succeeded)
{
_logger.LogWarning("Failed to create identity user for email {Email}: {Errors}", model.Email, result.Errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to create user", result.Errors, 400));
}
// Get admin job role or fallback role of the tenant
var jobRole = await _context.JobRoles.FirstOrDefaultAsync(jr => jr.Name == "Admin" && jr.TenantId == tenantId)
?? await _context.JobRoles.FirstOrDefaultAsync(jr => jr.TenantId == tenantId);
// Parse full name safely (consider improving split logic for multi-part names)
var fullName = model.ContactPerson.Split(' ', StringSplitOptions.RemoveEmptyEntries);
Employee newEmployee = new Employee
{
FirstName = fullName.Length > 0 ? fullName[0] : string.Empty,
LastName = fullName.Length > 1 ? fullName[^1] : string.Empty,
Email = model.Email,
PermanentAddress = model.Address,
CurrentAddress = model.Address,
PhoneNumber = model.ContactNumber,
ApplicationUserId = user.Id,
JobRoleId = jobRole?.Id ?? Guid.Empty,
IsActive = true,
IsSystem = false,
IsPrimary = true,
OrganizationId = organization.Id,
HasApplicationAccess = true
};
_context.Employees.Add(newEmployee);
// Map organization services
if (model.ServiceIds?.Any() ?? false)
{
var serviceOrgMappings = model.ServiceIds.Select(s => new OrgServiceMapping
{
ServiceId = s,
OrganizationId = organization.Id
}).ToList();
_context.OrgServiceMappings.AddRange(serviceOrgMappings);
}
// Persist all changes
await _context.SaveChangesAsync();
await transaction.CommitAsync();
// Send user registration email with password reset link
var token = await userManager.GeneratePasswordResetTokenAsync(user);
var resetLink = $"{configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
if (!string.IsNullOrEmpty(newEmployee.FirstName))
{
await emailSender.SendResetPasswordEmailOnRegister(user.Email, newEmployee.FirstName, resetLink);
}
// Prepare response DTO
var response = _mapper.Map<OrganizationVM>(organization);
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
return Ok(ApiResponse<object>.SuccessResponse(response, "Successfully created the organization", 200));
}
catch (DbUpdateException dbEx)
{
await transaction.RollbackAsync();
_logger.LogError(dbEx, "Database exception occurred while creating organization");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500));
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected exception occurred while creating organization");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "An unexpected error occurred", 500));
}
}
[HttpPost("assign/project")]
public async Task<IActionResult> AssignOrganizationToProjectAsync([FromBody] AssignOrganizationDto model)
{
_logger.LogDebug("Started assigning organization {OrganizationId} to project {ProjectId} with service IDs {@ServiceIds}",
model.OrganizationId, model.ProjectId, model.ServiceIds);
// Create DbContext for the method scope
await using var _context = await _dbContextFactory.CreateDbContextAsync();
// Begin a database transaction
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Get currently logged in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var today = DateTime.UtcNow.Date;
// Fetch all needed entities concurrently using the single context
var projectServicesTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ProjectServiceMappings
.Where(sp => model.ServiceIds.Contains(sp.ServiceId) && sp.ProjectId == model.ProjectId && sp.IsActive).ToListAsync();
});
var projectOrgMappingsTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ProjectOrgMappings
.Include(po => po.ProjectService)
.Where(po => po.ProjectService != null && model.ServiceIds.Contains(po.ProjectService.ServiceId) && po.ProjectService.ProjectId == model.ProjectId).ToListAsync();
});
var serviceTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceMasters.Where(s => model.ServiceIds.Contains(s.Id) && s.TenantId == tenantId).ToListAsync();
});
var orgTypeTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.OrgTypeMasters.FirstOrDefaultAsync(o => o.Id == model.OrganizationId);
});
var organizationTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.OrganizationId);
});
var parentOrgTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.ParentOrganizationId);
});
var projectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects.FirstOrDefaultAsync(p => p.Id == model.ProjectId);
});
var isPMCTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects.AnyAsync(p => p.Id == model.ProjectId && p.PMCId == loggedInEmployee.OrganizationId);
});
var isServiceProviderTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ProjectOrgMappings.AnyAsync(p => p.Id == model.ProjectId && p.OrganizationId == loggedInEmployee.OrganizationId
&& p.OrganizationTypeId == ServiceProvider);
});
await Task.WhenAll(projectTask, organizationTask, parentOrgTask, serviceTask, orgTypeTask, projectServicesTask, projectOrgMappingsTask, isPMCTask, isServiceProviderTask);
var project = projectTask.Result;
var organization = organizationTask.Result;
var parentOrganization = parentOrgTask.Result;
var services = serviceTask.Result;
var organizationType = orgTypeTask.Result;
var projectServices = projectServicesTask.Result;
var projectOrganizations = projectOrgMappingsTask.Result;
var isPMC = isPMCTask.Result;
var isServiceProvider = isServiceProviderTask.Result;
// Validation checks
if (organization == null)
{
_logger.LogWarning("Organization with ID {OrganizationId} not found.", model.OrganizationId);
return NotFound(ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found in database", 404));
}
if (project == null)
{
_logger.LogWarning("Project with ID {ProjectId} not found.", model.ProjectId);
return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found in database", 404));
}
if (services == null || !services.Any())
{
_logger.LogWarning("No services found for Service IDs {@ServiceIds}.", model.ServiceIds);
return NotFound(ApiResponse<object>.ErrorResponse("Project Service not found", "Project Service not found in database", 404));
}
// Check whether mapping exists between service provider organization and tenant
var serviceProviderTenantMapping = await _context.TenantOrgMappings
.FirstOrDefaultAsync(spt => spt.OrganizationId == model.OrganizationId && spt.TenantId == project.TenantId && spt.IsActive);
if (serviceProviderTenantMapping == null)
{
var newMapping = new TenantOrgMapping
{
OrganizationId = organization.Id,
SPRID = organization.SPRID,
AssignedDate = today,
IsActive = true,
AssignedById = loggedInEmployee.Id,
TenantId = project.TenantId
};
_context.TenantOrgMappings.Add(newMapping);
_logger.LogInfo("Created new TenantOrgMapping for OrganizationId {OrganizationId} and TenantId {TenantId}",
organization.Id, project.TenantId);
}
// Access control validations
if (isPMC && model.OrganizationTypeId != ServiceProvider && model.OrganizationTypeId != SubContractorProvider)
{
_logger.LogWarning("PMCs cannot assign organization type {OrganizationTypeId}. UserId: {UserId}",
model.OrganizationTypeId, loggedInEmployee.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "You don't have access to assign this type of organization", 403));
}
if (isServiceProvider && model.OrganizationTypeId == ServiceProvider)
{
_logger.LogWarning("Service providers cannot assign organization type {OrganizationTypeId}. UserId: {UserId}",
model.OrganizationTypeId, loggedInEmployee.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "You don't have access to assign this type of organization", 403));
}
var newProjectOrgMappings = new List<ProjectOrgMapping>();
var newProjectServiceMappings = new List<ProjectServiceMapping>();
// Loop through each service to create mappings
foreach (var serviceId in model.ServiceIds)
{
var service = services.FirstOrDefault(s => s.Id == serviceId);
if (service == null)
{
_logger.LogWarning("Service with ID {ServiceId} not found.", serviceId);
return NotFound(ApiResponse<object>.ErrorResponse("Service not found", "Service not found in database", 404));
}
var projectService = projectServices.FirstOrDefault(ps => ps.ServiceId == serviceId);
if (projectService == null)
{
projectService = new ProjectServiceMapping
{
Id = Guid.NewGuid(),
ProjectId = project.Id,
ServiceId = serviceId,
TenantId = project.TenantId,
PlannedStartDate = project.StartDate ?? today,
PlannedEndDate = project.EndDate ?? today,
ActualStartDate = today,
IsActive = true
};
newProjectServiceMappings.Add(projectService);
}
// Check if the organization is already assigned for this service
var existingAssignment = projectOrganizations.FirstOrDefault(po => po.ProjectService != null
&& po.ProjectService.ProjectId == project.Id
&& po.ProjectService.ServiceId == serviceId
&& po.OrganizationId == model.OrganizationId);
if (existingAssignment != null)
{
_logger.LogWarning("Organization {OrganizationId} is already assigned to project {ProjectId} for service {ServiceId}.",
model.OrganizationId, project.Id, serviceId);
return Conflict(ApiResponse<object>.ErrorResponse("Organization already assigned", "Organization is already assigned to this project and service", 409));
}
// Prepare new project-org mapping
var projectOrgMapping = new ProjectOrgMapping
{
ProjectServiceId = projectService.Id,
OrganizationId = model.OrganizationId,
OrganizationTypeId = model.OrganizationTypeId,
ParentOrganizationId = model.ParentOrganizationId ?? loggedInEmployee.OrganizationId,
AssignedDate = today,
AssignedById = loggedInEmployee.Id,
TenantId = project.TenantId
};
newProjectOrgMappings.Add(projectOrgMapping);
}
// Save new project service mappings if any
if (newProjectServiceMappings.Any())
{
_context.ProjectServiceMappings.AddRange(newProjectServiceMappings);
await _context.SaveChangesAsync();
_logger.LogInfo("Added {Count} new ProjectServiceMappings for ProjectId {ProjectId}.", newProjectServiceMappings.Count, project.Id);
}
// Save new project organization mappings
_context.ProjectOrgMappings.AddRange(newProjectOrgMappings);
await _context.SaveChangesAsync();
// Commit transaction
await transaction.CommitAsync();
_logger.LogInfo("Assigned organization {OrganizationId} to project {ProjectId} successfully.", model.OrganizationId, model.ProjectId);
// Prepare response view models
var organizationVm = _mapper.Map<BasicOrganizationVm>(organization);
var parentOrganizationVm = _mapper.Map<BasicOrganizationVm>(parentOrganization);
var projectVm = _mapper.Map<BasicProjectVM>(project);
var response = services.Select(s => new AssignOrganizationVm
{
Project = projectVm,
OrganizationType = organizationType,
Organization = organizationVm,
ParentOrganization = parentOrganizationVm,
Service = _mapper.Map<ServiceMasterVM>(s)
}).ToList();
return Ok(ApiResponse<object>.SuccessResponse(response, "Organization successfully assigned to the project", 200));
}
catch (DbUpdateException dbEx)
{
await transaction.RollbackAsync();
_logger.LogError(dbEx, "Database exception occurred while assigning organization {OrganizationId} to project {ProjectId}",
model.OrganizationId, model.ProjectId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500));
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Unhandled exception occurred while assigning organization {OrganizationId} to project {ProjectId}",
model.OrganizationId, model.ProjectId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "An internal exception occurred", 500));
}
}
[HttpPost("assign/tenant/{organizationId}")]
public async Task<IActionResult> AssignOrganizationToTenantAsync(Guid organizationId)
{
_logger.LogInfo("Started assigning organization {OrganizationId} to tenant {TenantId}", organizationId, tenantId);
// Create a DbContext instance for this method scope
await using var _context = await _dbContextFactory.CreateDbContextAsync();
// Begin a database transaction
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Get currently logged in employee for auditing purposes
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Fetch existing tenant-organization mapping if any
var organizationTenantMapping = await _context.TenantOrgMappings
.FirstOrDefaultAsync(spt => spt.OrganizationId == organizationId && spt.TenantId == tenantId && spt.IsActive);
// Fetch the organization details
var organization = await _context.Organizations.FirstOrDefaultAsync(o => o.Id == organizationId);
// Validate organization existence
if (organization == null)
{
_logger.LogWarning("Organization with ID {OrganizationId} not found.", organizationId);
return NotFound(ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found in database", 404));
}
if (organizationTenantMapping == null)
{
// Create new tenant-organization mapping if none exists
var newMapping = new TenantOrgMapping
{
OrganizationId = organization.Id,
SPRID = organization.SPRID,
AssignedDate = DateTime.UtcNow,
IsActive = true,
AssignedById = loggedInEmployee.Id,
TenantId = tenantId
};
_context.TenantOrgMappings.Add(newMapping);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
_logger.LogInfo("Assigned organization {OrganizationId} to tenant {TenantId} successfully.", organizationId, tenantId);
}
else
{
_logger.LogInfo("Organization {OrganizationId} is already assigned to tenant {TenantId}. No action taken.", organizationId, tenantId);
// Commit transaction anyway to complete scope cleanly (optional)
await transaction.CommitAsync();
}
// Prepare response view model
var response = _mapper.Map<BasicOrganizationVm>(organization);
return Ok(ApiResponse<object>.SuccessResponse(response, "Organization has been assigned to tenant", 200));
}
catch (DbUpdateException dbEx)
{
await transaction.RollbackAsync();
_logger.LogError(dbEx, "Database exception occurred while assigning organization {OrganizationId} to tenant {TenantId}.", organizationId, tenantId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "A database exception has occurred", 500));
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Unhandled exception occurred while assigning organization {OrganizationId} to tenant {TenantId}.", organizationId, tenantId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "An internal exception has occurred", 500));
}
}
#endregion
#region =================================================================== Put Functions ===================================================================
[HttpPut("edit/{id}")]
public async Task<IActionResult> UpdateOrganiationAsync(Guid id, [FromBody] UpdateOrganizationDto model)
{
await using var _context = await _dbContextFactory.CreateDbContextAsync();
try
{
// Get the current logged-in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogDebug("Started updating service provider OrganizationId: {OrganizationId} by EmployeeId: {EmployeeId}",
id, loggedInEmployee.Id);
// Check if the user is a tenant-level employee and restrict editing to their own org
var isTenantEmployee = await _context.Tenants.AnyAsync(t => t.Id == tenantId && t.OrganizationId == loggedInEmployee.OrganizationId);
if (!isTenantEmployee && loggedInEmployee.OrganizationId != id)
{
_logger.LogWarning("Access denied. Tenant-level employee {EmployeeId} attempted to update another organization (OrganizationId: {OrganizationId})",
loggedInEmployee.Id, id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "User does not have permission to update the organization", 403));
}
// Fetch the active organization entity
var organization = await _context.Organizations.FirstOrDefaultAsync(o => o.Id == id && o.IsActive);
if (organization == null)
{
_logger.LogWarning("Organization with Id {OrganizationId} not found or inactive.", id);
return NotFound(ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found", 404));
}
// Update basic organization fields
organization.Name = model.Name;
organization.ContactPerson = model.ContactPerson;
organization.Address = model.Address;
organization.ContactNumber = model.ContactNumber;
organization.UpdatedById = loggedInEmployee.Id;
organization.UpdatedAt = DateTime.UtcNow;
// Fetch the primary active employee of the organization
var employee = await _context.Employees.FirstOrDefaultAsync(e => e.OrganizationId == id && e.IsPrimary && e.IsActive);
if (employee == null)
{
_logger.LogWarning("Primary employee not found for OrganizationId: {OrganizationId}", id);
return NotFound(ApiResponse<object>.ErrorResponse("Primary employee not found", "Primary employee not found", 404));
}
// Split contact person's name into first and last names
var fullName = (model.ContactPerson ?? string.Empty).Split(' ', StringSplitOptions.RemoveEmptyEntries);
employee.FirstName = fullName.Length > 0 ? fullName[0] : string.Empty;
employee.LastName = fullName.Length > 1 ? fullName[^1] : string.Empty;
employee.CurrentAddress = model.Address;
employee.PermanentAddress = model.Address;
employee.PhoneNumber = model.ContactNumber;
// Update organization's service mappings if service IDs are provided
if (model.ServiceIds?.Any() ?? false)
{
// Fetch existing service mappings (as no tracking for diff logic)
var orgServiceMappings = await _context.OrgServiceMappings
.AsNoTracking()
.Where(os => os.OrganizationId == id)
.ToListAsync();
var existedServiceIds = orgServiceMappings.Select(os => os.ServiceId).ToList();
// Determine new service mappings to add
var newServiceIds = model.ServiceIds.Except(existedServiceIds).ToList();
var orgServicesToDelete = orgServiceMappings
.Where(s => !model.ServiceIds.Contains(s.ServiceId))
.ToList();
// Add new service mappings
if (newServiceIds.Any())
{
var newMappings = newServiceIds.Select(sid => new OrgServiceMapping
{
OrganizationId = id,
ServiceId = sid
});
await _context.OrgServiceMappings.AddRangeAsync(newMappings);
}
// Remove deleted service mappings
if (orgServicesToDelete.Any())
{
_context.OrgServiceMappings.RemoveRange(orgServicesToDelete);
}
}
await _context.SaveChangesAsync();
var response = _mapper.Map<OrganizationVM>(organization);
_logger.LogInfo("Successfully updated service provider OrganizationId: {OrganizationId} by EmployeeId: {EmployeeId}",
id, loggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(response, "Successfully updated the service provider", 200));
}
catch (DbUpdateException dbEx)
{
_logger.LogError(dbEx, "Database exception occurred while updating OrganizationId: {OrganizationId}", id);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500));
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception occurred while updating OrganizationId: {OrganizationId}", id);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "An internal exception occurred", 500));
}
}
#endregion
#region =================================================================== Delete Functions ===================================================================
//[HttpDelete("delete/{id}")]
//public async Task<IActionResult> DeleteServiceProviderAsync(Guid id, [FromQuery] bool active)
//{
// await using var _context = await _dbContextFactory.CreateDbContextAsync();
// using var scope = _serviceScope.CreateScope();
// var message = active ? "Restore" : "Delete";
// var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// var client = await _context.Clients.FirstOrDefaultAsync(c => c.PrimaryEmployeeId == loggedInEmployee.Id && c.TenantId == tenantId);
// if (!(loggedInEmployee.ApplicationUser?.IsRootUser ?? false) && !loggedInEmployee.IsPrimary && client == null)
// {
// return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", $"You do not have permission to {message}d service provider.", 403));
// }
// var serviceProvider = await _context.ServiceProviders.FirstOrDefaultAsync(sp => sp.Id == id);
// if (serviceProvider == null)
// {
// return NotFound(ApiResponse<object>.ErrorResponse("Service Provider not Found", "Service Provider not Found in database", 404));
// }
// if (serviceProvider.IsActive == active)
// {
// return BadRequest(ApiResponse<object>.ErrorResponse($"Service Provider is already {message}d", $"Service Provider is already {message}d", 400));
// }
// var employeeIds = await _context.Employees.Where(e => e.ServiceProviderId == id).Select(e => e.Id).ToListAsync();
// var isPendingTask = await _context.TaskMembers.AnyAsync(tm => employeeIds.Contains(tm.EmployeeId));
// if (isPendingTask && !active)
// {
// return BadRequest(ApiResponse<object>.ErrorResponse("There is an unfinshed task, Service provider cannot be deleted", "There is an unfinshed task, Service provider cannot be deleted", 400));
// }
// serviceProvider.IsActive = active;
// if (!active)
// {
// var servicePeroviderTenant = await _context.ServiceProviderTenantMappings.AsNoTracking().Where(spt => spt.ServiceProviderId == id && spt.IsActive).ToListAsync();
// var newServiceProviderTenant = servicePeroviderTenant.Select(spt =>
// {
// spt.IsActive = false;
// return spt;
// }).ToList();
// _context.ServiceProviderTenantMappings.UpdateRange(newServiceProviderTenant);
// }
// await _context.SaveChangesAsync();
// return Ok(ApiResponse<object>.SuccessResponse(new { }, $"Service Provider is {message}d", 200));
//}
#endregion
#region =================================================================== Helper Functions ===================================================================
//private ServicesProviderFilter? TryDeserializeServicesProviderFilter(string? filter)
//{
// if (string.IsNullOrWhiteSpace(filter))
// {
// return null;
// }
// var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
// ServicesProviderFilter? documentFilter = null;
// try
// {
// // First, try to deserialize directly. This is the expected case (e.g., from a web client).
// documentFilter = JsonSerializer.Deserialize<ServicesProviderFilter>(filter, options);
// }
// catch (JsonException ex)
// {
// _logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeServicesProviderFilter), filter);
// // If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients).
// try
// {
// // Unescape the string first, then deserialize the result.
// string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
// if (!string.IsNullOrWhiteSpace(unescapedJsonString))
// {
// documentFilter = JsonSerializer.Deserialize<ServicesProviderFilter>(unescapedJsonString, options);
// }
// }
// catch (JsonException ex1)
// {
// // If both attempts fail, log the final error and return null.
// _logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeServicesProviderFilter), filter);
// return null;
// }
// }
// return documentFilter;
//}
#endregion
}
}