Added the get list of invoices API
This commit is contained in:
parent
b30369baa5
commit
5ff87cd870
6514
Marco.Pms.DataAccess/Migrations/20251013141456_Added_EInvoiceNumber_In_Invoice_Table.Designer.cs
generated
Normal file
6514
Marco.Pms.DataAccess/Migrations/20251013141456_Added_EInvoiceNumber_In_Invoice_Table.Designer.cs
generated
Normal file
File diff suppressed because one or more lines are too long
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -375,7 +375,7 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<double>("Amount")
|
||||
b.Property<double>("BasicAmount")
|
||||
.HasColumnType("double");
|
||||
|
||||
b.Property<DateTime>("ClientSubmitedDate")
|
||||
@ -391,6 +391,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("EInvoiceNumber")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("ExceptedPaymentDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
@ -404,9 +407,15 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("MarkAsCompleted")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<double>("TaxAmount")
|
||||
.HasColumnType("double");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
|
@ -12,6 +12,7 @@ namespace Marco.Pms.Model.Collection
|
||||
public string Title { get; set; } = default!;
|
||||
public string Description { get; set; } = default!;
|
||||
public string InvoiceNumber { get; set; } = default!;
|
||||
public string? EInvoiceNumber { get; set; }
|
||||
public Guid ProjectId { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
@ -20,8 +21,10 @@ namespace Marco.Pms.Model.Collection
|
||||
public DateTime InvoiceDate { get; set; }
|
||||
public DateTime ClientSubmitedDate { 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 MarkAsCompleted { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public Guid CreatedById { get; set; }
|
||||
|
||||
|
@ -8,11 +8,13 @@ namespace Marco.Pms.Model.Dtos.Collection
|
||||
public required string Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public required string InvoiceNumber { get; set; }
|
||||
public string? EInvoiceNumber { get; set; }
|
||||
public required Guid ProjectId { get; set; }
|
||||
public required DateTime InvoiceDate { get; set; }
|
||||
public required DateTime ClientSubmitedDate { 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; }
|
||||
}
|
||||
}
|
||||
|
@ -9,12 +9,16 @@ namespace Marco.Pms.Model.ViewModels.Collection
|
||||
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 Amount { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public double BasicAmount { get; set; }
|
||||
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 BasicEmployeeVM? CreatedBy { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
@ -44,18 +44,139 @@ namespace Marco.Pms.Services.Controllers
|
||||
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>();
|
||||
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}",
|
||||
@ -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", 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}",
|
||||
@ -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", 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
|
||||
var project = await _context.Projects
|
||||
.FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
|
||||
if (project == null)
|
||||
{
|
||||
@ -84,19 +221,20 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
|
||||
// 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();
|
||||
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
|
||||
_context.Invoices.Add(invoice);
|
||||
await _context.SaveChangesAsync(); // Save to generate invoice.Id
|
||||
|
||||
// Handle attachments
|
||||
var documents = new List<Document>();
|
||||
@ -143,9 +281,9 @@ namespace Marco.Pms.Services.Controllers
|
||||
invoiceAttachments.Add(invoiceAttachment);
|
||||
}
|
||||
|
||||
context.Documents.AddRange(documents);
|
||||
context.InvoiceAttachments.AddRange(invoiceAttachments);
|
||||
await context.SaveChangesAsync(); // Save attachments and mappings
|
||||
_context.Documents.AddRange(documents);
|
||||
_context.InvoiceAttachments.AddRange(invoiceAttachments);
|
||||
await _context.SaveChangesAsync(); // Save attachments and mappings
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
@ -166,6 +304,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
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));
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user