From bad784e147a5b898b523e318408f75aafa2f7d32 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 11 Oct 2025 12:59:31 +0530 Subject: [PATCH] Optimized the contact related msater APIs --- .../Dtos/Master/CreateContactCategoryDto.cs | 4 +- .../Dtos/Master/CreateContactTagDto.cs | 4 +- .../Dtos/Master/UpdateContactCategoryDto.cs | 6 +- .../Dtos/Master/UpdateContactTagDto.cs | 4 +- .../MappingProfiles/MappingProfile.cs | 21 + Marco.Pms.Services/Service/MasterService.cs | 939 +++++++++++++++--- 6 files changed, 805 insertions(+), 173 deletions(-) diff --git a/Marco.Pms.Model/Dtos/Master/CreateContactCategoryDto.cs b/Marco.Pms.Model/Dtos/Master/CreateContactCategoryDto.cs index 3efc443..0dbe2c7 100644 --- a/Marco.Pms.Model/Dtos/Master/CreateContactCategoryDto.cs +++ b/Marco.Pms.Model/Dtos/Master/CreateContactCategoryDto.cs @@ -2,7 +2,7 @@ { public class CreateContactCategoryDto { - public string? Name { get; set; } - public string? Description { get; set; } + public required string Name { get; set; } + public required string Description { get; set; } } } diff --git a/Marco.Pms.Model/Dtos/Master/CreateContactTagDto.cs b/Marco.Pms.Model/Dtos/Master/CreateContactTagDto.cs index 2fec4ae..0caa896 100644 --- a/Marco.Pms.Model/Dtos/Master/CreateContactTagDto.cs +++ b/Marco.Pms.Model/Dtos/Master/CreateContactTagDto.cs @@ -2,7 +2,7 @@ { public class CreateContactTagDto { - public string? Name { get; set; } - public string? Description { get; set; } + public required string Name { get; set; } + public required string Description { get; set; } } } diff --git a/Marco.Pms.Model/Dtos/Master/UpdateContactCategoryDto.cs b/Marco.Pms.Model/Dtos/Master/UpdateContactCategoryDto.cs index 0bd5cc7..9274b97 100644 --- a/Marco.Pms.Model/Dtos/Master/UpdateContactCategoryDto.cs +++ b/Marco.Pms.Model/Dtos/Master/UpdateContactCategoryDto.cs @@ -2,8 +2,8 @@ { public class UpdateContactCategoryDto { - public Guid Id { get; set; } - public string? Name { get; set; } - public string? Description { get; set; } + public required Guid Id { get; set; } + public required string Name { get; set; } + public required string Description { get; set; } } } diff --git a/Marco.Pms.Model/Dtos/Master/UpdateContactTagDto.cs b/Marco.Pms.Model/Dtos/Master/UpdateContactTagDto.cs index e97ece6..cfc4706 100644 --- a/Marco.Pms.Model/Dtos/Master/UpdateContactTagDto.cs +++ b/Marco.Pms.Model/Dtos/Master/UpdateContactTagDto.cs @@ -3,7 +3,7 @@ public class UpdateContactTagDto { public Guid Id { get; set; } - public string? Name { get; set; } - public string? Description { get; set; } + public required string Name { get; set; } + public required string Description { get; set; } } } diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index a7aee31..fa1ea71 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -379,6 +379,27 @@ namespace Marco.Pms.Services.MappingProfiles #endregion + #region ======================================================= Contact Category Master ======================================================= + CreateMap(); + CreateMap(); + CreateMap(); + #endregion + #region ======================================================= Contact Tag Master ======================================================= + CreateMap(); + CreateMap(); + CreateMap(); + #endregion + #region ======================================================= Expenses Status Master ======================================================= + #endregion + #region ======================================================= Expenses Status Master ======================================================= + #endregion + #region ======================================================= Expenses Status Master ======================================================= + #endregion + #region ======================================================= Expenses Status Master ======================================================= + #endregion + #region ======================================================= Expenses Status Master ======================================================= + #endregion + #endregion #region ======================================================= Document ======================================================= diff --git a/Marco.Pms.Services/Service/MasterService.cs b/Marco.Pms.Services/Service/MasterService.cs index c693e05..091ba3c 100644 --- a/Marco.Pms.Services/Service/MasterService.cs +++ b/Marco.Pms.Services/Service/MasterService.cs @@ -8,14 +8,12 @@ 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.Mapper; 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.DocumentManager; using Marco.Pms.Model.ViewModels.Master; -using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Service; using Microsoft.CodeAnalysis; @@ -26,29 +24,29 @@ 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; - private readonly CacheUpdateHelper _cache; public MasterService( IDbContextFactory dbContextFactory, + IServiceScopeFactory serviceScopeFactory, ApplicationDbContext context, ILoggingService logger, PermissionServices permission, IMapper mapper, - UtilityMongoDBHelper updateLogHelper, - CacheUpdateHelper cache) + UtilityMongoDBHelper updateLogHelper) { - _dbContextFactory = dbContextFactory; - _context = context; - _logger = logger; - _permission = permission; - _mapper = mapper; - _updateLogHelper = updateLogHelper; - _cache = cache; + _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 =================================================================== @@ -451,6 +449,8 @@ namespace Marco.Pms.Services.Service 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(); @@ -459,6 +459,14 @@ namespace Marco.Pms.Services.Service 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); } @@ -500,10 +508,27 @@ namespace Marco.Pms.Services.Service 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); @@ -637,14 +662,25 @@ namespace Marco.Pms.Services.Service 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); } @@ -686,10 +722,27 @@ namespace Marco.Pms.Services.Service 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); @@ -885,6 +938,8 @@ namespace Marco.Pms.Services.Service 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(); @@ -892,6 +947,14 @@ namespace Marco.Pms.Services.Service 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() @@ -986,10 +1049,20 @@ namespace Marco.Pms.Services.Service 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); @@ -1006,205 +1079,743 @@ namespace Marco.Pms.Services.Service #region =================================================================== Contact Category APIs =================================================================== - public async Task> CreateContactCategory(CreateContactCategoryDto contactCategoryDto, Employee loggedInEmployee, Guid tenantId) - { - if (contactCategoryDto != null) - { - ContactCategoryMaster? existingContactCategory = await _context.ContactCategoryMasters.FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Name.ToLower() == (contactCategoryDto.Name != null ? contactCategoryDto.Name.ToLower() : "")); - if (existingContactCategory == null) - { - ContactCategoryMaster contactCategory = contactCategoryDto.ToContactCategoryMasterFromCreateContactCategoryDto(tenantId); - _context.ContactCategoryMasters.Add(contactCategory); - await _context.SaveChangesAsync(); - ContactCategoryVM categoryVM = contactCategory.ToContactCategoryVMFromContactCategoryMaster(); - - _logger.LogInfo("Employee ID {LoggedInEmployeeId} created a contact category {ContactCategoryId}.", loggedInEmployee.Id, contactCategory.Id); - return ApiResponse.SuccessResponse(categoryVM, "Category Created Successfully", 200); - } - _logger.LogWarning("Employee ID {LoggedInEmployeeId} attempted to create an existing contact category.", loggedInEmployee.Id); - return ApiResponse.ErrorResponse("Category already existed", "Category already existed", 409); - } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", loggedInEmployee.Id); - return ApiResponse.ErrorResponse("User Send empty Payload", "User Send empty Payload", 400); - } - public async Task> UpdateContactCategory(Guid id, UpdateContactCategoryDto contactCategoryDto, Employee loggedInEmployee, Guid tenantId) - { - if (contactCategoryDto != null && id == contactCategoryDto.Id) - { - ContactCategoryMaster? contactCategory = await _context.ContactCategoryMasters.FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); - if (contactCategory != null) - { - contactCategory.Name = contactCategoryDto.Name ?? ""; - contactCategory.Description = contactCategoryDto.Description ?? ""; - - _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog - { - RefereanceId = contactCategory.Id, - UpdatedById = loggedInEmployee.Id, - UpdateAt = DateTime.UtcNow - }); - - await _context.SaveChangesAsync(); - ContactCategoryVM categoryVM = contactCategory.ToContactCategoryVMFromContactCategoryMaster(); - - _logger.LogInfo("Employee ID {LoggedInEmployeeId} created a contact category {ContactCategoryId}.", loggedInEmployee.Id, contactCategory.Id); - return ApiResponse.SuccessResponse(categoryVM, "Category Created Successfully", 200); - } - _logger.LogWarning("Employee ID {LoggedInEmployeeId} attempted to update a contact category but not found in database.", loggedInEmployee.Id); - return ApiResponse.ErrorResponse("Category not found", "Category not found", 404); - } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", loggedInEmployee.Id); - return ApiResponse.ErrorResponse("User Send empty Payload", "User Send empty Payload", 400); - } + /// + /// 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) { - var categoryList = await _context.ContactCategoryMasters.Where(c => c.TenantId == tenantId).ToListAsync(); - List contactCategories = new List(); - foreach (var category in categoryList) + // Validate parameters + if (loggedInEmployee == null) { - ContactCategoryVM categoryVM = category.ToContactCategoryVMFromContactCategoryMaster(); - contactCategories.Add(categoryVM); + _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); } - _logger.LogInfo("{count} contact categoires are fetched by Employee with ID {LoggedInEmployeeId}", contactCategories.Count, loggedInEmployee.Id); - return ApiResponse.SuccessResponse(contactCategories, System.String.Format("{0} contact categories fetched successfully", contactCategories.Count), 200); } + + /// + /// 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) { - var category = await _context.ContactCategoryMasters.FirstOrDefaultAsync(c => c.Id == id && c.TenantId == tenantId); - if (category != null) + // Validate required parameters + if (loggedInEmployee == null) { - ContactCategoryVM categoryVM = category.ToContactCategoryVMFromContactCategoryMaster(); - _logger.LogInfo("Employee {EmployeeId} fetched contact category {ContactCategoryID}", loggedInEmployee.Id, category.Id); - return ApiResponse.SuccessResponse(categoryVM, "Category fetched successfully", 200); + _logger.LogWarning("Null employee object provided when fetching contact category {ContactCategoryID}", id); + return ApiResponse.ErrorResponse("Invalid employee context", "Invalid employee context", 400); } - _logger.LogWarning("Employee {EmployeeId} attempted to fetch contact category {ContactCategoryID} but not found in database", loggedInEmployee.Id, id); - return ApiResponse.ErrorResponse("Category not found", "Category not found", 404); + 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) { - ContactCategoryMaster? contactCategory = await _context.ContactCategoryMasters.FirstOrDefaultAsync(c => c.Id == id && c.TenantId == tenantId); - if (contactCategory != null) + // Validate input parameters + if (loggedInEmployee == null) { - List? existingContacts = await _context.Contacts.AsNoTracking().Where(c => c.ContactCategoryId == contactCategory.Id).ToListAsync(); - if (existingContacts.Count > 0) + _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) { - List? contacts = new List(); - foreach (var contact in existingContacts) - { - contact.ContactCategoryId = null; - contacts.Add(contact); - } - _context.Contacts.UpdateRange(contacts); + _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 }); - await _context.SaveChangesAsync(); - _logger.LogInfo("Employee {EmployeeId} deleted contact category {ContactCategoryId}", loggedInEmployee.Id, id); - } - _logger.LogWarning("Employee {EmployeeId} tries to delete Category {CategoryId} but not found in database", loggedInEmployee.Id, id); - return ApiResponse.SuccessResponse(new { }, "Category deleted successfully", 200); + // 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 =================================================================== - public async Task> GetContactTags(Employee loggedInEmployee, Guid tenantId) + /// + /// 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) { - var taglist = await _context.ContactTagMasters.Where(t => t.TenantId == tenantId).ToListAsync(); - List contactTags = new List(); - foreach (var tag in taglist) + // Validate input parameters + if (loggedInEmployee == null) { - ContactTagVM tagVm = tag.ToContactTagVMFromContactTagMaster(); - contactTags.Add(tagVm); + _logger.LogWarning("GetContactTags: Request with null employee context"); + return ApiResponse.ErrorResponse("Invalid employee context", "Invalid employee context", 400); } - _logger.LogInfo("{count} contact Tags are fetched by Employee with ID {LoggedInEmployeeId}", contactTags.Count, loggedInEmployee.Id); - return ApiResponse.SuccessResponse(contactTags, System.String.Format("{0} contact tags fetched successfully", contactTags.Count), 200); - } - public async Task> CreateContactTag(CreateContactTagDto contactTagDto, Employee loggedInEmployee, Guid tenantId) - { - if (contactTagDto != null) + + if (tenantId == Guid.Empty) { - ContactTagMaster? existingContactTag = await _context.ContactTagMasters.FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Name.ToLower() == (contactTagDto.Name != null ? contactTagDto.Name.ToLower() : "")); - if (existingContactTag == null) + _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) { - ContactTagMaster contactTag = contactTagDto.ToContactTagMasterFromCreateContactTagDto(tenantId); - _context.ContactTagMasters.Add(contactTag); - await _context.SaveChangesAsync(); - ContactTagVM tagVM = contactTag.ToContactTagVMFromContactTagMaster(); - - _logger.LogInfo("Employee ID {LoggedInEmployeeId} created a contact tag {ContactTagId}.", loggedInEmployee.Id, contactTag.Id); - return ApiResponse.SuccessResponse(tagVM, "Tag Created Successfully", 200); + _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); } - _logger.LogWarning("Employee ID {LoggedInEmployeeId} attempted to create an existing contact tag.", loggedInEmployee.Id); - return ApiResponse.ErrorResponse("Tag already existed", "Tag already existed", 409); - } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", loggedInEmployee.Id); - return ApiResponse.ErrorResponse("User Send empty Payload", "User Send empty Payload", 400); - } - public async Task> UpdateContactTag(Guid id, UpdateContactTagDto contactTagDto, Employee loggedInEmployee, Guid tenantId) - { - if (contactTagDto != null && contactTagDto.Id == id) - { - ContactTagMaster? contactTag = await _context.ContactTagMasters.AsNoTracking().FirstOrDefaultAsync(t => t.TenantId == tenantId && t.Id == contactTagDto.Id); - if (contactTag != null) + + // Create new tag entity + var contactTag = new ContactTagMaster { - contactTag = contactTagDto.ToContactTagMasterFromUpdateContactTagDto(tenantId); - _context.ContactTagMasters.Update(contactTag); + Id = Guid.NewGuid(), + Name = trimmedName, + Description = model.Description.Trim(), // Normalize description + TenantId = tenantId, + }; - _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog - { - RefereanceId = contactTag.Id, - UpdatedById = loggedInEmployee.Id, - UpdateAt = DateTime.UtcNow - }); - await _context.SaveChangesAsync(); - await _context.SaveChangesAsync(); + // Add and save to database + _context.ContactTagMasters.Add(contactTag); + await _context.SaveChangesAsync(); - ContactTagVM contactTagVm = contactTag.ToContactTagVMFromContactTagMaster(); + // 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); - - _logger.LogInfo("Contact tag master {ConatctTagId} updated successfully by employee {EmployeeId}", contactTagVm.Id, loggedInEmployee.Id); - return ApiResponse.SuccessResponse(contactTagVm, "Contact Tag master updated successfully", 200); - } - _logger.LogWarning("Contact Tag master {ContactTagId} not found in database", id); - return ApiResponse.ErrorResponse("Contact Tag master not found", "Contact tag master not found", 404); + 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); } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", loggedInEmployee.Id); - return ApiResponse.ErrorResponse("User Send empty Payload", "User Send empty Payload", 400); } + + /// + /// 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) { - ContactTagMaster? contactTag = await _context.ContactTagMasters.FirstOrDefaultAsync(c => c.Id == id && c.TenantId == tenantId); - if (contactTag != null) + // Validate input parameters + if (loggedInEmployee == null) { - List? tagMappings = await _context.ContactTagMappings.Where(t => t.ContactTagId == contactTag.Id).ToListAsync(); + _logger.LogWarning("DeleteContactTag: Request with null employee context"); + return ApiResponse.ErrorResponse("Invalid employee context", "Invalid employee context", 400); + } - _context.ContactTagMasters.Remove(contactTag); - if (tagMappings.Any()) + 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) { - _context.ContactTagMappings.RemoveRange(tagMappings); + _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 }); - await _context.SaveChangesAsync(); - _logger.LogInfo("Employee {EmployeeId} deleted contact tag {ContactTagId}", loggedInEmployee.Id, id); - } - _logger.LogWarning("Employee {EmployeeId} tries to delete Tag {ContactTagId} but not found in database", loggedInEmployee.Id, id); - return ApiResponse.SuccessResponse(new { }, "Tag deleted successfully", 200); + // 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 @@ -1501,7 +2112,7 @@ namespace Marco.Pms.Services.Service // Saving the old entity in mongoDB - var mongoDBTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject + await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = expensesType.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), @@ -1556,7 +2167,7 @@ namespace Marco.Pms.Services.Service // Saving the old entity in mongoDB - var mongoDBTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject + await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = expensesType.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), @@ -1711,7 +2322,7 @@ namespace Marco.Pms.Services.Service // Saving the old entity in mongoDB - var mongoDBTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject + await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = paymentMode.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), @@ -1766,7 +2377,7 @@ namespace Marco.Pms.Services.Service // Saving the old entity in mongoDB - var mongoDBTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject + await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = paymentMode.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), @@ -1937,7 +2548,7 @@ namespace Marco.Pms.Services.Service // Saving the old entity in mongoDB - var mongoDBTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject + await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = documentCategory.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), @@ -1991,7 +2602,7 @@ namespace Marco.Pms.Services.Service // Saving the old entity in mongoDB - var mongoDBTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject + await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = documentCategory.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), @@ -2163,7 +2774,7 @@ namespace Marco.Pms.Services.Service // Saving the old entity in mongoDB - var mongoDBTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject + await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = documentType.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), @@ -2222,7 +2833,7 @@ namespace Marco.Pms.Services.Service // Saving the old entity in mongoDB - var mongoDBTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject + await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = documentType.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), @@ -2268,4 +2879,4 @@ namespace Marco.Pms.Services.Service #endregion } -} +} \ No newline at end of file