using AutoMapper; using Marco.Pms.DataAccess.Data; using Marco.Pms.Helpers.Utility; using Marco.Pms.Model.Directory; using Marco.Pms.Model.DocumentManager; using Marco.Pms.Model.Dtos.Activities; using Marco.Pms.Model.Dtos.DocumentManager; using Marco.Pms.Model.Dtos.Master; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels.Utility; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Collection; using Marco.Pms.Model.ViewModels.DocumentManager; using Marco.Pms.Model.ViewModels.Master; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Service; using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; namespace Marco.Pms.Services.Service { public class MasterService : IMasterService { private readonly IDbContextFactory _dbContextFactory; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ApplicationDbContext _context; private readonly ILoggingService _logger; private readonly PermissionServices _permission; private readonly IMapper _mapper; private readonly UtilityMongoDBHelper _updateLogHelper; public MasterService( IDbContextFactory dbContextFactory, IServiceScopeFactory serviceScopeFactory, ApplicationDbContext context, ILoggingService logger, PermissionServices permission, IMapper mapper, UtilityMongoDBHelper updateLogHelper) { _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); _context = context ?? throw new ArgumentNullException(nameof(context)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _permission = permission ?? throw new ArgumentNullException(nameof(permission)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _updateLogHelper = updateLogHelper ?? throw new ArgumentNullException(nameof(updateLogHelper)); } #region =================================================================== Organization Type APIs =================================================================== public async Task> GetOrganizationTypesAsync(Employee loggedInEmployee, Guid tenantId) { _logger.LogDebug("GetOrganizationTypes called"); try { // Step 1: Fetch global services var services = await _context.OrgTypeMasters.OrderBy(ot => ot.Name).ToListAsync(); _logger.LogInfo("Fetched {Count} organization type records for tenantId: {TenantId}", services.Count, tenantId); return ApiResponse.SuccessResponse(services, $"{services.Count} record(s) of organization type fetched successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Error fetching organization type"); return ApiResponse.ErrorResponse("An error occurred while fetching organization type", ex.Message, 500); } } #endregion #region =================================================================== Global Services APIs =================================================================== public async Task> GetGlobalServicesAsync(Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("GetGlobalServices called"); try { // Step 1: Fetch global services var services = await _context.GlobalServiceMasters.ToListAsync(); _logger.LogInfo("Fetched {Count} global service records for tenantId: {TenantId}", services.Count, tenantId); return ApiResponse.SuccessResponse(services, $"{services.Count} record(s) of global services fetched successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Error fetching global services"); return ApiResponse.ErrorResponse("An error occurred while fetching global services", ex.Message, 500); } } public async Task> CreateGlobalServiceAsync(ServiceMasterDto serviceMasterDto, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("CreateGlobalService called with Name: {ServiceName}", serviceMasterDto.Name); try { // Step 1: Permission check var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManagePermission) { _logger.LogWarning("Access denied for employeeId: {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("You don't have access", "You don't have permission to take this action", 403); } // Step 2: Check for duplicate name bool isExist = await _context.GlobalServiceMasters .AnyAsync(s => s.Name == serviceMasterDto.Name); if (isExist) { _logger.LogWarning("Duplicate service name '{ServiceName}' attempted by employeeId: {EmployeeId}", serviceMasterDto.Name, loggedInEmployee.Id); return ApiResponse.ErrorResponse( $"Service with name '{serviceMasterDto.Name}' already exists", $"Service with name '{serviceMasterDto.Name}' already exists", 400); } // Step 3: Save new service GlobalServiceMaster service = _mapper.Map(serviceMasterDto); _context.GlobalServiceMasters.Add(service); await _context.SaveChangesAsync(); _logger.LogInfo("New global service '{ServiceName}' created successfully by employeeId: {EmployeeId}", service.Name, loggedInEmployee.Id); return ApiResponse.SuccessResponse(service, "New global service created successfully", 201); } catch (Exception ex) { _logger.LogError(ex, "Error creating service"); return ApiResponse.ErrorResponse("Failed to create global service", ex.Message, 500); } } //public async Task> UpdateGlobalServiceAsync(Guid id, ServiceMasterDto serviceMasterDto, Employee loggedInEmployee, Guid tenantId) //{ // _logger.LogInfo("UpdateService called for Id: {Id}", id); // try // { // // Step 1: Permission check // var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); // if (!hasPermission) // { // _logger.LogWarning("Access denied. EmployeeId: {EmployeeId} lacks Manage_Master permission.", loggedInEmployee.Id); // return ApiResponse.ErrorResponse("Access denied", "You don't have permission to take this action", 403); // } // // Step 2: Input validation // if (serviceMasterDto.Id != id) // { // _logger.LogWarning("Invalid input data provided for UpdateService. Id: {Id}", id); // return ApiResponse.ErrorResponse("Invalid input", "Please provide valid service data", 400); // } // // Step 3: Retrieve service // var service = await _context.ServiceMasters // .FirstOrDefaultAsync(s => s.Id == id && s.TenantId == tenantId && s.IsActive); // if (service == null) // { // _logger.LogWarning("Service not found for Id: {Id}, Tenant: {TenantId}", id, tenantId); // return ApiResponse.ErrorResponse("Service not found", "The requested service does not exist", 404); // } // // Step 4: Update and save // service.Name = serviceMasterDto.Name.Trim(); // service.Description = serviceMasterDto.Description.Trim(); // await _context.SaveChangesAsync(); // var response = _mapper.Map(service); // _logger.LogInfo("Service updated successfully. Id: {Id}, TenantId: {TenantId}", service.Id, tenantId); // return ApiResponse.SuccessResponse(response, "Service updated successfully", 200); // } // catch (Exception ex) // { // _logger.LogError(ex, "Error while updating service Id: {Id}.", id); // return ApiResponse.ErrorResponse("An error occurred while updating the service", ex.Message, 500); // } //} //public async Task> DeleteGlobalServiceAsync(Guid id, bool active, Employee loggedInEmployee, Guid tenantId) //{ // _logger.LogInfo("DeleteService called with ServiceId: {ServiceId}, IsActive: {IsActive}", id, active); // try // { // // Step 1: Get validate permission // var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); // if (!hasPermission) // { // _logger.LogWarning("Access denied. EmployeeId: {EmployeeId} does not have Manage_Master permission.", loggedInEmployee.Id); // return ApiResponse.ErrorResponse("Access denied", "You don't have permission to delete services", 403); // } // // Step 2: Check if the service exists // var service = await _context.ServiceMasters // .FirstOrDefaultAsync(s => s.Id == id && s.TenantId == tenantId); // if (service == null) // { // _logger.LogWarning("Service not found. ServiceId: {ServiceId}", id); // return ApiResponse.ErrorResponse("Service not found", "Service not found or already deleted", 404); // } // // Protect system-defined service // if (service.IsSystem) // { // _logger.LogWarning("Attempt to delete system-defined service. ServiceId: {ServiceId}", id); // return ApiResponse.ErrorResponse("Cannot delete system-defined service", "This service is system-defined and cannot be deleted", 400); // } // // Step 3: Soft delete or restore // service.IsActive = active; // await _context.SaveChangesAsync(); // var status = active ? "restored" : "deactivated"; // _logger.LogInfo("Service {ServiceId} has been {Status} successfully by EmployeeId: {EmployeeId}", id, status, loggedInEmployee.Id); // return ApiResponse.SuccessResponse(new { }, $"Service {status} successfully", 200); // } // catch (Exception ex) // { // _logger.LogError(ex, "Unexpected error occurred while deleting service {ServiceId}", id); // return ApiResponse.ErrorResponse("Error deleting service", ex.Message, 500); // } //} #endregion #region =================================================================== Services APIs =================================================================== public async Task> GetServicesAsync(Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("GetServices called"); try { // Step 1: Fetch services for the tenant var services = await _context.ServiceMasters .Where(s => s.TenantId == tenantId && s.IsActive) .Select(s => _mapper.Map(s)) .ToListAsync(); _logger.LogInfo("Fetched {Count} service records for tenantId: {TenantId}", services.Count, tenantId); return ApiResponse.SuccessResponse(services, $"{services.Count} record(s) of services fetched successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Error fetching services"); return ApiResponse.ErrorResponse("An error occurred while fetching services", ex.Message, 500); } } /// /// Asynchronously retrieves a structured list of services, their activity groups, activities, and associated checklists for a specific tenant. /// This method fetches data in multiple steps and processes it in-memory, preserving the original business logic. /// /// The employee currently logged in. This parameter is available for future authorization or logging extensions. /// The unique identifier of the tenant for which to retrieve the data. /// An ApiResponse containing a list of ServiceDetailsListVM or an error message in case of failure. public async Task> GetServiceDetailsListAsync(Guid? serviceId, Employee loggedInEmployee, Guid tenantId) { // Log the initiation of the request with structured parameters for better traceability. _logger.LogInfo("Attempting to fetch service details list for TenantId: {TenantId}", tenantId); try { // --- Step 1: Fetch all relevant activities for the tenant --- // This query retrieves all active 'ActivityMaster' records for the given tenant that are properly linked to an ActivityGroup and a Service. // Eager loading (.Include/.ThenInclude) is used to bring in related ActivityGroup and Service entities to prevent N+1 query problems later. var activityQuery = _context.ActivityMasters .Include(a => a.ActivityGroup) .ThenInclude(ag => ag!.Service) .Where(a => a.TenantId == tenantId && a.IsActive); if (serviceId.HasValue) { activityQuery = activityQuery.Where(a => a.ActivityGroup != null && a.ActivityGroup.Service != null && a.ActivityGroup.Service.Id == serviceId); } var activities = await activityQuery .ToListAsync(); _logger.LogInfo("Step 1 complete: Fetched {ActivityCount} activities for TenantId: {TenantId}", activities.Count, tenantId); // --- Step 2: Fetch all checklists related to the retrieved activities --- // To avoid fetching all checklists in the database, we first collect the IDs of the activities from the previous step. var activityIds = activities.Select(a => a.Id).ToList(); // This second database query fetches only the 'ActivityCheckList' records associated with the relevant activities. var checkLists = await _context.ActivityCheckLists .Where(c => c.TenantId == tenantId && activityIds.Contains(c.ActivityId)) .ToListAsync(); _logger.LogInfo("Step 2 complete: Fetched {ChecklistCount} checklists for TenantId: {TenantId}", checkLists.Count, tenantId); // --- Step 3: Group checklists by activity in memory for efficient lookup --- // To quickly find all checklists for a given activity, we group the flat list of checklists into a dictionary. // The key is the ActivityId, and the value is the list of associated checklists. var groupedChecklists = checkLists .GroupBy(c => c.ActivityId) .ToDictionary(g => g.Key, g => g.ToList()); // --- Step 4: Build the hierarchical ViewModel structure in memory --- // First, get distinct lists of services and activity groups from the already fetched 'activities' collection. var services = activities.Select(a => a.ActivityGroup!.Service!).Distinct().ToList(); var activityGroupList = activities.Select(a => a.ActivityGroup!).Distinct().ToList(); // Now, construct the final nested ViewModel. // This part iterates through the distinct services and builds the response object by filtering the in-memory collections. List Vm = services.Select(s => { var response = new ServiceDetailsListVM { Id = s.Id, Name = s.Name, Description = s.Description, IsActive = s.IsActive, IsSystem = s.IsSystem, ActivityGroups = activityGroupList .Where(ag => ag.ServiceId == s.Id) // Find groups for the current service .Select(ag => new ActivityGroupDetailsListVM { Id = ag.Id, Name = ag.Name, Description = ag.Description, IsActive = ag.IsActive, IsSystem = ag.IsSystem, Activities = activities .Where(a => a.ActivityGroupId == ag.Id) // Find activities for the current group .Select(a => { // Retrieve the checklists for the current activity from our dictionary lookup. var checklistForActivity = groupedChecklists.ContainsKey(a.Id) ? groupedChecklists[a.Id] : new List(); return new ActivityVM { Id = a.Id, ActivityName = a.ActivityName, UnitOfMeasurement = a.UnitOfMeasurement, // BUG FIX: Correctly mapping properties from the activity ('a') itself, not the parent service ('s'). IsActive = a.IsActive, IsSystem = a.IsSystem, CheckLists = _mapper.Map>(checklistForActivity) }; }).ToList() }).ToList() }; return response; }).ToList(); _logger.LogInfo("Successfully processed and mapped {ServiceCount} services for TenantId: {TenantId}", Vm.Count, tenantId); return ApiResponse.SuccessResponse(Vm, "Service details list fetched successfully", 200); } catch (Exception ex) { // If any part of the process fails, log the detailed exception and return a standardized error response. _logger.LogError(ex, "An error occurred while fetching service details for TenantId: {TenantId}", tenantId); return ApiResponse.ErrorResponse("An internal server error occurred while fetching service details.", 500); } } public async Task> CreateServiceAsync(ServiceMasterDto serviceMasterDto, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("CreateService called with Name: {ServiceName}", serviceMasterDto.Name); try { // Step 1: Permission check var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManagePermission) { _logger.LogWarning("Access denied for employeeId: {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("You don't have access", "You don't have permission to take this action", 403); } // Step 2: Check for duplicate name bool isExist = await _context.ServiceMasters .AnyAsync(s => s.TenantId == tenantId && s.Name == serviceMasterDto.Name); if (isExist) { _logger.LogWarning("Duplicate service name '{ServiceName}' attempted by employeeId: {EmployeeId}", serviceMasterDto.Name, loggedInEmployee.Id); return ApiResponse.ErrorResponse( $"Service with name '{serviceMasterDto.Name}' already exists", $"Service with name '{serviceMasterDto.Name}' already exists", 400); } // Step 3: Save new service ServiceMaster service = _mapper.Map(serviceMasterDto); service.TenantId = tenantId; service.IsActive = true; service.IsSystem = false; _context.ServiceMasters.Add(service); await _context.SaveChangesAsync(); var response = _mapper.Map(service); _logger.LogInfo("New service '{ServiceName}' created successfully by employeeId: {EmployeeId}", service.Name, loggedInEmployee.Id); return ApiResponse.SuccessResponse(response, "New service created successfully", 201); } catch (Exception ex) { _logger.LogError(ex, "Error creating service"); return ApiResponse.ErrorResponse("Failed to create service", ex.Message, 500); } } public async Task> UpdateServiceAsync(Guid id, ServiceMasterDto serviceMasterDto, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("UpdateService called for Id: {Id}", id); try { // Step 1: Permission check var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasPermission) { _logger.LogWarning("Access denied. EmployeeId: {EmployeeId} lacks Manage_Master permission.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access denied", "You don't have permission to take this action", 403); } // Step 2: Input validation if (serviceMasterDto.Id != id) { _logger.LogWarning("Invalid input data provided for UpdateService. Id: {Id}", id); return ApiResponse.ErrorResponse("Invalid input", "Please provide valid service data", 400); } // Step 3: Retrieve service var service = await _context.ServiceMasters .FirstOrDefaultAsync(s => s.Id == id && s.TenantId == tenantId && s.IsActive); if (service == null) { _logger.LogWarning("Service not found for Id: {Id}, Tenant: {TenantId}", id, tenantId); return ApiResponse.ErrorResponse("Service not found", "The requested service does not exist", 404); } var existingEntityBson = _updateLogHelper.EntityToBsonDocument(service); // Step 4: Update and save service.Name = serviceMasterDto.Name.Trim(); service.Description = serviceMasterDto.Description.Trim(); await _context.SaveChangesAsync(); var response = _mapper.Map(service); await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = service.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "ServiceMasterModificationLog"); _logger.LogInfo("Service updated successfully. Id: {Id}, TenantId: {TenantId}", service.Id, tenantId); return ApiResponse.SuccessResponse(response, "Service updated successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Error while updating service Id: {Id}.", id); return ApiResponse.ErrorResponse("An error occurred while updating the service", ex.Message, 500); } } public async Task> DeleteServiceAsync(Guid id, bool active, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("DeleteService called with ServiceId: {ServiceId}, IsActive: {IsActive}", id, active); try { // Step 1: Get validate permission var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasPermission) { _logger.LogWarning("Access denied. EmployeeId: {EmployeeId} does not have Manage_Master permission.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access denied", "You don't have permission to delete services", 403); } // Step 2: Check if the service exists var service = await _context.ServiceMasters .FirstOrDefaultAsync(s => s.Id == id && s.TenantId == tenantId); if (service == null) { _logger.LogWarning("Service not found. ServiceId: {ServiceId}", id); return ApiResponse.ErrorResponse("Service not found", "Service not found or already deleted", 404); } // Protect system-defined service if (service.IsSystem) { _logger.LogWarning("Attempt to delete system-defined service. ServiceId: {ServiceId}", id); return ApiResponse.ErrorResponse("Cannot delete system-defined service", "This service is system-defined and cannot be deleted", 400); } var activityGroupExists = await _context.ActivityGroupMasters.AnyAsync(ag => ag.ServiceId == service.Id && ag.TenantId == tenantId); if (activityGroupExists) { _logger.LogWarning("Activity group exists for this cannot be deleted ServiceId: {ServiceId}", id); return ApiResponse.ErrorResponse("Activity group existed for this service cannot delete", "Activity group existed for this service cannot delete", 400); } var existingEntityBson = _updateLogHelper.EntityToBsonDocument(service); // Step 3: Soft delete or restore service.IsActive = active; await _context.SaveChangesAsync(); await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = service.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "ServiceMasterModificationLog"); var status = active ? "restored" : "deactivated"; _logger.LogInfo("Service {ServiceId} has been {Status} successfully by EmployeeId: {EmployeeId}", id, status, loggedInEmployee.Id); return ApiResponse.SuccessResponse(new { }, $"Service {status} successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Unexpected error occurred while deleting service {ServiceId}", id); return ApiResponse.ErrorResponse("Error deleting service", ex.Message, 500); } } #endregion #region =================================================================== Activity Group APIs =================================================================== public async Task> GetActivityGroupsAsync(Guid? serviceId, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("GetActivityGroups called"); try { // Step 1: Fetch all activity groups for the tenant var activityGroupQuery = _context.ActivityGroupMasters .Include(ag => ag.Service) .Where(ag => ag.TenantId == tenantId && ag.IsActive); if (serviceId.HasValue) { activityGroupQuery = activityGroupQuery.Where(ag => ag.ServiceId == serviceId); } var activityGroups = await activityGroupQuery .Select(ag => _mapper.Map(ag)) .ToListAsync(); _logger.LogInfo("{Count} activity group(s) fetched for tenantId: {TenantId}", activityGroups.Count, tenantId); return ApiResponse.SuccessResponse(activityGroups, $"{activityGroups.Count} record(s) of activity groups fetched successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Error fetching activity groups"); return ApiResponse.ErrorResponse("An error occurred while fetching activity groups", ex.Message, 500); } } public async Task> CreateActivityGroupAsync(ActivityGroupDto activityGroupDto, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("CreateActivityGroup called with Name: {Name}", activityGroupDto.Name); try { // Step 1: Check Manage Master permission var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasPermission) { _logger.LogWarning("Access denied. EmployeeId: {EmployeeId} lacks Manage_Master permission", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access denied", "You don't have permission to take this action", 403); } // Step 2: Check for duplicate name within ActivityGroupMasters bool isExist = await _context.ActivityGroupMasters .AnyAsync(ag => ag.TenantId == tenantId && ag.Name.ToLower() == activityGroupDto.Name.ToLower()); if (isExist) { _logger.LogWarning("Duplicate activity group name '{Name}' attempted by employeeId: {EmployeeId}", activityGroupDto.Name, loggedInEmployee.Id); return ApiResponse.ErrorResponse( $"Activity group with name '{activityGroupDto.Name}' already exists", $"Activity group with name '{activityGroupDto.Name}' already exists", 400); } // Step 3: Map and persist var activityGroup = _mapper.Map(activityGroupDto); activityGroup.TenantId = tenantId; activityGroup.IsActive = true; activityGroup.IsSystem = false; _context.ActivityGroupMasters.Add(activityGroup); await _context.SaveChangesAsync(); var service = await _context.ServiceMasters.FirstOrDefaultAsync(s => s.Id == activityGroup.ServiceId); var response = _mapper.Map(activityGroup); _logger.LogInfo("New activity group '{Name}' created successfully by employeeId: {EmployeeId}", activityGroup.Name, loggedInEmployee.Id); return ApiResponse.SuccessResponse(response, "New activity group created successfully", 201); } catch (Exception ex) { _logger.LogError(ex, "Error creating activity group"); return ApiResponse.ErrorResponse("Failed to create activity group", ex.Message, 500); } } public async Task> UpdateActivityGroupAsync(Guid id, ActivityGroupDto activityGroupDto, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("UpdateActivityGroup called for Id: {Id}", id); try { // Step 1: Permission check var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasPermission) { _logger.LogWarning("Access denied. EmployeeId: {EmployeeId} lacks Manage_Master permission.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access denied", "You don't have permission to take this action", 403); } // Step 2: Input validation if (activityGroupDto.Id != id) { _logger.LogWarning("Invalid input for activity group update. Id: {Id}", id); return ApiResponse.ErrorResponse("Invalid input", "Please provide valid data to update activity group", 400); } var service = await _context.ServiceMasters.FirstOrDefaultAsync(s => s.Id == activityGroupDto.ServiceId && s.IsActive); if (service == null) { _logger.LogWarning("User tries to update activity group, but service not found"); return ApiResponse.ErrorResponse("Invalid service ID", "Please provide valid service ID to update activity group", 400); } // Step 3: Retrieve existing activity group var activityGroup = await _context.ActivityGroupMasters .Include(ag => ag.Service) .FirstOrDefaultAsync(ag => ag.Id == id && ag.TenantId == tenantId && ag.IsActive); if (activityGroup == null) { _logger.LogWarning("Activity group not found. Id: {Id}, TenantId: {TenantId}", id, tenantId); return ApiResponse.ErrorResponse("Activity group not found", "No such activity group exists", 404); } var existingEntityBson = _updateLogHelper.EntityToBsonDocument(activityGroup); // Step 4: Update and save activityGroup.Name = activityGroupDto.Name.Trim(); activityGroup.Description = activityGroupDto.Description.Trim(); activityGroup.ServiceId = activityGroupDto.ServiceId; await _context.SaveChangesAsync(); var response = _mapper.Map(activityGroup); await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = activityGroup.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "ActivityGroupMasterModificationLog"); _logger.LogInfo("Activity group updated successfully. Id: {Id}, TenantId: {TenantId}", activityGroup.Id, tenantId); return ApiResponse.SuccessResponse(response, "Activity group updated successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Error while updating activity group Id: {Id}", id); return ApiResponse.ErrorResponse("An error occurred while updating the activity group", ex.Message, 500); } } public async Task> DeleteActivityGroupAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("DeleteActivityGroup called with ActivityGroupId: {ActivityGroupId}, IsActive: {IsActive}", id, isActive); try { // Step 1: Permission check var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasPermission) { _logger.LogWarning("Access denied. EmployeeId: {EmployeeId} does not have Manage_Master permission.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access denied", "You don't have permission to delete activity groups", 403); } // Step 2: Fetch the activity group var activityGroup = await _context.ActivityGroupMasters .FirstOrDefaultAsync(ag => ag.Id == id && ag.TenantId == tenantId); if (activityGroup == null) { _logger.LogWarning("ActivityGroup not found. Id: {ActivityGroupId}", id); return ApiResponse.ErrorResponse("Activity group not found", "Activity group not found or already deleted", 404); } //Protect system-defined activity group if (activityGroup.IsSystem) { _logger.LogWarning("Attempt to delete system-defined activity group. Id: {ActivityGroupId}", id); return ApiResponse.ErrorResponse("Cannot delete system-defined activity group", "This activity group is system-defined and cannot be deleted", 400); } var activityExists = await _context.ActivityMasters.AnyAsync(ag => ag.ActivityGroupId == activityGroup.Id && ag.TenantId == tenantId); if (activityExists) { _logger.LogWarning("Activity exists for this cannot be deleted ActivityGroupId: {ActivityGroupId}", id); return ApiResponse.ErrorResponse("Activity existed for this service cannot delete", "Activity existed for this service cannot delete", 400); } var existingEntityBson = _updateLogHelper.EntityToBsonDocument(activityGroup); // Step 3: Perform soft delete or restore activityGroup.IsActive = isActive; await _context.SaveChangesAsync(); await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = activityGroup.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "ActivityGroupMasterModificationLog"); var status = isActive ? "restored" : "deactivated"; _logger.LogInfo("ActivityGroup {ActivityGroupId} has been {Status} by EmployeeId: {EmployeeId}", id, status, loggedInEmployee.Id); return ApiResponse.SuccessResponse(new { }, $"Activity group {status} successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Unexpected error occurred while deleting ActivityGroup {ActivityGroupId}", id); return ApiResponse.ErrorResponse("Error deleting activity group", ex.Message, 500); } } #endregion #region =================================================================== Activity APIs =================================================================== public async Task> GetActivitiesMasterAsync(Guid? activityGroupId, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("GetActivitiesMaster called"); try { // Step 1: Fetch all active activities for the tenant var activityQuery = _context.ActivityMasters .Include(c => c.ActivityGroup) .ThenInclude(ag => ag!.Service) .Where(c => c.TenantId == tenantId && c.IsActive); if (activityGroupId.HasValue) { activityQuery = activityQuery.Where(a => a.ActivityGroupId == activityGroupId); } var activities = await activityQuery .ToListAsync(); if (activities.Count == 0) { _logger.LogWarning("No active activities found for tenantId: {TenantId}", tenantId); return ApiResponse.SuccessResponse(new List(), "No activity records found", 200); } // Step 2: Fetch all checklists for those activities in a single query to avoid N+1 var activityIds = activities.Select(a => a.Id).ToList(); var checkLists = await _context.ActivityCheckLists .Where(c => c.TenantId == tenantId && activityIds.Contains(c.ActivityId)) .ToListAsync(); // Step 3: Group checklists by activity var groupedChecklists = checkLists .GroupBy(c => c.ActivityId) .ToDictionary(g => g.Key, g => g.ToList()); // Step 4: Map to ViewModel var activityVMs = activities.Select(activity => { var checklistForActivity = groupedChecklists.ContainsKey(activity.Id) ? groupedChecklists[activity.Id] : new List(); var response = _mapper.Map(activity); response.CheckLists = _mapper.Map>(checklistForActivity); return response; }).ToList(); _logger.LogInfo("{Count} activity records fetched successfully for tenantId: {TenantId}", activityVMs.Count, tenantId); return ApiResponse.SuccessResponse(activityVMs, $"{activityVMs.Count} activity records fetched successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Error occurred in GetActivitiesMaster"); return ApiResponse.ErrorResponse("Failed to fetch activity records", ex.Message, 500); } } public async Task> CreateActivityAsync(CreateActivityMasterDto createActivity, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("CreateActivity called with ActivityName: {Name}", createActivity?.ActivityName ?? "null"); try { // Step 1: Validate input if (createActivity == null) { _logger.LogWarning("Null request body received in CreateActivity"); return ApiResponse.ErrorResponse("Invalid input", "Activity data is required", 400); } var activityGroup = await _context.ActivityGroupMasters .Include(ag => ag.Service) .FirstOrDefaultAsync(ag => ag.Id == createActivity.ActivityGroupId && ag.TenantId == tenantId); if (activityGroup == null) { _logger.LogWarning("User tried to create new activity, but not found activity group"); return ApiResponse.ErrorResponse("Invalid input", "Activity data is required", 400); } // Step 2: Check permissions var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasPermission) { _logger.LogWarning("Access denied. EmployeeId: {EmployeeId} does not have Manage_Master permission.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access denied", "You don't have permission to perform this action", 403); } // Step 3: Convert DTO to entity and add activity var activityMaster = _mapper.Map(createActivity); activityMaster.TenantId = tenantId; _context.ActivityMasters.Add(activityMaster); await _context.SaveChangesAsync(); List checkListVMs = new(); // Step 4: Handle checklist items if present if (createActivity.CheckList?.Any() == true) { var activityCheckLists = createActivity.CheckList .Select(c => { var response = _mapper.Map(c); response.ActivityId = activityMaster.Id; response.IsChecked = false; response.TenantId = tenantId; return response; }) .ToList(); _context.ActivityCheckLists.AddRange(activityCheckLists); await _context.SaveChangesAsync(); checkListVMs = activityCheckLists .Select(c => { var response = _mapper.Map(c); return response; }) .ToList(); } // Step 5: Prepare final response var activityVM = _mapper.Map(activityMaster); activityVM.CheckLists = checkListVMs; _logger.LogInfo("Activity '{Name}' created successfully for tenant {TenantId}", activityMaster.ActivityName, tenantId); return ApiResponse.SuccessResponse(activityVM, "Activity created successfully", 201); } catch (Exception ex) { _logger.LogError(ex, "Error occurred while creating activity"); return ApiResponse.ErrorResponse("An error occurred while creating activity", ex.Message, 500); } } public async Task> UpdateActivityAsync(Guid id, CreateActivityMasterDto createActivity, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("UpdateActivity called for ActivityId: {ActivityId}", id); try { // Step 1: Validate input if (createActivity == null || string.IsNullOrWhiteSpace(createActivity.ActivityName) || string.IsNullOrWhiteSpace(createActivity.UnitOfMeasurement)) { _logger.LogWarning("Invalid activity update input for ActivityId: {ActivityId}", id); return ApiResponse.ErrorResponse("Invalid input", "ActivityName and UnitOfMeasurement are required", 400); } // Step 2: Check permissions var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasPermission) { _logger.LogWarning("Access denied for employeeId: {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access denied", "You don't have permission to update activities", 403); } // Step 3: Validate service, activity group, and activity existence var activityGroup = await _context.ActivityGroupMasters.Include(ag => ag.Service).FirstOrDefaultAsync(ag => ag.Id == createActivity.ActivityGroupId && ag.IsActive); if (activityGroup == null) { _logger.LogWarning("User tries to update activity, but cannot able found activity group or service"); return ApiResponse.ErrorResponse("Invalid activity group ID or service ID", "Please provide valid activity group ID or service ID to update activity group", 400); } var activity = await _context.ActivityMasters .Include(a => a.ActivityGroup) .ThenInclude(ag => ag!.Service) .FirstOrDefaultAsync(a => a.Id == id && a.IsActive && a.TenantId == tenantId); if (activity == null) { _logger.LogWarning("Activity not found for ActivityId: {ActivityId}, TenantId: {TenantId}", id, tenantId); return ApiResponse.ErrorResponse("Activity not found", "Activity not found", 404); } var existingEntityBson = _updateLogHelper.EntityToBsonDocument(activity); // Step 4: Update activity core data activity.ActivityName = createActivity.ActivityName.Trim(); activity.UnitOfMeasurement = createActivity.UnitOfMeasurement.Trim(); activity.ActivityGroupId = createActivity.ActivityGroupId; await _context.SaveChangesAsync(); await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = activity.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "ActivityMasterModificationLog"); // Step 5: Handle checklist updates var existingChecklists = await _context.ActivityCheckLists .AsNoTracking() .Where(c => c.ActivityId == activity.Id) .ToListAsync(); var updatedChecklistVMs = new List(); if (createActivity.CheckList != null && createActivity.CheckList.Any()) { var incomingCheckIds = createActivity.CheckList.Where(c => c.Id != null).Select(c => c.Id!.Value).ToHashSet(); // Prepare lists var newChecks = createActivity.CheckList.Where(c => c.Id == null); var updates = createActivity.CheckList.Where(c => c.Id != null && existingChecklists.Any(ec => ec.Id == c.Id)); var deletes = existingChecklists.Where(ec => !incomingCheckIds.Contains(ec.Id)).ToList(); var toAdd = newChecks .Select(c => { var response = _mapper.Map(c); response.ActivityId = activity.Id; response.IsChecked = false; response.TenantId = tenantId; return response; }) .ToList(); var toUpdate = updates .Select(c => { var response = _mapper.Map(c); response.ActivityId = activity.Id; response.TenantId = tenantId; return response; }) .ToList(); _context.ActivityCheckLists.AddRange(toAdd); _context.ActivityCheckLists.UpdateRange(toUpdate); _context.ActivityCheckLists.RemoveRange(deletes); await _context.SaveChangesAsync(); // Prepare view model updatedChecklistVMs = toAdd.Concat(toUpdate) .Select(c => _mapper.Map(c)) .ToList(); } else if (existingChecklists.Any()) { // If no checklist provided, remove all existing _context.ActivityCheckLists.RemoveRange(existingChecklists); await _context.SaveChangesAsync(); } // Step 6: Prepare response var activityVM = _mapper.Map(activity); activityVM.CheckLists = updatedChecklistVMs; _logger.LogInfo("Activity updated successfully. ActivityId: {ActivityId}, TenantId: {TenantId}", activity.Id, tenantId); return ApiResponse.SuccessResponse(activityVM, "Activity updated successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Exception in UpdateActivity"); return ApiResponse.ErrorResponse("Error updating activity", ex.Message, 500); } } public async Task> DeleteActivityAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("DeleteActivity called with ActivityId: {ActivityId}, IsActive: {IsActive}", id, isActive); try { // Step 1: Validate permission var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasPermission) { _logger.LogWarning("Access denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access denied", "You don't have permission to delete activities", 403); } // Step 2: Fetch the activity var activity = await _context.ActivityMasters .FirstOrDefaultAsync(a => a.Id == id && a.TenantId == tenantId); if (activity == null) { _logger.LogWarning("Activity not found. ActivityId: {ActivityId}", id); return ApiResponse.ErrorResponse("Activity not found", "Activity not found or already deleted", 404); } var existingEntityBson = _updateLogHelper.EntityToBsonDocument(activity); // Step 3: Perform soft delete/restore activity.IsActive = isActive; await _context.SaveChangesAsync(); await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = activity.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "ActivityMasterModificationLog"); string status = isActive ? "restored" : "deactivated"; _logger.LogInfo("Activity {ActivityId} {Status} successfully by EmployeeId: {EmployeeId}", id, status, loggedInEmployee.Id); return ApiResponse.SuccessResponse(new { }, $"Activity {status} successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Unexpected error while deleting activity {ActivityId}", id); return ApiResponse.ErrorResponse("Error deleting activity", ex.Message, 500); } } #endregion #region =================================================================== Contact Category APIs =================================================================== /// /// Retrieves the list of contact categories for the specified tenant. /// Ensures the tenantId is valid, logs relevant information and handles errors gracefully. /// /// The employee making the request. /// The unique identifier for the tenant. /// ApiResponse containing a list of contact categories. public async Task> GetContactCategoriesList(Employee loggedInEmployee, Guid tenantId) { // Validate parameters if (loggedInEmployee == null) { _logger.LogWarning("Attempt to fetch contact categories with null employee object"); return ApiResponse.ErrorResponse("Invalid employee context", "Invalid employee context", 400); } if (tenantId == Guid.Empty) { _logger.LogWarning("Attempt to fetch contact categories with empty tenantId by Employee ID {LoggedInEmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid tenant identifier", "Invalid tenant identifier", 400); } try { // Fetch categories filtered by tenantId, ensuring no unnecessary tracking var categoryList = await _context.ContactCategoryMasters .AsNoTracking() .Where(c => c.TenantId == tenantId) .ToListAsync(); // Map database entities to view models var contactCategories = _mapper.Map>(categoryList); int fetchedCount = contactCategories.Count; _logger.LogInfo("{Count} contact categories fetched for TenantId {TenantId} by Employee ID {EmployeeId}", fetchedCount, tenantId, loggedInEmployee.Id); return ApiResponse.SuccessResponse(contactCategories, $"{fetchedCount} contact categories fetched successfully", 200); } catch (Exception ex) { // Log exception details with context _logger.LogError(ex, "Error fetching contact categories for TenantId {TenantId}, Employee ID {EmployeeId}", tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("An unexpected error occurred while fetching categories", "An unexpected error occurred while fetching categories", 500); } } /// /// Retrieves a single contact category by its unique ID and associated tenant. /// Validates parameters, logs operations, and handles exceptions gracefully. /// /// Unique identifier for the contact category. /// Employee requesting the data. /// Unique identifier for the tenant. /// ApiResponse with the contact category data, or error details. public async Task> GetContactCategoryById(Guid id, Employee loggedInEmployee, Guid tenantId) { // Validate required parameters if (loggedInEmployee == null) { _logger.LogWarning("Null employee object provided when fetching contact category {ContactCategoryID}", id); return ApiResponse.ErrorResponse("Invalid employee context", "Invalid employee context", 400); } if (tenantId == Guid.Empty) { _logger.LogWarning("Empty tenantId provided by Employee {EmployeeId} when fetching contact category {ContactCategoryID}", loggedInEmployee.Id, id); return ApiResponse.ErrorResponse("Invalid tenant identifier", "Invalid tenant identifier", 400); } if (id == Guid.Empty) { _logger.LogWarning("Empty contact category ID specified by Employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid contact category ID", "Invalid contact category ID", 400); } try { // Efficient search for category, read-only query var category = await _context.ContactCategoryMasters .AsNoTracking() .FirstOrDefaultAsync(c => c.Id == id && c.TenantId == tenantId); if (category == null) { _logger.LogWarning("Employee {EmployeeId} attempted to fetch contact category {ContactCategoryID} (TenantId {TenantId}), but it was not found", loggedInEmployee.Id, id, tenantId); return ApiResponse.ErrorResponse("Category not found", "Category not found", 404); } // Map database entity to ViewModel var categoryVM = _mapper.Map(category); _logger.LogInfo("Employee {EmployeeId} fetched contact category {ContactCategoryID} (TenantId {TenantId}) successfully", loggedInEmployee.Id, category.Id, tenantId); return ApiResponse.SuccessResponse(categoryVM, "Category fetched successfully", 200); } catch (Exception ex) { // Exception logging with relevant context _logger.LogError(ex, "Error fetching contact category {ContactCategoryID} for TenantId {TenantId} by Employee {EmployeeId}", id, tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("An unexpected error occurred while fetching category", "An unexpected error occurred while fetching category", 500); } } /// /// Creates a new contact category for the specified tenant. /// Ensures the category name is unique within the tenant and logs all relevant actions. /// /// The DTO containing category creation data. /// The employee initiating the request. /// The tenant identifier to scope the operation. /// ApiResponse containing the created category or error details. public async Task> CreateContactCategory(CreateContactCategoryDto model, Employee loggedInEmployee, Guid tenantId) { // Validate input parameters if (loggedInEmployee == null) { _logger.LogWarning("CreateContactCategory: Request with null employee context"); return ApiResponse.ErrorResponse("Invalid employee context", "Invalid employee context", 400); } if (tenantId == Guid.Empty) { _logger.LogWarning("CreateContactCategory: Invalid tenant ID {TenantId} provided by Employee {EmployeeId}", tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid tenant identifier", "Invalid tenant identifier", 400); } if (model == null) { _logger.LogWarning("Employee {EmployeeId} sent empty payload for contact category creation", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Request payload cannot be null", "Request payload cannot be null", 400); } // Trim and validate name to prevent duplicates due to whitespace // Trim and validate name string trimmedName = model.Name.Trim(); if (string.IsNullOrWhiteSpace(trimmedName)) { _logger.LogWarning("Employee {EmployeeId} attempted to create contact category with empty or whitespace-only name", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Category name is required", "Category name is required", 400); } try { // Check for existing category with same name in the tenant bool categoryExists = await _context.ContactCategoryMasters .AnyAsync(c => c.TenantId == tenantId && c.Name == trimmedName); if (categoryExists) { _logger.LogWarning("Employee {EmployeeId} attempted to create duplicate contact category with name '{CategoryName}' for Tenant {TenantId}", loggedInEmployee.Id, trimmedName, tenantId); return ApiResponse.ErrorResponse("A category with this name already exists", "A category with this name already exists", 409); } // Map DTO to entity var contactCategory = new ContactCategoryMaster { Id = Guid.NewGuid(), // Ensure new ID is generated Name = trimmedName, Description = model.Description.Trim(), // Normalize description TenantId = tenantId }; // Add and save to database _context.ContactCategoryMasters.Add(contactCategory); await _context.SaveChangesAsync(); // Map to response model var categoryVM = _mapper.Map(contactCategory); _logger.LogInfo("Contact category created successfully: ID {ContactCategoryId}, Name '{CategoryName}', Tenant {TenantId}, by Employee {EmployeeId}", contactCategory.Id, contactCategory.Name, tenantId, loggedInEmployee.Id); return ApiResponse.SuccessResponse(categoryVM, "Category created successfully", 201); // 201 Created } catch (Exception ex) { _logger.LogError(ex, "Error creating contact category for Tenant {TenantId} by Employee {EmployeeId}. Payload: {Payload}", tenantId, loggedInEmployee.Id, model.Name); return ApiResponse.ErrorResponse("An error occurred while creating the category", "An error occurred while creating the category", 500); } } /// /// Updates an existing contact category within the specified tenant. /// Validates ownership, ensures data integrity, logs changes, and supports audit tracking. /// /// The unique identifier of the contact category to update. /// The DTO containing updated category data. /// The employee initiating the update. /// The tenant identifier to scope the operation. /// ApiResponse containing the updated category or error details. public async Task> UpdateContactCategory(Guid id, UpdateContactCategoryDto model, Employee loggedInEmployee, Guid tenantId) { // Validate input parameters if (loggedInEmployee == null) { _logger.LogWarning("UpdateContactCategory: Request with null employee context"); return ApiResponse.ErrorResponse("Invalid employee context", "Invalid employee context", 400); } if (tenantId == Guid.Empty) { _logger.LogWarning("UpdateContactCategory: Invalid tenant ID {TenantId} provided by Employee {EmployeeId}", tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid tenant identifier", "Invalid tenant identifier", 400); } if (id == Guid.Empty) { _logger.LogWarning("UpdateContactCategory: Invalid category ID {CategoryId} provided by Employee {EmployeeId}", id, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid category identifier", "Invalid category identifier", 400); } if (model == null) { _logger.LogWarning("Employee {EmployeeId} sent null DTO for updating contact category {CategoryId}", loggedInEmployee.Id, id); return ApiResponse.ErrorResponse("Request payload cannot be null", "Request payload cannot be null", 400); } if (id != model.Id) { _logger.LogWarning("Employee {EmployeeId} attempted to update category {CategoryId} with mismatched DTO ID {DtoId}", loggedInEmployee.Id, id, model.Id); return ApiResponse.ErrorResponse("Category ID mismatch between route and payload", "Category ID mismatch between route and payload", 400); } try { // Fetch the existing category with tenant scoping var contactCategory = await _context.ContactCategoryMasters .FirstOrDefaultAsync(c => c.Id == id && c.TenantId == tenantId); if (contactCategory == null) { _logger.LogWarning("Employee {EmployeeId} attempted to update non-existent contact category {CategoryId} for Tenant {TenantId}", loggedInEmployee.Id, id, tenantId); return ApiResponse.ErrorResponse("Category not found", "Category not found", 404); } // Trim and validate name string trimmedName = model.Name.Trim(); if (string.IsNullOrWhiteSpace(trimmedName)) { _logger.LogWarning("Employee {EmployeeId} attempted to update category {CategoryId} with empty or whitespace-only name", loggedInEmployee.Id, id); return ApiResponse.ErrorResponse("Category name is required", "Category name is required", 400); } // Check for duplicate name within tenant (excluding current category) bool nameExists = await _context.ContactCategoryMasters .AnyAsync(c => c.TenantId == tenantId && c.Name == trimmedName && c.Id != id); if (nameExists) { _logger.LogWarning("Employee {EmployeeId} attempted to rename category {CategoryId} to '{NewName}', which already exists in Tenant {TenantId}", loggedInEmployee.Id, id, trimmedName, tenantId); return ApiResponse.ErrorResponse("A category with this name already exists", "A category with this name already exists", 409); } // Capture original state for audit log var existingEntityBson = _updateLogHelper.EntityToBsonDocument(contactCategory); // Update entity properties contactCategory.Name = trimmedName; contactCategory.Description = model.Description.Trim(); // Normalize description // Log update in directory and audit trail _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog { RefereanceId = contactCategory.Id, UpdatedById = loggedInEmployee.Id, UpdateAt = DateTime.UtcNow }); await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = contactCategory.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "ContactCategoryMasterModificationLog"); // Save changes to database await _context.SaveChangesAsync(); // Map to response model var categoryVM = _mapper.Map(contactCategory); _logger.LogInfo("Contact category updated successfully: ID {ContactCategoryId}, Name '{CategoryName}', Tenant {TenantId}, by Employee {EmployeeId}", contactCategory.Id, contactCategory.Name, tenantId, loggedInEmployee.Id); return ApiResponse.SuccessResponse(categoryVM, "Category updated successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Error updating contact category {CategoryId} for Tenant {TenantId} by Employee {EmployeeId}", id, tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("An error occurred while updating the category", "An error occurred while updating the category", 500); } } /// /// Deletes a contact category by ID after ensuring it's not in use. /// Orphaned contacts have their category reference cleared. Full audit trail is maintained. /// /// The unique identifier of the contact category to delete. /// The employee initiating the deletion. /// The tenant identifier to scope the operation. /// ApiResponse indicating success or failure. public async Task> DeleteContactCategory(Guid id, Employee loggedInEmployee, Guid tenantId) { // Validate input parameters if (loggedInEmployee == null) { _logger.LogWarning("DeleteContactCategory: Request with null employee context"); return ApiResponse.ErrorResponse("Invalid employee context", "Invalid employee context", 400); } if (tenantId == Guid.Empty) { _logger.LogWarning("DeleteContactCategory: Invalid tenant ID {TenantId} provided by Employee {EmployeeId}", tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid tenant identifier", "Invalid tenant identifier", 400); } if (id == Guid.Empty) { _logger.LogWarning("DeleteContactCategory: Invalid category ID {CategoryId} provided by Employee {EmployeeId}", id, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid category identifier", "Invalid category identifier", 400); } try { // Retrieve the category to delete with tenant scoping var contactCategory = await _context.ContactCategoryMasters .FirstOrDefaultAsync(c => c.Id == id && c.TenantId == tenantId); if (contactCategory == null) { _logger.LogWarning("Employee {EmployeeId} attempted to delete non-existent contact category {CategoryId} for Tenant {TenantId}", loggedInEmployee.Id, id, tenantId); return ApiResponse.ErrorResponse("Category not found", "Category not found", 404); } // Check for associated contacts and update them in bulk var hasAssociatedContacts = await _context.Contacts .AnyAsync(c => c.ContactCategoryId == id && c.TenantId == tenantId); if (hasAssociatedContacts) { // Bulk update: Set ContactCategoryId to null for all related contacts var rowsAffected = await _context.Contacts .Where(c => c.ContactCategoryId == id && c.TenantId == tenantId) .ExecuteUpdateAsync(setters => setters.SetProperty(c => c.ContactCategoryId, (Guid?)null)); _logger.LogInfo("Cleared ContactCategoryId for {RowCount} contacts previously linked to category {CategoryId}", rowsAffected, id); } // Capture original state for audit log before deletion var existingEntityBson = _updateLogHelper.EntityToBsonDocument(contactCategory); // Remove the category _context.ContactCategoryMasters.Remove(contactCategory); // Log deletion in directory update log _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog { RefereanceId = id, UpdatedById = loggedInEmployee.Id, UpdateAt = DateTime.UtcNow }); // Save all changes to database await _context.SaveChangesAsync(); // Push audit log to external store (e.g., MongoDB) await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "ContactCategoryMasterModificationLog"); _logger.LogInfo("Contact category deleted successfully: ID {ContactCategoryId}, Tenant {TenantId}, by Employee {EmployeeId}", id, tenantId, loggedInEmployee.Id); return ApiResponse.SuccessResponse(new { }, "Category deleted successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Error deleting contact category {CategoryId} for Tenant {TenantId} by Employee {EmployeeId}", id, tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("An error occurred while deleting the category", "An error occurred while deleting the category", 500); } } #endregion #region =================================================================== Contact Tag APIs =================================================================== /// /// Retrieves all contact tags for the specified tenant. /// Returns a list of active tags mapped to view models with full audit logging. /// /// The employee making the request. /// The unique identifier for the tenant. /// ApiResponse containing the list of contact tags or error details. public async Task> GetContactTags(Employee loggedInEmployee, Guid tenantId) { // Validate input parameters if (loggedInEmployee == null) { _logger.LogWarning("GetContactTags: Request with null employee context"); return ApiResponse.ErrorResponse("Invalid employee context", "Invalid employee context", 400); } if (tenantId == Guid.Empty) { _logger.LogWarning("GetContactTags: Invalid tenant ID {TenantId} provided by Employee {EmployeeId}", tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid tenant identifier", "Invalid tenant identifier", 400); } try { // Fetch tags with tenant filtering and no tracking (read-only operation) var tagList = await _context.ContactTagMasters .AsNoTracking() .Where(t => t.TenantId == tenantId) .ToListAsync(); // Map to view models var contactTags = _mapper.Map>(tagList); int tagCount = contactTags.Count; // Log successful retrieval with context _logger.LogInfo("{TagCount} contact tags fetched for Tenant {TenantId} by Employee {EmployeeId}", tagCount, tenantId, loggedInEmployee.Id); return ApiResponse.SuccessResponse(contactTags, $"{tagCount} contact tags fetched successfully", 200); } catch (Exception ex) { // Log any unexpected errors with full context _logger.LogError(ex, "Error fetching contact tags for Tenant {TenantId} by Employee {EmployeeId}", tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("An error occurred while retrieving contact tags", "An error occurred while retrieving contact tags", 500); } } /// /// Creates a new contact tag for the specified tenant. /// Ensures name uniqueness within the tenant, logs all actions, and supports auditability. /// /// The DTO containing tag creation data. /// The employee initiating the request. /// The tenant identifier to scope the operation. /// ApiResponse containing the created tag or error details. public async Task> CreateContactTag(CreateContactTagDto model, Employee loggedInEmployee, Guid tenantId) { // Validate input parameters if (loggedInEmployee == null) { _logger.LogWarning("CreateContactTag: Request with null employee context"); return ApiResponse.ErrorResponse("Invalid employee context", "Invalid employee context", 400); } if (tenantId == Guid.Empty) { _logger.LogWarning("CreateContactTag: Invalid tenant ID {TenantId} provided by Employee {EmployeeId}", tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid tenant identifier", "Invalid tenant identifier", 400); } if (model == null) { _logger.LogWarning("Employee {EmployeeId} sent empty payload for contact tag creation", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Request payload cannot be null", "Request payload cannot be null", 400); } // Trim and validate name string trimmedName = model.Name.Trim(); if (string.IsNullOrWhiteSpace(trimmedName)) { _logger.LogWarning("Employee {EmployeeId} attempted to create contact tag with empty or whitespace-only name", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Tag name is required", "Tag name is required", 400); } try { // Check for existing tag with same name in the tenant bool tagExists = await _context.ContactTagMasters .AnyAsync(t => t.TenantId == tenantId && t.Name == trimmedName); if (tagExists) { _logger.LogWarning("Employee {EmployeeId} attempted to create duplicate contact tag with name '{TagName}' for Tenant {TenantId}", loggedInEmployee.Id, trimmedName, tenantId); return ApiResponse.ErrorResponse("A tag with this name already exists", "A tag with this name already exists", 409); } // Create new tag entity var contactTag = new ContactTagMaster { Id = Guid.NewGuid(), Name = trimmedName, Description = model.Description.Trim(), // Normalize description TenantId = tenantId, }; // Add and save to database _context.ContactTagMasters.Add(contactTag); await _context.SaveChangesAsync(); // Map to response model var tagVM = _mapper.Map(contactTag); // Log successful creation with full context _logger.LogInfo("Contact tag created successfully: ID {ContactTagId}, Name '{TagName}', Tenant {TenantId}, by Employee {EmployeeId}", contactTag.Id, contactTag.Name, tenantId, loggedInEmployee.Id); return ApiResponse.SuccessResponse(tagVM, "Tag created successfully", 201); // 201 Created } catch (Exception ex) { // Log any unexpected errors with full context _logger.LogError(ex, "Error creating contact tag for Tenant {TenantId} by Employee {EmployeeId}. Payload: {TagName}", tenantId, loggedInEmployee.Id, model.Name); return ApiResponse.ErrorResponse("An error occurred while creating the tag", "An error occurred while creating the tag", 500); } } /// /// Updates an existing contact tag within the specified tenant. /// Ensures data integrity, prevents name conflicts, and maintains a full audit trail. /// /// The unique identifier of the contact tag to update. /// The DTO containing updated tag data. /// The employee initiating the update. /// The tenant identifier to scope the operation. /// ApiResponse containing the updated tag or error details. public async Task> UpdateContactTag(Guid id, UpdateContactTagDto model, Employee loggedInEmployee, Guid tenantId) { // Validate input parameters if (loggedInEmployee == null) { _logger.LogWarning("UpdateContactTag: Request with null employee context"); return ApiResponse.ErrorResponse("Invalid employee context", "Invalid employee context", 400); } if (tenantId == Guid.Empty) { _logger.LogWarning("UpdateContactTag: Invalid tenant ID {TenantId} provided by Employee {EmployeeId}", tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid tenant identifier", "Invalid tenant identifier", 400); } if (id == Guid.Empty) { _logger.LogWarning("UpdateContactTag: Invalid tag ID {TagId} provided by Employee {EmployeeId}", id, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid tag identifier", "Invalid tag identifier", 400); } if (model == null) { _logger.LogWarning("Employee {EmployeeId} sent null DTO for updating contact tag {TagId}", loggedInEmployee.Id, id); return ApiResponse.ErrorResponse("Request payload cannot be null", "Request payload cannot be null", 400); } if (model.Id != id) { _logger.LogWarning("Employee {EmployeeId} attempted to update tag {TagId} with mismatched DTO ID {DtoId}", loggedInEmployee.Id, id, model.Id); return ApiResponse.ErrorResponse("Tag ID mismatch between route and payload", "Tag ID mismatch between route and payload", 400); } try { // Fetch the existing tag with tenant scoping var contactTag = await _context.ContactTagMasters .FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); if (contactTag == null) { _logger.LogWarning("Employee {EmployeeId} attempted to update non-existent contact tag {TagId} for Tenant {TenantId}", loggedInEmployee.Id, id, tenantId); return ApiResponse.ErrorResponse("Contact tag not found", "Contact tag not found", 404); } // Trim and validate name string trimmedName = (model.Name ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(trimmedName)) { _logger.LogWarning("Employee {EmployeeId} attempted to update tag {TagId} with empty or whitespace-only name", loggedInEmployee.Id, id); return ApiResponse.ErrorResponse("Tag name is required", "Tag name is required", 400); } // Check for duplicate name within tenant (excluding current tag) bool nameExists = await _context.ContactTagMasters .AnyAsync(t => t.TenantId == tenantId && t.Name == trimmedName && t.Id != id); if (nameExists) { _logger.LogWarning("Employee {EmployeeId} attempted to rename tag {TagId} to '{NewName}', which already exists in Tenant {TenantId}", loggedInEmployee.Id, id, trimmedName, tenantId); return ApiResponse.ErrorResponse("A tag with this name already exists", "A tag with this name already exists", 409); } // Capture original state for audit log var existingEntityBson = _updateLogHelper.EntityToBsonDocument(contactTag); // Update entity properties contactTag.Name = trimmedName; contactTag.Description = model.Description.Trim(); // Normalize description // Log update in directory and audit trail _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog { RefereanceId = contactTag.Id, UpdatedById = loggedInEmployee.Id, UpdateAt = DateTime.UtcNow }); await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = contactTag.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "ContactTagMasterModificationLog"); // Save changes to database await _context.SaveChangesAsync(); // Map to response model var contactTagVm = _mapper.Map(contactTag); _logger.LogInfo("Contact tag updated successfully: ID {ContactTagId}, Name '{TagName}', Tenant {TenantId}, by Employee {EmployeeId}", contactTag.Id, contactTag.Name, tenantId, loggedInEmployee.Id); return ApiResponse.SuccessResponse(contactTagVm, "Contact tag updated successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Error updating contact tag {TagId} for Tenant {TenantId} by Employee {EmployeeId}", id, tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("An error occurred while updating the tag", "An error occurred while updating the tag", 500); } } /// /// Deletes a contact tag by ID after removing all associated tag mappings. /// Maintains referential integrity and full audit trail for compliance and traceability. /// /// The unique identifier of the contact tag to delete. /// The employee initiating the deletion. /// The tenant identifier to scope the operation. /// ApiResponse indicating success or failure. public async Task> DeleteContactTag(Guid id, Employee loggedInEmployee, Guid tenantId) { // Validate input parameters if (loggedInEmployee == null) { _logger.LogWarning("DeleteContactTag: Request with null employee context"); return ApiResponse.ErrorResponse("Invalid employee context", "Invalid employee context", 400); } if (tenantId == Guid.Empty) { _logger.LogWarning("DeleteContactTag: Invalid tenant ID {TenantId} provided by Employee {EmployeeId}", tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid tenant identifier", "Invalid tenant identifier", 400); } if (id == Guid.Empty) { _logger.LogWarning("DeleteContactTag: Invalid tag ID {TagId} provided by Employee {EmployeeId}", id, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid tag identifier", "Invalid tag identifier", 400); } try { // Retrieve the tag to delete with tenant scoping var contactTag = await _context.ContactTagMasters .FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); if (contactTag == null) { _logger.LogWarning("Employee {EmployeeId} attempted to delete non-existent contact tag {TagId} for Tenant {TenantId}", loggedInEmployee.Id, id, tenantId); return ApiResponse.ErrorResponse("Contact tag not found", "Contact tag not found", 404); } // Capture original state for audit log before deletion var existingEntityBson = _updateLogHelper.EntityToBsonDocument(contactTag); // Remove all associated tag mappings in bulk var mappingsExist = await _context.ContactTagMappings .AnyAsync(m => m.ContactTagId == id); if (mappingsExist) { var rowsAffected = await _context.ContactTagMappings .Where(m => m.ContactTagId == id) .ExecuteDeleteAsync(); _logger.LogInfo("Deleted {RowCount} contact tag mappings associated with tag {TagId}", rowsAffected, id); } // Log deletion in directory update log _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog { RefereanceId = id, UpdatedById = loggedInEmployee.Id, UpdateAt = DateTime.UtcNow }); // Remove the tag _context.ContactTagMasters.Remove(contactTag); // Save all changes to database await _context.SaveChangesAsync(); // Push audit log to external store (e.g., MongoDB) await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "ContactTagMasterModificationLog"); _logger.LogInfo("Contact tag deleted successfully: ID {ContactTagId}, Tenant {TenantId}, by Employee {EmployeeId}", id, tenantId, loggedInEmployee.Id); return ApiResponse.SuccessResponse(new { }, "Tag deleted successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Error deleting contact tag {TagId} for Tenant {TenantId} by Employee {EmployeeId}", id, tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("An error occurred while deleting the tag", "An error occurred while deleting the tag", 500); } } #endregion #region =================================================================== Work Status APIs =================================================================== public async Task> GetWorkStatusList(Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("GetWorkStatusList called."); try { // Step 1: Check permission to view master data bool hasViewPermission = await _permission.HasPermission(PermissionsMaster.ViewMasters, loggedInEmployee.Id); if (!hasViewPermission) { _logger.LogWarning("Access denied for employeeId: {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("You don't have access", "Don't have access to take action", 403); } // Step 2: Fetch work statuses for the tenant var workStatusList = await _context.WorkStatusMasters .Where(ws => ws.TenantId == tenantId) .Select(ws => new { ws.Id, ws.Name, ws.Description, ws.IsSystem }) .ToListAsync(); _logger.LogInfo("{Count} work statuses fetched for tenantId: {TenantId}", workStatusList.Count, tenantId); // Step 3: Return successful response return ApiResponse.SuccessResponse( workStatusList, $"{workStatusList.Count} work status records fetched successfully", 200 ); } catch (Exception ex) { _logger.LogError(ex, "Error occurred while fetching work status list"); return ApiResponse.ErrorResponse("An error occurred", "Unable to fetch work status list", 500); } } public async Task> CreateWorkStatus(CreateWorkStatusMasterDto model, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("CreateWorkStatus called with Name: {Name}", model.Name); try { // Step 1: Check if user has permission to manage master data var hasManageMasterPermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManageMasterPermission) { _logger.LogWarning("Access denied for employeeId: {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("You don't have access", "Don't have access to take action", 403); } // Step 2: Check if work status with the same name already exists var existingWorkStatus = await _context.WorkStatusMasters .FirstOrDefaultAsync(ws => ws.Name == model.Name && ws.TenantId == tenantId); if (existingWorkStatus != null) { _logger.LogWarning("Work status already exists: {Name}", model.Name); return ApiResponse.ErrorResponse("Work status already exists", "Work status already exists", 400); } // Step 3: Create new WorkStatusMaster entry var workStatus = new WorkStatusMaster { Name = model.Name.Trim(), Description = model.Description.Trim(), IsSystem = false, TenantId = tenantId }; _context.WorkStatusMasters.Add(workStatus); await _context.SaveChangesAsync(); _logger.LogInfo("Work status created successfully: {Id}, Name: {Name}", workStatus.Id, workStatus.Name); return ApiResponse.SuccessResponse(workStatus, "Work status created successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Error occurred while creating work status"); return ApiResponse.ErrorResponse("An error occurred", "Unable to create work status", 500); } } public async Task> UpdateWorkStatus(Guid id, UpdateWorkStatusMasterDto model, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("UpdateWorkStatus called for WorkStatus ID: {Id}, New Name: {Name}", id, model.Name); try { // Step 1: Validate input if (id == Guid.Empty || id != model.Id) { _logger.LogWarning("Invalid ID provided for update. Route ID: {RouteId}, DTO ID: {DtoId}", id, model.Id); return ApiResponse.ErrorResponse("Invalid data provided", "The provided work status ID is invalid", 400); } // Step 2: Check permissions var hasManageMasterPermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManageMasterPermission) { _logger.LogWarning("Access denied. EmployeeId: {EmployeeId} does not have Manage Master permission.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access denied", "You do not have permission to update this work status", 403); } // Step 3: Retrieve the work status record var workStatus = await _context.WorkStatusMasters .FirstOrDefaultAsync(ws => ws.Id == id && ws.TenantId == tenantId); if (workStatus == null) { _logger.LogWarning("Work status not found for ID: {Id}", id); return ApiResponse.ErrorResponse("Work status not found", "No work status found with the provided ID", 404); } // Step 4: Check for duplicate name (optional) var isDuplicate = await _context.WorkStatusMasters .AnyAsync(ws => ws.Name == model.Name.Trim() && ws.Id != id && ws.TenantId == tenantId); if (isDuplicate) { _logger.LogWarning("Duplicate work status name '{Name}' detected during update. ID: {Id}", model.Name, id); return ApiResponse.ErrorResponse("Work status with the same name already exists", "Duplicate name", 400); } // Capture original state for audit log var existingEntityBson = _updateLogHelper.EntityToBsonDocument(workStatus); // Step 5: Update fields workStatus.Name = model.Name.Trim(); workStatus.Description = model.Description.Trim(); await _context.SaveChangesAsync(); await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = workStatus.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "WorkStatusMasterModificationLog"); _logger.LogInfo("Work status updated successfully. ID: {Id}", id); return ApiResponse.SuccessResponse(workStatus, "Work status updated successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Error occurred while updating work status ID: {Id}", id); return ApiResponse.ErrorResponse("An error occurred", "Unable to update the work status at this time", 500); } } public async Task> DeleteWorkStatus(Guid id, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("DeleteWorkStatus called for Id: {Id}", id); try { // Step 2: Check permission to manage master data var hasManageMasterPermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManageMasterPermission) { _logger.LogWarning("Delete denied. EmployeeId: {EmployeeId} lacks Manage_Master permission.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("You don't have access", "Access denied for deleting work status", 403); } // Step 3: Find the work status var workStatus = await _context.WorkStatusMasters .FirstOrDefaultAsync(ws => ws.Id == id && ws.TenantId == tenantId); if (workStatus == null) { _logger.LogWarning("Work status not found for Id: {Id}", id); return ApiResponse.ErrorResponse("Work status not found", "Work status not found", 404); } // Step 4: Check for dependencies in TaskAllocations bool hasDependency = await _context.TaskAllocations .AnyAsync(ta => ta.TenantId == tenantId && ta.WorkStatusId == id); if (hasDependency) { _logger.LogWarning("Cannot delete WorkStatus Id: {Id} due to existing task dependency", id); return ApiResponse.ErrorResponse( "Work status has a dependency in assigned tasks and cannot be deleted", "Deletion failed due to associated tasks", 400 ); } // Capture original state for audit log var existingEntityBson = _updateLogHelper.EntityToBsonDocument(workStatus); // Step 5: Delete and persist _context.WorkStatusMasters.Remove(workStatus); await _context.SaveChangesAsync(); await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = workStatus.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "WorkStatusMasterModificationLog"); _logger.LogInfo("Work status deleted successfully. Id: {Id}", id); return ApiResponse.SuccessResponse(new { }, "Work status deleted successfully", 200); } catch (Exception ex) { _logger.LogError(ex, "Error occurred while deleting WorkStatus Id: {Id}", id); return ApiResponse.ErrorResponse("An error occurred", "Unable to delete work status", 500); } } #endregion #region =================================================================== Expenses Type APIs =================================================================== public async Task> GetExpenseTypeListAsync(Employee loggedInEmployee, Guid tenantId, bool isActive) { try { // Featching the list of Expenses Type. var typeList = await _context.ExpensesTypeMaster.Where(et => et.TenantId == tenantId && et.IsActive == isActive).ToListAsync(); var response = _mapper.Map>(typeList); _logger.LogInfo("{Count} records of expense type have been fetched successfully by employee {EmployeeId}", response.Count, loggedInEmployee.Id); return ApiResponse.SuccessResponse(response, $"{response.Count} records of expense type have been fetched successfully.", 200); } catch (Exception ex) { _logger.LogError(ex, "Error occured while fetching list of expense type list by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500); } } public async Task> CreateExpenseTypeAsync(ExpensesTypeMasterDto model, Employee loggedInEmployee, Guid tenantId) { try { var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManagePermission) { _logger.LogWarning("Access DENIED for employee {EmployeeId} for managing EXPANSES TYPE MASTER.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to Manage masters", 403); } var expensesType = _mapper.Map(model); expensesType.TenantId = tenantId; _context.ExpensesTypeMaster.Add(expensesType); await _context.SaveChangesAsync(); _logger.LogInfo("New Expense Type {ExpensesTypeId} was added by employee {EmployeeId}", expensesType.Id, loggedInEmployee.Id); var response = _mapper.Map(expensesType); return ApiResponse.SuccessResponse(response, "Expense type craeted Successfully", 201); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database Exception occured while adding new expense type by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while adding new expense type by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500); } } public async Task> UpdateExpenseTypeAsync(Guid id, ExpensesTypeMasterDto model, Employee loggedInEmployee, Guid tenantId) { try { // Checking permssion for managing masters var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManagePermission) { _logger.LogWarning("Access DENIED for employee {EmployeeId} for managing EXPANSES TYPE MASTER.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to Manage masters", 403); } // Validating the prvided data if (model.Id != id) { _logger.LogWarning("Employee {EmployeeId} provide different Ids in payload and path variable", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid Data", "User has send invalid payload", 400); } var expensesType = await _context.ExpensesTypeMaster.AsNoTracking().FirstOrDefaultAsync(et => et.Id == model.Id.Value && et.TenantId == tenantId); // Checking if expense type exists if (expensesType == null) { _logger.LogWarning("Employee {EmployeeId} tries to update expense type, but not found in database", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Expense Type not found", "Expense Type not found", 404); } // Mapping ExpensesTypeMaster to BsonDocument var existingEntityBson = _updateLogHelper.EntityToBsonDocument(expensesType); // Mapping ExpensesTypeMasterDto to ExpensesTypeMaster _mapper.Map(model, expensesType); _context.ExpensesTypeMaster.Update(expensesType); await _context.SaveChangesAsync(); _logger.LogInfo("Expense Type {ExpensesTypeId} was updated by employee {EmployeeId}", expensesType.Id, loggedInEmployee.Id); // Saving the old entity in mongoDB await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = expensesType.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "ExpensesTypeMasterModificationLog"); // Mapping ExpensesTypeMaster to ExpensesTypeMasterVM var response = _mapper.Map(expensesType); return ApiResponse.SuccessResponse(response, "Expense type updated Successfully", 200); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database Exception occured while updating expense type by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while updating expense type by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500); } } public async Task> DeleteExpenseTypeAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId) { string action = isActive ? "restore" : "delete"; try { // Checking permssion for managing masters var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManagePermission) { _logger.LogWarning("Access DENIED for employee {EmployeeId} for managing EXPANSES TYPE MASTER.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to Manage masters", 403); } var expensesType = await _context.ExpensesTypeMaster.FirstOrDefaultAsync(et => et.Id == id && et.TenantId == tenantId); // Checking if expense type exists if (expensesType == null) { _logger.LogWarning("Employee {EmployeeId} tries to {Action} expense type, but not found in database", loggedInEmployee.Id, action); return ApiResponse.ErrorResponse("Expense Type not found", "Expense Type not found", 404); } // Mapping ExpensesTypeMaster to BsonDocument var existingEntityBson = _updateLogHelper.EntityToBsonDocument(expensesType); expensesType.IsActive = isActive; await _context.SaveChangesAsync(); _logger.LogInfo("Expense Type {ExpensesTypeId} was {Action}d by employee {EmployeeId}", expensesType.Id, action, loggedInEmployee.Id); // Saving the old entity in mongoDB await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = expensesType.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "ExpensesTypeMasterModificationLog"); // Mapping ExpensesTypeMaster to ExpensesTypeMasterVM var response = _mapper.Map(expensesType); return ApiResponse.SuccessResponse(response, $"Expense type {action}d Successfully", 200); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database Exception occured while {Action}ing expense type by employee {EmployeeId}", action, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while {Action}ing expense type by employee {EmployeeId}", action, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500); } } #endregion #region =================================================================== Expenses Status APIs =================================================================== public async Task> GetExpensesStatusListAsync(Employee loggedInEmployee, Guid tenantId) { try { // Featching the list of Expenses Status. var statusList = await _context.ExpensesStatusMaster.ToListAsync(); var response = _mapper.Map>(statusList); var statusIds = statusList.Select(s => s.Id).ToList(); var permissionStatusMapping = await _context.StatusPermissionMapping .Where(ps => statusIds.Contains(ps.StatusId)) .GroupBy(ps => ps.StatusId) .Select(g => new { StatusId = g.Key, PermissionIds = g.Select(ps => ps.PermissionId).ToList() }).ToListAsync(); foreach (var status in response) { status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault(); } _logger.LogInfo("{Count} records of expense status have been fetched successfully by employee {EmployeeId}", response.Count, loggedInEmployee.Id); return ApiResponse.SuccessResponse(response, $"{response.Count} records of expense status have been fetched successfully.", 200); } catch (Exception ex) { _logger.LogError(ex, "Error occured while fetching list of expense sattus list by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500); } } #endregion #region =================================================================== Payment mode APIs =================================================================== public async Task> GetPaymentModeListAsync(Employee loggedInEmployee, Guid tenantId, bool isActive) { try { // Featching the list of Payment Modes. var paymentModes = await _context.PaymentModeMatser.Where(pm => pm.TenantId == tenantId && pm.IsActive == isActive).ToListAsync(); var response = _mapper.Map>(paymentModes); _logger.LogInfo("{Count} records of payment modes have been fetched successfully by employee {EmployeeId}", response.Count, loggedInEmployee.Id); return ApiResponse.SuccessResponse(response, $"{response.Count} records of payment modes have been fetched successfully.", 200); } catch (Exception ex) { _logger.LogError(ex, "Error occured while featching list of payment modes list by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured while featching list of payment modes list", ExceptionMapper(ex), 500); } } public async Task> CreatePaymentModeAsync(PaymentModeMatserDto model, Employee loggedInEmployee, Guid tenantId) { try { var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManagePermission) { _logger.LogWarning("Access DENIED for employee {EmployeeId} for managing PAYMENT MODE MASTER.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to Manage masters", 403); } // Mapping the DTO to PaymentModeMatser Model var paymentMode = _mapper.Map(model); paymentMode.TenantId = tenantId; _context.PaymentModeMatser.Add(paymentMode); await _context.SaveChangesAsync(); _logger.LogInfo("New Payment Mode {PaymentModeId} was added by employee {EmployeeId}", paymentMode.Id, loggedInEmployee.Id); // Mapping the PaymentModeMatser Model to View Model var response = _mapper.Map(paymentMode); return ApiResponse.SuccessResponse(response, "Payment Mode craeted Successfully", 201); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database Exception occured while adding new payment mode by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while adding new payment mode by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500); } } public async Task> UpdatePaymentModeAsync(Guid id, PaymentModeMatserDto model, Employee loggedInEmployee, Guid tenantId) { try { // Checking permssion for managing masters var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManagePermission) { _logger.LogWarning("Access DENIED for employee {EmployeeId} for managing PAYMENT MODE MASTER.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to Manage masters", 403); } // Validating the prvided data if (model.Id != id) { _logger.LogWarning("Employee {EmployeeId} provide different Ids in payload and path variable", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid Data", "User has send invalid payload", 400); } var paymentMode = await _context.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(et => et.Id == model.Id.Value && et.TenantId == tenantId); // Checking if Payment Mode exists if (paymentMode == null) { _logger.LogWarning("Employee {EmployeeId} tries to update Payment Mode, but not found in database", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Payment Mode not found", "Payment Mode not found", 404); } // Mapping PaymentModeMatser to BsonDocument var existingEntityBson = _updateLogHelper.EntityToBsonDocument(paymentMode); // Mapping PaymentModeMatserDto to PaymentModeMatser _mapper.Map(model, paymentMode); _context.PaymentModeMatser.Update(paymentMode); await _context.SaveChangesAsync(); _logger.LogInfo("Payment Mode {PaymentModeId} was updated by employee {EmployeeId}", paymentMode.Id, loggedInEmployee.Id); // Saving the old entity in mongoDB await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = paymentMode.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "PaymentModeMasterModificationLog"); // Mapping PaymentModeMatser to PaymentModeMatserVM var response = _mapper.Map(paymentMode); return ApiResponse.SuccessResponse(response, "Payment Mode updated Successfully", 200); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database Exception occured while updating Payment Mode by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while updating Payment Mode by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500); } } public async Task> DeletePaymentModeAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId) { string action = isActive ? "restore" : "delete"; try { // Checking permssion for managing masters var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManagePermission) { _logger.LogWarning("Access DENIED for employee {EmployeeId} for managing PAYMENT MODE MASTER.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to Manage masters", 403); } var paymentMode = await _context.PaymentModeMatser.FirstOrDefaultAsync(et => et.Id == id && et.TenantId == tenantId); // Checking if Payment Mode exists if (paymentMode == null) { _logger.LogWarning("Employee {EmployeeId} tries to {Action} Payment Mode, but not found in database", loggedInEmployee.Id, action); return ApiResponse.ErrorResponse("Payment Mode not found", "Payment Mode not found", 404); } // Mapping PaymentModeMatser to BsonDocument var existingEntityBson = _updateLogHelper.EntityToBsonDocument(paymentMode); paymentMode.IsActive = isActive; await _context.SaveChangesAsync(); _logger.LogInfo("Payment Mode {PaymentModeId} was {Action}d by employee {EmployeeId}", paymentMode.Id, action, loggedInEmployee.Id); // Saving the old entity in mongoDB await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = paymentMode.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "PaymentModeMatserModificationLog"); // Mapping PaymentModeMatser to PaymentModeMatserVM var response = _mapper.Map(paymentMode); return ApiResponse.SuccessResponse(response, $"Payment Mode {action}d Successfully", 200); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database Exception occured while {Action}ing Payment Mode by employee {EmployeeId}", action, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while {Action}ing Payment Mode by employee {EmployeeId}", action, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500); } } #endregion #region =================================================================== Document Category APIs =================================================================== /// /// Fetches the list of Document Categories for a given tenant and optional entity type. /// Ensures tenant validation, mapping, and proper logging. /// /// Optional entity type filter (e.g., EmployeeEntity, ProjectEntity). /// Currently logged-in employee. /// Tenant Id context. /// ApiResponse containing the document categories or error details. public async Task> GetDocumentCategoryMasterListAsync(Guid? entityTypeId, Employee loggedInEmployee, Guid tenantId) { try { // ✅ Build query IQueryable documentCategoryQuery = _context.DocumentCategoryMasters .AsNoTracking() // optimization: read-only .Where(dc => dc.TenantId == tenantId); // ✅ Apply optional filter if (entityTypeId.HasValue) { documentCategoryQuery = documentCategoryQuery.Where(dc => dc.EntityTypeId == entityTypeId.Value); } // ✅ Fetch and map var documentCategories = await documentCategoryQuery.ToListAsync(); var response = _mapper.Map>(documentCategories); _logger.LogInfo("{Count} document categories fetched successfully for TenantId: {TenantId} by Employee {EmployeeId}", response.Count, tenantId, loggedInEmployee.Id); return ApiResponse.SuccessResponse(response, $"{response.Count} document categories have been fetched successfully.", 200); } catch (Exception ex) { _logger.LogError(ex, "Error occurred while fetching document categories for TenantId: {TenantId} by Employee {EmployeeId}", tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Server Error", "Server Error occured", 500); } } public async Task> CreateDocumentCategoryMasterAsync(CreateDocumentCategoryDto model, Employee loggedInEmployee, Guid tenantId) { try { var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManagePermission) { _logger.LogWarning("Access DENIED for employee {EmployeeId} for managing Document Category Master.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to Manage masters", 403); } var oldExists = await _context.DocumentCategoryMasters .AnyAsync(dc => dc.Name == model.Name && dc.EntityTypeId == model.EntityTypeId && dc.TenantId == tenantId); if (oldExists) { _logger.LogWarning("Document Category of {Name} is already exists in database for {TenantId}", model.Name, tenantId); return ApiResponse.ErrorResponse("Document Category already exists.", "Document Category already exists in database", 409); } // Mapping the DTO to Document Category Master Model var documentCategory = _mapper.Map(model); documentCategory.CreatedAt = DateTime.UtcNow; documentCategory.TenantId = tenantId; _context.DocumentCategoryMasters.Add(documentCategory); await _context.SaveChangesAsync(); _logger.LogInfo("New Document Category {DocumentCategoryId} was added by employee {EmployeeId}", documentCategory.Id, loggedInEmployee.Id); // Mapping the Document Category Master Model to View Model var response = _mapper.Map(documentCategory); return ApiResponse.SuccessResponse(response, "Document Category craeted Successfully", 201); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database Exception occured while adding new Document Category by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while adding new Document Category by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500); } } public async Task> UpdateDocumentCategoryMasterAsync(Guid id, CreateDocumentCategoryDto model, Employee loggedInEmployee, Guid tenantId) { try { // Checking permssion for managing masters var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManagePermission) { _logger.LogWarning("Access DENIED for employee {EmployeeId} for managing DOCUMENT CATEGORY MASTER.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to Manage masters", 403); } // Validating the prvided data if (model.Id != id) { _logger.LogWarning("Employee {EmployeeId} provide different Ids in payload and path variable", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid Data", "User has send invalid payload", 400); } var categoryTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.DocumentCategoryMasters.AsNoTracking().FirstOrDefaultAsync(et => et.Id == model.Id.Value && et.TenantId == tenantId); }); var oldCategoryExistsTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.DocumentCategoryMasters.AnyAsync(dc => dc.Name == model.Name && dc.Id != model.Id.Value && dc.EntityTypeId == model.EntityTypeId && dc.TenantId == tenantId); }); await Task.WhenAll(categoryTask, oldCategoryExistsTask); var documentCategory = categoryTask.Result; var oldCategoryExists = oldCategoryExistsTask.Result; // Checking if Document Category exists if (documentCategory == null) { _logger.LogWarning("Employee {EmployeeId} tries to update Document Category, but not found in database", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Document Category not found", "Document Category not found", 404); } if (oldCategoryExists) { _logger.LogWarning("Document Category of {Name} is already exists in database for {TenantId} while updating document category", model.Name, tenantId); return ApiResponse.ErrorResponse("Document Category already exists.", "Document Category already exists in database", 409); } // Mapping DocumentCategoryMaster to BsonDocument var existingEntityBson = _updateLogHelper.EntityToBsonDocument(documentCategory); // Mapping DocumentCategoryDto to DocumentCategoryMaster _mapper.Map(model, documentCategory); _context.DocumentCategoryMasters.Update(documentCategory); await _context.SaveChangesAsync(); _logger.LogInfo("Document Category {DocumentCategoryId} was updated by employee {EmployeeId}", documentCategory.Id, loggedInEmployee.Id); // Saving the old entity in mongoDB await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = documentCategory.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "DocumentCategoryModificationLog"); // Mapping DocumentCategoryMaster to DocumentCategoryVM var response = _mapper.Map(documentCategory); return ApiResponse.SuccessResponse(response, "Document Category updated Successfully", 200); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database Exception occured while updating Document Category by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while updating Document Category by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500); } } public async Task> DeleteDocumentCategoryMasterAsync(Guid id, Employee loggedInEmployee, Guid tenantId) { try { // Checking permssion for managing masters var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManagePermission) { _logger.LogWarning("Access DENIED for employee {EmployeeId} for managing DOCUMENT CATEGORY MASTER.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to Manage masters", 403); } var documentCategory = await _context.DocumentCategoryMasters.FirstOrDefaultAsync(et => et.Id == id && et.TenantId == tenantId); // Checking if Document Category exists if (documentCategory == null) { _logger.LogWarning("Employee {EmployeeId} tries to delete Document Category, but not found in database", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Document Category not found", "Document Category not found", 404); } // Mapping DocumentCategoryMaster to BsonDocument var existingEntityBson = _updateLogHelper.EntityToBsonDocument(documentCategory); _context.DocumentCategoryMasters.Remove(documentCategory); await _context.SaveChangesAsync(); _logger.LogInfo("Document Category {DocumentCategoryId} was deleted by employee {EmployeeId}", documentCategory.Id, loggedInEmployee.Id); // Saving the old entity in mongoDB await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = documentCategory.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "DocumentCategoryModificationLog"); // Mapping DocumentCategoryMatser to DocumentCategoryVM var response = _mapper.Map(documentCategory); return ApiResponse.SuccessResponse(response, "Document Category deleted Successfully", 200); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database Exception occured while deleteing Document Category by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while deleteing Document Category by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500); } } #endregion #region =================================================================== Document Type APIs =================================================================== public async Task> GetDocumentTypeMasterListAsync(Guid? documentCategoryId, Employee loggedInEmployee, Guid tenantId) { try { // ✅ Build query IQueryable documentTypeQuery = _context.DocumentTypeMasters .AsNoTracking() // optimization: read-only .Where(dc => dc.TenantId == tenantId); // ✅ Apply optional filter if (documentCategoryId.HasValue) { documentTypeQuery = documentTypeQuery.Where(dc => dc.DocumentCategoryId == documentCategoryId.Value); } // ✅ Fetch and map var documentType = await documentTypeQuery.Include(dt => dt.DocumentCategory).ToListAsync(); var response = _mapper.Map>(documentType); _logger.LogInfo("{Count} document type fetched successfully for TenantId: {TenantId} by Employee {EmployeeId}", response.Count, tenantId, loggedInEmployee.Id); return ApiResponse.SuccessResponse(response, $"{response.Count} document type have been fetched successfully.", 200); } catch (Exception ex) { _logger.LogError(ex, "Error occurred while fetching document type for TenantId: {TenantId} by Employee {EmployeeId}", tenantId, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Server Error", "Server Error occured", 500); } } public async Task> CreateDocumentTypeMasterAsync(CreateDocumentTypeDto model, Employee loggedInEmployee, Guid tenantId) { try { var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManagePermission) { _logger.LogWarning("Access DENIED for employee {EmployeeId} for managing DOCUMENT TYPE MASTER.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to Manage masters", 403); } var oldExists = await _context.DocumentTypeMasters .AnyAsync(dt => dt.Name == model.Name && dt.DocumentCategoryId == model.DocumentCategoryId && dt.TenantId == tenantId); if (oldExists) { _logger.LogWarning("Document Type of {Name} is already exists in database for {TenantId} while creating new document type", model.Name, tenantId); return ApiResponse.ErrorResponse("Document Type already exists.", "Document Type already exists in database", 409); } // Mapping the DTO to Document Type Master Model var documentType = _mapper.Map(model); if (string.IsNullOrWhiteSpace(model.RegexExpression)) { documentType.IsValidationRequired = false; } documentType.IsSystem = false; documentType.IsActive = true; documentType.CreatedAt = DateTime.UtcNow; documentType.TenantId = tenantId; _context.DocumentTypeMasters.Add(documentType); await _context.SaveChangesAsync(); _logger.LogInfo("New Document Type {DocumentTypeId} was added by employee {EmployeeId}", documentType.Id, loggedInEmployee.Id); // Mapping the Document Type Master Model to View Model var response = _mapper.Map(documentType); return ApiResponse.SuccessResponse(response, "Document Type craeted Successfully", 201); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database Exception occured while adding new Document Type by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while adding new Document Type by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500); } } public async Task> UpdateDocumentTypeMasterAsync(Guid id, CreateDocumentTypeDto model, Employee loggedInEmployee, Guid tenantId) { try { // Checking permssion for managing masters var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManagePermission) { _logger.LogWarning("Access DENIED for employee {EmployeeId} for managing DOCUMENT TYPE MASTER.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to Manage masters", 403); } // Validating the prvided data if (model.Id != id) { _logger.LogWarning("Employee {EmployeeId} provide different Ids in payload and path variable", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Invalid Data", "User has send invalid payload", 400); } var documentTypeTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.DocumentTypeMasters.AsNoTracking().FirstOrDefaultAsync(et => et.Id == model.Id.Value && et.TenantId == tenantId); }); var oldTypeExistsTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.DocumentTypeMasters .AnyAsync(dt => dt.Name == model.Name && dt.Id != model.Id.Value && dt.DocumentCategoryId == model.DocumentCategoryId && dt.TenantId == tenantId); }); await Task.WhenAll(documentTypeTask, oldTypeExistsTask); var documentType = documentTypeTask.Result; var oldTypeExists = oldTypeExistsTask.Result; // Checking if Document Type exists if (documentType == null) { _logger.LogWarning("Employee {EmployeeId} tries to update Document Type, but not found in database", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Document Type not found", "Document Type not found", 404); } if (oldTypeExists) { _logger.LogWarning("Document Type of {Name} is already exists in database for {TenantId} while updating document Type", model.Name, tenantId); return ApiResponse.ErrorResponse("Document Type already exists.", "Document Type already exists in database", 409); } // Mapping DocumentTypeMaster to BsonDocument var existingEntityBson = _updateLogHelper.EntityToBsonDocument(documentType); // Mapping DocumentTypeDto to DocumentTypeMaster _mapper.Map(model, documentType); _context.DocumentTypeMasters.Update(documentType); await _context.SaveChangesAsync(); _logger.LogInfo("Document Type {DocumentTypeId} was updated by employee {EmployeeId}", documentType.Id, loggedInEmployee.Id); // Saving the old entity in mongoDB await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = documentType.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "DocumentTypeModificationLog"); // Mapping DocumentTypeMaster to DocumentTypeVM var response = _mapper.Map(documentType); return ApiResponse.SuccessResponse(response, "Document Type updated Successfully", 200); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database Exception occured while updating Document Type by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while updating Document Type by employee {EmployeeId}", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500); } } public async Task> DeleteDocumentTypeMasterAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId) { string action = isActive ? "restore" : "delete"; try { // Checking permssion for managing masters var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManagePermission) { _logger.LogWarning("Access DENIED for employee {EmployeeId} for managing DOCUMENT TYPE MASTER.", loggedInEmployee.Id); return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to Manage masters", 403); } var documentType = await _context.DocumentTypeMasters.FirstOrDefaultAsync(et => et.Id == id && et.TenantId == tenantId); // Checking if Document Type exists if (documentType == null) { _logger.LogWarning("Employee {EmployeeId} tries to {Action} Document Type, but not found in database", loggedInEmployee.Id, action); return ApiResponse.ErrorResponse("Document Type not found", "Document Type not found", 404); } if (documentType.IsSystem) { _logger.LogWarning("Employee {EmployeeId} tries to {Action} Document Type, but could not take action on system defined entity", loggedInEmployee.Id, action); return ApiResponse.ErrorResponse($"Document is system defined cannot be {action}d", $"Document is system defined cannot be {action}d", 400); } // Mapping DocumentTypeMatser to BsonDocument var existingEntityBson = _updateLogHelper.EntityToBsonDocument(documentType); documentType.IsActive = isActive; await _context.SaveChangesAsync(); _logger.LogInfo("Document Type {DocumentTypeId} was {Action}d by employee {EmployeeId}", documentType.Id, action, loggedInEmployee.Id); // Saving the old entity in mongoDB await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = documentType.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "DocumentTypeModificationLog"); // Mapping DocumentTypeMatser to DocumentTypeVM var response = _mapper.Map(documentType); return ApiResponse.SuccessResponse(response, $"Document Type {action}d Successfully", 200); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database Exception occured while {Action}ing Document Type by employee {EmployeeId}", action, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while {Action}ing Document Type by employee {EmployeeId}", action, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500); } } #endregion #region =================================================================== Payment Adjustment Head APIs =================================================================== /// /// Retrieves a list of payment adjustment heads for a specific tenant with optional active status filtering. /// /// Filter for active/inactive payment adjustment heads /// The employee making the request (for auditing/authorization) /// The tenant identifier to scope the data /// An API response containing the list of payment adjustment head view models /// /// This method performs database-level filtering and uses projection to minimize data transfer. /// Consider implementing pagination for tenants with large numbers of payment adjustment heads. /// public async Task> GetPaymentAdjustmentHeadListAsync(bool isActive, Employee loggedInEmployee, Guid tenantId) { try { // Log the request details for auditing and troubleshooting _logger.LogInfo("Fetching payment adjustment heads for tenant {TenantId} with IsActive={IsActive}", tenantId, isActive); var paymentAdjustmentHeads = await _context.PaymentAdjustmentHeads .AsNoTracking() // Improve performance by disabling change tracking for read-only operations .Where(pah => pah.TenantId == tenantId && pah.IsActive == isActive) .Select(pah => _mapper.Map(pah)) .ToListAsync(); _logger.LogInfo("Successfully retrieved {Count} payment adjustment heads for tenant {TenantId}", paymentAdjustmentHeads.Count, tenantId); return ApiResponse.SuccessResponse( paymentAdjustmentHeads.OrderBy(pah => pah.Name).ToList(), $"Payment Adjustment Heads fetched successfully. Count: {paymentAdjustmentHeads.Count}", 200); } catch (Exception ex) { // Log the full exception with context for better troubleshooting _logger.LogError(ex, "Error occurred while fetching payment adjustment heads for tenant {TenantId}. IsActive: {IsActive}", tenantId, isActive); return ApiResponse.ErrorResponse("An error occurred", "Unable to fetch payment adjustment heads", 500); } } #endregion #region =================================================================== Helper Function =================================================================== private static object ExceptionMapper(Exception ex) { return new { Message = ex.Message, StackTrace = ex.StackTrace, Source = ex.Source, InnerException = new { Message = ex.InnerException?.Message, StackTrace = ex.InnerException?.StackTrace, Source = ex.InnerException?.Source, } }; } #endregion } }