Added the add comment API and get Details API
This commit is contained in:
parent
c92e71b292
commit
62688d508f
8
Marco.Pms.Model/Dtos/Collection/InvoiceCommentDto.cs
Normal file
8
Marco.Pms.Model/Dtos/Collection/InvoiceCommentDto.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Marco.Pms.Model.Dtos.Collection
|
||||
{
|
||||
public class InvoiceCommentDto
|
||||
{
|
||||
public required string Comment { get; set; }
|
||||
public required Guid InvoiceId { get; set; }
|
||||
}
|
||||
}
|
14
Marco.Pms.Model/ViewModels/Collection/InvoiceAttachmentVM.cs
Normal file
14
Marco.Pms.Model/ViewModels/Collection/InvoiceAttachmentVM.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Marco.Pms.Model.ViewModels.Activities;
|
||||
|
||||
namespace Marco.Pms.Model.ViewModels.Collection
|
||||
{
|
||||
public class InvoiceAttachmentVM
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid InvoiceId { get; set; }
|
||||
public Guid DocumentId { get; set; }
|
||||
public string? PreSignedUrl { get; set; }
|
||||
public BasicEmployeeVM? UploadedBy { get; set; }
|
||||
|
||||
}
|
||||
}
|
13
Marco.Pms.Model/ViewModels/Collection/InvoiceCommentVM.cs
Normal file
13
Marco.Pms.Model/ViewModels/Collection/InvoiceCommentVM.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Marco.Pms.Model.ViewModels.Activities;
|
||||
|
||||
namespace Marco.Pms.Model.ViewModels.Collection
|
||||
{
|
||||
public class InvoiceCommentVM
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Comment { get; set; } = default!;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public BasicEmployeeVM? CreatedBy { get; set; }
|
||||
public Guid InvoiceId { get; set; }
|
||||
}
|
||||
}
|
31
Marco.Pms.Model/ViewModels/Collection/InvoiceDetailsVM.cs
Normal file
31
Marco.Pms.Model/ViewModels/Collection/InvoiceDetailsVM.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using Marco.Pms.Model.ViewModels.Activities;
|
||||
using Marco.Pms.Model.ViewModels.Projects;
|
||||
|
||||
namespace Marco.Pms.Model.ViewModels.Collection
|
||||
{
|
||||
public class InvoiceDetailsVM
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Title { get; set; } = default!;
|
||||
public string Description { get; set; } = default!;
|
||||
public string InvoiceNumber { get; set; } = default!;
|
||||
public string? EInvoiceNumber { get; set; }
|
||||
public BasicProjectVM? Project { get; set; }
|
||||
public DateTime InvoiceDate { get; set; }
|
||||
public DateTime ClientSubmitedDate { get; set; }
|
||||
public DateTime ExceptedPaymentDate { get; set; }
|
||||
public double BasicAmount { get; set; }
|
||||
public double TaxAmount { get; set; }
|
||||
public double BalanceAmount { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public bool MarkAsCompleted { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public BasicEmployeeVM? CreatedBy { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public BasicEmployeeVM? UpdatedBy { get; set; }
|
||||
public List<InvoiceAttachmentVM>? Attachments { get; set; }
|
||||
public List<ReceivedInvoicePaymentVM>? ReceivedInvoicePayments { get; set; }
|
||||
public List<InvoiceCommentVM>? Comments { get; set; }
|
||||
|
||||
}
|
||||
}
|
@ -148,13 +148,82 @@ namespace Marco.Pms.Services.Controllers
|
||||
return Ok(ApiResponse<object>.SuccessResponse(response, $"{results.Count} invoices fetched successfully"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves complete details of a specific invoice including associated comments, attachments, and payments.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the invoice.</param>
|
||||
/// <returns>Returns invoice details with associated data or a NotFound/BadRequest response.</returns>
|
||||
[HttpGet("invoice/details/{id}")]
|
||||
public async Task<IActionResult> GetInvoiceDetailsAsync(Guid id)
|
||||
{
|
||||
_logger.LogInfo("Fetching details for InvoiceId: {InvoiceId}, TenantId: {TenantId}", id, tenantId);
|
||||
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
// Retrieve primary invoice details with related entities (project, created/updated by + roles)
|
||||
var invoice = await context.Invoices
|
||||
.Include(i => i.Project)
|
||||
.Include(i => i.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.Include(i => i.UpdatedBy).ThenInclude(e => e!.JobRole)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(i => i.Id == id && i.TenantId == tenantId);
|
||||
|
||||
if (invoice == null)
|
||||
{
|
||||
_logger.LogWarning("Invoice not found. InvoiceId: {InvoiceId}, TenantId: {TenantId}", id, tenantId);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Invoice not found", "The specified invoice does not exist.", 404));
|
||||
}
|
||||
|
||||
_logger.LogInfo("Invoice {InvoiceId} found. Fetching related data...", id);
|
||||
|
||||
// Parallelize loading of child collections using independent DbContext instances — efficient and thread-safe
|
||||
var commentsTask = LoadInvoiceCommentsAsync(id, tenantId);
|
||||
var attachmentsTask = LoadInvoiceAttachmentsAsync(id, tenantId);
|
||||
var paymentsTask = LoadReceivedInvoicePaymentsAsync(id, tenantId);
|
||||
|
||||
await Task.WhenAll(commentsTask, attachmentsTask, paymentsTask);
|
||||
|
||||
var comments = commentsTask.Result;
|
||||
var attachments = attachmentsTask.Result;
|
||||
var receivedInvoicePayments = paymentsTask.Result;
|
||||
|
||||
// Map invoice to response view model
|
||||
var response = _mapper.Map<InvoiceDetailsVM>(invoice);
|
||||
|
||||
// Populate related data
|
||||
if (comments.Any())
|
||||
response.Comments = _mapper.Map<List<InvoiceCommentVM>>(comments);
|
||||
|
||||
if (attachments.Any())
|
||||
{
|
||||
response.Attachments = attachments.Select(a =>
|
||||
{
|
||||
var result = _mapper.Map<InvoiceAttachmentVM>(a);
|
||||
result.PreSignedUrl = _s3Service.GeneratePreSignedUrl(a.Document!.S3Key);
|
||||
result.UploadedBy = _mapper.Map<BasicEmployeeVM>(a.Document.UploadedBy);
|
||||
return result;
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
if (receivedInvoicePayments.Any())
|
||||
response.ReceivedInvoicePayments = _mapper.Map<List<ReceivedInvoicePaymentVM>>(receivedInvoicePayments);
|
||||
|
||||
// Compute total paid and balance amounts
|
||||
double totalPaidAmount = receivedInvoicePayments.Sum(rip => rip.Amount);
|
||||
double totalAmount = invoice.BasicAmount + invoice.TaxAmount;
|
||||
response.BalanceAmount = totalAmount - totalPaidAmount;
|
||||
|
||||
_logger.LogInfo("Invoice {InvoiceId} details fetched successfully: Total = {TotalAmount}, Paid = {PaidAmount}, Balance = {BalanceAmount}",
|
||||
id, totalAmount, totalPaidAmount, response.BalanceAmount);
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(response, "Invoice details fetched successfully", 200));
|
||||
}
|
||||
|
||||
[HttpPost("invoice/create")]
|
||||
public async Task<IActionResult> CreateInvoiceAsync([FromBody] 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}",
|
||||
@ -183,7 +252,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
_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 cannot be later than the client submitted date",
|
||||
"Invoice date is later than client submitted date", 400));
|
||||
}
|
||||
if (model.ClientSubmitedDate.Date > DateTime.UtcNow.Date)
|
||||
@ -199,18 +268,49 @@ namespace Marco.Pms.Services.Controllers
|
||||
_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 submission date cannot be later than the expected payment date",
|
||||
"Client submitted date is later than expected payment date", 400));
|
||||
}
|
||||
if (model.ExceptedPaymentDate.Date < DateTime.UtcNow.Date)
|
||||
|
||||
// Check for existing InvoiceNumber for this tenant before creating/updating to maintain uniqueness.
|
||||
var invoiceNumberExists = await _context.Invoices
|
||||
.AnyAsync(i => i.InvoiceNumber == model.InvoiceNumber && i.TenantId == tenantId);
|
||||
|
||||
if (invoiceNumberExists)
|
||||
{
|
||||
_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));
|
||||
// Log the conflict event with full context for audit/review.
|
||||
_logger.LogWarning(
|
||||
"Invoice number conflict detected for InvoiceNumber: {InvoiceNumber} and TenantId: {TenantId}",
|
||||
model.InvoiceNumber, tenantId);
|
||||
|
||||
// Return HTTP 409 (Conflict) with a descriptive, actionable message.
|
||||
return StatusCode(409, ApiResponse<object>.ErrorResponse(
|
||||
"Invoice number already exists",
|
||||
$"The invoice number '{model.InvoiceNumber}' is already in use for this tenant. Please choose a unique invoice number.",
|
||||
409));
|
||||
}
|
||||
|
||||
// If E-InvoiceNumber is provided (optional), validate its uniqueness for this tenant.
|
||||
if (!string.IsNullOrWhiteSpace(model.EInvoiceNumber))
|
||||
{
|
||||
var eInvoiceNumberExists = await _context.Invoices
|
||||
.AnyAsync(i => i.EInvoiceNumber == model.EInvoiceNumber && i.TenantId == tenantId);
|
||||
|
||||
if (eInvoiceNumberExists)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"E-Invoice number conflict detected for EInvoiceNumber: {EInvoiceNumber} and TenantId: {TenantId}",
|
||||
model.EInvoiceNumber, tenantId);
|
||||
|
||||
// Return HTTP 409 (Conflict) with a tailored message for E-Invoice.
|
||||
return StatusCode(409, ApiResponse<object>.ErrorResponse(
|
||||
"E-Invoice number already exists",
|
||||
$"The E-Invoice number '{model.EInvoiceNumber}' is already assigned to another invoice for this tenant. Please provide a unique E-Invoice number.",
|
||||
409));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fetch project
|
||||
var project = await _context.Projects
|
||||
.FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
|
||||
@ -365,10 +465,39 @@ namespace Marco.Pms.Services.Controllers
|
||||
_logger.LogWarning("Client submitted date {ClientSubmitedDate} is later than payment received date {PaymentReceivedDate} for invoice {InvoiceId}",
|
||||
invoice.ClientSubmitedDate, model.PaymentReceivedDate, model.InvoiceId);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse(
|
||||
"Client submitted date is later than payment received date",
|
||||
"Client submission date cannot be later than the payment received date",
|
||||
"The client submission date cannot be later than the payment received date", 400));
|
||||
}
|
||||
|
||||
// Retrieve all previous payments for the given invoice and tenant in a single, efficient query.
|
||||
var receivedInvoicePayments = await _context.ReceivedInvoicePayments
|
||||
.Where(rip => rip.InvoiceId == invoice.Id && rip.TenantId == tenantId)
|
||||
.Select(rip => rip.Amount) // Only select required field for better performance.
|
||||
.ToListAsync();
|
||||
|
||||
// Calculate the sum of all previous payments to determine the total paid so far.
|
||||
var previousPaidAmount = receivedInvoicePayments.Sum();
|
||||
var totalPaidAmount = previousPaidAmount + model.Amount;
|
||||
|
||||
// Compute the invoice's total amount payable including taxes.
|
||||
var totalAmount = invoice.BasicAmount + invoice.TaxAmount;
|
||||
|
||||
// Business rule validation: Prevent the overpayment scenario.
|
||||
if (totalPaidAmount > totalAmount)
|
||||
{
|
||||
// Log the details for easier debugging and audit trails.
|
||||
_logger.LogWarning(
|
||||
"Overpayment attempt detected. InvoiceId: {InvoiceId}, TenantId: {TenantId}, TotalInvoiceAmount: {TotalInvoiceAmount}, PreviousPaidAmount: {PreviousPaidAmount}, AttemptedPayment: {AttemptedPayment}, CalculatedTotalPaid: {TotalPaidAmount}.",
|
||||
invoice.Id, tenantId, totalAmount, previousPaidAmount, model.Amount, totalPaidAmount);
|
||||
|
||||
// Return a bad request response with a clear, actionable error message.
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse(
|
||||
"Invalid payment: total paid amount exceeds invoice total.",
|
||||
$"The total of existing payments ({previousPaidAmount}) plus the new payment ({model.Amount}) would exceed the invoice total ({totalAmount}). Please verify payment details.",
|
||||
400));
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
// Map DTO to entity and set creation metadata
|
||||
@ -398,6 +527,67 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a comment to the specified invoice, validating model and invoice existence.
|
||||
/// </summary>
|
||||
/// <param name="model">DTO containing InvoiceId and Comment text.</param>
|
||||
/// <returns>201 Created with comment details, or error codes for validation/invoice not found.</returns>
|
||||
[HttpPost("invoice/add/comment")]
|
||||
public async Task<IActionResult> AddCommentToInvoiceAsync([FromBody] InvoiceCommentDto model)
|
||||
{
|
||||
// Validate incoming data early to avoid unnecessary database calls.
|
||||
if (string.IsNullOrWhiteSpace(model.Comment))
|
||||
{
|
||||
_logger.LogWarning("Invalid or missing comment data for InvoiceId {InvoiceId}, TenantId {TenantId}", model.InvoiceId, tenantId);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse(
|
||||
"Invalid comment data",
|
||||
"The comment text and model must not be null or empty.",
|
||||
400));
|
||||
}
|
||||
|
||||
await using var _context = await _dbContextFactory.CreateDbContextAsync();
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
// Find the target invoice for the specified tenant.
|
||||
var invoice = await _context.Invoices
|
||||
.FirstOrDefaultAsync(i => i.Id == model.InvoiceId && i.TenantId == tenantId);
|
||||
|
||||
if (invoice == null)
|
||||
{
|
||||
_logger.LogWarning("Cannot add comment—invoice not found. InvoiceId {InvoiceId}, TenantId {TenantId}", model.InvoiceId, tenantId);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse(
|
||||
"Invoice not found",
|
||||
$"Invoice with ID '{model.InvoiceId}' does not exist for the specified tenant.",
|
||||
404));
|
||||
}
|
||||
|
||||
// Construct the new comment entity with required audit metadata.
|
||||
var comment = new InvoiceComment
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Comment = model.Comment.Trim(),
|
||||
InvoiceId = model.InvoiceId,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedById = loggedInEmployee.Id,
|
||||
TenantId = tenantId
|
||||
};
|
||||
|
||||
_context.InvoiceComments.Add(comment);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInfo("Added new comment to invoice {InvoiceId} by employee {EmployeeId}, TenantId {TenantId}",
|
||||
comment.InvoiceId, loggedInEmployee.Id, tenantId);
|
||||
|
||||
var response = _mapper.Map<InvoiceCommentVM>(comment);
|
||||
|
||||
// Return successful creation with comment details.
|
||||
return StatusCode(201, ApiResponse<object>.SuccessResponse(
|
||||
response,
|
||||
"Comment added to invoice successfully.",
|
||||
201));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Marks the specified invoice as completed if it exists and is not already completed.
|
||||
/// </summary>
|
||||
@ -454,5 +644,46 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads invoice comments asynchronously with related metadata.
|
||||
/// </summary>
|
||||
private async Task<List<InvoiceComment>> LoadInvoiceCommentsAsync(Guid invoiceId, Guid tenantId)
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.InvoiceComments
|
||||
.Include(ic => ic.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.AsNoTracking()
|
||||
.Where(ic => ic.InvoiceId == invoiceId && ic.TenantId == tenantId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads invoice attachments and their upload metadata asynchronously.
|
||||
/// </summary>
|
||||
private async Task<List<InvoiceAttachment>> LoadInvoiceAttachmentsAsync(Guid invoiceId, Guid tenantId)
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.InvoiceAttachments
|
||||
.Include(ia => ia.Document)
|
||||
.ThenInclude(d => d!.UploadedBy)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.AsNoTracking()
|
||||
.Where(ia => ia.InvoiceId == invoiceId && ia.TenantId == tenantId && ia.Document != null && ia.Document.UploadedBy != null)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads received invoice payment records asynchronously with creator metadata.
|
||||
/// </summary>
|
||||
private async Task<List<ReceivedInvoicePayment>> LoadReceivedInvoicePaymentsAsync(Guid invoiceId, Guid tenantId)
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.ReceivedInvoicePayments
|
||||
.Include(rip => rip.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.AsNoTracking()
|
||||
.Where(rip => rip.InvoiceId == invoiceId && rip.TenantId == tenantId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -261,9 +261,14 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
#region ======================================================= Collection =======================================================
|
||||
CreateMap<InvoiceDto, Invoice>();
|
||||
CreateMap<Invoice, InvoiceListVM>();
|
||||
CreateMap<Invoice, InvoiceDetailsVM>();
|
||||
|
||||
CreateMap<ReceivedInvoicePaymentDto, ReceivedInvoicePayment>();
|
||||
CreateMap<ReceivedInvoicePayment, ReceivedInvoicePaymentVM>();
|
||||
|
||||
CreateMap<InvoiceComment, InvoiceCommentVM>();
|
||||
|
||||
CreateMap<InvoiceAttachment, InvoiceAttachmentVM>();
|
||||
#endregion
|
||||
|
||||
#region ======================================================= Master =======================================================
|
||||
|
Loading…
x
Reference in New Issue
Block a user