@ -0,0 +1,682 @@
using AutoMapper ;
using Marco.Pms.DataAccess.Data ;
using Marco.Pms.Helpers.Utility ;
using Marco.Pms.Model.Dtos.Tenant ;
using Marco.Pms.Model.Employees ;
using Marco.Pms.Model.Entitlements ;
using Marco.Pms.Model.OrganizationModel ;
using Marco.Pms.Model.Projects ;
using Marco.Pms.Model.Roles ;
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.Helpers ;
using Marco.Pms.Services.Service.ServiceInterfaces ;
using MarcoBMS.Services.Helpers ;
using MarcoBMS.Services.Service ;
using Microsoft.AspNetCore.Identity ;
using Microsoft.EntityFrameworkCore ;
using System.Net ;
namespace Marco.Pms.Services.Service
{
public class TenantService : ITenantService
{
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 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" ) ;
private readonly static string AdminRoleName = "Admin" ;
public TenantService ( IDbContextFactory < ApplicationDbContext > dbContextFactory ,
IServiceScopeFactory serviceScopeFactory ,
ILoggingService logger ,
UserManager < ApplicationUser > userManager ,
IMapper mapper ,
UserHelper userHelper ,
FeatureDetailsHelper featureDetailsHelper )
{
_dbContextFactory = dbContextFactory ? ? throw new ArgumentNullException ( nameof ( dbContextFactory ) ) ;
_serviceScopeFactory = serviceScopeFactory ? ? throw new ArgumentNullException ( nameof ( serviceScopeFactory ) ) ;
_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 ) ) ;
}
public async Task < ApiResponse < object > > CreateTenantAsync ( Guid enquireId , Guid paymentDetailId , Guid planId )
{
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 tenantEnquireTask = Task . Run ( async ( ) = >
{
await using var context = await _dbContextFactory . CreateDbContextAsync ( ) ;
return await context . TenantEnquires . FirstOrDefaultAsync ( te = > te . Id = = enquireId ) ;
} ) ;
var paymentDetailTask = Task . Run ( async ( ) = >
{
await using var context = await _dbContextFactory . CreateDbContextAsync ( ) ;
return await context . PaymentDetails . FirstOrDefaultAsync ( pd = > pd . Id = = paymentDetailId ) ;
} ) ;
var subscriptionPlanTask = Task . Run ( async ( ) = >
{
await using var context = await _dbContextFactory . CreateDbContextAsync ( ) ;
return await context . SubscriptionPlanDetails . Include ( sp = > sp . Plan ) . FirstOrDefaultAsync ( sp = > sp . Id = = planId ) ;
} ) ;
await Task . WhenAll ( tenantEnquireTask , paymentDetailTask , subscriptionPlanTask ) ;
var tenantEnquire = tenantEnquireTask . Result ;
var paymentDetail = paymentDetailTask . Result ;
var subscriptionPlan = subscriptionPlanTask . Result ;
if ( tenantEnquire = = null )
{
_logger . LogWarning ( "Tenant Enquire {TenantEnquireId} not found in database" , enquireId ) ;
return ApiResponse < object > . ErrorResponse ( "Tenant Enquire not found" , "Tenant Enquire not found" , 404 ) ;
}
if ( paymentDetail = = null )
{
_logger . LogWarning ( "Payment Details {PaymentDetailsId} not found in database" , paymentDetailId ) ;
return ApiResponse < object > . ErrorResponse ( "Payment Details not found" , "Payment Details not found" , 404 ) ;
}
if ( subscriptionPlan = = null )
{
_logger . LogWarning ( "Subscription plan {PlanId} not found in database" , planId ) ;
return ApiResponse < object > . ErrorResponse ( "Subscription plan not found" , "Subscription plan not found" , 404 ) ;
}
var existingUser = await _userManager . FindByEmailAsync ( tenantEnquire . Email ) ;
if ( existingUser ! = null )
{
_logger . LogWarning ( "Tenant creation failed for email {Email}: an application user with this email already exists." , tenantEnquire . Email ) ;
return ApiResponse < object > . ErrorResponse ( "Tenant cannot be created" , "A user with the specified email already exists." , 409 ) ;
}
await using var transaction = await _context . Database . BeginTransactionAsync ( ) ;
try
{
Guid employeeId = Guid . NewGuid ( ) ;
DateTime onBoardingDate = DateTime . UtcNow ;
Guid tenantId = Guid . NewGuid ( ) ;
// Get last SPRID and increment for new organization
var lastOrganization = await _context . Organizations . OrderByDescending ( sp = > sp . SPRID ) . FirstOrDefaultAsync ( ) ;
double lastSPRID = lastOrganization ? . SPRID ? ? 5400 ;
// Map DTO to entity and set defaults
Organization organization = new Organization
{
Name = tenantEnquire . OrganizationName ,
Email = tenantEnquire . Email ,
ContactPerson = $"{tenantEnquire.FirstName} {tenantEnquire.LastName}" ,
Address = tenantEnquire . BillingAddress ,
ContactNumber = tenantEnquire . ContactNumber ,
SPRID = lastSPRID + 1 ,
CreatedAt = DateTime . UtcNow ,
CreatedById = employeeId ,
IsActive = true
} ;
_context . Organizations . Add ( organization ) ;
// Create the primary Tenant entity
var tenant = new Tenant
{
Id = tenantId ,
Name = tenantEnquire . OrganizationName ,
Email = tenantEnquire . Email ,
ContactName = $"{tenantEnquire.FirstName} {tenantEnquire.LastName}" ,
ContactNumber = tenantEnquire . ContactNumber ,
OrganizationSize = tenantEnquire . OrganizationSize ,
BillingAddress = tenantEnquire . BillingAddress ,
IndustryId = tenantEnquire . IndustryId ,
Reference = tenantEnquire . Reference ,
OnBoardingDate = onBoardingDate ,
TenantStatusId = tenantActiveStatus ,
OrganizationId = organization . Id ,
CreatedById = employeeId ,
IsActive = true ,
IsSuperTenant = false
} ;
_context . Tenants . Add ( tenant ) ;
// Create the root ApplicationUser for the new tenant
var applicationUser = new ApplicationUser
{
Email = tenantEnquire . Email ,
UserName = tenantEnquire . 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
} ;
// 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}" , tenantEnquire . OrganizationName , string . Join ( ", " , errors ) ) ;
return 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 = tenantId
} ;
_context . JobRoles . Add ( adminJobRole ) ;
// Create the primary Employee record and link it to the ApplicationUser and JobRole
var employeeUser = new Employee
{
Id = employeeId ,
FirstName = tenantEnquire . FirstName ,
LastName = tenantEnquire . LastName ,
Email = tenantEnquire . Email ,
PhoneNumber = tenantEnquire . ContactNumber ,
JoiningDate = onBoardingDate ,
ApplicationUserId = applicationUser . Id ,
JobRole = adminJobRole , // Link to the newly created role
CurrentAddress = tenantEnquire . BillingAddress ,
IsActive = true ,
IsSystem = false ,
IsPrimary = true ,
OrganizationId = organization . Id ,
HasApplicationAccess = true
} ;
_context . Employees . Add ( employeeUser ) ;
var applicationRole = new ApplicationRole
{
Role = "Super User" ,
Description = "Super User" ,
IsSystem = true ,
TenantId = tenantId
} ;
_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
} ,
new RolePermissionMappings
{
ApplicationRoleId = applicationRole . Id ,
FeaturePermissionId = PermissionsMaster . ViewOrganization
} ,
new RolePermissionMappings
{
ApplicationRoleId = applicationRole . Id ,
FeaturePermissionId = PermissionsMaster . AddOrganization
} ,
new RolePermissionMappings
{
ApplicationRoleId = applicationRole . Id ,
FeaturePermissionId = PermissionsMaster . EditOrganization
}
} ;
_context . RolePermissionMappings . AddRange ( rolePermissionMappigs ) ;
_context . EmployeeRoleMappings . Add ( new EmployeeRoleMapping
{
EmployeeId = employeeUser . Id ,
RoleId = applicationRole . Id ,
IsEnabled = true ,
TenantId = tenantId
} ) ;
// 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 = tenantEnquire . BillingAddress ,
StartDate = onBoardingDate ,
EndDate = DateTime . MaxValue ,
PromoterId = organization . Id ,
PMCId = organization . Id ,
ContactPerson = tenant . ContactName ,
TenantId = tenantId
} ;
_context . Projects . Add ( project ) ;
var projectAllocation = new ProjectAllocation
{
ProjectId = project . Id ,
EmployeeId = employeeUser . Id ,
AllocationDate = onBoardingDate ,
IsActive = true ,
JobRoleId = adminJobRole . Id ,
TenantId = tenantId
} ;
_context . ProjectAllocations . Add ( projectAllocation ) ;
// 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 > ( employeeUser ) ;
await AddSubscriptionAsync ( tenantId , employeeId , paymentDetailId , planId ) ;
// 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 ApiResponse < object > . 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}" ,
tenantEnquire . Email , dbEx . InnerException ? . Message ? ? string . Empty ) ;
return ApiResponse < object > . ErrorResponse ( "An internal database error occurred." , ExceptionMapper ( dbEx ) , 500 ) ;
}
catch ( Exception ex )
{
// Log the general exception.
_logger . LogError ( ex , "An unexpected exception occurred while creating tenant for email {Email}." , tenantEnquire . Email ) ;
return ApiResponse < object > . ErrorResponse ( "An unexpected internal error occurred." , ExceptionMapper ( ex ) , 500 ) ;
}
}
public async Task < ApiResponse < object > > AddSubscriptionAsync ( Guid tenantId , Guid employeeId , Guid paymentDetailId , Guid planId )
{
_logger . LogInfo ( "AddSubscription called for Tenant {TenantId} and Plan {PlanId}" , tenantId , planId ) ;
await using var _context = await _dbContextFactory . CreateDbContextAsync ( ) ;
using var scope = _serviceScopeFactory . CreateScope ( ) ;
var subscriptionPlan = await _context . SubscriptionPlanDetails . Include ( sp = > sp . Plan ) . FirstOrDefaultAsync ( sp = > sp . Id = = planId ) ;
var tenant = await _context . Tenants . FirstOrDefaultAsync ( t = > t . Id = = tenantId ) ;
if ( tenant = = null )
{
_logger . LogWarning ( "Tenant {TenantId} not found in database" , tenantId ) ;
return ApiResponse < object > . ErrorResponse ( "Tenant not found" , "Tenant not found" , 404 ) ;
}
if ( subscriptionPlan = = null )
{
_logger . LogWarning ( "Subscription plan {PlanId} not found in database" , planId ) ;
return ApiResponse < object > . ErrorResponse ( "Subscription plan not found" , "Subscription plan not found" , 404 ) ;
}
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 ( "Add less max user than the active user in the tenant {TenantId}" , tenant . Id ) ;
return ApiResponse < object > . ErrorResponse ( "Invalid Max user count" , "Max User count must be higher than active user count" , 400 ) ;
}
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 . 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 tenantSubscription = new TenantSubscriptions
{
TenantId = tenantId ,
PlanId = planId ,
StatusId = activePlanStatus ,
CreatedAt = utcNow ,
MaxUsers = subscriptionPlan . MaxUser ,
CreatedById = employeeId ,
CurrencyId = subscriptionPlan . CurrencyId ,
PaymentDetailId = paymentDetailId ,
IsTrial = false ,
StartDate = utcNow ,
EndDate = endDate ,
NextBillingDate = endDate ,
AutoRenew = false
} ;
_context . TenantSubscriptions . Add ( tenantSubscription ) ;
try
{
await _context . SaveChangesAsync ( ) ;
_logger . LogInfo ( "Tenant subscription added successfully for Tenant {TenantId}, Plan {PlanId}" ,
tenantId , planId ) ;
}
catch ( DbUpdateException dbEx )
{
_logger . LogError ( dbEx , "Database exception while adding subscription plan to tenant {TenantId}" , tenantId ) ;
return 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}" , planId ) ;
await transaction . CommitAsync ( ) ;
return 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 . OrganizationId = = tenant . OrganizationId ) ;
if ( rootEmployee = = null )
{
_logger . LogWarning ( "Root employee not found for tenant {TenantId}" , tenantId ) ;
await transaction . CommitAsync ( ) ;
return ApiResponse < object > . SuccessResponse ( tenantSubscription , "Tenant subscription successfully added" , 200 ) ;
}
var roleId = await _context . EmployeeRoleMappings
. AsNoTracking ( )
. Where ( er = > er . EmployeeId = = rootEmployee . Id & & er . TenantId = = tenantId )
. Select ( er = > er . RoleId )
. FirstOrDefaultAsync ( ) ;
if ( roleId = = Guid . Empty )
{
_logger . LogWarning ( "RoleId for root employee {EmployeeId} in tenant {TenantId} not found" , rootEmployee . Id , tenantId ) ;
await transaction . CommitAsync ( ) ;
return 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 ) ;
}
var _cache = scope . ServiceProvider . GetRequiredService < CacheUpdateHelper > ( ) ;
await _cache . ClearAllEmployeesFromCacheByTenantId ( tenant . Id ) ;
var _masteData = scope . ServiceProvider . GetRequiredService < MasterDataService > ( ) ;
if ( features . Modules ? . ProjectManagement ? . Enabled ? ? false )
{
var workCategoryMaster = _masteData . GetWorkCategoriesData ( tenant . Id ) ;
var workStatusMaster = _masteData . GetWorkStatusesData ( tenant . Id ) ;
_context . WorkCategoryMasters . AddRange ( workCategoryMaster ) ;
_context . WorkStatusMasters . AddRange ( workStatusMaster ) ;
}
if ( features . Modules ? . Expense ? . Enabled ? ? false )
{
var expensesTypeMaster = _masteData . GetExpensesTypeesData ( tenant . Id ) ;
var paymentModeMatser = _masteData . GetPaymentModesData ( tenant . Id ) ;
_context . ExpensesTypeMaster . AddRange ( expensesTypeMaster ) ;
_context . PaymentModeMatser . AddRange ( paymentModeMatser ) ;
}
await _context . SaveChangesAsync ( ) ;
await transaction . CommitAsync ( ) ;
_logger . LogInfo ( "Permissions updated successfully for tenant {TenantId} subscription" , tenantId ) ;
return 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}" , tenantId ) ;
return ApiResponse < object > . ErrorResponse ( "Internal error occured" , ExceptionMapper ( ex ) , 500 ) ;
}
}
#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 ;
}
}
/// <summary>
/// Handles the creation and persistence of SubscriptionPlanDetails for a particular frequency.
/// </summary>
private async Task < ApiResponse < SubscriptionPlanVM > > CreateSubscriptionPlanDetails ( SubscriptionPlanDetailsDto ? model , SubscriptionPlan plan , Employee loggedInEmployee , PLAN_FREQUENCY frequency )
{
if ( model = = null )
{
_logger . LogInfo ( "No plan detail provided for {Frequency} - skipping." , frequency ) ;
return ApiResponse < SubscriptionPlanVM > . ErrorResponse ( "Invalid" , "No data provided for this frequency" , 400 ) ;
}
await using var _dbContext = await _dbContextFactory . CreateDbContextAsync ( ) ;
// Fetch currency master record
var currencyMaster = await _dbContext . CurrencyMaster . AsNoTracking ( ) . FirstOrDefaultAsync ( c = > c . Id = = model . CurrencyId ) ;
if ( currencyMaster = = null )
{
_logger . LogWarning ( "Currency with Id {CurrencyId} not found for plan {PlanId}/{Frequency}." , model . CurrencyId , plan . Id , frequency ) ;
return ApiResponse < SubscriptionPlanVM > . ErrorResponse ( "Currency not found" , "Specified currency not found" , 404 ) ;
}
// Map to entity and create related feature details
var planDetails = _mapper . Map < SubscriptionPlanDetails > ( model ) ;
var features = _mapper . Map < FeatureDetails > ( model . Features ) ;
try
{
await _featureDetailsHelper . AddFeatureDetails ( features ) ;
_logger . LogInfo ( "FeatureDetails for plan {PlanId}/{Frequency} saved in MongoDB." , plan . Id , frequency ) ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Exception occurred while saving features in MongoDB for {PlanId}/{Frequency}." , plan . Id , frequency ) ;
return ApiResponse < SubscriptionPlanVM > . ErrorResponse ( "Internal error occurred" , ExceptionMapper ( ex ) , 500 ) ;
}
planDetails . PlanId = plan . Id ;
planDetails . Frequency = frequency ;
planDetails . FeaturesId = features . Id ;
planDetails . CreatedById = loggedInEmployee . Id ;
planDetails . CreateAt = DateTime . UtcNow ;
_dbContext . SubscriptionPlanDetails . Add ( planDetails ) ;
// Prepare view model
var VM = _mapper . Map < SubscriptionPlanVM > ( planDetails ) ;
VM . PlanName = plan . PlanName ;
VM . Description = plan . Description ;
VM . Features = features ;
VM . Currency = currencyMaster ;
try
{
await _dbContext . SaveChangesAsync ( ) ;
_logger . LogInfo ( "Subscription plan details for {PlanId}/{Frequency} saved to SQL." , plan . Id , frequency ) ;
}
catch ( DbUpdateException dbEx )
{
_logger . LogError ( dbEx , "Database exception occurred while saving plan details for {PlanId}/{Frequency}." , plan . Id , frequency ) ;
return ApiResponse < SubscriptionPlanVM > . ErrorResponse ( "Internal error occurred" , ExceptionMapper ( dbEx ) , 500 ) ;
}
return ApiResponse < SubscriptionPlanVM > . SuccessResponse ( VM , "Success" , 200 ) ;
}
#endregion
}
}