using AutoMapper; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Collection; using Marco.Pms.Model.DocumentManager; using Marco.Pms.Model.Dtos.Collection; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Collection; using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace Marco.Pms.Services.Controllers { [Route("api/[controller]")] [ApiController] [Authorize] public class CollectionController : ControllerBase { private readonly IDbContextFactory _dbContextFactory; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly UserHelper _userHelper; private readonly S3UploadService _s3Service; private readonly IMapper _mapper; private readonly ILoggingService _logger; private readonly Guid tenantId; public CollectionController(IDbContextFactory dbContextFactory, IServiceScopeFactory serviceScopeFactory, S3UploadService s3Service, UserHelper userhelper, ILoggingService logger, IMapper mapper) { _dbContextFactory = dbContextFactory; _serviceScopeFactory = serviceScopeFactory; _userHelper = userhelper; _s3Service = s3Service; _mapper = mapper; _logger = logger; tenantId = userhelper.GetTenantId(); } [HttpGet("invoice/list")] public async Task GetInvoiceListAsync([FromQuery] string? searchString, [FromQuery] DateTime? fromDate, [FromQuery] DateTime? toDate, [FromQuery] int pageSize = 20, [FromQuery] int pageNumber = 1 , [FromQuery] bool isActive = true, [FromQuery] bool isPending = false) { _logger.LogInfo( "Fetching invoice list: Page {PageNumber}, Size {PageSize}, Active={IsActive}, PendingOnly={IsPending}, Search='{SearchString}', From={From}, To={To}", pageNumber, pageSize, isActive, isPending, searchString ?? "", fromDate?.Date ?? DateTime.MinValue, toDate?.Date ?? DateTime.MaxValue); await using var context = await _dbContextFactory.CreateDbContextAsync(); // Build base query with required includes and no tracking var invoicesQuery = context.Invoices .Include(i => i.Project) .Include(i => i.CreatedBy).ThenInclude(e => e!.JobRole) .Include(i => i.UpdatedBy).ThenInclude(e => e!.JobRole) .Where(i => i.IsActive == isActive && i.TenantId == tenantId) .AsNoTracking(); // Disable change tracking for read-only query // Apply date filter if (fromDate.HasValue && toDate.HasValue) { var fromDateUtc = fromDate.Value.Date; var toDateUtc = toDate.Value.Date.AddDays(1).AddTicks(-1); // End of day invoicesQuery = invoicesQuery.Where(i => i.InvoiceDate >= fromDateUtc && i.InvoiceDate <= toDateUtc); _logger.LogDebug("Applied date filter: {From} to {To}", fromDateUtc, toDateUtc); } // Apply search filter if (!string.IsNullOrWhiteSpace(searchString)) { invoicesQuery = invoicesQuery.Where(i => i.Title.Contains(searchString) || i.InvoiceNumber.Contains(searchString)); _logger.LogDebug("Applied search filter with term: {SearchString}", searchString); } // Get total count before pagination var totalEntites = await invoicesQuery.CountAsync(); _logger.LogDebug("Total matching invoices: {TotalCount}", totalEntites); // Apply sorting and pagination var invoices = await invoicesQuery .OrderByDescending(i => i.InvoiceDate) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); if (!invoices.Any()) { _logger.LogInfo("No invoices found for the given criteria."); var emptyResponse = new { CurrentPage = pageNumber, TotalPages = 0, TotalEntites = 0, Data = new List() }; return Ok(ApiResponse.SuccessResponse(emptyResponse, "No invoices found")); } // Fetch all related payment data in a single query var invoiceIds = invoices.Select(i => i.Id).ToList(); var paymentGroups = await context.ReceivedInvoicePayments .AsNoTracking() .Where(rip => invoiceIds.Contains(rip.InvoiceId) && rip.TenantId == tenantId) .GroupBy(rip => rip.InvoiceId) .Select(g => new { InvoiceId = g.Key, PaidAmount = g.Sum(rip => rip.Amount) }) .ToDictionaryAsync(x => x.InvoiceId, x => x.PaidAmount); _logger.LogDebug("Fetched payment data for {Count} invoices", paymentGroups.Count); // Map and calculate balance in memory var results = new List(); foreach (var invoice in invoices) { var totalAmount = invoice.BasicAmount + invoice.TaxAmount; var paidAmount = paymentGroups.GetValueOrDefault(invoice.Id, 0); var balanceAmount = totalAmount - paidAmount; // Skip if filtering for pending invoices and balance is zero if (isPending && balanceAmount <= 0) continue; var result = _mapper.Map(invoice); result.BalanceAmount = balanceAmount; results.Add(result); } var totalPages = (int)Math.Ceiling((double)totalEntites / pageSize); var response = new { CurrentPage = pageNumber, TotalPages = totalPages, TotalEntites = totalEntites, Data = results }; _logger.LogInfo("Successfully returned {ResultCount} invoices out of {TotalCount} total", results.Count, totalEntites); return Ok(ApiResponse.SuccessResponse(response, $"{results.Count} invoices fetched successfully")); } [HttpPost("invoice/create")] public async Task CreateInvoiceAsync(InvoiceDto model) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); //using var scope = _serviceScopeFactory.CreateScope(); //var permissionService = scope.ServiceProvider.GetRequiredService(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); _logger.LogInfo("Starting invoice creation for ProjectId: {ProjectId} by EmployeeId: {EmployeeId}", model.ProjectId, loggedInEmployee.Id); if (model.InvoiceNumber.Length > 17) { _logger.LogWarning("Invoice Number {InvoiceNumber} is greater than 17 charater", model.InvoiceNumber); return BadRequest(ApiResponse.ErrorResponse( "Invoice Number {InvoiceNumber} is greater than 17 charater", "Invoice Number {InvoiceNumber} is greater than 17 charater", 400)); } // Validate date sequence if (model.InvoiceDate.Date > DateTime.UtcNow.Date) { _logger.LogWarning("Invoice date {InvoiceDate} cannot be in the future.", model.InvoiceDate); return BadRequest(ApiResponse.ErrorResponse( "Invoice date cannot be in the future", "Invoice date cannot be in the future", 400)); } if (model.InvoiceDate.Date > model.ClientSubmitedDate.Date) { _logger.LogWarning("Invoice date {InvoiceDate} is later than client submitted date {ClientSubmitedDate}", model.InvoiceDate, model.ClientSubmitedDate); return BadRequest(ApiResponse.ErrorResponse( "Invoice date is later than client submitted date", "Invoice date is later than client submitted date", 400)); } if (model.ClientSubmitedDate.Date > DateTime.UtcNow.Date) { _logger.LogWarning("Client submited date {ClientSubmitedDate} cannot be in the future.", model.InvoiceDate); return BadRequest(ApiResponse.ErrorResponse( "Client submited date cannot be in the future", "Client submited date cannot be in the future", 400)); } if (model.ClientSubmitedDate.Date > model.ExceptedPaymentDate.Date) { _logger.LogWarning("Client submitted date {ClientSubmitedDate} is later than expected payment date {ExpectedPaymentDate}", model.ClientSubmitedDate, model.ExceptedPaymentDate); return BadRequest(ApiResponse.ErrorResponse( "Client submitted date is later than expected payment date", "Client submitted date is later than expected payment date", 400)); } if (model.ExceptedPaymentDate.Date < DateTime.UtcNow.Date) { _logger.LogWarning("Excepted Payment Date {ExceptedPaymentDate} cannot be in the future.", model.ExceptedPaymentDate); return BadRequest(ApiResponse.ErrorResponse( "Excepted Payment Date cannot be in the future", "Excepted Payment Date cannot be in the future", 400)); } // Fetch project var project = await _context.Projects .FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId); if (project == null) { _logger.LogWarning("Project not found: ProjectId {ProjectId}, TenantId {TenantId}", model.ProjectId, tenantId); return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); } // Begin transaction scope with async flow support await using var transaction = await _context.Database.BeginTransactionAsync(); var invoice = new Invoice(); try { // Map and create invoice invoice = _mapper.Map(model); invoice.IsActive = true; invoice.MarkAsCompleted = false; invoice.CreatedAt = DateTime.UtcNow; invoice.CreatedById = loggedInEmployee.Id; invoice.TenantId = tenantId; _context.Invoices.Add(invoice); await _context.SaveChangesAsync(); // Save to generate invoice.Id // Handle attachments var documents = new List(); var invoiceAttachments = new List(); if (model.Attachments?.Any() == true) { var batchId = Guid.NewGuid(); foreach (var attachment in model.Attachments) { string base64 = attachment.Base64Data?.Split(',').LastOrDefault() ?? ""; if (string.IsNullOrWhiteSpace(base64)) { _logger.LogWarning("Base64 data is missing for attachment {FileName}", attachment.FileName ?? ""); return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Image data missing", 400)); } var fileType = _s3Service.GetContentTypeFromBase64(base64); var fileName = _s3Service.GenerateFileName(fileType, tenantId, "invoice"); var objectKey = $"tenant-{tenantId}/Project/{model.ProjectId}/Invoice/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); var document = new Document { Id = Guid.NewGuid(), BatchId = batchId, UploadedById = loggedInEmployee.Id, FileName = attachment.FileName ?? fileName, ContentType = attachment.ContentType, S3Key = objectKey, FileSize = attachment.FileSize, UploadedAt = DateTime.UtcNow, TenantId = tenantId }; documents.Add(document); var invoiceAttachment = new InvoiceAttachment { InvoiceId = invoice.Id, DocumentId = document.Id, TenantId = tenantId }; invoiceAttachments.Add(invoiceAttachment); } _context.Documents.AddRange(documents); _context.InvoiceAttachments.AddRange(invoiceAttachments); await _context.SaveChangesAsync(); // Save attachments and mappings } // Commit transaction await transaction.CommitAsync(); _logger.LogInfo("Invoice {InvoiceId} created successfully with {AttachmentCount} attachments.", invoice.Id, documents.Count); } catch (Exception ex) { await transaction.RollbackAsync(); _logger.LogError(ex, "Transaction rolled back during invoice creation for ProjectId {ProjectId}", model.ProjectId); return StatusCode(500, ApiResponse.ErrorResponse( "Transaction failed: " + ex.Message, "An error occurred while creating the invoice", 500)); } // Build response var response = _mapper.Map(invoice); response.Project = _mapper.Map(project); response.CreatedBy = _mapper.Map(loggedInEmployee); response.BalanceAmount = response.BasicAmount + response.TaxAmount; return Ok(ApiResponse.SuccessResponse(response, "Invoice Created Successfully", 201)); } } }