Added the get list of invoices API

This commit is contained in:
ashutosh.nehete 2025-10-13 19:59:18 +05:30
parent b30369baa5
commit 5ff87cd870
7 changed files with 6747 additions and 15 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,61 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_EInvoiceNumber_In_Invoice_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "Amount",
table: "Invoices",
newName: "TaxAmount");
migrationBuilder.AddColumn<double>(
name: "BasicAmount",
table: "Invoices",
type: "double",
nullable: false,
defaultValue: 0.0);
migrationBuilder.AddColumn<string>(
name: "EInvoiceNumber",
table: "Invoices",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<bool>(
name: "MarkAsCompleted",
table: "Invoices",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BasicAmount",
table: "Invoices");
migrationBuilder.DropColumn(
name: "EInvoiceNumber",
table: "Invoices");
migrationBuilder.DropColumn(
name: "MarkAsCompleted",
table: "Invoices");
migrationBuilder.RenameColumn(
name: "TaxAmount",
table: "Invoices",
newName: "Amount");
}
}
}

View File

@ -375,7 +375,7 @@ namespace Marco.Pms.DataAccess.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<double>("Amount") b.Property<double>("BasicAmount")
.HasColumnType("double"); .HasColumnType("double");
b.Property<DateTime>("ClientSubmitedDate") b.Property<DateTime>("ClientSubmitedDate")
@ -391,6 +391,9 @@ namespace Marco.Pms.DataAccess.Migrations
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<string>("EInvoiceNumber")
.HasColumnType("longtext");
b.Property<DateTime>("ExceptedPaymentDate") b.Property<DateTime>("ExceptedPaymentDate")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
@ -404,9 +407,15 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<bool>("IsActive") b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
b.Property<bool>("MarkAsCompleted")
.HasColumnType("tinyint(1)");
b.Property<Guid>("ProjectId") b.Property<Guid>("ProjectId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<double>("TaxAmount")
.HasColumnType("double");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");

View File

@ -12,6 +12,7 @@ namespace Marco.Pms.Model.Collection
public string Title { get; set; } = default!; public string Title { get; set; } = default!;
public string Description { get; set; } = default!; public string Description { get; set; } = default!;
public string InvoiceNumber { get; set; } = default!; public string InvoiceNumber { get; set; } = default!;
public string? EInvoiceNumber { get; set; }
public Guid ProjectId { get; set; } public Guid ProjectId { get; set; }
[ValidateNever] [ValidateNever]
@ -20,8 +21,10 @@ namespace Marco.Pms.Model.Collection
public DateTime InvoiceDate { get; set; } public DateTime InvoiceDate { get; set; }
public DateTime ClientSubmitedDate { get; set; } public DateTime ClientSubmitedDate { get; set; }
public DateTime ExceptedPaymentDate { get; set; } public DateTime ExceptedPaymentDate { get; set; }
public double Amount { get; set; } public double BasicAmount { get; set; }
public double TaxAmount { get; set; }
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;
public bool MarkAsCompleted { get; set; } = true;
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public Guid CreatedById { get; set; } public Guid CreatedById { get; set; }

View File

@ -8,11 +8,13 @@ namespace Marco.Pms.Model.Dtos.Collection
public required string Title { get; set; } public required string Title { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public required string InvoiceNumber { get; set; } public required string InvoiceNumber { get; set; }
public string? EInvoiceNumber { get; set; }
public required Guid ProjectId { get; set; } public required Guid ProjectId { get; set; }
public required DateTime InvoiceDate { get; set; } public required DateTime InvoiceDate { get; set; }
public required DateTime ClientSubmitedDate { get; set; } public required DateTime ClientSubmitedDate { get; set; }
public required DateTime ExceptedPaymentDate { get; set; } public required DateTime ExceptedPaymentDate { get; set; }
public required double Amount { get; set; } public double BasicAmount { get; set; }
public double TaxAmount { get; set; }
public List<FileUploadModel>? Attachments { get; set; } public List<FileUploadModel>? Attachments { get; set; }
} }
} }

View File

@ -9,12 +9,16 @@ namespace Marco.Pms.Model.ViewModels.Collection
public string Title { get; set; } = default!; public string Title { get; set; } = default!;
public string Description { get; set; } = default!; public string Description { get; set; } = default!;
public string InvoiceNumber { get; set; } = default!; public string InvoiceNumber { get; set; } = default!;
public string? EInvoiceNumber { get; set; }
public BasicProjectVM? Project { get; set; } public BasicProjectVM? Project { get; set; }
public DateTime InvoiceDate { get; set; } public DateTime InvoiceDate { get; set; }
public DateTime ClientSubmitedDate { get; set; } public DateTime ClientSubmitedDate { get; set; }
public DateTime ExceptedPaymentDate { get; set; } public DateTime ExceptedPaymentDate { get; set; }
public double Amount { get; set; } public double BasicAmount { get; set; }
public bool IsActive { get; set; } = true; public double TaxAmount { get; set; }
public double BalanceAmount { get; set; }
public bool IsActive { get; set; }
public bool MarkAsCompleted { get; set; }
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public BasicEmployeeVM? CreatedBy { get; set; } public BasicEmployeeVM? CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; } public DateTime? UpdatedAt { get; set; }

View File

@ -44,18 +44,139 @@ namespace Marco.Pms.Services.Controllers
tenantId = userhelper.GetTenantId(); 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")] [HttpPost("invoice/create")]
public async Task<IActionResult> CreateInvoiceAsync(InvoiceDto model) public async Task<IActionResult> CreateInvoiceAsync(InvoiceDto model)
{ {
await using var context = await _dbContextFactory.CreateDbContextAsync(); await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScopeFactory.CreateScope(); //using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>(); //var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogInfo("Starting invoice creation for ProjectId: {ProjectId} by EmployeeId: {EmployeeId}", _logger.LogInfo("Starting invoice creation for ProjectId: {ProjectId} by EmployeeId: {EmployeeId}",
model.ProjectId, loggedInEmployee.Id); 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 // 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) if (model.InvoiceDate.Date > model.ClientSubmitedDate.Date)
{ {
_logger.LogWarning("Invoice date {InvoiceDate} is later than client submitted date {ClientSubmitedDate}", _logger.LogWarning("Invoice date {InvoiceDate} is later than client submitted date {ClientSubmitedDate}",
@ -64,6 +185,14 @@ namespace Marco.Pms.Services.Controllers
"Invoice date is later than client submitted date", "Invoice date is later than client submitted date",
"Invoice date is later than client submitted date", 400)); "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) if (model.ClientSubmitedDate.Date > model.ExceptedPaymentDate.Date)
{ {
_logger.LogWarning("Client submitted date {ClientSubmitedDate} is later than expected payment date {ExpectedPaymentDate}", _logger.LogWarning("Client submitted date {ClientSubmitedDate} is later than expected payment date {ExpectedPaymentDate}",
@ -72,9 +201,17 @@ namespace Marco.Pms.Services.Controllers
"Client submitted date is later than expected payment date", "Client submitted date is later than expected payment date",
"Client submitted date is later than expected payment date", 400)); "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 // Fetch project
var project = await context.Projects var project = await _context.Projects
.FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId); .FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
if (project == null) if (project == null)
{ {
@ -84,19 +221,20 @@ namespace Marco.Pms.Services.Controllers
} }
// Begin transaction scope with async flow support // Begin transaction scope with async flow support
await using var transaction = await context.Database.BeginTransactionAsync(); await using var transaction = await _context.Database.BeginTransactionAsync();
var invoice = new Invoice(); var invoice = new Invoice();
try try
{ {
// Map and create invoice // Map and create invoice
invoice = _mapper.Map<Invoice>(model); invoice = _mapper.Map<Invoice>(model);
invoice.IsActive = true; invoice.IsActive = true;
invoice.MarkAsCompleted = false;
invoice.CreatedAt = DateTime.UtcNow; invoice.CreatedAt = DateTime.UtcNow;
invoice.CreatedById = loggedInEmployee.Id; invoice.CreatedById = loggedInEmployee.Id;
invoice.TenantId = tenantId; invoice.TenantId = tenantId;
context.Invoices.Add(invoice); _context.Invoices.Add(invoice);
await context.SaveChangesAsync(); // Save to generate invoice.Id await _context.SaveChangesAsync(); // Save to generate invoice.Id
// Handle attachments // Handle attachments
var documents = new List<Document>(); var documents = new List<Document>();
@ -143,9 +281,9 @@ namespace Marco.Pms.Services.Controllers
invoiceAttachments.Add(invoiceAttachment); invoiceAttachments.Add(invoiceAttachment);
} }
context.Documents.AddRange(documents); _context.Documents.AddRange(documents);
context.InvoiceAttachments.AddRange(invoiceAttachments); _context.InvoiceAttachments.AddRange(invoiceAttachments);
await context.SaveChangesAsync(); // Save attachments and mappings await _context.SaveChangesAsync(); // Save attachments and mappings
} }
// Commit transaction // Commit transaction
@ -166,6 +304,7 @@ namespace Marco.Pms.Services.Controllers
var response = _mapper.Map<InvoiceListVM>(invoice); var response = _mapper.Map<InvoiceListVM>(invoice);
response.Project = _mapper.Map<BasicProjectVM>(project); response.Project = _mapper.Map<BasicProjectVM>(project);
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee); response.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
response.BalanceAmount = response.BasicAmount + response.TaxAmount;
return Ok(ApiResponse<object>.SuccessResponse(response, "Invoice Created Successfully", 201)); return Ok(ApiResponse<object>.SuccessResponse(response, "Invoice Created Successfully", 201));
} }