using AutoMapper; using FirebaseAdmin.Messaging; using Marco.Pms.DataAccess.Data; using Marco.Pms.Helpers.Utility; using Marco.Pms.Model.DocumentManager; using Marco.Pms.Model.Dtos.DocumentManager; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Filters; 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.Services.Service; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; using System.Text.Json; using System.Text.RegularExpressions; using Document = Marco.Pms.Model.DocumentManager.Document; namespace Marco.Pms.Services.Controllers { [Route("api/[controller]")] [ApiController] [Authorize] public class DocumentController : ControllerBase { private readonly IDbContextFactory _dbContextFactory; private readonly IServiceScopeFactory _serviceScope; private readonly UserHelper _userHelper; private readonly ILoggingService _logger; private readonly IMapper _mapper; private readonly Guid tenantId; private static readonly Guid ProjectEntity = Guid.Parse("c8fe7115-aa27-43bc-99f4-7b05fabe436e"); private static readonly Guid EmployeeEntity = Guid.Parse("dbb9555a-7a0c-40f2-a9ed-f0463f1ceed7"); private static readonly string Collection = "DocumentModificationLog"; public DocumentController(IDbContextFactory dbContextFactory, IServiceScopeFactory serviceScope, UserHelper userHelper, ILoggingService logger, IMapper mapper) { _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _serviceScope = serviceScope ?? throw new ArgumentNullException(nameof(serviceScope)); _userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); tenantId = userHelper.GetTenantId(); } [HttpGet("list/{entityTypeId}/entity/{entityId}")] public async Task GetDocumentListAsync(Guid entityTypeId, Guid entityId, [FromQuery] string? filter, [FromQuery] string? searchString, [FromQuery] bool isActive = true, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20) { using var scope = _serviceScope.CreateScope(); await using var _context = await _dbContextFactory.CreateDbContextAsync(); var _permission = scope.ServiceProvider.GetRequiredService(); try { _logger.LogInfo("Fetching documents for EntityTypeId: {EntityTypeId}, EntityId: {EntityId}", entityTypeId, entityId); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Check global permission var hasViewPermission = false; if (ProjectEntity == entityTypeId) { hasViewPermission = await _permission.HasPermission(PermissionsMaster.ViewDocument, loggedInEmployee.Id, entityId); } else if (EmployeeEntity == entityTypeId) { hasViewPermission = await _permission.HasPermission(PermissionsMaster.ViewDocument, loggedInEmployee.Id); } if (!hasViewPermission && loggedInEmployee.Id != entityId) { _logger.LogWarning("Access Denied for Employee {EmployeeId} on EntityId {EntityId}", loggedInEmployee.Id, entityId); return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view documents", 403)); } // Validate entity type if (ProjectEntity != entityTypeId && EmployeeEntity != entityTypeId) { _logger.LogWarning("Invalid EntityTypeId: {EntityTypeId}", entityTypeId); return NotFound(ApiResponse.ErrorResponse("Entity type not found", "Entity Type not found in database", 404)); } // Project permission check if (ProjectEntity == entityTypeId) { var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, entityId); if (!hasProjectPermission) { _logger.LogWarning("Employee {EmployeeId} does not have project access for ProjectId {ProjectId}", loggedInEmployee.Id, entityId); return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to access project documents", 403)); } } // Employee validation else if (EmployeeEntity == entityTypeId) { var isEmployeeExists = await _context.Employees .AnyAsync(e => e.Id == entityId && e.TenantId == tenantId); if (!isEmployeeExists) { _logger.LogWarning("Employee {EmployeeId} not found for Tenant {TenantId}", entityId, tenantId); return NotFound(ApiResponse.ErrorResponse("Employee not found", "Employee not found in database", 404)); } } // Base Query (with includes to avoid lazy loading) IQueryable documentQuery = _context.DocumentAttachments .AsNoTracking() // Optimization: Read-only query .Include(da => da.UploadedBy) .ThenInclude(e => e!.JobRole) .Include(da => da.DocumentType!) .ThenInclude(dt => dt.DocumentCategory) .Where(da => da.EntityId == entityId && da.DocumentType != null && da.DocumentType.DocumentCategory != null && da.DocumentType.DocumentCategory.EntityTypeId == entityTypeId && da.IsActive == isActive && da.TenantId == tenantId); // Apply filter if provided var documentFilter = TryDeserializeFilter(filter); if (documentFilter != null) { _logger.LogInfo("Applying document filters for EntityId {EntityId}", entityId); if (documentFilter.IsVerified != null) documentQuery = documentQuery.Where(da => da.IsVerified == documentFilter.IsVerified); if (documentFilter.DocumentCategoryIds?.Any() ?? false) documentQuery = documentQuery.Where(da => documentFilter.DocumentCategoryIds.Contains(da.DocumentType!.DocumentCategoryId)); if (documentFilter.DocumentTypeIds?.Any() ?? false) documentQuery = documentQuery.Where(da => documentFilter.DocumentTypeIds.Contains(da.DocumentTypeId)); if (documentFilter.DocumentTagIds?.Any() ?? false) { var filteredIds = await _context.AttachmentTagMappings .AsNoTracking() .Where(at => at.DocumentTag != null && documentFilter.DocumentTagIds.Contains(at.DocumentTag.Id) && at.TenantId == tenantId) .Select(at => at.AttachmentId) .ToListAsync(); documentQuery = documentQuery.Where(da => filteredIds.Contains(da.Id)); } if (documentFilter.UploadedByIds?.Any() ?? false) documentQuery = documentQuery.Where(da => documentFilter.UploadedByIds.Contains(da.UploadedById)); // Date Range Filtering (Uploaded vs Verified date) if (documentFilter.StartDate != null && documentFilter.EndDate != null) { if (documentFilter.IsUploadedAt) { documentQuery = documentQuery.Where(da => da.UploadedAt.Date >= documentFilter.StartDate.Value.Date && da.UploadedAt.Date <= documentFilter.EndDate.Value.Date); } else { documentQuery = documentQuery.Where(da => da.VerifiedAt.HasValue && da.VerifiedAt.Value.Date >= documentFilter.StartDate.Value.Date && da.VerifiedAt.Value.Date <= documentFilter.EndDate.Value.Date); } } else { documentQuery = documentQuery.Where(da => da.IsCurrentVersion); } } else { // Default: only latest version documentQuery = documentQuery.Where(da => da.IsCurrentVersion); } // Apply search filter if (!string.IsNullOrWhiteSpace(searchString)) { documentQuery = documentQuery.Where(da => da.Name.Contains(searchString) || (da.DocumentId != null && da.DocumentId.Contains(searchString)) ); } var totalCount = await documentQuery.CountAsync(); var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); ; // Apply pagination & ordering var documents = await documentQuery .OrderByDescending(t => t.UploadedAt) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); var documentIds = documents.Select(da => da.Id).ToList(); // Get versions for the selected documents var versions = await _context.AttachmentVersionMappings .AsNoTracking() .Where(av => documentIds.Contains(av.ChildAttachmentId) && av.TenantId == tenantId) .ToListAsync(); // Map to ViewModel var documentListVMs = documents.Select(doc => { var version = versions.FirstOrDefault(v => v.ChildAttachmentId == doc.Id); var vm = _mapper.Map(doc); vm.ParentAttachmentId = version?.ParentAttachmentId; vm.Version = version?.Version ?? 1; return vm; }).ToList(); _logger.LogInfo("Fetched {Count} documents for EntityId {EntityId}", documentListVMs.Count, entityId); var response = new { CurrentFilter = documentFilter, CurrentPage = pageNumber, TotalPages = totalPages, TotalEntites = totalCount, Data = documentListVMs, }; return Ok(ApiResponse.SuccessResponse(response, "Document list fetched successfully", 200)); } catch (Exception ex) { _logger.LogError(ex, "Error fetching documents for EntityId {EntityId}", entityId); return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", ex.Message, 500)); } } [HttpGet("get/details/{id}")] public async Task GetDetailsAsync(Guid id) { _logger.LogInfo("Starting GetDetails API for AttachmentId: {AttachmentId}", id); // Create a new DbContext instance to fetch data await using var _context = await _dbContextFactory.CreateDbContextAsync(); // Create a new scoped service provider to resolve scoped dependencies using var scope = _serviceScope.CreateScope(); // Resolve the permission service from the scoped service provider var _permission = scope.ServiceProvider.GetRequiredService(); // Get the currently logged-in employee var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); _logger.LogDebug("Logged in employee id: {EmployeeId}", loggedInEmployee.Id); // Fetch the AttachmentVersionMapping with all necessary related data loaded eagerly var versionMapping = await _context.AttachmentVersionMappings .Include(av => av.ChildAttachment) .ThenInclude(da => da!.UploadedBy) .ThenInclude(e => e!.JobRole) .Include(av => av.ChildAttachment) .ThenInclude(da => da!.UpdatedBy) .ThenInclude(e => e!.JobRole) .Include(av => av.ChildAttachment) .ThenInclude(da => da!.VerifiedBy) .ThenInclude(e => e!.JobRole) .Include(av => av.ChildAttachment) .ThenInclude(da => da!.DocumentType) .ThenInclude(dt => dt!.DocumentCategory) .Include(av => av.ChildAttachment) .ThenInclude(da => da!.Document) .FirstOrDefaultAsync(av => av.ChildAttachmentId == id && av.TenantId == tenantId); // If no mapping found, return 404 if (versionMapping == null || versionMapping.ChildAttachment == null) { _logger.LogWarning("AttachmentVersionMapping not found for AttachmentId: {AttachmentId}, TenantId: {TenantId}", id, tenantId); return NotFound(ApiResponse.ErrorResponse("Document Attachment not found", "Document Attachment not found in database", 404)); } // Check if the logged in employee has permission to view the document OR is the owner of the attachment entity var hasViewPermission = false; if (ProjectEntity == versionMapping.ChildAttachment.DocumentType!.DocumentCategory!.EntityTypeId) { hasViewPermission = await _permission.HasPermission(PermissionsMaster.ViewDocument, loggedInEmployee.Id, versionMapping.ChildAttachment.EntityId); } else if (EmployeeEntity == versionMapping.ChildAttachment.DocumentType!.DocumentCategory!.EntityTypeId) { hasViewPermission = await _permission.HasPermission(PermissionsMaster.ViewDocument, loggedInEmployee.Id); } if (!hasViewPermission && loggedInEmployee.Id != versionMapping.ChildAttachment.EntityId) { _logger.LogWarning("Access Denied for Employee {EmployeeId} on EntityId {EntityId}", loggedInEmployee.Id, versionMapping.ChildAttachment.EntityId); return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view documents", 403)); } var tags = await _context.AttachmentTagMappings .Include(at => at.DocumentTag) .Where(at => at.AttachmentId == versionMapping.ChildAttachmentId && at.TenantId == tenantId && at.DocumentTag != null) .Select(at => new DocumentTagDto { Name = at.DocumentTag!.Name, IsActive = true }) .ToListAsync(); // Map the domain entity to the view model var documentAttachmentVM = _mapper.Map(versionMapping.ChildAttachment); documentAttachmentVM.Version = versionMapping.Version; documentAttachmentVM.ParentAttachmentId = versionMapping.ParentAttachmentId; documentAttachmentVM.Tags = tags; documentAttachmentVM.FileSize = versionMapping.ChildAttachment.Document!.FileSize; documentAttachmentVM.ContentType = versionMapping.ChildAttachment.Document.ContentType; _logger.LogInfo("Document details fetched successfully for AttachmentId: {AttachmentId}", id); // Return success response with document details return Ok(ApiResponse.SuccessResponse(documentAttachmentVM, "Document details fetched successfully", 200)); } [HttpGet("get/filter/{entityTypeId}")] public async Task GetFilterObjectAsync(Guid entityTypeId) { // Log: Starting filter fetch process _logger.LogInfo("Initiating GetFilterObjectAsync to retrieve document filter data."); await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScope.CreateScope(); // Get current logged-in employee var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); _logger.LogDebug("Fetched current employee: {EmployeeId}", loggedInEmployee.Id); // Fetch all relevant document attachments for the tenant with related data var documentList = await _context.DocumentAttachments .Include(da => da.UploadedBy) .Include(da => da.DocumentType) .ThenInclude(dt => dt!.DocumentCategory) .Where(da => da.DocumentType != null && da.DocumentType.DocumentCategory != null && da.DocumentType.DocumentCategory.EntityTypeId == entityTypeId && da.TenantId == tenantId) .ToListAsync(); _logger.LogInfo("Fetched {Count} document attachments for tenant {TenantId}", documentList.Count, tenantId); // Select IDs for attachments present in documentList var documentIds = documentList.Select(da => da.Id).ToList(); // Preload tags for given ids var documentTags = await _context.AttachmentTagMappings .Where(at => documentIds.Contains(at.AttachmentId) && at.DocumentTag != null) .Select(at => new { Id = at.DocumentTag!.Id, Name = at.DocumentTag.Name }) .Distinct() .ToListAsync(); _logger.LogInfo("Loaded {Count} document tags", documentTags.Count); // Gather unique UploadedBy users var uploadedBy = documentList .Where(da => da.UploadedBy != null) .Select(da => new { Id = da.UploadedBy!.Id, Name = $"{da.UploadedBy.FirstName} {da.UploadedBy.LastName}" }) .Distinct() .ToList(); _logger.LogInfo("Collected {Count} unique uploaders", uploadedBy.Count); // Gather unique DocumentCategories var documentCategories = documentList .Where(da => da.DocumentType?.DocumentCategory != null) .Select(da => new { Id = da.DocumentType!.DocumentCategory!.Id, Name = da.DocumentType.DocumentCategory.Name }) .Distinct() .ToList(); _logger.LogInfo("Collected {Count} unique document categories", documentCategories.Count); // Gather unique DocumentTypes var documentTypes = documentList .Where(da => da.DocumentType != null) .Select(da => new { Id = da.DocumentType!.Id, Name = da.DocumentType.Name }) .Distinct() .ToList(); _logger.LogInfo("Collected {Count} unique document types", documentTypes.Count); // Compose response var response = new { UploadedBy = uploadedBy, DocumentCategory = documentCategories, DocumentType = documentTypes, DocumentTag = documentTags }; _logger.LogInfo("Returning filter response successfully."); return Ok(ApiResponse.SuccessResponse(response, "Filters for documents fetched successfully", 200)); } [HttpGet("list/versions/{parentAttachmentId}")] public async Task GetListAllVersionsAsync(Guid parentAttachmentId, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20) { _logger.LogInfo("Start fetching document versions for ParentAttachmentId: {ParentAttachmentId}", parentAttachmentId); // Create a new DbContext instance asynchronously await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScope.CreateScope(); var _permission = scope.ServiceProvider.GetRequiredService(); try { // Retrieve currently logged in employee details for potential security or filtering checks var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (loggedInEmployee == null) { _logger.LogWarning("No logged in employee found while fetching versions for ParentAttachmentId: {ParentAttachmentId}", parentAttachmentId); return Unauthorized(ApiResponse.ErrorResponse("Unauthorized access", 401)); } // Retrieve all version mappings linked to the parent attachment and tenant var versionMappingsQuery = _context.AttachmentVersionMappings .Include(av => av.ChildAttachment) .ThenInclude(da => da!.DocumentType) .ThenInclude(dt => dt!.DocumentCategory) .Include(av => av.ChildAttachment) .ThenInclude(da => da!.UploadedBy) .ThenInclude(e => e!.JobRole) .Include(av => av.ChildAttachment) .ThenInclude(da => da!.UpdatedBy) .ThenInclude(e => e!.JobRole) .Include(av => av.ChildAttachment) .ThenInclude(da => da!.VerifiedBy) .ThenInclude(e => e!.JobRole) .Include(av => av.ChildAttachment) .ThenInclude(da => da!.Document) .Where(av => av.ParentAttachmentId == parentAttachmentId && av.TenantId == tenantId); var totalCount = await versionMappingsQuery.CountAsync(); var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); ; var versionMappings = await versionMappingsQuery .OrderByDescending(da => da.ChildAttachment!.UploadedAt) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); var entityId = versionMappings.Select(av => av.ChildAttachment?.EntityId).FirstOrDefault(); var entityTypeId = versionMappings.Select(av => av.ChildAttachment?.DocumentType?.DocumentCategory?.EntityTypeId).FirstOrDefault(); // Check global permission var hasViewPermission = false; if (ProjectEntity == entityTypeId) { hasViewPermission = await _permission.HasPermission(PermissionsMaster.ViewDocument, loggedInEmployee.Id, entityId); } else if (EmployeeEntity == entityTypeId) { hasViewPermission = await _permission.HasPermission(PermissionsMaster.ViewDocument, loggedInEmployee.Id); } if (!hasViewPermission && loggedInEmployee.Id != entityId) { _logger.LogWarning("Access Denied for Employee {EmployeeId} on EntityId {EntityId}", loggedInEmployee.Id, entityId ?? Guid.Empty); return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view documents", 403)); } _logger.LogInfo("Found {Count} versions for ParentAttachmentId: {ParentAttachmentId}", versionMappings.Count, parentAttachmentId); // Map the retrieved child attachments to view models with version info var attachmentVersionVMs = versionMappings.Select(versionMapping => { var documentVM = _mapper.Map(versionMapping.ChildAttachment); documentVM.Version = versionMapping.Version; documentVM.FileSize = versionMapping.ChildAttachment!.Document!.FileSize; documentVM.ContentType = versionMapping.ChildAttachment.Document!.ContentType; return documentVM; }).ToList(); var response = new { CurrentPage = pageNumber, TotalPages = totalPages, TotalEntites = totalCount, Data = attachmentVersionVMs, }; _logger.LogInfo("Successfully mapped version data for ParentAttachmentId: {ParentAttachmentId}", parentAttachmentId); return Ok(ApiResponse.SuccessResponse(response, "Document versions fetched successfully", 200)); } catch (Exception ex) { // Log the exception and return an internal server error response _logger.LogError(ex, "Error occurred while fetching document versions for ParentAttachmentId: {ParentAttachmentId}", parentAttachmentId); return StatusCode(500, ApiResponse.ErrorResponse("An error occurred while fetching document versions", 500)); } finally { _logger.LogInfo("End processing GetAllVersions for ParentAttachmentId: {ParentAttachmentId}", parentAttachmentId); } } [HttpGet("get/version/{id}")] public async Task GetAllVersionsAsync(Guid id) { // API endpoint to get all versions of an attachment by its ID _logger.LogInfo($"Fetching versions for attachment with ID: {id}"); await using var _context = await _dbContextFactory.CreateDbContextAsync(); // Create a new DbContext from the factory asynchronously using var scope = _serviceScope.CreateScope(); // Create a new service scope for scoped services var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Get the currently logged in employee asynchronously var versionMapping = await _context.AttachmentVersionMappings // Retrieve version mapping including the child attachment and its document .Include(av => av.ChildAttachment) .ThenInclude(da => da!.Document) .Include(av => av.ChildAttachment) .ThenInclude(da => da!.DocumentType) .ThenInclude(dt => dt!.DocumentCategory) .FirstOrDefaultAsync(av => av.ChildAttachmentId == id); if (versionMapping == null || versionMapping.ChildAttachment == null || versionMapping.ChildAttachment.Document == null) // Return 404 if no version mapping or related entities found { _logger.LogWarning($"Version mapping not found for attachment ID: {id}"); return NotFound(ApiResponse.ErrorResponse("Version not found", "Version not found in database", 404)); } var _permission = scope.ServiceProvider.GetRequiredService(); var hasDownloadPermission = false; if (ProjectEntity == versionMapping.ChildAttachment.DocumentType?.DocumentCategory?.EntityTypeId) { hasDownloadPermission = await _permission.HasPermission(PermissionsMaster.DownloadDocument, loggedInEmployee.Id, versionMapping.ChildAttachment.EntityId); } else if (EmployeeEntity == versionMapping.ChildAttachment.DocumentType?.DocumentCategory?.EntityTypeId) { hasDownloadPermission = await _permission.HasPermission(PermissionsMaster.DownloadDocument, loggedInEmployee.Id); } if (!hasDownloadPermission && loggedInEmployee.Id != versionMapping.ChildAttachment.EntityId) { _logger.LogWarning("Access Denied for Employee {EmployeeId} on EntityId {EntityId} for downloading", loggedInEmployee.Id, versionMapping.ChildAttachment.EntityId); return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to download documents", 403)); } var s3Service = scope.ServiceProvider.GetRequiredService(); // Resolve S3UploadService from the scope to generate pre-signed URL var preSignedUrl = s3Service.GeneratePreSignedUrl(versionMapping.ChildAttachment.Document.S3Key); // Generate pre-signed URL for the document stored in S3 _logger.LogInfo($"Generated pre-signed URL for attachment ID: {id}"); return Ok(ApiResponse.SuccessResponse(preSignedUrl, "Pre-Signed Url for old version fetched successfully", 200)); // Return the pre-signed URL with a success response } [HttpGet("get/tags")] public async Task GetAllDocumentTagsAsync() { // Log: API endpoint execution started _logger.LogInfo("Executing GetAllDocumentTagsAsync to retrieve document tags for tenant."); // Immediately create DbContext asynchronously using the factory pattern for efficiency and DI compliance await using var context = await _dbContextFactory.CreateDbContextAsync(); // Create a new DI scope for scoped services (e.g., user/context-specific dependencies) using var scope = _serviceScope.CreateScope(); try { // Fetch the currently logged-in employee (auth/user context) var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (loggedInEmployee == null) { _logger.LogWarning("Current employee could not be identified."); return Unauthorized(ApiResponse.ErrorResponse("Unauthorized", 401)); } // Retrieve the tags that belong to the specified tenant and project only the names (performance: projection) var tags = await context.DocumentTagMasters .Where(dt => dt.TenantId == tenantId) .Select(dt => new { Name = dt.Name, IsActive = true }) .ToListAsync(); _logger.LogInfo("Successfully retrieved {TagCount} document tags for tenant {TenantId}.", tags.Count, tenantId); // Return tags wrapped in ApiResponse object return Ok(ApiResponse.SuccessResponse(tags, "Tags fetched successfully", 200)); } catch (Exception ex) { // Log: Unexpected error handling _logger.LogError(ex, "Error occurred while retrieving document tags for tenant {TenantId}.", tenantId); return StatusCode(500, ApiResponse.ErrorResponse("An error occurred while fetching tags.", 500)); } } /// /// Uploads a document attachment for an Employee or Project. /// Validates permissions, document type, entity existence, tags, and uploads to S3. /// [HttpPost("upload")] public async Task UploadDocumentAsync([FromBody] DocumentAttachmentDto model) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScope.CreateScope(); _logger.LogInfo("Document upload initiated for EntityId: {EntityId}, DocumentTypeId: {DocumentTypeId}", model.EntityId, model.DocumentTypeId); try { // Get logged in user var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Validate Document Type var documentType = await _context.DocumentTypeMasters .Include(dt => dt.DocumentCategory) .FirstOrDefaultAsync(dt => dt.Id == model.DocumentTypeId && dt.TenantId == tenantId && dt.DocumentCategory != null); if (documentType == null) { _logger.LogWarning("DocumentTypeId {DocumentTypeId} not found for Tenant {TenantId}", model.DocumentTypeId, tenantId); return NotFound(ApiResponse.ErrorResponse("Document Type not found", "Document Type not found in database", 404)); } // Permission check var _permission = scope.ServiceProvider.GetRequiredService(); var hasUploadPermission = false; if (ProjectEntity == documentType.DocumentCategory?.EntityTypeId) { hasUploadPermission = await _permission.HasPermission(PermissionsMaster.UploadDocument, loggedInEmployee.Id, model.EntityId); } else if (EmployeeEntity == documentType.DocumentCategory?.EntityTypeId) { hasUploadPermission = await _permission.HasPermission(PermissionsMaster.UploadDocument, loggedInEmployee.Id); } if (!hasUploadPermission && loggedInEmployee.Id != model.EntityId) { _logger.LogWarning("Access Denied. User {UserId} tried to upload document for {EntityId}", loggedInEmployee.Id, model.EntityId); return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to upload this document", 403)); } // Document ID validation if (documentType.IsMandatory && string.IsNullOrWhiteSpace(model.DocumentId)) { _logger.LogWarning("Mandatory DocumentId missing for DocumentTypeId: {DocumentTypeId}", documentType.Id); return BadRequest(ApiResponse.ErrorResponse("Document ID missing", "User must provide the document ID for this document", 400)); } if (documentType.IsValidationRequired && !string.IsNullOrWhiteSpace(model.DocumentId) && !string.IsNullOrWhiteSpace(documentType.RegexExpression)) { if (!Regex.IsMatch(model.DocumentId, documentType.RegexExpression)) { _logger.LogWarning("Invalid DocumentId format for DocumentTypeId: {DocumentTypeId}, Provided: {DocumentId}", documentType.Id, model.DocumentId); return BadRequest(ApiResponse.ErrorResponse("Invalid Document ID", "Provided document ID is not valid", 400)); } } // Verify if Employee/Project exists var entityType = documentType.DocumentCategory!.EntityTypeId; bool entityExists = false; if (entityType.Equals(EmployeeEntity)) { entityExists = await _context.Employees.AnyAsync(e => e.Id == model.EntityId && e.TenantId == tenantId); } else if (entityType.Equals(ProjectEntity)) { entityExists = await _context.Projects.AnyAsync(p => p.Id == model.EntityId && p.TenantId == tenantId); } if (!entityExists) { _logger.LogWarning("Entity not found. EntityType: {EntityType}, EntityId: {EntityId}", entityType, model.EntityId); return NotFound(ApiResponse.ErrorResponse($"{(entityType == EmployeeEntity ? "Employee" : "Project")} Not Found", "Entity not found in database", 404)); } // Map DTO to DB entity var attachment = _mapper.Map(model); attachment.UploadedAt = DateTime.UtcNow; attachment.UploadedById = loggedInEmployee.Id; attachment.IsCurrentVersion = true; attachment.TenantId = tenantId; // Validate Attachment var allowedSize = documentType.MaxSizeAllowedInMB * 1024; if (model.Attachment.FileSize > allowedSize) { _logger.LogWarning("File size {FileSize} exceeded max allowed {MaxSize}MB", model.Attachment.FileSize, documentType.MaxSizeAllowedInMB); return BadRequest(ApiResponse.ErrorResponse("File size limit exceeded", $"Max allowed {documentType.MaxSizeAllowedInMB} MB.", 400)); } string base64 = model.Attachment.Base64Data?.Split(',').LastOrDefault() ?? ""; if (string.IsNullOrWhiteSpace(base64)) { _logger.LogWarning("Missing Base64 data for attachment."); return BadRequest(ApiResponse.ErrorResponse("Base64 data missing", "File data required", 400)); } var s3Service = scope.ServiceProvider.GetRequiredService(); var fileType = s3Service.GetContentTypeFromBase64(base64); var validContentTypes = documentType.AllowedContentType.Split(',').ToList(); if (!validContentTypes.Contains(fileType)) { _logger.LogWarning("Unsupported file type {FileType} for DocumentType {DocumentTypeId}", fileType, documentType.Id); return BadRequest(ApiResponse.ErrorResponse("Unsupported file type", $"Unsupported file type: {fileType}", 400)); } // Generate S3 ObjectKey/FileName string folderName = entityType == EmployeeEntity ? "EmployeeDocuments" : "ProjectDocuments"; string fileName = s3Service.GenerateFileName(fileType, tenantId, folderName); string objectKey = entityType == EmployeeEntity ? $"tenant-{tenantId}/Employee/{model.EntityId}/{folderName}/{fileName}" : $"tenant-{tenantId}/project-{model.EntityId}/{folderName}/{fileName}"; // Fire-and-forget upload _ = Task.Run(async () => { var logger = scope.ServiceProvider.GetRequiredService(); try { await s3Service.UploadFileAsync(base64, fileType, objectKey); logger.LogInfo("File uploaded successfully to S3: {ObjectKey}", objectKey); } catch (Exception ex) { logger.LogError(ex, "S3 upload failed for {ObjectKey}.", objectKey); } }); // Create Document record var document = new Document { BatchId = Guid.NewGuid(), UploadedById = loggedInEmployee.Id, FileName = model.Attachment.FileName ?? fileName, ContentType = model.Attachment.ContentType ?? fileType, S3Key = objectKey, FileSize = model.Attachment.FileSize, UploadedAt = DateTime.UtcNow, TenantId = tenantId }; _context.Documents.Add(document); attachment.DocumentDataId = document.Id; _context.DocumentAttachments.Add(attachment); //Process Versioning var versionMapping = new AttachmentVersionMapping { ParentAttachmentId = attachment.Id, ChildAttachmentId = attachment.Id, Version = 1, TenantId = tenantId }; _context.AttachmentVersionMappings.Add(versionMapping); // Process Tags if (model.Tags?.Any() == true) { var names = model.Tags.Select(t => t.Name).ToList(); var existingTags = await _context.DocumentTagMasters .Where(t => names.Contains(t.Name) && t.TenantId == tenantId) .ToListAsync(); var attachmentTagMappings = new List(); foreach (var tag in model.Tags.Where(t => t.IsActive)) { var existingTag = existingTags.FirstOrDefault(t => t.Name == tag.Name); var tagEntity = existingTag ?? new DocumentTagMaster { Id = Guid.NewGuid(), Name = tag.Name, Description = tag.Name, TenantId = tenantId }; if (existingTag == null) { _context.DocumentTagMasters.Add(tagEntity); } attachmentTagMappings.Add(new AttachmentTagMapping { DocumentTagId = tagEntity.Id, AttachmentId = attachment.Id, TenantId = tenantId }); } _context.AttachmentTagMappings.AddRange(attachmentTagMappings); } await _context.SaveChangesAsync(); _logger.LogInfo("Document uploaded successfully. AttachmentId: {AttachmentId}, DocumentId: {DocumentId}", attachment.Id, document.Id); var response = _mapper.Map(attachment); response.UploadedBy = _mapper.Map(loggedInEmployee); response.ParentAttachmentId = versionMapping.ParentAttachmentId; response.Version = versionMapping.Version; var _firebase = scope.ServiceProvider.GetRequiredService(); _ = Task.Run(async () => { // --- Push Notification Section --- // This section attempts to send a test push notification to the user's device. // It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens. var name = $"{loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; if (EmployeeEntity == entityType && model.EntityId != loggedInEmployee.Id) { var notification = new Notification { Title = $"Document added to Employee.", Body = $"Document added to your profile of type \"{documentType.Name}\" by {name}" }; await _firebase.SendEmployeeDocumentMessageAsync(attachment.Id, model.EntityId, notification, tenantId); } if (ProjectEntity == entityType) { var notification = new Notification { Title = $"Document added to Project.", Body = $"Document added to your Project of type \"{documentType.Name}\" by {name}" }; await _firebase.SendProjectDocumentMessageAsync(attachment.Id, model.EntityId, notification, tenantId); } }); return Ok(ApiResponse.SuccessResponse(response, "Document added successfully", 200)); } catch (Exception ex) { _logger.LogError(ex, "Unexpected error during document upload."); return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while uploading the document", 500)); } } /// /// Verifies a document attachment by its ID. Checks permissions, logs the operation, and updates verification fields. /// /// Document Attachment ID (Guid) /// Flag to verify or unverify the document (default: true) [HttpPost("verify/{id}")] public async Task VerifyDocumentAsync(Guid id, [FromQuery] bool isVerify = true) { // Begin: Create DbContext and DI scope await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScope.CreateScope(); try { // Get current logged-in employee for authentication/auditing var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var tenantId = loggedInEmployee.TenantId; _logger.LogInfo("Attempting to verify document. EmployeeId: {EmployeeId}, DocumentId: {DocumentId}, IsVerify: {IsVerify}", loggedInEmployee.Id, id, isVerify); // Fetch active/current document by Id, TenantId, and relevant conditions var documentAttachment = await _context.DocumentAttachments .Include(da => da.DocumentType) .ThenInclude(dt => dt!.DocumentCategory) .FirstOrDefaultAsync(da => da.Id == id && da.IsActive && da.IsCurrentVersion && da.TenantId == tenantId); if (documentAttachment == null) { _logger.LogWarning("Document attachment not found. Requested Id: {DocumentId}, TenantId: {TenantId}", id, tenantId); return NotFound(ApiResponse.ErrorResponse("Attachment not found", "Attachment not found in database", 404)); } // Permission service: check if employee is authorized to verify documents var _permission = scope.ServiceProvider.GetRequiredService(); var hasVerifyPermission = false; if (ProjectEntity == documentAttachment.DocumentType?.DocumentCategory?.EntityTypeId) { hasVerifyPermission = await _permission.HasPermission(PermissionsMaster.VerifyDocument, loggedInEmployee.Id, documentAttachment.EntityId); } else if (EmployeeEntity == documentAttachment.DocumentType?.DocumentCategory?.EntityTypeId) { hasVerifyPermission = await _permission.HasPermission(PermissionsMaster.VerifyDocument, loggedInEmployee.Id); } if (!hasVerifyPermission) { _logger.LogWarning("Access denied for document verification. EmployeeId: {EmployeeId}, DocumentId: {DocumentId}", loggedInEmployee.Id, id); return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to verify this document", 403)); } // Log existing entity state before update (for audit trail) var updateLogHelper = scope.ServiceProvider.GetRequiredService(); var existingEntityBson = updateLogHelper.EntityToBsonDocument(documentAttachment); // Update document verification status and audit fields documentAttachment.IsVerified = isVerify; documentAttachment.VerifiedAt = DateTime.UtcNow; documentAttachment.VerifiedById = loggedInEmployee.Id; // Commit changes await _context.SaveChangesAsync(); // Log the update to MongoDB for change tracking await updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = documentAttachment.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, Collection); var versionMapping = await _context.AttachmentVersionMappings.FirstOrDefaultAsync(av => av.ChildAttachmentId == documentAttachment.Id); var response = _mapper.Map(documentAttachment); if (versionMapping != null) { response.ParentAttachmentId = versionMapping.ParentAttachmentId; response.Version = versionMapping.Version; } _logger.LogInfo("Document verified successfully. DocumentId: {DocumentId}, VerifiedBy: {EmployeeId}", documentAttachment.Id, loggedInEmployee.Id); var _firebase = scope.ServiceProvider.GetRequiredService(); _ = Task.Run(async () => { // --- Push Notification Section --- // This section attempts to send a test push notification to the user's device. // It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens. var name = $"{loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; var entityType = documentAttachment.DocumentType?.DocumentCategory?.EntityTypeId; var documentType = documentAttachment.DocumentType; if (EmployeeEntity == entityType && documentAttachment.EntityId != loggedInEmployee.Id) { var notification = new Notification { Title = $"Your Document is Verified.", Body = $"Your Document of type \"{documentType?.Name}\" is Verified by {name}" }; await _firebase.SendEmployeeDocumentMessageAsync(documentAttachment.Id, documentAttachment.EntityId, notification, tenantId); } if (ProjectEntity == entityType) { var notification = new Notification { Title = $"Document for your project is verified", Body = $"Document for your Project of type \"{documentType?.Name}\" is Verified by {name}" }; await _firebase.SendProjectDocumentMessageAsync(documentAttachment.Id, documentAttachment.EntityId, notification, tenantId); } }); return Ok(ApiResponse.SuccessResponse(new { }, "Document is verified successfully", 200)); } catch (Exception ex) { // Handle unexpected errors gracefully _logger.LogError(ex, "Error occurred during document verification. DocumentId: {DocumentId}", id); return StatusCode(500, ApiResponse.ErrorResponse("Server Error", "An error occurred while verifying the document", 500)); } } [HttpPut("edit/{id}")] public async Task UpdateDocumentAsync(Guid id, [FromBody] UpdateDocumentAttachmentDto model) { // Logger initialization at the start for consistent logging using var scope = _serviceScope.CreateScope(); _logger.LogInfo("Start UpdateDocument API for AttachmentId: {AttachmentId}", id); try { await using var _context = await _dbContextFactory.CreateDbContextAsync(); // Get logged-in employee details var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Fetch the existing attachment var oldAttachment = await _context.DocumentAttachments .Include(da => da.DocumentType) .ThenInclude(dt => dt!.DocumentCategory) .FirstOrDefaultAsync(da => da.Id == id && da.IsActive && da.IsCurrentVersion && da.TenantId == tenantId); if (oldAttachment == null) { _logger.LogWarning("Attachment not found for Id: {AttachmentId}", id); return NotFound(ApiResponse.ErrorResponse("Attachment not found", "Attachment not found in database", 404)); } var _permission = scope.ServiceProvider.GetRequiredService(); var hasUpdatePermission = false; if (ProjectEntity == oldAttachment.DocumentType?.DocumentCategory?.EntityTypeId) { hasUpdatePermission = await _permission.HasPermission(PermissionsMaster.UploadDocument, loggedInEmployee.Id, oldAttachment.EntityId); } else if (EmployeeEntity == oldAttachment.DocumentType?.DocumentCategory?.EntityTypeId) { hasUpdatePermission = await _permission.HasPermission(PermissionsMaster.UploadDocument, loggedInEmployee.Id); } // Permission check: ensure uploader is authorized if (!hasUpdatePermission && loggedInEmployee.Id != oldAttachment.EntityId) { _logger.LogWarning("Access denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to upload this document", 403)); } // Validate the document type var documentType = oldAttachment.DocumentType; if (documentType == null) { _logger.LogWarning("Document type not found for AttachmentId: {AttachmentId}", id); return NotFound(ApiResponse.ErrorResponse("Document Type not found", "Document Type not found in database", 404)); } // Mandatory DocumentID check if (documentType.IsMandatory && string.IsNullOrWhiteSpace(model.DocumentId)) { _logger.LogWarning("Document ID missing for mandatory DocumentTypeId: {DocumentTypeId}", documentType.Id); return BadRequest(ApiResponse.ErrorResponse("Document ID missing", "User must provide the document ID for this document", 400)); } // DocumentID Regex validation if (documentType.IsValidationRequired && !string.IsNullOrWhiteSpace(model.DocumentId) && !string.IsNullOrWhiteSpace(documentType.RegexExpression)) { if (!Regex.IsMatch(model.DocumentId, documentType.RegexExpression)) { _logger.LogWarning("Provided document ID does not match regex for DocumentTypeId: {DocumentTypeId}", documentType.Id); return BadRequest(ApiResponse.ErrorResponse("Invalid Document ID", "Provided document ID is not valid", 400)); } } // Validate entity existence and project-level permission var entityType = documentType.DocumentCategory!.EntityTypeId; bool entityExists; if (entityType.Equals(EmployeeEntity)) { entityExists = await _context.Employees.AnyAsync(e => e.Id == oldAttachment.EntityId && e.TenantId == tenantId); } else if (entityType.Equals(ProjectEntity)) { entityExists = await _context.Projects.AnyAsync(p => p.Id == oldAttachment.EntityId && p.TenantId == tenantId); if (entityExists) { entityExists = await _permission.HasProjectPermission(loggedInEmployee, oldAttachment.EntityId); } } else { entityExists = false; } if (!entityExists) { _logger.LogWarning("Entity not found (Employee/Project) for AttachmentId: {AttachmentId}", id); return NotFound(ApiResponse.ErrorResponse($"{(entityType == EmployeeEntity ? "Employee" : "Project")} Not Found", "Entity not found in database", 404)); } // Prepare for versioning var oldVersionMapping = await _context.AttachmentVersionMappings .FirstOrDefaultAsync(av => av.ChildAttachmentId == oldAttachment.Id && av.TenantId == tenantId); var updateLogHelper = scope.ServiceProvider.GetRequiredService(); var existingEntityBson = updateLogHelper.EntityToBsonDocument(oldAttachment); DocumentAttachment newAttachment; AttachmentVersionMapping newVersionMapping; if (model.Attachment != null) { // File size check var allowedSize = documentType.MaxSizeAllowedInMB * 1024; if (model.Attachment.FileSize > allowedSize) { _logger.LogWarning("Attachment exceeded max file size for DocumentTypeId: {DocumentTypeId}", documentType.Id); return BadRequest(ApiResponse.ErrorResponse("File size limit exceeded", $"Max allowed {documentType.MaxSizeAllowedInMB} MB.", 400)); } // Base64 validation string base64 = model.Attachment.Base64Data?.Split(',').LastOrDefault() ?? ""; if (string.IsNullOrWhiteSpace(base64)) { _logger.LogWarning("Base64 data missing for new attachment"); return BadRequest(ApiResponse.ErrorResponse("Base64 data missing", "File data required", 400)); } // Content type verification var s3Service = scope.ServiceProvider.GetRequiredService(); var fileType = s3Service.GetContentTypeFromBase64(base64); var validContentTypes = documentType.AllowedContentType.Split(',').ToList(); if (!validContentTypes.Contains(fileType)) { _logger.LogWarning("Unsupported file type: {FileType}", fileType); return BadRequest(ApiResponse.ErrorResponse("Unsupported file type", $"Unsupported file type: {fileType}", 400)); } // S3 keys and folder structure string folderName = entityType == EmployeeEntity ? "EmployeeDocuments" : "ProjectDocuments"; string fileName = s3Service.GenerateFileName(fileType, tenantId, folderName); string objectKey = entityType == EmployeeEntity ? $"tenant-{tenantId}/Employee/{oldAttachment.EntityId}/{folderName}/{fileName}" : $"tenant-{tenantId}/project-{oldAttachment.EntityId}/{folderName}/{fileName}"; // Asynchronous S3 upload with logging _ = Task.Run(async () => { var logger = scope.ServiceProvider.GetRequiredService(); try { await s3Service.UploadFileAsync(base64, fileType, objectKey); logger.LogInfo("File uploaded successfully to S3: {ObjectKey}", objectKey); } catch (Exception ex) { logger.LogError(ex, "S3 upload failed for {ObjectKey}.", objectKey); } }); // Create Document record var document = new Document { BatchId = Guid.NewGuid(), UploadedById = loggedInEmployee.Id, FileName = model.Attachment.FileName ?? fileName, ContentType = model.Attachment.ContentType ?? fileType, S3Key = objectKey, FileSize = model.Attachment.FileSize, UploadedAt = DateTime.UtcNow, TenantId = tenantId }; _context.Documents.Add(document); if (oldAttachment.IsVerified == true) { // Record new document attachment as the current version var attachment = new DocumentAttachment { Name = model.Name, DocumentId = model.DocumentId, Description = model.Description, IsCurrentVersion = true, EntityId = oldAttachment.EntityId, DocumentDataId = document.Id, UploadedAt = DateTime.UtcNow, UploadedById = loggedInEmployee.Id, DocumentTypeId = oldAttachment.DocumentTypeId, TenantId = oldAttachment.TenantId }; _context.DocumentAttachments.Add(attachment); // Mark old version as not current oldAttachment.IsCurrentVersion = false; // Version mapping AttachmentVersionMapping versionMapping; if (oldVersionMapping != null) { versionMapping = new AttachmentVersionMapping { ParentAttachmentId = oldVersionMapping.ParentAttachmentId, ChildAttachmentId = attachment.Id, Version = (oldVersionMapping.Version + 1), TenantId = tenantId }; } else { versionMapping = new AttachmentVersionMapping { ParentAttachmentId = attachment.Id, ChildAttachmentId = attachment.Id, Version = 1, TenantId = tenantId }; } _context.AttachmentVersionMappings.Add(versionMapping); newAttachment = attachment; newVersionMapping = versionMapping; _logger.LogInfo("Created new current version for AttachmentId: {AttachmentId}", attachment.Id); } else { oldAttachment.Name = model.Name; oldAttachment.DocumentId = model.DocumentId; oldAttachment.Description = model.Description; oldAttachment.DocumentDataId = document.Id; if (oldAttachment.IsVerified != null) { oldAttachment.IsVerified = null; _logger.LogInfo("Reset verification flag for AttachmentId: {AttachmentId}", oldAttachment.Id); } oldAttachment.UpdatedAt = DateTime.UtcNow; oldAttachment.UpdatedById = loggedInEmployee.Id; newAttachment = oldAttachment; newVersionMapping = oldVersionMapping ?? new AttachmentVersionMapping(); _logger.LogInfo("Attachment metadata updated for AttachmentId: {AttachmentId}", oldAttachment.Id); } } else { // Update attachment metadata only (no file upload) oldAttachment.Name = model.Name; oldAttachment.DocumentId = model.DocumentId; oldAttachment.Description = model.Description; if (oldAttachment.IsVerified != null) { oldAttachment.IsVerified = null; _logger.LogInfo("Reset verification flag for AttachmentId: {AttachmentId}", oldAttachment.Id); } oldAttachment.UpdatedAt = DateTime.UtcNow; oldAttachment.UpdatedById = loggedInEmployee.Id; newAttachment = oldAttachment; newVersionMapping = oldVersionMapping ?? new AttachmentVersionMapping(); _logger.LogInfo("Attachment metadata updated for AttachmentId: {AttachmentId}", oldAttachment.Id); } // Tag management if (model.Tags?.Any() == true) { var names = model.Tags.Select(t => t.Name).ToList(); var existingTags = await _context.DocumentTagMasters .Where(t => names.Contains(t.Name) && t.TenantId == tenantId) .ToListAsync(); var attachmentTagMappings = new List(); var oldTags = await _context.AttachmentTagMappings .Include(dt => dt.DocumentTag) .Where(dt => dt.DocumentTag != null && dt.AttachmentId == newAttachment.Id && dt.TenantId == tenantId) .ToListAsync(); var oldTagNames = oldTags.Select(dt => dt.DocumentTag!.Name).ToList(); foreach (var tag in model.Tags.Where(t => t.IsActive && !oldTagNames.Contains(t.Name))) { var existingTag = existingTags.FirstOrDefault(t => t.Name == tag.Name); var tagEntity = existingTag ?? new DocumentTagMaster { Id = Guid.NewGuid(), Name = tag.Name, Description = tag.Name, TenantId = tenantId }; if (existingTag == null) { _context.DocumentTagMasters.Add(tagEntity); } attachmentTagMappings.Add(new AttachmentTagMapping { DocumentTagId = tagEntity.Id, AttachmentId = newAttachment.Id, TenantId = tenantId }); } _context.AttachmentTagMappings.AddRange(attachmentTagMappings); var deletedTagMappings = new List(); foreach (var tag in model.Tags.Where(t => !t.IsActive && oldTagNames.Contains(t.Name))) { var deletedTagMapping = oldTags.FirstOrDefault(at => at.DocumentTag!.Name == tag.Name); if (deletedTagMapping != null) { deletedTagMappings.Add(deletedTagMapping); } } _context.AttachmentTagMappings.RemoveRange(deletedTagMappings); _logger.LogInfo("Tags processed for AttachmentId: {AttachmentId}", newAttachment.Id); } // Persist changes to database await _context.SaveChangesAsync(); _logger.LogInfo("Database changes committed for AttachmentId: {AttachmentId}", newAttachment.Id); // Update logs await updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = oldAttachment.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, Collection); _logger.LogInfo("Update logs pushed for AttachmentId: {AttachmentId}", oldAttachment.Id); // Prepare response var response = _mapper.Map(newAttachment); response.UploadedBy = _mapper.Map(loggedInEmployee); response.ParentAttachmentId = newVersionMapping.ParentAttachmentId; response.Version = newVersionMapping.Version; _logger.LogInfo("API completed successfully for AttachmentId: {AttachmentId}", newAttachment.Id); var _firebase = scope.ServiceProvider.GetRequiredService(); _ = Task.Run(async () => { // --- Push Notification Section --- // This section attempts to send a test push notification to the user's device. // It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens. var name = $"{loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; if (EmployeeEntity == entityType && newAttachment.EntityId != loggedInEmployee.Id) { var notification = new Notification { Title = $"Your Document is updated.", Body = $"Your Document of type \"{documentType?.Name}\" is updated by {name}" }; await _firebase.SendEmployeeDocumentMessageAsync(newAttachment.Id, newAttachment.EntityId, notification, tenantId); } if (ProjectEntity == entityType) { var notification = new Notification { Title = "Your Project Document is updated.", Body = $"Your Project Document of type \"{documentType?.Name}\" is updated by {name}" }; await _firebase.SendProjectDocumentMessageAsync(newAttachment.Id, newAttachment.EntityId, notification, tenantId); } }); return Ok(ApiResponse.SuccessResponse(response, "Document Updated successfully", 200)); } catch (Exception ex) { _logger.LogError(ex, "Exception occurred while updating document for AttachmentId: {AttachmentId}", id); return StatusCode(500, ApiResponse.ErrorResponse("Exception occured", "Exception occured while the updating document", 500)); } } [HttpDelete("delete/{id}")] public async Task DeleteDocumentAsync(Guid id, [FromQuery] bool isActive = false) { // Create a new DbContext instance asynchronously await using var _context = await _dbContextFactory.CreateDbContextAsync(); // Create a new service scope to resolve scoped services like permission and logging helpers using var scope = _serviceScope.CreateScope(); var _permission = scope.ServiceProvider.GetRequiredService(); var updateLogHelper = scope.ServiceProvider.GetRequiredService(); // Message to indicate whether the document is being activated or deactivated var message = isActive ? "activated" : "deactivated"; // Get the currently logged-in employee var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Log the start of the delete operation for traceability _logger.LogInfo("DeleteDocument started for document ID: {DocumentId} by employee ID: {EmployeeId}", id, loggedInEmployee.Id); // Retrieve the document attachment matching the criteria from the database var documentAttachment = await _context.DocumentAttachments .Include(da => da.DocumentType) .ThenInclude(dt => dt!.DocumentCategory) .FirstOrDefaultAsync(da => da.Id == id && da.IsCurrentVersion && da.TenantId == tenantId && da.IsActive != isActive); // If the document attachment is not found, log a warning and return 404 Not Found if (documentAttachment == null) { _logger.LogWarning("DocumentAttachment not found for ID: {DocumentId}", id); return NotFound(ApiResponse.ErrorResponse("Document Attachment not found", "Document Attachment not found in database", 404)); } // Check if the logged in employee has permission to delete OR is the owner of the document attachment var hasDeletePermission = false; if (ProjectEntity == documentAttachment.DocumentType?.DocumentCategory?.EntityTypeId) { hasDeletePermission = await _permission.HasPermission(PermissionsMaster.DeleteDocument, loggedInEmployee.Id, documentAttachment.EntityId); } else if (EmployeeEntity == documentAttachment.DocumentType?.DocumentCategory?.EntityTypeId) { hasDeletePermission = await _permission.HasPermission(PermissionsMaster.DeleteDocument, loggedInEmployee.Id); } if (!hasDeletePermission && loggedInEmployee.Id != documentAttachment.EntityId) { _logger.LogWarning("Access denied for employee ID: {EmployeeId} when attempting to delete document ID: {DocumentId}", loggedInEmployee.Id, id); return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to delete documents", 403)); } // Log the current state of the document attachment before updating for audit trail var existingEntityBson = updateLogHelper.EntityToBsonDocument(documentAttachment); // Update document attachment status and metadata documentAttachment.IsActive = isActive; documentAttachment.IsVerified = null; documentAttachment.UpdatedAt = DateTime.UtcNow; documentAttachment.UpdatedById = loggedInEmployee.Id; // Persist changes to the database await _context.SaveChangesAsync(); // Log the update operation to MongoDB for inspection and history await updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = documentAttachment.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, Collection); // Log the successful completion of the operation _logger.LogInfo("DocumentAttachment ID: {DocumentId} has been {Message} by employee ID: {EmployeeId}", id, message, loggedInEmployee.Id); var _firebase = scope.ServiceProvider.GetRequiredService(); _ = Task.Run(async () => { // --- Push Notification Section --- // This section attempts to send a test push notification to the user's device. // It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens. var entityType = documentAttachment.DocumentType?.DocumentCategory?.EntityTypeId; var documentType = documentAttachment.DocumentType; var name = $"{loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; if (EmployeeEntity == entityType && documentAttachment.EntityId != loggedInEmployee.Id) { var notification = new Notification { Title = $"Your Document is {message}.", Body = $"Your Document of type \"{documentType?.Name}\" is {message} by {name}" }; await _firebase.SendEmployeeDocumentMessageAsync(documentAttachment.Id, documentAttachment.EntityId, notification, tenantId); } if (ProjectEntity == entityType) { var notification = new Notification { Title = "Your Project Document is {message}.", Body = $"Your Project Document of type \"{documentType?.Name}\" is {message} by {name}" }; await _firebase.SendProjectDocumentMessageAsync(documentAttachment.Id, documentAttachment.EntityId, notification, tenantId); } }); // Return success response return Ok(ApiResponse.SuccessResponse(new { }, $"Document attachment is {message}", 200)); } #region =================================================================== Helper Functions =================================================================== private DocumentFilter? TryDeserializeFilter(string? filter) { if (string.IsNullOrWhiteSpace(filter)) { return null; } var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; DocumentFilter? documentFilter = null; try { // First, try to deserialize directly. This is the expected case (e.g., from a web client). documentFilter = JsonSerializer.Deserialize(filter, options); } catch (JsonException ex) { _logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeFilter), filter); // If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients). try { // Unescape the string first, then deserialize the result. string unescapedJsonString = JsonSerializer.Deserialize(filter, options) ?? ""; if (!string.IsNullOrWhiteSpace(unescapedJsonString)) { documentFilter = JsonSerializer.Deserialize(unescapedJsonString, options); } } catch (JsonException ex1) { // If both attempts fail, log the final error and return null. _logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeFilter), filter); return null; } } return documentFilter; } #endregion } }