Optimzed the add subscription API
This commit is contained in:
parent
1072a2da03
commit
7bf30d722b
@ -2,6 +2,7 @@
|
||||
{
|
||||
public class AttendanceDetailsDto
|
||||
{
|
||||
public List<Guid>? FeatureId { get; set; }
|
||||
public bool Enabled { get; set; } = false;
|
||||
public bool ManualEntry { get; set; } = true;
|
||||
public bool LocationTracking { get; set; } = true;
|
||||
|
@ -2,6 +2,7 @@
|
||||
{
|
||||
public class DirectoryDetailsDto
|
||||
{
|
||||
public List<Guid>? FeatureId { get; set; }
|
||||
public bool Enabled { get; set; } = false;
|
||||
public int BucketLimit { get; set; } = 25;
|
||||
public bool OrganizationChart { get; set; } = false;
|
||||
|
@ -2,6 +2,7 @@
|
||||
{
|
||||
public class ExpenseModuleDetailsDto
|
||||
{
|
||||
public List<Guid>? FeatureId { get; set; }
|
||||
public bool Enabled { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
{
|
||||
public class ProjectManagementDetailsDto
|
||||
{
|
||||
public List<Guid>? FeatureId { get; set; }
|
||||
public bool Enabled { get; set; } = false;
|
||||
public int MaxProject { get; set; } = 10;
|
||||
public double MaxTaskPerProject { get; set; } = 100000000;
|
||||
|
@ -7,7 +7,7 @@
|
||||
public required string Description { get; set; }
|
||||
public double PriceQuarterly { get; set; }
|
||||
public double PriceMonthly { get; set; }
|
||||
public double PriceHalfMonthly { get; set; }
|
||||
public double PriceHalfYearly { get; set; }
|
||||
public double PriceYearly { get; set; }
|
||||
public required int TrialDays { get; set; }
|
||||
public required double MaxUser { get; set; }
|
||||
|
@ -8,6 +8,9 @@ namespace Marco.Pms.Model.TenantModels.MongoDBModel
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.String)]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[BsonRepresentation(BsonType.String)]
|
||||
public List<Guid> FeatureId { get; set; } = new List<Guid>();
|
||||
public bool Enabled { get; set; } = false;
|
||||
public bool ManualEntry { get; set; } = true;
|
||||
public bool LocationTracking { get; set; } = true;
|
||||
|
@ -8,6 +8,9 @@ namespace Marco.Pms.Model.TenantModels.MongoDBModel
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.String)]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[BsonRepresentation(BsonType.String)]
|
||||
public List<Guid> FeatureId { get; set; } = new List<Guid>();
|
||||
public bool Enabled { get; set; } = false;
|
||||
public int BucketLimit { get; set; } = 25;
|
||||
public bool OrganizationChart { get; set; } = false;
|
||||
|
@ -8,6 +8,9 @@ namespace Marco.Pms.Model.TenantModels.MongoDBModel
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.String)]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[BsonRepresentation(BsonType.String)]
|
||||
public List<Guid> FeatureId { get; set; } = new List<Guid>();
|
||||
public bool Enabled { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,9 @@ namespace Marco.Pms.Model.TenantModels.MongoDBModel
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.String)]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[BsonRepresentation(BsonType.String)]
|
||||
public List<Guid> FeatureId { get; set; } = new List<Guid>();
|
||||
public bool Enabled { get; set; } = false;
|
||||
public int MaxProject { get; set; } = 10;
|
||||
public double MaxTaskPerProject { get; set; } = 100000000;
|
||||
|
@ -41,6 +41,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
private readonly static Guid activeStatus = Guid.Parse("62b05792-5115-4f99-8ff5-e8374859b191");
|
||||
private readonly static Guid activePlanStatus = Guid.Parse("cd3a68ea-41fd-42f0-bd0c-c871c7337727");
|
||||
private readonly static Guid EmployeeFeatureId = Guid.Parse("81ab8a87-8ccd-4015-a917-0627cee6a100");
|
||||
private readonly static string AdminRoleName = "Admin";
|
||||
public TenantController(IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
@ -461,60 +462,214 @@ namespace Marco.Pms.Services.Controllers
|
||||
#region =================================================================== Subscription APIs ===================================================================
|
||||
|
||||
[HttpPost("add-subscription")]
|
||||
public async Task<IActionResult> AddSubscription(AddSubscriptionDto model)
|
||||
public async Task<IActionResult> AddSubscriptionAsync(AddSubscriptionDto model)
|
||||
{
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
_logger.LogInfo("AddSubscription called by employee {EmployeeId} for Tenant {TenantId} and Plan {PlanId}",
|
||||
loggedInEmployee.Id, model.TenantId, model.PlanId);
|
||||
if (loggedInEmployee == null)
|
||||
{
|
||||
_logger.LogWarning("No logged-in employee found.");
|
||||
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "User must be logged in.", 401));
|
||||
}
|
||||
|
||||
await using var _context = await _dbContextFactory.CreateDbContextAsync();
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
|
||||
var _permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
|
||||
// A root user should have access regardless of the specific permission.
|
||||
var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false;
|
||||
var hasPermission = await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id);
|
||||
|
||||
if (!hasPermission || !isRootUser)
|
||||
if (!hasPermission && !isRootUser) // fixed logic here
|
||||
{
|
||||
_logger.LogWarning("Permission denied: User {EmployeeId} attempted to list tenants without 'ManageTenants' permission or root access.", loggedInEmployee.Id);
|
||||
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403));
|
||||
_logger.LogWarning("Permission denied: User {EmployeeId} attempted to add subscription without permission or root access.",
|
||||
loggedInEmployee.Id);
|
||||
|
||||
return StatusCode(403,
|
||||
ApiResponse<object>.ErrorResponse("Access denied",
|
||||
"User does not have the required permissions for this action.", 403));
|
||||
}
|
||||
|
||||
var subscriptionPlan = await _context.SubscriptionPlans.FirstOrDefaultAsync(sp => sp.Id == model.PlanId);
|
||||
if (subscriptionPlan == null)
|
||||
{
|
||||
_logger.LogWarning("Subscription plan {PlanId} not found in database", model.PlanId);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Subscription plan not found", "Subscription plan not found", 400));
|
||||
}
|
||||
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
// Prepare subscription dates based on frequency
|
||||
var endDate = model.Frequency switch
|
||||
{
|
||||
PLAN_FREQUENCY.MONTHLY => utcNow.AddMonths(1),
|
||||
PLAN_FREQUENCY.QUARTERLY => utcNow.AddMonths(3),
|
||||
PLAN_FREQUENCY.HALF_MONTHLY => utcNow.AddMonths(6),
|
||||
PLAN_FREQUENCY.YEARLY => utcNow.AddMonths(12),
|
||||
_ => utcNow.AddMonths(1) // default to monthly if unknown
|
||||
};
|
||||
|
||||
var tenantSubscription = new TenantSubscriptions
|
||||
{
|
||||
TenantId = model.TenantId,
|
||||
PlanId = model.PlanId,
|
||||
StatusId = activePlanStatus,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedAt = utcNow,
|
||||
CreatedById = loggedInEmployee.Id,
|
||||
CurrencyId = model.CurrencyId,
|
||||
IsTrial = model.IsTrial,
|
||||
StartDate = DateTime.UtcNow,
|
||||
StartDate = utcNow,
|
||||
EndDate = endDate,
|
||||
NextBillingDate = endDate,
|
||||
AutoRenew = model.AutoRenew
|
||||
};
|
||||
switch (model.Frequency)
|
||||
{
|
||||
case PLAN_FREQUENCY.MONTHLY:
|
||||
tenantSubscription.EndDate = DateTime.UtcNow.AddMonths(1);
|
||||
tenantSubscription.NextBillingDate = DateTime.UtcNow.AddMonths(1);
|
||||
break;
|
||||
case PLAN_FREQUENCY.QUARTERLY:
|
||||
tenantSubscription.EndDate = DateTime.UtcNow.AddMonths(3);
|
||||
tenantSubscription.NextBillingDate = DateTime.UtcNow.AddMonths(3);
|
||||
break;
|
||||
case PLAN_FREQUENCY.HALF_MONTHLY:
|
||||
tenantSubscription.EndDate = DateTime.UtcNow.AddMonths(6);
|
||||
tenantSubscription.NextBillingDate = DateTime.UtcNow.AddMonths(6);
|
||||
break;
|
||||
case PLAN_FREQUENCY.YEARLY:
|
||||
tenantSubscription.EndDate = DateTime.UtcNow.AddMonths(12);
|
||||
tenantSubscription.NextBillingDate = DateTime.UtcNow.AddMonths(12);
|
||||
break;
|
||||
}
|
||||
|
||||
_context.TenantSubscriptions.Add(tenantSubscription);
|
||||
await _context.SaveChangesAsync();
|
||||
return Ok(ApiResponse<object>.SuccessResponse(tenantSubscription, "Tenant Subscription Successfully", 200));
|
||||
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
_logger.LogInfo("Tenant subscription added successfully for Tenant {TenantId}, Plan {PlanId}",
|
||||
model.TenantId, model.PlanId);
|
||||
}
|
||||
catch (DbUpdateException dbEx)
|
||||
{
|
||||
_logger.LogError(dbEx, "Database exception while adding subscription plan to tenant {TenantId}", model.TenantId);
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error occured", ExceptionMapper(dbEx), 500));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId);
|
||||
if (features == null)
|
||||
{
|
||||
_logger.LogInfo("No features found for subscription plan {PlanId}", model.PlanId);
|
||||
await transaction.CommitAsync();
|
||||
return Ok(ApiResponse<object>.SuccessResponse(tenantSubscription, "Tenant subscription successfully added", 200));
|
||||
}
|
||||
|
||||
// Helper to get permissions for a module asynchronously
|
||||
async Task<List<Guid>> GetPermissionsForModuleAsync(List<Guid>? featureIds)
|
||||
{
|
||||
if (featureIds == null || featureIds.Count == 0) return new List<Guid>();
|
||||
|
||||
await using var ctx = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await ctx.FeaturePermissions.AsNoTracking()
|
||||
.Where(fp => featureIds.Contains(fp.FeatureId))
|
||||
.Select(fp => fp.Id)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
// Fetch permission tasks for all modules in parallel
|
||||
var projectPermissionTask = GetPermissionsForModuleAsync(features.Modules?.ProjectManagement?.FeatureId);
|
||||
var attendancePermissionTask = GetPermissionsForModuleAsync(features.Modules?.Attendance?.FeatureId);
|
||||
var directoryPermissionTask = GetPermissionsForModuleAsync(features.Modules?.Directory?.FeatureId);
|
||||
var expensePermissionTask = GetPermissionsForModuleAsync(features.Modules?.Expense?.FeatureId);
|
||||
var employeePermissionTask = GetPermissionsForModuleAsync(new List<Guid> { EmployeeFeatureId });
|
||||
|
||||
await Task.WhenAll(projectPermissionTask, attendancePermissionTask, directoryPermissionTask, expensePermissionTask, employeePermissionTask);
|
||||
|
||||
var newPermissionIds = new List<Guid>();
|
||||
var deletePermissionIds = new List<Guid>();
|
||||
|
||||
// Add or remove permissions based on modules enabled status
|
||||
void ProcessPermissions(bool? enabled, List<Guid> permissions)
|
||||
{
|
||||
if (enabled == true)
|
||||
newPermissionIds.AddRange(permissions);
|
||||
else
|
||||
deletePermissionIds.AddRange(permissions);
|
||||
}
|
||||
|
||||
ProcessPermissions(features.Modules?.ProjectManagement?.Enabled, projectPermissionTask.Result);
|
||||
ProcessPermissions(features.Modules?.Attendance?.Enabled, attendancePermissionTask.Result);
|
||||
ProcessPermissions(features.Modules?.Directory?.Enabled, directoryPermissionTask.Result);
|
||||
ProcessPermissions(features.Modules?.Expense?.Enabled, expensePermissionTask.Result);
|
||||
|
||||
newPermissionIds = newPermissionIds.Distinct().ToList();
|
||||
deletePermissionIds = deletePermissionIds.Distinct().ToList();
|
||||
|
||||
// Get root employee and role for this tenant
|
||||
var rootEmployee = await _context.Employees
|
||||
.Include(e => e.ApplicationUser)
|
||||
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.TenantId == model.TenantId);
|
||||
|
||||
if (rootEmployee == null)
|
||||
{
|
||||
_logger.LogWarning("Root employee not found for tenant {TenantId}", model.TenantId);
|
||||
await transaction.CommitAsync();
|
||||
return Ok(ApiResponse<object>.SuccessResponse(tenantSubscription, "Tenant subscription successfully added", 200));
|
||||
}
|
||||
|
||||
var roleId = await _context.EmployeeRoleMappings
|
||||
.AsNoTracking()
|
||||
.Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == model.TenantId)
|
||||
.Select(er => er.RoleId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (roleId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("RoleId for root employee {EmployeeId} in tenant {TenantId} not found", rootEmployee.Id, model.TenantId);
|
||||
await transaction.CommitAsync();
|
||||
return Ok(ApiResponse<object>.SuccessResponse(tenantSubscription, "Tenant subscription successfully added", 200));
|
||||
}
|
||||
|
||||
var oldRolePermissionMappings = await _context.RolePermissionMappings
|
||||
.Where(rp => rp.ApplicationRoleId == roleId)
|
||||
.ToListAsync();
|
||||
|
||||
var oldPermissionIds = oldRolePermissionMappings.Select(rp => rp.FeaturePermissionId).ToList();
|
||||
|
||||
// Prevent accidentally deleting essential employee permissions
|
||||
var permissionIdCount = oldPermissionIds.Count - deletePermissionIds.Count;
|
||||
if (permissionIdCount <= 4 && deletePermissionIds.Any())
|
||||
{
|
||||
var employeePermissionIds = employeePermissionTask.Result;
|
||||
deletePermissionIds = deletePermissionIds.Where(p => !employeePermissionIds.Contains(p)).ToList();
|
||||
}
|
||||
|
||||
// Prepare mappings to delete and add
|
||||
var deleteMappings = oldRolePermissionMappings.Where(rp => deletePermissionIds.Contains(rp.FeaturePermissionId)).ToList();
|
||||
var addRolePermissionMappings = newPermissionIds
|
||||
.Where(p => !oldPermissionIds.Contains(p))
|
||||
.Select(p => new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = roleId,
|
||||
FeaturePermissionId = p
|
||||
})
|
||||
.ToList();
|
||||
|
||||
if (addRolePermissionMappings.Any())
|
||||
{
|
||||
_context.RolePermissionMappings.AddRange(addRolePermissionMappings);
|
||||
_logger.LogInfo("Added {Count} new role permission mappings for role {RoleId}", addRolePermissionMappings.Count, roleId);
|
||||
}
|
||||
if (deleteMappings.Any())
|
||||
{
|
||||
_context.RolePermissionMappings.RemoveRange(deleteMappings);
|
||||
_logger.LogInfo("Removed {Count} role permission mappings for role {RoleId}", deleteMappings.Count, roleId);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
await transaction.CommitAsync();
|
||||
|
||||
_logger.LogInfo("Permissions updated successfully for tenant {TenantId} subscription", model.TenantId);
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(tenantSubscription, "Tenant Subscription Successfully", 200));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(ex, "Exception occurred while updating permissions for tenant {TenantId}", model.TenantId);
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error occured", ExceptionMapper(ex), 500));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Subscription Plan APIs ===================================================================
|
||||
@ -523,7 +678,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
public async Task<IActionResult> GetSubscriptionPlanList([FromQuery] int? frequency)
|
||||
{
|
||||
await using var _context = await _dbContextFactory.CreateDbContextAsync();
|
||||
var plans = await _context.SubscriptionPlans.Include(s => s.Currency).ToListAsync();
|
||||
var plans = await _context.SubscriptionPlans.Include(s => s.Currency).OrderBy(s => s.PriceHalfYearly).ToListAsync();
|
||||
|
||||
if (frequency == null)
|
||||
{
|
||||
@ -545,7 +700,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
response.Price = p.PriceMonthly;
|
||||
break;
|
||||
case 1:
|
||||
response.Price = p.PriceMonthly;
|
||||
response.Price = p.PriceQuarterly;
|
||||
break;
|
||||
case 2:
|
||||
response.Price = p.PriceHalfYearly;
|
||||
|
@ -81,10 +81,12 @@ string? connString = builder.Configuration.GetConnectionString("DefaultConnectio
|
||||
|
||||
// This single call correctly registers BOTH the DbContext (scoped) AND the IDbContextFactory (singleton).
|
||||
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
|
||||
options.UseMySql(connString, ServerVersion.AutoDetect(connString)));
|
||||
options.UseMySql(connString, ServerVersion.AutoDetect(connString))
|
||||
.EnableSensitiveDataLogging());
|
||||
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseMySql(connString, ServerVersion.AutoDetect(connString)));
|
||||
options.UseMySql(connString, ServerVersion.AutoDetect(connString))
|
||||
.EnableSensitiveDataLogging());
|
||||
|
||||
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
|
Loading…
x
Reference in New Issue
Block a user