Added the API to change the status of payment request
This commit is contained in:
parent
b54b83c63d
commit
96411c43b0
@ -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 ===================================================================
|
||||
|
||||
|
||||
@ -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 ===================================================================
|
||||
|
||||
@ -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 ===================================================================
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user