Added an APIs get payment history list and add payment to purchase invoices
This commit is contained in:
parent
28deae6416
commit
e92976049e
@ -1,4 +1,5 @@
|
||||
using AutoMapper;
|
||||
using Marco.Pms.Model.Dtos.Collection;
|
||||
using Marco.Pms.Model.Dtos.PurchaseInvoice;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Services.Service.ServiceInterfaces;
|
||||
@ -167,6 +168,33 @@ namespace Marco.Pms.Services.Controllers
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Purchase Invoice History Functions ===================================================================
|
||||
[HttpGet("payment-history/list/{purchaseInvoiceId}")]
|
||||
public async Task<IActionResult> GetPurchaseInvoiceHistoryList(Guid purchaseInvoiceId, CancellationToken cancellationToken)
|
||||
{
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _purchaseInvoiceService.GetPurchaseInvoiceHistoryListAsync(purchaseInvoiceId, loggedInEmployee, tenantId, cancellationToken);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("add/payment")]
|
||||
public async Task<IActionResult> AddPurchaseInvoicePayment([FromBody] ReceivedInvoicePaymentDto model, CancellationToken ct)
|
||||
{
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _purchaseInvoiceService.AddPurchaseInvoicePaymentAsync(model, loggedInEmployee, tenantId, ct);
|
||||
if (response.Success)
|
||||
{
|
||||
var notification = new
|
||||
{
|
||||
LoggedInUserId = loggedInEmployee.Id,
|
||||
Keyword = "Delivery_Challan",
|
||||
Response = response.Data
|
||||
};
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
|
||||
// Return the HTTP response
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using AutoMapper;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Helpers.Utility;
|
||||
using Marco.Pms.Model.Dtos.Collection;
|
||||
using Marco.Pms.Model.Dtos.PurchaseInvoice;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Filters;
|
||||
@ -11,6 +12,7 @@ using Marco.Pms.Model.PurchaseInvoice;
|
||||
using Marco.Pms.Model.ServiceProject;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.Activities;
|
||||
using Marco.Pms.Model.ViewModels.Collection;
|
||||
using Marco.Pms.Model.ViewModels.Organization;
|
||||
using Marco.Pms.Model.ViewModels.Projects;
|
||||
using Marco.Pms.Model.ViewModels.PurchaseInvoice;
|
||||
@ -1135,6 +1137,219 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
#region =================================================================== Purchase Invoice History Functions ===================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the full payment history for a given Purchase Invoice,
|
||||
/// including related adjustment heads and creator information,
|
||||
/// with multi-tenant safety and structured logging.
|
||||
/// </summary>
|
||||
/// <param name="purchaseInvoiceId">Identifier of the purchase invoice.</param>
|
||||
/// <param name="loggedInEmployee">The employee requesting the history.</param>
|
||||
/// <param name="tenantId">Current tenant identifier for multi-tenancy boundary.</param>
|
||||
/// <param name="ct">Cancellation token for cooperative cancellation.</param>
|
||||
/// <returns>
|
||||
/// Standardized <see cref="ApiResponse{T}"/> containing the list of payment history
|
||||
/// view models or a detailed error response.
|
||||
/// </returns>
|
||||
public async Task<ApiResponse<object>> GetPurchaseInvoiceHistoryListAsync(Guid purchaseInvoiceId, Employee loggedInEmployee, Guid tenantId, CancellationToken ct)
|
||||
{
|
||||
// Guard clauses to fail fast on obviously invalid parameters.
|
||||
if (purchaseInvoiceId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("GetPurchaseInvoiceHistoryListAsync called with empty PurchaseInvoiceId. TenantId: {TenantId}, EmployeeId: {EmployeeId}", tenantId, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("Purchase invoice reference is required.", "PurchaseInvoiceId is empty.", 400);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Create a short-lived DbContext instance via factory for this operation.
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync(ct);
|
||||
|
||||
// Step 1: Ensure the invoice exists for this tenant.
|
||||
var purchaseInvoice = await context.PurchaseInvoiceDetails
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(
|
||||
pi => pi.Id == purchaseInvoiceId && pi.TenantId == tenantId,
|
||||
ct);
|
||||
|
||||
if (purchaseInvoice == null)
|
||||
{
|
||||
_logger.LogWarning("Purchase Invoice not found. InvoiceId: {InvoiceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}", purchaseInvoiceId, tenantId, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("Purchase invoice not found.", $"Purchase invoice not found for InvoiceId: {purchaseInvoiceId}, TenantId: {tenantId}.", 404);
|
||||
}
|
||||
|
||||
// Step 2: Query payment history with necessary related data eagerly loaded.
|
||||
var paymentHistoryQuery = context.PurchaseInvoicePayments
|
||||
.Include(pip => pip.PaymentAdjustmentHead)
|
||||
.Include(pip => pip.CreatedBy) // Include creator
|
||||
.ThenInclude(e => e!.JobRole) // Include creator's job role
|
||||
.Where(pip => pip.InvoiceId == purchaseInvoiceId && pip.TenantId == tenantId);
|
||||
|
||||
var paymentHistory = await paymentHistoryQuery.ToListAsync(ct);
|
||||
|
||||
// Step 3: Map to view models for safe response shaping.
|
||||
var responseVm = _mapper.Map<List<ReceivedInvoicePaymentVM>>(paymentHistory);
|
||||
|
||||
_logger.LogInfo("Purchase Invoice payment history retrieved successfully. InvoiceId: {InvoiceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}, PaymentsCount: {PaymentsCount}",
|
||||
purchaseInvoiceId, tenantId, loggedInEmployee.Id, responseVm.Count);
|
||||
|
||||
// Even if there is no payment history, return 200 with empty collection.
|
||||
return ApiResponse<object>.SuccessResponse(responseVm, "Purchase invoice payment history retrieved successfully.", 200);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Explicitly handle cancellation to avoid logging it as an error.
|
||||
_logger.LogWarning("GetPurchaseInvoiceHistoryListAsync operation was canceled. InvoiceId: {InvoiceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}",
|
||||
purchaseInvoiceId, tenantId, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("The operation was canceled.", "GetPurchaseInvoiceHistoryListAsync was canceled by the caller.", 499);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Catch-all to ensure no unhandled exception reaches the client.
|
||||
_logger.LogError(ex, "Unexpected error while retrieving Purchase Invoice payment history. InvoiceId: {InvoiceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}",
|
||||
purchaseInvoiceId, tenantId, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("An unexpected error occurred while retrieving purchase invoice history.", "Unhandled exception in GetPurchaseInvoiceHistoryListAsync.", 500);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a payment entry against an existing Purchase Invoice with full validation
|
||||
/// and structured logging suitable for enterprise scenarios.
|
||||
/// </summary>
|
||||
/// <param name="model">Payment details to be recorded against the invoice.</param>
|
||||
/// <param name="loggedInEmployee">The currently logged-in employee performing this action.</param>
|
||||
/// <param name="tenantId">Current tenant identifier to enforce multi-tenancy boundaries.</param>
|
||||
/// <param name="ct">Cancellation token for cooperative cancellation.</param>
|
||||
/// <returns>Standardized ApiResponse with the created payment view model or error details.</returns>
|
||||
public async Task<ApiResponse<object>> AddPurchaseInvoicePaymentAsync(ReceivedInvoicePaymentDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct)
|
||||
{
|
||||
// Guard clauses to fail fast on invalid input and avoid null reference issues.
|
||||
if (model == null)
|
||||
{
|
||||
_logger.LogWarning("AddPurchaseInvoicePaymentAsync called with null model. TenantId: {TenantId}, EmployeeId: {EmployeeId}", tenantId, loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Invalid payment data.", "Received null payment model.", 400);
|
||||
}
|
||||
|
||||
if (model.InvoiceId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("AddPurchaseInvoicePaymentAsync called with empty InvoiceId. TenantId: {TenantId}, EmployeeId: {EmployeeId}", tenantId, loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Invoice reference is required.", "InvoiceId is empty in payment model.", 200);
|
||||
}
|
||||
|
||||
if (model.Amount <= 0)
|
||||
{
|
||||
_logger.LogWarning("AddPurchaseInvoicePaymentAsync called with non-positive Amount. TenantId: {TenantId}, EmployeeId: {EmployeeId}, Amount: {Amount}", tenantId, loggedInEmployee.Id, model.Amount);
|
||||
return ApiResponse<object>.ErrorResponse("Payment amount must be greater than zero.", $"Invalid payment amount: {model.Amount}.", 400);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Create a short-lived DbContext instance using the factory to ensure proper scope per operation.
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync(ct);
|
||||
|
||||
// Step 1: Validate that the invoice exists for the current tenant.
|
||||
var purchaseInvoice = await context.PurchaseInvoiceDetails
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(
|
||||
pi => pi.Id == model.InvoiceId && pi.TenantId == tenantId,
|
||||
ct);
|
||||
|
||||
if (purchaseInvoice == null)
|
||||
{
|
||||
_logger.LogWarning("Purchase Invoice not found. InvoiceId: {InvoiceId}, TenantId: {TenantId}", model.InvoiceId, tenantId);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("Purchase invoice not found.", $"Purchase invoice not found for InvoiceId: {model.InvoiceId}, TenantId: {tenantId}.", 404);
|
||||
}
|
||||
|
||||
// Step 2: Validate Payment Adjustment Head.
|
||||
var paymentAdjustmentHead = await context.PaymentAdjustmentHeads
|
||||
.FirstOrDefaultAsync(pah => pah.Id == model.PaymentAdjustmentHeadId && pah.TenantId == tenantId, ct);
|
||||
|
||||
if (paymentAdjustmentHead == null)
|
||||
{
|
||||
_logger.LogWarning("Payment Adjustment Head not found. PaymentAdjustmentHeadId: {PaymentAdjustmentHeadId}, TenantId: {TenantId}", model.PaymentAdjustmentHeadId, tenantId);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("Payment adjustment head not found.", $"Payment adjustment head not found for Id: {model.PaymentAdjustmentHeadId}, TenantId: {tenantId}.",
|
||||
404);
|
||||
}
|
||||
|
||||
// Step 3: Get existing payments and ensure the new payment does not exceed the invoice total.
|
||||
var existingPayments = await context.PurchaseInvoicePayments
|
||||
.Where(pi => pi.InvoiceId == model.InvoiceId && pi.TenantId == tenantId)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var alreadyPaidAmount = existingPayments.Sum(pi => pi.Amount);
|
||||
var proposedTotalPaidAmount = alreadyPaidAmount + model.Amount;
|
||||
|
||||
if (proposedTotalPaidAmount > purchaseInvoice.TotalAmount)
|
||||
{
|
||||
_logger.LogWarning("Attempt to add payment exceeding invoice total. InvoiceId: {InvoiceId}, TenantId: {TenantId}, InvoiceTotal: {InvoiceTotal}, AlreadyPaid: {AlreadyPaid}, NewAmount: {NewAmount}, ProposedTotal: {ProposedTotal}",
|
||||
model.InvoiceId, tenantId, purchaseInvoice.TotalAmount, alreadyPaidAmount, model.Amount, proposedTotalPaidAmount);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("Total payment amount cannot exceed the invoice amount.", "Payment addition rejected due to exceeding invoice total amount.",
|
||||
400);
|
||||
}
|
||||
|
||||
// Step 4: Map DTO to entity and initialize metadata.
|
||||
var receivedInvoicePayment = _mapper.Map<PurchaseInvoicePayment>(model);
|
||||
receivedInvoicePayment.Id = Guid.NewGuid();
|
||||
receivedInvoicePayment.CreatedAt = DateTime.UtcNow;
|
||||
receivedInvoicePayment.CreatedById = loggedInEmployee.Id;
|
||||
receivedInvoicePayment.TenantId = tenantId;
|
||||
|
||||
// Step 5: Persist the new payment record.
|
||||
context.PurchaseInvoicePayments.Add(receivedInvoicePayment);
|
||||
|
||||
// For enterprise robustness, pass the cancellation token to SaveChangesAsync.
|
||||
var saveResult = await context.SaveChangesAsync(ct);
|
||||
|
||||
if (saveResult <= 0)
|
||||
{
|
||||
_logger.LogError(null, "SaveChangesAsync returned 0 while adding Purchase Invoice payment. InvoiceId: {InvoiceId}, TenantId: {TenantId}, PaymentId: {PaymentId}",
|
||||
model.InvoiceId, tenantId, receivedInvoicePayment.Id);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("Failed to add payment due to a persistence issue.", "Database SaveChangesAsync returned 0 rows affected while adding PurchaseInvoicePayment.",
|
||||
statusCode: StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
|
||||
// Step 6: Map entity back to a response view model.
|
||||
var responseVm = _mapper.Map<ReceivedInvoicePaymentVM>(receivedInvoicePayment);
|
||||
responseVm.PaymentAdjustmentHead = _mapper.Map<PaymentAdjustmentHeadVM>(paymentAdjustmentHead);
|
||||
|
||||
_logger.LogInfo("Purchase Invoice payment added successfully. InvoiceId: {InvoiceId}, TenantId: {TenantId}, PaymentId: {PaymentId}, Amount: {Amount}, EmployeeId: {EmployeeId}",
|
||||
model.InvoiceId, tenantId, receivedInvoicePayment.Id, receivedInvoicePayment.Amount, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(responseVm, "Payment has been recorded successfully.", 201); // 201 Created is more appropriate for new resource.
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Explicitly handle cancellation to avoid logging it as an error.
|
||||
_logger.LogError(null, "AddPurchaseInvoicePaymentAsync operation was canceled. InvoiceId: {InvoiceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}",
|
||||
model.InvoiceId, tenantId, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("The operation was canceled.", "AddPurchaseInvoicePaymentAsync was canceled by the caller.", 499); // 499 used by some systems for client cancellation.
|
||||
}
|
||||
catch (DbUpdateException dbEx)
|
||||
{
|
||||
// Database-related exceptions with structured logging for observability.
|
||||
_logger.LogError(dbEx, "Database update error while adding Purchase Invoice payment. InvoiceId: {InvoiceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}",
|
||||
model.InvoiceId, tenantId, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("An error occurred while saving the payment.", "Database update exception occurred during payment creation.", 500);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Catch-all to avoid leaking unhandled exceptions to the client.
|
||||
_logger.LogError(ex, "Unexpected error while adding Purchase Invoice payment. InvoiceId: {InvoiceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}",
|
||||
model.InvoiceId, tenantId, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("An unexpected error occurred while processing the payment.", "Unhandled exception in AddPurchaseInvoicePaymentAsync.", 500);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Helper Functions ===================================================================
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Marco.Pms.Model.Dtos.PurchaseInvoice;
|
||||
using Marco.Pms.Model.Dtos.Collection;
|
||||
using Marco.Pms.Model.Dtos.PurchaseInvoice;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.PurchaseInvoice;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
@ -23,6 +24,8 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Purchase Invoice History Functions ===================================================================
|
||||
Task<ApiResponse<object>> GetPurchaseInvoiceHistoryListAsync(Guid purchaseInvoiceId, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
||||
Task<ApiResponse<object>> AddPurchaseInvoicePaymentAsync(ReceivedInvoicePaymentDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Helper Functions ===================================================================
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user