1393 lines
69 KiB
C#

using AutoMapper;
using Marco.Pms.CacheHelper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Tenant;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Roles;
using Marco.Pms.Model.TenantModel;
using Marco.Pms.Model.TenantModels;
using Marco.Pms.Model.TenantModels.MongoDBModel;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Tenant;
using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Net;
using System.Text.Json;
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
namespace Marco.Pms.Services.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class TenantController : ControllerBase
{
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ILoggingService _logger;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IMapper _mapper;
private readonly UserHelper _userHelper;
private readonly FeatureDetailsHelper _featureDetailsHelper;
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,
ILoggingService logger,
UserManager<ApplicationUser> userManager,
IMapper mapper,
UserHelper userHelper,
FeatureDetailsHelper featureDetailsHelper)
{
_dbContextFactory = dbContextFactory;
_serviceScopeFactory = serviceScopeFactory;
_logger = logger;
_userManager = userManager;
_mapper = mapper;
_userHelper = userHelper;
_featureDetailsHelper = featureDetailsHelper;
}
#region =================================================================== Tenant APIs ===================================================================
/// <summary>
/// Retrieves a paginated list of active tenants with optional filtering and searching.
/// </summary>
/// <param name="searchString">A string to search for across various tenant fields.</param>
/// <param name="filter">A JSON serialized string containing advanced filter criteria.</param>
/// <param name="pageSize">The number of records to return per page.</param>
/// <param name="pageNumber">The page number to retrieve.</param>
/// <returns>A paginated list of tenants matching the criteria.</returns>
[HttpGet("list")]
public async Task<IActionResult> GetTenantList([FromQuery] string? searchString, string? filter, int pageSize = 20, int pageNumber = 1)
{
using var scope = _serviceScopeFactory.CreateScope();
var _permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
_logger.LogInfo("Attempting to fetch tenant list with pageNumber: {PageNumber} and pageSize: {PageSize}", pageNumber, pageSize);
try
{
// --- 1. PERMISSION CHECK ---
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (loggedInEmployee == null)
{
// This case should be handled by the [Authorize] attribute.
// This check is a safeguard.
_logger.LogWarning("Authentication failed: No logged-in employee found.");
return StatusCode(403, ApiResponse<object>.ErrorResponse("Authentication required", "User is not logged in.", 403));
}
// 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)
{
_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));
}
// --- 2. QUERY CONSTRUCTION ---
// Using a DbContext from the factory ensures a fresh context for this request.
await using var _context = await _dbContextFactory.CreateDbContextAsync();
// Start with a base IQueryable. Filters will be appended to this.
var tenantQuery = _context.Tenants.Where(t => t.IsActive);
// Apply advanced filters from the JSON filter object.
var tenantFilter = TryDeserializeFilter(filter);
if (tenantFilter != null)
{
// Date range filtering
if (tenantFilter.StartDate.HasValue && tenantFilter.EndDate.HasValue)
{
// OPTIMIZATION: Avoid using .Date on the database column, as it can prevent index usage.
// This structure (`>= start` and `< end.AddDays(1)`) is index-friendly and correctly inclusive.
var endDateExclusive = tenantFilter.EndDate.Value.AddDays(1);
tenantQuery = tenantQuery.Where(t => t.OnBoardingDate >= tenantFilter.StartDate.Value && t.OnBoardingDate < endDateExclusive);
}
// List-based filtering
if (tenantFilter.IndustryIds?.Any() == true)
{
tenantQuery = tenantQuery.Where(t => t.IndustryId.HasValue && tenantFilter.IndustryIds.Contains(t.IndustryId.Value));
}
if (tenantFilter.References?.Any() == true)
{
tenantQuery = tenantQuery.Where(t => tenantFilter.References.Contains(t.Reference));
}
if (tenantFilter.TenantStatusIds?.Any() == true)
{
tenantQuery = tenantQuery.Where(t => tenantFilter.TenantStatusIds.Contains(t.TenantStatusId));
}
if (tenantFilter.CreatedByIds?.Any() == true)
{
tenantQuery = tenantQuery.Where(t => t.CreatedById.HasValue && tenantFilter.CreatedByIds.Contains(t.CreatedById.Value));
}
}
// Apply free-text search string.
if (!string.IsNullOrWhiteSpace(searchString))
{
// OPTIMIZATION: Do not use .ToLower() on the database columns (e.g., `t.Name.ToLower()`).
// This makes the query non-SARGable and kills performance by preventing index usage.
// This implementation relies on the database collation being case-insensitive (e.g., `SQL_Latin1_General_CP1_CI_AS` in SQL Server).
tenantQuery = tenantQuery.Where(t =>
t.Name.Contains(searchString) ||
t.ContactName.Contains(searchString) ||
t.Email.Contains(searchString) ||
t.ContactNumber.Contains(searchString) ||
t.BillingAddress.Contains(searchString) ||
(t.TaxId != null && t.TaxId.Contains(searchString)) ||
(t.Description != null && t.Description.Contains(searchString)) ||
(t.DomainName != null && t.DomainName.Contains(searchString))
);
}
// --- 3. PAGINATION AND EXECUTION ---
// First, get the total count of records for pagination metadata.
// This executes a separate, efficient `COUNT(*)` query.
int totalRecords = await tenantQuery.CountAsync();
int totalPages = (int)Math.Ceiling((double)totalRecords / pageSize);
_logger.LogInfo("Found {TotalRecords} total tenants matching the query.", totalRecords);
// Now, apply ordering and pagination to fetch only the data for the current page.
// This is efficient server-side pagination.
var tenantList = await tenantQuery
.Include(t => t.Industry) // Eager load related data to prevent N+1 queries.
.Include(t => t.TenantStatus)
.OrderByDescending(t => t.OnBoardingDate)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
// Map the entities to a ViewModel/DTO for the response.
var vm = _mapper.Map<List<TenantListVM>>(tenantList);
// --- 4. CONSTRUCT RESPONSE ---
var response = new
{
TotalCount = totalRecords,
TotalPages = totalPages,
CurrentPage = pageNumber,
PageSize = pageSize,
Filter = tenantFilter, // Return the applied filter for context on the client-side.
Data = vm
};
_logger.LogInfo("Successfully fetched {RecordCount} tenant records.", vm.Count);
return Ok(ApiResponse<object>.SuccessResponse(response, $"{totalRecords} records of tenants fetched successfully", 200));
}
catch (Exception ex)
{
// CRITICAL SECURITY FIX: Do not expose the exception details to the client.
// Log the full exception for debugging purposes.
_logger.LogError(ex, "An unhandled exception occurred while fetching the tenant list.");
// Return a generic 500 Internal Server Error response.
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", "An unexpected error prevented the request from completing.", 500));
}
}
// GET api/<TenantController>/5
[HttpGet("details/{id}")]
public async Task<IActionResult> GetDetails(Guid id)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
await using var _context = await _dbContextFactory.CreateDbContextAsync();
var manageTenantsTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var _permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id);
});
var modifyTenantTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var _permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await _permissionService.HasPermission(PermissionsMaster.ModifyTenant, loggedInEmployee.Id);
});
var viewTenantTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var _permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await _permissionService.HasPermission(PermissionsMaster.ViewTenant, loggedInEmployee.Id);
});
await Task.WhenAll(manageTenantsTask, modifyTenantTask, viewTenantTask);
var hasManageTenantsPermission = manageTenantsTask.Result;
var hasModifyTenantPermission = modifyTenantTask.Result;
var hasViewTenantPermission = viewTenantTask.Result;
if (!hasManageTenantsPermission && !hasModifyTenantPermission && !hasViewTenantPermission)
{
_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 tenant = await _context.Tenants
.Include(t => t.Industry)
.Include(t => t.TenantStatus)
.AsNoTracking()
.FirstOrDefaultAsync(t => t.Id == id);
if (tenant == null)
{
_logger.LogWarning("Tenant {TenantId} not found in database", id);
return NotFound(ApiResponse<object>.ErrorResponse("Tenant not found", "Tenant not found", 404));
}
var employeeTask = Task.Run(async () =>
{
await using var _dbContext = await _dbContextFactory.CreateDbContextAsync();
return await _dbContext.Employees.Include(e => e.ApplicationUser).AsNoTracking().Where(e => e.TenantId == tenant.Id).ToListAsync();
});
var createdByTask = Task.Run(async () =>
{
await using var _dbContext = await _dbContextFactory.CreateDbContextAsync();
return await _dbContext.Employees.AsNoTracking().Where(e => e.Id == tenant.CreatedById).Select(e => _mapper.Map<BasicEmployeeVM>(e)).FirstOrDefaultAsync();
});
var planTask = Task.Run(async () =>
{
await using var _dbContext = await _dbContextFactory.CreateDbContextAsync();
return await _dbContext.TenantSubscriptions.AsNoTracking().Where(ts => ts.TenantId == tenant.Id && !ts.IsCancelled && ts.Plan != null).FirstOrDefaultAsync();
});
var projectTask = Task.Run(async () =>
{
await using var _dbContext = await _dbContextFactory.CreateDbContextAsync();
return await _dbContext.Projects
.Include(p => p.ProjectStatus)
.AsNoTracking()
.Where(p => p.TenantId == tenant.Id)
.GroupBy(p => p.ProjectStatusId)
.Select(g => new
{
Status = g.Where(p => p.ProjectStatus != null && p.ProjectStatus.Id == g.Key).Select(p => p.ProjectStatus).FirstOrDefault(),
ProjectsCount = g.Where(p => p.ProjectStatusId == g.Key).Count()
})
.ToListAsync();
});
await Task.WhenAll(employeeTask, projectTask, planTask, createdByTask);
var employees = employeeTask.Result;
var projects = projectTask.Result;
var currentPlan = planTask.Result;
var createdBy = createdByTask.Result;
var activeEmployeesCount = employees.Where(e => e.IsActive).Count();
var inActiveEmployeesCount = employees.Where(e => !e.IsActive).Count();
var expiryDate = currentPlan?.EndDate;
var nextBillingDate = currentPlan?.NextBillingDate;
var response = _mapper.Map<TenantDetailsVM>(tenant);
response.ActiveEmployees = activeEmployeesCount;
response.InActiveEmployees = inActiveEmployeesCount;
response.Projects = projects;
response.ExpiryDate = expiryDate;
response.NextBillingDate = nextBillingDate;
response.CreatedBy = createdBy;
return Ok();
}
// POST api/<TenantController>
[HttpPost("create")]
public async Task<IActionResult> CreateTenant([FromBody] CreateTenantDto model)
{
using var scope = _serviceScopeFactory.CreateScope();
await using var _context = await _dbContextFactory.CreateDbContextAsync();
var _configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
var _emailSender = scope.ServiceProvider.GetRequiredService<IEmailSender>();
var _permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
_logger.LogInfo("Attempting to create a new tenant with organization name: {OrganizationName}", model.OrganizationName);
// 1. --- PERMISSION CHECK ---
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (loggedInEmployee == null)
{
// This case should ideally be handled by an [Authorize] attribute, but it's good practice to double-check.
return Unauthorized(ApiResponse<object>.ErrorResponse("Authentication required", "User is not logged in.", 401));
}
var hasPermission = await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id);
if (!hasPermission || !(loggedInEmployee.ApplicationUser?.IsRootUser ?? false))
{
_logger.LogWarning("Permission denied: User {EmployeeId} attempted to create a tenant without sufficient rights.", loggedInEmployee.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403));
}
// 2. --- VALIDATION ---
// Check if a user with the same email already exists.
var existingUser = await _userManager.FindByEmailAsync(model.Email);
if (existingUser != null)
{
_logger.LogWarning("Tenant creation failed for email {Email}: an application user with this email already exists.", model.Email);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Tenant cannot be created", "A user with the specified email already exists.", 409));
}
// Check if a tenant with the same Tax ID and Domain Name already exists.
var taxTask = Task.Run(async () =>
{
if (!string.IsNullOrWhiteSpace(model.TaxId))
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Tenants.AnyAsync(t => t.TaxId == model.TaxId);
}
return false;
});
var domainTask = Task.Run(async () =>
{
if (!string.IsNullOrWhiteSpace(model.DomainName))
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Tenants.AnyAsync(t => t.DomainName == model.DomainName);
}
return false;
});
await Task.WhenAll(taxTask, domainTask);
if (taxTask.Result || domainTask.Result)
{
if (!string.IsNullOrWhiteSpace(model.TaxId))
{
_logger.LogWarning("Tenant creation failed for Tax ID {TaxId}: a tenant with this Tax ID already exists.", model.TaxId);
}
if (!string.IsNullOrWhiteSpace(model.DomainName))
{
_logger.LogWarning("Tenant creation failed for Domain Name {DomainName}: a tenant with this Domain Name already exists.", model.DomainName);
}
return StatusCode(409, ApiResponse<object>.ErrorResponse("Tenant cannot be created", "A tenant already exists.", 409));
}
// Check if the provided logo is a valid Base64 string.
if (!string.IsNullOrWhiteSpace(model.logoImage) && !IsBase64String(model.logoImage))
{
_logger.LogWarning("Tenant creation failed for user {EmployeeId}: The provided logo image was not a valid Base64 string.", loggedInEmployee.Id);
return StatusCode(400, ApiResponse<object>.ErrorResponse("Tenant cannot be created", "The provided logo image is invalid.", 400));
}
// 3. --- DATABASE TRANSACTION ---
// Use a transaction to ensure all related entities are created successfully or none at all.
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Create the primary Tenant entity
var tenant = _mapper.Map<Tenant>(model);
tenant.TenantStatusId = activeStatus;
tenant.CreatedById = loggedInEmployee.Id;
tenant.IsSuperTenant = false;
_context.Tenants.Add(tenant);
// Create the root ApplicationUser for the new tenant
var applicationUser = new ApplicationUser
{
Email = model.Email,
UserName = model.Email, // Best practice to use email as username for simplicity
IsRootUser = true,
EmailConfirmed = true, // Auto-confirming email as it's part of a trusted setup process
TenantId = tenant.Id
};
// SECURITY WARNING: Hardcoded passwords are a major vulnerability.
// Replace "User@123" with a securely generated random password.
var initialPassword = "User@123"; // TODO: Replace with password generation service.
var result = await _userManager.CreateAsync(applicationUser, initialPassword);
if (!result.Succeeded)
{
// If user creation fails, roll back the transaction immediately and return the errors.
await transaction.RollbackAsync();
var errors = result.Errors.Select(e => e.Description).ToList();
_logger.LogWarning("Failed to create ApplicationUser for tenant {TenantName}. Errors: {Errors}", model.OrganizationName, string.Join(", ", errors));
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to create user", errors, 400));
}
// Create the default "Admin" Job Role for the tenant
var adminJobRole = new JobRole
{
Name = AdminRoleName,
Description = "Default administrator role for the tenant.",
TenantId = tenant.Id
};
_context.JobRoles.Add(adminJobRole);
// Create the primary Employee record and link it to the ApplicationUser and JobRole
var employeeUser = new Employee
{
FirstName = model.FirstName,
LastName = model.LastName,
Email = model.Email,
PhoneNumber = model.ContactNumber,
JoiningDate = model.OnBoardingDate,
ApplicationUserId = applicationUser.Id,
JobRole = adminJobRole, // Link to the newly created role
CurrentAddress = model.BillingAddress,
TenantId = tenant.Id
};
_context.Employees.Add(employeeUser);
var applicationRole = new ApplicationRole
{
Role = "Super User",
Description = "Super User",
IsSystem = true,
TenantId = tenant.Id
};
_context.ApplicationRoles.Add(applicationRole);
var rolePermissionMappigs = new List<RolePermissionMappings> {
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ModifyTenant
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ViewTenant
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ManageMasters
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ViewMasters
}
};
_context.RolePermissionMappings.AddRange(rolePermissionMappigs);
_context.EmployeeRoleMappings.Add(new EmployeeRoleMapping
{
EmployeeId = employeeUser.Id,
RoleId = applicationRole.Id,
IsEnabled = true,
TenantId = tenant.Id
});
// Create a default project for the new tenant
var project = new Project
{
Name = "Default Project",
ProjectStatusId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731"), // Consider using a constant for this GUID
ProjectAddress = model.BillingAddress,
StartDate = model.OnBoardingDate,
EndDate = DateTime.MaxValue,
ContactPerson = tenant.ContactName,
TenantId = tenant.Id
};
_context.Projects.Add(project);
// All entities are now added to the context. Save them all in a single database operation.
await _context.SaveChangesAsync();
// 4. --- POST-CREATION ACTIONS ---
// Generate a password reset token so the new user can set their own password.
_logger.LogInfo("User {Email} created. Sending password setup email.", applicationUser.Email);
var token = await _userManager.GeneratePasswordResetTokenAsync(applicationUser);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}&email={WebUtility.UrlEncode(applicationUser.Email)}";
await _emailSender.SendResetPasswordEmailOnRegister(applicationUser.Email, employeeUser.FirstName, resetLink);
// Map the result to a ViewModel for the API response.
var tenantVM = _mapper.Map<TenantVM>(tenant);
tenantVM.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
// Commit the transaction as all operations were successful.
await transaction.CommitAsync();
_logger.LogInfo("Successfully created tenant {TenantId} for organization {OrganizationName}.", tenant.Id, tenant.Name);
return StatusCode(201, ApiResponse<TenantVM>.SuccessResponse(tenantVM, "Tenant created successfully.", 201));
}
catch (DbUpdateException dbEx)
{
await transaction.RollbackAsync();
// Log the detailed database exception, including the inner exception if available.
_logger.LogError(dbEx, "A database update exception occurred while creating tenant for email {Email}. Inner Exception: {InnerException}",
model.Email, dbEx.InnerException?.Message ?? string.Empty);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal database error occurred.", ExceptionMapper(dbEx), 500));
}
catch (Exception ex)
{
await transaction.RollbackAsync();
// Log the general exception.
_logger.LogError(ex, "An unexpected exception occurred while creating tenant for email {Email}.", model.Email);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An unexpected internal error occurred.", ExceptionMapper(ex), 500));
}
}
// PUT api/<TenantController>/5
[HttpPut("{id}")]
public void Put(int id, [FromBody] string value)
{
}
// DELETE api/<TenantController>/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
#endregion
#region =================================================================== Subscription APIs ===================================================================
[HttpPost("add-subscription")]
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>();
var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false;
var hasPermission = await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id);
if (!hasPermission || !isRootUser)
{
_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.SubscriptionPlanDetails.Include(sp => sp.Plan).FirstOrDefaultAsync(sp => sp.Id == model.PlanId);
var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.Id == model.TenantId);
if (tenant == null)
{
_logger.LogWarning("Tenant {TenantId} not found in database", model.TenantId);
return NotFound(ApiResponse<object>.ErrorResponse("Tenant not found", "Tenant not found", 404));
}
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", 404));
}
await using var transaction = await _context.Database.BeginTransactionAsync();
var utcNow = DateTime.UtcNow;
// Prepare subscription dates based on frequency
var endDate = subscriptionPlan.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 // default if unknown
};
var tenantSubscription = new TenantSubscriptions
{
TenantId = model.TenantId,
PlanId = model.PlanId,
StatusId = activePlanStatus,
CreatedAt = utcNow,
MaxUsers = model.MaxUsers,
CreatedById = loggedInEmployee.Id,
CurrencyId = model.CurrencyId,
IsTrial = model.IsTrial,
StartDate = utcNow,
EndDate = endDate,
NextBillingDate = endDate,
AutoRenew = model.AutoRenew
};
_context.TenantSubscriptions.Add(tenantSubscription);
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));
}
}
[HttpPut("update-subscription")]
public async Task<IActionResult> UpdateSubscriptionAsync(UpdateSubscriptionDto model)
{
// 1. Get the logged-in user's employee record.
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// 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<PermissionServices>();
// 4. Check user permissions: must be both Root user and have ManageTenants permission.
var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false;
var hasPermission = await permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id);
if (!isRootUser || !hasPermission)
{
_logger.LogWarning("Permission denied for EmployeeId={EmployeeId}. Root: {IsRoot}, HasPermission: {HasPermission}",
loggedInEmployee.Id, isRootUser, hasPermission);
return StatusCode(403, ApiResponse<object>.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 == model.TenantId)).Unwrap();
var planDetailsTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(ctx =>
ctx.Result.SubscriptionPlanDetails.Include(sp => sp.Plan).AsNoTracking().FirstOrDefaultAsync(sp => sp.Id == model.PlanId)).Unwrap();
var currentSubscriptionTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(ctx =>
ctx.Result.TenantSubscriptions.Include(ts => ts.Currency).AsNoTracking().FirstOrDefaultAsync(ts => ts.TenantId == model.TenantId && !ts.IsCancelled)).Unwrap();
await Task.WhenAll(tenantTask, planDetailsTask, currentSubscriptionTask);
var tenant = tenantTask.Result;
if (tenant == null)
{
_logger.LogWarning("Tenant {TenantId} not found.", model.TenantId);
return NotFound(ApiResponse<object>.ErrorResponse("Tenant not found", "Tenant not found", 404));
}
var subscriptionPlan = planDetailsTask.Result;
if (subscriptionPlan == null)
{
_logger.LogWarning("Subscription plan {PlanId} not found.", model.PlanId);
return NotFound(ApiResponse<object>.ErrorResponse("Subscription plan not found", "Subscription plan not found", 404));
}
var currentSubscription = currentSubscriptionTask.Result;
var utcNow = DateTime.UtcNow;
// 6. If the tenant already has this plan, extend/renew it.
if (currentSubscription != null && currentSubscription.PlanId == subscriptionPlan.Id)
{
DateTime newEndDate;
// 6a. If the subscription is still active, extend from current EndDate; else start from now.
if (currentSubscription.EndDate.Date >= utcNow.Date)
{
newEndDate = subscriptionPlan.Frequency switch
{
PLAN_FREQUENCY.MONTHLY => currentSubscription.EndDate.AddMonths(1),
PLAN_FREQUENCY.QUARTERLY => currentSubscription.EndDate.AddMonths(3),
PLAN_FREQUENCY.HALF_MONTHLY => currentSubscription.EndDate.AddMonths(6),
PLAN_FREQUENCY.YEARLY => currentSubscription.EndDate.AddMonths(12),
_ => currentSubscription.EndDate
};
}
else
{
newEndDate = subscriptionPlan.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
};
}
// 6b. Update subscription details
if (model.MaxUsers != null && model.MaxUsers > 0)
{
currentSubscription.MaxUsers = model.MaxUsers.Value;
}
currentSubscription.StartDate = utcNow;
currentSubscription.EndDate = newEndDate;
currentSubscription.NextBillingDate = newEndDate;
currentSubscription.UpdateAt = utcNow;
currentSubscription.UpdatedById = loggedInEmployee.Id;
context.TenantSubscriptions.Update(currentSubscription);
await context.SaveChangesAsync();
_logger.LogInfo("Subscription renewed: Tenant={TenantId}, Plan={PlanId}, NewEnd={EndDate}",
model.TenantId, model.PlanId, newEndDate);
return Ok(ApiResponse<object>.SuccessResponse(currentSubscription, "Subscription renewed/extended", 200));
}
// 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.AddMonths(1),
PLAN_FREQUENCY.QUARTERLY => utcNow.AddMonths(3),
PLAN_FREQUENCY.HALF_MONTHLY => utcNow.AddMonths(6),
PLAN_FREQUENCY.YEARLY => utcNow.AddMonths(12),
_ => utcNow
};
var newSubscription = new TenantSubscriptions
{
TenantId = model.TenantId,
PlanId = model.PlanId,
StatusId = activePlanStatus,
CreatedAt = utcNow,
MaxUsers = model.MaxUsers ?? (currentSubscription?.MaxUsers ?? subscriptionPlan.MaxUser),
CreatedById = loggedInEmployee.Id,
CurrencyId = model.CurrencyId,
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 = loggedInEmployee.Id;
context.TenantSubscriptions.Update(currentSubscription);
}
await context.SaveChangesAsync();
_logger.LogInfo("Subscription plan changed: Tenant={TenantId}, NewPlan={PlanId}",
model.TenantId, model.PlanId);
// 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}.", model.PlanId);
await transaction.CommitAsync();
return Ok(ApiResponse<object>.SuccessResponse(newSubscription, "Tenant subscription updated (no features)", 200));
}
// 8a. Async helper to get all permission IDs for a given module.
async Task<List<Guid>> GetPermissionsForFeaturesAsync(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();
}
// 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<Guid> { EmployeeFeatureId }); // assumed defined
await Task.WhenAll(projectPermTask, attendancePermTask, directoryPermTask, expensePermTask, employeePermTask);
// 8c. Prepare add and remove permission lists.
var newPermissionIds = new List<Guid>();
var revokePermissionIds = new List<Guid>();
void ProcessPerms(bool? enabled, List<Guid> ids)
{
if (enabled == true) newPermissionIds.AddRange(ids);
else 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();
// 8d. 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 == model.TenantId);
if (rootEmployee == null)
{
_logger.LogWarning("No root employee for Tenant={TenantId}.", model.TenantId);
await transaction.CommitAsync();
return Ok(ApiResponse<object>.SuccessResponse(newSubscription, "Tenant subscription updated (no root employee)", 200));
}
var rootRoleId = await context.EmployeeRoleMappings
.AsNoTracking()
.Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == model.TenantId)
.Select(er => er.RoleId)
.FirstOrDefaultAsync();
if (rootRoleId == Guid.Empty)
{
_logger.LogWarning("No root role for Employee={EmployeeId}, Tenant={TenantId}.", rootEmployee.Id, model.TenantId);
await transaction.CommitAsync();
return Ok(ApiResponse<object>.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();
// 8e. Prevent accidental loss of basic employee permissions.
if ((oldPermIds.Count - revokePermissionIds.Count) >= 4 && revokePermissionIds.Any())
{
var employeePerms = employeePermTask.Result;
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);
}
await context.SaveChangesAsync();
await transaction.CommitAsync();
_logger.LogInfo("Tenant subscription and permissions updated: Tenant={TenantId}", model.TenantId);
return Ok(ApiResponse<object>.SuccessResponse(newSubscription, "Tenant subscription successfully updated", 200));
}
catch (DbUpdateException dbEx)
{
await transaction.RollbackAsync();
_logger.LogError(dbEx, "Database exception updating subscription for TenantId={TenantId}", model.TenantId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal database error", ExceptionMapper(dbEx), 500));
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "General exception for TenantId={TenantId}", model.TenantId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error occurred", ExceptionMapper(ex), 500));
}
}
#endregion
#region =================================================================== Subscription Plan APIs ===================================================================
[HttpGet("list/subscription-plan")]
public async Task<IActionResult> GetSubscriptionPlanList([FromQuery] PLAN_FREQUENCY? frequency)
{
_logger.LogInfo("GetSubscriptionPlanList called with frequency: {Frequency}", frequency ?? PLAN_FREQUENCY.MONTHLY);
// Initialize the list to store subscription plan view models
List<SubscriptionPlanVM> detailsVM = new List<SubscriptionPlanVM>();
try
{
// Create DbContext
await using var _context = await _dbContextFactory.CreateDbContextAsync();
// Load subscription plans with optional frequency filtering
IQueryable<SubscriptionPlanDetails> query = _context.SubscriptionPlanDetails.Include(sp => sp.Plan);
if (frequency.HasValue)
{
query = query.Where(sp => sp.Frequency == frequency.Value);
_logger.LogInfo("Filtering subscription plans by frequency: {Frequency}", frequency);
}
else
{
_logger.LogInfo("Fetching all subscription plans without frequency filter");
}
var subscriptionPlans = await query.ToListAsync();
// Map and fetch feature details for each subscription plan
foreach (var subscriptionPlan in subscriptionPlans)
{
var response = _mapper.Map<SubscriptionPlanVM>(subscriptionPlan);
try
{
response.Features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId);
}
catch (Exception exFeature)
{
_logger.LogError(exFeature, "Failed to fetch features for FeaturesId: {FeaturesId}", subscriptionPlan.FeaturesId);
response.Features = null; // or set to a default/fallback value
}
detailsVM.Add(response);
}
_logger.LogInfo("Successfully fetched {Count} subscription plans", detailsVM.Count);
return Ok(ApiResponse<object>.SuccessResponse(detailsVM, "List of plans fetched successfully", 200));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while fetching subscription plans");
return StatusCode(500, ApiResponse<object>.ErrorResponse("An error occurred while fetching subscription plans."));
}
}
[HttpPost("create/subscription-plan")]
public async Task<IActionResult> CreateSubscriptionPlan1([FromBody] SubscriptionPlanDto model)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
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)
{
_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));
}
var plan = _mapper.Map<SubscriptionPlan>(model);
_context.SubscriptionPlans.Add(plan);
List<SubscriptionPlanVM> response = new List<SubscriptionPlanVM>();
if (model.MonthlyPlan != null)
{
var currencyMaster = await _context.CurrencyMaster.AsNoTracking().FirstOrDefaultAsync(c => c.Id == model.MonthlyPlan.CurrencyId);
if (currencyMaster == null)
{
return NotFound(ApiResponse<object>.ErrorResponse("Currency not found", "Currency not found", 404));
}
var monthlyPlan = _mapper.Map<SubscriptionPlanDetails>(model.MonthlyPlan);
var features = _mapper.Map<FeatureDetails>(model.MonthlyPlan.Features);
try
{
await _featureDetailsHelper.AddFeatureDetails(features);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occured while saving feature in mongoDB");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500));
}
monthlyPlan.PlanId = plan.Id;
monthlyPlan.Frequency = PLAN_FREQUENCY.MONTHLY;
monthlyPlan.FeaturesId = features.Id;
monthlyPlan.CreatedById = loggedInEmployee.Id;
monthlyPlan.CreateAt = DateTime.UtcNow;
_context.SubscriptionPlanDetails.Add(monthlyPlan);
var VM = _mapper.Map<SubscriptionPlanVM>(monthlyPlan);
VM.PlanName = plan.PlanName;
VM.Description = plan.Description;
VM.Currency = currencyMaster;
response.Add(VM);
}
if (model.QuarterlyPlan != null)
{
var currencyMaster = await _context.CurrencyMaster.AsNoTracking().FirstOrDefaultAsync(c => c.Id == model.QuarterlyPlan.CurrencyId);
if (currencyMaster == null)
{
return NotFound(ApiResponse<object>.ErrorResponse("Currency not found", "Currency not found", 404));
}
var quarterlyPlan = _mapper.Map<SubscriptionPlanDetails>(model.QuarterlyPlan);
var features = _mapper.Map<FeatureDetails>(model.QuarterlyPlan.Features);
try
{
await _featureDetailsHelper.AddFeatureDetails(features);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occured while saving feature in mongoDB");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500));
}
quarterlyPlan.PlanId = plan.Id;
quarterlyPlan.Frequency = PLAN_FREQUENCY.QUARTERLY;
quarterlyPlan.FeaturesId = features.Id;
quarterlyPlan.CreatedById = loggedInEmployee.Id;
quarterlyPlan.CreateAt = DateTime.UtcNow;
_context.SubscriptionPlanDetails.Add(quarterlyPlan);
var VM = _mapper.Map<SubscriptionPlanVM>(quarterlyPlan);
VM.PlanName = plan.PlanName;
VM.Description = plan.Description;
VM.Currency = currencyMaster;
response.Add(VM);
}
if (model.HalfYearlyPlan != null)
{
var currencyMaster = await _context.CurrencyMaster.AsNoTracking().FirstOrDefaultAsync(c => c.Id == model.HalfYearlyPlan.CurrencyId);
if (currencyMaster == null)
{
return NotFound(ApiResponse<object>.ErrorResponse("Currency not found", "Currency not found", 404));
}
var halfYearlyPlan = _mapper.Map<SubscriptionPlanDetails>(model.HalfYearlyPlan);
var features = _mapper.Map<FeatureDetails>(model.HalfYearlyPlan.Features);
try
{
await _featureDetailsHelper.AddFeatureDetails(features);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occured while saving feature in mongoDB");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500));
}
halfYearlyPlan.PlanId = plan.Id;
halfYearlyPlan.Frequency = PLAN_FREQUENCY.HALF_MONTHLY;
halfYearlyPlan.FeaturesId = features.Id;
halfYearlyPlan.CreatedById = loggedInEmployee.Id;
halfYearlyPlan.CreateAt = DateTime.UtcNow;
_context.SubscriptionPlanDetails.Add(halfYearlyPlan);
var VM = _mapper.Map<SubscriptionPlanVM>(halfYearlyPlan);
VM.PlanName = plan.PlanName;
VM.Description = plan.Description;
VM.Currency = currencyMaster;
response.Add(VM);
}
if (model.YearlyPlan != null)
{
var currencyMaster = await _context.CurrencyMaster.AsNoTracking().FirstOrDefaultAsync(c => c.Id == model.YearlyPlan.CurrencyId);
if (currencyMaster == null)
{
return NotFound(ApiResponse<object>.ErrorResponse("Currency not found", "Currency not found", 404));
}
var yearlyPlan = _mapper.Map<SubscriptionPlanDetails>(model.YearlyPlan);
var features = _mapper.Map<FeatureDetails>(model.YearlyPlan.Features);
try
{
await _featureDetailsHelper.AddFeatureDetails(features);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occured while saving feature in mongoDB");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500));
}
yearlyPlan.PlanId = plan.Id;
yearlyPlan.Frequency = PLAN_FREQUENCY.YEARLY;
yearlyPlan.FeaturesId = features.Id;
yearlyPlan.CreatedById = loggedInEmployee.Id;
yearlyPlan.CreateAt = DateTime.UtcNow;
_context.SubscriptionPlanDetails.Add(yearlyPlan);
var VM = _mapper.Map<SubscriptionPlanVM>(yearlyPlan);
VM.PlanName = plan.PlanName;
VM.Description = plan.Description;
VM.Currency = currencyMaster;
response.Add(VM);
}
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException dbEx)
{
_logger.LogError(dbEx, "Database Exception occured while saving subscription plan");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500));
}
return StatusCode(201, ApiResponse<object>.SuccessResponse(response, "Plan Created Successfully", 201));
}
#endregion
#region =================================================================== Helper Functions ===================================================================
private static object ExceptionMapper(Exception ex)
{
return new
{
Message = ex.Message,
StackTrace = ex.StackTrace,
Source = ex.Source,
InnerException = new
{
Message = ex.InnerException?.Message,
StackTrace = ex.InnerException?.StackTrace,
Source = ex.InnerException?.Source,
}
};
}
private bool IsBase64String(string? input)
{
if (string.IsNullOrWhiteSpace(input))
{
return false;
}
string base64Data = input;
const string dataUriMarker = "base64,";
int markerIndex = input.IndexOf(dataUriMarker, StringComparison.Ordinal);
// If the marker is found, extract the actual Base64 data
if (markerIndex >= 0)
{
base64Data = input.Substring(markerIndex + dataUriMarker.Length);
}
// Now, validate the extracted payload
base64Data = base64Data.Trim();
// Check for valid length (must be a multiple of 4) and non-empty
if (base64Data.Length == 0 || base64Data.Length % 4 != 0)
{
return false;
}
// The most reliable test is to simply try to convert it.
// The .NET converter is strict and will throw a FormatException
// for invalid characters or incorrect padding.
try
{
Convert.FromBase64String(base64Data);
return true;
}
catch (FormatException)
{
// The string is not a valid Base64 payload.
return false;
}
}
private TenantFilter? TryDeserializeFilter(string? filter)
{
if (string.IsNullOrWhiteSpace(filter))
{
return null;
}
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
TenantFilter? expenseFilter = null;
try
{
// First, try to deserialize directly. This is the expected case (e.g., from a web client).
expenseFilter = JsonSerializer.Deserialize<TenantFilter>(filter, options);
}
catch (JsonException ex)
{
_logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeFilter), filter);
// If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients).
try
{
// Unescape the string first, then deserialize the result.
string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
if (!string.IsNullOrWhiteSpace(unescapedJsonString))
{
expenseFilter = JsonSerializer.Deserialize<TenantFilter>(unescapedJsonString, options);
}
}
catch (JsonException ex1)
{
// If both attempts fail, log the final error and return null.
_logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeFilter), filter);
return null;
}
}
return expenseFilter;
}
#endregion
}
}