Removed Project ForignKey From PaymentRequest Table

This commit is contained in:
ashutosh.nehete 2025-11-21 11:33:57 +05:30
parent 1b94592026
commit c7a73e78fb
5 changed files with 9173 additions and 201 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Removed_Project_ForignKey_From_RecurringPayment_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_RecurringPayments_Projects_ProjectId",
table: "RecurringPayments");
migrationBuilder.DropIndex(
name: "IX_RecurringPayments_ProjectId",
table: "RecurringPayments");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_RecurringPayments_ProjectId",
table: "RecurringPayments",
column: "ProjectId");
migrationBuilder.AddForeignKey(
name: "FK_RecurringPayments_Projects_ProjectId",
table: "RecurringPayments",
column: "ProjectId",
principalTable: "Projects",
principalColumn: "Id");
}
}
}

View File

@ -2922,8 +2922,6 @@ namespace Marco.Pms.DataAccess.Migrations
b.HasIndex("ExpenseCategoryId");
b.HasIndex("ProjectId");
b.HasIndex("StatusId");
b.HasIndex("TenantId");
@ -7612,10 +7610,6 @@ namespace Marco.Pms.DataAccess.Migrations
.WithMany()
.HasForeignKey("ExpenseCategoryId");
b.HasOne("Marco.Pms.Model.Projects.Project", "Project")
.WithMany()
.HasForeignKey("ProjectId");
b.HasOne("Marco.Pms.Model.Expenses.Masters.RecurringPaymentStatus", "Status")
.WithMany()
.HasForeignKey("StatusId")
@ -7638,8 +7632,6 @@ namespace Marco.Pms.DataAccess.Migrations
b.Navigation("ExpenseCategory");
b.Navigation("Project");
b.Navigation("Status");
b.Navigation("Tenant");

View File

@ -1,7 +1,6 @@
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Expenses.Masters;
using Marco.Pms.Model.Master;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.TenantModels;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
@ -29,10 +28,6 @@ namespace Marco.Pms.Model.Expenses
public DateTime? LatestPRGeneratedAt { get; set; }
public DateTime? NextStrikeDate { get; set; }
public Guid? ProjectId { get; set; }
[ValidateNever]
[ForeignKey("ProjectId")]
public Project? Project { get; set; }
public int PaymentBufferDays { get; set; }
public Guid? ExpenseCategoryId { get; set; }

View File

@ -110,23 +110,16 @@ namespace Marco.Pms.Services.Service
Guid loggedInEmployeeId = loggedInEmployee.Id;
List<ExpenseList> expenseVM = new List<ExpenseList>();
var totalEntites = 0;
var hasViewSelfPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseViewSelf, loggedInEmployeeId);
});
var hasViewAllPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseViewAll, loggedInEmployeeId);
});
var hasViewSelfPermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseViewSelf, loggedInEmployee.Id);
var hasViewAllPermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseViewAll, loggedInEmployee.Id);
await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask);
if (!hasViewAllPermissionTask.Result && !hasViewSelfPermissionTask.Result)
var hasViewAllPermission = hasViewAllPermissionTask.Result;
var hasViewSelfPermission = hasViewSelfPermissionTask.Result;
if (!hasViewAllPermission && !hasViewSelfPermission)
{
// User has neither required permission. Deny access.
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get expenses list.", loggedInEmployeeId);
@ -165,11 +158,11 @@ namespace Marco.Pms.Services.Service
//await _cache.AddExpensesListToCache(expenses: await expensesQuery.ToListAsync(), tenantId);
// Apply permission-based filtering BEFORE any other filters or pagination.
if (hasViewAllPermissionTask.Result)
if (hasViewAllPermission)
{
expensesQuery = expensesQuery.Where(e => e.CreatedById == loggedInEmployeeId || e.StatusId != Draft);
}
else if (hasViewSelfPermissionTask.Result)
else if (hasViewSelfPermission)
{
// User only has 'View Self' permission, so restrict the query to their own expenses.
_logger.LogInfo("User {EmployeeId} has 'View Self' permission. Restricting query to their expenses.", loggedInEmployeeId);
@ -367,11 +360,7 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Expense Not Found", "Expense Not Found", 404);
}
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var hasManagePermission = await permissionService.HasPermission(PermissionsMaster.ExpenseManage, loggedInEmployee.Id);
expenseDetails = await GetAllExpnesRelatedTablesForSingle(expense, hasManagePermission, loggedInEmployee.Id, expense.TenantId);
expenseDetails = await GetAllExpnesRelatedTablesForSingle(expense, loggedInEmployee.Id, expense.TenantId);
}
var vm = _mapper.Map<ExpenseDetailsVM>(expenseDetails);
@ -452,10 +441,13 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Databsae Exception", ExceptionMapper(dbEx), 500);
}
}
public async Task<ApiResponse<object>> GetFilterObjectAsync(Employee loggedInEmployee, Guid tenantId)
{
try
{
_logger.LogInfo("Fetching expenses for tenant: {TenantId}", tenantId);
var expenses = await _context.Expenses
.Include(e => e.PaidBy)
.Include(e => e.CreatedBy)
@ -464,6 +456,8 @@ namespace Marco.Pms.Services.Service
.Where(e => e.TenantId == tenantId)
.ToListAsync();
_logger.LogInfo("Fetched {ExpenseCount} expenses", expenses.Count);
var projectIds = expenses.Select(e => e.ProjectId).ToList();
var infraProjectTask = Task.Run(async () =>
@ -490,6 +484,8 @@ namespace Marco.Pms.Services.Service
var projects = infraProjectTask.Result;
projects.AddRange(serviceProjectTask.Result);
_logger.LogInfo("Fetched {ProjectCount} projects", projects.Count);
// Construct the final object from the results of the completed tasks.
var response = new
{
@ -499,12 +495,15 @@ namespace Marco.Pms.Services.Service
Status = expenses.Where(e => e.Status != null).Select(e => new { Id = e.Status!.Id, Name = e.Status.Name }).Distinct().ToList(),
ExpenseCategory = expenses.Where(e => e.ExpenseCategory != null).Select(e => new { Id = e.ExpenseCategory!.Id, Name = e.ExpenseCategory.Name }).Distinct().ToList()
};
_logger.LogInfo("Successfully fetched the filter list");
return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the filter list", 200);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occured while fetching the list filters for expenses");
return ApiResponse<object>.ErrorResponse("Internal Exception Occured", ExceptionMapper(ex), 500);
_logger.LogError(ex, "Exception occurred while fetching the list filters for expenses");
return ApiResponse<object>.ErrorResponse("Internal Exception Occurred", ExceptionMapper(ex), 500);
}
}
@ -529,13 +528,8 @@ namespace Marco.Pms.Services.Service
{
// 1. Authorization & Validation: Run all I/O-bound checks concurrently using factories for safety.
// PERMISSION CHECKS: Use IServiceScopeFactory for thread-safe access to scoped services.
var hasUploadPermissionTask = Task.Run(async () => // Task.Run is acceptable here to create a new scope, but let's do it cleaner.
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
});
// PERMISSION CHECKS
var hasUploadPermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
// VALIDATION CHECKS: Use IDbContextFactory for thread-safe, parallel database queries.
// Each task gets its own DbContext instance.
@ -798,10 +792,9 @@ namespace Marco.Pms.Services.Service
}
else if (requiredPermissions.Any())
{
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
foreach (var permission in requiredPermissions)
{
if (await permissionService.HasPermission(permission.PermissionId, loggedInEmployee.Id) && model.StatusId != Review)
if (await HasPermissionAsync(permission.PermissionId, loggedInEmployee.Id) && model.StatusId != Review)
{
hasPermission = true;
break;
@ -1046,10 +1039,8 @@ namespace Marco.Pms.Services.Service
"The employee Id in the path does not match the Id in the request body.", 400);
}
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var hasManagePermission = await permissionService.HasPermission(PermissionsMaster.ExpenseManage, loggedInEmployee.Id);
// Check if the employee has the required permission
var hasManagePermission = await HasPermissionAsync(PermissionsMaster.ExpenseManage, loggedInEmployee.Id);
var existingExpense = await _context.Expenses
.Include(e => e.ExpenseCategory)
@ -1242,12 +1233,7 @@ namespace Marco.Pms.Services.Service
return await context.Expenses.Where(e => e.Id == id && e.StatusId == Draft && e.TenantId == tenantId).FirstOrDefaultAsync();
});
var hasAprrovePermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseApprove, loggedInEmployee.Id);
});
var hasAprrovePermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseApprove, loggedInEmployee.Id);
await Task.WhenAll(expenseTask, hasAprrovePermissionTask);
@ -1344,23 +1330,16 @@ namespace Marco.Pms.Services.Service
try
{
var hasViewSelfPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseViewSelf, loggedInEmployee.Id);
});
var hasViewAllPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseViewAll, loggedInEmployee.Id);
});
// Check if the user has the required permission
var hasViewSelfPermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseViewSelf, loggedInEmployee.Id);
var hasViewAllPermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseViewAll, loggedInEmployee.Id);
await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask);
if (!hasViewAllPermissionTask.Result && !hasViewSelfPermissionTask.Result)
var hasViewAllPermission = hasViewAllPermissionTask.Result;
var hasViewSelfPermission = hasViewSelfPermissionTask.Result;
if (!hasViewAllPermission && !hasViewSelfPermission)
{
// User has neither required permission. Deny access.
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get payment request list.", loggedInEmployee.Id);
@ -1383,7 +1362,8 @@ namespace Marco.Pms.Services.Service
pr.CreatedBy != null &&
pr.CreatedBy.JobRole != null);
if (hasViewSelfPermissionTask.Result && !hasViewAllPermissionTask.Result)
// Filter the query based on the user's permissions
if (hasViewSelfPermission && !hasViewAllPermission)
{
paymentRequestQuery = paymentRequestQuery.Where(pr => pr.CreatedById == loggedInEmployee.Id);
}
@ -1393,6 +1373,7 @@ namespace Marco.Pms.Services.Service
// Deserialize and apply advanced filter if provided
PaymentRequestFilter? paymentRequestFilter = TryDeserializePaymentRequestFilter(filter);
// filter the query based on the advanced filter
if (paymentRequestFilter != null)
{
if (paymentRequestFilter.ProjectIds?.Any() ?? false)
@ -1456,10 +1437,11 @@ namespace Marco.Pms.Services.Service
.Take(pageSize)
.ToListAsync();
// Calculate total pages
var totalPages = (int)Math.Ceiling((double)totalEntities / pageSize);
// Fetch projects for each payment request
var projectIds = paymentRequests.Select(pr => pr.ProjectId).ToList();
var infraProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
@ -1477,6 +1459,7 @@ namespace Marco.Pms.Services.Service
var projects = infraProjectTask.Result;
projects.AddRange(serviceProjectTask.Result);
// mapping to view model
var results = paymentRequests.Select(pr =>
{
var result = _mapper.Map<PaymentRequestVM>(pr);
@ -1523,26 +1506,9 @@ namespace Marco.Pms.Services.Service
}
// Check user permissions concurrently
var hasViewSelfPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseViewSelf, loggedInEmployee.Id);
});
var hasViewAllPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseViewAll, loggedInEmployee.Id);
});
var hasReviewPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseReview, loggedInEmployee.Id);
});
var hasViewSelfPermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseViewSelf, loggedInEmployee.Id);
var hasViewAllPermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseViewAll, loggedInEmployee.Id);
var hasReviewPermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseReview, loggedInEmployee.Id);
await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask, hasReviewPermissionTask);
@ -1550,26 +1516,9 @@ namespace Marco.Pms.Services.Service
bool hasViewAllPermission = hasViewAllPermissionTask.Result;
bool hasReviewPermission = hasReviewPermissionTask.Result;
var hasApprovePermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseApprove, loggedInEmployee.Id);
});
var hasProcessPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseProcess, loggedInEmployee.Id);
});
var hasManagePermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseManage, loggedInEmployee.Id);
});
var hasApprovePermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseApprove, loggedInEmployee.Id);
var hasProcessPermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseProcess, loggedInEmployee.Id);
var hasManagePermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseManage, loggedInEmployee.Id);
await Task.WhenAll(hasApprovePermissionTask, hasProcessPermissionTask, hasManagePermissionTask);
@ -1581,7 +1530,7 @@ namespace Marco.Pms.Services.Service
if (!hasViewSelfPermission && !hasViewAllPermission && !hasReviewPermission && !hasApprovePermission && !hasProcessPermission)
{
_logger.LogWarning("Access DENIED: Employee {EmployeeId} has no permission to view payment requests.", loggedInEmployee.Id);
return ApiResponse<object>.SuccessResponse(new { }, "You do not have permission to view any payment request.", 200);
return ApiResponse<object>.ErrorResponse("You do not have permission to view payment requests.", "You do not have permission to view any payment request.", 403);
}
// Query payment request with all necessary navigation properties and validation constraints
@ -1864,9 +1813,7 @@ namespace Marco.Pms.Services.Service
try
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var hasUploadPermission = await permissionService.HasPermission(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
var hasUploadPermission = await HasPermissionAsync(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
if (!hasUploadPermission)
{
@ -1890,13 +1837,8 @@ namespace Marco.Pms.Services.Service
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);
await Task.WhenAll(expenseCategoryTask, currencyTask, expenseStatusTask);
var expenseCategory = await expenseCategoryTask;
if (expenseCategory == null)
@ -1919,8 +1861,42 @@ namespace Marco.Pms.Services.Service
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
BasicProjectVM? project = null;
if (model.ProjectId.HasValue && model.ProjectId.Value != Guid.Empty)
{
var infraProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects
.AsNoTracking()
.Where(p => p.Id == model.ProjectId && p.TenantId == tenantId)
.Select(p => _mapper.Map<BasicProjectVM>(p))
.FirstOrDefaultAsync();
});
var serviceProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceProjects
.AsNoTracking()
.Where(sp => sp.Id == model.ProjectId && sp.TenantId == tenantId)
.Select(sp => _mapper.Map<BasicProjectVM>(sp))
.FirstOrDefaultAsync();
});
await Task.WhenAll(infraProjectTask, serviceProjectTask);
var infraProject = infraProjectTask.Result;
var serviceProject = serviceProjectTask.Result;
if (infraProject == null && serviceProject == null)
{
_logger.LogWarning("Cannot proceed: Both infrastructure and service projects are not found.");
return ApiResponse<object>.ErrorResponse("Cannot proceed: Both infrastructure and service projects are not found.",
"Cannot proceed: Both infrastructure and service projects are not found.", 404);
}
project = infraProject ?? serviceProject;
}
// Generate unique UID postfix based on existing requests for the current prefix
var lastPR = await _context.PaymentRequests.Where(pr => pr.UIDPrefix == uIDPrefix)
@ -1999,7 +1975,7 @@ namespace Marco.Pms.Services.Service
response.Currency = currency;
response.ExpenseCategory = _mapper.Map<ExpenseCategoryMasterVM>(expenseCategory);
response.ExpenseStatus = _mapper.Map<ExpensesStatusMasterVM>(expenseStatus);
response.Project = _mapper.Map<BasicProjectVM>(project);
response.Project = project;
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
_logger.LogInfo("Payment request created successfully with UID: {PaymentRequestUID}", response.PaymentRequestUID);
@ -2517,12 +2493,7 @@ namespace Marco.Pms.Services.Service
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.CurrencyMaster.FirstOrDefaultAsync(c => c.Id == model.CurrencyId);
});
var hasManagePermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseManage, loggedInEmployee.Id);
});
var hasManagePermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseManage, loggedInEmployee.Id);
await Task.WhenAll(expenseCategoryTask, currencyTask, hasManagePermissionTask);
@ -2542,7 +2513,7 @@ namespace Marco.Pms.Services.Service
BasicProjectVM? project = null;
if (model.ProjectId.HasValue)
if (model.ProjectId.HasValue && model.ProjectId.Value != Guid.Empty)
{
var infraProjectTask = Task.Run(async () =>
{
@ -2746,12 +2717,7 @@ namespace Marco.Pms.Services.Service
return await context.PaymentRequests.AsNoTracking().Where(e => e.Id == id && e.ExpenseStatusId == Draft && e.TenantId == tenantId).FirstOrDefaultAsync();
});
var hasAprrovePermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseApprove, loggedInEmployee.Id);
});
var hasAprrovePermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseApprove, loggedInEmployee.Id);
await Task.WhenAll(paymentRequestTask, hasAprrovePermissionTask);
@ -2849,19 +2815,8 @@ namespace Marco.Pms.Services.Service
try
{
// Check permissions concurrently: view self and view all recurring payments
var hasViewSelfPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ViewSelfRecurring, loggedInEmployee.Id);
});
var hasViewAllPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ViewAllRecurring, loggedInEmployee.Id);
});
var hasViewSelfPermissionTask = HasPermissionAsync(PermissionsMaster.ViewSelfRecurring, loggedInEmployee.Id);
var hasViewAllPermissionTask = HasPermissionAsync(PermissionsMaster.ViewAllRecurring, loggedInEmployee.Id);
await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask);
@ -2880,7 +2835,6 @@ namespace Marco.Pms.Services.Service
.Include(rp => rp.Currency)
.Include(rp => rp.ExpenseCategory)
.Include(rp => rp.Status)
.Include(rp => rp.Project)
.Include(rp => rp.CreatedBy).ThenInclude(e => e!.JobRole)
.Where(rp => rp.TenantId == tenantId && rp.IsActive == isActive);
@ -2959,11 +2913,31 @@ namespace Marco.Pms.Services.Service
.Take(pageSize)
.ToListAsync();
var projectIds = recurringPayments.Select(pr => pr.ProjectId).ToList();
var infraProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).Select(p => _mapper.Map<BasicProjectVM>(p)).ToListAsync();
});
var serviceProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceProjects.Where(sp => projectIds.Contains(sp.Id) && sp.TenantId == tenantId).Select(sp => _mapper.Map<BasicProjectVM>(sp)).ToListAsync();
});
await Task.WhenAll(infraProjectTask, serviceProjectTask);
var projects = infraProjectTask.Result;
projects.AddRange(serviceProjectTask.Result);
// Map entities to view models and set recurring payment UID
var results = recurringPayments.Select(rp =>
{
var vm = _mapper.Map<RecurringPaymentVM>(rp);
vm.RecurringPaymentUId = $"{rp.UIDPrefix}/{rp.UIDPostfix:D5}";
vm.Project = projects.FirstOrDefault(p => p.Id == rp.ProjectId);
return vm;
}).ToList();
@ -3005,26 +2979,9 @@ namespace Marco.Pms.Services.Service
}
// Concurrent permission checks for view-self, view-all, and manage recurring payments
var hasViewSelfPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ViewSelfRecurring, loggedInEmployee.Id);
});
var hasViewAllPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ViewAllRecurring, loggedInEmployee.Id);
});
var hasManagePermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ManageRecurring, loggedInEmployee.Id);
});
var hasViewSelfPermissionTask = HasPermissionAsync(PermissionsMaster.ViewSelfRecurring, loggedInEmployee.Id);
var hasViewAllPermissionTask = HasPermissionAsync(PermissionsMaster.ViewAllRecurring, loggedInEmployee.Id);
var hasManagePermissionTask = HasPermissionAsync(PermissionsMaster.ManageRecurring, loggedInEmployee.Id);
await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask, hasManagePermissionTask);
@ -3042,7 +2999,6 @@ namespace Marco.Pms.Services.Service
// Query recurring payment by Id or UID with navigation and tenant checks
var recurringPayment = await _context.RecurringPayments
.Include(rp => rp.Currency)
.Include(rp => rp.Project)
.Include(rp => rp.ExpenseCategory)
.Include(rp => rp.Status)
.Include(rp => rp.CreatedBy).ThenInclude(e => e!.JobRole)
@ -3100,9 +3056,34 @@ namespace Marco.Pms.Services.Service
var employees = employeeTask.Result;
var paymentRequests = paymentRequestTask.Result;
var infraProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects
.AsNoTracking()
.Where(p => p.Id == recurringPayment.ProjectId && p.TenantId == tenantId)
.Select(p => _mapper.Map<BasicProjectVM>(p))
.FirstOrDefaultAsync();
});
var serviceProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceProjects
.AsNoTracking()
.Where(sp => sp.Id == recurringPayment.ProjectId && sp.TenantId == tenantId)
.Select(sp => _mapper.Map<BasicProjectVM>(sp))
.FirstOrDefaultAsync();
});
await Task.WhenAll(infraProjectTask, serviceProjectTask);
var infraProject = infraProjectTask.Result;
var serviceProject = serviceProjectTask.Result;
// Map main response DTO and enrich with notification employees and payment requests
var response = _mapper.Map<RecurringPaymentDetailsVM>(recurringPayment);
response.NotifyTo = _mapper.Map<List<BasicEmployeeVM>>(employees);
response.Project = infraProject ?? serviceProject;
response.PaymentRequests = paymentRequests;
_logger.LogInfo("Recurring payment details fetched successfully for RecurringPaymentId: {RecurringPaymentId} by EmployeeId: {EmployeeId}",
@ -3135,9 +3116,7 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("End date cannot be earlier than today.", "End date cannot be earlier than today.", 400);
}
// Check if user has permission to create recurring payment templates
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var hasPermission = await permissionService.HasPermission(PermissionsMaster.ManageRecurring, loggedInEmployee.Id);
var hasPermission = await HasPermissionAsync(PermissionsMaster.ManageRecurring, loggedInEmployee.Id);
if (!hasPermission)
{
@ -3164,13 +3143,7 @@ namespace Marco.Pms.Services.Service
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, recurringStatusTask, currencyTask, projectTask);
await Task.WhenAll(expenseCategoryTask, recurringStatusTask, currencyTask);
var expenseCategory = await expenseCategoryTask;
if (expenseCategory == null)
@ -3193,7 +3166,42 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Currency not found.", "Currency not found.", 404);
}
var project = await projectTask; // Optional, can be null
BasicProjectVM? project = null;
if (model.ProjectId.HasValue && model.ProjectId.Value != Guid.Empty)
{
var infraProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects
.AsNoTracking()
.Where(p => p.Id == model.ProjectId && p.TenantId == tenantId)
.Select(p => _mapper.Map<BasicProjectVM>(p))
.FirstOrDefaultAsync();
});
var serviceProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceProjects
.AsNoTracking()
.Where(sp => sp.Id == model.ProjectId && sp.TenantId == tenantId)
.Select(sp => _mapper.Map<BasicProjectVM>(sp))
.FirstOrDefaultAsync();
});
await Task.WhenAll(infraProjectTask, serviceProjectTask);
var infraProject = infraProjectTask.Result;
var serviceProject = serviceProjectTask.Result;
if (infraProject == null && serviceProject == null)
{
_logger.LogWarning("Cannot proceed: Both infrastructure and service projects are not found.");
return ApiResponse<object>.ErrorResponse("Cannot proceed: Both infrastructure and service projects are not found.",
"Cannot proceed: Both infrastructure and service projects are not found.", 404);
}
project = infraProject ?? serviceProject;
}
// Generate unique UID prefix and postfix per current month/year
string uIDPrefix = $"RP/{DateTime.Now:MMyy}";
@ -3224,7 +3232,7 @@ namespace Marco.Pms.Services.Service
response.ExpenseCategory = _mapper.Map<ExpenseCategoryMasterVM>(expenseCategory);
response.Status = recurringStatus;
response.Currency = currency;
response.Project = _mapper.Map<BasicProjectVM>(project);
response.Project = project;
_logger.LogInfo("Recurring Payment Template created successfully with UID: {RecurringPaymentUId} by EmployeeId: {EmployeeId}", response.RecurringPaymentUId, loggedInEmployee.Id);
@ -3387,7 +3395,6 @@ namespace Marco.Pms.Services.Service
.Include(rp => rp.Currency)
.Include(rp => rp.ExpenseCategory)
.Include(rp => rp.Status)
.Include(rp => rp.Project)
.AsNoTracking()
.Where(rp => newRecurringTemplateIds.Contains(rp.Id))
.ToListAsync();
@ -3395,11 +3402,35 @@ namespace Marco.Pms.Services.Service
_logger.LogInfo("{Count} payment requests created successfully from recurring payments by EmployeeId: {EmployeeId} for TenantId: {TenantId}",
paymentRequests.Count, loggedInEmployee.Id, tenantId);
var response = newRecurringPayments.Select(rp => new
var projectIds = recurringPayments.Select(pr => pr.ProjectId).ToList();
var infraProjectTask = Task.Run(async () =>
{
RecurringPayment = _mapper.Map<RecurringPaymentVM>(rp),
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).Select(p => _mapper.Map<BasicProjectVM>(p)).ToListAsync();
});
var serviceProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceProjects.Where(sp => projectIds.Contains(sp.Id) && sp.TenantId == tenantId).Select(sp => _mapper.Map<BasicProjectVM>(sp)).ToListAsync();
});
await Task.WhenAll(infraProjectTask, serviceProjectTask);
var projects = infraProjectTask.Result;
projects.AddRange(serviceProjectTask.Result);
var response = newRecurringPayments.Select(rp =>
{
var result = _mapper.Map<RecurringPaymentVM>(rp);
result.Project = projects.FirstOrDefault(p => p.Id == rp.ProjectId);
return new
{
RecurringPayment = result,
DueDate = DateTime.UtcNow.AddDays(rp.PaymentBufferDays),
Emails = rp.NotifyTo.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
};
}).ToList();
return ApiResponse<object>.SuccessResponse(response, $"{paymentRequests.Count} conversion(s) to payment request completed successfully.", 201);
@ -3450,9 +3481,7 @@ namespace Marco.Pms.Services.Service
}
// Permission check for managing recurring payments
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var hasPermission = await permissionService.HasPermission(PermissionsMaster.ManageRecurring, loggedInEmployee.Id);
var hasPermission = await HasPermissionAsync(PermissionsMaster.ManageRecurring, loggedInEmployee.Id);
if (!hasPermission)
{
@ -3479,13 +3508,7 @@ namespace Marco.Pms.Services.Service
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, recurringStatusTask, currencyTask, projectTask);
await Task.WhenAll(expenseCategoryTask, recurringStatusTask, currencyTask);
var expenseCategory = await expenseCategoryTask;
if (expenseCategory == null)
@ -3508,7 +3531,42 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Currency not found.", "Currency not found.", 404);
}
var project = await projectTask; // Optional
BasicProjectVM? project = null;
if (model.ProjectId.HasValue && model.ProjectId.Value != Guid.Empty)
{
var infraProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects
.AsNoTracking()
.Where(p => p.Id == model.ProjectId && p.TenantId == tenantId)
.Select(p => _mapper.Map<BasicProjectVM>(p))
.FirstOrDefaultAsync();
});
var serviceProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceProjects
.AsNoTracking()
.Where(sp => sp.Id == model.ProjectId && sp.TenantId == tenantId)
.Select(sp => _mapper.Map<BasicProjectVM>(sp))
.FirstOrDefaultAsync();
});
await Task.WhenAll(infraProjectTask, serviceProjectTask);
var infraProject = infraProjectTask.Result;
var serviceProject = serviceProjectTask.Result;
if (infraProject == null && serviceProject == null)
{
_logger.LogWarning("Cannot proceed: Both infrastructure and service projects are not found.");
return ApiResponse<object>.ErrorResponse("Cannot proceed: Both infrastructure and service projects are not found.",
"Cannot proceed: Both infrastructure and service projects are not found.", 404);
}
project = infraProject ?? serviceProject;
}
// Retrieve the existing recurring payment record for update
var recurringPayment = await _context.RecurringPayments
@ -3535,7 +3593,7 @@ namespace Marco.Pms.Services.Service
response.ExpenseCategory = _mapper.Map<ExpenseCategoryMasterVM>(expenseCategory);
response.Status = recurringStatus;
response.Currency = currency;
response.Project = _mapper.Map<BasicProjectVM>(project);
response.Project = project;
_logger.LogInfo("Recurring Payment Template updated successfully with UID: {RecurringPaymentUId} by EmployeeId: {EmployeeId}", response.RecurringPaymentUId, loggedInEmployee.Id);
@ -3665,6 +3723,12 @@ namespace Marco.Pms.Services.Service
#endregion
#region =================================================================== Helper Functions ===================================================================
private async Task<bool> HasPermissionAsync(Guid permission, Guid employeeId)
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(permission, employeeId);
}
private static object ExceptionMapper(Exception ex)
{
return new
@ -3680,7 +3744,7 @@ namespace Marco.Pms.Services.Service
}
};
}
private async Task<ExpenseDetailsMongoDB> GetAllExpnesRelatedTablesForSingle(Expenses model, bool hasManagePermission, Guid loggedInEmployeeId, Guid tenantId)
private async Task<ExpenseDetailsMongoDB> GetAllExpnesRelatedTablesForSingle(Expenses model, Guid loggedInEmployeeId, Guid tenantId)
{
var statusMappingTask = Task.Run(async () =>
{