Added the API to change the status of payment request

This commit is contained in:
ashutosh.nehete 2025-11-07 17:38:38 +05:30
parent b54b83c63d
commit 96411c43b0
3 changed files with 219 additions and 0 deletions

View File

@ -157,6 +157,7 @@ namespace Marco.Pms.Services.Controllers
var response = await _expensesService.GetPaymentRequestFilterObjectAsync(loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpPost("payment-request/create")]
public async Task<IActionResult> CreatePaymentRequest([FromBody] PaymentRequestDto model)
{
@ -170,6 +171,19 @@ namespace Marco.Pms.Services.Controllers
return StatusCode(response.StatusCode, response);
}
[HttpPost("payment-request/action")]
public async Task<IActionResult> ChangePaymentRequestStatus([FromBody] PaymentRequestRecordDto model)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _expensesService.ChangePaymentRequestStatusAsync(model, loggedInEmployee, tenantId);
if (response.Success)
{
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Payment_Request", Response = response.Data };
await _signalR.SendNotificationAsync(notification);
}
return StatusCode(response.StatusCode, response);
}
#endregion
#region =================================================================== Payment Request Functions ===================================================================

View File

@ -1819,6 +1819,210 @@ namespace Marco.Pms.Services.Service
_logger.LogInfo("End CreatePaymentRequestAsync for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
}
}
public async Task<ApiResponse<object>> ChangePaymentRequestStatusAsync(PaymentRequestRecordDto model, Employee loggedInEmployee, Guid tenantId)
{
using var scope = _serviceScopeFactory.CreateScope();
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
// 1. Fetch Existing Payment Request with Related Entities (Single Query)
var paymentRequest = await _context.PaymentRequests
.Include(pr => pr.Currency)
.Include(pr => pr.Project)
.Include(pr => pr.RecurringPayment)
.Include(pr => pr.ExpenseCategory)
.Include(pr => pr.ExpenseStatus)
.Include(pr => pr.CreatedBy).ThenInclude(e => e!.JobRole)
.FirstOrDefaultAsync(pr =>
pr.Id == model.PaymentRequestId &&
pr.ExpenseStatusId != model.StatusId &&
pr.TenantId == tenantId
);
if (paymentRequest == null)
{
_logger.LogWarning("ChangeStatus: Payment Request not found or already at target status. payment RequestId={PaymentRequestId}, TenantId={TenantId}", model.PaymentRequestId, tenantId);
return ApiResponse<object>.ErrorResponse("payment Request not found or status is already set.", "payment Request not found", 404);
}
_logger.LogInfo("ChangeStatus: Requested status change. PaymentRequestId={PaymentRequestId} FromStatus={FromStatusId} ToStatus={ToStatusId}",
paymentRequest.Id, paymentRequest.ExpenseStatusId, model.StatusId);
// 2. Run Prerequisite Checks in Parallel (Status transition + Permissions)
var statusTransitionTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ExpensesStatusMapping
.Include(m => m.NextStatus)
.FirstOrDefaultAsync(m => m.StatusId == paymentRequest.ExpenseStatusId && m.NextStatusId == model.StatusId);
});
var targetStatusPermissionsTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.StatusPermissionMapping
.Where(spm => spm.StatusId == model.StatusId)
.ToListAsync();
});
await Task.WhenAll(statusTransitionTask, targetStatusPermissionsTask);
var statusTransition = await statusTransitionTask;
var requiredPermissions = await targetStatusPermissionsTask;
// 3. Validate Transition and Required Fields
if (statusTransition == null)
{
_logger.LogWarning("ChangeStatus: Invalid status transition. PaymentRequestId={PaymentRequestId}, FromStatus={FromStatus}, ToStatus={ToStatus}",
paymentRequest.Id, paymentRequest.ExpenseStatusId, model.StatusId);
return ApiResponse<object>.ErrorResponse("Status change is not permitted.", "Invalid Transition", 400);
}
// Validate special logic for "Processed"
if (statusTransition.NextStatusId == Processed &&
(string.IsNullOrWhiteSpace(model.PaidTransactionId) ||
!model.PaidAt.HasValue ||
model.PaidById == null ||
model.PaidById == Guid.Empty))
{
_logger.LogWarning("ChangeStatus: Missing payment fields for 'Processed'. PaymentRequestId={PaymentRequestId}", paymentRequest.Id);
return ApiResponse<object>.ErrorResponse("payment details are missing or invalid.", "Invalid Payment", 400);
}
// 4. Permission Check (CreatedBy -> Reviewer bypass, else required permissions)
bool hasPermission = false;
if (model.StatusId == Review && paymentRequest.CreatedById == loggedInEmployee.Id)
{
hasPermission = true;
}
else if (requiredPermissions.Any())
{
var permissionIds = requiredPermissions.Select(p => p.PermissionId).ToList();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
hasPermission = await permissionService.HasPermissionAny(permissionIds, loggedInEmployee.Id) && model.StatusId != Review;
}
if (!hasPermission)
{
_logger.LogWarning("ChangeStatus: Permission denied. EmployeeId={EmployeeId} PaymentRequestId={PaymentRequestId} ToStatus={ToStatusId}",
loggedInEmployee.Id, paymentRequest.Id, model.StatusId);
return ApiResponse<object>.ErrorResponse("You do not have permission for this action.", "Access Denied", 403);
}
// 5. Prepare for update (Audit snapshot)
var paymentRequestStateBeforeChange = _updateLogHelper.EntityToBsonDocument(paymentRequest);
// 6. Apply Status Transition
paymentRequest.ExpenseStatusId = statusTransition.NextStatusId;
paymentRequest.ExpenseStatus = statusTransition.NextStatus;
// 7. Add Reimbursement if applicable
if (model.StatusId == Processed)
{
var totalAmount = model.BaseAmount + model.TaxAmount;
if (!totalAmount.HasValue || totalAmount != paymentRequest.Amount)
{
// Log the mismatch error with relevant details
_logger.LogWarning("Payment amount mismatch: calculated totalAmount = {TotalAmount}, expected Amount = {ExpectedAmount}", totalAmount ?? 0, paymentRequest.Amount);
// Return a structured error response indicating the amount discrepancy
return ApiResponse<object>.ErrorResponse(
"Payment amount validation failed.",
$"The sum of the base amount and tax amount ({totalAmount}) does not match the expected payment request amount ({paymentRequest.Amount}).",
400);
}
paymentRequest.PaidAt = model.PaidAt;
paymentRequest.PaidById = model.PaidById;
paymentRequest.PaidTransactionId = model.PaidTransactionId;
paymentRequest.TDSPercentage = model.TDSPercentage;
paymentRequest.BaseAmount = model.BaseAmount;
paymentRequest.TaxAmount = model.TaxAmount;
var lastTransaction = await _context.AdvancePaymentTransactions.OrderByDescending(apt => apt.CreatedAt).FirstOrDefaultAsync(apt => apt.TenantId == tenantId);
double lastBalance = 0;
if (lastTransaction != null)
{
lastBalance = lastTransaction.CurrentBalance;
}
_context.AdvancePaymentTransactions.Add(new AdvancePaymentTransaction
{
Id = Guid.NewGuid(),
FinanceUIdPostfix = paymentRequest.UIDPostfix,
FinanceUIdPrefix = paymentRequest.UIDPrefix,
Title = paymentRequest.Title,
ProjectId = paymentRequest.ProjectId,
EmployeeId = paymentRequest.CreatedById,
Amount = paymentRequest.Amount,
CurrentBalance = lastBalance + paymentRequest.Amount,
PaidAt = model.PaidAt!.Value,
CreatedAt = DateTime.UtcNow,
CreatedById = loggedInEmployee.Id,
IsActive = true,
TenantId = tenantId
});
}
// 8. Add paymentRequest Log Entry
_context.StatusUpdateLogs.Add(new StatusUpdateLog
{
Id = Guid.NewGuid(),
EntityId = paymentRequest.Id,
StatusId = statusTransition.StatusId,
NextStatusId = statusTransition.NextStatusId,
UpdatedById = loggedInEmployee.Id,
UpdatedAt = DateTime.UtcNow,
Comment = model.Comment,
TenantId = tenantId
});
// 9. Commit database transaction
try
{
await _context.SaveChangesAsync();
_logger.LogInfo("ChangeStatus: Status updated successfully. PaymentRequestId={PaymentRequestId} NewStatus={NewStatusId}", paymentRequest.Id, paymentRequest.ExpenseStatusId);
}
catch (DbUpdateConcurrencyException ex)
{
_logger.LogError(ex, "ChangeStatus: Concurrency error. PaymentRequestId={PaymentRequestId}", paymentRequest.Id);
return ApiResponse<object>.ErrorResponse("Payment Request was modified by another user. Please refresh and try again.", "Concurrency Error", 409);
}
//_ = Task.Run(async () =>
//{
// // --- Push Notification Section ---
// // This section attempts to send a test push notification to the user's device.
// // It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens.
// var name = $"{loggedInEmployee.FirstName} {loggedInEmployee.LastName}";
// await _firebase.SendExpenseMessageAsync(paymentRequest, name, tenantId);
//});
// 10. Post-processing (audit log, cache, fetch next states)
try
{
await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
{
EntityId = paymentRequest.Id.ToString(),
UpdatedById = loggedInEmployee.Id.ToString(),
OldObject = paymentRequestStateBeforeChange,
UpdatedAt = DateTime.UtcNow
}, "PaymentRequestModificationLog");
// Prepare response
var responseDto = _mapper.Map<PaymentRequestVM>(paymentRequest);
return ApiResponse<object>.SuccessResponse(responseDto, "Payment Request status chnaged successfully", 200);
}
catch (Exception ex)
{
_logger.LogError(ex, "ChangeStatus: Post-operation error (e.g. audit logging). PaymentRequestId={PaymentRequestId}", paymentRequest.Id);
var responseDto = _mapper.Map<PaymentRequestVM>(paymentRequest);
return ApiResponse<object>.SuccessResponse(responseDto, "Status updated, but audit logging or cache update failed.");
}
}
#endregion
#region =================================================================== Payment Request Functions ===================================================================

View File

@ -23,6 +23,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> GetPayeeNameListAsync(Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> GetPaymentRequestFilterObjectAsync(Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> CreatePaymentRequestAsync(PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> ChangePaymentRequestStatusAsync(PaymentRequestRecordDto model, Employee loggedInEmployee, Guid tenantId);
#endregion
#region =================================================================== Payment Request Functions ===================================================================