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 _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 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 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 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 organizationIdsTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.TenantOrgMappings .Where(to => to.TenantId == tenantId && to.IsActive) .Select(to => to.OrganizationId) .ToListAsync(); }); var tenantTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.Tenants .FirstOrDefaultAsync(t => t.Id == tenantId && t.IsActive); }); await Task.WhenAll(organizationIdsTask, tenantTask); var organizationIds = organizationIdsTask.Result; var tenant = tenantTask.Result; if (tenant == null) { _logger.LogWarning("Tenant {TenantId} is not found", tenantId); return NotFound(ApiResponse.ErrorResponse("Tenant not found", "Tenant not found in database", 404)); } organizationIds.Add(tenant.OrganizationId); organizationIds = organizationIds.Distinct().ToList(); 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(); if (organizations.Any()) { organizations = await _context.Tenants.AsNoTracking() .Include(t => t.Organization) .Where(t => t.Id == tenantId && t.OrganizationId == loggedInEmployee.OrganizationId && t.Organization != null && t.IsActive) .Select(t => t.Organization!).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(o); orgVm.CreatedBy = employees.Where(e => e.Id == o.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault(); orgVm.UpdatedBy = employees.Where(e => e.Id == o.UpdatedById).Select(e => _mapper.Map(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.SuccessResponse(response, "Successfully fetched the organization list", 200)); } [HttpGet("details/{id}")] public async Task 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.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 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(organization); response.ActiveApplicationUserCount = activeApplicationUserCount; response.ActiveEmployeeCount = activeEmployeeCount; response.CreatedBy = _mapper.Map(createdByEmployee); response.UpdatedBy = _mapper.Map(updatedByEmployee); response.Projects = _mapper.Map>(projectOrgMappings.Select(po => po.ProjectService).ToList()); response.Services = orgServiceMappings.Where(os => os.Service != null).Select(os => os.Service!).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.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.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.ErrorResponse("Internal error", "An internal exception occurred", 500)); } } #endregion #region =================================================================== Post Functions =================================================================== [HttpPost("create")] public async Task 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(); 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.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(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(); var emailSender = scope.ServiceProvider.GetRequiredService(); var userManager = scope.ServiceProvider.GetRequiredService>(); // 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.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(organization); response.CreatedBy = _mapper.Map(loggedInEmployee); return Ok(ApiResponse.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.ErrorResponse("Internal error", "A database exception occurred", 500)); } catch (Exception ex) { _logger.LogError(ex, "Unexpected exception occurred while creating organization"); return StatusCode(500, ApiResponse.ErrorResponse("Internal error", "An unexpected error occurred", 500)); } } [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(); // 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.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 (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 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.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 = 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) { 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.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(organization); var parentOrganizationVm = _mapper.Map(parentOrganization); var projectVm = _mapper.Map(project); var response = services.Select(s => new AssignOrganizationVm { Project = projectVm, OrganizationType = organizationType, Organization = organizationVm, ParentOrganization = parentOrganizationVm, Service = _mapper.Map(s) }).ToList(); return Ok(ApiResponse.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.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.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(); // 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.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(organization); return Ok(ApiResponse.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.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.ErrorResponse("Internal error", "An internal exception has occurred", 500)); } } #endregion #region =================================================================== Put Functions =================================================================== [HttpPut("edit/{id}")] public async Task 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 organization 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.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.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.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(organization); var createdByEmployee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == organization.CreatedById); response.CreatedBy = _mapper.Map(createdByEmployee); response.UpdatedBy = _mapper.Map(loggedInEmployee); _logger.LogInfo("Successfully updated organization OrganizationId: {OrganizationId} by EmployeeId: {EmployeeId}", id, loggedInEmployee.Id); return Ok(ApiResponse.SuccessResponse(response, "Organization updated Successfully", 200)); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database exception occurred while updating OrganizationId: {OrganizationId}", id); return StatusCode(500, ApiResponse.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.ErrorResponse("Internal error", "An internal exception occurred", 500)); } } #endregion #region =================================================================== Delete Functions =================================================================== //[HttpDelete("delete/{id}")] //public async Task 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.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.ErrorResponse("Service Provider not Found", "Service Provider not Found in database", 404)); // } // if (serviceProvider.IsActive == active) // { // return BadRequest(ApiResponse.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.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.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(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(filter, options) ?? ""; // if (!string.IsNullOrWhiteSpace(unescapedJsonString)) // { // documentFilter = JsonSerializer.Deserialize(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 } }