diff --git a/Marco.Pms.Services/Controllers/ExpenseController.cs b/Marco.Pms.Services/Controllers/ExpenseController.cs index 888ed97..af2e9eb 100644 --- a/Marco.Pms.Services/Controllers/ExpenseController.cs +++ b/Marco.Pms.Services/Controllers/ExpenseController.cs @@ -184,6 +184,19 @@ namespace Marco.Pms.Services.Controllers return StatusCode(response.StatusCode, response); } + [HttpPost("payment-request/expense/create")] + public async Task ChangeToExpanseFromPaymentRequest(ExpenseConversionDto model) + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _expensesService.ChangeToExpanseFromPaymentRequestAsync(model, loggedInEmployee, tenantId); + if (response.Success) + { + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Expanse", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + } + #endregion #region =================================================================== Payment Request Functions =================================================================== diff --git a/Marco.Pms.Services/Service/ExpensesService.cs b/Marco.Pms.Services/Service/ExpensesService.cs index d767d5c..43f8aa6 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -2023,6 +2023,174 @@ namespace Marco.Pms.Services.Service return ApiResponse.SuccessResponse(responseDto, "Status updated, but audit logging or cache update failed."); } } + public async Task> ChangeToExpanseFromPaymentRequestAsync(ExpenseConversionDto model, Employee loggedInEmployee, Guid tenantId) + { + _logger.LogInfo("Start ChangeToExpanseFromPaymentRequestAsync called by EmployeeId: {EmployeeId} for TenantId: {TenantId} PaymentRequestId: {PaymentRequestId}", + loggedInEmployee.Id, tenantId, model.PaymentRequestId); + + await using var transaction = await _context.Database.BeginTransactionAsync(); + + try + { + // Retrieve payment request with required navigation property and validation + var paymentRequest = await _context.PaymentRequests + .Include(pr => pr.ExpenseCategory) + .FirstOrDefaultAsync(pr => + pr.Id == model.PaymentRequestId && + !pr.IsAdvancePayment && + pr.ProjectId.HasValue && + pr.ExpenseCategoryId.HasValue && + pr.PaidById.HasValue && + pr.PaidAt.HasValue && + pr.ExpenseCategory != null && + pr.TenantId == tenantId && + pr.IsActive); + + if (paymentRequest == null) + { + _logger.LogWarning("Payment request not found for Id: {PaymentRequestId}, TenantId: {TenantId}", model.PaymentRequestId, tenantId); + return ApiResponse.ErrorResponse("Payment request not found.", "Payment request not found.", 404); + } + + // Check payment request status for eligibility to convert + if (paymentRequest.ExpenseStatusId != Processed) + { + _logger.LogWarning("Payment request {PaymentRequestId} status is not processed. Current status: {StatusId}", paymentRequest.Id, paymentRequest.ExpenseStatusId); + return ApiResponse.ErrorResponse("Payment is not processed.", "Payment is not processed.", 400); + } + + // Verify attachment requirements + var hasAttachments = model.BillAttachments?.Any() ?? false; + + if (!hasAttachments && paymentRequest.ExpenseCategory!.IsAttachmentRequried) + { + _logger.LogWarning("Attachment is required for ExpenseCategory {ExpenseCategoryId} but no attachments provided.", paymentRequest.ExpenseCategoryId!); + return ApiResponse.ErrorResponse("Attachment is required.", "Attachment is required.", 400); + } + + // Concurrently fetch status update logs and expense statuses required for logging and state transition + var statusUpdateLogTask = Task.Run(async () => + { + await using var context = await _dbContextFactory.CreateDbContextAsync(); + return await context.StatusUpdateLogs + .Where(sul => sul.EntityId == paymentRequest.Id && sul.TenantId == tenantId) + .OrderByDescending(sul => sul.UpdatedAt) + .ToListAsync(); + }); + + var expenseStatusTask = Task.Run(async () => + { + await using var context = await _dbContextFactory.CreateDbContextAsync(); + return await context.ExpensesStatusMaster.ToListAsync(); + }); + + await Task.WhenAll(statusUpdateLogTask, expenseStatusTask); + + var statusUpdateLogs = statusUpdateLogTask.Result; + var expenseStatuses = expenseStatusTask.Result; + + // Generate Expense UID with prefix for current period + string uIDPrefix = $"EX/{DateTime.Now:MMyy}"; + + var lastExpense = await _context.Expenses + .Where(e => e.UIDPrefix == uIDPrefix) + .OrderByDescending(e => e.UIDPostfix) + .FirstOrDefaultAsync(); + + int uIDPostfix = lastExpense == null ? 1 : (lastExpense.UIDPostfix + 1); + + // Get user IDs involved in review, approval, and processing from logs for audit trail linking + var reviewedLog = statusUpdateLogs.FirstOrDefault(sul => sul.NextStatusId == Approve || sul.NextStatusId == RejectedByReviewer); + var approvedLog = statusUpdateLogs.FirstOrDefault(sul => sul.NextStatusId == ProcessPending || sul.NextStatusId == RejectedByApprover); + var processedLog = statusUpdateLogs.FirstOrDefault(sul => sul.NextStatusId == Processed); + + // Create new Expense record replicating required data from PaymentRequest and input model + var expense = new Expenses + { + Id = Guid.NewGuid(), + UIDPrefix = uIDPrefix, + UIDPostfix = uIDPostfix, + ProjectId = paymentRequest.ProjectId!.Value, + ExpenseCategoryId = paymentRequest.ExpenseCategoryId!.Value, + PaymentModeId = model.PaymentModeId, + PaidById = paymentRequest.PaidById!.Value, + CreatedById = loggedInEmployee.Id, + ReviewedById = reviewedLog?.UpdatedById, + ApprovedById = approvedLog?.UpdatedById, + ProcessedById = processedLog?.UpdatedById, + TransactionDate = paymentRequest.PaidAt!.Value, + Description = paymentRequest.Description, + CreatedAt = DateTime.UtcNow, + TransactionId = paymentRequest.PaidTransactionId, + Location = model.Location, + GSTNumber = model.GSTNumber, + SupplerName = paymentRequest.Payee, + CurrencyId = paymentRequest.CurrencyId, + Amount = paymentRequest.Amount, + BaseAmount = paymentRequest.BaseAmount, + TaxAmount = paymentRequest.TaxAmount, + TDSPercentage = paymentRequest.TDSPercentage, + PaymentRequestId = paymentRequest.Id, + StatusId = Processed, + PreApproved = false, + IsActive = true, + TenantId = tenantId + }; + + _context.Expenses.Add(expense); + + // Prepare ExpenseLog entries for each relevant previous status update log with reduced timestamp to preserve order + var millisecondsOffset = 60; + var expenseLogs = statusUpdateLogs + .Where(sul => expenseStatuses.Any(es => es.Id == sul.NextStatusId)) + .Select(sul => + { + var nextStatus = expenseStatuses.FirstOrDefault(es => es.Id == sul.NextStatusId); + var log = new ExpenseLog + { + ExpenseId = expense.Id, + Action = $"Status changed to '{nextStatus?.Name}'", + UpdatedById = loggedInEmployee.Id, + UpdateAt = DateTime.UtcNow.AddMilliseconds(millisecondsOffset), + Comment = $"Status changed to '{nextStatus?.Name}'", + TenantId = tenantId + }; + millisecondsOffset -= 1; + return log; + }).ToList(); + + _context.ExpenseLogs.AddRange(expenseLogs); + + // Process and upload bill attachments if present + if (hasAttachments) + { + await ProcessAndUploadAttachmentsAsync(model.BillAttachments!, expense, loggedInEmployee.Id, tenantId); + } + + // Mark the payment request as converted to expense to prevent duplicates + paymentRequest.IsExpenseCreated = true; + + // Persist all changes within transaction to ensure atomicity + await _context.SaveChangesAsync(); + + await transaction.CommitAsync(); + + _logger.LogInfo("Expense converted successfully from PaymentRequestId: {PaymentRequestId} by EmployeeId: {EmployeeId}", paymentRequest.Id, loggedInEmployee.Id); + + return ApiResponse.SuccessResponse(model, "Expense created successfully.", 201); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in ChangeToExpanseFromPaymentRequestAsync for PaymentRequestId: {PaymentRequestId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}: {Message}", + model.PaymentRequestId, tenantId, loggedInEmployee.Id, ex.Message); + await transaction.RollbackAsync(); + return ApiResponse.ErrorResponse("An error occurred while converting payment request to expense.", ex.Message, 500); + } + finally + { + _logger.LogInfo("End ChangeToExpanseFromPaymentRequestAsync called by EmployeeId: {EmployeeId}", loggedInEmployee.Id); + } + } #endregion #region =================================================================== Payment Request Functions =================================================================== diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs index 2045e30..7ed0b28 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs @@ -24,6 +24,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetPaymentRequestFilterObjectAsync(Employee loggedInEmployee, Guid tenantId); Task> CreatePaymentRequestAsync(PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId); Task> ChangePaymentRequestStatusAsync(PaymentRequestRecordDto model, Employee loggedInEmployee, Guid tenantId); + Task> ChangeToExpanseFromPaymentRequestAsync(ExpenseConversionDto model, Employee loggedInEmployee, Guid tenantId); #endregion #region =================================================================== Payment Request Functions ===================================================================