@ -1,10 +1,12 @@
using AutoMapper ;
using Marco.Pms.DataAccess.Data ;
using Marco.Pms.Helpers.Utility ;
using Marco.Pms.Model.Directory ;
using Marco.Pms.Model.Dtos.Directory ;
using Marco.Pms.Model.Employees ;
using Marco.Pms.Model.Entitlements ;
using Marco.Pms.Model.Mapper ;
using Marco.Pms.Model.MongoDBModels.Utility ;
using Marco.Pms.Model.Utilities ;
using Marco.Pms.Model.ViewModels.Directory ;
using Marco.Pms.Model.ViewModels.Master ;
@ -26,7 +28,13 @@ namespace Marco.Pms.Services.Service
private readonly ILoggingService _logger ;
private readonly UserHelper _userHelper ;
private readonly IMapper _mapper ;
private readonly PermissionServices _permissionServices ;
private readonly UtilityMongoDBHelper _updateLogsHelper ;
private static readonly string contactCollection = "ContactModificationLog" ;
private static readonly string contactPhoneCollection = "ContactPhoneModificationLog" ;
private static readonly string contactEmailCollection = "ContactEmailModificationLog" ;
public DirectoryService (
IDbContextFactory < ApplicationDbContext > dbContextFactory ,
@ -34,8 +42,7 @@ namespace Marco.Pms.Services.Service
ILoggingService logger ,
IServiceScopeFactory serviceScopeFactory ,
UserHelper userHelper ,
IMapper mapper ,
PermissionServices permissionServices )
IMapper mapper )
{
_dbContextFactory = dbContextFactory ? ? throw new ArgumentNullException ( nameof ( dbContextFactory ) ) ;
_context = context ? ? throw new ArgumentNullException ( nameof ( context ) ) ;
@ -43,7 +50,8 @@ namespace Marco.Pms.Services.Service
_serviceScopeFactory = serviceScopeFactory ? ? throw new ArgumentNullException ( nameof ( serviceScopeFactory ) ) ;
_userHelper = userHelper ? ? throw new ArgumentNullException ( nameof ( userHelper ) ) ;
_mapper = mapper ? ? throw new ArgumentNullException ( nameof ( mapper ) ) ;
_permissionServices = permissionServices ? ? throw new ArgumentNullException ( nameof ( permissionServices ) ) ;
using var scope = serviceScopeFactory . CreateScope ( ) ;
_updateLogsHelper = scope . ServiceProvider . GetRequiredService < UtilityMongoDBHelper > ( ) ;
}
#region = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = Contact APIs = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
@ -747,6 +755,66 @@ namespace Marco.Pms.Services.Service
return ApiResponse < object > . ErrorResponse ( "An internal server error occurred." , ExceptionMapper ( ex ) , 500 ) ;
}
}
public async Task < ApiResponse < object > > GetDesignationListAsync ( 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 designations list for tenant {TenantId} due to lack of permissions." , employeeId , tenantId ) ;
// Return a strongly-typed error response.
return ApiResponse < object > . 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 designations list for Tenant {TenantId} by Employee {EmployeeId}" , tenantId , employeeId ) ;
var designationsList = await _context . Contacts
// Filter contacts by the specified tenant to ensure data isolation and Filter out contacts that do not have an designations name to ensure data quality.
. Where ( c = > c . TenantId = = tenantId & & ! string . IsNullOrEmpty ( c . Designation ) )
// Project only the 'Designation' column. This is a major performance optimization
// as it avoids loading entire 'Contact' entities into memory.
. Select ( c = > c . Designation )
// 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 {DesignationsCount} distinct designations for Tenant {TenantId} for employee {EmployeeId}" , designationsList . Count , tenantId , employeeId ) ;
// Return a strongly-typed success response with the data and a descriptive message.
return ApiResponse < object > . SuccessResponse (
designationsList ,
$"{designationsList.Count} unique designation(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 designation 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 < object > . ErrorResponse ( "An internal server error occurred." , ExceptionMapper ( ex ) , 500 ) ;
}
}
#endregion
@ -968,6 +1036,19 @@ namespace Marco.Pms.Services.Service
var allTags = await context . ContactTagMasters . Where ( t = > t . TenantId = = tenantId ) . ToListAsync ( ) ;
var tagNameLookup = allTags . ToDictionary ( t = > t . Name . ToLowerInvariant ( ) , t = > t ) ;
var contactObject = _updateLogsHelper . EntityToBsonDocument ( contact ) ;
var contactUpdateLog = new UpdateLogsObject
{
EntityId = contact . Id . ToString ( ) ,
UpdatedById = loggedInEmployee . Id . ToString ( ) ,
OldObject = contactObject ,
UpdatedAt = DateTime . UtcNow
} ;
List < UpdateLogsObject > phoneUpdateLogs = new List < UpdateLogsObject > ( ) ;
List < UpdateLogsObject > emailUpdateLogs = new List < UpdateLogsObject > ( ) ;
// ---------------------- Update Phones -----------------------
if ( updateContact . ContactPhones ! = null )
{
@ -975,18 +1056,41 @@ namespace Marco.Pms.Services.Service
foreach ( var phoneDto in updateContact . ContactPhones )
{
var phoneEntity = phoneDto . ToContactPhoneFromUpdateContactPhoneDto ( tenantId , contact . Id ) ;
if ( phoneDto . Id ! = null & & phoneDto . Id ! = Guid . Empty & & phoneIds . Contains ( phoneEntity . Id ) )
var phoneEntity = _mapper . Map < ContactPhone > ( phoneDto ) ;
phoneEntity . TenantId = tenantId ;
phoneEntity . ContactId = contact . Id ;
var existingPhone = phones . FirstOrDefault ( p = > p . Id = = phoneEntity . Id ) ;
if ( phoneDto . Id ! = null & & phoneDto . Id ! = Guid . Empty & & existingPhone ! = null )
{
var phoneObject = _updateLogsHelper . EntityToBsonDocument ( existingPhone ) ;
phoneUpdateLogs . Add ( new UpdateLogsObject
{
EntityId = existingPhone . Id . ToString ( ) ,
UpdatedById = loggedInEmployee . Id . ToString ( ) ,
OldObject = phoneObject ,
UpdatedAt = DateTime . UtcNow
} ) ;
context . ContactsPhones . Update ( phoneEntity ) ;
}
else
{
context . ContactsPhones . Add ( phoneEntity ) ;
}
}
// Remove phones not updated in payload
foreach ( var phone in phones )
{
if ( ! updatedPhoneIds . Contains ( phone . Id ) )
{
var phoneObject = _updateLogsHelper . EntityToBsonDocument ( phone ) ;
phoneUpdateLogs . Add ( new UpdateLogsObject
{
EntityId = phone . Id . ToString ( ) ,
UpdatedById = loggedInEmployee . Id . ToString ( ) ,
OldObject = phoneObject ,
UpdatedAt = DateTime . UtcNow
} ) ;
context . ContactsPhones . Remove ( phone ) ;
}
}
@ -1003,18 +1107,44 @@ namespace Marco.Pms.Services.Service
foreach ( var emailDto in updateContact . ContactEmails )
{
var emailEntity = emailDto . ToContactEmailFromUpdateContactEmailDto ( tenantId , contact . Id ) ;
if ( emailDto . Id ! = null & & emailDto . Id ! = Guid . Empty & & emailIds . Contains ( emailEntity . Id ) )
var emailEntity = _mapper . Map < ContactEmail > ( emailDto ) ;
emailEntity . TenantId = tenantId ;
emailEntity . ContactId = contact . Id ;
var existingEmail = emails . FirstOrDefault ( e = > e . Id = = emailEntity . Id ) ;
if ( emailDto . Id ! = null & & emailDto . Id ! = Guid . Empty & & existingEmail ! = null )
{
var emailObject = _updateLogsHelper . EntityToBsonDocument ( existingEmail ) ;
emailUpdateLogs . Add ( new UpdateLogsObject
{
EntityId = existingEmail . Id . ToString ( ) ,
UpdatedById = loggedInEmployee . Id . ToString ( ) ,
OldObject = emailObject ,
UpdatedAt = DateTime . UtcNow
} ) ;
context . ContactsEmails . Update ( emailEntity ) ;
}
else
{
context . ContactsEmails . Add ( emailEntity ) ;
}
}
// Remove emails not updated in payload
foreach ( var email in emails )
{
if ( ! updatedEmailIds . Contains ( email . Id ) )
{
var emailObject = _updateLogsHelper . EntityToBsonDocument ( email ) ;
phoneUpdateLogs . Add ( new UpdateLogsObject
{
EntityId = email . Id . ToString ( ) ,
UpdatedById = loggedInEmployee . Id . ToString ( ) ,
OldObject = emailObject ,
UpdatedAt = DateTime . UtcNow
} ) ;
context . ContactsEmails . Remove ( email ) ;
}
}
@ -1047,6 +1177,7 @@ namespace Marco.Pms.Services.Service
{
if ( ! incomingBucketIds . Contains ( contactBucket . BucketId ) )
{
context . ContactBucketMappings . Remove ( contactBucket ) ;
}
}
@ -1163,6 +1294,8 @@ namespace Marco.Pms.Services.Service
}
// Reload updated contact and related data for response, using a fresh context
var responseTask = Task . Run ( async ( ) = >
{
using var responseContext = _dbContextFactory . CreateDbContext ( ) ;
var reloadedContact = await responseContext . Contacts
@ -1179,18 +1312,38 @@ namespace Marco.Pms.Services.Service
var tagsForResponse = await responseContext . ContactTagMasters . Where ( t = > tagIdsForResponse . Contains ( t . Id ) ) . ToListAsync ( ) ;
// Map entities to view models
var contactVM = reloadedContact . ToContactVMFromContact ( ) ;
var response = reloadedContact . ToContactVMFromContact ( ) ;
contactVM . ContactPhones = responsePhones . Select ( p = > p . ToContactPhoneVMFromContactPhone ( ) ) . ToList ( ) ;
contactVM . ContactEmails = responseEmails . Select ( e = > e . ToContactEmailVMFromContactEmail ( ) ) . ToList ( ) ;
contactVM . Tags = responseContactTags . Select ( ctm = >
response . ContactPhones = responsePhones . Select ( p = > p . ToContactPhoneVMFromContactPhone ( ) ) . ToList ( ) ;
response . ContactEmails = responseEmails . Select ( e = > e . ToContactEmailVMFromContactEmail ( ) ) . ToList ( ) ;
response . Tags = responseContactTags . Select ( ctm = >
{
var tag = tagsForResponse . Find ( t = > t . Id = = ctm . ContactTagId ) ;
return tag ! = null ? tag . ToContactTagVMFromContactTagMaster ( ) : new ContactTagVM ( ) ;
} ) . ToList ( ) ;
contactVM . BucketIds = responseContactBuckets . Select ( cb = > cb . BucketId ) . ToList ( ) ;
contactVM . ProjectIds = responseContactProjects . Select ( cp = > cp . ProjectId ) . ToList ( ) ;
response . BucketIds = responseContactBuckets . Select ( cb = > cb . BucketId ) . ToList ( ) ;
response . ProjectIds = responseContactProjects . Select ( cp = > cp . ProjectId ) . ToList ( ) ;
return response ;
} ) ;
var contactUpdateLogTask = Task . Run ( async ( ) = >
{
await _updateLogsHelper . PushToUpdateLogsAsync ( contactUpdateLog , contactCollection ) ;
} ) ;
var phoneUpdateLogTask = Task . Run ( async ( ) = >
{
await _updateLogsHelper . PushListToUpdateLogsAsync ( phoneUpdateLogs , contactPhoneCollection ) ;
} ) ;
var emailUpdateLogTask = Task . Run ( async ( ) = >
{
await _updateLogsHelper . PushListToUpdateLogsAsync ( emailUpdateLogs , contactEmailCollection ) ;
} ) ;
await Task . WhenAll ( responseTask , contactUpdateLogTask , phoneUpdateLogTask , emailUpdateLogTask ) ;
var contactVM = responseTask . Result ;
_logger . LogInfo ( "Contact {ContactId} successfully updated by employee {EmployeeId}." , contact . Id , loggedInEmployee . Id ) ;