Completed the Create payment request API

This commit is contained in:
ashutosh.nehete 2025-11-01 16:07:47 +05:30
parent e1d6434fc6
commit eccb87ebb4

View File

@ -1084,115 +1084,148 @@ namespace Marco.Pms.Services.Service
#endregion #endregion
#region =================================================================== Payment Request Functions =================================================================== #region =================================================================== Payment Request Functions ===================================================================
public async Task<ApiResponse<object>> CreatePaymentRequestAsync(PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId) public async Task<ApiResponse<object>> CreatePaymentRequestAsync(PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId)
{ {
string uIDPrefix = $"PY/{DateTime.Now.ToString("MMyy")}"; _logger.LogInfo("Start CreatePaymentRequestAsync for EmployeeId: {EmployeeId} TenantId: {TenantId}", loggedInEmployee.Id, tenantId);
var expenseCategoryTask = Task.Run(async () => string uIDPrefix = $"PY/{DateTime.Now:MMyy}";
try
{ {
await using var context = await _dbContextFactory.CreateDbContextAsync(); // Execute database lookups concurrently
return await context.ExpenseCategoryMasters.FirstOrDefaultAsync(et => et.Id == model.ExpenseCategoryId && et.IsActive); var expenseCategoryTask = Task.Run(async () =>
});
var currencyTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.CurrencyMaster.FirstOrDefaultAsync(c => c.Id == model.CurrencyId);
});
var expenseStatusTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ExpensesStatusMaster.FirstOrDefaultAsync(es => es.Id == Draft && es.IsActive);
});
var projectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects.FirstOrDefaultAsync(P => model.ProjectId.HasValue && P.Id == model.ProjectId.Value);
});
await Task.WhenAll(expenseCategoryTask, currencyTask, expenseStatusTask, projectTask);
var expenseCategory = expenseCategoryTask.Result;
if (expenseCategory == null)
{
return ApiResponse<object>.ErrorResponse("Expense Category not found", "Expense Category not found", 404);
}
var currency = currencyTask.Result;
if (currency == null)
{
return ApiResponse<object>.ErrorResponse("Currency not found", "Currency not found", 404);
}
var expenseStatus = expenseStatusTask.Result;
if (expenseCategory == null)
{
return ApiResponse<object>.ErrorResponse("Expense Status not found", "Expense Status not found", 404);
}
var project = projectTask.Result;
var lastPR = await _context.PaymentRequests.Where(pr => pr.UIDPrefix == uIDPrefix).OrderByDescending(pr => pr.UIDPostfix).FirstOrDefaultAsync();
int uIDPostfix = lastPR == null ? 1 : (lastPR.UIDPostfix + 1);
var paymentRequest = _mapper.Map<PaymentRequest>(model);
paymentRequest.ExpenseStatusId = Draft;
paymentRequest.UIDPrefix = uIDPrefix;
paymentRequest.UIDPostfix = uIDPostfix;
paymentRequest.IsActive = true;
paymentRequest.CreatedAt = DateTime.UtcNow;
paymentRequest.CreatedById = loggedInEmployee.Id;
paymentRequest.TenantId = tenantId;
_context.PaymentRequests.Add(paymentRequest);
await _context.SaveChangesAsync();
// 4. Process Attachments
if (model.BillAttachments?.Any() ?? false)
{
var attachments = model.BillAttachments;
// Pre-validate all attachments to fail fast before any uploads.
foreach (var attachment in attachments)
{ {
if (string.IsNullOrWhiteSpace(attachment.Base64Data) || !_s3Service.IsBase64String(attachment.Base64Data)) await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ExpenseCategoryMasters.FirstOrDefaultAsync(et => et.Id == model.ExpenseCategoryId && et.IsActive);
});
var currencyTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.CurrencyMaster.FirstOrDefaultAsync(c => c.Id == model.CurrencyId);
});
var expenseStatusTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ExpensesStatusMaster.FirstOrDefaultAsync(es => es.Id == Draft && es.IsActive);
});
var projectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects.FirstOrDefaultAsync(P => model.ProjectId.HasValue && P.Id == model.ProjectId.Value);
});
await Task.WhenAll(expenseCategoryTask, currencyTask, expenseStatusTask, projectTask);
var expenseCategory = await expenseCategoryTask;
if (expenseCategory == null)
{
_logger.LogWarning("Expense Category not found with Id: {ExpenseCategoryId}", model.ExpenseCategoryId);
return ApiResponse<object>.ErrorResponse("Expense Category not found.", "Expense Category not found.", 404);
}
var currency = await currencyTask;
if (currency == null)
{
_logger.LogWarning("Currency not found with Id: {CurrencyId}", model.CurrencyId);
return ApiResponse<object>.ErrorResponse("Currency not found.", "Currency not found.", 404);
}
var expenseStatus = await expenseStatusTask;
if (expenseStatus == null)
{
_logger.LogWarning("Expense Status not found for Draft.");
return ApiResponse<object>.ErrorResponse("Expense Status (Draft) not found.", "Expense Status not found.", 404);
}
var project = await projectTask;
// Project is optional so no error if not found
// Generate unique UID postfix based on existing requests for the current prefix
var lastPR = await _context.PaymentRequests.Where(pr => pr.UIDPrefix == uIDPrefix)
.OrderByDescending(pr => pr.UIDPostfix)
.FirstOrDefaultAsync();
int uIDPostfix = lastPR == null ? 1 : (lastPR.UIDPostfix + 1);
// Map DTO to PaymentRequest entity and set additional mandatory fields
var paymentRequest = _mapper.Map<PaymentRequest>(model);
paymentRequest.ExpenseStatusId = Draft;
paymentRequest.UIDPrefix = uIDPrefix;
paymentRequest.UIDPostfix = uIDPostfix;
paymentRequest.IsActive = true;
paymentRequest.CreatedAt = DateTime.UtcNow;
paymentRequest.CreatedById = loggedInEmployee.Id;
paymentRequest.TenantId = tenantId;
await _context.PaymentRequests.AddAsync(paymentRequest);
await _context.SaveChangesAsync();
// Process bill attachments if any
if (model.BillAttachments?.Any() == true)
{
_logger.LogInfo("Processing {AttachmentCount} attachments for PaymentRequest Id: {PaymentRequestId}",
model.BillAttachments.Count, paymentRequest.Id);
// Validate base64 attachments data before processing
foreach (var attachment in model.BillAttachments)
{ {
throw new ArgumentException($"Invalid or missing Base64 data for attachment: {attachment.FileName ?? "N/A"}"); if (string.IsNullOrWhiteSpace(attachment.Base64Data) || !_s3Service.IsBase64String(attachment.Base64Data))
{
_logger.LogWarning("Invalid or missing Base64 data for attachment: {FileName}", attachment.FileName ?? "N/A");
throw new ArgumentException($"Invalid or missing Base64 data for attachment: {attachment.FileName ?? "N/A"}");
}
} }
var batchId = Guid.NewGuid();
// Process all attachments concurrently
var processingTasks = model.BillAttachments.Select(attachment =>
ProcessSinglePaymentRequestAttachmentAsync(attachment, paymentRequest, loggedInEmployee.Id, tenantId, batchId)
).ToList();
var results = await Task.WhenAll(processingTasks);
// Add documents and payment request attachments after concurrent processing
foreach (var (document, paymentRequestAttachment) in results)
{
_context.Documents.Add(document);
_context.PaymentRequestAttachments.Add(paymentRequestAttachment);
}
await _context.SaveChangesAsync();
_logger.LogInfo("{AttachmentCount} attachments processed and saved for PaymentRequest Id: {PaymentRequestId}",
results.Length, paymentRequest.Id);
} }
var batchId = Guid.NewGuid(); // Prepare response VM
var response = _mapper.Map<PaymentRequestVM>(paymentRequest);
response.PaymentRequestUID = $"{paymentRequest.UIDPrefix}/{paymentRequest.UIDPostfix:D5}";
response.Currency = currency;
response.ExpenseCategory = _mapper.Map<ExpensesTypeMasterVM>(expenseCategory);
response.ExpenseStatus = _mapper.Map<ExpensesStatusMasterVM>(expenseStatus);
response.Project = _mapper.Map<BasicProjectVM>(project);
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
// Create a list of tasks to be executed concurrently. _logger.LogInfo("Payment request created successfully with UID: {PaymentRequestUID}", response.PaymentRequestUID);
var processingTasks = attachments.Select(attachment => return ApiResponse<object>.SuccessResponse(response, "Created the Payment Request Successfully.", 201);
ProcessSinglePaymentRequestAttachmentAsync(attachment, paymentRequest, loggedInEmployee.Id, tenantId, batchId) }
).ToList(); catch (ArgumentException ex)
{
var results = await Task.WhenAll(processingTasks); _logger.LogError(ex, "Argument error in CreatePaymentRequestAsync: {Message}", ex.Message);
return ApiResponse<object>.ErrorResponse(ex.Message, "Invalid data.", 400);
// This part is thread-safe as it runs after all concurrent tasks are complete. }
foreach (var (document, paymentRequestAttachment) in results) catch (Exception ex)
{ {
_context.Documents.Add(document); _logger.LogError(ex, "Unexpected error in CreatePaymentRequestAsync: {Message}", ex.Message);
_context.PaymentRequestAttachments.Add(paymentRequestAttachment); return ApiResponse<object>.ErrorResponse("An error occurred while creating the payment request.", ex.Message, 500);
} }
_logger.LogInfo("{AttachmentCount} attachments processed and staged for saving.", results.Length); finally
{
_logger.LogInfo("End CreatePaymentRequestAsync for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
} }
await _context.SaveChangesAsync();
var response = _mapper.Map<PaymentRequestVM>(paymentRequest);
response.PaymentRequestUID = $"{paymentRequest.UIDPrefix}/{paymentRequest.UIDPostfix.ToString("D5")}";
response.Currency = currency;
response.ExpenseCategory = _mapper.Map<ExpensesTypeMasterVM>(expenseCategory);
response.ExpenseStatus = _mapper.Map<ExpensesStatusMasterVM>(expenseStatus);
response.Project = _mapper.Map<BasicProjectVM>(project);
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
return ApiResponse<object>.SuccessResponse(response, "Created the Payment Request Successfully", 201);
} }
#endregion #endregion
#region =================================================================== Payment Request Functions =================================================================== #region =================================================================== Payment Request Functions ===================================================================