315 lines
15 KiB
C#
315 lines
15 KiB
C#
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<ApplicationDbContext> _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<ApplicationDbContext> 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<IActionResult> 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<InvoiceListVM>()
|
|
};
|
|
return Ok(ApiResponse<object>.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<InvoiceListVM>();
|
|
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<InvoiceListVM>(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<object>.SuccessResponse(response, $"{results.Count} invoices fetched successfully"));
|
|
}
|
|
|
|
|
|
[HttpPost("invoice/create")]
|
|
public async Task<IActionResult> CreateInvoiceAsync(InvoiceDto model)
|
|
{
|
|
await using var _context = await _dbContextFactory.CreateDbContextAsync();
|
|
//using var scope = _serviceScopeFactory.CreateScope();
|
|
//var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
|
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<object>.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<object>.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<object>.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<object>.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<object>.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<object>.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<object>.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<Invoice>(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<Document>();
|
|
var invoiceAttachments = new List<InvoiceAttachment>();
|
|
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<object>.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<object>.ErrorResponse(
|
|
"Transaction failed: " + ex.Message,
|
|
"An error occurred while creating the invoice", 500));
|
|
}
|
|
|
|
// Build response
|
|
var response = _mapper.Map<InvoiceListVM>(invoice);
|
|
response.Project = _mapper.Map<BasicProjectVM>(project);
|
|
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
|
|
response.BalanceAmount = response.BasicAmount + response.TaxAmount;
|
|
|
|
return Ok(ApiResponse<object>.SuccessResponse(response, "Invoice Created Successfully", 201));
|
|
}
|
|
|
|
|
|
}
|
|
}
|