diff --git a/Marco.Pms.Services/Controllers/OrganizationController.cs b/Marco.Pms.Services/Controllers/OrganizationController.cs index cc32b48..0af263e 100644 --- a/Marco.Pms.Services/Controllers/OrganizationController.cs +++ b/Marco.Pms.Services/Controllers/OrganizationController.cs @@ -269,19 +269,25 @@ namespace Marco.Pms.Services.Controllers } } - [HttpPost("assign/project")] public async Task 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(); - using var scope = _serviceScope.CreateScope(); + + // Begin a database transaction await using var transaction = await _context.Database.BeginTransactionAsync(); try { + // Get currently logged in employee var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var todaysDate = DateTime.UtcNow.Date; + 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(); @@ -289,7 +295,7 @@ namespace Marco.Pms.Services.Controllers .Where(sp => model.ServiceIds.Contains(sp.ServiceId) && sp.ProjectId == model.ProjectId && sp.IsActive).ToListAsync(); }); - var projectOrganizationsTask = Task.Run(async () => + var projectOrgMappingsTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.ProjectOrgMappings @@ -303,7 +309,7 @@ namespace Marco.Pms.Services.Controllers return await context.ServiceMasters.Where(s => model.ServiceIds.Contains(s.Id) && s.TenantId == tenantId).ToListAsync(); }); - var organizationTypeTask = Task.Run(async () => + var orgTypeTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.OrgTypeMasters.FirstOrDefaultAsync(o => o.Id == model.OrganizationId); @@ -315,7 +321,7 @@ namespace Marco.Pms.Services.Controllers return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.OrganizationId); }); - var parentorganizationTask = Task.Run(async () => + var parentOrgTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.ParentOrganizationId); @@ -340,68 +346,82 @@ namespace Marco.Pms.Services.Controllers && p.OrganizationTypeId == ServiceProvider); }); - await Task.WhenAll(organizationTask, parentorganizationTask, projectTask, projectServicesTask, isPMCTask, isServiceProviderTask, - serviceTask, organizationTypeTask, projectOrganizationsTask); + await Task.WhenAll(projectTask, organizationTask, parentOrgTask, serviceTask, orgTypeTask, projectServicesTask, projectOrgMappingsTask, isPMCTask, isServiceProviderTask); - var services = serviceTask.Result; - var organizationType = organizationTypeTask.Result; - var organization = organizationTask.Result; - var projectServices = projectServicesTask.Result; - var parentorganization = parentorganizationTask.Result; - var projectOrganizations = projectOrganizationsTask.Result; 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.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.ErrorResponse("Project not found", "Project not found in database", 404)); } - if (projectServices == null) + if (services == null || !services.Any()) { + _logger.LogWarning("No services found for Service IDs {@ServiceIds}.", model.ServiceIds); return NotFound(ApiResponse.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 newServiceProviderTenantMapping = new TenantOrgMapping + var newMapping = new TenantOrgMapping { OrganizationId = organization.Id, SPRID = organization.SPRID, - AssignedDate = DateTime.UtcNow, + AssignedDate = today, IsActive = true, AssignedById = loggedInEmployee.Id, TenantId = project.TenantId }; - _context.TenantOrgMappings.Add(newServiceProviderTenantMapping); + _context.TenantOrgMappings.Add(newMapping); + _logger.LogInfo("Created new TenantOrgMapping for OrganizationId {OrganizationId} and TenantId {TenantId}", + organization.Id, project.TenantId); } - List projectOrgMappings = new List(); - List projectServiceMappings = new List(); - + // 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.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.ErrorResponse("Access Denied", "You don't have access to assign this type of organization", 403)); } + var newProjectOrgMappings = new List(); + var newProjectServiceMappings = new List(); + + // Loop through each service to create mappings foreach (var serviceId in model.ServiceIds) { - var service = await _context.ServiceMasters.FirstOrDefaultAsync(s => s.Id == serviceId); + var service = services.FirstOrDefault(s => s.Id == serviceId); if (service == null) { + _logger.LogWarning("Service with ID {ServiceId} not found.", serviceId); return NotFound(ApiResponse.ErrorResponse("Service not found", "Service not found in database", 404)); } + var projectService = projectServices.FirstOrDefault(ps => ps.ServiceId == serviceId); if (projectService == null) { @@ -411,54 +431,69 @@ namespace Marco.Pms.Services.Controllers ProjectId = project.Id, ServiceId = serviceId, TenantId = project.TenantId, - PlannedStartDate = project.StartDate ?? DateTime.UtcNow, - PlannedEndDate = project.EndDate ?? DateTime.UtcNow, - ActualStartDate = DateTime.UtcNow, + PlannedStartDate = project.StartDate ?? today, + PlannedEndDate = project.EndDate ?? today, + ActualStartDate = today, IsActive = true }; - projectServiceMappings.Add(projectService); + 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.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 = DateTime.UtcNow, + AssignedDate = today, AssignedById = loggedInEmployee.Id, TenantId = project.TenantId }; - var projectOrganization = projectOrganizations - .FirstOrDefault(po => po.ProjectService != null && po.ProjectService.ProjectId == project.Id && po.ProjectService.ServiceId == serviceId - && po.OrganizationId == model.OrganizationId); - if (projectOrganization != null) - { - return StatusCode(409, ApiResponse.ErrorResponse("Organization is already assigned to this project", "Organization is already assigned to this project", 409)); - } - projectOrgMappings.Add(projectOrgMapping); + newProjectOrgMappings.Add(projectOrgMapping); } - if (projectServiceMappings.Any()) + // Save new project service mappings if any + if (newProjectServiceMappings.Any()) { - _context.ProjectServiceMappings.AddRange(projectServiceMappings); + _context.ProjectServiceMappings.AddRange(newProjectServiceMappings); await _context.SaveChangesAsync(); + _logger.LogInfo("Added {Count} new ProjectServiceMappings for ProjectId {ProjectId}.", newProjectServiceMappings.Count, project.Id); } - _context.ProjectOrgMappings.AddRange(projectOrgMappings); - + // 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(organization); - var parentorganizationVm = _mapper.Map(parentorganization); - var projectvm = _mapper.Map(project); + var parentOrganizationVm = _mapper.Map(parentOrganization); + var projectVm = _mapper.Map(project); var response = services.Select(s => new AssignOrganizationVm { - Project = projectvm, + Project = projectVm, OrganizationType = organizationType, Organization = organizationVm, - ParentOrganization = parentorganizationVm, + ParentOrganization = parentOrganizationVm, Service = _mapper.Map(s) }).ToList(); @@ -467,54 +502,53 @@ namespace Marco.Pms.Services.Controllers catch (DbUpdateException dbEx) { await transaction.RollbackAsync(); - - _logger.LogError(dbEx, "Database Exception has been occured, While assigning the organization to project"); - return StatusCode(500, ApiResponse.ErrorResponse("Internal error", "An database exception has been occured", 500)); + _logger.LogError(dbEx, "Database exception occurred while assigning organization {OrganizationId} to project {ProjectId}", + model.OrganizationId, model.ProjectId); + return StatusCode(500, ApiResponse.ErrorResponse("Internal error", "A database exception occurred", 500)); } catch (Exception ex) { - _logger.LogError(ex, "Exception has been occured, While assigned the organizatio to project"); - return StatusCode(500, ApiResponse.ErrorResponse("Internal error", "An internal exception has been occured", 500)); + await transaction.RollbackAsync(); + _logger.LogError(ex, "Unhandled exception occurred while assigning organization {OrganizationId} to project {ProjectId}", + model.OrganizationId, model.ProjectId); + return StatusCode(500, ApiResponse.ErrorResponse("Internal error", "An internal exception occurred", 500)); } } [HttpPost("assign/tenant/{organizationId}")] public async Task 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(); - using var scope = _serviceScope.CreateScope(); + + // 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(); - var organizationTenantMappingTask = Task.Run(async () => - { - await using var context = await _dbContextFactory.CreateDbContextAsync(); - return await context.TenantOrgMappings - .FirstOrDefaultAsync(spt => spt.OrganizationId == organizationId && spt.TenantId == tenantId && spt.IsActive); - }); + // Fetch existing tenant-organization mapping if any + var organizationTenantMapping = await _context.TenantOrgMappings + .FirstOrDefaultAsync(spt => spt.OrganizationId == organizationId && spt.TenantId == tenantId && spt.IsActive); - var organizationTask = Task.Run(async () => - { - await using var context = await _dbContextFactory.CreateDbContextAsync(); - return await context.Organizations - .FirstOrDefaultAsync(o => o.Id == organizationId); - }); - - await Task.WhenAll(organizationTenantMappingTask, organizationTask); - - var organizationTenantMapping = organizationTenantMappingTask.Result; - var organization = organizationTask.Result; + // 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.ErrorResponse("Organization not found", "Organization not found in database", 404)); } if (organizationTenantMapping == null) { - var newOrganizationTenantMapping = new TenantOrgMapping + // Create new tenant-organization mapping if none exists + var newMapping = new TenantOrgMapping { OrganizationId = organization.Id, SPRID = organization.SPRID, @@ -523,26 +557,38 @@ namespace Marco.Pms.Services.Controllers AssignedById = loggedInEmployee.Id, TenantId = tenantId }; - _context.TenantOrgMappings.Add(newOrganizationTenantMapping); + _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(organization); + return Ok(ApiResponse.SuccessResponse(response, "Organization has been assigned to tenant", 200)); } catch (DbUpdateException dbEx) { await transaction.RollbackAsync(); - - _logger.LogError(dbEx, "Database Exception has been occured, While assigning the organization to project"); - return StatusCode(500, ApiResponse.ErrorResponse("Internal error", "An database exception has been occured", 500)); + _logger.LogError(dbEx, "Database exception occurred while assigning organization {OrganizationId} to tenant {TenantId}.", organizationId, tenantId); + return StatusCode(500, ApiResponse.ErrorResponse("Internal error", "A database exception has occurred", 500)); } catch (Exception ex) { - _logger.LogError(ex, "Exception has been occured, While assigned the organizatio to project"); - return StatusCode(500, ApiResponse.ErrorResponse("Internal error", "An internal exception has been occured", 500)); + await transaction.RollbackAsync(); + _logger.LogError(ex, "Unhandled exception occurred while assigning organization {OrganizationId} to tenant {TenantId}.", organizationId, tenantId); + return StatusCode(500, ApiResponse.ErrorResponse("Internal error", "An internal exception has occurred", 500)); } } + #endregion #region =================================================================== Put Functions ===================================================================