From 9e15cf04472bcbedc74d4427b6ddde0447b0354a Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 29 Jul 2025 15:00:23 +0530 Subject: [PATCH] Optimized the Create contact API in directory module --- Marco.Pms.Model/Directory/ContactEmail.cs | 4 +- Marco.Pms.Model/Directory/ContactPhone.cs | 4 +- .../Dtos/Directory/CreateContactEmailDto.cs | 1 + .../Dtos/Directory/CreateContactPhoneDto.cs | 1 + .../ViewModels/Directory/ContactProfileVM.cs | 2 +- .../Controllers/DirectoryController.cs | 24 +- .../MappingProfiles/MappingProfile.cs | 18 + .../Service/DirectoryService.cs | 543 +++++++++++++----- .../ServiceInterfaces/IDirectoryService.cs | 4 +- 9 files changed, 425 insertions(+), 176 deletions(-) diff --git a/Marco.Pms.Model/Directory/ContactEmail.cs b/Marco.Pms.Model/Directory/ContactEmail.cs index 1eb1b34..a572be6 100644 --- a/Marco.Pms.Model/Directory/ContactEmail.cs +++ b/Marco.Pms.Model/Directory/ContactEmail.cs @@ -1,6 +1,6 @@ -using System.ComponentModel; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using System.ComponentModel; using System.ComponentModel.DataAnnotations.Schema; -using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; namespace Marco.Pms.Model.Directory { diff --git a/Marco.Pms.Model/Directory/ContactPhone.cs b/Marco.Pms.Model/Directory/ContactPhone.cs index d10439b..f030bc7 100644 --- a/Marco.Pms.Model/Directory/ContactPhone.cs +++ b/Marco.Pms.Model/Directory/ContactPhone.cs @@ -1,6 +1,6 @@ -using System.ComponentModel; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using System.ComponentModel; using System.ComponentModel.DataAnnotations.Schema; -using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; namespace Marco.Pms.Model.Directory { diff --git a/Marco.Pms.Model/Dtos/Directory/CreateContactEmailDto.cs b/Marco.Pms.Model/Dtos/Directory/CreateContactEmailDto.cs index 654890a..0102c39 100644 --- a/Marco.Pms.Model/Dtos/Directory/CreateContactEmailDto.cs +++ b/Marco.Pms.Model/Dtos/Directory/CreateContactEmailDto.cs @@ -4,6 +4,7 @@ { public string? Label { get; set; } public string? EmailAddress { get; set; } + public bool IsPrimary { get; set; } = false; } } diff --git a/Marco.Pms.Model/Dtos/Directory/CreateContactPhoneDto.cs b/Marco.Pms.Model/Dtos/Directory/CreateContactPhoneDto.cs index a72ac3d..84c10c5 100644 --- a/Marco.Pms.Model/Dtos/Directory/CreateContactPhoneDto.cs +++ b/Marco.Pms.Model/Dtos/Directory/CreateContactPhoneDto.cs @@ -4,5 +4,6 @@ { public string? Label { get; set; } public string? PhoneNumber { get; set; } + public bool IsPrimary { get; set; } = false; } } diff --git a/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs b/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs index 4969dfe..9e8f4cb 100644 --- a/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs +++ b/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs @@ -21,6 +21,6 @@ namespace Marco.Pms.Model.ViewModels.Directory public List Projects { get; set; } = new List(); public List Buckets { get; set; } = new List(); public List Tags { get; set; } = new List(); - //public List Notes { get; set; } = new List(); + public List Notes { get; set; } = new List(); } } diff --git a/Marco.Pms.Services/Controllers/DirectoryController.cs b/Marco.Pms.Services/Controllers/DirectoryController.cs index f63907b..037838a 100644 --- a/Marco.Pms.Services/Controllers/DirectoryController.cs +++ b/Marco.Pms.Services/Controllers/DirectoryController.cs @@ -97,7 +97,8 @@ namespace Marco.Pms.Services.Controllers [HttpGet("organization")] public async Task GetOrganizationList() { - var response = await _directoryService.GetOrganizationList(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.GetOrganizationListAsync(tenantId, loggedInEmployee); return Ok(response); } @@ -106,24 +107,9 @@ namespace Marco.Pms.Services.Controllers [HttpPost] public async Task CreateContact([FromBody] CreateContactDto createContact) { - if (!ModelState.IsValid) - { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - _logger.LogWarning("User sent Invalid Date while marking attendance"); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - } - var response = await _directoryService.CreateContact(createContact); - if (response.StatusCode == 200) - { - return Ok(response); - } - else - { - return BadRequest(response); - } + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.CreateContactAsync(createContact, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } [HttpPut("{id}")] diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 0fe8d22..b6f5b4c 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -1,5 +1,6 @@ using AutoMapper; using Marco.Pms.Model.Directory; +using Marco.Pms.Model.Dtos.Directory; using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Master; @@ -74,15 +75,32 @@ namespace Marco.Pms.Services.MappingProfiles #region ======================================================= Directory ======================================================= CreateMap(); + CreateMap(); CreateMap(); CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap() + .ForMember( + dest => dest.ContactName, + opt => opt.MapFrom(src => src.Contact != null ? src.Contact.Name : string.Empty) + ) + .ForMember( + dest => dest.OrganizationName, + opt => opt.MapFrom(src => src.Contact != null ? src.Contact.Organization : string.Empty) + ); + #endregion diff --git a/Marco.Pms.Services/Service/DirectoryService.cs b/Marco.Pms.Services/Service/DirectoryService.cs index ebcdbec..2ff97b5 100644 --- a/Marco.Pms.Services/Service/DirectoryService.cs +++ b/Marco.Pms.Services/Service/DirectoryService.cs @@ -12,6 +12,7 @@ using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; +using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; using System.Text.Json; @@ -382,6 +383,129 @@ namespace Marco.Pms.Services.Service return ApiResponse.SuccessResponse(list, System.String.Format("{0} contacts fetched successfully", list.Count), 200); } + public async Task> GetContactsListByBucketIdAsync(Guid bucketId, Guid tenantId, Employee loggedInEmployee) + { + if (bucketId == Guid.Empty) + { + _logger.LogInfo("Employee ID {EmployeeId} sent an empty Bucket id", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Bucket ID is empty", "Bucket ID is empty", 400); + } + + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id); + + if (!hasAdminPermission && !hasManagerPermission && !hasUserPermission) + { + // Log the specific denial reason for security auditing. + _logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get organization list for tenant {TenantId} due to lack of permissions.", loggedInEmployee.Id, tenantId); + // Return a strongly-typed error response. + return ApiResponse.ErrorResponse("Access Denied", "You do not have permission to perform this action.", 403); + } + + Bucket? bucket = await _context.Buckets.FirstOrDefaultAsync(b => b.Id == bucketId && b.TenantId == tenantId); + if (bucket == null) + { + _logger.LogInfo("Employee ID {EmployeeId} attempted access to bucket ID {BucketId}, but not found in database", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Bucket not found", "Bucket not found", 404); + } + List? employeeBuckets = await _context.EmployeeBucketMappings.Where(em => em.BucketId == bucketId).ToListAsync(); + + EmployeeBucketMapping? employeeBucket = null; + if (hasAdminPermission) + { + employeeBucket = employeeBuckets.FirstOrDefault(); + } + else if (hasManagerPermission || hasUserPermission) + { + employeeBucket = employeeBuckets.FirstOrDefault(eb => eb.EmployeeId == loggedInEmployee.Id); + } + + + if (employeeBucket == null) + { + _logger.LogInfo("Employee ID {EmployeeId} does not have access to bucket ID {BucketId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("You do not have access to this bucket.", "You do not have access to this bucket.", 401); + } + + List contactBucket = await _context.ContactBucketMappings.Where(cb => cb.BucketId == bucketId).ToListAsync() ?? new List(); + List contactVMs = new List(); + if (contactBucket.Count > 0) + { + var contactIds = contactBucket.Select(cb => cb.ContactId).ToList(); + List contacts = await _context.Contacts.Include(c => c.ContactCategory).Where(c => contactIds.Contains(c.Id) && c.IsActive).ToListAsync(); + List phones = await _context.ContactsPhones.Where(p => contactIds.Contains(p.ContactId)).ToListAsync(); + List emails = await _context.ContactsEmails.Where(e => contactIds.Contains(e.ContactId)).ToListAsync(); + + List? tags = await _context.ContactTagMappings.Where(ct => contactIds.Contains(ct.ContactId)).ToListAsync(); + List? contactProjects = await _context.ContactProjectMappings.Where(cp => contactIds.Contains(cp.ContactId)).ToListAsync(); + List? contactBuckets = await _context.ContactBucketMappings.Where(cp => contactIds.Contains(cp.ContactId)).ToListAsync(); + + List tagIds = new List(); + List tagMasters = new List(); + if (tags.Count > 0) + { + tagIds = tags.Select(ct => ct.ContactTagId).ToList(); + tagMasters = await _context.ContactTagMasters.Where(t => tagIds.Contains(t.Id)).ToListAsync(); + } + + if (contacts.Count > 0) + { + + + foreach (var contact in contacts) + { + List? emailVMs = new List(); + List? phoneVMs = new List(); + List? tagVMs = new List(); + + List contactPhones = phones.Where(p => p.ContactId == contact.Id).ToList(); + List contactEmails = emails.Where(e => e.ContactId == contact.Id).ToList(); + + List? contactTags = tags.Where(t => t.ContactId == contact.Id).ToList(); + List? projectMappings = contactProjects.Where(cp => cp.ContactId == contact.Id).ToList(); + List? bucketMappings = contactBuckets.Where(cb => cb.ContactId == contact.Id).ToList(); + + if (contactPhones.Count > 0) + { + foreach (var phone in contactPhones) + { + ContactPhoneVM phoneVM = phone.ToContactPhoneVMFromContactPhone(); + phoneVMs.Add(phoneVM); + } + } + if (contactEmails.Count > 0) + { + foreach (var email in contactEmails) + { + ContactEmailVM emailVM = email.ToContactEmailVMFromContactEmail(); + emailVMs.Add(emailVM); + } + } + if (contactTags.Count > 0) + { + foreach (var contactTag in contactTags) + { + ContactTagMaster? tagMaster = tagMasters.Find(t => t.Id == contactTag.ContactTagId); + if (tagMaster != null) + { + ContactTagVM tagVM = tagMaster.ToContactTagVMFromContactTagMaster(); + tagVMs.Add(tagVM); + } + } + } + ContactVM contactVM = contact.ToContactVMFromContact(); + contactVM.ContactEmails = emailVMs; + contactVM.ContactPhones = phoneVMs; + contactVM.Tags = tagVMs; + contactVM.ProjectIds = projectMappings.Select(cp => cp.ProjectId).ToList(); + contactVM.BucketIds = bucketMappings.Select(cb => cb.BucketId).ToList(); + contactVMs.Add(contactVM); + } + } + + } + _logger.LogInfo("{count} contact from Bucket {BucketId} fetched by Employee {EmployeeId}", contactVMs.Count, bucketId, loggedInEmployee.Id); + return ApiResponse.SuccessResponse(contactVMs, $"{contactVMs.Count} contacts fetched successfully", 200); + } public async Task> GetContactsListByBucketId(Guid id) { Guid tenantId = _userHelper.GetTenantId(); @@ -611,192 +735,199 @@ namespace Marco.Pms.Services.Service .ToListAsync(); }); - await Task.WhenAll(phonesTask, emailsTask, contactProjectsTask, contactBucketsTask, contactTagsTask); + var contactNotesTask = Task.Run(async () => + { + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactNotes + .AsNoTracking() + .Include(cn => cn.Createdby) + .Include(cn => cn.UpdatedBy) + .Include(cn => cn.Contact) + .Where(cn => cn.ContactId == contact.Id && cn.Createdby != null && cn.Createdby.TenantId == tenantId) + .Select(cn => _mapper.Map(cn)) + .ToListAsync(); + }); + + await Task.WhenAll(phonesTask, emailsTask, contactProjectsTask, contactBucketsTask, contactTagsTask, contactNotesTask); contactVM.ContactPhones = phonesTask.Result; contactVM.ContactEmails = emailsTask.Result; contactVM.Tags = contactTagsTask.Result; contactVM.Buckets = contactBucketsTask.Result; contactVM.Projects = contactProjectsTask.Result; - _logger.LogInfo("Employee ID {EmployeeId} fetched profile of contact {COntactId}", loggedInEmployeeId, contact.Id); + contactVM.Notes = contactNotesTask.Result; + _logger.LogInfo("Employee ID {EmployeeId} fetched profile of contact {ContactId}", loggedInEmployeeId, contact.Id); return ApiResponse.SuccessResponse(contactVM, "Contact profile fetched successfully"); } catch (Exception ex) { - _logger.LogError(ex, "An unexpected error occurred while fetching contact list for Tenant {TenantId} by Employee {EmployeeId}", tenantId, loggedInEmployeeId); + _logger.LogError(ex, "An unexpected error occurred while fetching contact profile for Tenant {TenantId} by Employee {EmployeeId}", tenantId, loggedInEmployeeId); return ApiResponse.ErrorResponse("An internal error occurred.", ExceptionMapper(ex), 500); } } - public async Task> GetOrganizationList() - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var organizationList = await _context.Contacts.Where(c => c.TenantId == tenantId).Select(c => c.Organization).Distinct().ToListAsync(); - _logger.LogInfo("Employee {EmployeeId} fetched list of organizations in a tenant {TenantId}", LoggedInEmployee.Id, tenantId); - return ApiResponse.SuccessResponse(organizationList, $"{organizationList.Count} records of organization names fetched from contacts", 200); + /// + /// Asynchronously retrieves a distinct list of organization names for a given tenant. + /// + /// The unique identifier of the tenant. + /// The employee making the request, used for permission checks. + /// + /// An ApiResponse containing the list of organization names on success, + /// or an error response with appropriate status codes (403 for Forbidden, 500 for internal errors). + /// + public async Task> GetOrganizationListAsync(Guid tenantId, Employee loggedInEmployee) + { + // --- Parameter Validation --- + // Fail fast if essential parameters are not provided. + ArgumentNullException.ThrowIfNull(loggedInEmployee); + + var employeeId = loggedInEmployee.Id; + + try + { + // --- 1. Permission Check --- + // Verify that the employee has at least one of the required permissions to view this data. + // This prevents unauthorized data access early in the process. + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(employeeId); + + if (!hasAdminPermission && !hasManagerPermission && !hasUserPermission) + { + // Log the specific denial reason for security auditing. + _logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get organization list for tenant {TenantId} due to lack of permissions.", employeeId, tenantId); + // Return a strongly-typed error response. + return ApiResponse.ErrorResponse("Access Denied", "You do not have permission to perform this action.", 403); + } + + // --- 2. Database Query --- + // Build and execute the database query efficiently. + _logger.LogDebug("Fetching organization list for Tenant {TenantId} by Employee {EmployeeId}", tenantId, employeeId); + + var organizationList = await _context.Contacts + // Filter contacts by the specified tenant to ensure data isolation and Filter out contacts that do not have an organization name to ensure data quality. + .Where(c => c.TenantId == tenantId && !string.IsNullOrEmpty(c.Organization)) + + // Project only the 'Organization' column. This is a major performance optimization + // as it avoids loading entire 'Contact' entities into memory. + .Select(c => c.Organization) + // Let the database perform the distinct operation, which is highly efficient. + .Distinct() + // Execute the query asynchronously and materialize the results into a list. + .ToListAsync(); + + // --- 3. Success Response --- + // Log the successful operation with key details. + _logger.LogInfo("Successfully fetched {OrganizationCount} distinct organizations for Tenant {TenantId} for employee {EmployeeId}", organizationList.Count, tenantId, employeeId); + + // Return a strongly-typed success response with the data and a descriptive message. + return ApiResponse.SuccessResponse( + organizationList, + $"{organizationList.Count} unique organization(s) found.", + 200 + ); + } + catch (Exception ex) + { + // --- 4. Exception Handling --- + // Log the full exception details for effective debugging, including context. + _logger.LogError(ex, "An unexpected error occurred while fetching organization list for Tenant {TenantId} by Employee {EmployeeId}", tenantId, employeeId); + + // Return a generic, strongly-typed error response to the client to avoid leaking implementation details. + return ApiResponse.ErrorResponse("An internal server error occurred.", ExceptionMapper(ex), 500); + } } #endregion #region =================================================================== Contact Post APIs =================================================================== - public async Task> CreateContact(CreateContactDto createContact) + + /// + /// Creates a new contact along with its associated details such as phone numbers, emails, tags, and project/bucket mappings. + /// This operation is performed within a single database transaction to ensure data integrity. + /// + /// The DTO containing the details for the new contact. + /// The ID of the tenant to which the contact belongs. + /// The employee performing the action. + /// An ApiResponse containing the newly created contact's view model or an error. + public async Task> CreateContactAsync(CreateContactDto createContact, Guid tenantId, Employee loggedInEmployee) { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (createContact != null) + Guid loggedInEmployeeId = loggedInEmployee.Id; + + if (string.IsNullOrWhiteSpace(createContact.Name) || + string.IsNullOrWhiteSpace(createContact.Organization) || + !(createContact.BucketIds?.Any() ?? false)) { - List phones = new List(); - List emails = new List(); - List contactBucketMappings = new List(); - List contactTagMappings = new List(); + _logger.LogWarning("Validation failed for CreateContactAsync. Payload missing required fields. Triggered by Employee: {LoggedInEmployeeId}", loggedInEmployeeId); + return ApiResponse.ErrorResponse("Payload is missing required fields: Name, Organization, and at least one BucketId are required.", "Invalid Payload", 400); + } - Contact? contact = createContact.ToContactFromCreateContactDto(tenantId, LoggedInEmployee.Id); + using var transaction = await _context.Database.BeginTransactionAsync(); + _logger.LogInfo("Starting transaction to create contact for Tenant {TenantId} by Employee {EmployeeId}", tenantId, loggedInEmployeeId); + try + { + var contact = _mapper.Map(createContact); + contact.CreatedAt = DateTime.UtcNow; + contact.CreatedById = loggedInEmployeeId; + contact.TenantId = tenantId; _context.Contacts.Add(contact); - await _context.SaveChangesAsync(); - _logger.LogInfo("Contact with ID {ContactId} created by Employee with ID {LoggedInEmployeeId}", contact.Id, LoggedInEmployee.Id); - var tags = await _context.ContactTagMasters.Where(t => t.TenantId == tenantId).ToListAsync(); - var tagNames = tags.Select(t => t.Name.ToLower()).ToList(); - var buckets = await _context.Buckets.Where(b => b.TenantId == tenantId).Select(b => b.Id).ToListAsync(); - var projects = await _context.Projects.Where(p => p.TenantId == tenantId).Select(p => p.Id).ToListAsync(); + // --- Process Phones --- + var existingPhoneList = await _context.ContactsPhones + .Where(p => p.TenantId == tenantId && p.IsPrimary) + .Select(p => p.PhoneNumber) + .ToListAsync(); + var existingPhones = new HashSet(existingPhoneList); + var phoneVMs = ProcessContactPhones(createContact, contact, existingPhones); - if (createContact.ContactPhones != null) + // --- Process Emails --- + var existingEmailList = await _context.ContactsEmails + .Where(e => e.TenantId == tenantId && e.IsPrimary) + .Select(e => e.EmailAddress) + .ToListAsync(); + var existingEmails = new HashSet(existingEmailList, StringComparer.OrdinalIgnoreCase); + var emailVMs = ProcessContactEmails(createContact, contact, existingEmails); + + // --- Process Tags --- + var tenantTags = await _context.ContactTagMasters + .Where(t => t.TenantId == tenantId) + .ToDictionaryAsync(t => t.Name.ToLowerInvariant(), t => t); + var tagVMs = ProcessTags(createContact, contact, tenantTags); + + // --- Process Mappings --- + var contactBucketMappings = await ProcessBucketMappingsAsync(createContact, contact, tenantId); + var projectMappings = await ProcessProjectMappingsAsync(createContact, contact, tenantId); + + // --- Final Save and Commit --- + try { + var changesCount = await _context.SaveChangesAsync(); + await transaction.CommitAsync(); - foreach (var contactPhone in createContact.ContactPhones) - { - ContactPhone phone = contactPhone.ToContactPhoneFromCreateContactPhoneDto(tenantId, contact.Id); - phones.Add(phone); - } - _context.ContactsPhones.AddRange(phones); - _logger.LogInfo("{count} phone number are saved in contact with ID {ContactId} by employee with ID {LoggedEmployeeId}", phones.Count, contact.Id, LoggedInEmployee.Id); + _logger.LogInfo( + "Successfully created Contact {ContactId} with {Count} related records for Tenant {TenantId} by Employee {EmployeeId}. Transaction committed.", + contact.Id, changesCount - 1, tenantId, loggedInEmployeeId); } - if (createContact.ContactEmails != null) + catch (DbUpdateException dbEx) { - - foreach (var contactEmail in createContact.ContactEmails) - { - ContactEmail email = contactEmail.ToContactEmailFromCreateContactEmailDto(tenantId, contact.Id); - emails.Add(email); - } - _context.ContactsEmails.AddRange(emails); - _logger.LogInfo("{count} email addresses are saved in contact with ID {ContactId} by employee with ID {LoggedEmployeeId}", emails.Count, contact.Id, LoggedInEmployee.Id); + await transaction.RollbackAsync(); + _logger.LogError(dbEx, "Database exception during contact creation for Tenant {TenantId}. Transaction rolled back.", tenantId); + return ApiResponse.ErrorResponse("An internal database error occurred.", ExceptionMapper(dbEx), 500); } - if (createContact.BucketIds != null) - { - foreach (var bucket in createContact.BucketIds) - { - if (buckets.Contains(bucket)) - { - ContactBucketMapping bucketMapping = new ContactBucketMapping - { - BucketId = bucket, - ContactId = contact.Id - }; - contactBucketMappings.Add(bucketMapping); - } - } - _context.ContactBucketMappings.AddRange(contactBucketMappings); - _logger.LogInfo("Contact with ID {ContactId} added to {count} number of buckets by employee with ID {LoggedEmployeeId}", contact.Id, contactBucketMappings.Count, LoggedInEmployee.Id); - } - - if (createContact.ProjectIds != null) - { - List projectMappings = new List(); - foreach (var projectId in createContact.ProjectIds) - { - if (projects.Contains(projectId)) - { - ContactProjectMapping projectMapping = new ContactProjectMapping - { - ProjectId = projectId, - ContactId = contact.Id, - TenantId = tenantId - }; - projectMappings.Add(projectMapping); - } - } - _context.ContactProjectMappings.AddRange(projectMappings); - _logger.LogInfo("Contact with ID {ContactId} added to {count} number of project by employee with ID {LoggedEmployeeId}", contact.Id, projectMappings.Count, LoggedInEmployee.Id); - } - - if (createContact.Tags != null) - { - foreach (var tag in createContact.Tags) - { - if (tagNames.Contains(tag.Name.ToLower())) - { - ContactTagMaster existingTag = tags.Find(t => t.Name == tag.Name) ?? new ContactTagMaster(); - _context.ContactTagMappings.Add(new ContactTagMapping - { - ContactId = contact.Id, - ContactTagId = tag.Id ?? existingTag.Id - }); - } - else if (tag.Id == null || tags.Where(t => t.Name == tag.Name) == null) - { - var newtag = new ContactTagMaster - { - Name = tag.Name, - TenantId = tenantId - }; - _context.ContactTagMasters.Add(newtag); - ContactTagMapping tagMapping = new ContactTagMapping - { - ContactTagId = newtag.Id, - ContactId = contact.Id - }; - contactTagMappings.Add(tagMapping); - } - } - - _context.ContactTagMappings.AddRange(contactTagMappings); - _logger.LogInfo("{count} number of tags added to Contact with ID {ContactId} by employee with ID {LoggedEmployeeId}", contactTagMappings.Count, contact.Id, LoggedInEmployee.Id); - } - await _context.SaveChangesAsync(); - - ContactVM contactVM = new ContactVM(); - List phoneVMs = new List(); - - contact = await _context.Contacts.Include(c => c.ContactCategory).FirstOrDefaultAsync(c => c.Id == contact.Id) ?? new Contact(); - var tagIds = contactTagMappings.Select(t => t.ContactTagId).ToList(); - tags = await _context.ContactTagMasters.Where(t => t.TenantId == tenantId && tagIds.Contains(t.Id)).ToListAsync(); - List contactProjects = await _context.ContactProjectMappings.Where(cp => cp.ContactId == contact.Id).ToListAsync(); - List bucketMappings = await _context.ContactBucketMappings.Where(cb => cb.ContactId == contact.Id).ToListAsync(); - foreach (var phone in phones) - { - ContactPhoneVM phoneVM = phone.ToContactPhoneVMFromContactPhone(); - phoneVMs.Add(phoneVM); - } - List emailVMs = new List(); - foreach (var email in emails) - { - ContactEmailVM emailVM = email.ToContactEmailVMFromContactEmail(); - emailVMs.Add(emailVM); - } - List tagVMs = new List(); - foreach (var contactTagMapping in contactTagMappings) - { - ContactTagVM tagVM = new ContactTagVM(); - var tag = tags.Find(t => t.Id == contactTagMapping.ContactTagId); - tagVM = tag != null ? tag.ToContactTagVMFromContactTagMaster() : new ContactTagVM(); - tagVMs.Add(tagVM); - } - - - contactVM = contact.ToContactVMFromContact(); + // --- Construct and Return Response --- + var contactVM = _mapper.Map(contact); contactVM.ContactPhones = phoneVMs; contactVM.ContactEmails = emailVMs; contactVM.Tags = tagVMs; - contactVM.ProjectIds = contactProjects.Select(cp => cp.ProjectId).ToList(); - contactVM.BucketIds = bucketMappings.Select(cb => cb.BucketId).ToList(); + contactVM.BucketIds = contactBucketMappings.Select(cb => cb.BucketId).ToList(); + contactVM.ProjectIds = projectMappings.Select(cp => cp.ProjectId).ToList(); - return ApiResponse.SuccessResponse(contactVM, "Contact Created Successfully", 200); + return ApiResponse.SuccessResponse(contactVM, "Contact created successfully.", 201); + } + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected exception occurred during contact creation for Tenant {TenantId}. Transaction rolled back.", tenantId); + return ApiResponse.ErrorResponse("An unexpected internal error occurred.", ExceptionMapper(ex), 500); } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("User Send empty Payload", "User Send empty Payload", 400); } #endregion @@ -1787,6 +1918,118 @@ namespace Marco.Pms.Services.Service }; } + // --- Helper Methods for Readability and Separation of Concerns --- + + private List ProcessContactPhones(CreateContactDto dto, Contact contact, ISet existingPhones) + { + if (!(dto.ContactPhones?.Any() ?? false)) return new List(); + + var newPhones = dto.ContactPhones + .Where(p => !string.IsNullOrWhiteSpace(p.PhoneNumber) && existingPhones.Add(p.PhoneNumber)) // .Add returns true if the item was added (i.e., not present) + .Select(pDto => + { + var phone = _mapper.Map(pDto); + phone.ContactId = contact.Id; + phone.TenantId = contact.TenantId; // Ensure tenant is set on child entities + return phone; + }).ToList(); + + _context.ContactsPhones.AddRange(newPhones); + _logger.LogInfo("Adding {Count} new phone numbers for Contact {ContactId}.", newPhones.Count, contact.Id); + + return newPhones.Select(p => _mapper.Map(p)).ToList(); + } + private List ProcessContactEmails(CreateContactDto dto, Contact contact, ISet existingEmails) + { + if (!(dto.ContactEmails?.Any() ?? false)) return new List(); + + var newEmails = dto.ContactEmails + .Where(e => !string.IsNullOrWhiteSpace(e.EmailAddress) && existingEmails.Add(e.EmailAddress)) // HashSet handles case-insensitivity set in constructor + .Select(eDto => + { + var email = _mapper.Map(eDto); + email.ContactId = contact.Id; + email.TenantId = contact.TenantId; + return email; + }).ToList(); + + _context.ContactsEmails.AddRange(newEmails); + _logger.LogInfo("Adding {Count} new email addresses for Contact {ContactId}.", newEmails.Count, contact.Id); + + return newEmails.Select(e => _mapper.Map(e)).ToList(); + } + private List ProcessTags(CreateContactDto dto, Contact contact, IDictionary tenantTags) + { + if (!(dto.Tags?.Any() ?? false)) return new List(); + + var tagVMs = new List(); + var newTagMappings = new List(); + + foreach (var tagName in dto.Tags.Select(t => t.Name).Distinct(StringComparer.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(tagName)) continue; + + var normalizedTag = tagName.ToLowerInvariant(); + if (!tenantTags.TryGetValue(normalizedTag, out var tagMaster)) + { + // Tag does not exist, create it. + tagMaster = new ContactTagMaster { Name = tagName, Description = tagName, TenantId = contact.TenantId }; + _context.ContactTagMasters.Add(tagMaster); + tenantTags.Add(normalizedTag, tagMaster); // Add to dictionary to handle duplicates in the input list + _logger.LogDebug("Creating new tag '{TagName}' for Tenant {TenantId}.", tagName, contact.TenantId); + } + + newTagMappings.Add(new ContactTagMapping { ContactId = contact.Id, ContactTag = tagMaster }); + tagVMs.Add(_mapper.Map(tagMaster)); + } + + _context.ContactTagMappings.AddRange(newTagMappings); + _logger.LogInfo("Adding {Count} tag mappings for Contact {ContactId}.", newTagMappings.Count, contact.Id); + + return tagVMs; + } + private async Task> ProcessBucketMappingsAsync(CreateContactDto dto, Contact contact, Guid tenantId) + { + if (!(dto.BucketIds?.Any() ?? false)) return new List(); + + var validBucketIds = await _context.Buckets + .Where(b => dto.BucketIds.Contains(b.Id) && b.TenantId == tenantId) + .Select(b => b.Id) + .ToListAsync(); + + var mappings = validBucketIds.Select(bucketId => new ContactBucketMapping + { + BucketId = bucketId, + ContactId = contact.Id + }).ToList(); + + _context.ContactBucketMappings.AddRange(mappings); + _logger.LogInfo("Adding {Count} bucket mappings for Contact {ContactId}.", mappings.Count, contact.Id); + + return mappings; + } + private async Task> ProcessProjectMappingsAsync(CreateContactDto dto, Contact contact, Guid tenantId) + { + if (!(dto.ProjectIds?.Any() ?? false)) return new List(); + + var validProjectIds = await _context.Projects + .Where(p => dto.ProjectIds.Contains(p.Id) && p.TenantId == tenantId) + .Select(p => p.Id) + .ToListAsync(); + + var mappings = validProjectIds.Select(projectId => new ContactProjectMapping + { + ProjectId = projectId, + ContactId = contact.Id, + TenantId = tenantId + }).ToList(); + + _context.ContactProjectMappings.AddRange(mappings); + _logger.LogInfo("Adding {Count} project mappings for Contact {ContactId}.", mappings.Count, contact.Id); + + return mappings; + } + #endregion } } diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs index b9e1be2..1204ce7 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs @@ -10,8 +10,8 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetListOfContactsOld(string? search, bool active, ContactFilterDto? filterDto, Guid? projectId); Task> GetContactsListByBucketId(Guid id); Task> GetContactProfileAsync(Guid id, Guid tenantId, Employee loggedInEmployee); - Task> GetOrganizationList(); - Task> CreateContact(CreateContactDto createContact); + Task> GetOrganizationListAsync(Guid tenantId, Employee loggedInEmployee); + Task> CreateContactAsync(CreateContactDto createContact, Guid tenantId, Employee loggedInEmployee); Task> UpdateContact(Guid id, UpdateContactDto updateContact); Task> DeleteContact(Guid id, bool active); Task> GetListOFAllNotes(Guid? projectId, int pageSize, int pageNumber);