From a37176204dab675f1ba3257c39e6750728b1f2a8 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Thu, 4 Sep 2025 14:54:06 +0530
Subject: [PATCH 01/29] refactor: remove dep Redis configuration and
 communication files

---
 src/configs/redis.js                | 26 ----------------
 src/generics/redis-communication.js | 47 -----------------------------
 2 files changed, 73 deletions(-)
 delete mode 100644 src/configs/redis.js
 delete mode 100644 src/generics/redis-communication.js

diff --git a/src/configs/redis.js b/src/configs/redis.js
deleted file mode 100644
index bb5c357bc..000000000
--- a/src/configs/redis.js
+++ /dev/null
@@ -1,26 +0,0 @@
-const redis = require('redis')
-
-const { elevateLog } = require('elevate-logger')
-const logger = elevateLog.init()
-
-module.exports = async function () {
-	const redisClient = redis.createClient({ url: process.env.REDIS_HOST })
-
-	try {
-		await redisClient.connect()
-	} catch (error) {
-		logger.error('Error while making connection to redis client: ', {
-			triggerNotification: true,
-			err: error,
-		})
-	}
-
-	redisClient.on('error', (err) => {
-		logger.error('Error while making connection to redis client: ', {
-			triggerNotification: true,
-			error: err,
-		})
-	})
-
-	global.redisClient = redisClient
-}
diff --git a/src/generics/redis-communication.js b/src/generics/redis-communication.js
deleted file mode 100644
index 57413e73b..000000000
--- a/src/generics/redis-communication.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * @method
- * @name setKey - Sets Key to the redis cache
- * @param {String} key key to save
- * @param {Object | Number | String} value data to save
- * @param {Number} exp key expiration value in seconds
- * @returns {Promise<Object>} Returns the success response
- * @author Aman Gupta
- */
-const setKey = async function (key, value, exp) {
-	value = JSON.stringify(value)
-	const result = await redisClient.set(key, value, {
-		// NX: true, // Only set the key if it does not already exist.
-		EX: exp,
-	})
-	return result
-}
-
-/**
- * @method
- * @name getKey - Get Key from the redis cache
- * @param {String} key key to get corresponding saved data
- * @returns {Promise<Object>} Returns the saved corresponding object
- * @author Aman Gupta
- */
-const getKey = async function (key) {
-	const data = await redisClient.get(key)
-	return JSON.parse(data)
-}
-
-/**
- * @method
- * @name deleteKey - delete key from the redis cache
- * @param {String} key key to get corresponding saved data
- * @returns {Promise<Object>} Returns the deleted corresponding object
- * @author Rakesh
- **/
-const deleteKey = async function (key) {
-	const data = await redisClient.del(key)
-	return JSON.parse(data)
-}
-
-module.exports = {
-	setKey,
-	getKey,
-	deleteKey,
-}

From d68a4585fecfbe73f553e3c6525b71e4408ca765 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Thu, 11 Sep 2025 21:15:31 +0530
Subject: [PATCH 02/29] feat: add cacheHelper module for Redis and internal
 caching operations

---
 src/generics/cacheHelper.js | 255 ++++++++++++++++++++++++++++++++++++
 1 file changed, 255 insertions(+)
 create mode 100644 src/generics/cacheHelper.js

diff --git a/src/generics/cacheHelper.js b/src/generics/cacheHelper.js
new file mode 100644
index 000000000..75a068e02
--- /dev/null
+++ b/src/generics/cacheHelper.js
@@ -0,0 +1,255 @@
+// src/generics/cacheHelper.js
+/* eslint-disable no-console */
+const { RedisCache, InternalCache } = require('elevate-node-cache')
+const md5 = require('md5')
+const common = require('@constants/common')
+
+/** CONFIG */
+const CACHE_CONFIG = (() => {
+	try {
+		if (process.env.CACHE_CONFIG) return JSON.parse(process.env.CACHE_CONFIG)
+		return common.CACHE_CONFIG
+	} catch {
+		return common.CACHE_CONFIG
+	}
+})()
+
+const ENABLE_CACHE = pickBool(CACHE_CONFIG.enableCache, true)
+const SHARDS = toInt(CACHE_CONFIG.shards, 32)
+const BATCH = toInt(CACHE_CONFIG.scanBatch, 1000)
+const SHARD_RETENTION_DAYS = toInt(CACHE_CONFIG.shardRetentionDays, 7)
+
+/** Helpers */
+function toInt(v, d) {
+	const n = parseInt(v, 10)
+	return Number.isFinite(n) ? n : d
+}
+function pickBool(v, d) {
+	if (typeof v === 'boolean') return v
+	if (typeof v === 'string') return ['1', 'true', 'yes'].includes(v.toLowerCase())
+	return d
+}
+
+function tenantKey(tenantCode, parts = []) {
+	return ['tenant', tenantCode, ...parts].join(':')
+}
+function orgKey(tenantCode, orgId, parts = []) {
+	return ['tenant', tenantCode, 'org', orgId, ...parts].join(':')
+}
+function namespaceEnabled(ns) {
+	if (!ns) return true
+	const nsCfg = CACHE_CONFIG.namespaces && CACHE_CONFIG.namespaces[ns]
+	return !(nsCfg && nsCfg.enabled === false)
+}
+
+/**
+ * TTL resolution for namespace.
+ * callerTtl (explicit) wins.
+ * fallback to namespace.defaultTtl.
+ * fallback to undefined (no expiry).
+ */
+function nsTtl(ns, callerTtl) {
+	if (callerTtl != null) return Number(parseInt(callerTtl, 10))
+	const nsCfg = CACHE_CONFIG.namespaces && CACHE_CONFIG.namespaces[ns]
+	const v = nsCfg && nsCfg.defaultTtl
+	return v != null ? Number(parseInt(v, 10)) : undefined
+}
+
+/**
+ * Determine whether to use internal (in-memory) cache for this namespace.
+ * callerUseInternal (explicit param) wins.
+ * Otherwise check namespace.useInternal, then global CACHE_CONFIG.useInternal, then false.
+ */
+function nsUseInternal(ns, callerUseInternal) {
+	if (typeof callerUseInternal === 'boolean') return callerUseInternal
+	const nsCfg = CACHE_CONFIG.namespaces && CACHE_CONFIG.namespaces[ns]
+	if (nsCfg && typeof nsCfg.useInternal === 'boolean') return nsCfg.useInternal
+	if (typeof CACHE_CONFIG.useInternal === 'boolean') return CACHE_CONFIG.useInternal
+	return false
+}
+
+function namespacedKey({ tenantCode, orgId, ns, id }) {
+	const base = orgId ? orgKey(tenantCode, orgId, []) : tenantKey(tenantCode, [])
+	return [base, ns, id].filter(Boolean).join(':')
+}
+function shardOf(key) {
+	const h = md5(key)
+	const asInt = parseInt(h.slice(0, 8), 16)
+	return (asInt >>> 0) % SHARDS
+}
+
+/** Low-level redis client (best-effort) */
+function getRedisClient() {
+	try {
+		if (RedisCache && typeof RedisCache.native === 'function') return RedisCache.native()
+	} catch (err) {
+		console.log(err, 'error in getting native redis client')
+	}
+}
+
+/** Base ops (Internal cache opt-in via config or caller) */
+async function get(key, { useInternal = false } = {}) {
+	if (!ENABLE_CACHE) return null
+	// Try Redis first
+	try {
+		const val = await RedisCache.getKey(key)
+		if (val !== null && val !== undefined) return val
+	} catch (e) {
+		console.error('redis get error', e)
+	}
+	// Only hit InternalCache if explicitly requested
+	if (useInternal && InternalCache && InternalCache.getKey) {
+		try {
+			return InternalCache.getKey(key)
+		} catch (e) {
+			/* ignore internal errors */
+		}
+	}
+	return null
+}
+
+async function set(key, value, ttlSeconds, { useInternal = false } = {}) {
+	if (!ENABLE_CACHE) return false
+	let wroteRedis = false
+	try {
+		if (ttlSeconds) await RedisCache.setKey(key, value, ttlSeconds)
+		else await RedisCache.setKey(key, value)
+		wroteRedis = true
+	} catch (e) {
+		console.error('redis set error', e)
+	}
+	// Only write to InternalCache if opted in
+	if (useInternal && InternalCache && InternalCache.setKey) {
+		try {
+			InternalCache.setKey(key, value)
+		} catch (e) {}
+	}
+	return wroteRedis
+}
+
+async function del(key, { useInternal = false } = {}) {
+	try {
+		await RedisCache.deleteKey(key)
+	} catch (e) {
+		console.error('redis del error', e)
+	}
+	if (useInternal && InternalCache && InternalCache.delKey) {
+		try {
+			InternalCache.delKey(key)
+		} catch (e) {}
+	}
+}
+
+/**
+ * getOrSet
+ * - key (fallback id)
+ * - tenantCode
+ * - ttl (optional): explicit TTL seconds
+ * - fetchFn: function that returns value
+ * - orgId, ns, id: for namespaced keys
+ * - useInternal: optional boolean override. If omitted, resolved from namespace/config.
+ */
+async function getOrSet({ key, tenantCode, ttl = undefined, fetchFn, orgId, ns, id, useInternal = undefined }) {
+	if (!namespaceEnabled(ns)) return await fetchFn()
+
+	const resolvedUseInternal = nsUseInternal(ns, useInternal)
+	console.log('resolvedUseInternal', resolvedUseInternal, ns, useInternal)
+	const fullKey =
+		ns || id ? namespacedKey({ tenantCode, orgId, ns: ns || 'ns', id: id || key }) : tenantKey(tenantCode, [key])
+
+	const cached = await get(fullKey, { useInternal: resolvedUseInternal })
+	if (cached !== null && cached !== undefined) return cached
+
+	const value = await fetchFn()
+	if (value !== undefined) {
+		await set(fullKey, value, nsTtl(ns, ttl), { useInternal: resolvedUseInternal })
+	}
+	return value
+}
+
+/** Scoped set that uses namespace TTL and namespace useInternal setting */
+async function setScoped({ tenantCode, orgId, ns, id, value, ttl = undefined, useInternal = undefined }) {
+	if (!namespaceEnabled(ns)) return null
+	const resolvedUseInternal = nsUseInternal(ns, useInternal)
+	const fullKey = namespacedKey({ tenantCode, orgId, ns, id })
+	await set(fullKey, value, nsTtl(ns, ttl), { useInternal: resolvedUseInternal })
+	return fullKey
+}
+
+/**
+ * Eviction helpers using SCAN by pattern.
+ * These do not require any tracked sets. Caller should build patterns to match keys to remove.
+ *
+ * - scanAndDelete(pattern, opts)
+ *    pattern: glob-style pattern for SCAN (e.g. "tenant:acme:org:123:*")
+ *    opts.batchSize: number of keys to fetch per SCAN iteration (default BATCH)
+ *    opts.unlink: if true will attempt UNLINK when available
+ */
+async function scanAndDelete(pattern, { batchSize = BATCH, unlink = true } = {}) {
+	const redis = getRedisClient()
+	if (!redis) return
+	let cursor = '0'
+	do {
+		// redis.scan(cursor, 'MATCH', pattern, 'COUNT', batchSize)
+		const res = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', batchSize)
+		cursor = res && res[0] ? res[0] : '0'
+		const keys = res && res[1] ? res[1] : []
+		if (keys.length) {
+			// try unlink as best-effort
+			try {
+				if (unlink && typeof redis.unlink === 'function') await redis.unlink(...keys)
+				else await redis.del(...keys)
+			} catch (e) {
+				// fallback to individual deletes
+				for (const k of keys) {
+					try {
+						if (unlink && typeof redis.unlink === 'function') await redis.unlink(k)
+						else await redis.del(k)
+					} catch (__) {}
+				}
+			}
+		}
+	} while (cursor !== '0')
+}
+
+/** Evict all keys for a tenant + org by pattern */
+async function evictOrgByPattern(tenantCode, orgId, { patternSuffix = '*' } = {}) {
+	if (!tenantCode || !orgId) return
+	const pattern = `tenant:${tenantCode}:org:${orgId}:${patternSuffix}`
+	await scanAndDelete(pattern)
+}
+
+/** Evict tenant-level keys by pattern */
+async function evictTenantByPattern(tenantCode, { patternSuffix = '*' } = {}) {
+	if (!tenantCode) return
+	const pattern = `tenant:${tenantCode}:${patternSuffix}`
+	await scanAndDelete(pattern)
+}
+
+/** Public API */
+module.exports = {
+	// Base ops
+	get,
+	set,
+	del,
+	getOrSet,
+	tenantKey,
+
+	// Scoped helpers
+	setScoped,
+	namespacedKey,
+
+	// Eviction (pattern based)
+	evictOrgByPattern,
+	evictTenantByPattern,
+	scanAndDelete,
+
+	// Introspection
+	_internal: {
+		getRedisClient,
+		SHARDS,
+		BATCH,
+		ENABLE_CACHE,
+		CACHE_CONFIG,
+	},
+}

From 96ca958bd72750128555e9324f8b4adb95c1e966 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Thu, 11 Sep 2025 21:16:47 +0530
Subject: [PATCH 03/29] feat: integrate caching mechanism for user profile
 retrieval and updates

---
 src/services/user.js | 214 ++++++++++++++++++++++++-------------------
 1 file changed, 119 insertions(+), 95 deletions(-)

diff --git a/src/services/user.js b/src/services/user.js
index c5663fa2f..5fe5c0575 100644
--- a/src/services/user.js
+++ b/src/services/user.js
@@ -25,6 +25,8 @@ const { eventBodyDTO, keysFilter } = require('@dtos/userDTO')
 
 const { broadcastUserEvent } = require('@helpers/eventBroadcasterMain')
 
+const cacheClient = require('@generics/cacheHelper')
+
 module.exports = class UserHelper {
 	/**
 	 * update profile
@@ -140,10 +142,16 @@ module.exports = class UserHelper {
 					},
 				})
 			}
-			const redisUserKey = common.redisUserPrefix + tenantCode + '_' + id.toString()
-			if (await utils.redisGet(redisUserKey)) {
-				await utils.redisDel(redisUserKey)
-			}
+
+			const ns = common.CACHE_CONFIG.namespaces.profile.name
+			const cacheKey = cacheClient.namespacedKey({
+				tenantCode,
+				orgId: orgCode,
+				ns,
+				id,
+			})
+			await cacheClient.del(cacheKey)
+
 			const processDbResponse = await utils.processDbResponse(
 				JSON.parse(JSON.stringify(updatedData[0])),
 				validationData
@@ -295,107 +303,117 @@ module.exports = class UserHelper {
 	 * @param {string} searchText - search text.
 	 * @returns {JSON} - user information
 	 */
-	static async read(id, header = null, language, tenantCode) {
+	static async read(id, header = null, language, tenantCode, organizationCode) {
 		try {
-			let filter = {}
-			console.log(tenantCode)
-			if (utils.isNumeric(id)) {
-				filter = { id: id, tenant_code: tenantCode }
-			} else {
-				filter = { share_link: id }
-			}
-
-			const redisUserKey = common.redisUserPrefix + tenantCode + '_' + id.toString()
-			const userDetails = (await utils.redisGet(redisUserKey)) || false
-			if (!userDetails) {
-				let options = {
-					attributes: {
-						exclude: ['password', 'refresh_tokens'],
-					},
-				}
-				if (header.internal_access_token) {
-					options.paranoid = false
-				}
-				let user = await userQueries.findUserWithOrganization(filter, options)
-
-				if (!user) {
-					return responses.failureResponse({
-						message: 'USER_NOT_FOUND',
-						statusCode: httpStatusCode.not_found,
-						responseCode: 'UNAUTHORIZED',
-					})
-				}
-
-				let roles = user.organizations[0].roles
-
-				if (!roles) {
-					return responses.failureResponse({
-						message: 'ROLE_NOT_FOUND',
-						statusCode: httpStatusCode.not_acceptable,
-						responseCode: 'CLIENT_ERROR',
-					})
-				}
-				if (language && language !== common.ENGLISH_LANGUGE_CODE) {
-					utils.setRoleLabelsByLanguage(roles, language)
-				} else {
-					roles.map((roles) => {
-						delete roles.translations
-						return roles
-					})
-				}
+			const filter = utils.isNumeric(id) ? { id, tenant_code: tenantCode } : { share_link: id }
+
+			const ns = common.CACHE_CONFIG.namespaces.profile.name
+			const cacheKey = cacheClient.namespacedKey({
+				tenantCode,
+				orgId: organizationCode,
+				ns,
+				id,
+			})
 
-				//user.user_roles = roles
+			// Try cache first
+			let userDetails = await cacheClient.get(cacheKey)
+			if (userDetails) {
+				if (userDetails.image) userDetails.image = await utils.getDownloadableUrl(userDetails.image)
+				return responses.successResponse({
+					statusCode: httpStatusCode.ok,
+					message: 'PROFILE_FETCHED_SUCCESSFULLY',
+					result: userDetails,
+				})
+			}
 
-				let defaultOrganizationCode = process.env.DEFAULT_ORGANISATION_CODE
+			// Cache miss -> load from DB and compute
+			const options = {
+				attributes: { exclude: ['password', 'refresh_tokens'] },
+			}
+			if (header && header.internal_access_token) options.paranoid = false
 
-				let userOrg = user.organizations[0].code
+			const user = await userQueries.findUserWithOrganization(filter, options)
+			if (!user) {
+				return responses.failureResponse({
+					message: 'USER_NOT_FOUND',
+					statusCode: httpStatusCode.not_found,
+					responseCode: 'UNAUTHORIZED',
+				})
+			}
 
-				let validationData = await entityTypeQueries.findUserEntityTypesAndEntities({
-					status: 'ACTIVE',
-					organization_code: {
-						[Op.in]: [userOrg, defaultOrganizationCode],
-					},
-					tenant_code: tenantCode,
-					model_names: { [Op.contains]: [await userQueries.getModelName()] },
+			const roles = user.organizations[0].roles
+			if (!roles) {
+				return responses.failureResponse({
+					message: 'ROLE_NOT_FOUND',
+					statusCode: httpStatusCode.not_acceptable,
+					responseCode: 'CLIENT_ERROR',
 				})
-				const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.organizations[0].id)
-				const permissionsByModule = await this.getPermissions(user.organizations[0].roles)
-				user.permissions = permissionsByModule
+			}
 
-				const processDbResponse = await utils.processDbResponse(user, prunedEntities)
+			if (language && language !== common.ENGLISH_LANGUAGE_CODE) {
+				utils.setRoleLabelsByLanguage(roles, language)
+			} else {
+				roles.forEach((r) => {
+					delete r.translations
+				})
+			}
 
-				if (processDbResponse) {
-					;['email', 'phone'].forEach((field) => {
-						const value = processDbResponse[field]
-						if (typeof value === 'string' && value.trim() !== '') {
-							processDbResponse[field] = emailEncryption.decrypt(value)
-						}
+			const defaultOrganizationCode = process.env.DEFAULT_ORGANISATION_CODE
+			const userOrg = user.organizations[0].code
+			const modelName = await userQueries.getModelName()
+
+			const prunedEntities = await cacheClient.getOrSet({
+				tenantCode,
+				orgId: organizationCode,
+				ns: common.CACHE_CONFIG.namespaces.entity_types.name,
+				id: `${userOrg}:${modelName}`, // per-org stable id
+				fetchFn: async () => {
+					const raw = await entityTypeQueries.findUserEntityTypesAndEntities({
+						status: 'ACTIVE',
+						organization_code: { [Op.in]: [userOrg, defaultOrganizationCode] },
+						tenant_code: tenantCode,
+						model_names: { [Op.contains]: [modelName] },
 					})
-				}
+					return removeDefaultOrgEntityTypes(raw, user.organizations[0].id)
+				},
+			})
 
-				if (utils.validateRoleAccess(roles, [common.MENTOR_ROLE, common.MENTEE_ROLE])) {
-					await utils.redisSet(redisUserKey, processDbResponse, common.redisUserCacheTTL)
-				}
+			const permissionsByModule = await this.getPermissions(user.organizations[0].roles)
+			user.permissions = permissionsByModule
 
-				processDbResponse['image_cloud_path'] = processDbResponse.image
-				if (processDbResponse && processDbResponse.image) {
-					processDbResponse.image = await utils.getDownloadableUrl(processDbResponse.image)
-				}
-				return responses.successResponse({
-					statusCode: httpStatusCode.ok,
-					message: 'PROFILE_FETCHED_SUCCESSFULLY',
-					result: processDbResponse ? processDbResponse : {},
+			const processDbResponse = await utils.processDbResponse(user, prunedEntities)
+
+			if (processDbResponse) {
+				;['email', 'phone'].forEach((field) => {
+					const value = processDbResponse[field]
+					if (typeof value === 'string' && value.trim() !== '') {
+						processDbResponse[field] = emailEncryption.decrypt(value)
+					}
 				})
-			} else {
-				if (userDetails && userDetails.image) {
-					userDetails.image = await utils.getDownloadableUrl(userDetails.image)
-				}
-				return responses.successResponse({
-					statusCode: httpStatusCode.ok,
-					message: 'PROFILE_FETCHED_SUCCESSFULLY',
-					result: userDetails ? userDetails : {},
+			}
+
+			// Conditional caching: only cache if user has mentor/mentee role access
+			if (utils.validateRoleAccess(roles, [common.MENTOR_ROLE, common.MENTEE_ROLE])) {
+				await cacheClient.setScoped({
+					tenantCode,
+					orgId: organizationCode,
+					ns,
+					id,
+					value: processDbResponse,
 				})
 			}
+
+			// Prepare image url for response
+			if (processDbResponse && processDbResponse.image) {
+				processDbResponse.image_cloud_path = processDbResponse.image
+				processDbResponse.image = await utils.getDownloadableUrl(processDbResponse.image)
+			}
+
+			return responses.successResponse({
+				statusCode: httpStatusCode.ok,
+				message: 'PROFILE_FETCHED_SUCCESSFULLY',
+				result: processDbResponse || {},
+			})
 		} catch (error) {
 			console.log(error)
 			throw error
@@ -565,10 +583,16 @@ module.exports = class UserHelper {
 				{ id: id, tenant_code: tenantCode },
 				bodyData
 			)
-			const redisUserKey = common.redisUserPrefix + tenantCode + '_' + id.toString()
-			if (await utils.redisGet(redisUserKey)) {
-				await utils.redisDel(redisUserKey)
-			}
+
+			const ns = common.CACHE_CONFIG.namespaces.profile.name
+			const cacheKey = cacheClient.namespacedKey({
+				tenantCode,
+				orgId: orgCode,
+				ns,
+				id,
+			})
+			await cacheClient.del(cacheKey)
+
 			const processDbResponse = await utils.processDbResponse(
 				JSON.parse(JSON.stringify(updatedData[0])),
 				dataValidation

From 25c96a02f38ba3411a2c6564003aa1f98b400e29 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Thu, 11 Sep 2025 21:17:08 +0530
Subject: [PATCH 04/29] feat: enhance user read method to include organization
 code in user details retrieval

---
 src/controllers/v1/user.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/controllers/v1/user.js b/src/controllers/v1/user.js
index dc6954306..a2f884280 100644
--- a/src/controllers/v1/user.js
+++ b/src/controllers/v1/user.js
@@ -49,7 +49,8 @@ module.exports = class User {
 				req.params.id ? req.params.id : req.decodedToken.id,
 				req.headers,
 				req.query.language ? req.query.language : '',
-				req.decodedToken.tenant_code
+				req.decodedToken.tenant_code,
+				req.decodedToken.organization_code
 			)
 			return userDetails
 		} catch (error) {

From d7119e5b7313a6286351dcd1ea860c8a79d9e67c Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Thu, 11 Sep 2025 21:17:34 +0530
Subject: [PATCH 05/29] fix: correct spelling of ENGLISH_LANGUAGE_CODE in
 common constants and update user-role service to use the corrected constant

---
 src/constants/common.js   | 12 +++++++++++-
 src/services/user-role.js |  2 +-
 2 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/constants/common.js b/src/constants/common.js
index 54bc26da9..0335c471a 100644
--- a/src/constants/common.js
+++ b/src/constants/common.js
@@ -101,7 +101,7 @@ module.exports = {
 	WRITE_ACCESS: 'w',
 	READ_ACCESS: 'r',
 	TYPE_ALL: 'all',
-	ENGLISH_LANGUGE_CODE: 'en',
+	ENGLISH_LANGUAGE_CODE: 'en',
 	ORG_CODE_HEADER: 'organizationcode',
 	TENANT_CODE_HEADER: 'tenantcode',
 	DELETE_METHOD: 'DELETE',
@@ -113,4 +113,14 @@ module.exports = {
 	SIGNEDUP_STATUS: 'SIGNEDUP',
 	SEQUELIZE_UNIQUE_CONSTRAINT_ERROR: 'SequelizeUniqueConstraintError',
 	SEQUELIZE_UNIQUE_CONSTRAINT_ERROR_CODE: 'ER_DUP_ENTRY',
+	CACHE_CONFIG: {
+		enableCache: true,
+		enableTracking: true,
+		shards: 32,
+		namespaces: {
+			profile: { name: 'profile', enabled: true, defaultTtl: 43200, useInternal: false },
+			entity_types: { name: 'entity_types', enabled: true, defaultTtl: 86400, useInternal: false },
+			tenant: { name: 'tenant', enabled: true, defaultTtl: 86400, useInternal: false },
+		},
+	},
 }
diff --git a/src/services/user-role.js b/src/services/user-role.js
index 12032da20..4f0d93133 100644
--- a/src/services/user-role.js
+++ b/src/services/user-role.js
@@ -200,7 +200,7 @@ module.exports = class userRoleHelper {
 					responseCode: 'CLIENT_ERROR',
 				})
 			}
-			if (language && language !== common.ENGLISH_LANGUGE_CODE) {
+			if (language && language !== common.ENGLISH_LANGUAGE_CODE) {
 				utils.setRoleLabelsByLanguage(roles.rows, language)
 			} else {
 				roles.rows.map((labels) => {

From 43b08fb2510dab43c8a3e7c9ef1f5a0985658255 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Thu, 11 Sep 2025 21:17:49 +0530
Subject: [PATCH 06/29] chore: update elevate-node-cache dependency to version
 2.0.0

---
 src/package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/package.json b/src/package.json
index 51f05042b..17fcdc120 100644
--- a/src/package.json
+++ b/src/package.json
@@ -43,7 +43,7 @@
 		"elevate-cloud-storage": "^2.6.3",
 		"elevate-encryption": "^1.0.1",
 		"elevate-logger": "^3.1.0",
-		"elevate-node-cache": "^1.0.6",
+		"elevate-node-cache": "^2.0.0",
 		"elevate-services-health-check": "^0.0.6",
 		"email-validator": "^2.0.4",
 		"express": "^4.17.1",

From 53114eeee2233a3f656777e3a6d6114ac65bd7a9 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Thu, 11 Sep 2025 21:18:30 +0530
Subject: [PATCH 07/29] feat: implement caching for tenant domain, tenant
 details and entity type read in user login process

---
 src/services/account.js | 176 ++++++++++++++++++++++++++--------------
 1 file changed, 116 insertions(+), 60 deletions(-)

diff --git a/src/services/account.js b/src/services/account.js
index 724479616..41d586f77 100644
--- a/src/services/account.js
+++ b/src/services/account.js
@@ -39,7 +39,7 @@ const UserTransformDTO = require('@dtos/userDTO')
 const notificationUtils = require('@utils/notification')
 const userHelper = require('@helpers/userHelper')
 const { broadcastUserEvent } = require('@helpers/eventBroadcasterMain')
-
+const cacheClient = require('@generics/cacheHelper')
 module.exports = class AccountHelper {
 	/**
 	 * create account
@@ -636,43 +636,94 @@ module.exports = class AccountHelper {
 	}
 
 	/**
-	 * login user account
-	 * @method
-	 * @name login
-	 * @param {Object} bodyData -request body contains user login deatils.
-	 * @param {String} bodyData.email - user email.
-	 * @param {String} bodyData.password - user password.
-	 * @param {Object} deviceInformation - device information
-	 * @returns {JSON} - returns susccess or failure of login details.
+	 * Authenticate a user and create a session for the given tenant domain.
+	 *
+	 * Steps (logic unchanged):
+	 * 1. Validate tenant domain via cache then load tenant details.
+	 * 2. Validate and classify identifier (email | phone | username).
+	 * 3. Query user by identifier and tenant, ensure password exists and user is active.
+	 * 4. Enforce active session limit when configured.
+	 * 5. Verify password, create user session, prune admin roles, transform user object.
+	 * 6. Generate access and refresh tokens, fetch pruned entity types for user's org,
+	 *    process DB response, attach downloadable image URL if present.
+	 * 7. Update session storage/Redis and return success response.
+	 *
+	 * Logic, return shape and error handling are preserved exactly from the original.
+	 *
+	 * @async
+	 * @function login
+	 * @param {Object} bodyData - Request body for login.
+	 * @param {string} bodyData.identifier - Email, phone or username used to identify the user. Case-insensitive.
+	 * @param {string} bodyData.password - Plain text password to validate.
+	 * @param {string} [bodyData.phone_code] - Optional phone country code used when identifier is a phone.
+	 * @param {Object} deviceInformation - Device metadata to store with the session (e.g. { ip, userAgent, deviceId }).
+	 * @param {string} domain - Tenant domain string used to resolve tenant information.
+	 * @returns {Promise<Object>} Resolves to a response object from responses.successResponse or responses.failureResponse.
+	 *
+	 * The success response resolves with:
+	 * {
+	 *   statusCode: httpStatusCode.ok,
+	 *   message: 'LOGGED_IN_SUCCESSFULLY',
+	 *   result: {
+	 *     access_token: string,
+	 *     refresh_token: string,
+	 *     user: Object // transformed user DTO (password removed) with `identifier` set to the original identifier
+	 *   }
+	 * }
+	 *
+	 * Failure responses use responses.failureResponse and return early for cases such as:
+	 * - missing identifier
+	 * - tenant domain not found
+	 * - tenant not found
+	 * - identifier/password invalid
+	 * - active session limit exceeded
+	 *
+	 * @throws {Error} Re-throws any unexpected error encountered during processing.
 	 */
-
 	static async login(bodyData, deviceInformation, domain) {
 		try {
-			const notFoundResponse = (message) =>
+			const makeNotFoundResponse = (message) =>
 				responses.failureResponse({
 					message,
 					statusCode: httpStatusCode.not_acceptable,
 					responseCode: 'CLIENT_ERROR',
 				})
 
-			// Validate tenant domain
-			const tenantDomain = await tenantDomainQueries.findOne({ domain })
+			// Cache lookup for tenant domain
+			const tenantDomain = await cacheClient.getOrSet({
+				tenantCode: null,
+				orgId: null,
+				ns: common.CACHE_CONFIG.namespaces.tenant.name,
+				id: domain,
+				fetchFn: async () => tenantDomainQueries.findOne({ domain }),
+			})
+
 			if (!tenantDomain) {
-				return notFoundResponse('TENANT_DOMAIN_NOT_FOUND_PING_ADMIN')
+				return makeNotFoundResponse('TENANT_DOMAIN_NOT_FOUND_PING_ADMIN')
 			}
 
-			// Validate tenant
-			const tenantDetail = await tenantQueries.findOne({
-				code: tenantDomain.tenant_code,
+			// Cache lookup for tenant by tenant_code
+			const tenantCode = tenantDomain.tenant_code
+			const tenantDetail = await cacheClient.getOrSet({
+				tenantCode,
+				orgId: null,
+				ns: common.CACHE_CONFIG.namespaces.tenant.name,
+				id: tenantCode,
+				fetchFn: async () => tenantQueries.findOne({ code: tenantCode }),
 			})
+
 			if (!tenantDetail) {
-				return notFoundResponse('TENANT_NOT_FOUND_PING_ADMIN')
+				return makeNotFoundResponse('TENANT_NOT_FOUND_PING_ADMIN')
 			}
 
-			// Helper functions to detect identifier type
-			const isEmail = (str) => /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(str)
-			const isPhone = (str) => /^\+?[1-9]\d{1,14}$/.test(str) // Adjust regex as needed
-			const isUsername = (str) => /^[a-zA-Z0-9_]{3,30}$/.test(str)
+			// Identifier validators
+			const EMAIL_RE = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/
+			const PHONE_RE = /^\+?[1-9]\d{1,14}$/
+			const USERNAME_RE = /^[a-zA-Z0-9_]{3,30}$/
+
+			const isEmailFormat = (s) => EMAIL_RE.test(s)
+			const isPhoneFormat = (s) => PHONE_RE.test(s)
+			const isUsernameFormat = (s) => USERNAME_RE.test(s)
 
 			const identifier = bodyData.identifier?.toLowerCase()
 			if (!identifier) {
@@ -683,7 +734,7 @@ module.exports = class AccountHelper {
 				})
 			}
 
-			// Prepare query based on identifier type
+			// Build user query based on identifier type
 			const query = {
 				[Op.or]: [],
 				password: { [Op.ne]: null },
@@ -691,14 +742,18 @@ module.exports = class AccountHelper {
 				tenant_code: tenantDetail.code,
 			}
 
-			if (isEmail(identifier)) {
+			if (isEmailFormat(identifier)) {
 				query[Op.or].push({ email: emailEncryption.encrypt(identifier) })
-			} else if (isPhone(identifier)) {
-				query[Op.or].push({ phone: emailEncryption.encrypt(identifier), phone_code: bodyData.phone_code }) // Adjust if phone encryption differs
+			} else if (isPhoneFormat(identifier)) {
+				query[Op.or].push({
+					phone: emailEncryption.encrypt(identifier),
+					phone_code: bodyData.phone_code,
+				})
 			} else {
 				query[Op.or].push({ username: identifier })
 			}
-			// Find user
+
+			// Find user with org
 			const userInstance = await userQueries.findUserWithOrganization(query, {}, true)
 			let user = userInstance ? userInstance.toJSON() : null
 
@@ -710,7 +765,7 @@ module.exports = class AccountHelper {
 				})
 			}
 
-			// Check active session limit
+			// Enforce active session limit
 			if (process.env.ALLOWED_ACTIVE_SESSIONS != null) {
 				const activeSessionCount = await userSessionsService.activeUserSessionCounts(user.id)
 				if (activeSessionCount >= process.env.ALLOWED_ACTIVE_SESSIONS) {
@@ -732,7 +787,7 @@ module.exports = class AccountHelper {
 				})
 			}
 
-			// Create user session
+			// Create session
 			const userSessionDetails = await userSessionsService.createUserSession(
 				user.id,
 				'',
@@ -741,16 +796,7 @@ module.exports = class AccountHelper {
 				user.tenant_code
 			)
 
-			// Determine tenant ID
-			/* 			let tenantDetails = await organizationQueries.findOne(
-				{ id: user.organization_id },
-				{ attributes: ['related_orgs'] }
-			)
-			const tenant_id =
-				tenantDetails && tenantDetails.parent_id !== null ? tenantDetails.parent_id : user.organization_id
- */
-
-			//Remove all 'admin' roles from user.user_organizations
+			// Remove admin roles from user_organizations
 			if (user?.user_organizations?.length) {
 				user.user_organizations.forEach((org) => {
 					if (org.roles) {
@@ -759,16 +805,16 @@ module.exports = class AccountHelper {
 				})
 			}
 
-			// Transform user data
+			// Transform user for response
 			user = UserTransformDTO.transform(user)
 
-			const tokenDetail = {
+			const tokenPayload = {
 				data: {
 					id: user.id,
 					name: user.name,
 					session_id: userSessionDetails.result.id,
-					organization_ids: user.organizations.map((org) => String(org.id)), // Convert to string
-					organization_codes: user.organizations.map((org) => String(org.code)), // Convert to string					// tenant_id: tenant_id,
+					organization_ids: user.organizations.map((org) => String(org.id)),
+					organization_codes: user.organizations.map((org) => String(org.code)),
 					tenant_code: tenantDetail.code,
 					organizations: user.organizations,
 				},
@@ -776,12 +822,12 @@ module.exports = class AccountHelper {
 
 			// Generate tokens
 			const accessToken = utilsHelper.generateToken(
-				tokenDetail,
+				tokenPayload,
 				process.env.ACCESS_TOKEN_SECRET,
 				common.accessTokenExpiry
 			)
 			const refreshToken = utilsHelper.generateToken(
-				tokenDetail,
+				tokenPayload,
 				process.env.REFRESH_TOKEN_SECRET,
 				common.refreshTokenExpiry
 			)
@@ -790,31 +836,37 @@ module.exports = class AccountHelper {
 
 			const modelName = await userQueries.getModelName()
 
-			const orgCodes = user.organizations?.map((org) => org.code).filter(Boolean) || []
+			const orgCodes = user.organizations?.map((o) => o.code).filter(Boolean) || []
 			orgCodes.push(process.env.DEFAULT_ORGANISATION_CODE)
 
-			let validationData = await entityTypeQueries.findUserEntityTypesAndEntities({
-				status: 'ACTIVE',
-				organization_code: {
-					[Op.in]: orgCodes,
+			// Fetch pruned entities for the user's primary org and model
+			const prunedEntities = await cacheClient.getOrSet({
+				tenantCode,
+				orgId: tokenPayload.data.organization_codes[0],
+				ns: common.CACHE_CONFIG.namespaces.entity_types.name,
+				id: `${tokenPayload.data.organization_codes[0]}:${modelName}`,
+				fetchFn: async () => {
+					const raw = await entityTypeQueries.findUserEntityTypesAndEntities({
+						status: 'ACTIVE',
+						organization_code: { [Op.in]: orgCodes },
+						tenant_code: tenantDetail.code,
+						model_names: { [Op.contains]: [modelName] },
+					})
+					return removeDefaultOrgEntityTypes(raw, user.organizations[0].id)
 				},
-				tenant_code: tenantDetail.code,
-				model_names: { [Op.contains]: [modelName] },
 			})
 
-			const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.organizations?.[0]?.id)
-
 			user = await utils.processDbResponse(user, prunedEntities)
 
 			if (user && user.image) {
 				user.image = await utils.getDownloadableUrl(user.image)
 			}
 
-			// Return original identifier (email, phone, or username)
+			// Preserve original identifier for response
 			user.identifier = identifier
 			const result = { access_token: accessToken, refresh_token: refreshToken, user }
 
-			// Update session and Redis
+			// Update session storage / Redis
 			await userSessionsService.updateUserSessionAndsetRedisData(
 				userSessionDetails.result.id,
 				accessToken,
@@ -1989,10 +2041,14 @@ module.exports = class AccountHelper {
 			await userQueries.updateUser({ id: user.id, tenant_code: tenantCode }, updateParams)
 			//await UserCredentialQueries.updateUser({ email: userCredentials.email }, { password: bodyData.newPassword })
 
-			const redisUserKey = common.redisUserPrefix + tenantCode + '_' + user.id.toString()
-
-			// remove profile caching
-			await utils.redisDel(redisUserKey)
+			const ns = common.CACHE_CONFIG.namespaces.profile.name
+			const cacheKey = cacheClient.namespacedKey({
+				tenantCode,
+				orgId: orgCode,
+				ns,
+				id,
+			})
+			await cacheClient.del(cacheKey)
 
 			// remove reset otp caching
 			await utils.redisDel(user?.username)

From 406a70e382906ac1fe1fdcbd2cd64a13b281b9f0 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 15 Sep 2025 17:59:17 +0530
Subject: [PATCH 08/29] feat: implement versioning support in cache helper with
 TTL management and eviction methods

---
 src/generics/cacheHelper.js | 170 +++++++++++++++++++++++++++++++++++-
 1 file changed, 166 insertions(+), 4 deletions(-)

diff --git a/src/generics/cacheHelper.js b/src/generics/cacheHelper.js
index 75a068e02..0463c2df7 100644
--- a/src/generics/cacheHelper.js
+++ b/src/generics/cacheHelper.js
@@ -19,6 +19,10 @@ const SHARDS = toInt(CACHE_CONFIG.shards, 32)
 const BATCH = toInt(CACHE_CONFIG.scanBatch, 1000)
 const SHARD_RETENTION_DAYS = toInt(CACHE_CONFIG.shardRetentionDays, 7)
 
+// Version config
+const VERSION_CACHE_TTL = toInt(CACHE_CONFIG.versionCacheTtlSeconds, 5) // seconds, short in-process TTL
+const VERSION_DEFAULT = toInt(CACHE_CONFIG.versionDefault || 0, 0)
+
 /** Helpers */
 function toInt(v, d) {
 	const n = parseInt(v, 10)
@@ -72,12 +76,19 @@ function namespacedKey({ tenantCode, orgId, ns, id }) {
 	const base = orgId ? orgKey(tenantCode, orgId, []) : tenantKey(tenantCode, [])
 	return [base, ns, id].filter(Boolean).join(':')
 }
+
+async function versionedKey({ tenantCode, orgId, ns, id, key }) {
+	return buildVersionedKey({ tenantCode, orgId, ns, id, key })
+}
 function shardOf(key) {
 	const h = md5(key)
 	const asInt = parseInt(h.slice(0, 8), 16)
 	return (asInt >>> 0) % SHARDS
 }
 
+/** In-process short cache for version lookups */
+const _versionCache = new Map() // key -> { ver: number, expiresAt: timestamp }
+
 /** Low-level redis client (best-effort) */
 function getRedisClient() {
 	try {
@@ -87,6 +98,98 @@ function getRedisClient() {
 	}
 }
 
+/** Version key name resolution */
+function versionKeyName({ tenantCode, orgId, ns }) {
+	if (tenantCode && orgId && ns) return `__version:tenant:${tenantCode}:org:${orgId}:ns:${ns}`
+	if (tenantCode && ns) return `__version:tenant:${tenantCode}:ns:${ns}`
+	if (tenantCode) return `__version:tenant:${tenantCode}`
+	return `__version:global`
+}
+
+/** Get version from in-process cache or Redis (short TTL). */
+async function getVersion({ tenantCode, orgId, ns } = {}) {
+	const vKey = versionKeyName({ tenantCode, orgId, ns })
+	const cached = _versionCache.get(vKey)
+	if (cached && cached.expiresAt > Date.now()) return cached.ver
+
+	try {
+		// try Redis via wrapper
+		if (RedisCache && typeof RedisCache.getKey === 'function') {
+			const raw = await RedisCache.getKey(vKey)
+			const ver = raw ? parseInt(raw, 10) || VERSION_DEFAULT : VERSION_DEFAULT
+			_versionCache.set(vKey, { ver, expiresAt: Date.now() + VERSION_CACHE_TTL * 1000 })
+			return ver
+		}
+	} catch (e) {
+		console.error('getVersion redis read error', e)
+	}
+
+	// fallback default
+	_versionCache.set(vKey, { ver: VERSION_DEFAULT, expiresAt: Date.now() + VERSION_CACHE_TTL * 1000 })
+	return VERSION_DEFAULT
+}
+
+/**
+ * Bump version atomically for the given level.
+ * Preferred method is native Redis INCR. Fallback to read+set.
+ * Returns new version number.
+ */
+async function bumpVersion({ tenantCode, orgId, ns } = {}) {
+	const vKey = versionKeyName({ tenantCode, orgId, ns })
+	const redis = getRedisClient()
+	try {
+		if (redis && typeof redis.incr === 'function') {
+			// atomic increment
+			const newVer = await redis.incr(vKey)
+			// ensure wrapper reflects new value (best-effort)
+			try {
+				if (RedisCache && typeof RedisCache.setKey === 'function') {
+					await RedisCache.setKey(vKey, String(newVer))
+				}
+			} catch (_) {}
+			_versionCache.set(vKey, { ver: Number(newVer), expiresAt: Date.now() + VERSION_CACHE_TTL * 1000 })
+			return Number(newVer)
+		}
+		// fallback: read + increment + set
+		const currRaw = await RedisCache.getKey(vKey)
+		const curr = currRaw ? parseInt(currRaw, 10) || VERSION_DEFAULT : VERSION_DEFAULT
+		const newVer = curr + 1
+		await RedisCache.setKey(vKey, String(newVer))
+		_versionCache.set(vKey, { ver: newVer, expiresAt: Date.now() + VERSION_CACHE_TTL * 1000 })
+		return newVer
+	} catch (e) {
+		console.error('bumpVersion error', e)
+		// as last resort update in-memory and return incremented value
+		const currCached = _versionCache.get(vKey)
+		const curr = currCached ? currCached.ver : VERSION_DEFAULT
+		const newVer = curr + 1
+		_versionCache.set(vKey, { ver: newVer, expiresAt: Date.now() + VERSION_CACHE_TTL * 1000 })
+		try {
+			await RedisCache.setKey(vKey, String(newVer))
+		} catch (_) {}
+		return newVer
+	}
+}
+
+/** Build final key with version token inserted so patterns still match. */
+async function buildVersionedKey({ tenantCode, orgId, ns, id, key }) {
+	// If caller provided ns or id, treat as namespaced. Matches previous behaviour:
+	// previous code used ns || id ? namespacedKey({ ns: ns || 'ns', id: id||key }) : tenantKey(tenantCode, [key])
+	const isNamespaced = Boolean(ns || id)
+	if (isNamespaced) {
+		const effNs = ns || 'ns'
+		const ver = await getVersion({ tenantCode, orgId, ns: effNs })
+		const base = orgId ? orgKey(tenantCode, orgId, []) : tenantKey(tenantCode, [])
+		const final = [base, effNs, `v${ver}`, id || key].filter(Boolean).join(':')
+		return final
+	}
+	// tenant-level key
+	const ver = await getVersion({ tenantCode })
+	const base = tenantKey(tenantCode, [])
+	const final = [base, `v${ver}`, key].filter(Boolean).join(':')
+	return final
+}
+
 /** Base ops (Internal cache opt-in via config or caller) */
 async function get(key, { useInternal = false } = {}) {
 	if (!ENABLE_CACHE) return null
@@ -148,14 +251,18 @@ async function del(key, { useInternal = false } = {}) {
  * - fetchFn: function that returns value
  * - orgId, ns, id: for namespaced keys
  * - useInternal: optional boolean override. If omitted, resolved from namespace/config.
+ *
+ * NOTE: This function now resolves a versioned key internally.
  */
 async function getOrSet({ key, tenantCode, ttl = undefined, fetchFn, orgId, ns, id, useInternal = undefined }) {
 	if (!namespaceEnabled(ns)) return await fetchFn()
 
 	const resolvedUseInternal = nsUseInternal(ns, useInternal)
-	console.log('resolvedUseInternal', resolvedUseInternal, ns, useInternal)
+	// build versioned key (keeps previous behaviour but adds version token)
 	const fullKey =
-		ns || id ? namespacedKey({ tenantCode, orgId, ns: ns || 'ns', id: id || key }) : tenantKey(tenantCode, [key])
+		ns || id
+			? await buildVersionedKey({ tenantCode, orgId, ns: ns || 'ns', id: id || key })
+			: await buildVersionedKey({ tenantCode, key })
 
 	const cached = await get(fullKey, { useInternal: resolvedUseInternal })
 	if (cached !== null && cached !== undefined) return cached
@@ -167,15 +274,44 @@ async function getOrSet({ key, tenantCode, ttl = undefined, fetchFn, orgId, ns,
 	return value
 }
 
-/** Scoped set that uses namespace TTL and namespace useInternal setting */
+/** Scoped set that uses namespace TTL and namespace useInternal setting
+ * Returns the versioned key that was written.
+ */
 async function setScoped({ tenantCode, orgId, ns, id, value, ttl = undefined, useInternal = undefined }) {
 	if (!namespaceEnabled(ns)) return null
 	const resolvedUseInternal = nsUseInternal(ns, useInternal)
-	const fullKey = namespacedKey({ tenantCode, orgId, ns, id })
+	const fullKey = await buildVersionedKey({ tenantCode, orgId, ns, id })
 	await set(fullKey, value, nsTtl(ns, ttl), { useInternal: resolvedUseInternal })
 	return fullKey
 }
 
+/** Scoped delete that uses namespace config (TTL/useInternal)
+ * Returns the versioned key that was deleted.
+ */
+async function delScoped({ tenantCode, orgId, ns, id, useInternal = undefined }) {
+	if (!namespaceEnabled(ns)) return null
+	const resolvedUseInternal = nsUseInternal(ns, useInternal)
+	const fullKey = await buildVersionedKey({ tenantCode, orgId, ns, id })
+	await del(fullKey, { useInternal: resolvedUseInternal })
+	return fullKey
+}
+
+/**
+ * Evict all keys for a namespace.
+ * If orgId is provided will target org-level keys, otherwise tenant-level keys.
+ * patternSuffix defaults to '*' (delete all keys under the namespace).
+ *
+ * NOTE: Because version is a token between ns and id, the glob pattern `tenant:acme:users:*`
+ * will match versioned keys like `tenant:acme:users:v3:...`.
+ */
+async function evictNamespace({ tenantCode, orgId = null, ns, patternSuffix = '*' } = {}) {
+	if (!tenantCode || !ns) return
+	if (!namespaceEnabled(ns)) return
+	const base = orgId ? `tenant:${tenantCode}:org:${orgId}` : `tenant:${tenantCode}`
+	const pattern = `${base}:${ns}:${patternSuffix}`
+	await scanAndDelete(pattern)
+}
+
 /**
  * Eviction helpers using SCAN by pattern.
  * These do not require any tracked sets. Caller should build patterns to match keys to remove.
@@ -226,6 +362,20 @@ async function evictTenantByPattern(tenantCode, { patternSuffix = '*' } = {}) {
 	await scanAndDelete(pattern)
 }
 
+/** Convenience invalidation by bumping version (fast) */
+async function invalidateNamespaceVersion({ tenantCode, orgId = null, ns } = {}) {
+	if (!tenantCode || !ns) return
+	return bumpVersion({ tenantCode, orgId, ns })
+}
+async function invalidateTenantVersion({ tenantCode } = {}) {
+	if (!tenantCode) return
+	return bumpVersion({ tenantCode })
+}
+async function invalidateOrgNamespaceVersion({ tenantCode, orgId, ns } = {}) {
+	if (!tenantCode || !orgId || !ns) return
+	return bumpVersion({ tenantCode, orgId, ns })
+}
+
 /** Public API */
 module.exports = {
 	// Base ops
@@ -238,12 +388,22 @@ module.exports = {
 	// Scoped helpers
 	setScoped,
 	namespacedKey,
+	versionedKey,
 
 	// Eviction (pattern based)
+	delScoped,
+	evictNamespace,
 	evictOrgByPattern,
 	evictTenantByPattern,
 	scanAndDelete,
 
+	// Versioning API
+	getVersion,
+	bumpVersion,
+	invalidateNamespaceVersion,
+	invalidateTenantVersion,
+	invalidateOrgNamespaceVersion,
+
 	// Introspection
 	_internal: {
 		getRedisClient,
@@ -251,5 +411,7 @@ module.exports = {
 		BATCH,
 		ENABLE_CACHE,
 		CACHE_CONFIG,
+		VERSION_CACHE_TTL,
+		VERSION_DEFAULT,
 	},
 }

From a1eb5d52724d2fb6228b964cb0ddfbc0c3ad0a26 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 15 Sep 2025 17:59:42 +0530
Subject: [PATCH 09/29] feat: enhance entity type management with cache
 invalidation on create, update, and delete operations

---
 src/services/entityType.js | 79 +++++++++++++++++++++++---------------
 1 file changed, 47 insertions(+), 32 deletions(-)

diff --git a/src/services/entityType.js b/src/services/entityType.js
index 955685af5..115efff18 100644
--- a/src/services/entityType.js
+++ b/src/services/entityType.js
@@ -1,21 +1,43 @@
-// DependenciesI
+// Dependencies
 const httpStatusCode = require('@generics/http-status')
 const common = require('@constants/common')
 const entityTypeQueries = require('@database/queries/entityType')
 const { UniqueConstraintError } = require('sequelize')
-const organizationQueries = require('@database/queries/organization')
 const { Op } = require('sequelize')
 const { removeDefaultOrgEntityTypes } = require('@generics/utils')
 const responses = require('@helpers/responses')
+const cacheClient = require('@generics/cacheHelper')
+
 module.exports = class EntityHelper {
-	/**
-	 * Create entity type.
-	 * @method
-	 * @name create
-	 * @param {Object} bodyData - entity type body data.
-	 * @param {String} id -  id.
-	 * @returns {JSON} - Created entity type response.
-	 */
+	static async _invalidateEntityTypeCaches({ tenantCode, organizationCode }) {
+		try {
+			await cacheClient.invalidateOrgNamespaceVersion({
+				tenantCode,
+				orgId: organizationCode,
+				ns: common.CACHE_CONFIG.namespaces.entity_types.name,
+			})
+
+			await cacheClient.invalidateOrgNamespaceVersion({
+				tenantCode,
+				orgId: organizationCode,
+				ns: common.CACHE_CONFIG.namespaces.profile.name,
+			})
+
+			if (process.env.DEFAULT_ORGANISATION_CODE === organizationCode) {
+				await cacheClient.invalidateNamespaceVersion({
+					tenantCode,
+					ns: common.CACHE_CONFIG.namespaces.entity_types.name,
+				})
+				await cacheClient.invalidateNamespaceVersion({
+					tenantCode,
+					ns: common.CACHE_CONFIG.namespaces.profile.name,
+				})
+			}
+		} catch (err) {
+			console.error('Entity type cache invalidation failed', err)
+			// Do not throw. Caching failure should not block main operation.
+		}
+	}
 
 	static async create(bodyData, id, organizationCode, organizationId, tenantCode) {
 		bodyData.created_by = id
@@ -25,6 +47,10 @@ module.exports = class EntityHelper {
 		bodyData.tenant_code = tenantCode
 		try {
 			const entityType = await entityTypeQueries.createEntityType(bodyData)
+
+			// invalidate caches after successful create
+			await this._invalidateEntityTypeCaches({ tenantCode, organizationCode })
+
 			return responses.successResponse({
 				statusCode: httpStatusCode.created,
 				message: 'ENTITY_TYPE_CREATED_SUCCESSFULLY',
@@ -42,16 +68,6 @@ module.exports = class EntityHelper {
 		}
 	}
 
-	/**
-	 * Update entity type.
-	 * @method
-	 * @name update
-	 * @param {Object} bodyData -  body data.
-	 * @param {String} id - entity type id.
-	 * @param {String} loggedInUserId - logged in user id.
-	 * @returns {JSON} - Updated Entity Type.
-	 */
-
 	static async update(bodyData, id, loggedInUserId, organizationCode, tenantCode) {
 		;(bodyData.updated_by = loggedInUserId), (bodyData.organization_code = organizationCode)
 		try {
@@ -74,6 +90,9 @@ module.exports = class EntityHelper {
 				})
 			}
 
+			// invalidate caches after successful update
+			await this._invalidateEntityTypeCaches({ tenantCode, organizationCode })
+
 			return responses.successResponse({
 				statusCode: httpStatusCode.accepted,
 				message: 'ENTITY_TYPE_UPDATED_SUCCESSFULLY',
@@ -93,6 +112,8 @@ module.exports = class EntityHelper {
 
 	static async readAllSystemEntityTypes(organizationCode, tenantCode, organizationId) {
 		try {
+			await this._invalidateEntityTypeCaches({ tenantCode, organizationCode })
+
 			const attributes = ['value', 'label', 'id', 'organization_code']
 
 			const entities = await entityTypeQueries.findAllEntityTypes(
@@ -123,11 +144,10 @@ module.exports = class EntityHelper {
 
 	static async readUserEntityTypes(body, organizationCode, tenantCode, organizationId = '') {
 		try {
-			// Include tenant_code in filter for consistency with schema
 			const filter = {
 				value: body.value,
 				status: 'ACTIVE',
-				tenant_code: tenantCode, // Ensure tenant isolation
+				tenant_code: tenantCode,
 				organization_code: {
 					[Op.in]: [process.env.DEFAULT_ORGANISATION_CODE, organizationCode],
 				},
@@ -135,7 +155,6 @@ module.exports = class EntityHelper {
 
 			const entities = await entityTypeQueries.findUserEntityTypesAndEntities(filter)
 
-			// Deduplicate entity types by value, prioritizing orgId
 			const prunedEntities = removeDefaultOrgEntityTypes(entities, organizationId)
 
 			return responses.successResponse({
@@ -148,15 +167,8 @@ module.exports = class EntityHelper {
 			throw error
 		}
 	}
-	/**
-	 * Delete entity type.
-	 * @method
-	 * @name delete
-	 * @param {String} id - Delete entity type.
-	 * @returns {JSON} - Entity deleted response.
-	 */
-
-	static async delete(id, organizationCode) {
+
+	static async delete(id, organizationCode, tenantCode) {
 		try {
 			const deleteCount = await entityTypeQueries.deleteOneEntityType(id, organizationCode)
 			if (deleteCount === 0) {
@@ -167,6 +179,9 @@ module.exports = class EntityHelper {
 				})
 			}
 
+			// invalidate caches after successful delete
+			await this._invalidateEntityTypeCaches({ tenantCode, organizationCode })
+
 			return responses.successResponse({
 				statusCode: httpStatusCode.accepted,
 				message: 'ENTITY_TYPE_DELETED_SUCCESSFULLY',

From 072a8701099ec4425d76213858be863b6f9dd07e Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 15 Sep 2025 18:14:30 +0530
Subject: [PATCH 10/29] feat: add cache invalidation for entity operations on
 create, update, and delete

---
 src/services/entities.js | 43 +++++++++++++++++++++++++++++++++++-----
 1 file changed, 38 insertions(+), 5 deletions(-)

diff --git a/src/services/entities.js b/src/services/entities.js
index f517d4af1..17c41cdb7 100644
--- a/src/services/entities.js
+++ b/src/services/entities.js
@@ -1,11 +1,38 @@
 // Dependencies
 const httpStatusCode = require('@generics/http-status')
 const entityQueries = require('@database/queries/entities')
-const entityTypeQueries = require('@database/queries/entityType')
 const { UniqueConstraintError, ForeignKeyConstraintError } = require('sequelize')
 const responses = require('@helpers/responses')
+const common = require('@constants/common')
+const cacheClient = require('@generics/cacheHelper')
 
 module.exports = class EntityHelper {
+	static async _invalidateEntityCaches({ tenantCode, organizationCode }) {
+		try {
+			await cacheClient.invalidateOrgNamespaceVersion({
+				tenantCode,
+				orgId: organizationCode,
+				ns: common.CACHE_CONFIG.namespaces.entity_types.name,
+			})
+
+			await cacheClient.invalidateOrgNamespaceVersion({
+				tenantCode,
+				orgId: organizationCode,
+				ns: common.CACHE_CONFIG.namespaces.profile.name,
+			})
+
+			if (process.env.DEFAULT_ORGANISATION_CODE === organizationCode) {
+				await cacheClient.invalidateNamespaceVersion({
+					tenantCode,
+					ns: common.CACHE_CONFIG.namespaces.entity_types.name,
+				})
+			}
+		} catch (err) {
+			console.error('Entity cache invalidation failed', err)
+			// Do not throw. Cache failures should not block DB ops.
+		}
+	}
+
 	/**
 	 * Create entity.
 	 * @method
@@ -14,7 +41,6 @@ module.exports = class EntityHelper {
 	 * @param {String} id -  id.
 	 * @returns {JSON} - Entity created response.
 	 */
-
 	static async create(bodyData, userId, tenantCode, organizationCode) {
 		bodyData.created_by = userId
 		bodyData.updated_by = userId
@@ -24,6 +50,9 @@ module.exports = class EntityHelper {
 		try {
 			const entity = await entityQueries.createEntity(bodyData)
 
+			// invalidate caches after successful create
+			await this._invalidateEntityCaches({ tenantCode, organizationCode })
+
 			return responses.successResponse({
 				statusCode: httpStatusCode.created,
 				message: 'ENTITY_CREATED_SUCCESSFULLY',
@@ -59,7 +88,6 @@ module.exports = class EntityHelper {
 	 * @param {String} loggedInUserId - logged in user id.
 	 * @returns {JSON} - Entity updated response.
 	 */
-
 	static async update(bodyData, id, loggedInUserId, organizationCode, tenantCode) {
 		bodyData.updated_by = loggedInUserId
 		try {
@@ -81,6 +109,10 @@ module.exports = class EntityHelper {
 					responseCode: 'CLIENT_ERROR',
 				})
 			}
+
+			// invalidate caches after successful update
+			await this._invalidateEntityCaches({ tenantCode, organizationCode })
+
 			return responses.successResponse({
 				statusCode: httpStatusCode.accepted,
 				message: 'ENTITY_UPDATED_SUCCESSFULLY',
@@ -105,7 +137,6 @@ module.exports = class EntityHelper {
 	 * @param {Object} bodyData - entity body data.
 	 * @returns {JSON} - Entity read response.
 	 */
-
 	static async read(query, tenantCode) {
 		try {
 			let filter
@@ -149,7 +180,6 @@ module.exports = class EntityHelper {
 	 * @param {String} _id - Delete entity.
 	 * @returns {JSON} - Entity deleted response.
 	 */
-
 	static async delete(id, organizationCode, tenantCode) {
 		try {
 			const deleteCount = await entityQueries.deleteOneEntity(id, organizationCode, tenantCode)
@@ -162,6 +192,9 @@ module.exports = class EntityHelper {
 				})
 			}
 
+			// invalidate caches after successful delete
+			await this._invalidateEntityCaches({ tenantCode, organizationCode })
+
 			return responses.successResponse({
 				statusCode: httpStatusCode.accepted,
 				message: 'ENTITY_DELETED_SUCCESSFULLY',

From 83f3397f42e64979ee6f2110ddc6e991b9b0ee0a Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 15 Sep 2025 18:15:17 +0530
Subject: [PATCH 11/29] feat: implement caching for read operations of system
 and user entity types

---
 src/services/entityType.js | 73 ++++++++++++++++++++++++++------------
 1 file changed, 50 insertions(+), 23 deletions(-)

diff --git a/src/services/entityType.js b/src/services/entityType.js
index 115efff18..9e3d6ed2c 100644
--- a/src/services/entityType.js
+++ b/src/services/entityType.js
@@ -1,3 +1,4 @@
+// EntityHelper.js
 // Dependencies
 const httpStatusCode = require('@generics/http-status')
 const common = require('@constants/common')
@@ -110,28 +111,41 @@ module.exports = class EntityHelper {
 		}
 	}
 
+	// Read all system entity types (cached)
 	static async readAllSystemEntityTypes(organizationCode, tenantCode, organizationId) {
 		try {
-			await this._invalidateEntityTypeCaches({ tenantCode, organizationCode })
-
-			const attributes = ['value', 'label', 'id', 'organization_code']
+			const ns = common.CACHE_CONFIG.namespaces.entity_types.name
+			const cacheId = `all` // stable id under namespace; versioning handles invalidation
+			const fetchFn = async () => {
+				const attributes = ['value', 'label', 'id', 'organization_code']
+				const entities = await entityTypeQueries.findAllEntityTypes(
+					[organizationCode, process.env.DEFAULT_ORGANISATION_CODE],
+					attributes,
+					{
+						tenant_code: tenantCode,
+					}
+				)
+				const pruned = removeDefaultOrgEntityTypes(entities, organizationId)
+				return pruned
+			}
 
-			const entities = await entityTypeQueries.findAllEntityTypes(
-				[organizationCode, process.env.DEFAULT_ORGANISATION_CODE],
-				attributes,
-				{
-					tenant_code: tenantCode,
-				}
-			)
-			const prunedEntities = removeDefaultOrgEntityTypes(entities, organizationId)
+			const prunedEntities = await cacheClient.getOrSet({
+				key: cacheId,
+				tenantCode,
+				orgId: organizationCode,
+				ns,
+				id: cacheId,
+				fetchFn,
+			})
 
-			if (!prunedEntities.length) {
+			if (!prunedEntities || !prunedEntities.length) {
 				return responses.failureResponse({
 					message: 'ENTITY_TYPE_NOT_FOUND',
 					statusCode: httpStatusCode.bad_request,
 					responseCode: 'CLIENT_ERROR',
 				})
 			}
+
 			return responses.successResponse({
 				statusCode: httpStatusCode.ok,
 				message: 'ENTITY_TYPE_FETCHED_SUCCESSFULLY',
@@ -142,25 +156,38 @@ module.exports = class EntityHelper {
 		}
 	}
 
+	// Read user entity types by value (cached)
 	static async readUserEntityTypes(body, organizationCode, tenantCode, organizationId = '') {
 		try {
-			const filter = {
-				value: body.value,
-				status: 'ACTIVE',
-				tenant_code: tenantCode,
-				organization_code: {
-					[Op.in]: [process.env.DEFAULT_ORGANISATION_CODE, organizationCode],
-				},
+			const ns = common.CACHE_CONFIG.namespaces.entity_types.name
+			const cacheId = `user:value:${body.value}`
+			const fetchFn = async () => {
+				const filter = {
+					value: body.value,
+					status: 'ACTIVE',
+					tenant_code: tenantCode,
+					organization_code: {
+						[Op.in]: [process.env.DEFAULT_ORGANISATION_CODE, organizationCode],
+					},
+				}
+				const entities = await entityTypeQueries.findUserEntityTypesAndEntities(filter)
+				const pruned = removeDefaultOrgEntityTypes(entities, organizationId)
+				return { entity_types: pruned }
 			}
 
-			const entities = await entityTypeQueries.findUserEntityTypesAndEntities(filter)
-
-			const prunedEntities = removeDefaultOrgEntityTypes(entities, organizationId)
+			const result = await cacheClient.getOrSet({
+				key: cacheId,
+				tenantCode,
+				orgId: organizationCode,
+				ns,
+				id: cacheId,
+				fetchFn,
+			})
 
 			return responses.successResponse({
 				statusCode: httpStatusCode.ok,
 				message: 'ENTITY_TYPE_FETCHED_SUCCESSFULLY',
-				result: { entity_types: prunedEntities },
+				result,
 			})
 		} catch (error) {
 			console.error('Error in readUserEntityTypes:', error)

From 6d867ac3664087b1e3ba9faffdc7ba79a4c790fe Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 15 Sep 2025 18:15:34 +0530
Subject: [PATCH 12/29] feat: add default versioning and new namespaces for
 branding and organization in constants

---
 src/constants/common.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/constants/common.js b/src/constants/common.js
index 0335c471a..77abf15d5 100644
--- a/src/constants/common.js
+++ b/src/constants/common.js
@@ -117,10 +117,13 @@ module.exports = {
 		enableCache: true,
 		enableTracking: true,
 		shards: 32,
+		versionDefault: 0,
 		namespaces: {
 			profile: { name: 'profile', enabled: true, defaultTtl: 43200, useInternal: false },
 			entity_types: { name: 'entity_types', enabled: true, defaultTtl: 86400, useInternal: false },
 			tenant: { name: 'tenant', enabled: true, defaultTtl: 86400, useInternal: false },
+			branding: { name: 'branding', enabled: true, defaultTtl: 86400, useInternal: false },
+			organization: { name: 'organization', enabled: true, defaultTtl: 86400, useInternal: false },
 		},
 	},
 }

From e6739a320f98c20fdad743c3d7f5764905e87c16 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 15 Sep 2025 18:19:52 +0530
Subject: [PATCH 13/29] feat: update caching mechanism to use versioned keys
 for user profile retrieval

---
 src/services/user.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/services/user.js b/src/services/user.js
index 5fe5c0575..dc76fbe1f 100644
--- a/src/services/user.js
+++ b/src/services/user.js
@@ -308,15 +308,15 @@ module.exports = class UserHelper {
 			const filter = utils.isNumeric(id) ? { id, tenant_code: tenantCode } : { share_link: id }
 
 			const ns = common.CACHE_CONFIG.namespaces.profile.name
-			const cacheKey = cacheClient.namespacedKey({
+			const fullKey = await cacheClient.versionedKey({
 				tenantCode,
 				orgId: organizationCode,
 				ns,
 				id,
 			})
 
-			// Try cache first
-			let userDetails = await cacheClient.get(cacheKey)
+			const userDetails = await cacheClient.get(fullKey)
+
 			if (userDetails) {
 				if (userDetails.image) userDetails.image = await utils.getDownloadableUrl(userDetails.image)
 				return responses.successResponse({

From b6b80aae06f4aa0a6be6653b34ff4e4018d23f50 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 15 Sep 2025 18:29:08 +0530
Subject: [PATCH 14/29] feat: implement cache clearing for deleted user
 profiles

---
 src/helpers/userHelper.js | 17 +++++++++++++++--
 1 file changed, 15 insertions(+), 2 deletions(-)

diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js
index d3c160988..2123eb8a6 100644
--- a/src/helpers/userHelper.js
+++ b/src/helpers/userHelper.js
@@ -1,11 +1,11 @@
 const common = require('@constants/common')
+const cacheClient = require('@generics/cacheHelper')
 const utils = require('@generics/utils')
 const userQueries = require('@database/queries/users')
 const userOrganizationQueries = require('@database/queries/userOrganization')
 const userOrganizationRoleQueries = require('@database/queries/userOrganizationRole')
 const userSessionsService = require('@services/user-sessions')
 const Sequelize = require('@database/models/index').sequelize
-const REDIS_USER_PREFIX = common.redisUserPrefix
 const DELETED_STATUS = common.DELETED_STATUS
 const userSessionsQueries = require('@database/queries/user-sessions')
 
@@ -77,7 +77,6 @@ const userHelper = {
 					{ user_id: user.id, tenant_code: user.tenant_code },
 					{ transaction }
 				)
-				await utils.redisDel([`${REDIS_USER_PREFIX}${user.tenant_code}_${userId}`])
 
 				const userSessionData = await userSessionsService.findUserSession(
 					{
@@ -91,6 +90,20 @@ const userHelper = {
 				)
 				const userSessionIds = userSessionData.map(({ id }) => id)
 				await userSessionsService.removeUserSessions(userSessionIds)
+
+				// Clear cache entries for this user
+				try {
+					const ns = common.CACHE_CONFIG.namespaces.profile.name
+					const fullKey = await cacheClient.versionedKey({
+						tenantCode: user.tenant_code,
+						orgId: user.organization_code,
+						ns,
+						id: userId,
+					})
+					await cacheClient.del(fullKey)
+				} catch (err) {
+					console.error('Failed to delete user cache', err)
+				}
 			},
 			transactionOptions
 		)

From 18465cf98fdc501ffd597c3eaeb8fe431b9fb317 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Tue, 16 Sep 2025 12:15:31 +0530
Subject: [PATCH 15/29] fix: update cache key organization ID reference in
 account and user services

---
 src/services/account.js | 2 +-
 src/services/user.js    | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/services/account.js b/src/services/account.js
index 41d586f77..c32626866 100644
--- a/src/services/account.js
+++ b/src/services/account.js
@@ -2044,7 +2044,7 @@ module.exports = class AccountHelper {
 			const ns = common.CACHE_CONFIG.namespaces.profile.name
 			const cacheKey = cacheClient.namespacedKey({
 				tenantCode,
-				orgId: orgCode,
+				orgId: organizationCode,
 				ns,
 				id,
 			})
diff --git a/src/services/user.js b/src/services/user.js
index dc76fbe1f..475f0a859 100644
--- a/src/services/user.js
+++ b/src/services/user.js
@@ -587,7 +587,7 @@ module.exports = class UserHelper {
 			const ns = common.CACHE_CONFIG.namespaces.profile.name
 			const cacheKey = cacheClient.namespacedKey({
 				tenantCode,
-				orgId: orgCode,
+				orgId: organizationCode,
 				ns,
 				id,
 			})

From b1f600214314353ec09f4b8c3a4968cf8f22a9df Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Tue, 16 Sep 2025 12:33:56 +0530
Subject: [PATCH 16/29] fix: user id reference in change password

---
 src/services/account.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/services/account.js b/src/services/account.js
index c32626866..631fd79f4 100644
--- a/src/services/account.js
+++ b/src/services/account.js
@@ -2046,7 +2046,7 @@ module.exports = class AccountHelper {
 				tenantCode,
 				orgId: organizationCode,
 				ns,
-				id,
+				id: userId,
 			})
 			await cacheClient.del(cacheKey)
 

From e23df979cafebcaf6a6e9846beea3b6e34a2040f Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 22 Sep 2025 20:46:24 +0530
Subject: [PATCH 17/29] feat: enhance organization service to enforce tenant
 code in related org operations

---
 src/controllers/v1/organization.js   |  13 +-
 src/database/queries/organization.js |  48 +++----
 src/services/organization.js         | 182 +++++++++++++++++++++------
 3 files changed, 181 insertions(+), 62 deletions(-)

diff --git a/src/controllers/v1/organization.js b/src/controllers/v1/organization.js
index e56eb6ea5..24e1dcbcf 100644
--- a/src/controllers/v1/organization.js
+++ b/src/controllers/v1/organization.js
@@ -92,7 +92,12 @@ module.exports = class Organization {
 				})
 			}
 
-			const updatedOrg = await orgService.update(req.params.id, req.body, req.decodedToken.id)
+			const updatedOrg = await orgService.update(
+				req.params.id,
+				req.body,
+				req.decodedToken.id,
+				req.decodedToken.tenant_code
+			)
 			return updatedOrg
 		} catch (error) {
 			return error
@@ -188,7 +193,8 @@ module.exports = class Organization {
 		try {
 			const result = await orgService.addRelatedOrg(
 				req.params.id ? req.params.id : '',
-				req.body.related_orgs ? req.body.related_orgs : []
+				req.body.related_orgs ? req.body.related_orgs : [],
+				req.decodedToken.tenant_code
 			)
 			return result
 		} catch (error) {
@@ -199,7 +205,8 @@ module.exports = class Organization {
 		try {
 			const result = await orgService.removeRelatedOrg(
 				req.params.id ? req.params.id : '',
-				req.body.related_orgs ? req.body.related_orgs : []
+				req.body.related_orgs ? req.body.related_orgs : [],
+				req.decodedToken.tenant_code
 			)
 			return result
 		} catch (error) {
diff --git a/src/database/queries/organization.js b/src/database/queries/organization.js
index 0e6d4d9ee..6dd5837a5 100644
--- a/src/database/queries/organization.js
+++ b/src/database/queries/organization.js
@@ -111,35 +111,36 @@ exports.update = async (filter, update, options = {}) => {
 	}
 }
 
-exports.appendRelatedOrg = async (relatedOrg, ids, options = {}) => {
+exports.appendRelatedOrg = async (relatedOrg, ids, tenantCode, options = {}) => {
 	try {
+		const whereClause = {
+			id: ids,
+			tenant_code: tenantCode, // enforce tenant
+			[Op.or]: [
+				{
+					[Op.not]: {
+						related_orgs: {
+							[Op.contains]: [relatedOrg],
+						},
+					},
+				},
+				{
+					related_orgs: {
+						[Op.is]: null,
+					},
+				},
+			],
+		}
 		const result = await Organization.update(
 			{
 				related_orgs: sequelize.fn('array_append', sequelize.col('related_orgs'), relatedOrg),
 			},
 			{
-				where: {
-					id: ids,
-					[Op.or]: [
-						{
-							[Op.not]: {
-								related_orgs: {
-									[Op.contains]: [relatedOrg],
-								},
-							},
-						},
-						{
-							related_orgs: {
-								[Op.is]: null,
-							},
-						},
-					],
-				},
+				where: whereClause,
 				...options,
 				individualHooks: true,
 			}
 		)
-
 		const [rowsAffected, updatedRows] = result
 		return options.returning ? { rowsAffected, updatedRows } : rowsAffected
 	} catch (error) {
@@ -148,13 +149,17 @@ exports.appendRelatedOrg = async (relatedOrg, ids, options = {}) => {
 	}
 }
 
-exports.removeRelatedOrg = async (removedOrgIds, ids, options = {}) => {
+// (removedOrgIds, ids, tenantCode, options = {})
+exports.removeRelatedOrg = async (removedOrgIds, ids, tenantCode, options = {}) => {
 	try {
 		const result = await Organization.update(
-			{ related_orgs: sequelize.fn('array_remove', sequelize.col('related_orgs'), removedOrgIds) },
+			{
+				related_orgs: sequelize.fn('array_remove', sequelize.col('related_orgs'), removedOrgIds),
+			},
 			{
 				where: {
 					id: ids,
+					tenant_code: tenantCode, // enforce same tenant
 				},
 				...options,
 				individualHooks: true,
@@ -168,6 +173,7 @@ exports.removeRelatedOrg = async (removedOrgIds, ids, options = {}) => {
 		throw error
 	}
 }
+
 exports.listOrganizations = async (page, limit, search) => {
 	try {
 		let filterQuery = {
diff --git a/src/services/organization.js b/src/services/organization.js
index c83a854b0..dc5459d05 100644
--- a/src/services/organization.js
+++ b/src/services/organization.js
@@ -19,6 +19,7 @@ const { eventBodyDTO } = require('@dtos/eventBody')
 const organizationDTO = require('@dtos/organizationDTO')
 const responses = require('@helpers/responses')
 const userOrgQueries = require('@database/queries/userOrganization')
+const cacheClient = require('@generics/cacheHelper')
 
 module.exports = class OrganizationsHelper {
 	/**
@@ -177,7 +178,7 @@ module.exports = class OrganizationsHelper {
 	 * @returns {JSON} - Update Organization data.
 	 */
 
-	static async update(id, bodyData, loggedInUserId) {
+	static async update(id, bodyData, loggedInUserId, tenantCode) {
 		try {
 			bodyData.updated_by = loggedInUserId
 			if (bodyData.relatedOrgs) {
@@ -192,7 +193,24 @@ module.exports = class OrganizationsHelper {
 				})
 			}
 			const orgDetails = await organizationQueries.update({ id: id }, bodyData, { returning: true, raw: true })
-
+			await cacheClient
+				.invalidateOrgNamespaceVersion({
+					tenantCode,
+					orgId: orgDetailsBeforeUpdate.code,
+					ns: common.CACHE_CONFIG.namespaces.organization.name,
+				})
+				.catch((error) => {
+					console.error(error)
+				})
+			await cacheClient
+				.invalidateOrgNamespaceVersion({
+					tenantCode,
+					orgId: orgDetailsBeforeUpdate.code,
+					ns: common.CACHE_CONFIG.namespaces.profile.name,
+				})
+				.catch((error) => {
+					console.error(error)
+				})
 			let domains = []
 			if (bodyData.domains?.length) {
 				let existingDomains = await orgDomainQueries.findAll({
@@ -501,10 +519,10 @@ module.exports = class OrganizationsHelper {
 		}
 	}
 
-	static async addRelatedOrg(id, relatedOrgs = []) {
+	static async addRelatedOrg(id, relatedOrgs = [], tenantCode) {
 		try {
-			// fetch organization details before update
-			const orgDetailsBeforeUpdate = await organizationQueries.findOne({ id })
+			// fetch organization details before update (ensures tenant_code checked)
+			const orgDetailsBeforeUpdate = await organizationQueries.findOne({ id, tenant_code: tenantCode })
 			if (!orgDetailsBeforeUpdate) {
 				return responses.failureResponse({
 					statusCode: httpStatusCode.not_acceptable,
@@ -512,15 +530,44 @@ module.exports = class OrganizationsHelper {
 					message: 'ORGANIZATION_NOT_FOUND',
 				})
 			}
-			// append related organizations and make sure it is unique
-			let newRelatedOrgs = [...new Set([...(orgDetailsBeforeUpdate?.related_orgs ?? []), ...relatedOrgs])]
+
+			// normalize and remove self if present
+			let requestedRelated = Array.isArray(relatedOrgs) ? relatedOrgs.map(Number) : []
+			requestedRelated = [...new Set(requestedRelated)].filter((rid) => rid && rid !== Number(id))
+
+			if (requestedRelated.length === 0) {
+				return responses.successResponse({
+					statusCode: httpStatusCode.accepted,
+					message: 'ORGANIZATION_UPDATED_SUCCESSFULLY',
+				})
+			}
+
+			const relatedOrgRecords = await organizationQueries.findAll(
+				{ id: requestedRelated, tenant_code: tenantCode },
+				{ raw: true }
+			)
+
+			// not found or tenant mismatch (because tenant_code filter applied)
+			const foundIds = relatedOrgRecords.map((r) => Number(r.id))
+			const notFound = requestedRelated.filter((rid) => !foundIds.includes(Number(rid)))
+			if (notFound.length) {
+				return responses.failureResponse({
+					statusCode: httpStatusCode.not_acceptable,
+					responseCode: 'CLIENT_ERROR',
+					message: `RELATED_ORGANIZATIONS_NOT_FOUND`,
+				})
+			}
+
+			// append related organizations and make sure unique (local copy)
+			let newRelatedOrgs = [...new Set([...(orgDetailsBeforeUpdate?.related_orgs ?? []), ...requestedRelated])]
 
 			// check if there are any addition to related_org
 			if (!_.isEqual(orgDetailsBeforeUpdate?.related_orgs, newRelatedOrgs)) {
-				// update org related orgs
-				await organizationQueries.update(
+				// update org related orgs (scoped by tenant)
+				const updatedOrg = await organizationQueries.update(
 					{
 						id,
+						tenant_code: tenantCode,
 					},
 					{
 						related_orgs: newRelatedOrgs,
@@ -530,13 +577,44 @@ module.exports = class OrganizationsHelper {
 						raw: true,
 					}
 				)
-				// update related orgs to append org Id
-				await organizationQueries.appendRelatedOrg(id, newRelatedOrgs, {
+
+				// update related orgs to append org Id. scoped to same tenant
+				const updatedRelatedOrgs = await organizationQueries.appendRelatedOrg(id, newRelatedOrgs, tenantCode, {
 					returning: true,
 					raw: true,
 				})
+
 				const deltaOrgs = _.difference(newRelatedOrgs, orgDetailsBeforeUpdate?.related_orgs)
 
+				// build list of orgs with their tenant codes
+				const orgsToInvalidateRecords = [...updatedOrg.updatedRows, ...updatedRelatedOrgs.updatedRows].map(
+					(r) => ({
+						orgId: r.code,
+						tenantCode: r.tenant_code,
+					})
+				)
+
+				const { organization, profile } = common.CACHE_CONFIG.namespaces
+				const { invalidateOrgNamespaceVersion } = cacheClient
+
+				const tasks = orgsToInvalidateRecords.flatMap(({ orgId, tenantCode: tCode }) =>
+					[organization.name, profile.name].map((ns) => ({
+						orgId,
+						tenantCode: tCode,
+						ns,
+						promise: invalidateOrgNamespaceVersion({ tenantCode: tCode, orgId, ns }),
+					}))
+				)
+
+				const results = await Promise.allSettled(tasks.map((t) => t.promise))
+
+				results.forEach((res, i) => {
+					if (res.status === 'rejected') {
+						const { orgId, tenantCode, ns } = tasks[i]
+						console.error(`invalidate failed for org ${orgId} (tenant ${tenantCode}) ns: ${ns}`, res.reason)
+					}
+				})
+
 				eventBroadcaster('updateRelatedOrgs', {
 					requestBody: {
 						delta_organization_ids: deltaOrgs,
@@ -551,13 +629,15 @@ module.exports = class OrganizationsHelper {
 				message: 'ORGANIZATION_UPDATED_SUCCESSFULLY',
 			})
 		} catch (error) {
+			console.log(error)
 			throw error
 		}
 	}
-	static async removeRelatedOrg(id, relatedOrgs = []) {
+
+	static async removeRelatedOrg(id, relatedOrgs = [], tenantCode) {
 		try {
 			// fetch organization details before update
-			const orgDetailsBeforeUpdate = await organizationQueries.findOne({ id })
+			const orgDetailsBeforeUpdate = await organizationQueries.findOne({ id, tenant_code: tenantCode })
 			if (!orgDetailsBeforeUpdate) {
 				return responses.failureResponse({
 					statusCode: httpStatusCode.not_acceptable,
@@ -565,50 +645,76 @@ module.exports = class OrganizationsHelper {
 					message: 'ORGANIZATION_NOT_FOUND',
 				})
 			}
-			if (orgDetailsBeforeUpdate?.related_orgs == null) {
+			if (
+				!Array.isArray(orgDetailsBeforeUpdate.related_orgs) ||
+				orgDetailsBeforeUpdate.related_orgs.length === 0
+			) {
 				return responses.failureResponse({
 					statusCode: httpStatusCode.not_acceptable,
 					responseCode: 'CLIENT_ERROR',
 					message: 'RELATED_ORG_REMOVAL_FAILED',
 				})
 			}
-			const relatedOrganizations = _.difference(orgDetailsBeforeUpdate?.related_orgs, relatedOrgs)
 
-			// check if the given org ids are present in the organization's related org
-			const relatedOrgMismatchFlag = relatedOrgs.some(
-				(orgId) => !orgDetailsBeforeUpdate?.related_orgs.includes(orgId)
-			)
-
-			if (relatedOrgMismatchFlag) {
+			// ensure the orgs to remove exist in same tenant
+			const relatedOrgRecords = await organizationQueries.findAll({
+				where: { id: relatedOrgs, tenant_code: tenantCode },
+				raw: true,
+			})
+			const foundIds = relatedOrgRecords.map((r) => Number(r.id))
+			const notFound = relatedOrgs.filter((rid) => !foundIds.includes(Number(rid)))
+			if (notFound.length) {
 				return responses.failureResponse({
 					statusCode: httpStatusCode.not_acceptable,
 					responseCode: 'CLIENT_ERROR',
-					message: 'RELATED_ORG_REMOVAL_FAILED',
+					message: `RELATED_ORG_REMOVAL_FAILED_NOT_FOUND_OR_DIFFERENT_TENANT: ${notFound.join(',')}`,
 				})
 			}
 
-			// check if there are any addition to related_org
-			if (!_.isEqual(orgDetailsBeforeUpdate?.related_orgs, relatedOrganizations)) {
-				// update org remove related orgs
-				await organizationQueries.update(
-					{
-						id: parseInt(id, 10),
-					},
-					{
-						related_orgs: relatedOrganizations,
-					},
-					{
-						returning: true,
-						raw: true,
-					}
+			// recalc related orgs for this org
+			const newRelated = _.difference(orgDetailsBeforeUpdate.related_orgs, relatedOrgs)
+
+			if (!_.isEqual(orgDetailsBeforeUpdate.related_orgs, newRelated)) {
+				// update this org
+				const updatedOrg = await organizationQueries.update(
+					{ id, tenant_code: tenantCode },
+					{ related_orgs: newRelated },
+					{ returning: true, raw: true }
 				)
 
-				// update related orgs remove orgId
-				await organizationQueries.removeRelatedOrg(id, relatedOrgs, {
+				// update reverse side (other orgs remove this org's id), scoped by tenant
+				const updatedRelatedOrgs = await organizationQueries.removeRelatedOrg(id, relatedOrgs, tenantCode, {
 					returning: true,
 					raw: true,
 				})
 
+				// invalidate cache and broadcast event
+				const orgsToInvalidateRecords = [...updatedOrg.updatedRows, ...updatedRelatedOrgs.updatedRows].map(
+					(r) => ({
+						orgId: r.code,
+						tenantCode: r.tenant_code,
+					})
+				)
+				const { organization, profile } = common.CACHE_CONFIG.namespaces
+				const { invalidateOrgNamespaceVersion } = cacheClient
+
+				const tasks = orgsToInvalidateRecords.flatMap(({ orgId, tenantCode: tCode }) =>
+					[organization.name, profile.name].map((ns) => ({
+						orgId,
+						tenantCode: tCode,
+						ns,
+						promise: invalidateOrgNamespaceVersion({ tenantCode: tCode, orgId, ns }),
+					}))
+				)
+
+				const results = await Promise.allSettled(tasks.map((t) => t.promise))
+				results.forEach((res, i) => {
+					if (res.status === 'rejected') {
+						const { orgId, tenantCode, ns } = tasks[i]
+						console.error(`invalidate failed for org ${orgId} (tenant ${tenantCode}) ns: ${ns}`, res.reason)
+					}
+				})
+
 				eventBroadcaster('updateRelatedOrgs', {
 					requestBody: {
 						delta_organization_ids: relatedOrgs,

From 4099fffc7cdd74ecfeb9c15b7bfa11d8295a7b26 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 22 Sep 2025 20:52:44 +0530
Subject: [PATCH 18/29] feat: update user role service to include organization
 code and invalidate cache on role updates and deletions

---
 src/constants/common.js         |  8 +++---
 src/controllers/v1/user-role.js |  2 ++
 src/services/user-role.js       | 46 +++++++++++++++++++++++++++++++--
 3 files changed, 50 insertions(+), 6 deletions(-)

diff --git a/src/constants/common.js b/src/constants/common.js
index 77abf15d5..a8b10d135 100644
--- a/src/constants/common.js
+++ b/src/constants/common.js
@@ -119,11 +119,11 @@ module.exports = {
 		shards: 32,
 		versionDefault: 0,
 		namespaces: {
-			profile: { name: 'profile', enabled: true, defaultTtl: 43200, useInternal: false },
+			profile: { name: 'profile', enabled: true, defaultTtl: 3600, useInternal: false },
 			entity_types: { name: 'entity_types', enabled: true, defaultTtl: 86400, useInternal: false },
-			tenant: { name: 'tenant', enabled: true, defaultTtl: 86400, useInternal: false },
-			branding: { name: 'branding', enabled: true, defaultTtl: 86400, useInternal: false },
-			organization: { name: 'organization', enabled: true, defaultTtl: 86400, useInternal: false },
+			tenant: { name: 'tenant', enabled: true, defaultTtl: 21600, useInternal: false },
+			branding: { name: 'branding', enabled: true, defaultTtl: 43200, useInternal: false },
+			organization: { name: 'organization', enabled: true, defaultTtl: 21600, useInternal: false },
 		},
 	},
 }
diff --git a/src/controllers/v1/user-role.js b/src/controllers/v1/user-role.js
index 7bd0dc4ec..aa2658e9b 100644
--- a/src/controllers/v1/user-role.js
+++ b/src/controllers/v1/user-role.js
@@ -61,6 +61,7 @@ module.exports = class userRole {
 				req.params.id,
 				req.body,
 				req.decodedToken.organization_id,
+				req.decodedToken.organization_code,
 				req.decodedToken.tenant_code
 			)
 			return updateRole
@@ -83,6 +84,7 @@ module.exports = class userRole {
 			return await roleService.delete(
 				req.params.id,
 				req.decodedToken.organization_id,
+				req.decodedToken.organization_code,
 				req.decodedToken.tenant_code
 			)
 		} catch (error) {
diff --git a/src/services/user-role.js b/src/services/user-role.js
index 4f0d93133..2f021a73e 100644
--- a/src/services/user-role.js
+++ b/src/services/user-role.js
@@ -14,6 +14,7 @@ const { Op } = require('sequelize')
 const organizationQueries = require('@database/queries/organization')
 const responses = require('@helpers/responses')
 const utils = require('@generics/utils')
+const cacheClient = require('@generics/cacheHelper')
 
 module.exports = class userRoleHelper {
 	/**
@@ -78,7 +79,7 @@ module.exports = class userRoleHelper {
 	 * @returns {Promise<Object>} - Updated role response.
 	 */
 
-	static async update(id, bodyData, userOrganizationId, tenantCode) {
+	static async update(id, bodyData, userOrganizationId, userOrganizationCode, tenantCode) {
 		try {
 			const filter = { id: id, organization_id: userOrganizationId, tenant_code: tenantCode }
 			const [updateCount, updateRole] = await roleQueries.updateRole(filter, bodyData)
@@ -89,11 +90,32 @@ module.exports = class userRoleHelper {
 					responseCode: 'CLIENT_ERROR',
 				})
 			}
+
+			await cacheClient
+				.invalidateOrgNamespaceVersion({
+					tenantCode,
+					orgId: userOrganizationCode,
+					ns: common.CACHE_CONFIG.namespaces.organization.name,
+				})
+				.catch((error) => {
+					console.error(error)
+				})
+			await cacheClient
+				.invalidateOrgNamespaceVersion({
+					tenantCode,
+					orgId: userOrganizationCode,
+					ns: common.CACHE_CONFIG.namespaces.profile.name,
+				})
+				.catch((error) => {
+					console.error(error)
+				})
+
 			return responses.successResponse({
 				statusCode: httpStatusCode.created,
 				message: 'ROLE_UPDATED_SUCCESSFULLY',
 				result: {
 					title: updateRole[0].title,
+					label: updateRole[0].label,
 					user_type: updateRole[0].user_type,
 					status: updateRole[0].status,
 					visibility: updateRole[0].visibility,
@@ -123,7 +145,7 @@ module.exports = class userRoleHelper {
 	 * @returns {Promise<Object>} - Deletion result response.
 	 */
 
-	static async delete(id, userOrganizationId, tenantCode) {
+	static async delete(id, userOrganizationId, userOrganizationCode, tenantCode) {
 		try {
 			const filter = { id: id, organization_id: userOrganizationId, tenant_code: tenantCode }
 			const deleteRole = await roleQueries.deleteRole(filter)
@@ -136,6 +158,26 @@ module.exports = class userRoleHelper {
 				})
 			}
 
+			await cacheClient
+				.invalidateOrgNamespaceVersion({
+					tenantCode,
+					orgId: userOrganizationCode,
+					ns: common.CACHE_CONFIG.namespaces.organization.name,
+				})
+				.catch((error) => {
+					console.error(error)
+				})
+			await cacheClient
+				.invalidateOrgNamespaceVersion({
+					tenantCode,
+					orgId: userOrganizationCode,
+					orgId: userOrganizationCode,
+					ns: common.CACHE_CONFIG.namespaces.profile.name,
+				})
+				.catch((error) => {
+					console.error(error)
+				})
+
 			return responses.successResponse({
 				statusCode: httpStatusCode.accepted,
 				message: 'ROLE_DELETED_SUCCESSFULLY',

From 1e45f47f0444e47173629c5682a4e2e8f15e2eb5 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 22 Sep 2025 20:53:12 +0530
Subject: [PATCH 19/29] feat: add cache invalidation for organization namespace
 version in account service

---
 src/generics/cacheHelper.js | 4 ++--
 src/services/account.js     | 6 ++++++
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/src/generics/cacheHelper.js b/src/generics/cacheHelper.js
index 0463c2df7..7f80aaf41 100644
--- a/src/generics/cacheHelper.js
+++ b/src/generics/cacheHelper.js
@@ -141,7 +141,7 @@ async function bumpVersion({ tenantCode, orgId, ns } = {}) {
 		if (redis && typeof redis.incr === 'function') {
 			// atomic increment
 			const newVer = await redis.incr(vKey)
-			// ensure wrapper reflects new value (best-effort)
+			// Ensure wrapper reflects new value (best-effort)
 			try {
 				if (RedisCache && typeof RedisCache.setKey === 'function') {
 					await RedisCache.setKey(vKey, String(newVer))
@@ -173,7 +173,7 @@ async function bumpVersion({ tenantCode, orgId, ns } = {}) {
 
 /** Build final key with version token inserted so patterns still match. */
 async function buildVersionedKey({ tenantCode, orgId, ns, id, key }) {
-	// If caller provided ns or id, treat as namespaced. Matches previous behaviour:
+	// If caller provided ns or id, treat as namespaced. Matches previous behavior:
 	// previous code used ns || id ? namespacedKey({ ns: ns || 'ns', id: id||key }) : tenantKey(tenantCode, [key])
 	const isNamespaced = Boolean(ns || id)
 	if (isNamespaced) {
diff --git a/src/services/account.js b/src/services/account.js
index 631fd79f4..ba6d6dafb 100644
--- a/src/services/account.js
+++ b/src/services/account.js
@@ -557,6 +557,12 @@ module.exports = class AccountHelper {
 						org_admin: orgAdmins,
 					}
 				)
+
+				cacheClient.invalidateOrgNamespaceVersion({
+					tenantCode: tenantDetail.code,
+					orgId: user.organization_id,
+					ns: common.CACHE_CONFIG.namespaces.organization.name,
+				})
 			}
 
 			const result = { access_token: accessToken, refresh_token: refreshToken, user }

From 887493fde1f4e8aeb6d89e2a86f7ee212661b35c Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 22 Sep 2025 20:53:48 +0530
Subject: [PATCH 20/29] feat: implement cache invalidation for organization and
 profile namespaces in admin service

---
 src/services/admin.js | 30 ++++++++++++++++++++++++++++++
 1 file changed, 30 insertions(+)

diff --git a/src/services/admin.js b/src/services/admin.js
index 21a44139c..d018a9ba9 100644
--- a/src/services/admin.js
+++ b/src/services/admin.js
@@ -37,6 +37,7 @@ const { eventBroadcaster } = require('@helpers/eventBroadcaster')
 const { generateUniqueUsername } = require('@utils/usernameGenerator')
 const responses = require('@helpers/responses')
 const userHelper = require('@helpers/userHelper')
+const cacheClient = require('@generics/cacheHelper')
 
 // DTOs
 const UserTransformDTO = require('@dtos/userDTO')
@@ -600,6 +601,20 @@ module.exports = class AdminHelper {
 				{ attributes: { exclude: ['created_at', 'updated_at', 'deleted_at'] } }
 			)
 
+			cacheClient.invalidateOrgNamespaceVersion({
+				tenantCode,
+				orgId: organizationId,
+				ns: common.CACHE_CONFIG.namespaces.organization.name,
+			})
+
+			const cacheKey = cacheClient.namespacedKey({
+				tenantCode,
+				orgId: organizationId,
+				ns: common.CACHE_CONFIG.namespaces.profile.name,
+				id: user.id,
+			})
+			await cacheClient.del(cacheKey)
+
 			// Broadcast event asynchronously
 			setImmediate(() =>
 				eventBroadcaster('updateOrganization', {
@@ -685,6 +700,21 @@ module.exports = class AdminHelper {
 				true // so we can get the user IDs
 			)
 
+			const namespaces = [
+				common.CACHE_CONFIG.namespaces.organization.name,
+				common.CACHE_CONFIG.namespaces.profile.name,
+			]
+
+			await Promise.allSettled(
+				namespaces.map((ns) => cacheClient.invalidateOrgNamespaceVersion({ tenantCode, orgId, ns }))
+			).then((results) =>
+				results.forEach((r, i) => {
+					if (r.status === 'rejected') {
+						console.error(`invalidate failed for ns=${namespaces[i]} org=${orgId}`, r.reason)
+					}
+				})
+			)
+
 			// 3. Broadcast & remove sessions if users were found
 			if (userRowsAffected > 0) {
 				const userIds = updatedUsers.map((u) => u.id)

From f857f40a7558678fbd108a98f0912ee623a81677 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 22 Sep 2025 20:55:03 +0530
Subject: [PATCH 21/29] feat: add cache invalidation for tenant updates in
 tenant helper

---
 src/services/public.js | 31 +++++++++++++++++++++++--------
 1 file changed, 23 insertions(+), 8 deletions(-)

diff --git a/src/services/public.js b/src/services/public.js
index 2a6a9f135..b05a4b9bf 100644
--- a/src/services/public.js
+++ b/src/services/public.js
@@ -12,6 +12,7 @@ const { Op } = require('sequelize')
 const UserTransformDTO = require('@dtos/userDTO')
 const emailEncryption = require('@utils/emailEncryption')
 const common = require('@constants/common')
+const cacheClient = require('@generics/cacheHelper')
 
 module.exports = class AccountHelper {
 	static async tenantBranding(domain = null, organizationCode, tenantCode = null) {
@@ -32,16 +33,28 @@ module.exports = class AccountHelper {
 				code = tenantDomain?.tenant_code
 			}
 			if (!code) return notFoundResponse('TENANT_NOT_FOUND_PING_ADMIN')
-			const tenantDetail = await tenantQueries.findOne({ code }, {})
+
+			const tenantDetail = await cacheClient.getOrSet({
+				tenantCode: code, // ensures key is tenant-scoped
+				ns: common.CACHE_CONFIG.namespaces.tenant.name,
+				id: code, // unique per tenant
+				fetchFn: () => tenantQueries.findOne({ code }, {}),
+			})
 
 			if (!tenantDetail) {
 				return notFoundResponse('TENANT_NOT_FOUND_PING_ADMIN')
 			}
 			let orgDetails
 			if (organizationCode) {
-				orgDetails = await organizationQueries.findOne({
-					code: organizationCode,
-					tenant_code: tenantDomain.tenant_code,
+				orgDetails = await cacheClient.getOrSet({
+					tenantCode: code, // ensures key is tenant-scoped
+					ns: common.CACHE_CONFIG.namespaces.organization.name,
+					id: organizationCode, // unique per tenant
+					fetchFn: () =>
+						organizationQueries.findOne({
+							code: organizationCode,
+							tenant_code: code,
+						}),
 				})
 			}
 			return responses.successResponse({
@@ -77,10 +90,12 @@ module.exports = class AccountHelper {
 				return notFoundResponse('TENANT_DOMAIN_NOT_FOUND_PING_ADMIN')
 			}
 
-			const tenantDetail = await tenantQueries.findOne(
-				{ code: tenantDomain.tenant_code },
-				{ attributes: ['code'] }
-			)
+			const tenantDetail = await cacheClient.getOrSet({
+				tenantCode: code, // ensures key is tenant-scoped
+				ns: common.CACHE_CONFIG.namespaces.tenant.name,
+				id: code, // unique per tenant
+				fetchFn: () => tenantQueries.findOne({ code: tenantDomain.tenant_code }, {}),
+			})
 
 			if (!tenantDetail) {
 				return notFoundResponse('TENANT_NOT_FOUND_PING_ADMIN')

From 112fcbca80174ec8244b99c266e7fac42c13d88b Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 22 Sep 2025 20:55:11 +0530
Subject: [PATCH 22/29] feat: add cache invalidation for tenant updates in
 tenant helper

---
 src/services/tenant.js | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/services/tenant.js b/src/services/tenant.js
index 6562394a9..480009719 100644
--- a/src/services/tenant.js
+++ b/src/services/tenant.js
@@ -30,6 +30,7 @@ const utils = require('@generics/utils')
 const _ = require('lodash')
 const responses = require('@helpers/responses')
 const { Op } = require('sequelize')
+const cacheClient = require('@generics/cacheHelper')
 
 module.exports = class tenantHelper {
 	/**
@@ -438,6 +439,7 @@ module.exports = class tenantHelper {
 					tenantUpdateBody
 				)
 			}
+			await cacheClient.invalidateTenantVersion({ tenantCode }).catch(() => {})
 
 			return responses.successResponse({
 				statusCode: httpStatusCode.accepted,
@@ -519,6 +521,7 @@ module.exports = class tenantHelper {
 					result: {},
 				})
 			}
+			await cacheClient.invalidateTenantVersion({ tenantCode }).catch(() => {})
 
 			return responses.successResponse({
 				statusCode: httpStatusCode.accepted,
@@ -632,6 +635,7 @@ module.exports = class tenantHelper {
 			})
 
 			await Promise.all(domainRemovePromise)
+			await cacheClient.invalidateTenantVersion({ tenantCode }).catch(() => {})
 
 			return responses.successResponse({
 				statusCode: httpStatusCode.accepted,

From f640ade0c4517b1398490867270bc752494e0717 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 22 Sep 2025 20:55:17 +0530
Subject: [PATCH 23/29] feat: add message for related organizations not found
 in locale file

---
 src/locales/en.json | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/locales/en.json b/src/locales/en.json
index 21726f8aa..de0648a80 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -137,6 +137,7 @@
 	"TENANT_DOMAIN_ALREADY_EXISTS": "Tenant domain already exists.",
 	"TENANT_CREATED_SUCCESSFULLY": "Tenant created successfully.",
 	"DEFAULT_ORG_CREATION_FAILED": "Default Organization creation under tenant failed.",
+	"RELATED_ORGANIZATIONS_NOT_FOUND": "Related organisations not found!",
 	"TENANT_NOT_FOUND": "Tenant not found.",
 	"TENANT_UPDATED_SUCCESSFULLY": "Tenant updated successfully.",
 	"TENANT_DOMAINS_ALREADY_PRESENT": "All the domains are already added for the tenant",

From 0b57cc1f0cb403f818ef4403ea53eec05e85e595 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 22 Sep 2025 21:11:54 +0530
Subject: [PATCH 24/29] feat: ensure organization ID is included in cache key
 for organization details

---
 src/services/public.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/services/public.js b/src/services/public.js
index b05a4b9bf..88a892ff7 100644
--- a/src/services/public.js
+++ b/src/services/public.js
@@ -48,6 +48,7 @@ module.exports = class AccountHelper {
 			if (organizationCode) {
 				orgDetails = await cacheClient.getOrSet({
 					tenantCode: code, // ensures key is tenant-scoped
+					orgId: organizationCode, // ensures key is tenant-scoped
 					ns: common.CACHE_CONFIG.namespaces.organization.name,
 					id: organizationCode, // unique per tenant
 					fetchFn: () =>

From 04425bd54255b4c3ad15784a407aed2a35ffa5c3 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Mon, 22 Sep 2025 21:25:37 +0530
Subject: [PATCH 25/29] feat: add optional chaining for organizationAttributes
 and implement cache invalidation for user organization deletions

---
 src/database/queries/userOrganization.js |  2 +-
 src/helpers/userHelper.js                | 24 +++++++++++++++++-------
 2 files changed, 18 insertions(+), 8 deletions(-)

diff --git a/src/database/queries/userOrganization.js b/src/database/queries/userOrganization.js
index 1904c662c..7a9bb067a 100644
--- a/src/database/queries/userOrganization.js
+++ b/src/database/queries/userOrganization.js
@@ -106,7 +106,7 @@ exports.findOne = async (filter, options = {}) => {
 
 exports.findAll = async (filter = {}, options = {}) => {
 	try {
-		if (options.organizationAttributes.length > 0) {
+		if (options?.organizationAttributes?.length > 0) {
 			options.include = [
 				{
 					model: Organization,
diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js
index 2123eb8a6..289c3f2dc 100644
--- a/src/helpers/userHelper.js
+++ b/src/helpers/userHelper.js
@@ -69,6 +69,14 @@ const userHelper = {
 				const update = { ...rest, ...generateUpdateParams(userId) }
 
 				await userQueries.updateUser({ id: user.id }, update, { transaction })
+
+				// Capture org codes before delete for cache invalidation
+				const orgRows = await userOrganizationQueries.findAll(
+					{ user_id: user.id, tenant_code: user.tenant_code },
+					{ attributes: ['organization_code'], transaction }
+				)
+				const orgCodes = [...new Set(orgRows.map((r) => r.organization_code))]
+
 				await userOrganizationQueries.delete(
 					{ user_id: user.id, tenant_code: user.tenant_code },
 					{ transaction }
@@ -94,13 +102,15 @@ const userHelper = {
 				// Clear cache entries for this user
 				try {
 					const ns = common.CACHE_CONFIG.namespaces.profile.name
-					const fullKey = await cacheClient.versionedKey({
-						tenantCode: user.tenant_code,
-						orgId: user.organization_code,
-						ns,
-						id: userId,
-					})
-					await cacheClient.del(fullKey)
+					for (const orgId of orgCodes) {
+						const fullKey = await cacheClient.versionedKey({
+							tenantCode: user.tenant_code,
+							orgId,
+							ns,
+							id: userId,
+						})
+						await cacheClient.del(fullKey)
+					}
 				} catch (err) {
 					console.error('Failed to delete user cache', err)
 				}

From 931c0dec104c8f2c49f5a75ecc96ff92725c089f Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Tue, 23 Sep 2025 00:42:52 +0530
Subject: [PATCH 26/29] fix: improve user instance handling and correct cache
 invalidation parameters

---
 src/services/account.js      | 2 +-
 src/services/admin.js        | 6 ++++--
 src/services/organization.js | 8 ++++----
 src/services/user-role.js    | 1 -
 4 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/services/account.js b/src/services/account.js
index ba6d6dafb..2bf9f07ca 100644
--- a/src/services/account.js
+++ b/src/services/account.js
@@ -761,7 +761,7 @@ module.exports = class AccountHelper {
 
 			// Find user with org
 			const userInstance = await userQueries.findUserWithOrganization(query, {}, true)
-			let user = userInstance ? userInstance.toJSON() : null
+			let user = userInstance ? (userInstance.toJSON ? userInstance.toJSON() : userInstance) : null
 
 			if (!user) {
 				return responses.failureResponse({
diff --git a/src/services/admin.js b/src/services/admin.js
index d018a9ba9..933082764 100644
--- a/src/services/admin.js
+++ b/src/services/admin.js
@@ -697,7 +697,7 @@ module.exports = class AdminHelper {
 					status: common.INACTIVE_STATUS,
 					updated_by: loggedInUserId,
 				},
-				true // so we can get the user IDs
+				true // So we can get the user IDs
 			)
 
 			const namespaces = [
@@ -706,7 +706,9 @@ module.exports = class AdminHelper {
 			]
 
 			await Promise.allSettled(
-				namespaces.map((ns) => cacheClient.invalidateOrgNamespaceVersion({ tenantCode, orgId, ns }))
+				namespaces.map((ns) =>
+					cacheClient.invalidateOrgNamespaceVersion({ tenantCode, orgId: organizationCode, ns })
+				)
 			).then((results) =>
 				results.forEach((r, i) => {
 					if (r.status === 'rejected') {
diff --git a/src/services/organization.js b/src/services/organization.js
index dc5459d05..497765bb2 100644
--- a/src/services/organization.js
+++ b/src/services/organization.js
@@ -657,10 +657,10 @@ module.exports = class OrganizationsHelper {
 			}
 
 			// ensure the orgs to remove exist in same tenant
-			const relatedOrgRecords = await organizationQueries.findAll({
-				where: { id: relatedOrgs, tenant_code: tenantCode },
-				raw: true,
-			})
+			const relatedOrgRecords = await organizationQueries.findAll(
+				{ id: relatedOrgs, tenant_code: tenantCode },
+				{ raw: true }
+			)
 			const foundIds = relatedOrgRecords.map((r) => Number(r.id))
 			const notFound = relatedOrgs.filter((rid) => !foundIds.includes(Number(rid)))
 			if (notFound.length) {
diff --git a/src/services/user-role.js b/src/services/user-role.js
index 2f021a73e..b24480c68 100644
--- a/src/services/user-role.js
+++ b/src/services/user-role.js
@@ -171,7 +171,6 @@ module.exports = class userRoleHelper {
 				.invalidateOrgNamespaceVersion({
 					tenantCode,
 					orgId: userOrganizationCode,
-					orgId: userOrganizationCode,
 					ns: common.CACHE_CONFIG.namespaces.profile.name,
 				})
 				.catch((error) => {

From 89eada38ed8c212577174639df42f21ee5ec69ea Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Tue, 23 Sep 2025 01:03:33 +0530
Subject: [PATCH 27/29] feat: add namespace parameter to
 invalidateTenantVersion for improved cache management

---
 src/generics/cacheHelper.js |  4 ++--
 src/services/public.js      |  2 +-
 src/services/tenant.js      | 12 +++++++++---
 3 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/src/generics/cacheHelper.js b/src/generics/cacheHelper.js
index 7f80aaf41..75f2a2b75 100644
--- a/src/generics/cacheHelper.js
+++ b/src/generics/cacheHelper.js
@@ -367,9 +367,9 @@ async function invalidateNamespaceVersion({ tenantCode, orgId = null, ns } = {})
 	if (!tenantCode || !ns) return
 	return bumpVersion({ tenantCode, orgId, ns })
 }
-async function invalidateTenantVersion({ tenantCode } = {}) {
+async function invalidateTenantVersion({ tenantCode, ns } = {}) {
 	if (!tenantCode) return
-	return bumpVersion({ tenantCode })
+	return bumpVersion({ tenantCode, ns })
 }
 async function invalidateOrgNamespaceVersion({ tenantCode, orgId, ns } = {}) {
 	if (!tenantCode || !orgId || !ns) return
diff --git a/src/services/public.js b/src/services/public.js
index 88a892ff7..11b4c5663 100644
--- a/src/services/public.js
+++ b/src/services/public.js
@@ -59,7 +59,7 @@ module.exports = class AccountHelper {
 				})
 			}
 			return responses.successResponse({
-				statusCode: httpStatusCode.created,
+				statusCode: httpStatusCode.ok,
 				message: 'TENANT_DETAILS',
 				result: tenantTransformDTO.publicTransform({
 					tenant: tenantDetail,
diff --git a/src/services/tenant.js b/src/services/tenant.js
index 480009719..04ea9dacd 100644
--- a/src/services/tenant.js
+++ b/src/services/tenant.js
@@ -439,7 +439,9 @@ module.exports = class tenantHelper {
 					tenantUpdateBody
 				)
 			}
-			await cacheClient.invalidateTenantVersion({ tenantCode }).catch(() => {})
+			await cacheClient
+				.invalidateTenantVersion({ tenantCode, ns: common.CACHE_CONFIG.namespaces.tenant.name })
+				.catch(() => {})
 
 			return responses.successResponse({
 				statusCode: httpStatusCode.accepted,
@@ -521,7 +523,9 @@ module.exports = class tenantHelper {
 					result: {},
 				})
 			}
-			await cacheClient.invalidateTenantVersion({ tenantCode }).catch(() => {})
+			await cacheClient
+				.invalidateTenantVersion({ tenantCode, ns: common.CACHE_CONFIG.namespaces.tenant.name })
+				.catch(() => {})
 
 			return responses.successResponse({
 				statusCode: httpStatusCode.accepted,
@@ -635,7 +639,9 @@ module.exports = class tenantHelper {
 			})
 
 			await Promise.all(domainRemovePromise)
-			await cacheClient.invalidateTenantVersion({ tenantCode }).catch(() => {})
+			await cacheClient
+				.invalidateTenantVersion({ tenantCode, ns: common.CACHE_CONFIG.namespaces.tenant.name })
+				.catch(() => {})
 
 			return responses.successResponse({
 				statusCode: httpStatusCode.accepted,

From 428ac4c5e4185f47eb72d9c7b38b0e27f3fb05c3 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Tue, 23 Sep 2025 01:35:50 +0530
Subject: [PATCH 28/29] refactor: replace cache invalidation with eviction by
 pattern for entity types and profiles

---
 src/services/account.js    |  9 +--------
 src/services/entities.js   |  8 +++++---
 src/services/entityType.js | 10 ++++------
 3 files changed, 10 insertions(+), 17 deletions(-)

diff --git a/src/services/account.js b/src/services/account.js
index 2bf9f07ca..2aebfa7e4 100644
--- a/src/services/account.js
+++ b/src/services/account.js
@@ -695,14 +695,7 @@ module.exports = class AccountHelper {
 					responseCode: 'CLIENT_ERROR',
 				})
 
-			// Cache lookup for tenant domain
-			const tenantDomain = await cacheClient.getOrSet({
-				tenantCode: null,
-				orgId: null,
-				ns: common.CACHE_CONFIG.namespaces.tenant.name,
-				id: domain,
-				fetchFn: async () => tenantDomainQueries.findOne({ domain }),
-			})
+			const tenantDomain = await tenantDomainQueries.findOne({ domain })
 
 			if (!tenantDomain) {
 				return makeNotFoundResponse('TENANT_DOMAIN_NOT_FOUND_PING_ADMIN')
diff --git a/src/services/entities.js b/src/services/entities.js
index 17c41cdb7..7783e2a6c 100644
--- a/src/services/entities.js
+++ b/src/services/entities.js
@@ -22,9 +22,11 @@ module.exports = class EntityHelper {
 			})
 
 			if (process.env.DEFAULT_ORGANISATION_CODE === organizationCode) {
-				await cacheClient.invalidateNamespaceVersion({
-					tenantCode,
-					ns: common.CACHE_CONFIG.namespaces.entity_types.name,
+				await cacheClient.evictTenantByPattern(tenantCode, {
+					patternSuffix: `org:*:${common.CACHE_CONFIG.namespaces.entity_types.name}:*`,
+				})
+				await cacheClient.evictTenantByPattern(tenantCode, {
+					patternSuffix: `org:*:${common.CACHE_CONFIG.namespaces.profile.name}:*`,
 				})
 			}
 		} catch (err) {
diff --git a/src/services/entityType.js b/src/services/entityType.js
index 9e3d6ed2c..d0d62201a 100644
--- a/src/services/entityType.js
+++ b/src/services/entityType.js
@@ -25,13 +25,11 @@ module.exports = class EntityHelper {
 			})
 
 			if (process.env.DEFAULT_ORGANISATION_CODE === organizationCode) {
-				await cacheClient.invalidateNamespaceVersion({
-					tenantCode,
-					ns: common.CACHE_CONFIG.namespaces.entity_types.name,
+				await cacheClient.evictTenantByPattern(tenantCode, {
+					patternSuffix: `org:*:${common.CACHE_CONFIG.namespaces.entity_types.name}:*`,
 				})
-				await cacheClient.invalidateNamespaceVersion({
-					tenantCode,
-					ns: common.CACHE_CONFIG.namespaces.profile.name,
+				await cacheClient.evictTenantByPattern(tenantCode, {
+					patternSuffix: `org:*:${common.CACHE_CONFIG.namespaces.profile.name}:*`,
 				})
 			}
 		} catch (err) {

From 2c59b2ec822182eb1ffdf1dca49f9c335ea50f35 Mon Sep 17 00:00:00 2001
From: Nevil Mathew <nevil@tunerlabs.com>
Date: Tue, 23 Sep 2025 01:37:41 +0530
Subject: [PATCH 29/29] fix: update cache invalidation to use organization code
 instead of user organization ID

---
 src/services/account.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/services/account.js b/src/services/account.js
index 2aebfa7e4..6089b6064 100644
--- a/src/services/account.js
+++ b/src/services/account.js
@@ -560,7 +560,7 @@ module.exports = class AccountHelper {
 
 				cacheClient.invalidateOrgNamespaceVersion({
 					tenantCode: tenantDetail.code,
-					orgId: user.organization_id,
+					orgId: organization.code,
 					ns: common.CACHE_CONFIG.namespaces.organization.name,
 				})
 			}
