Merge pull request 'Enhanced the Create and update project API' (#138) from Ashutosh_Enhancement#1347 into Organization_Management
Reviewed-on: #138
This commit is contained in:
commit
2258771229
@ -1,7 +1,9 @@
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Master;
|
||||
using Marco.Pms.Model.MongoDBModels;
|
||||
using Marco.Pms.Model.MongoDBModels.Masters;
|
||||
using Marco.Pms.Model.MongoDBModels.Project;
|
||||
using Marco.Pms.Model.OrganizationModel;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@ -55,7 +57,7 @@ namespace Marco.Pms.Helpers
|
||||
var indexModel = new CreateIndexModel<ProjectMongoDB>(indexKeys, indexOptions);
|
||||
await _projectCollection.Indexes.CreateOneAsync(indexModel);
|
||||
}
|
||||
public async Task<bool> UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus)
|
||||
public async Task<bool> UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus, Organization promotor, Organization pmc)
|
||||
{
|
||||
// Build the update definition
|
||||
var updates = Builders<ProjectMongoDB>.Update.Combine(
|
||||
@ -67,6 +69,20 @@ namespace Marco.Pms.Helpers
|
||||
Id = projectStatus.Id.ToString(),
|
||||
Status = projectStatus.Status
|
||||
}),
|
||||
Builders<ProjectMongoDB>.Update.Set(r => r.Promoter, new OrganizationMongoDB
|
||||
{
|
||||
Id = promotor.Id.ToString(),
|
||||
Name = promotor.Name,
|
||||
ContactPerson = promotor.ContactPerson,
|
||||
Email = promotor.Email
|
||||
}),
|
||||
Builders<ProjectMongoDB>.Update.Set(r => r.PMC, new OrganizationMongoDB
|
||||
{
|
||||
Id = pmc.Id.ToString(),
|
||||
Name = pmc.Name,
|
||||
ContactPerson = pmc.ContactPerson,
|
||||
Email = pmc.Email
|
||||
}),
|
||||
Builders<ProjectMongoDB>.Update.Set(r => r.StartDate, project.StartDate),
|
||||
Builders<ProjectMongoDB>.Update.Set(r => r.EndDate, project.EndDate),
|
||||
Builders<ProjectMongoDB>.Update.Set(r => r.ContactPerson, project.ContactPerson)
|
||||
|
@ -7,17 +7,18 @@ namespace Marco.Pms.Model.Dtos.Project
|
||||
{
|
||||
[Required(ErrorMessage = "Project Name is required!")]
|
||||
[DisplayName("Project Name")]
|
||||
public string? Name { get; set; }
|
||||
public required string Name { get; set; }
|
||||
|
||||
[DisplayName("Short Name")]
|
||||
public string? ShortName { get; set; }
|
||||
|
||||
[DisplayName("Project Address")]
|
||||
[Required(ErrorMessage = "Project Address is required!")]
|
||||
public string? ProjectAddress { get; set; }
|
||||
public required string ProjectAddress { get; set; }
|
||||
|
||||
|
||||
[DisplayName("Contact Person")]
|
||||
public string? ContactPerson { get; set; }
|
||||
public required string ContactPerson { get; set; }
|
||||
|
||||
|
||||
public DateTime? StartDate { get; set; }
|
||||
@ -25,6 +26,8 @@ namespace Marco.Pms.Model.Dtos.Project
|
||||
|
||||
[DisplayName("Project Status")]
|
||||
[Required(ErrorMessage = "Project Status is required!")]
|
||||
public Guid ProjectStatusId { get; set; }
|
||||
public required Guid ProjectStatusId { get; set; }
|
||||
public required Guid PromoterId { get; set; }
|
||||
public required Guid PMCId { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -5,20 +5,20 @@ namespace Marco.Pms.Model.Dtos.Project
|
||||
{
|
||||
public class UpdateProjectDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public required Guid Id { get; set; }
|
||||
[Required(ErrorMessage = "Project Name is required!")]
|
||||
[DisplayName("Project Name")]
|
||||
public string? Name { get; set; }
|
||||
public required string Name { get; set; }
|
||||
[DisplayName("Short Name")]
|
||||
public string? ShortName { get; set; }
|
||||
|
||||
[DisplayName("Project Address")]
|
||||
[Required(ErrorMessage = "Project Address is required!")]
|
||||
public string? ProjectAddress { get; set; }
|
||||
public required string ProjectAddress { get; set; }
|
||||
|
||||
|
||||
[DisplayName("Contact Person")]
|
||||
public string? ContactPerson { get; set; }
|
||||
public required string ContactPerson { get; set; }
|
||||
|
||||
|
||||
public DateTime? StartDate { get; set; }
|
||||
@ -26,6 +26,8 @@ namespace Marco.Pms.Model.Dtos.Project
|
||||
|
||||
[DisplayName("Project Status")]
|
||||
[Required(ErrorMessage = "Project Status is required!")]
|
||||
public Guid ProjectStatusId { get; set; }
|
||||
public required Guid ProjectStatusId { get; set; }
|
||||
public required Guid PromoterId { get; set; }
|
||||
public required Guid PMCId { get; set; }
|
||||
}
|
||||
}
|
||||
|
10
Marco.Pms.Model/MongoDBModels/OrganizationMongoDB.cs
Normal file
10
Marco.Pms.Model/MongoDBModels/OrganizationMongoDB.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Marco.Pms.Model.MongoDBModels
|
||||
{
|
||||
public class OrganizationMongoDB
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string ContactPerson { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
@ -13,6 +13,8 @@ namespace Marco.Pms.Model.MongoDBModels.Project
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
public StatusMasterMongoDB? ProjectStatus { get; set; }
|
||||
public OrganizationMongoDB? Promoter { get; set; }
|
||||
public OrganizationMongoDB? PMC { get; set; }
|
||||
public int TeamSize { get; set; }
|
||||
public double CompletedWork { get; set; }
|
||||
public double PlannedWork { get; set; }
|
||||
|
@ -12,6 +12,7 @@ namespace Marco.Pms.Model.ViewModels.Tenant
|
||||
public string ContactNumber { get; set; } = string.Empty;
|
||||
public string? logoImage { get; set; } // Base64
|
||||
public string? OrganizationSize { get; set; }
|
||||
public Guid OrganizationId { get; set; }
|
||||
public Industry? Industry { get; set; }
|
||||
public TenantStatus? TenantStatus { get; set; }
|
||||
}
|
||||
|
@ -1294,33 +1294,37 @@ namespace MarcoBMS.Services.Controllers
|
||||
_logger.LogInfo("Fetching TenantOrgMappings for OrganizationId: {OrganizationId}", organizationId);
|
||||
|
||||
// Retrieve all TenantOrgMappings that match the organizationId and have a related Tenant
|
||||
var tenantOrganizationMapping = await _context.TenantOrgMappings
|
||||
.Include(to => to.Tenant)
|
||||
.ThenInclude(t => t!.TenantStatus)
|
||||
.Include(to => to.Tenant)
|
||||
.ThenInclude(t => t!.Industry)
|
||||
.Where(to => to.OrganizationId == organizationId && to.Tenant != null)
|
||||
.ToListAsync();
|
||||
var tenantOrgMappingTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.TenantOrgMappings.Where(to => to.OrganizationId == organizationId && to.Tenant != null).Select(to => to.TenantId).ToListAsync();
|
||||
});
|
||||
var projectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Projects.Where(to => to.PromoterId == organizationId || to.PMCId == organizationId).Select(to => to.TenantId).ToListAsync();
|
||||
});
|
||||
|
||||
var tenantList = tenantOrganizationMapping.Select(to => to.Tenant!).ToList();
|
||||
await Task.WhenAll(tenantOrgMappingTask, projectTask);
|
||||
|
||||
var tenantIds = tenantOrgMappingTask.Result;
|
||||
|
||||
tenantIds.AddRange(projectTask.Result);
|
||||
|
||||
tenantIds = tenantIds.Distinct().ToList();
|
||||
|
||||
// Additionally fetch the Tenant record associated directly with this OrganizationId if any
|
||||
var tenant = await _context.Tenants
|
||||
var tenants = await _context.Tenants
|
||||
.Include(t => t.Industry)
|
||||
.Include(t => t.TenantStatus)
|
||||
.FirstOrDefaultAsync(t => t.OrganizationId == organizationId);
|
||||
if (tenant != null)
|
||||
{
|
||||
tenantList.Add(tenant);
|
||||
}
|
||||
.Where(t => t.OrganizationId == organizationId || tenantIds.Contains(t.Id)).ToListAsync();
|
||||
|
||||
|
||||
tenantList = tenantList.Distinct().ToList();
|
||||
tenants = tenants.Distinct().ToList();
|
||||
|
||||
// Map the tenant entities to TenantListVM view models
|
||||
var response = _mapper.Map<List<TenantListVM>>(tenantList);
|
||||
var response = _mapper.Map<List<TenantListVM>>(tenants);
|
||||
|
||||
_logger.LogInfo("Fetched {Count} tenants for OrganizationId: {OrganizationId}", tenantList.Count, organizationId);
|
||||
_logger.LogInfo("Fetched {Count} tenants for OrganizationId: {OrganizationId}", tenants.Count, organizationId);
|
||||
_logger.LogDebug("GetTenantAsync method completed successfully.");
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(response, "Successfully fetched the list of tenant", 200));
|
||||
|
@ -10,6 +10,7 @@ using Marco.Pms.Model.MongoDBModels.Expenses;
|
||||
using Marco.Pms.Model.MongoDBModels.Masters;
|
||||
using Marco.Pms.Model.MongoDBModels.Project;
|
||||
using Marco.Pms.Model.MongoDBModels.Utility;
|
||||
using Marco.Pms.Model.OrganizationModel;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using MarcoBMS.Services.Service;
|
||||
@ -69,6 +70,39 @@ namespace Marco.Pms.Services.Helpers
|
||||
.Where(s => s.Id == project.ProjectStatusId)
|
||||
.Select(s => new { s.Id, s.Status }) // Projection
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
});
|
||||
|
||||
var promotorTask = Task.Run(async () =>
|
||||
{
|
||||
using var context = _dbContextFactory.CreateDbContext();
|
||||
return await context.Organizations
|
||||
.AsNoTracking()
|
||||
.Where(o => o.Id == project.PromoterId)
|
||||
.Select(o => new OrganizationMongoDB
|
||||
{
|
||||
Id = o.Id.ToString(),
|
||||
Name = o.Name,
|
||||
ContactPerson = o.ContactPerson,
|
||||
Email = o.Email
|
||||
}) // Projection
|
||||
.FirstOrDefaultAsync();
|
||||
});
|
||||
|
||||
var pmcTask = Task.Run(async () =>
|
||||
{
|
||||
using var context = _dbContextFactory.CreateDbContext();
|
||||
return await context.Organizations
|
||||
.AsNoTracking()
|
||||
.Where(o => o.Id == project.PMCId)
|
||||
.Select(o => new OrganizationMongoDB
|
||||
{
|
||||
Id = o.Id.ToString(),
|
||||
Name = o.Name,
|
||||
ContactPerson = o.ContactPerson,
|
||||
Email = o.Email
|
||||
}) // Projection
|
||||
.FirstOrDefaultAsync();
|
||||
});
|
||||
|
||||
var teamSizeTask = Task.Run(async () =>
|
||||
@ -120,12 +154,15 @@ namespace Marco.Pms.Services.Helpers
|
||||
});
|
||||
|
||||
// Wait for all parallel database operations to complete.
|
||||
await Task.WhenAll(statusTask, teamSizeTask, infrastructureTask);
|
||||
await Task.WhenAll(statusTask, teamSizeTask, infrastructureTask, promotorTask, pmcTask);
|
||||
|
||||
// Get the results from the completed tasks.
|
||||
var status = await statusTask;
|
||||
var teamSize = await teamSizeTask;
|
||||
var (allBuildings, allFloors, allWorkAreas, workSummariesByWorkAreaId) = await infrastructureTask;
|
||||
|
||||
var status = statusTask.Result;
|
||||
var teamSize = teamSizeTask.Result;
|
||||
var (allBuildings, allFloors, allWorkAreas, workSummariesByWorkAreaId) = infrastructureTask.Result;
|
||||
var promotor = promotorTask.Result;
|
||||
var pmc = pmcTask.Result;
|
||||
|
||||
// --- Step 2: Process the fetched data and build the MongoDB model ---
|
||||
|
||||
@ -216,6 +253,8 @@ namespace Marco.Pms.Services.Helpers
|
||||
projectDetails.Buildings = buildingMongoList;
|
||||
projectDetails.PlannedWork = totalPlannedWork;
|
||||
projectDetails.CompletedWork = totalCompletedWork;
|
||||
projectDetails.Promoter = promotor;
|
||||
projectDetails.PMC = pmc;
|
||||
|
||||
try
|
||||
{
|
||||
@ -234,6 +273,8 @@ namespace Marco.Pms.Services.Helpers
|
||||
return; // Nothing to do
|
||||
}
|
||||
var projectStatusIds = projects.Select(p => p.ProjectStatusId).Distinct().ToList();
|
||||
var promotorIds = projects.Select(p => p.PromoterId).Distinct().ToList();
|
||||
var pmcsIds = projects.Select(p => p.PMCId).Distinct().ToList();
|
||||
|
||||
// --- Step 1: Fetch all required data in maximum parallel ---
|
||||
// Each task uses its own DbContext and selects only the required columns (projection).
|
||||
@ -248,6 +289,22 @@ namespace Marco.Pms.Services.Helpers
|
||||
.ToDictionaryAsync(s => s.Id);
|
||||
});
|
||||
|
||||
var organizationTask = Task.Run(async () =>
|
||||
{
|
||||
using var context = _dbContextFactory.CreateDbContext();
|
||||
return await context.Organizations
|
||||
.AsNoTracking()
|
||||
.Where(o => promotorIds.Contains(o.Id) || pmcsIds.Contains(o.Id))
|
||||
.Select(o => new OrganizationMongoDB
|
||||
{
|
||||
Id = o.Id.ToString(),
|
||||
Name = o.Name,
|
||||
ContactPerson = o.ContactPerson,
|
||||
Email = o.Email
|
||||
}) // Projection
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
var teamSizeTask = Task.Run(async () =>
|
||||
{
|
||||
using var context = _dbContextFactory.CreateDbContext();
|
||||
@ -322,20 +379,22 @@ namespace Marco.Pms.Services.Helpers
|
||||
});
|
||||
|
||||
// Await the remaining parallel tasks.
|
||||
await Task.WhenAll(statusTask, teamSizeTask, workAreasTask, workSummaryTask);
|
||||
await Task.WhenAll(statusTask, teamSizeTask, workAreasTask, workSummaryTask, organizationTask);
|
||||
|
||||
// --- Step 2: Process the fetched data and build the MongoDB models ---
|
||||
|
||||
var allStatuses = await statusTask;
|
||||
var teamSizesByProjectId = await teamSizeTask;
|
||||
var allWorkAreas = await workAreasTask;
|
||||
var workSummariesByWorkAreaId = await workSummaryTask;
|
||||
var allStatuses = statusTask.Result;
|
||||
var teamSizesByProjectId = teamSizeTask.Result;
|
||||
var allWorkAreas = workAreasTask.Result;
|
||||
var workSummariesByWorkAreaId = workSummaryTask.Result;
|
||||
var organizations = organizationTask.Result;
|
||||
|
||||
// Create fast in-memory lookups for hierarchical data
|
||||
var buildingsByProjectId = allBuildings.ToLookup(b => b.ProjectId);
|
||||
var floorsByBuildingId = allFloors.ToLookup(f => f.BuildingId);
|
||||
var workAreasByFloorId = allWorkAreas.ToLookup(wa => wa.FloorId);
|
||||
|
||||
|
||||
var projectDetailsList = new List<ProjectMongoDB>(projects.Count);
|
||||
foreach (var project in projects)
|
||||
{
|
||||
@ -427,6 +486,8 @@ namespace Marco.Pms.Services.Helpers
|
||||
projectDetails.Buildings = buildingMongoList;
|
||||
projectDetails.PlannedWork = totalPlannedWork;
|
||||
projectDetails.CompletedWork = totalCompletedWork;
|
||||
projectDetails.Promoter = organizations.FirstOrDefault(o => o.Id == project.PromoterId.ToString());
|
||||
projectDetails.PMC = organizations.FirstOrDefault(o => o.Id == project.PMCId.ToString());
|
||||
|
||||
projectDetailsList.Add(projectDetails);
|
||||
}
|
||||
@ -443,11 +504,32 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
public async Task<bool> UpdateProjectDetailsOnly(Project project)
|
||||
{
|
||||
StatusMaster projectStatus = await _context.StatusMasters
|
||||
var projectStatusTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.StatusMasters
|
||||
.FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId) ?? new StatusMaster();
|
||||
});
|
||||
var promotorTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == project.PromoterId) ?? new Organization();
|
||||
});
|
||||
var pmcTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == project.PMCId) ?? new Organization();
|
||||
});
|
||||
|
||||
await Task.WhenAll(projectStatusTask, promotorTask, pmcTask);
|
||||
|
||||
var projectStatus = projectStatusTask.Result;
|
||||
var promotor = promotorTask.Result;
|
||||
var pmc = pmcTask.Result;
|
||||
|
||||
try
|
||||
{
|
||||
bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project, projectStatus);
|
||||
bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project, projectStatus, promotor, pmc);
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -351,63 +351,110 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
#region =================================================================== Project Manage APIs ===================================================================
|
||||
|
||||
public async Task<ApiResponse<object>> CreateProjectAsync(CreateProjectDto projectDto, Guid tenantId, Employee loggedInEmployee)
|
||||
public async Task<ApiResponse<object>> CreateProjectAsync(CreateProjectDto model, Guid tenantId, Employee loggedInEmployee)
|
||||
{
|
||||
// Begin a new scope for service resolution.
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
|
||||
|
||||
// 1. Prepare data without I/O
|
||||
// Step 1: Validate tenant access for the current employee.
|
||||
var tenant = await _context.Tenants
|
||||
.FirstOrDefaultAsync(t => t.Id == tenantId && t.OrganizationId == loggedInEmployee.OrganizationId);
|
||||
|
||||
if (tenant == null)
|
||||
{
|
||||
_logger.LogWarning("Access DENIED (OrgId:{OrgId}) by Employee {EmployeeId} for tenantId={TenantId}.",
|
||||
loggedInEmployee.OrganizationId, loggedInEmployee.Id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to create a project for this tenant.", 403);
|
||||
}
|
||||
|
||||
// Step 2: Concurrent validation for Promoter and PMC organization existence.
|
||||
// Run database queries in parallel for better performance.
|
||||
var promoterExistsTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Organizations.AnyAsync(o => o.Id == model.PromoterId);
|
||||
});
|
||||
var pmcExistsTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Organizations.AnyAsync(o => o.Id == model.PMCId);
|
||||
});
|
||||
|
||||
await Task.WhenAll(promoterExistsTask, pmcExistsTask);
|
||||
|
||||
bool promoterExists = promoterExistsTask.Result;
|
||||
bool pmcExists = pmcExistsTask.Result;
|
||||
|
||||
if (!promoterExists)
|
||||
{
|
||||
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", model.PromoterId);
|
||||
return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404);
|
||||
}
|
||||
if (!pmcExists)
|
||||
{
|
||||
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", model.PMCId);
|
||||
return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404);
|
||||
}
|
||||
|
||||
// Step 3: Prepare the project entity.
|
||||
var loggedInUserId = loggedInEmployee.Id;
|
||||
var project = _mapper.Map<Project>(projectDto);
|
||||
var project = _mapper.Map<Project>(model);
|
||||
project.TenantId = tenantId;
|
||||
|
||||
// 2. Store it to database
|
||||
// Step 4: Save the new project to the database.
|
||||
try
|
||||
{
|
||||
_context.Projects.Add(project);
|
||||
await _context.SaveChangesAsync();
|
||||
_logger.LogInfo("Project {ProjectId} created successfully for TenantId={TenantId}, by Employee {EmployeeId}.",
|
||||
project.Id, tenantId, loggedInUserId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the detailed exception
|
||||
_logger.LogError(ex, "Failed to create project in database. Rolling back transaction.");
|
||||
// Return a server error as the primary operation failed
|
||||
_logger.LogError(ex, "DB Failure: Project creation failed for TenantId={TenantId}. Rolling back.", tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("An error occurred while saving the project.", ex.Message, 500);
|
||||
}
|
||||
|
||||
// 3. Perform non-critical side-effects (caching, notifications) concurrently
|
||||
// Step 5: Perform non-critical post-save side effects (e.g., caching) in parallel.
|
||||
try
|
||||
{
|
||||
// These operations do not depend on each other, so they can run in parallel.
|
||||
Task cacheAddDetailsTask = _cache.AddProjectDetails(project);
|
||||
Task cacheClearListTask = _cache.ClearAllProjectIdsByPermissionId(PermissionsMaster.ManageProject, tenantId);
|
||||
var cacheAddDetailsTask = _cache.AddProjectDetails(project);
|
||||
var cacheClearListTask = _cache.ClearAllProjectIdsByPermissionId(PermissionsMaster.ManageProject, tenantId);
|
||||
|
||||
// Await all side-effect tasks to complete in parallel
|
||||
await Task.WhenAll(cacheAddDetailsTask, cacheClearListTask);
|
||||
|
||||
_logger.LogInfo("Cache updated for ProjectId={ProjectId} after creation.", project.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// The project was created successfully, but a side-effect failed.
|
||||
// Log this as a warning, as the primary operation succeeded. Don't return an error to the user.
|
||||
_logger.LogError(ex, "Project {ProjectId} was created, but a post-creation side-effect (caching/notification) failed. ", project.Id);
|
||||
// Log the issue but do not interrupt the user flow.
|
||||
_logger.LogError(ex, "Post-save cache operation failed for ProjectId={ProjectId}.", project.Id);
|
||||
}
|
||||
|
||||
// Step 6: Fire-and-forget push notification for post-creation event. Designed to be non-blocking.
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// --- Push Notification Section ---
|
||||
// This section attempts to send a test push notification to the user's device.
|
||||
// It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens.
|
||||
|
||||
var name = $"{loggedInEmployee.FirstName} {loggedInEmployee.LastName}";
|
||||
|
||||
await _firebase.SendModifyProjectMessageAsync(project, name, false, tenantId);
|
||||
|
||||
try
|
||||
{
|
||||
var name = $"{loggedInEmployee.FirstName} {loggedInEmployee.LastName}";
|
||||
await _firebase.SendModifyProjectMessageAsync(project, name, false, tenantId);
|
||||
_logger.LogInfo("Push notification sent for ProjectId={ProjectId} ({Name}).", project.Id, name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Push notification sending failed for ProjectId={ProjectId}.", project.Id);
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Return a success response to the user as soon as the critical data is saved.
|
||||
return ApiResponse<object>.SuccessResponse(_mapper.Map<ProjectDto>(project), "Project created successfully.", 200);
|
||||
// Step 7: Return success response as soon as critical operation is complete.
|
||||
var resultDto = _mapper.Map<ProjectDto>(project);
|
||||
_logger.LogInfo("Returning success response for ProjectId={ProjectId}.", project.Id);
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(resultDto, "Project created successfully.", 200);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing project's details.
|
||||
/// This endpoint is secure, handles concurrency, and performs non-essential tasks in the background.
|
||||
@ -415,7 +462,7 @@ namespace Marco.Pms.Services.Service
|
||||
/// <param name="id">The ID of the project to update.</param>
|
||||
/// <param name="updateProjectDto">The data to update the project with.</param>
|
||||
/// <returns>An ApiResponse confirming the update or an appropriate error.</returns>
|
||||
public async Task<ApiResponse<object>> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, Guid tenantId, Employee loggedInEmployee)
|
||||
public async Task<ApiResponse<object>> UpdateProjectAsync(Guid id, UpdateProjectDto model, Guid tenantId, Employee loggedInEmployee)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
|
||||
@ -436,7 +483,46 @@ namespace Marco.Pms.Services.Service
|
||||
return ApiResponse<object>.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404);
|
||||
}
|
||||
|
||||
// 1b. Security Check
|
||||
var tenant = await _context.Tenants
|
||||
.FirstOrDefaultAsync(t => t.Id == tenantId && t.OrganizationId == loggedInEmployee.OrganizationId);
|
||||
|
||||
if (tenant == null && existingProject.PMCId == loggedInEmployee.OrganizationId)
|
||||
{
|
||||
_logger.LogWarning("Access DENIED (OrgId:{OrgId}) by Employee {EmployeeId} for tenantId={TenantId}.",
|
||||
loggedInEmployee.OrganizationId, loggedInEmployee.Id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to update a project for this tenant.", 403);
|
||||
}
|
||||
|
||||
// 1bb. Concurrent validation for Promoter and PMC organization existence.
|
||||
// Run database queries in parallel for better performance.
|
||||
var promoterExistsTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Organizations.AnyAsync(o => o.Id == model.PromoterId);
|
||||
});
|
||||
var pmcExistsTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Organizations.AnyAsync(o => o.Id == model.PMCId);
|
||||
});
|
||||
|
||||
await Task.WhenAll(promoterExistsTask, pmcExistsTask);
|
||||
|
||||
bool promoterExists = promoterExistsTask.Result;
|
||||
bool pmcExists = pmcExistsTask.Result;
|
||||
|
||||
if (!promoterExists)
|
||||
{
|
||||
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", model.PromoterId);
|
||||
return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404);
|
||||
}
|
||||
if (!pmcExists)
|
||||
{
|
||||
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", model.PMCId);
|
||||
return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404);
|
||||
}
|
||||
|
||||
// 1c. Security Check
|
||||
var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, id);
|
||||
if (!hasPermission)
|
||||
{
|
||||
@ -447,7 +533,7 @@ namespace Marco.Pms.Services.Service
|
||||
// --- Step 2: Apply Changes and Save ---
|
||||
// Map the changes from the DTO onto the entity we just fetched from the database.
|
||||
// This only modifies the properties defined in the mapping, preventing data loss.
|
||||
_mapper.Map(updateProjectDto, existingProject);
|
||||
_mapper.Map(model, existingProject);
|
||||
|
||||
// Mark the entity as modified (if your mapping doesn't do it automatically).
|
||||
_context.Entry(existingProject).State = EntityState.Modified;
|
||||
|
Loading…
x
Reference in New Issue
Block a user