From 83e8e8c7de37cdd637a7c8c578a327de8f347897 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 29 Oct 2025 17:25:20 +0530 Subject: [PATCH] Added the renew Subscription API --- .../Dtos/Tenant/RenewSubscriptionDto.cs | 8 + .../Controllers/TenantController.cs | 23 +- .../ServiceInterfaces/ITenantService.cs | 1 + Marco.Pms.Services/Service/TenantService.cs | 336 +++++++++++++++++- 4 files changed, 355 insertions(+), 13 deletions(-) create mode 100644 Marco.Pms.Model/Dtos/Tenant/RenewSubscriptionDto.cs diff --git a/Marco.Pms.Model/Dtos/Tenant/RenewSubscriptionDto.cs b/Marco.Pms.Model/Dtos/Tenant/RenewSubscriptionDto.cs new file mode 100644 index 0000000..f1a0c40 --- /dev/null +++ b/Marco.Pms.Model/Dtos/Tenant/RenewSubscriptionDto.cs @@ -0,0 +1,8 @@ +namespace Marco.Pms.Model.Dtos.Tenant +{ + public class RenewSubscriptionDto + { + public Guid PaymentDetailId { get; set; } + public Guid PlanId { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/TenantController.cs b/Marco.Pms.Services/Controllers/TenantController.cs index 9a2233a..a4739e4 100644 --- a/Marco.Pms.Services/Controllers/TenantController.cs +++ b/Marco.Pms.Services/Controllers/TenantController.cs @@ -1592,8 +1592,8 @@ namespace Marco.Pms.Services.Controllers { using var scope = _serviceScopeFactory.CreateScope(); var _tenantService = scope.ServiceProvider.GetRequiredService(); - var tenant = await _tenantService.CreateTenantAsync(model.TenantEnquireId, model.PaymentDetailId, model.PlanId); - return Ok(ApiResponse.SuccessResponse(tenant, "Tenant Registration Successfully", 201)); + var response = await _tenantService.CreateTenantAsync(model.TenantEnquireId, model.PaymentDetailId, model.PlanId); + return StatusCode(response.StatusCode, response); } catch (Exception ex) { @@ -1602,6 +1602,25 @@ namespace Marco.Pms.Services.Controllers } } + [HttpPut("renew/subscription")] + public async Task RenewSubscriptionAsync(RenewSubscriptionDto model) + { + try + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + using var scope = _serviceScopeFactory.CreateScope(); + var _tenantService = scope.ServiceProvider.GetRequiredService(); + var response = await _tenantService.RenewSubscriptionAsync(tenantId, loggedInEmployee.Id, model.PaymentDetailId, model.PlanId); + return StatusCode(response.StatusCode, response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while add renewing subscription"); + return StatusCode(500, ApiResponse.ErrorResponse("Error Occured while renewing subscription", "Error Occured while renewing subscription", 500)); + } + } + #endregion #region =================================================================== Subscription Plan APIs =================================================================== diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/ITenantService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/ITenantService.cs index 2070877..4a0de4a 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/ITenantService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/ITenantService.cs @@ -5,5 +5,6 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces public interface ITenantService { Task> CreateTenantAsync(Guid enquireId, Guid paymentDetailId, Guid planId); + Task> RenewSubscriptionAsync(Guid tenantId, Guid employeeId, Guid paymentDetailId, Guid planId); } } diff --git a/Marco.Pms.Services/Service/TenantService.cs b/Marco.Pms.Services/Service/TenantService.cs index f5aa6ef..e085f7b 100644 --- a/Marco.Pms.Services/Service/TenantService.cs +++ b/Marco.Pms.Services/Service/TenantService.cs @@ -14,7 +14,6 @@ using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Tenant; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service.ServiceInterfaces; -using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -29,15 +28,8 @@ namespace Marco.Pms.Services.Service private readonly ILoggingService _logger; private readonly UserManager _userManager; private readonly IMapper _mapper; - private readonly UserHelper _userHelper; private readonly FeatureDetailsHelper _featureDetailsHelper; - private readonly static Guid projectActiveStatus = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731"); - private readonly static Guid projectInProgressStatus = Guid.Parse("cdad86aa-8a56-4ff4-b633-9c629057dfef"); - private readonly static Guid projectOnHoldStatus = Guid.Parse("603e994b-a27f-4e5d-a251-f3d69b0498ba"); - private readonly static Guid projectInActiveStatus = Guid.Parse("ef1c356e-0fe0-42df-a5d3-8daee355492d"); - private readonly static Guid projectCompletedStatus = Guid.Parse("33deaef9-9af1-4f2a-b443-681ea0d04f81"); - private readonly static Guid tenantActiveStatus = 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"); @@ -47,7 +39,6 @@ namespace Marco.Pms.Services.Service ILoggingService logger, UserManager userManager, IMapper mapper, - UserHelper userHelper, FeatureDetailsHelper featureDetailsHelper) { _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); @@ -55,7 +46,6 @@ namespace Marco.Pms.Services.Service _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); - _userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper)); _featureDetailsHelper = featureDetailsHelper ?? throw new ArgumentNullException(nameof(featureDetailsHelper)); } @@ -333,7 +323,6 @@ namespace Marco.Pms.Services.Service return ApiResponse.ErrorResponse("An unexpected internal error occurred.", ExceptionMapper(ex), 500); } } - public async Task> AddSubscriptionAsync(Guid tenantId, Guid employeeId, Guid paymentDetailId, Guid planId) { @@ -554,6 +543,318 @@ namespace Marco.Pms.Services.Service return ApiResponse.ErrorResponse("Internal error occured", ExceptionMapper(ex), 500); } } + public async Task> RenewSubscriptionAsync(Guid tenantId, Guid employeeId, Guid paymentDetailId, Guid planId) + { + // 2. Create a new DbContext instance for this request. + await using var context = await _dbContextFactory.CreateDbContextAsync(); + + // 3. Get PermissionServices from DI inside a fresh scope (rarely needed, but retained for your design). + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + var _updateLogHelper = scope.ServiceProvider.GetRequiredService(); + + // 4. Check user permissions: must be both Root user and have ManageTenants permission. + var hasPermission = await permissionService.HasPermission(PermissionsMaster.ManageTenants, employeeId); + + if (!hasPermission) + { + _logger.LogWarning("Permission denied for EmployeeId={EmployeeId}. HasPermission: {HasPermission}", + employeeId, hasPermission); + return ApiResponse.ErrorResponse("Access denied", "User does not have the required permissions.", 403); + } + + // 5. Fetch Tenant, SubscriptionPlan, and TenantSubscription in parallel (efficiently). + var tenantTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(ctx => + ctx.Result.Tenants.AsNoTracking().FirstOrDefaultAsync(t => t.Id == tenantId)).Unwrap(); + + var planDetailsTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(ctx => + ctx.Result.SubscriptionPlanDetails.Include(sp => sp.Plan).AsNoTracking().FirstOrDefaultAsync(sp => sp.Id == planId)).Unwrap(); + + var currentSubscriptionTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(ctx => + ctx.Result.TenantSubscriptions.Include(ts => ts.Currency).AsNoTracking().FirstOrDefaultAsync(ts => ts.TenantId == tenantId && !ts.IsCancelled)).Unwrap(); + + var paymentDetailsTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(ctx => + ctx.Result.PaymentDetails.AsNoTracking().FirstOrDefaultAsync(pd => pd.Id == paymentDetailId)).Unwrap(); + + + await Task.WhenAll(tenantTask, planDetailsTask, currentSubscriptionTask, paymentDetailsTask); + + var tenant = tenantTask.Result; + if (tenant == null) + { + _logger.LogWarning("Tenant {TenantId} not found.", tenantId); + return ApiResponse.ErrorResponse("Tenant not found", "Tenant not found", 404); + } + + var subscriptionPlan = planDetailsTask.Result; + if (subscriptionPlan == null) + { + _logger.LogWarning("Subscription plan {PlanId} not found.", planId); + return ApiResponse.ErrorResponse("Subscription plan not found", "Subscription plan not found", 404); + } + + var paymentDetail = paymentDetailsTask.Result; + if (paymentDetail == null) + { + _logger.LogWarning("Payment details {PaymentDetailId} not found", paymentDetailId); + return ApiResponse.ErrorResponse("Payment details not found", "Payment details not found", 404); + } + + var currentSubscription = currentSubscriptionTask.Result; + var utcNow = DateTime.UtcNow; + + var activeUsers = await context.Employees.CountAsync(e => e.Email != null && e.ApplicationUserId != null && e.TenantId == tenant.Id && e.IsActive); + if (activeUsers > subscriptionPlan.MaxUser) + { + _logger.LogWarning("Employee {EmployeeId} add less max user than the active user in the tenant {TenantId}", employeeId, tenant.Id); + return ApiResponse.ErrorResponse("Invalid Max user count", "Max User count must be higher than active user count", 400); + } + + // 7. Else, change plan: new subscription record, close the old if exists. + await using var transaction = await context.Database.BeginTransactionAsync(); + try + { + // 7a. Compute new plan dates + var endDate = subscriptionPlan.Frequency switch + { + PLAN_FREQUENCY.MONTHLY => utcNow.AddDays(30), + PLAN_FREQUENCY.QUARTERLY => utcNow.AddDays(90), + PLAN_FREQUENCY.HALF_YEARLY => utcNow.AddDays(120), + PLAN_FREQUENCY.YEARLY => utcNow.AddDays(360), + _ => utcNow // default if unknown + }; + + var newSubscription = new TenantSubscriptions + { + TenantId = tenantId, + PlanId = planId, + StatusId = activePlanStatus, + CreatedAt = utcNow, + MaxUsers = subscriptionPlan.MaxUser, + CreatedById = employeeId, + CurrencyId = subscriptionPlan.CurrencyId, + PaymentDetailId = paymentDetailId, + StartDate = utcNow, + EndDate = endDate, + NextBillingDate = endDate, + IsTrial = currentSubscription?.IsTrial ?? false, + AutoRenew = currentSubscription?.AutoRenew ?? false + }; + context.TenantSubscriptions.Add(newSubscription); + + // 7b. If an old subscription exists, cancel it. + if (currentSubscription != null) + { + currentSubscription.IsCancelled = true; + currentSubscription.CancellationDate = utcNow; + currentSubscription.UpdateAt = utcNow; + currentSubscription.UpdatedById = employeeId; + context.TenantSubscriptions.Update(currentSubscription); + } + await context.SaveChangesAsync(); + _logger.LogInfo("Subscription plan changed: Tenant={TenantId}, NewPlan={PlanId}", + tenantId, planId); + + _ = Task.Run(async () => + { + await ClearPermissionForTenant(tenantId); + }); + + // 8. Update tenant permissions based on subscription features. + var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId); + if (features == null) + { + _logger.LogInfo("No features for Plan={PlanId}.", planId); + await transaction.CommitAsync(); + return ApiResponse.SuccessResponse(newSubscription, "Tenant subscription updated (no features)", 200); + } + + // 8a. Async helper to get all permission IDs for a given module. + async Task> GetPermissionsForFeaturesAsync(List? featureIds) + { + if (featureIds == null || featureIds.Count == 0) return new List(); + + await using var ctx = await _dbContextFactory.CreateDbContextAsync(); + return await ctx.FeaturePermissions.AsNoTracking() + .Where(fp => featureIds.Contains(fp.FeatureId)) + .Select(fp => fp.Id) + .ToListAsync(); + } + + // 8b. Fetch all module permissions concurrently. + var projectPermTask = GetPermissionsForFeaturesAsync(features.Modules?.ProjectManagement?.FeatureId); + var attendancePermTask = GetPermissionsForFeaturesAsync(features.Modules?.Attendance?.FeatureId); + var directoryPermTask = GetPermissionsForFeaturesAsync(features.Modules?.Directory?.FeatureId); + var expensePermTask = GetPermissionsForFeaturesAsync(features.Modules?.Expense?.FeatureId); + var employeePermTask = GetPermissionsForFeaturesAsync(new List { EmployeeFeatureId }); // assumed defined + + await Task.WhenAll(projectPermTask, attendancePermTask, directoryPermTask, expensePermTask, employeePermTask); + + // 8c. Find root employee & role for this tenant. + var rootEmployee = await context.Employees + .Include(e => e.ApplicationUser) + .FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.TenantId == tenantId); + + if (rootEmployee == null) + { + _logger.LogWarning("No root employee for Tenant={TenantId}.", tenantId); + await transaction.CommitAsync(); + return ApiResponse.SuccessResponse(newSubscription, "Tenant subscription updated (no root employee)", 200); + } + + var rootRoleId = await context.EmployeeRoleMappings + .AsNoTracking() + .Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == tenantId) + .Select(er => er.RoleId) + .FirstOrDefaultAsync(); + + if (rootRoleId == Guid.Empty) + { + _logger.LogWarning("No root role for Employee={EmployeeId}, Tenant={TenantId}.", rootEmployee.Id, tenantId); + await transaction.CommitAsync(); + return ApiResponse.SuccessResponse(newSubscription, "Tenant subscription updated (no root role)", 200); + } + + var dbOldRolePerms = await context.RolePermissionMappings.Where(x => x.ApplicationRoleId == rootRoleId).ToListAsync(); + var oldPermIds = dbOldRolePerms.Select(rp => rp.FeaturePermissionId).ToList(); + + // 8d. Prepare add and remove permission lists. + var newPermissionIds = new List(); + var revokePermissionIds = new List(); + var employeePerms = employeePermTask.Result; + var isOldEmployeePermissionIdExist = oldPermIds.Any(fp => employeePerms.Contains(fp)); + + void ProcessPerms(bool? enabled, List ids) + { + var isOldPermissionIdExist = oldPermIds.Any(fp => ids.Contains(fp) && !employeePerms.Contains(fp)); + + if (enabled == true && !isOldPermissionIdExist) newPermissionIds.AddRange(ids); + if (enabled == true && !isOldEmployeePermissionIdExist) newPermissionIds.AddRange(ids); + if (enabled == false && isOldPermissionIdExist) + revokePermissionIds.AddRange(ids); + } + ProcessPerms(features.Modules?.ProjectManagement?.Enabled, projectPermTask.Result); + ProcessPerms(features.Modules?.Attendance?.Enabled, attendancePermTask.Result); + ProcessPerms(features.Modules?.Directory?.Enabled, directoryPermTask.Result); + ProcessPerms(features.Modules?.Expense?.Enabled, expensePermTask.Result); + + newPermissionIds = newPermissionIds.Distinct().ToList(); + revokePermissionIds = revokePermissionIds.Distinct().ToList(); + + + // 8e. Prevent accidental loss of basic employee permissions. + if ((features.Modules?.ProjectManagement?.Enabled == true || + features.Modules?.Attendance?.Enabled == true || + features.Modules?.Directory?.Enabled == true || + features.Modules?.Expense?.Enabled == true) && isOldEmployeePermissionIdExist) + { + revokePermissionIds = revokePermissionIds.Where(pid => !employeePerms.Contains(pid)).ToList(); + } + + // 8f. Prepare permission-mapping records to add/remove. + var mappingsToRemove = dbOldRolePerms.Where(rp => revokePermissionIds.Contains(rp.FeaturePermissionId)).ToList(); + var mappingsToAdd = newPermissionIds + .Where(pid => !oldPermIds.Contains(pid)) + .Select(pid => new RolePermissionMappings { ApplicationRoleId = rootRoleId, FeaturePermissionId = pid }) + .ToList(); + + if (mappingsToAdd.Any()) + { + context.RolePermissionMappings.AddRange(mappingsToAdd); + _logger.LogInfo("Permissions granted: {Count} for Role={RoleId}", mappingsToAdd.Count, rootRoleId); + } + if (mappingsToRemove.Any()) + { + context.RolePermissionMappings.RemoveRange(mappingsToRemove); + _logger.LogInfo("Permissions revoked: {Count} for Role={RoleId}", mappingsToRemove.Count, rootRoleId); + } + + var _cache = scope.ServiceProvider.GetRequiredService(); + await _cache.ClearAllEmployeesFromCacheByTenantId(tenant.Id); + + var _masteData = scope.ServiceProvider.GetRequiredService(); + + if (features.Modules?.ProjectManagement?.Enabled ?? false) + { + var workCategoryMaster = _masteData.GetWorkCategoriesData(tenant.Id); + var workStatusMaster = _masteData.GetWorkStatusesData(tenant.Id); + + var workCategoryTask = Task.Run(async () => + { + await using var _context = await _dbContextFactory.CreateDbContextAsync(); + return await _context.WorkCategoryMasters.AnyAsync(wc => wc.IsSystem && wc.TenantId == tenant.Id); + }); + var workStatusTask = Task.Run(async () => + { + await using var _context = await _dbContextFactory.CreateDbContextAsync(); + return await _context.WorkStatusMasters.AnyAsync(ws => ws.IsSystem && ws.TenantId == tenant.Id); + }); + + await Task.WhenAll(workCategoryTask, workStatusTask); + + var workCategoryExist = workCategoryTask.Result; + var workStatusExist = workStatusTask.Result; + if (!workCategoryExist) + { + context.WorkCategoryMasters.AddRange(workCategoryMaster); + } + if (!workStatusExist) + { + context.WorkStatusMasters.AddRange(workStatusMaster); + } + } + if (features.Modules?.Expense?.Enabled ?? false) + { + var expensesTypeMaster = _masteData.GetExpensesTypeesData(tenant.Id); + var paymentModeMatser = _masteData.GetPaymentModesData(tenant.Id); + + var expensesTypeTask = Task.Run(async () => + { + await using var _context = await _dbContextFactory.CreateDbContextAsync(); + var expensesTypeNames = expensesTypeMaster.Select(et => et.Name).ToList(); + return await _context.ExpensesTypeMaster.AnyAsync(et => expensesTypeNames.Contains(et.Name) && et.TenantId == tenant.Id); + }); + var paymentModeTask = Task.Run(async () => + { + await using var _context = await _dbContextFactory.CreateDbContextAsync(); + var paymentModeNames = paymentModeMatser.Select(py => py.Name).ToList(); + return await _context.PaymentModeMatser.AnyAsync(py => paymentModeNames.Contains(py.Name) && py.TenantId == tenant.Id); + }); + + await Task.WhenAll(expensesTypeTask, paymentModeTask); + + var expensesTypeExist = expensesTypeTask.Result; + var paymentModeExist = paymentModeTask.Result; + if (!expensesTypeExist) + { + context.ExpensesTypeMaster.AddRange(expensesTypeMaster); + } + if (!paymentModeExist) + { + context.PaymentModeMatser.AddRange(paymentModeMatser); + } + } + + await context.SaveChangesAsync(); + await transaction.CommitAsync(); + + _logger.LogInfo("Tenant subscription and permissions updated: Tenant={TenantId}", tenantId); + + return ApiResponse.SuccessResponse(newSubscription, "Tenant subscription successfully updated", 200); + } + catch (DbUpdateException dbEx) + { + await transaction.RollbackAsync(); + _logger.LogError(dbEx, "Database exception updating subscription for TenantId={TenantId}", tenantId); + return ApiResponse.ErrorResponse("Internal database error", ExceptionMapper(dbEx), 500); + } + catch (Exception ex) + { + await transaction.RollbackAsync(); + _logger.LogError(ex, "General exception for TenantId={TenantId}", tenantId); + return ApiResponse.ErrorResponse("Internal error occurred", ExceptionMapper(ex), 500); + } + } #region =================================================================== Helper Functions =================================================================== private static object ExceptionMapper(Exception ex) @@ -677,6 +978,19 @@ namespace Marco.Pms.Services.Service return ApiResponse.SuccessResponse(VM, "Success", 200); } + private async Task ClearPermissionForTenant(Guid tenantId) + { + await using var _context = await _dbContextFactory.CreateDbContextAsync(); + using var scope = _serviceScopeFactory.CreateScope(); + + var _cache = scope.ServiceProvider.GetRequiredService(); + var _cacheLogger = scope.ServiceProvider.GetRequiredService(); + + var employeeIds = await _context.Employees.Where(e => e.TenantId == tenantId).Select(e => e.Id).ToListAsync(); + await _cache.ClearAllEmployeesFromCacheByEmployeeIds(employeeIds, tenantId); + _cacheLogger.LogInfo("{EmployeeCount} number of employee deleted", employeeIds.Count); + } + #endregion } }