558 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			558 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
using Marco.Pms.DataAccess.Data;
 | 
						|
using Marco.Pms.Model.Dtos.Mail;
 | 
						|
using Marco.Pms.Model.Mail;
 | 
						|
using Marco.Pms.Model.MongoDBModels;
 | 
						|
using Marco.Pms.Model.Utilities;
 | 
						|
using Marco.Pms.Services.Helpers;
 | 
						|
using MarcoBMS.Services.Helpers;
 | 
						|
using MarcoBMS.Services.Service;
 | 
						|
using Microsoft.AspNetCore.Authorization;
 | 
						|
using Microsoft.AspNetCore.Mvc;
 | 
						|
using Microsoft.CodeAnalysis;
 | 
						|
using Microsoft.EntityFrameworkCore;
 | 
						|
using MongoDB.Driver;
 | 
						|
using System.Data;
 | 
						|
using System.Globalization;
 | 
						|
using System.Net.Mail;
 | 
						|
 | 
						|
namespace Marco.Pms.Services.Controllers
 | 
						|
{
 | 
						|
    [Route("api/[controller]")]
 | 
						|
    [ApiController]
 | 
						|
    [Authorize]
 | 
						|
    public class ReportController : ControllerBase
 | 
						|
    {
 | 
						|
        private readonly ApplicationDbContext _context;
 | 
						|
        private readonly IEmailSender _emailSender;
 | 
						|
        private readonly ILoggingService _logger;
 | 
						|
        private readonly UserHelper _userHelper;
 | 
						|
        private readonly IWebHostEnvironment _env;
 | 
						|
        private readonly ReportHelper _reportHelper;
 | 
						|
        private readonly IConfiguration _configuration;
 | 
						|
        private readonly CacheUpdateHelper _cache;
 | 
						|
        private readonly IServiceScopeFactory _serviceScopeFactory;
 | 
						|
        public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper,
 | 
						|
            IWebHostEnvironment env, ReportHelper reportHelper, IConfiguration configuration, CacheUpdateHelper cache, IServiceScopeFactory serviceScopeFactory)
 | 
						|
        {
 | 
						|
            _context = context;
 | 
						|
            _emailSender = emailSender;
 | 
						|
            _logger = logger;
 | 
						|
            _userHelper = userHelper;
 | 
						|
            _env = env;
 | 
						|
            _reportHelper = reportHelper;
 | 
						|
            _configuration = configuration;
 | 
						|
            _cache = cache;
 | 
						|
            _serviceScopeFactory = serviceScopeFactory;
 | 
						|
        }
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// Adds new mail details for a project report.
 | 
						|
        /// </summary>
 | 
						|
        /// <param name="mailDetailsDto">The mail details data.</param>
 | 
						|
        /// <returns>An API response indicating success or failure.</returns>
 | 
						|
        [HttpPost("mail-details")] // More specific route for adding mail details
 | 
						|
        public async Task<IActionResult> AddMailDetails([FromBody] MailDetailsDto mailDetailsDto)
 | 
						|
        {
 | 
						|
            // 1. Get Tenant ID and Basic Authorization Check
 | 
						|
            Guid tenantId = _userHelper.GetTenantId();
 | 
						|
            if (tenantId == Guid.Empty)
 | 
						|
            {
 | 
						|
                _logger.LogWarning("Authorization Error: Attempt to add mail details with an empty or invalid tenant ID.");
 | 
						|
                return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "Tenant ID not found or invalid.", 401));
 | 
						|
            }
 | 
						|
 | 
						|
            // 2. Input Validation (Leverage Model Validation attributes on DTO)
 | 
						|
            if (mailDetailsDto == null)
 | 
						|
            {
 | 
						|
                _logger.LogWarning("Validation Error: MailDetails DTO is null. TenantId: {TenantId}", tenantId);
 | 
						|
                return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Data", "Request body is empty.", 400));
 | 
						|
            }
 | 
						|
 | 
						|
            // Ensure ProjectId and Recipient are not empty
 | 
						|
            if (mailDetailsDto.ProjectId == Guid.Empty)
 | 
						|
            {
 | 
						|
                _logger.LogWarning("Validation Error: Project ID is empty. TenantId: {TenantId}", tenantId);
 | 
						|
                return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Project ID cannot be empty.", 400));
 | 
						|
            }
 | 
						|
 | 
						|
            if (string.IsNullOrWhiteSpace(mailDetailsDto.Recipient))
 | 
						|
            {
 | 
						|
                _logger.LogWarning("Validation Error: Recipient email is empty. ProjectId: {ProjectId}, TenantId: {TenantId}", mailDetailsDto.ProjectId, tenantId);
 | 
						|
                return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Recipient email cannot be empty.", 400));
 | 
						|
            }
 | 
						|
 | 
						|
            // Optional: Validate email format using regex or System.Net.Mail.MailAddress
 | 
						|
            try
 | 
						|
            {
 | 
						|
                var mailAddress = new MailAddress(mailDetailsDto.Recipient);
 | 
						|
                _logger.LogInfo("nothing");
 | 
						|
            }
 | 
						|
            catch (FormatException)
 | 
						|
            {
 | 
						|
                _logger.LogWarning("Validation Error: Invalid recipient email format '{Recipient}'. ProjectId: {ProjectId}, TenantId: {TenantId}", mailDetailsDto.Recipient, mailDetailsDto.ProjectId, tenantId);
 | 
						|
                return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Invalid recipient email format.", 400));
 | 
						|
            }
 | 
						|
 | 
						|
            // 3. Validate MailListId (Foreign Key Check)
 | 
						|
            // Ensure the MailListId refers to an existing MailBody (template) within the same tenant.
 | 
						|
            if (mailDetailsDto.MailListId != Guid.Empty) // Only validate if a MailListId is provided
 | 
						|
            {
 | 
						|
                bool mailTemplateExists;
 | 
						|
                try
 | 
						|
                {
 | 
						|
                    mailTemplateExists = await _context.MailingList
 | 
						|
                        .AsNoTracking()
 | 
						|
                        .AnyAsync(m => m.Id == mailDetailsDto.MailListId && m.TenantId == tenantId);
 | 
						|
                }
 | 
						|
                catch (Exception ex)
 | 
						|
                {
 | 
						|
                    _logger.LogError("Database Error: Failed to check existence of MailListId '{MailListId}' for TenantId: {TenantId}. : {Error}", mailDetailsDto.MailListId, tenantId, ex.Message);
 | 
						|
                    return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while validating mail template.", 500));
 | 
						|
                }
 | 
						|
 | 
						|
                if (!mailTemplateExists)
 | 
						|
                {
 | 
						|
                    _logger.LogWarning("Validation Error: Provided MailListId '{MailListId}' does not exist or does not belong to TenantId: {TenantId}.", mailDetailsDto.MailListId, tenantId);
 | 
						|
                    return NotFound(ApiResponse<object>.ErrorResponse("Invalid Mail Template", "The specified mail template (MailListId) was not found or accessible.", 404));
 | 
						|
                }
 | 
						|
            }
 | 
						|
            // If MailListId can be null/empty and implies no specific template, adjust logic accordingly.
 | 
						|
            // Currently assumes it must exist if provided.
 | 
						|
 | 
						|
            // 4. Create and Add New Mail Details
 | 
						|
            var newMailDetails = new MailDetails
 | 
						|
            {
 | 
						|
                ProjectId = mailDetailsDto.ProjectId,
 | 
						|
                Recipient = mailDetailsDto.Recipient,
 | 
						|
                Schedule = mailDetailsDto.Schedule,
 | 
						|
                MailListId = mailDetailsDto.MailListId,
 | 
						|
                TenantId = tenantId,
 | 
						|
            };
 | 
						|
 | 
						|
            try
 | 
						|
            {
 | 
						|
                _context.MailDetails.Add(newMailDetails);
 | 
						|
                await _context.SaveChangesAsync();
 | 
						|
                _logger.LogInfo("Successfully added new mail details with ID {MailDetailsId} for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}.", newMailDetails.Id, newMailDetails.ProjectId, newMailDetails.Recipient, tenantId);
 | 
						|
 | 
						|
                // 5. Return Success Response (201 Created is ideal for resource creation)
 | 
						|
                return StatusCode(201, ApiResponse<MailDetails>.SuccessResponse(
 | 
						|
                    newMailDetails, // Return the newly created object (or a DTO of it)
 | 
						|
                    "Mail details added successfully.",
 | 
						|
                    201));
 | 
						|
            }
 | 
						|
            catch (DbUpdateException dbEx)
 | 
						|
            {
 | 
						|
                _logger.LogError("Database Error: Failed to save new mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}. : {Error}", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId, dbEx.Message);
 | 
						|
                // Check for specific constraint violations if applicable (e.g., duplicate recipient for a project)
 | 
						|
                return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while saving the mail details.", 500));
 | 
						|
            }
 | 
						|
            catch (Exception ex)
 | 
						|
            {
 | 
						|
                _logger.LogError("Unexpected Error: An unhandled exception occurred while adding mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}. : {Error}", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId, ex.Message);
 | 
						|
                return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500));
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        [HttpPost("mail-template1")]
 | 
						|
        public async Task<IActionResult> AddMailTemplate1([FromBody] MailTemeplateDto mailTemeplateDto)
 | 
						|
        {
 | 
						|
            Guid tenantId = _userHelper.GetTenantId();
 | 
						|
            if (string.IsNullOrWhiteSpace(mailTemeplateDto.Body) && string.IsNullOrWhiteSpace(mailTemeplateDto.Title))
 | 
						|
            {
 | 
						|
                _logger.LogWarning("User tries to set email template but send invalid data");
 | 
						|
                return BadRequest(ApiResponse<object>.ErrorResponse("Provided Invalid data", "Provided Invalid data", 400));
 | 
						|
            }
 | 
						|
            var existngTemalate = await _context.MailingList.FirstOrDefaultAsync(t => t.Title.ToLower() == mailTemeplateDto.Title.ToLower());
 | 
						|
            if (existngTemalate != null)
 | 
						|
            {
 | 
						|
                _logger.LogWarning("User tries to set email template, but title already existed in database");
 | 
						|
                return BadRequest(ApiResponse<object>.ErrorResponse("Email title is already existed", "Email title is already existed", 400));
 | 
						|
            }
 | 
						|
            MailingList mailingList = new MailingList
 | 
						|
            {
 | 
						|
                Title = mailTemeplateDto.Title,
 | 
						|
                Body = mailTemeplateDto.Body,
 | 
						|
                Subject = mailTemeplateDto.Subject,
 | 
						|
                Keywords = mailTemeplateDto.Keywords,
 | 
						|
                TenantId = tenantId
 | 
						|
            };
 | 
						|
            _context.MailingList.Add(mailingList);
 | 
						|
            await _context.SaveChangesAsync();
 | 
						|
            return Ok("Success");
 | 
						|
        }
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// Adds a new mail template.
 | 
						|
        /// </summary>
 | 
						|
        /// <param name="mailTemplateDto">The mail template data.</param>
 | 
						|
        /// <returns>An API response indicating success or failure.</returns>
 | 
						|
        [HttpPost("mail-template")] // More specific route for adding a template
 | 
						|
        public async Task<IActionResult> AddMailTemplate([FromBody] MailTemeplateDto mailTemplateDto) // Renamed parameter for consistency
 | 
						|
        {
 | 
						|
            // 1. Get Tenant ID and Basic Authorization Check
 | 
						|
            Guid tenantId = _userHelper.GetTenantId();
 | 
						|
            if (tenantId == Guid.Empty)
 | 
						|
            {
 | 
						|
                _logger.LogWarning("Authorization Error: Attempt to add mail template with an empty or invalid tenant ID.");
 | 
						|
                return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "Tenant ID not found or invalid.", 401));
 | 
						|
            }
 | 
						|
 | 
						|
            // 2. Input Validation (Moved to model validation if possible, or keep explicit)
 | 
						|
            // Use proper model validation attributes ([Required], [StringLength]) on MailTemeplateDto
 | 
						|
            // and rely on ASP.NET Core's automatic model validation if possible.
 | 
						|
            // If not, these checks are good.
 | 
						|
            if (mailTemplateDto == null)
 | 
						|
            {
 | 
						|
                _logger.LogWarning("Validation Error: Mail template DTO is null.");
 | 
						|
                return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Data", "Request body is empty.", 400));
 | 
						|
            }
 | 
						|
 | 
						|
            if (string.IsNullOrWhiteSpace(mailTemplateDto.Title))
 | 
						|
            {
 | 
						|
                _logger.LogWarning("Validation Error: Mail template title is empty or whitespace. TenantId: {TenantId}", tenantId);
 | 
						|
                return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Mail template title cannot be empty.", 400));
 | 
						|
            }
 | 
						|
 | 
						|
            // The original logic checked both body and title, but often a template needs at least a title.
 | 
						|
            // Re-evalute if body can be empty. If so, remove the body check. Assuming title is always mandatory.
 | 
						|
            // If both body and title are empty, it's definitely invalid.
 | 
						|
            if (string.IsNullOrWhiteSpace(mailTemplateDto.Body) && string.IsNullOrWhiteSpace(mailTemplateDto.Subject))
 | 
						|
            {
 | 
						|
                _logger.LogWarning("Validation Error: Mail template body and subject are both empty or whitespace for title '{Title}'. TenantId: {TenantId}", mailTemplateDto.Title, tenantId);
 | 
						|
                return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Mail template body or subject must be provided.", 400));
 | 
						|
            }
 | 
						|
 | 
						|
            // 3. Check for Existing Template Title (Case-Insensitive)
 | 
						|
            // Use AsNoTracking() for read-only query
 | 
						|
            MailingList? existingTemplate;
 | 
						|
            try
 | 
						|
            {
 | 
						|
                existingTemplate = await _context.MailingList
 | 
						|
                    .AsNoTracking() // Important for read-only checks
 | 
						|
                    .FirstOrDefaultAsync(t => t.Title.ToLower() == mailTemplateDto.Title.ToLower() && t.TenantId == tenantId); // IMPORTANT: Filter by TenantId!
 | 
						|
            }
 | 
						|
            catch (Exception ex)
 | 
						|
            {
 | 
						|
                _logger.LogError("Database Error: Failed to check for existing mail template with title '{Title}' for TenantId: {TenantId}.: {Error}", mailTemplateDto.Title, tenantId, ex.Message);
 | 
						|
                return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while checking for existing templates.", 500));
 | 
						|
            }
 | 
						|
 | 
						|
 | 
						|
            if (existingTemplate != null)
 | 
						|
            {
 | 
						|
                _logger.LogWarning("Conflict Error: User tries to add email template with title '{Title}' which already exists for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId);
 | 
						|
                return Conflict(ApiResponse<object>.ErrorResponse("Conflict", $"Email template with title '{mailTemplateDto.Title}' already exists.", 409));
 | 
						|
            }
 | 
						|
 | 
						|
            // 4. Create and Add New Template
 | 
						|
            var newMailingList = new MailingList
 | 
						|
            {
 | 
						|
                Title = mailTemplateDto.Title,
 | 
						|
                Body = mailTemplateDto.Body,
 | 
						|
                Subject = mailTemplateDto.Subject,
 | 
						|
                Keywords = mailTemplateDto.Keywords,
 | 
						|
                TenantId = tenantId,
 | 
						|
            };
 | 
						|
 | 
						|
            try
 | 
						|
            {
 | 
						|
                _context.MailingList.Add(newMailingList);
 | 
						|
                await _context.SaveChangesAsync();
 | 
						|
                _logger.LogInfo("Successfully added new mail template with ID {TemplateId} and title '{Title}' for TenantId: {TenantId}.", newMailingList.Id, newMailingList.Title, tenantId);
 | 
						|
 | 
						|
                // 5. Return Success Response (201 Created is ideal for resource creation)
 | 
						|
                // It's good practice to return the created resource or its ID.
 | 
						|
                return StatusCode(201, ApiResponse<MailingList>.SuccessResponse(
 | 
						|
                    newMailingList, // Return the newly created object (or a DTO of it)
 | 
						|
                    "Mail template added successfully.",
 | 
						|
                    201));
 | 
						|
            }
 | 
						|
            catch (DbUpdateException dbEx)
 | 
						|
            {
 | 
						|
                _logger.LogError("Database Error: Failed to save new mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId, dbEx.Message);
 | 
						|
                return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while saving the mail template.", 500));
 | 
						|
            }
 | 
						|
            catch (Exception ex)
 | 
						|
            {
 | 
						|
                _logger.LogError("Unexpected Error: An unhandled exception occurred while adding mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId, ex.Message);
 | 
						|
                return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500));
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        [HttpGet("project-statistics")]
 | 
						|
        public async Task<IActionResult> SendProjectReport()
 | 
						|
        {
 | 
						|
            Guid tenantId = _userHelper.GetTenantId();
 | 
						|
 | 
						|
            // 1. OPTIMIZATION: Perform grouping and projection on the database server.
 | 
						|
            // This is far more efficient than loading all entities into memory.
 | 
						|
            var projectMailGroups = await _context.MailDetails
 | 
						|
                .AsNoTracking()
 | 
						|
                .Where(m => m.TenantId == tenantId)
 | 
						|
                .GroupBy(m => new { m.ProjectId, m.MailListId })
 | 
						|
                .Select(g => new
 | 
						|
                {
 | 
						|
                    ProjectId = g.Key.ProjectId,
 | 
						|
                    Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
 | 
						|
                    // Project the mail body and subject from the first record in the group
 | 
						|
                    MailInfo = g.Select(m => new { Body = m.MailBody != null ? m.MailBody.Body : "", Subject = m.MailBody != null ? m.MailBody.Subject : "" }).FirstOrDefault()
 | 
						|
                })
 | 
						|
                .ToListAsync();
 | 
						|
 | 
						|
            if (!projectMailGroups.Any())
 | 
						|
            {
 | 
						|
                return Ok(ApiResponse<object>.SuccessResponse(new { }, "No projects found to send reports for.", 200));
 | 
						|
            }
 | 
						|
 | 
						|
            int successCount = 0;
 | 
						|
            int notFoundCount = 0;
 | 
						|
            int invalidIdCount = 0;
 | 
						|
            int failureCount = 0;
 | 
						|
 | 
						|
            // 2. OPTIMIZATION: Use true concurrency by removing SemaphoreSlim(1)
 | 
						|
            // and giving each task its own isolated set of services (including DbContext).
 | 
						|
            var sendTasks = projectMailGroups.Select(async mailGroup =>
 | 
						|
            {
 | 
						|
                // SOLUTION: Create a new Dependency Injection scope for each parallel task.
 | 
						|
                using (var scope = _serviceScopeFactory.CreateScope())
 | 
						|
                {
 | 
						|
                    // Resolve a new instance of the helper from this isolated scope.
 | 
						|
                    // This ensures each task gets its own thread-safe DbContext.
 | 
						|
                    var reportHelper = scope.ServiceProvider.GetRequiredService<ReportHelper>();
 | 
						|
 | 
						|
                    try
 | 
						|
                    {
 | 
						|
                        // Ensure MailInfo and ProjectId are valid before proceeding
 | 
						|
                        if (mailGroup.MailInfo == null || mailGroup.ProjectId == Guid.Empty)
 | 
						|
                        {
 | 
						|
                            Interlocked.Increment(ref invalidIdCount);
 | 
						|
                            return;
 | 
						|
                        }
 | 
						|
 | 
						|
                        var response = await reportHelper.GetProjectStatistics(
 | 
						|
                            mailGroup.ProjectId,
 | 
						|
                            mailGroup.Recipients,
 | 
						|
                            mailGroup.MailInfo.Body,
 | 
						|
                            mailGroup.MailInfo.Subject,
 | 
						|
                            tenantId);
 | 
						|
 | 
						|
                        // Use a switch expression for cleaner counting
 | 
						|
                        switch (response.StatusCode)
 | 
						|
                        {
 | 
						|
                            case 200: Interlocked.Increment(ref successCount); break;
 | 
						|
                            case 404: Interlocked.Increment(ref notFoundCount); break;
 | 
						|
                            case 400: Interlocked.Increment(ref invalidIdCount); break;
 | 
						|
                            default: Interlocked.Increment(ref failureCount); break;
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                    catch (Exception ex)
 | 
						|
                    {
 | 
						|
                        // 3. OPTIMIZATION: Make the process resilient.
 | 
						|
                        // If one task fails unexpectedly, log it and continue with others.
 | 
						|
                        _logger.LogError("Failed to send report for project {ProjectId} : {Error}", mailGroup.ProjectId, ex.Message);
 | 
						|
                        Interlocked.Increment(ref failureCount);
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }).ToList();
 | 
						|
 | 
						|
            await Task.WhenAll(sendTasks);
 | 
						|
 | 
						|
            var summaryMessage = $"Processing complete. Success: {successCount}, Not Found: {notFoundCount}, Invalid ID: {invalidIdCount}, Failures: {failureCount}.";
 | 
						|
 | 
						|
            _logger.LogInfo(
 | 
						|
                "Project report sending complete for tenant {TenantId}. Success: {SuccessCount}, Not Found: {NotFoundCount}, Invalid ID: {InvalidIdCount}, Failures: {FailureCount}",
 | 
						|
                tenantId, successCount, notFoundCount, invalidIdCount, failureCount);
 | 
						|
 | 
						|
            return Ok(ApiResponse<object>.SuccessResponse(
 | 
						|
                new { successCount, notFoundCount, invalidIdCount, failureCount },
 | 
						|
                summaryMessage,
 | 
						|
                200));
 | 
						|
        }
 | 
						|
 | 
						|
        //[HttpPost("add-report-mail1")]
 | 
						|
        //public async Task<IActionResult> StoreProjectStatistics1()
 | 
						|
        //{
 | 
						|
 | 
						|
        //    Guid tenantId = _userHelper.GetTenantId();
 | 
						|
 | 
						|
        //    // Use AsNoTracking() for read-only queries to improve performance
 | 
						|
        //    List<MailDetails> mailDetails = await _context.MailDetails
 | 
						|
        //        .AsNoTracking()
 | 
						|
        //        .Include(m => m.MailBody)
 | 
						|
        //        .Where(m => m.TenantId == tenantId)
 | 
						|
        //        .ToListAsync();
 | 
						|
 | 
						|
        //    var groupedMails = mailDetails
 | 
						|
        //        .GroupBy(m => new { m.ProjectId, m.MailListId })
 | 
						|
        //        .Select(g => new
 | 
						|
        //        {
 | 
						|
        //            ProjectId = g.Key.ProjectId,
 | 
						|
        //            MailListId = g.Key.MailListId,
 | 
						|
        //            Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
 | 
						|
        //            MailBody = g.FirstOrDefault()?.MailBody?.Body ?? "",
 | 
						|
        //            Subject = g.FirstOrDefault()?.MailBody?.Subject ?? string.Empty,
 | 
						|
        //        })
 | 
						|
        //        .ToList();
 | 
						|
        //    foreach (var groupMail in groupedMails)
 | 
						|
        //    {
 | 
						|
        //        var projectId = groupMail.ProjectId;
 | 
						|
        //        var body = groupMail.MailBody;
 | 
						|
        //        var subject = groupMail.Subject;
 | 
						|
        //        var receivers = groupMail.Recipients;
 | 
						|
        //        if (projectId == Guid.Empty)
 | 
						|
        //        {
 | 
						|
        //            _logger.LogError("Provided empty project ID while fetching project report.");
 | 
						|
        //            return NotFound(ApiResponse<object>.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400));
 | 
						|
        //        }
 | 
						|
 | 
						|
 | 
						|
        //        var statisticReport = await _reportHelper.GetDailyProjectReport(projectId, tenantId);
 | 
						|
 | 
						|
        //        if (statisticReport == null)
 | 
						|
        //        {
 | 
						|
        //            _logger.LogWarning("User attempted to fetch project progress for project ID {ProjectId} but not found.", projectId);
 | 
						|
        //            return NotFound(ApiResponse<object>.ErrorResponse("Project not found.", "Project not found.", 404));
 | 
						|
        //        }
 | 
						|
        //        var date = statisticReport.Date.ToString("dd-MMM-yyyy", CultureInfo.InvariantCulture);
 | 
						|
 | 
						|
        //        // Send Email
 | 
						|
        //        var emailBody = await _emailSender.SendProjectStatisticsEmail(new List<string>(), body, subject, statisticReport);
 | 
						|
        //        var subjectReplacements = new Dictionary<string, string>
 | 
						|
        //        {
 | 
						|
        //            {"DATE", date },
 | 
						|
        //            {"PROJECT_NAME", statisticReport.ProjectName}
 | 
						|
        //        };
 | 
						|
        //        foreach (var item in subjectReplacements)
 | 
						|
        //        {
 | 
						|
        //            subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value);
 | 
						|
        //        }
 | 
						|
        //        string env = _configuration["environment:Title"] ?? string.Empty;
 | 
						|
        //        if (string.IsNullOrWhiteSpace(env))
 | 
						|
        //        {
 | 
						|
        //            subject = $"{subject}";
 | 
						|
        //        }
 | 
						|
        //        else
 | 
						|
        //        {
 | 
						|
        //            subject = $"({env}) {subject}";
 | 
						|
        //        }
 | 
						|
        //        var mail = new ProjectReportEmailMongoDB
 | 
						|
        //        {
 | 
						|
        //            IsSent = false,
 | 
						|
        //            Body = emailBody,
 | 
						|
        //            Receivers = receivers,
 | 
						|
        //            Subject = subject,
 | 
						|
        //        };
 | 
						|
        //        await _cache.AddProjectReportMail(mail);
 | 
						|
        //    }
 | 
						|
        //    return Ok(ApiResponse<object>.SuccessResponse("Project Report Mail is stored in MongoDB", "Project Report Mail is stored in MongoDB", 200));
 | 
						|
        //}
 | 
						|
 | 
						|
        [HttpPost("add-report-mail")]
 | 
						|
        public async Task<IActionResult> StoreProjectStatistics()
 | 
						|
        {
 | 
						|
            Guid tenantId = _userHelper.GetTenantId();
 | 
						|
 | 
						|
            // 1. Database-Side Grouping (Still the most efficient way to get initial data)
 | 
						|
            var projectMailGroups = await _context.MailDetails
 | 
						|
                .AsNoTracking()
 | 
						|
                .Where(m => m.TenantId == tenantId && m.ProjectId != Guid.Empty)
 | 
						|
                .GroupBy(m => new { m.ProjectId, m.MailListId })
 | 
						|
                .Select(g => new
 | 
						|
                {
 | 
						|
                    g.Key.ProjectId,
 | 
						|
                    Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
 | 
						|
                    MailInfo = g.Select(m => new { Body = m.MailBody != null ? m.MailBody.Body : "", Subject = m.MailBody != null ? m.MailBody.Subject : "" }).FirstOrDefault()
 | 
						|
                })
 | 
						|
                .ToListAsync();
 | 
						|
 | 
						|
            if (!projectMailGroups.Any())
 | 
						|
            {
 | 
						|
                _logger.LogInfo("No project mail details found for tenant {TenantId} to process.", tenantId);
 | 
						|
                return Ok(ApiResponse<object>.SuccessResponse("No project reports to generate.", "No project reports to generate.", 200));
 | 
						|
            }
 | 
						|
 | 
						|
            string env = _configuration["environment:Title"] ?? string.Empty;
 | 
						|
 | 
						|
            // 2. Process each group concurrently, but with isolated DBContexts.
 | 
						|
            var processingTasks = projectMailGroups.Select(async group =>
 | 
						|
            {
 | 
						|
                // SOLUTION: Create a new DI scope for each parallel task.
 | 
						|
                using (var scope = _serviceScopeFactory.CreateScope())
 | 
						|
                {
 | 
						|
                    // Resolve services from this new, isolated scope.
 | 
						|
                    // These helpers will get their own fresh DbContext instance.
 | 
						|
                    var reportHelper = scope.ServiceProvider.GetRequiredService<ReportHelper>();
 | 
						|
                    var emailSender = scope.ServiceProvider.GetRequiredService<IEmailSender>();
 | 
						|
                    var cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>(); // e.g., IProjectReportCache
 | 
						|
 | 
						|
                    // The rest of the logic is the same, but now it's thread-safe.
 | 
						|
                    try
 | 
						|
                    {
 | 
						|
                        var projectId = group.ProjectId;
 | 
						|
                        var statisticReport = await reportHelper.GetDailyProjectReport(projectId, tenantId);
 | 
						|
 | 
						|
                        if (statisticReport == null)
 | 
						|
                        {
 | 
						|
                            _logger.LogWarning("Statistic report for project ID {ProjectId} not found. Skipping.", projectId);
 | 
						|
                            return;
 | 
						|
                        }
 | 
						|
 | 
						|
                        if (group.MailInfo == null)
 | 
						|
                        {
 | 
						|
                            _logger.LogWarning("MailBody info for project ID {ProjectId} not found. Skipping.", projectId);
 | 
						|
                            return;
 | 
						|
                        }
 | 
						|
 | 
						|
                        var date = statisticReport.Date.ToString("dd-MMM-yyyy", CultureInfo.InvariantCulture);
 | 
						|
                        // Assuming the first param to SendProjectStatisticsEmail was just a placeholder
 | 
						|
                        var emailBody = await emailSender.SendProjectStatisticsEmail(new List<string>(), group.MailInfo.Body, string.Empty, statisticReport);
 | 
						|
 | 
						|
                        string subject = group.MailInfo.Subject
 | 
						|
                            .Replace("{{DATE}}", date)
 | 
						|
                            .Replace("{{PROJECT_NAME}}", statisticReport.ProjectName);
 | 
						|
 | 
						|
                        subject = string.IsNullOrWhiteSpace(env) ? subject : $"({env}) {subject}";
 | 
						|
 | 
						|
                        var mail = new ProjectReportEmailMongoDB
 | 
						|
                        {
 | 
						|
                            IsSent = false,
 | 
						|
                            Body = emailBody,
 | 
						|
                            Receivers = group.Recipients,
 | 
						|
                            Subject = subject,
 | 
						|
                        };
 | 
						|
 | 
						|
                        await cache.AddProjectReportMail(mail);
 | 
						|
                    }
 | 
						|
                    catch (Exception ex)
 | 
						|
                    {
 | 
						|
                        // It's good practice to log any unexpected errors within a concurrent task.
 | 
						|
                        _logger.LogError("Failed to process project report for ProjectId {ProjectId} : {Error}", group.ProjectId, ex.Message);
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            });
 | 
						|
 | 
						|
            // Await all the concurrent, now thread-safe, tasks.
 | 
						|
            await Task.WhenAll(processingTasks);
 | 
						|
 | 
						|
            return Ok(ApiResponse<object>.SuccessResponse(
 | 
						|
                $"{projectMailGroups.Count} Project Report Mail(s) are queued for storage.",
 | 
						|
                "Project Report Mail processing initiated.",
 | 
						|
                200));
 | 
						|
        }
 | 
						|
 | 
						|
 | 
						|
        [HttpGet("report-mail")]
 | 
						|
        public async Task<IActionResult> GetProjectStatisticsFromCache()
 | 
						|
        {
 | 
						|
            var mailList = await _cache.GetProjectReportMail(false);
 | 
						|
            if (mailList == null)
 | 
						|
            {
 | 
						|
                return NotFound(ApiResponse<object>.ErrorResponse("Not mail found", "Not mail found", 404));
 | 
						|
            }
 | 
						|
 | 
						|
            return Ok(ApiResponse<object>.SuccessResponse(mailList, "Fetched list of mail body successfully", 200));
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |