diff --git a/Marco.Pms.Services/Service/ExpensesService.cs b/Marco.Pms.Services/Service/ExpensesService.cs index 15bf027..1488f56 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -1084,115 +1084,148 @@ namespace Marco.Pms.Services.Service #endregion #region =================================================================== Payment Request Functions =================================================================== + public async Task> 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(); - 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 = expenseCategoryTask.Result; - - if (expenseCategory == null) - { - return ApiResponse.ErrorResponse("Expense Category not found", "Expense Category not found", 404); - } - - var currency = currencyTask.Result; - - if (currency == null) - { - return ApiResponse.ErrorResponse("Currency not found", "Currency not found", 404); - } - - var expenseStatus = expenseStatusTask.Result; - - if (expenseCategory == null) - { - return ApiResponse.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(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) + // Execute database lookups concurrently + var expenseCategoryTask = Task.Run(async () => { - 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.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.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.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(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(paymentRequest); + response.PaymentRequestUID = $"{paymentRequest.UIDPrefix}/{paymentRequest.UIDPostfix:D5}"; + response.Currency = currency; + response.ExpenseCategory = _mapper.Map(expenseCategory); + response.ExpenseStatus = _mapper.Map(expenseStatus); + response.Project = _mapper.Map(project); + response.CreatedBy = _mapper.Map(loggedInEmployee); - // Create a list of tasks to be executed concurrently. - var processingTasks = attachments.Select(attachment => - ProcessSinglePaymentRequestAttachmentAsync(attachment, paymentRequest, loggedInEmployee.Id, tenantId, batchId) - ).ToList(); - - var results = await Task.WhenAll(processingTasks); - - // This part is thread-safe as it runs after all concurrent tasks are complete. - foreach (var (document, paymentRequestAttachment) in results) - { - _context.Documents.Add(document); - _context.PaymentRequestAttachments.Add(paymentRequestAttachment); - } - _logger.LogInfo("{AttachmentCount} attachments processed and staged for saving.", results.Length); + _logger.LogInfo("Payment request created successfully with UID: {PaymentRequestUID}", response.PaymentRequestUID); + return ApiResponse.SuccessResponse(response, "Created the Payment Request Successfully.", 201); + } + catch (ArgumentException ex) + { + _logger.LogError(ex, "Argument error in CreatePaymentRequestAsync: {Message}", ex.Message); + return ApiResponse.ErrorResponse(ex.Message, "Invalid data.", 400); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error in CreatePaymentRequestAsync: {Message}", ex.Message); + return ApiResponse.ErrorResponse("An error occurred while creating the payment request.", ex.Message, 500); + } + finally + { + _logger.LogInfo("End CreatePaymentRequestAsync for EmployeeId: {EmployeeId}", loggedInEmployee.Id); } - await _context.SaveChangesAsync(); - - var response = _mapper.Map(paymentRequest); - response.PaymentRequestUID = $"{paymentRequest.UIDPrefix}/{paymentRequest.UIDPostfix.ToString("D5")}"; - response.Currency = currency; - response.ExpenseCategory = _mapper.Map(expenseCategory); - response.ExpenseStatus = _mapper.Map(expenseStatus); - response.Project = _mapper.Map(project); - response.CreatedBy = _mapper.Map(loggedInEmployee); - - return ApiResponse.SuccessResponse(response, "Created the Payment Request Successfully", 201); } + #endregion #region =================================================================== Payment Request Functions ===================================================================