feat: deduct from redis new api schema
This commit is contained in:
677
server/src/_luaScripts/luaUtils/loadBalances.lua
Normal file
677
server/src/_luaScripts/luaUtils/loadBalances.lua
Normal file
@@ -0,0 +1,677 @@
|
||||
-- loadBalances.lua
|
||||
-- Shared function to load customer balances with merged entity balances (customer + entities)
|
||||
-- Returns: { [featureId] = { granted_balance, purchased_balance, current_balance, usage, ... } } or nil if not in cache
|
||||
|
||||
-- Helper function to safely convert values to numbers for arithmetic
|
||||
local function toNum(value)
|
||||
return type(value) == "number" and value or 0
|
||||
end
|
||||
|
||||
-- Helper function to parse HGETALL result into balance data object
|
||||
local function parseBalanceHash(balanceHash)
|
||||
local balanceData = {}
|
||||
|
||||
-- Define field types for parsing
|
||||
local numericFields = {
|
||||
granted_balance = true,
|
||||
purchased_balance = true,
|
||||
current_balance = true,
|
||||
usage = true,
|
||||
max_purchase = true,
|
||||
_breakdown_count = true,
|
||||
_rollover_count = true
|
||||
}
|
||||
|
||||
local booleanFields = {
|
||||
unlimited = true,
|
||||
overage_allowed = true
|
||||
}
|
||||
|
||||
local jsonFields = {
|
||||
feature = true,
|
||||
reset = true
|
||||
}
|
||||
|
||||
for i = 1, #balanceHash, 2 do
|
||||
local key = balanceHash[i]
|
||||
local value = balanceHash[i + 1]
|
||||
|
||||
-- Check for null first before parsing
|
||||
if value == "null" then
|
||||
balanceData[key] = cjson.null
|
||||
elseif numericFields[key] then
|
||||
balanceData[key] = tonumber(value)
|
||||
elseif booleanFields[key] then
|
||||
balanceData[key] = (value == "true")
|
||||
elseif jsonFields[key] then
|
||||
-- Parse JSON fields (feature object and reset object)
|
||||
if value ~= "null" and value ~= "" then
|
||||
balanceData[key] = cjson.decode(value)
|
||||
else
|
||||
balanceData[key] = cjson.null
|
||||
end
|
||||
else
|
||||
balanceData[key] = value
|
||||
end
|
||||
end
|
||||
return balanceData
|
||||
end
|
||||
|
||||
|
||||
-- Helper function to fetch and parse rollover items
|
||||
-- Returns: array of rollover data objects, or nil if any key is missing (partial eviction)
|
||||
-- cacheKey: base cache key (customer or entity cache key)
|
||||
-- featureId: feature ID
|
||||
-- rolloverCount: number of rollover items to fetch
|
||||
local function fetchRollovers(cacheKey, featureId, rolloverCount)
|
||||
local rollovers = {}
|
||||
for i = 0, rolloverCount - 1 do
|
||||
local rolloverKey = buildRolloverCacheKey(cacheKey, featureId, i)
|
||||
local rolloverHash = redis.call("HGETALL", rolloverKey)
|
||||
|
||||
-- If rollover key is missing, return nil (partial eviction detected)
|
||||
if #rolloverHash == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local rolloverData = {}
|
||||
for j = 1, #rolloverHash, 2 do
|
||||
local key = rolloverHash[j]
|
||||
local value = rolloverHash[j + 1]
|
||||
|
||||
if value == "null" then
|
||||
rolloverData[key] = cjson.null
|
||||
elseif key == "balance" or key == "expires_at" then
|
||||
rolloverData[key] = tonumber(value)
|
||||
else
|
||||
rolloverData[key] = value
|
||||
end
|
||||
end
|
||||
table.insert(rollovers, rolloverData)
|
||||
end
|
||||
return rollovers
|
||||
end
|
||||
|
||||
-- Helper function to fetch and parse breakdown items
|
||||
-- Returns: array of breakdown data objects, or nil if any key is missing (partial eviction)
|
||||
-- cacheKey: base cache key (customer or entity cache key)
|
||||
-- featureId: feature ID
|
||||
-- breakdownCount: number of breakdown items to fetch
|
||||
local function fetchBreakdown(cacheKey, featureId, breakdownCount)
|
||||
local breakdown = {}
|
||||
|
||||
-- Define field types for parsing breakdown items
|
||||
local breakdownNumericFields = {
|
||||
granted_balance = true,
|
||||
purchased_balance = true,
|
||||
current_balance = true,
|
||||
usage = true,
|
||||
max_purchase = true
|
||||
}
|
||||
|
||||
local breakdownBooleanFields = {
|
||||
overage_allowed = true
|
||||
}
|
||||
|
||||
local breakdownJsonFields = {
|
||||
reset = true
|
||||
}
|
||||
|
||||
for i = 0, breakdownCount - 1 do
|
||||
local breakdownKey = buildBreakdownCacheKey(cacheKey, featureId, i)
|
||||
local breakdownHash = redis.call("HGETALL", breakdownKey)
|
||||
|
||||
-- If breakdown key is missing, return nil (partial eviction detected)
|
||||
if #breakdownHash == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local breakdownData = {}
|
||||
for j = 1, #breakdownHash, 2 do
|
||||
local key = breakdownHash[j]
|
||||
local value = breakdownHash[j + 1]
|
||||
|
||||
if value == "null" then
|
||||
breakdownData[key] = cjson.null
|
||||
elseif breakdownNumericFields[key] then
|
||||
breakdownData[key] = tonumber(value)
|
||||
elseif breakdownBooleanFields[key] then
|
||||
breakdownData[key] = (value == "true")
|
||||
elseif breakdownJsonFields[key] then
|
||||
-- Parse reset JSON object
|
||||
if value ~= "null" and value ~= "" then
|
||||
breakdownData[key] = cjson.decode(value)
|
||||
else
|
||||
breakdownData[key] = cjson.null
|
||||
end
|
||||
else
|
||||
breakdownData[key] = value
|
||||
end
|
||||
end
|
||||
table.insert(breakdown, breakdownData)
|
||||
end
|
||||
return breakdown
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- MERGE BALANCE UTILITIES
|
||||
-- ============================================================================
|
||||
|
||||
-- Helper function to merge numeric balance fields (sums values)
|
||||
-- Mutates target by adding source's numeric fields
|
||||
local function mergeBalanceNumericFields(target, source)
|
||||
target.granted_balance = toNum(target.granted_balance) + toNum(source.granted_balance)
|
||||
target.purchased_balance = toNum(target.purchased_balance) + toNum(source.purchased_balance)
|
||||
target.current_balance = toNum(target.current_balance) + toNum(source.current_balance)
|
||||
target.usage = toNum(target.usage) + toNum(source.usage)
|
||||
target.max_purchase = toNum(target.max_purchase or 0) + toNum(source.max_purchase or 0)
|
||||
end
|
||||
|
||||
-- Helper function to merge overage_allowed (true if at least one is true)
|
||||
-- Mutates target
|
||||
local function mergeBalanceOverageAllowed(target, source)
|
||||
if source.overage_allowed == true then
|
||||
target.overage_allowed = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper function to merge reset objects (uses minimum resets_at)
|
||||
-- Mutates target
|
||||
local function mergeBalanceReset(target, source)
|
||||
if source.reset and source.reset ~= cjson.null and type(source.reset) == "table" and source.reset.resets_at then
|
||||
local sourceResetsAt = source.reset.resets_at
|
||||
if type(sourceResetsAt) == "number" then
|
||||
if target.reset and target.reset ~= cjson.null and type(target.reset) == "table" and target.reset.resets_at then
|
||||
local targetResetsAt = target.reset.resets_at
|
||||
if type(targetResetsAt) == "number" then
|
||||
if sourceResetsAt < targetResetsAt then
|
||||
target.reset.resets_at = sourceResetsAt
|
||||
end
|
||||
else
|
||||
target.reset.resets_at = sourceResetsAt
|
||||
end
|
||||
else
|
||||
-- Initialize reset object if it doesn't exist
|
||||
target.reset = {
|
||||
interval = source.reset.interval,
|
||||
interval_count = source.reset.interval_count,
|
||||
resets_at = sourceResetsAt
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper function to merge source balance into target balance
|
||||
-- Mutates targetBalance by adding sourceBalance's balances, usage, breakdowns, and rollovers
|
||||
-- Also handles minimum resets_at (earliest reset time) and overage_allowed (true if any is true)
|
||||
local function mergeFeatureBalances(targetBalance, sourceBalance)
|
||||
if not sourceBalance then return end
|
||||
|
||||
-- Merge top-level balance fields
|
||||
mergeBalanceNumericFields(targetBalance, sourceBalance)
|
||||
mergeBalanceOverageAllowed(targetBalance, sourceBalance)
|
||||
mergeBalanceReset(targetBalance, sourceBalance)
|
||||
|
||||
-- Merge breakdown balances and usage
|
||||
-- Breakdown items are matched by reset.interval, not by index
|
||||
-- If a matching breakdown exists, merge it; otherwise, add as new breakdown item
|
||||
if sourceBalance.breakdown then
|
||||
for _, sourceBreakdown in ipairs(sourceBalance.breakdown) do
|
||||
local sourceInterval = sourceBreakdown.reset and sourceBreakdown.reset.interval
|
||||
local foundMatch = false
|
||||
|
||||
-- Try to find matching breakdown by reset.interval
|
||||
if targetBalance.breakdown then
|
||||
for _, targetBreakdown in ipairs(targetBalance.breakdown) do
|
||||
local targetInterval = targetBreakdown.reset and targetBreakdown.reset.interval
|
||||
if sourceInterval and targetInterval and sourceInterval == targetInterval then
|
||||
-- Found matching breakdown - merge it
|
||||
mergeBalanceNumericFields(targetBreakdown, sourceBreakdown)
|
||||
mergeBalanceOverageAllowed(targetBreakdown, sourceBreakdown)
|
||||
mergeBalanceReset(targetBreakdown, sourceBreakdown)
|
||||
foundMatch = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- If no matching breakdown found, add as new breakdown item
|
||||
if not foundMatch then
|
||||
if not targetBalance.breakdown then
|
||||
targetBalance.breakdown = {}
|
||||
end
|
||||
-- Create a copy of the source breakdown to add
|
||||
local newBreakdown = {
|
||||
granted_balance = sourceBreakdown.granted_balance,
|
||||
purchased_balance = sourceBreakdown.purchased_balance,
|
||||
current_balance = sourceBreakdown.current_balance,
|
||||
usage = sourceBreakdown.usage,
|
||||
max_purchase = sourceBreakdown.max_purchase,
|
||||
overage_allowed = sourceBreakdown.overage_allowed,
|
||||
reset = sourceBreakdown.reset
|
||||
}
|
||||
table.insert(targetBalance.breakdown, newBreakdown)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Merge rollover balances
|
||||
if targetBalance.rollovers and sourceBalance.rollovers then
|
||||
for i, targetRollover in ipairs(targetBalance.rollovers) do
|
||||
local sourceRollover = sourceBalance.rollovers[i]
|
||||
if sourceRollover then
|
||||
targetRollover.balance = toNum(targetRollover.balance) + toNum(sourceRollover.balance)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- LOAD SINGLE BALANCE (WITH _key FIELDS FOR REDIS OPERATIONS)
|
||||
-- ============================================================================
|
||||
|
||||
-- Load a single balance from Redis cache (no merging)
|
||||
-- Used by batchDeduction.lua for on-demand balance loading with Redis operation keys
|
||||
-- Parameters:
|
||||
-- cacheKey: Base cache key (customer or entity cache key)
|
||||
-- featureId: Feature ID to load
|
||||
-- Returns: balance object with _key fields for Redis operations, or nil if not found
|
||||
local function loadBalance(cacheKey, featureId)
|
||||
local balanceKey = buildBalanceCacheKey(cacheKey, featureId)
|
||||
local balanceHash = redis.call("HGETALL", balanceKey)
|
||||
|
||||
if #balanceHash == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Parse balance hash using helper function
|
||||
local balanceData = parseBalanceHash(balanceHash)
|
||||
balanceData._key = balanceKey -- Add Redis key for operations
|
||||
|
||||
-- Fetch rollovers using helper function
|
||||
local rolloverCount = balanceData._rollover_count or 0
|
||||
balanceData._rollover_count = nil
|
||||
|
||||
local rollovers = fetchRollovers(cacheKey, featureId, rolloverCount)
|
||||
if rollovers == nil then
|
||||
return nil -- Partial eviction detected
|
||||
end
|
||||
|
||||
-- Add _key fields to rollovers for Redis operations
|
||||
if #rollovers > 0 then
|
||||
for index, rollover in ipairs(rollovers) do
|
||||
rollover._key = buildRolloverCacheKey(cacheKey, featureId, index - 1)
|
||||
rollover._index = index - 1
|
||||
end
|
||||
balanceData.rollovers = rollovers
|
||||
end
|
||||
|
||||
-- Fetch breakdown using helper function
|
||||
local breakdownCount = balanceData._breakdown_count or 0
|
||||
balanceData._breakdown_count = nil
|
||||
|
||||
local breakdown = fetchBreakdown(cacheKey, featureId, breakdownCount)
|
||||
if breakdown == nil then
|
||||
return nil -- Partial eviction detected
|
||||
end
|
||||
|
||||
-- Add _key fields to breakdown items for Redis operations
|
||||
if #breakdown > 0 then
|
||||
for index, breakdownItem in ipairs(breakdown) do
|
||||
breakdownItem._key = buildBreakdownCacheKey(cacheKey, featureId, index - 1)
|
||||
breakdownItem._index = index - 1
|
||||
end
|
||||
balanceData.breakdown = breakdown
|
||||
end
|
||||
|
||||
return balanceData
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- LOAD BALANCES WITH MERGING
|
||||
-- ============================================================================
|
||||
|
||||
-- Load entity-level balances (entity + customer merged)
|
||||
-- Used for entity-level sync mode
|
||||
-- Parameters: cacheKey (customer cache key), orgId, env, customerId, entityId
|
||||
-- Returns: merged balances table (entity + customer) or nil
|
||||
local function loadEntityLevelFeatures(cacheKey, orgId, env, customerId, entityId)
|
||||
-- Build versioned entity cache key using shared utility
|
||||
local entityCacheKey = buildEntityCacheKey(orgId, env, customerId, entityId)
|
||||
|
||||
-- Get entity base JSON
|
||||
local entityBaseJson = redis.call("GET", entityCacheKey)
|
||||
if not entityBaseJson then
|
||||
return nil
|
||||
end
|
||||
|
||||
local entityBase = cjson.decode(entityBaseJson)
|
||||
local entityBalanceFeatureIds = entityBase._balanceFeatureIds or {}
|
||||
|
||||
-- Load entity balances
|
||||
local entityBalances = {}
|
||||
for _, featureId in ipairs(entityBalanceFeatureIds) do
|
||||
local balanceKey = buildBalanceCacheKey(entityCacheKey, featureId)
|
||||
local balanceHash = redis.call("HGETALL", balanceKey)
|
||||
|
||||
-- If balance key is missing, return nil (partial eviction detected)
|
||||
if #balanceHash == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Parse balance hash using helper function
|
||||
local balanceData = parseBalanceHash(balanceHash)
|
||||
|
||||
-- Fetch rollovers using helper function
|
||||
local rolloverCount = balanceData._rollover_count or 0
|
||||
balanceData._rollover_count = nil
|
||||
|
||||
local rollovers = fetchRollovers(entityCacheKey, featureId, rolloverCount)
|
||||
if rollovers == nil then
|
||||
return nil -- Partial eviction detected
|
||||
end
|
||||
|
||||
if #rollovers > 0 then
|
||||
balanceData.rollovers = rollovers
|
||||
end
|
||||
|
||||
-- Fetch breakdown using helper function
|
||||
local breakdownCount = balanceData._breakdown_count or 0
|
||||
balanceData._breakdown_count = nil
|
||||
|
||||
local breakdown = fetchBreakdown(entityCacheKey, featureId, breakdownCount)
|
||||
if breakdown == nil then
|
||||
return nil -- Partial eviction detected
|
||||
end
|
||||
|
||||
if #breakdown > 0 then
|
||||
balanceData.breakdown = breakdown
|
||||
end
|
||||
|
||||
entityBalances[featureId] = balanceData
|
||||
end
|
||||
|
||||
-- Load customer balances (raw, no entity aggregation)
|
||||
local customerCacheKey = cacheKey
|
||||
local customerBaseJson = redis.call("GET", customerCacheKey)
|
||||
|
||||
local customerBalances = {}
|
||||
if customerBaseJson then
|
||||
local customerBase = cjson.decode(customerBaseJson)
|
||||
local customerBalanceFeatureIds = customerBase._balanceFeatureIds or {}
|
||||
|
||||
for _, featureId in ipairs(customerBalanceFeatureIds) do
|
||||
local balanceKey = buildBalanceCacheKey(customerCacheKey, featureId)
|
||||
local balanceHash = redis.call("HGETALL", balanceKey)
|
||||
|
||||
if #balanceHash > 0 then
|
||||
-- Parse balance hash using helper function
|
||||
local balanceData = parseBalanceHash(balanceHash)
|
||||
|
||||
-- Fetch rollovers
|
||||
local rolloverCount = balanceData._rollover_count or 0
|
||||
balanceData._rollover_count = nil
|
||||
local rollovers = fetchRollovers(customerCacheKey, featureId, rolloverCount) or {}
|
||||
if #rollovers > 0 then
|
||||
balanceData.rollovers = rollovers
|
||||
end
|
||||
|
||||
-- Fetch breakdown
|
||||
local breakdownCount = balanceData._breakdown_count or 0
|
||||
balanceData._breakdown_count = nil
|
||||
local breakdown = fetchBreakdown(customerCacheKey, featureId, breakdownCount) or {}
|
||||
if #breakdown > 0 then
|
||||
balanceData.breakdown = breakdown
|
||||
end
|
||||
|
||||
customerBalances[featureId] = balanceData
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Merge customer and entity balances (entity + customer)
|
||||
local mergedBalances = {}
|
||||
|
||||
-- First, add all customer balances (inherited)
|
||||
for featureId, customerBalance in pairs(customerBalances) do
|
||||
mergedBalances[featureId] = customerBalance
|
||||
end
|
||||
|
||||
-- Then, merge or add entity balances
|
||||
for featureId, entityBalance in pairs(entityBalances) do
|
||||
local customerBalance = customerBalances[featureId]
|
||||
|
||||
if customerBalance then
|
||||
-- Both customer and entity have this balance - merge balances
|
||||
if not entityBalance.unlimited and not customerBalance.unlimited then
|
||||
mergeFeatureBalances(entityBalance, customerBalance)
|
||||
end
|
||||
mergedBalances[featureId] = entityBalance
|
||||
else
|
||||
-- Only entity has this balance - use entity's balance
|
||||
mergedBalances[featureId] = entityBalance
|
||||
end
|
||||
end
|
||||
|
||||
return mergedBalances
|
||||
end
|
||||
|
||||
-- Load customer balances with merged entity balances
|
||||
-- Parameters: cacheKey, orgId, env, customerId, entityId (optional)
|
||||
-- If entityId is "__CUSTOMER_ONLY__": returns ONLY customer balances (no merging)
|
||||
-- If entityId is provided (string): returns entity-level merged balances (entity + customer)
|
||||
-- If entityId is nil: returns customer-level merged balances (customer + all entities)
|
||||
-- Returns: merged balances table or nil
|
||||
local function loadBalances(cacheKey, orgId, env, customerId, entityId)
|
||||
-- Special case: Customer-only mode (no entity merging)
|
||||
if entityId == "__CUSTOMER_ONLY__" then
|
||||
local baseJson = redis.call("GET", cacheKey)
|
||||
if not baseJson then
|
||||
return nil
|
||||
end
|
||||
|
||||
local base = cjson.decode(baseJson)
|
||||
local balanceFeatureIds = base._balanceFeatureIds or {}
|
||||
|
||||
-- Load only customer's own balances without entity merging
|
||||
local customerBalances = {}
|
||||
for _, featureId in ipairs(balanceFeatureIds) do
|
||||
local balanceKey = buildBalanceCacheKey(cacheKey, featureId)
|
||||
local balanceHash = redis.call("HGETALL", balanceKey)
|
||||
|
||||
if #balanceHash == 0 then
|
||||
return nil -- Partial eviction detected
|
||||
end
|
||||
|
||||
-- Parse balance hash
|
||||
local balanceData = parseBalanceHash(balanceHash)
|
||||
|
||||
-- Fetch rollovers
|
||||
local rollovers = fetchRollovers(cacheKey, featureId, balanceData._rollover_count or 0)
|
||||
if rollovers == nil then
|
||||
return nil -- Partial eviction
|
||||
end
|
||||
if #rollovers > 0 then
|
||||
balanceData.rollovers = rollovers
|
||||
end
|
||||
|
||||
-- Fetch breakdown
|
||||
local breakdown = fetchBreakdown(cacheKey, featureId, balanceData._breakdown_count or 0)
|
||||
if breakdown == nil then
|
||||
return nil -- Partial eviction
|
||||
end
|
||||
if #breakdown > 0 then
|
||||
balanceData.breakdown = breakdown
|
||||
end
|
||||
|
||||
-- Remove metadata fields
|
||||
balanceData._breakdown_count = nil
|
||||
balanceData._rollover_count = nil
|
||||
|
||||
customerBalances[featureId] = balanceData
|
||||
end
|
||||
|
||||
return customerBalances
|
||||
end
|
||||
|
||||
-- If entityId is provided, load entity-level balances (entity + customer merged)
|
||||
if entityId then
|
||||
return loadEntityLevelFeatures(cacheKey, orgId, env, customerId, entityId)
|
||||
end
|
||||
|
||||
-- Otherwise, load customer-level balances (customer + all entities merged)
|
||||
-- Get base customer JSON
|
||||
local baseJson = redis.call("GET", cacheKey)
|
||||
if not baseJson then
|
||||
return nil
|
||||
end
|
||||
|
||||
local baseCustomer = cjson.decode(baseJson)
|
||||
local balanceFeatureIds = baseCustomer._balanceFeatureIds or {}
|
||||
local entityIds = baseCustomer._entityIds or {}
|
||||
|
||||
-- Build balances object
|
||||
local balances = {}
|
||||
|
||||
for _, featureId in ipairs(balanceFeatureIds) do
|
||||
local balanceKey = buildBalanceCacheKey(cacheKey, featureId)
|
||||
local balanceHash = redis.call("HGETALL", balanceKey)
|
||||
|
||||
-- If balance key is missing, return nil (partial eviction detected)
|
||||
if #balanceHash == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Parse balance hash using helper function
|
||||
local balanceData = parseBalanceHash(balanceHash)
|
||||
|
||||
-- Fetch rollovers using helper function
|
||||
local rolloverCount = balanceData._rollover_count or 0
|
||||
balanceData._rollover_count = nil -- Remove from final output
|
||||
|
||||
local rollovers = fetchRollovers(cacheKey, featureId, rolloverCount)
|
||||
if rollovers == nil then
|
||||
return nil -- Partial eviction detected
|
||||
end
|
||||
|
||||
if #rollovers > 0 then
|
||||
balanceData.rollovers = rollovers
|
||||
end
|
||||
|
||||
-- Fetch breakdown using helper function
|
||||
local breakdownCount = balanceData._breakdown_count or 0
|
||||
balanceData._breakdown_count = nil -- Remove from final output
|
||||
|
||||
local breakdown = fetchBreakdown(cacheKey, featureId, breakdownCount)
|
||||
if breakdown == nil then
|
||||
return nil -- Partial eviction detected
|
||||
end
|
||||
|
||||
if #breakdown > 0 then
|
||||
balanceData.breakdown = breakdown
|
||||
end
|
||||
|
||||
balances[featureId] = balanceData
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- FETCH AND MERGE ENTITY BALANCES
|
||||
-- ============================================================================
|
||||
|
||||
-- Fetch all entity balances and aggregate balances
|
||||
local entityBalanceData = {} -- {[entityId][featureId] = balanceData}
|
||||
local entityBaseData = {} -- {[entityId] = entityBase} - Store entity base for product access
|
||||
|
||||
for _, entityId in ipairs(entityIds) do
|
||||
local entityCacheKey = buildEntityCacheKey(orgId, env, customerId, entityId)
|
||||
local entityBaseJson = redis.call("GET", entityCacheKey)
|
||||
|
||||
if entityBaseJson then
|
||||
local entityBase = cjson.decode(entityBaseJson)
|
||||
entityBaseData[entityId] = entityBase -- Store entity base for product access
|
||||
local entityBalanceFeatureIds = entityBase._balanceFeatureIds or {}
|
||||
entityBalanceData[entityId] = {}
|
||||
|
||||
for _, featureId in ipairs(entityBalanceFeatureIds) do
|
||||
local balanceKey = buildBalanceCacheKey(entityCacheKey, featureId)
|
||||
local balanceHash = redis.call("HGETALL", balanceKey)
|
||||
|
||||
if #balanceHash > 0 then
|
||||
-- Parse entity balance using helper function
|
||||
local entityBalance = parseBalanceHash(balanceHash)
|
||||
|
||||
-- Fetch breakdown items for this entity balance using helper function
|
||||
local breakdownCount = entityBalance._breakdown_count or 0
|
||||
entityBalance._breakdown_count = nil
|
||||
entityBalance.breakdown = fetchBreakdown(entityCacheKey, featureId, breakdownCount) or {}
|
||||
|
||||
-- Fetch rollover items for this entity balance using helper function
|
||||
local rolloverCount = entityBalance._rollover_count or 0
|
||||
entityBalance._rollover_count = nil
|
||||
entityBalance.rollovers = fetchRollovers(entityCacheKey, featureId, rolloverCount) or {}
|
||||
|
||||
entityBalanceData[entityId][featureId] = entityBalance
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- MERGE ENTITY BALANCES INTO CUSTOMER BALANCES
|
||||
-- ============================================================================
|
||||
|
||||
for featureId, customerBalance in pairs(balances) do
|
||||
-- Skip if unlimited
|
||||
if not customerBalance.unlimited then
|
||||
-- Merge each entity's balances into customer balance
|
||||
for entityId, entityBalances in pairs(entityBalanceData) do
|
||||
local entityBalance = entityBalances[featureId]
|
||||
if entityBalance then
|
||||
mergeFeatureBalances(customerBalance, entityBalance)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Add entity-only balances (balances that exist in entities but not in customer)
|
||||
for entityId, entityBalances in pairs(entityBalanceData) do
|
||||
for featureId, entityBalance in pairs(entityBalances) do
|
||||
if not balances[featureId] then
|
||||
-- This balance doesn't exist in customer, add it with zero values
|
||||
balances[featureId] = {
|
||||
feature_id = featureId,
|
||||
feature = entityBalance.feature,
|
||||
unlimited = entityBalance.unlimited,
|
||||
granted_balance = 0,
|
||||
purchased_balance = 0,
|
||||
current_balance = 0,
|
||||
usage = 0,
|
||||
max_purchase = entityBalance.max_purchase or 0,
|
||||
overage_allowed = entityBalance.overage_allowed,
|
||||
reset = entityBalance.reset,
|
||||
breakdown = {},
|
||||
rollovers = {}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Aggregate balances for entity-only balances using mergeFeatureBalances
|
||||
for featureId, customerBalance in pairs(balances) do
|
||||
-- Only process if this was an entity-only balance (all balances are still 0 from initialization)
|
||||
if customerBalance.granted_balance == 0 and customerBalance.purchased_balance == 0 and customerBalance.current_balance == 0 and customerBalance.usage == 0 then
|
||||
for entityId, entityBalances in pairs(entityBalanceData) do
|
||||
local entityBalance = entityBalances[featureId]
|
||||
if entityBalance then
|
||||
mergeFeatureBalances(customerBalance, entityBalance)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Return merged balances
|
||||
return balances
|
||||
end
|
||||
Reference in New Issue
Block a user