Removed the project forign key and able to create expense for both infra and service project

This commit is contained in:
ashutosh.nehete 2025-11-20 16:47:25 +05:30
parent df0e9f7b46
commit 9c95b12a8f
13 changed files with 9328 additions and 119 deletions

File diff suppressed because one or more lines are too long

View File

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

View File

@ -2329,8 +2329,6 @@ namespace Marco.Pms.DataAccess.Migrations
b.HasIndex("ProcessedById");
b.HasIndex("ProjectId");
b.HasIndex("ReviewedById");
b.HasIndex("StatusId");
@ -7378,12 +7376,6 @@ namespace Marco.Pms.DataAccess.Migrations
.WithMany()
.HasForeignKey("ProcessedById");
b.HasOne("Marco.Pms.Model.Projects.Project", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.Employees.Employee", "ReviewedBy")
.WithMany()
.HasForeignKey("ReviewedById");
@ -7416,8 +7408,6 @@ namespace Marco.Pms.DataAccess.Migrations
b.Navigation("ProcessedBy");
b.Navigation("Project");
b.Navigation("ReviewedBy");
b.Navigation("Status");

View File

@ -0,0 +1,12 @@
using Marco.Pms.Model.Utilities;
namespace Marco.Pms.Model.Dtos.ServiceProject
{
public class TalkingPointDto
{
public Guid? Id { get; set; }
public required Guid ServiceProjectId { get; set; }
public required string Comment { get; set; }
public List<FileUploadModel>? Attachments { get; set; }
}
}

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.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
@ -14,10 +13,6 @@ namespace Marco.Pms.Model.Expenses
public string UIDPrefix { get; set; } = default!;
public int UIDPostfix { get; set; }
public Guid ProjectId { get; set; }
[ValidateNever]
[ForeignKey("ProjectId")]
public Project? Project { get; set; }
public Guid ExpensesTypeId { get; set; }
//[ValidateNever]

View File

@ -0,0 +1,31 @@
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.ServiceProject
{
public class TalkingPoint : TenantRelation
{
public Guid Id { get; set; }
public Guid ServiceProjectId { get; set; }
[ValidateNever]
[ForeignKey("ServiceProjectId")]
public ServiceProject? ServiceProject { get; set; }
public string Comment { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; }
public Guid CreatedById { get; set; }
[ValidateNever]
[ForeignKey("CreatedById")]
public Employee? CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public Guid? UpdatedById { get; set; }
[ValidateNever]
[ForeignKey("UpdatedById")]
public Employee? UpdatedBy { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.ServiceProject
{
public class TalkingPointAttachment : TenantRelation
{
public Guid Id { get; set; }
public Guid DocumentId { get; set; }
[ValidateNever]
[ForeignKey("DocumentId")]
public Document? Document { get; set; }
public Guid? TalkingPointId { get; set; }
[ValidateNever]
[ForeignKey("TalkingPointId")]
public TalkingPoint? TalkingPoint { get; set; }
}
}

View File

@ -10,7 +10,7 @@ namespace Marco.Pms.Model.ViewModels.Expenses
public class ExpenseDetailsVM
{
public Guid Id { get; set; }
public ProjectInfoVM? Project { get; set; }
public BasicProjectVM? Project { get; set; }
public ExpenseCategoryMasterVM? ExpenseCategory { get; set; }
public PaymentModeMatserVM? PaymentMode { get; set; }
public BasicEmployeeVM? PaidBy { get; set; }

View File

@ -9,7 +9,7 @@ namespace Marco.Pms.Model.ViewModels.Expanses
public class ExpenseList
{
public Guid Id { get; set; }
public ProjectInfoVM? Project { get; set; }
public BasicProjectVM? Project { get; set; }
public ExpenseCategoryMasterVM? ExpenseCategory { get; set; }
public PaymentModeMatserVM? PaymentMode { get; set; }
public BasicEmployeeVM? PaidBy { get; set; }

View File

@ -0,0 +1,18 @@
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.DocumentManager;
namespace Marco.Pms.Model.ViewModels.ServiceProject
{
public class TalkingPointVM
{
public Guid Id { get; set; }
public BasicServiceProjectVM? ServiceProject { get; set; }
public string? Comment { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
public BasicEmployeeVM? CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public BasicEmployeeVM? UpdatedBy { get; set; }
public List<DocumentVM>? Attachments { get; set; }
}
}

View File

@ -1162,35 +1162,23 @@ namespace Marco.Pms.Services.Helpers
var processedByIds = model.Select(m => m.ProcessedById).ToList();
var paidByIds = model.Select(m => m.PaidById).ToList();
var projectTask = Task.Run(async () =>
var infraProjectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Projects.AsNoTracking().Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
return await dbContext.Projects
.AsNoTracking()
.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId)
.Select(p => _mapper.Map<ProjectBasicMongoDB>(p))
.ToListAsync();
});
var paidByTask = Task.Run(async () =>
var serviceProjectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => paidByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
});
var createdByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => createdByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
});
var reviewedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => reviewedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
});
var approvedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => approvedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
});
var processedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => processedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
return await dbContext.ServiceProjects
.AsNoTracking()
.Where(sp => projectIds.Contains(sp.Id) && sp.TenantId == tenantId)
.Select(sp => _mapper.Map<ProjectBasicMongoDB>(sp))
.ToListAsync();
});
var expenseCategoryTask = Task.Run(async () =>
{
@ -1202,6 +1190,15 @@ namespace Marco.Pms.Services.Helpers
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.PaymentModeMatser.AsNoTracking().Where(pm => paymentModeIds.Contains(pm.Id)).ToListAsync();
});
await Task.WhenAll(infraProjectTask, serviceProjectTask, expenseCategoryTask, paymentModeTask);
var projects = infraProjectTask.Result;
projects.AddRange(serviceProjectTask.Result);
var expenseCategories = expenseCategoryTask.Result;
var paymentModes = paymentModeTask.Result;
var statusMappingTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -1218,6 +1215,43 @@ namespace Marco.Pms.Services.Helpers
NextStatus = g.Select(s => s.NextStatus).OrderBy(s => s!.Name).ToList()
}).ToListAsync();
});
var paidByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => paidByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
});
var createdByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => createdByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
});
await Task.WhenAll(statusMappingTask, paidByTask, createdByTask);
var statusMappings = statusMappingTask.Result;
var paidBys = paidByTask.Result;
var createdBys = createdByTask.Result;
var reviewedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => reviewedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
});
var approvedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => approvedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
});
var processedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => processedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
});
await Task.WhenAll(reviewedByTask, approvedByTask, processedByTask);
var reviewedBys = reviewedByTask.Result;
var approvedBys = approvedByTask.Result;
var processedBy = processedByTask.Result;
var statusTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -1249,26 +1283,17 @@ namespace Marco.Pms.Services.Helpers
.ToListAsync();
});
// Await all prerequisite checks at once.
await Task.WhenAll(projectTask, expenseCategoryTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask,
processedByTask, statusTask, billAttachmentsTask);
var projects = projectTask.Result;
var expenseCategories = expenseCategoryTask.Result;
var paymentModes = paymentModeTask.Result;
var statusMappings = statusMappingTask.Result;
var paidBys = paidByTask.Result;
var createdBys = createdByTask.Result;
var reviewedBys = reviewedByTask.Result;
var approvedBys = approvedByTask.Result;
var processedBy = processedByTask.Result;
// Await all prerequisite checks at once.
await Task.WhenAll(statusTask, billAttachmentsTask);
var billAttachments = billAttachmentsTask.Result;
expenseList = model.Select(m =>
{
var response = _mapper.Map<ExpenseDetailsMongoDB>(m);
response.Project = projects.Where(p => p.Id == m.ProjectId).Select(p => _mapper.Map<ProjectBasicMongoDB>(p)).FirstOrDefault() ?? new ProjectBasicMongoDB();
response.Project = projects.Where(p => Guid.Parse(p.Id) == m.ProjectId).FirstOrDefault() ?? new ProjectBasicMongoDB();
response.PaidBy = paidBys.Where(p => p.Id == m.PaidById).Select(p => _mapper.Map<BasicEmployeeMongoDB>(p)).FirstOrDefault() ?? new BasicEmployeeMongoDB();
response.CreatedBy = createdBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map<BasicEmployeeMongoDB>(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB();
response.ReviewedBy = reviewedBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map<BasicEmployeeMongoDB>(e)).FirstOrDefault();
@ -1292,35 +1317,23 @@ namespace Marco.Pms.Services.Helpers
}
private async Task<ExpenseDetailsMongoDB> GetAllExpnesRelatedTablesForSingle(Expenses model, Guid tenantId)
{
var projectTask = Task.Run(async () =>
var infraProjectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
return await dbContext.Projects
.AsNoTracking()
.Where(p => p.Id == model.ProjectId && p.TenantId == tenantId)
.Select(p => _mapper.Map<ProjectBasicMongoDB>(p))
.FirstOrDefaultAsync();
});
var paidByTask = Task.Run(async () =>
var serviceProjectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.PaidById && e.TenantId == tenantId);
});
var createdByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.CreatedById && e.TenantId == tenantId);
});
var reviewedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ReviewedById && e.TenantId == tenantId);
});
var approvedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ApprovedById && e.TenantId == tenantId);
});
var processedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ProcessedById && e.TenantId == tenantId);
return await dbContext.ServiceProjects
.AsNoTracking()
.Where(sp => sp.Id == model.ProjectId && sp.TenantId == tenantId)
.Select(sp => _mapper.Map<ProjectBasicMongoDB>(sp))
.FirstOrDefaultAsync();
});
var expenseCategoryTask = Task.Run(async () =>
{
@ -1332,6 +1345,12 @@ namespace Marco.Pms.Services.Helpers
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == model.PaymentModeId);
});
await Task.WhenAll(infraProjectTask, serviceProjectTask, expenseCategoryTask, paymentModeTask);
var project = infraProjectTask.Result ?? serviceProjectTask.Result ?? new ProjectBasicMongoDB();
var expenseCategory = expenseCategoryTask.Result;
var paymentMode = paymentModeTask.Result;
var statusMappingTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -1348,6 +1367,43 @@ namespace Marco.Pms.Services.Helpers
NextStatus = g.Select(s => s.NextStatus).OrderBy(s => s!.Name).ToList()
}).FirstOrDefaultAsync();
});
var paidByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.PaidById && e.TenantId == tenantId);
});
var createdByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.CreatedById && e.TenantId == tenantId);
});
await Task.WhenAll(statusMappingTask, paidByTask, createdByTask);
var statusMapping = statusMappingTask.Result;
var paidBy = paidByTask.Result;
var createdBy = createdByTask.Result;
var reviewedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ReviewedById && e.TenantId == tenantId);
});
var approvedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ApprovedById && e.TenantId == tenantId);
});
var processedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ProcessedById && e.TenantId == tenantId);
});
await Task.WhenAll(reviewedByTask, approvedByTask, processedByTask);
var reviewedBy = reviewedByTask.Result;
var approvedBy = approvedByTask.Result;
var processedBy = processedByTask.Result;
var statusTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -1378,25 +1434,13 @@ namespace Marco.Pms.Services.Helpers
.FirstOrDefaultAsync();
});
// Await all prerequisite checks at once.
await Task.WhenAll(projectTask, expenseCategoryTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask,
processedByTask, statusTask, billAttachmentsTask);
var project = projectTask.Result;
var expenseCategory = expenseCategoryTask.Result;
var paymentMode = paymentModeTask.Result;
var statusMapping = statusMappingTask.Result;
var paidBy = paidByTask.Result;
var createdBy = createdByTask.Result;
var reviewedBy = reviewedByTask.Result;
var approvedBy = approvedByTask.Result;
var processedBy = processedByTask.Result;
await Task.WhenAll(statusTask, billAttachmentsTask);
var billAttachment = billAttachmentsTask.Result;
var response = _mapper.Map<ExpenseDetailsMongoDB>(model);
response.Project = _mapper.Map<ProjectBasicMongoDB>(project);
response.Project = project;
response.PaidBy = _mapper.Map<BasicEmployeeMongoDB>(paidBy);
response.CreatedBy = _mapper.Map<BasicEmployeeMongoDB>(createdBy);
response.ReviewedBy = _mapper.Map<BasicEmployeeMongoDB>(reviewedBy);

View File

@ -162,6 +162,10 @@ namespace Marco.Pms.Services.MappingProfiles
dest => dest.ProjectStatusId,
opt => opt.MapFrom(src => Guid.Empty)
);
CreateMap<ProjectBasicMongoDB, BasicProjectVM>()
.ForMember(
dest => dest.Id,
opt => opt.MapFrom(src => new Guid(src.Id)));
CreateMap<ProjectMongoDB, Project>()
.ForMember(
@ -197,6 +201,7 @@ namespace Marco.Pms.Services.MappingProfiles
CreateMap<ServiceProject, BasicProjectVM>();
CreateMap<ServiceProject, ServiceProjectVM>();
CreateMap<ServiceProject, BasicServiceProjectVM>();
CreateMap<ServiceProject, ProjectBasicMongoDB>();
CreateMap<ServiceProject, ServiceProjectDetailsVM>();
CreateMap<ServiceProjectAllocation, ServiceProjectAllocationVM>();

View File

@ -1,7 +1,6 @@
using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Helpers.Utility;
using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Dtos.Expenses;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
@ -25,8 +24,10 @@ using Marco.Pms.Model.ViewModels.Projects;
using Marco.Pms.Services.Helpers;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Service;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using Document = Marco.Pms.Model.DocumentManager.Document;
namespace Marco.Pms.Services.Service
{
@ -152,7 +153,6 @@ namespace Marco.Pms.Services.Service
.Include(e => e.ApprovedBy)
.Include(e => e.ReviewedBy)
.Include(e => e.PaymentMode)
.Include(e => e.Project)
.Include(e => e.PaymentMode)
.Include(e => e.ExpenseCategory)
.Include(e => e.PaymentRequest)
@ -245,6 +245,25 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200);
}
var projectIds = expensesList.Select(e => e.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);
//expenseVM = await GetAllExpnesRelatedTables(expensesList, tenantId);
expenseVM = expensesList.Select(e =>
{
@ -252,6 +271,7 @@ namespace Marco.Pms.Services.Service
result.ExpenseUId = $"{e.UIDPrefix}/{e.UIDPostfix:D5}";
if (e.PaymentRequest != null)
result.PaymentRequestUID = $"{e.PaymentRequest.UIDPrefix}/{e.PaymentRequest.UIDPostfix:D5}";
result.Project = projects.FirstOrDefault(p => p.Id == e.ProjectId);
return result;
}).ToList();
totalPages = (int)Math.Ceiling((double)totalEntites / pageSize);
@ -327,7 +347,6 @@ namespace Marco.Pms.Services.Service
.Include(e => e.ApprovedBy)
.Include(e => e.ReviewedBy)
.Include(e => e.PaymentMode)
.Include(e => e.Project)
.Include(e => e.PaymentMode)
.Include(e => e.ExpenseCategory)
.Include(e => e.Status)
@ -439,17 +458,42 @@ namespace Marco.Pms.Services.Service
{
var expenses = await _context.Expenses
.Include(e => e.PaidBy)
.Include(e => e.Project)
.Include(e => e.CreatedBy)
.Include(e => e.Status)
.Include(e => e.ExpenseCategory)
.Where(e => e.TenantId == tenantId)
.ToListAsync();
var projectIds = expenses.Select(e => e.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 => new { Id = p.Id, Name = p.Name })
.Distinct()
.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 => new { Id = sp.Id, Name = sp.Name }).Distinct()
.ToListAsync();
});
await Task.WhenAll(infraProjectTask, serviceProjectTask);
var projects = infraProjectTask.Result;
projects.AddRange(serviceProjectTask.Result);
// Construct the final object from the results of the completed tasks.
var response = new
{
Projects = expenses.Where(e => e.Project != null).Select(e => new { Id = e.Project!.Id, Name = e.Project.Name }).Distinct().ToList(),
Projects = projects,
PaidBy = expenses.Where(e => e.PaidBy != null).Select(e => new { Id = e.PaidBy!.Id, Name = $"{e.PaidBy.FirstName} {e.PaidBy.LastName}" }).Distinct().ToList(),
CreatedBy = expenses.Where(e => e.CreatedBy != null).Select(e => new { Id = e.CreatedBy!.Id, Name = $"{e.CreatedBy.FirstName} {e.CreatedBy.LastName}" }).Distinct().ToList(),
Status = expenses.Where(e => e.Status != null).Select(e => new { Id = e.Status!.Id, Name = e.Status.Name }).Distinct().ToList(),
@ -495,10 +539,23 @@ namespace Marco.Pms.Services.Service
// VALIDATION CHECKS: Use IDbContextFactory for thread-safe, parallel database queries.
// Each task gets its own DbContext instance.
var projectTask = Task.Run(async () =>
var infraProjectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == dto.ProjectId);
return await dbContext.Projects
.AsNoTracking()
.Where(p => p.Id == dto.ProjectId && p.TenantId == tenantId)
.Select(p => _mapper.Map<BasicProjectVM>(p))
.FirstOrDefaultAsync();
});
var serviceProjectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ServiceProjects
.AsNoTracking()
.Where(sp => sp.Id == dto.ProjectId && sp.TenantId == tenantId)
.Select(sp => _mapper.Map<BasicProjectVM>(sp))
.FirstOrDefaultAsync();
});
var paidByTask = Task.Run(async () =>
{
@ -534,7 +591,7 @@ namespace Marco.Pms.Services.Service
// Await all prerequisite checks at once.
await Task.WhenAll(hasUploadPermissionTask, projectTask, expenseCategoriesTask, paymentModeTask, statusMappingTask, paidByTask);
await Task.WhenAll(hasUploadPermissionTask, infraProjectTask, serviceProjectTask);
// 2. Aggregate and Check Results
if (!await hasUploadPermissionTask)
@ -544,7 +601,11 @@ namespace Marco.Pms.Services.Service
}
var validationErrors = new List<string>();
var project = await projectTask;
var infraProject = infraProjectTask.Result;
var serviceProject = serviceProjectTask.Result;
var project = infraProject ?? serviceProject;
await Task.WhenAll(expenseCategoriesTask, paymentModeTask, statusMappingTask, paidByTask);
var expenseCategory = await expenseCategoriesTask;
var paymentMode = await paymentModeTask;
var statusMapping = await statusMappingTask;
@ -590,7 +651,8 @@ namespace Marco.Pms.Services.Service
// 4. Process Attachments
if (dto.BillAttachments?.Any() ?? false)
{
await ProcessAndUploadAttachmentsAsync(dto.BillAttachments, expense, loggedInEmployee.Id, tenantId);
bool isServiceProject = serviceProject != null;
await ProcessAndUploadAttachmentsAsync(dto.BillAttachments, expense, isServiceProject, loggedInEmployee.Id, tenantId);
}
var expenseLog = new ExpenseLog
@ -616,7 +678,7 @@ namespace Marco.Pms.Services.Service
var response = _mapper.Map<ExpenseList>(expense);
response.ExpenseUId = $"{expense.UIDPrefix}/{expense.UIDPostfix:D5}";
response.PaidBy = _mapper.Map<BasicEmployeeVM>(paidBy);
response.Project = _mapper.Map<ProjectInfoVM>(project);
response.Project = project;
response.Status = _mapper.Map<ExpensesStatusMasterVM>(statusMapping!.Status);
response.NextStatus = _mapper.Map<List<ExpensesStatusMasterVM>>(statusMapping.NextStatus);
response.PaymentMode = _mapper.Map<PaymentModeMatserVM>(paymentMode);
@ -658,7 +720,6 @@ namespace Marco.Pms.Services.Service
// 1. Fetch Existing Expense with Related Entities (Single Query)
var expense = await _context.Expenses
.Include(e => e.ExpenseCategory)
.Include(e => e.Project)
.Include(e => e.PaidBy).ThenInclude(e => e!.JobRole)
.Include(e => e.PaymentMode)
.Include(e => e.Status)
@ -934,6 +995,29 @@ namespace Marco.Pms.Services.Service
if (nextPossibleStatuses is { Count: > 0 })
responseDto.NextStatus = _mapper.Map<List<ExpensesStatusMasterVM>>(nextPossibleStatuses);
var infraProjectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Projects
.AsNoTracking()
.Where(p => p.Id == expense.ProjectId && p.TenantId == tenantId)
.Select(p => _mapper.Map<BasicProjectVM>(p))
.FirstOrDefaultAsync();
});
var serviceProjectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ServiceProjects
.AsNoTracking()
.Where(sp => sp.Id == expense.ProjectId && sp.TenantId == tenantId)
.Select(sp => _mapper.Map<BasicProjectVM>(sp))
.FirstOrDefaultAsync();
});
await Task.WhenAll(infraProjectTask, serviceProjectTask);
responseDto.Project = infraProjectTask.Result ?? serviceProjectTask.Result;
return ApiResponse<object>.SuccessResponse(responseDto);
}
catch (Exception ex)
@ -969,7 +1053,6 @@ namespace Marco.Pms.Services.Service
var existingExpense = await _context.Expenses
.Include(e => e.ExpenseCategory)
.Include(e => e.Project)
.Include(e => e.PaidBy)
.ThenInclude(e => e!.JobRole)
.Include(e => e.PaymentMode)
@ -1017,12 +1100,37 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409);
}
var infraProjectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Projects
.AsNoTracking()
.Where(p => p.Id == existingExpense.ProjectId && p.TenantId == tenantId)
.Select(p => _mapper.Map<BasicProjectVM>(p))
.FirstOrDefaultAsync();
});
var serviceProjectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ServiceProjects
.AsNoTracking()
.Where(sp => sp.Id == existingExpense.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 (model.BillAttachments?.Any() ?? false)
{
var newBillAttachments = model.BillAttachments.Where(ba => ba.DocumentId == null && ba.IsActive).ToList();
if (newBillAttachments.Any())
{
await ProcessAndUploadAttachmentsAsync(newBillAttachments, existingExpense, loggedInEmployee.Id, tenantId);
bool isServiceProject = serviceProject != null;
await ProcessAndUploadAttachmentsAsync(newBillAttachments, existingExpense, isServiceProject, loggedInEmployee.Id, tenantId);
try
{
await _context.SaveChangesAsync();
@ -1102,6 +1210,9 @@ namespace Marco.Pms.Services.Service
response.NextStatus = _mapper.Map<List<ExpensesStatusMasterVM>>(nextPossibleStatuses);
}
response.Project = serviceProject ?? infraProject;
return ApiResponse<object>.SuccessResponse(response, "Expense Updated Successfully", 200);
}
catch (Exception ex)
@ -1883,9 +1994,31 @@ namespace Marco.Pms.Services.Service
.ToListAsync();
});
await Task.WhenAll(statusTransitionTask, targetStatusPermissionsTask);
var statusTransition = await statusTransitionTask;
var requiredPermissions = await targetStatusPermissionsTask;
var infraProjectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Projects
.AsNoTracking()
.Where(p => p.Id == paymentRequest.ProjectId && p.TenantId == tenantId)
.Select(p => _mapper.Map<BasicProjectVM>(p))
.FirstOrDefaultAsync();
});
var serviceProjectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ServiceProjects
.AsNoTracking()
.Where(sp => sp.Id == paymentRequest.ProjectId && sp.TenantId == tenantId)
.Select(sp => _mapper.Map<BasicProjectVM>(sp))
.FirstOrDefaultAsync();
});
await Task.WhenAll(infraProjectTask, serviceProjectTask, statusTransitionTask, targetStatusPermissionsTask);
var statusTransition = statusTransitionTask.Result;
var requiredPermissions = targetStatusPermissionsTask.Result;
var infraProject = infraProjectTask.Result;
var serviceProject = serviceProjectTask.Result;
// 3. Validate Transition and Required Fields
if (statusTransition == null)
@ -2055,7 +2188,10 @@ namespace Marco.Pms.Services.Service
GSTNumber = model.GSTNumber,
BillAttachments = model.BillAttachments
};
var response = await ChangeToExpanseFromPaymentRequestAsync(expenseConversion, paymentRequest, loggedInEmployee, tenantId);
bool isServiceProject = serviceProject != null;
var response = await ChangeToExpanseFromPaymentRequestAsync(expenseConversion, paymentRequest, isServiceProject, loggedInEmployee, tenantId);
if (!response.Success)
{
return response;
@ -2122,7 +2258,8 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.SuccessResponse(responseDto, "Status updated, but audit logging or cache update failed.");
}
}
public async Task<ApiResponse<object>> ChangeToExpanseFromPaymentRequestAsync(ExpenseConversionDto model, PaymentRequest paymentRequest, Employee loggedInEmployee, Guid tenantId)
public async Task<ApiResponse<object>> ChangeToExpanseFromPaymentRequestAsync(ExpenseConversionDto model, PaymentRequest paymentRequest, bool isServiceProject,
Employee loggedInEmployee, Guid tenantId)
{
_logger.LogInfo("Start ChangeToExpanseFromPaymentRequestAsync called by EmployeeId: {EmployeeId} for TenantId: {TenantId} PaymentRequestId: {PaymentRequestId}",
loggedInEmployee.Id, tenantId, model.PaymentRequestId);
@ -2259,7 +2396,7 @@ namespace Marco.Pms.Services.Service
// Process and upload bill attachments if present
if (hasAttachments)
{
await ProcessAndUploadAttachmentsAsync(model.BillAttachments!, expense, loggedInEmployee.Id, tenantId);
await ProcessAndUploadAttachmentsAsync(model.BillAttachments!, expense, isServiceProject, loggedInEmployee.Id, tenantId);
}
// Mark the payment request as converted to expense to prevent duplicates
@ -3490,19 +3627,31 @@ namespace Marco.Pms.Services.Service
.FirstOrDefaultAsync();
});
var infraProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects.Where(p => p.Id == model.ProjectId && p.TenantId == tenantId).Select(p => _mapper.Map<ProjectBasicMongoDB>(p)).FirstOrDefaultAsync();
});
var serviceProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceProjects.Where(sp => sp.Id == model.ProjectId && sp.TenantId == tenantId).Select(sp => _mapper.Map<ProjectBasicMongoDB>(sp)).FirstOrDefaultAsync();
});
// Await all prerequisite checks at once.
await Task.WhenAll(statusTask, billAttachmentsTask);
await Task.WhenAll(statusTask, billAttachmentsTask, infraProjectTask, serviceProjectTask);
var statusMapping = statusMappingTask.Result;
var billAttachment = billAttachmentsTask.Result;
var project = infraProjectTask.Result ?? serviceProjectTask.Result ?? new ProjectBasicMongoDB();
var response = _mapper.Map<ExpenseDetailsMongoDB>(model);
response.ExpenseUId = $"{model.UIDPrefix}/{model.UIDPostfix:D5}";
if (model.PaymentRequest != null)
response.PaymentRequestUID = $"{model.PaymentRequest.UIDPrefix}/{model.PaymentRequest.UIDPostfix:D5}";
response.Project = _mapper.Map<ProjectBasicMongoDB>(model.Project);
response.Project = project;
response.PaidBy = _mapper.Map<BasicEmployeeMongoDB>(model.PaidBy);
response.CreatedBy = _mapper.Map<BasicEmployeeMongoDB>(model.CreatedBy);
response.ReviewedBy = _mapper.Map<BasicEmployeeMongoDB>(model.ReviewedBy);
@ -3653,7 +3802,7 @@ namespace Marco.Pms.Services.Service
/// <summary>
/// Processes and uploads attachments concurrently, then adds the resulting entities to the main DbContext.
/// </summary>
private async Task ProcessAndUploadAttachmentsAsync(IEnumerable<FileUploadModel> attachments, Expenses expense, Guid employeeId, Guid tenantId)
private async Task ProcessAndUploadAttachmentsAsync(IEnumerable<FileUploadModel> attachments, Expenses expense, bool isServiceProject, Guid employeeId, Guid tenantId)
{
// Pre-validate all attachments to fail fast before any uploads.
foreach (var attachment in attachments)
@ -3668,7 +3817,7 @@ namespace Marco.Pms.Services.Service
// Create a list of tasks to be executed concurrently.
var processingTasks = attachments.Select(attachment =>
ProcessSingleExpenseAttachmentAsync(attachment, expense, employeeId, tenantId, batchId)
ProcessSingleExpenseAttachmentAsync(attachment, expense, employeeId, isServiceProject, tenantId, batchId)
).ToList();
var results = await Task.WhenAll(processingTasks);
@ -3686,12 +3835,17 @@ namespace Marco.Pms.Services.Service
/// Handles the logic for a single attachment: upload to S3 and create corresponding entities.
/// </summary>
private async Task<(Document document, BillAttachments billAttachment)> ProcessSingleExpenseAttachmentAsync(
FileUploadModel attachment, Expenses expense, Guid employeeId, Guid tenantId, Guid batchId)
FileUploadModel attachment, Expenses expense, Guid employeeId, bool isServiceProject, Guid tenantId, Guid batchId)
{
var base64Data = attachment.Base64Data!.Contains(',') ? attachment.Base64Data[(attachment.Base64Data.IndexOf(",") + 1)..] : attachment.Base64Data;
var fileType = _s3Service.GetContentTypeFromBase64(base64Data);
var fileName = _s3Service.GenerateFileName(fileType, expense.Id, "Expense");
var objectKey = $"tenant-{tenantId}/project-{expense.ProjectId}/Expenses/{fileName}";
var objectKey = $"tenant-{tenantId}/Project/{expense.ProjectId}/Expenses/{fileName}";
if (isServiceProject)
{
objectKey = $"tenant-{tenantId}/ServiceProject/{expense.ProjectId}/Expenses/{fileName}";
}
// Await the I/O-bound upload operation directly.
await _s3Service.UploadFileAsync(base64Data, fileType, objectKey);