Merge pull request 'Ashutosh_Task#1612' (#148) from Ashutosh_Task#1612 into Upgrade_Expense
Reviewed-on: #148
This commit is contained in:
commit
be0230c265
@ -147,6 +147,18 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
}
|
}
|
||||||
return StatusCode(response.StatusCode, response);
|
return StatusCode(response.StatusCode, response);
|
||||||
}
|
}
|
||||||
|
[HttpPut("payment-request/edit/{id}")]
|
||||||
|
public async Task<IActionResult> EditPaymentRequest(Guid id, [FromBody] PaymentRequestDto model)
|
||||||
|
{
|
||||||
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
var response = await _expensesService.EditPaymentRequestAsync(id, 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
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,6 +37,18 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
tenantId = userHelper.GetTenantId();
|
tenantId = userHelper.GetTenantId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region =================================================================== Currency APIs ===================================================================
|
||||||
|
|
||||||
|
[HttpGet("currencies/list")]
|
||||||
|
public async Task<IActionResult> GetCurrency()
|
||||||
|
{
|
||||||
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
var response = await _masterService.GetCurrencyAsync(loggedInEmployee, tenantId);
|
||||||
|
return StatusCode(response.StatusCode, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Organization Type APIs ===================================================================
|
#region =================================================================== Organization Type APIs ===================================================================
|
||||||
|
|
||||||
[HttpGet("organization-type/list")]
|
[HttpGet("organization-type/list")]
|
||||||
|
|||||||
@ -1292,6 +1292,197 @@ namespace Marco.Pms.Services.Service
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse<object>> EditPaymentRequestAsync(Guid id, PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId)
|
||||||
|
{
|
||||||
|
_logger.LogInfo("Start EditPaymentRequestAsync for PaymentRequestId: {PaymentRequestId}, EmployeeId: {EmployeeId}", id, loggedInEmployee.Id);
|
||||||
|
|
||||||
|
if (model.Id == null || id != model.Id)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Mismatch between URL id and payload id: {Id} vs {ModelId}", id, model?.Id ?? Guid.Empty);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Invalid argument: ID mismatch.", "Invalid argument: ID mismatch.", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Concurrently fetch related entities to validate input references
|
||||||
|
var expenseCategoryTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
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 projectTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return model.ProjectId.HasValue ? await context.Projects.FirstOrDefaultAsync(p => p.Id == model.ProjectId.Value) : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(expenseCategoryTask, currencyTask, 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 project = await projectTask; // Project can be null (optional)
|
||||||
|
|
||||||
|
// Retrieve the existing payment request with relevant navigation properties for validation and mapping
|
||||||
|
var paymentRequest = await _context.PaymentRequests
|
||||||
|
.Include(pr => pr.ExpenseCategory)
|
||||||
|
.Include(pr => pr.Project)
|
||||||
|
.Include(pr => pr.ExpenseStatus)
|
||||||
|
.Include(pr => pr.RecurringPayment)
|
||||||
|
.Include(pr => pr.Currency)
|
||||||
|
.Include(pr => pr.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||||
|
.Include(pr => pr.UpdatedBy).ThenInclude(e => e!.JobRole)
|
||||||
|
.FirstOrDefaultAsync(pr => pr.Id == id);
|
||||||
|
|
||||||
|
if (paymentRequest == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Payment Request not found with Id: {PaymentRequestId}", id);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Payment Request not found.", "Payment Request not found.", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if status prevents editing (only allow edit if status Draft, RejectedByReviewer, or RejectedByApprover)
|
||||||
|
bool statusCheck = paymentRequest.ExpenseStatusId != Draft &&
|
||||||
|
paymentRequest.ExpenseStatusId != RejectedByReviewer &&
|
||||||
|
paymentRequest.ExpenseStatusId != RejectedByApprover;
|
||||||
|
|
||||||
|
bool isVariableRecurring = paymentRequest.RecurringPayment?.IsVariable ?? false;
|
||||||
|
|
||||||
|
// Capture existing state for auditing
|
||||||
|
var existingEntityBson = _updateLogHelper.EntityToBsonDocument(paymentRequest);
|
||||||
|
|
||||||
|
// Only map updates if allowed by status
|
||||||
|
if (!statusCheck && paymentRequest.CreatedById == loggedInEmployee.Id)
|
||||||
|
{
|
||||||
|
_mapper.Map(model, paymentRequest);
|
||||||
|
paymentRequest.UpdatedAt = DateTime.UtcNow;
|
||||||
|
paymentRequest.UpdatedById = loggedInEmployee.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVariableRecurring)
|
||||||
|
{
|
||||||
|
paymentRequest.Amount = model.Amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentRequest.IsAdvancePayment = model.IsAdvancePayment;
|
||||||
|
|
||||||
|
// Handle bill attachment updates: add new attachments and delete deactivated ones
|
||||||
|
if (model.BillAttachments?.Any() == true)
|
||||||
|
{
|
||||||
|
var newBillAttachments = model.BillAttachments.Where(ba => ba.DocumentId == null && ba.IsActive).ToList();
|
||||||
|
if (newBillAttachments.Any())
|
||||||
|
{
|
||||||
|
_logger.LogInfo("Processing {AttachmentCount} new attachments for PaymentRequest Id: {PaymentRequestId}", newBillAttachments.Count, paymentRequest.Id);
|
||||||
|
|
||||||
|
// Pre-validate base64 data before upload
|
||||||
|
foreach (var attachment in newBillAttachments)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
|
||||||
|
var processingTasks = newBillAttachments.Select(attachment =>
|
||||||
|
ProcessSinglePaymentRequestAttachmentAsync(attachment, paymentRequest, loggedInEmployee.Id, tenantId, batchId)).ToList();
|
||||||
|
|
||||||
|
var results = await Task.WhenAll(processingTasks);
|
||||||
|
|
||||||
|
foreach (var (document, paymentRequestAttachment) in results)
|
||||||
|
{
|
||||||
|
_context.Documents.Add(document);
|
||||||
|
_context.PaymentRequestAttachments.Add(paymentRequestAttachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
_logger.LogInfo("Added {Count} new attachments for PaymentRequest {PaymentRequestId} by Employee {EmployeeId}", newBillAttachments.Count, paymentRequest.Id, loggedInEmployee.Id);
|
||||||
|
}
|
||||||
|
catch (DbUpdateException dbEx)
|
||||||
|
{
|
||||||
|
_logger.LogError(dbEx, "Database Exception while adding attachments during PaymentRequest update");
|
||||||
|
return ApiResponse<object>.ErrorResponse("Database exception during attachment addition.", ExceptionMapper(dbEx), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleteBillAttachments = model.BillAttachments.Where(ba => ba.DocumentId != null && !ba.IsActive).ToList();
|
||||||
|
if (deleteBillAttachments.Any())
|
||||||
|
{
|
||||||
|
var documentIds = deleteBillAttachments.Select(d => d.DocumentId!.Value).ToList();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DeletePaymentRequestAttachemnts(documentIds);
|
||||||
|
_logger.LogInfo("Deleted {Count} attachments for PaymentRequest {PaymentRequestId} by Employee {EmployeeId}", deleteBillAttachments.Count, paymentRequest.Id, loggedInEmployee.Id);
|
||||||
|
}
|
||||||
|
catch (DbUpdateException dbEx)
|
||||||
|
{
|
||||||
|
_logger.LogError(dbEx, "Database Exception while deleting attachments during PaymentRequest update");
|
||||||
|
return ApiResponse<object>.ErrorResponse("Database exception during attachment deletion.", ExceptionMapper(dbEx), 500);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Unexpected exception while deleting attachments during PaymentRequest update");
|
||||||
|
return ApiResponse<object>.ErrorResponse("Error occurred while deleting attachments.", ExceptionMapper(ex), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the update audit trail
|
||||||
|
await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||||
|
{
|
||||||
|
EntityId = paymentRequest.Id.ToString(),
|
||||||
|
UpdatedById = loggedInEmployee.Id.ToString(),
|
||||||
|
OldObject = existingEntityBson,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
}, "PaymentRequestModificationLog");
|
||||||
|
|
||||||
|
// Prepare response view model with updated details
|
||||||
|
var response = _mapper.Map<PaymentRequestVM>(paymentRequest);
|
||||||
|
response.PaymentRequestUID = $"{paymentRequest.UIDPrefix}/{paymentRequest.UIDPostfix:D5}";
|
||||||
|
response.Currency = currency;
|
||||||
|
response.ExpenseCategory = _mapper.Map<ExpensesTypeMasterVM>(expenseCategory);
|
||||||
|
response.Project = _mapper.Map<BasicProjectVM>(project);
|
||||||
|
response.UpdatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
|
||||||
|
|
||||||
|
_logger.LogInfo("PaymentRequest updated successfully with UID: {PaymentRequestUID}", response.PaymentRequestUID);
|
||||||
|
return ApiResponse<object>.SuccessResponse(response, "Payment Request updated successfully.", 200);
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Argument error in EditPaymentRequestAsync: {Message}", ex.Message);
|
||||||
|
return ApiResponse<object>.ErrorResponse(ex.Message, "Invalid data.", 400);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Unexpected error in EditPaymentRequestAsync: {Message}", ex.Message);
|
||||||
|
return ApiResponse<object>.ErrorResponse("An error occurred while updating the payment request.", ex.Message, 500);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_logger.LogInfo("End EditPaymentRequestAsync for PaymentRequestId: {PaymentRequestId}", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Payment Request Functions ===================================================================
|
#region =================================================================== Payment Request Functions ===================================================================
|
||||||
@ -1726,6 +1917,47 @@ namespace Marco.Pms.Services.Service
|
|||||||
|
|
||||||
await Task.WhenAll(attachmentTask, documentsTask);
|
await Task.WhenAll(attachmentTask, documentsTask);
|
||||||
}
|
}
|
||||||
|
private async Task DeletePaymentRequestAttachemnts(List<Guid> documentIds)
|
||||||
|
{
|
||||||
|
var attachmentTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
var attachments = await dbContext.PaymentRequestAttachments.AsNoTracking().Where(ba => documentIds.Contains(ba.DocumentId)).ToListAsync();
|
||||||
|
|
||||||
|
dbContext.PaymentRequestAttachments.RemoveRange(attachments);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
});
|
||||||
|
var documentsTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
var documents = await dbContext.Documents.AsNoTracking().Where(ba => documentIds.Contains(ba.Id)).ToListAsync();
|
||||||
|
|
||||||
|
if (documents.Any())
|
||||||
|
{
|
||||||
|
dbContext.Documents.RemoveRange(documents);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
List<S3DeletionObject> deletionObject = new List<S3DeletionObject>();
|
||||||
|
foreach (var document in documents)
|
||||||
|
{
|
||||||
|
deletionObject.Add(new S3DeletionObject
|
||||||
|
{
|
||||||
|
Key = document.S3Key
|
||||||
|
});
|
||||||
|
if (!string.IsNullOrWhiteSpace(document.ThumbS3Key) && document.ThumbS3Key != document.S3Key)
|
||||||
|
{
|
||||||
|
deletionObject.Add(new S3DeletionObject
|
||||||
|
{
|
||||||
|
Key = document.ThumbS3Key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await _updateLogHelper.PushToS3DeletionAsync(deletionObject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(attachmentTask, documentsTask);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,6 +53,30 @@ namespace Marco.Pms.Services.Service
|
|||||||
_updateLogHelper = updateLogHelper ?? throw new ArgumentNullException(nameof(updateLogHelper));
|
_updateLogHelper = updateLogHelper ?? throw new ArgumentNullException(nameof(updateLogHelper));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region =================================================================== Currency APIs ===================================================================
|
||||||
|
|
||||||
|
public async Task<ApiResponse<object>> GetCurrencyAsync(Employee loggedInEmployee, Guid tenantId)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("GetCurrencyAsync called");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Step 1: Fetch global currencies
|
||||||
|
var currencies = await _context.CurrencyMaster.OrderBy(ot => ot.CurrencyName).ToListAsync();
|
||||||
|
|
||||||
|
_logger.LogInfo("Fetched {Count} currency records for tenantId: {TenantId}", currencies.Count, tenantId);
|
||||||
|
|
||||||
|
return ApiResponse<object>.SuccessResponse(currencies, $"{currencies.Count} record(s) of currency fetched successfully", 200);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error fetching currency");
|
||||||
|
return ApiResponse<object>.ErrorResponse("An error occurred while fetching currency", ex.Message, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Organization Type APIs ===================================================================
|
#region =================================================================== Organization Type APIs ===================================================================
|
||||||
|
|
||||||
public async Task<ApiResponse<object>> GetOrganizationTypesAsync(Employee loggedInEmployee, Guid tenantId)
|
public async Task<ApiResponse<object>> GetOrganizationTypesAsync(Employee loggedInEmployee, Guid tenantId)
|
||||||
|
|||||||
@ -19,5 +19,6 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
|||||||
|
|
||||||
|
|
||||||
Task<ApiResponse<object>> CreatePaymentRequestAsync(PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId);
|
Task<ApiResponse<object>> CreatePaymentRequestAsync(PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId);
|
||||||
|
Task<ApiResponse<object>> EditPaymentRequestAsync(Guid id, PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,11 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
|||||||
{
|
{
|
||||||
public interface IMasterService
|
public interface IMasterService
|
||||||
{
|
{
|
||||||
|
#region =================================================================== Currency APIs ===================================================================
|
||||||
|
|
||||||
|
Task<ApiResponse<object>> GetCurrencyAsync(Employee loggedInEmployee, Guid tenantId);
|
||||||
|
|
||||||
|
#endregion
|
||||||
#region =================================================================== Organization Type APIs ===================================================================
|
#region =================================================================== Organization Type APIs ===================================================================
|
||||||
|
|
||||||
Task<ApiResponse<object>> GetOrganizationTypesAsync(Employee loggedInEmployee, Guid tenantId);
|
Task<ApiResponse<object>> GetOrganizationTypesAsync(Employee loggedInEmployee, Guid tenantId);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user