feat: deduct from redis new api schema
This commit is contained in:
390
server/src/_luaScripts/archives/backupBatchDeduction.lua
Normal file
390
server/src/_luaScripts/archives/backupBatchDeduction.lua
Normal file
@@ -0,0 +1,390 @@
|
||||
-- batchDeduction.lua (BACKUP - Original code-generated version)
|
||||
-- Atomically processes a batch of deductions for a specific target feature
|
||||
-- Supports credit system features as alternative payment sources
|
||||
-- KEYS[1]: cache key (e.g., "org_id:env:customer:customer_id")
|
||||
-- KEYS[2]: target feature ID
|
||||
-- ARGV[1]: JSON array of deduction amounts [10, 20, -5, 30, ...] (negative = additions)
|
||||
-- ARGV[2]: overage_behavior ("reject" or "cap")
|
||||
|
||||
local cacheKey = KEYS[1]
|
||||
local targetFeatureId = KEYS[2]
|
||||
local amountsJson = ARGV[1]
|
||||
local overageBehavior = ARGV[2] or "cap"
|
||||
|
||||
-- Parse amounts
|
||||
local amounts = cjson.decode(amountsJson)
|
||||
|
||||
-- Base keys
|
||||
local baseKey = cacheKey
|
||||
|
||||
-- Check if customer exists
|
||||
local baseExists = redis.call("EXISTS", baseKey)
|
||||
if baseExists == 0 then
|
||||
return cjson.encode({
|
||||
success = false,
|
||||
error = "CUSTOMER_NOT_FOUND",
|
||||
successCount = 0
|
||||
})
|
||||
end
|
||||
|
||||
-- Load base customer to get all feature IDs
|
||||
local baseJson = redis.call("GET", baseKey)
|
||||
local baseCustomer = cjson.decode(baseJson)
|
||||
local allFeatureIds = baseCustomer._featureIds or {}
|
||||
|
||||
-- Helper function: Load a complete feature with rollovers and breakdowns
|
||||
local function loadFeature(featureId)
|
||||
local featureKey = cacheKey .. ":features:" .. featureId
|
||||
local featureHash = redis.call("HGETALL", featureKey)
|
||||
|
||||
if #featureHash == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Parse feature fields
|
||||
local feature = { id = featureId }
|
||||
for i = 1, #featureHash, 2 do
|
||||
local key = featureHash[i]
|
||||
local value = featureHash[i + 1]
|
||||
|
||||
if key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "_breakdown_count" or key == "_rollover_count" then
|
||||
feature[key] = tonumber(value)
|
||||
elseif key == "unlimited" or key == "overage_allowed" then
|
||||
feature[key] = (value == "true")
|
||||
elseif key == "credit_schema" then
|
||||
if value ~= "null" and value ~= "" then
|
||||
feature[key] = cjson.decode(value)
|
||||
else
|
||||
feature[key] = nil
|
||||
end
|
||||
elseif value == "null" then
|
||||
feature[key] = cjson.null
|
||||
else
|
||||
feature[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
-- Load rollovers
|
||||
local rolloverCount = feature._rollover_count or 0
|
||||
feature.rollovers = {}
|
||||
for i = 0, rolloverCount - 1 do
|
||||
local rolloverKey = cacheKey .. ":features:" .. featureId .. ":rollover:" .. i
|
||||
local rolloverHash = redis.call("HGETALL", rolloverKey)
|
||||
|
||||
if #rolloverHash > 0 then
|
||||
local rollover = { _index = i, _key = rolloverKey }
|
||||
for j = 1, #rolloverHash, 2 do
|
||||
local key = rolloverHash[j]
|
||||
local value = rolloverHash[j + 1]
|
||||
|
||||
if key == "balance" or key == "expires_at" then
|
||||
rollover[key] = tonumber(value)
|
||||
else
|
||||
rollover[key] = value
|
||||
end
|
||||
end
|
||||
table.insert(feature.rollovers, rollover)
|
||||
end
|
||||
end
|
||||
|
||||
-- Load breakdowns
|
||||
local breakdownCount = feature._breakdown_count or 0
|
||||
feature.breakdowns = {}
|
||||
for i = 0, breakdownCount - 1 do
|
||||
local breakdownKey = cacheKey .. ":features:" .. featureId .. ":breakdown:" .. i
|
||||
local breakdownHash = redis.call("HGETALL", breakdownKey)
|
||||
|
||||
if #breakdownHash > 0 then
|
||||
local breakdown = { _index = i, _key = breakdownKey }
|
||||
for j = 1, #breakdownHash, 2 do
|
||||
local key = breakdownHash[j]
|
||||
local value = breakdownHash[j + 1]
|
||||
|
||||
if key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" then
|
||||
breakdown[key] = tonumber(value)
|
||||
else
|
||||
breakdown[key] = value
|
||||
end
|
||||
end
|
||||
table.insert(feature.breakdowns, breakdown)
|
||||
end
|
||||
end
|
||||
|
||||
return feature
|
||||
end
|
||||
|
||||
-- Helper function: Calculate credit cost
|
||||
local function getCreditCost(feature, targetFeatureId)
|
||||
if not feature.credit_schema or type(feature.credit_schema) ~= "table" then
|
||||
return 1
|
||||
end
|
||||
|
||||
for _, schemaItem in ipairs(feature.credit_schema) do
|
||||
if schemaItem.feature_id == targetFeatureId then
|
||||
local creditAmount = schemaItem.credit_cost or schemaItem.credit_amount or 1
|
||||
local featureAmount = schemaItem.feature_amount or 1
|
||||
return creditAmount / featureAmount
|
||||
end
|
||||
end
|
||||
|
||||
return 1
|
||||
end
|
||||
|
||||
-- Load all features and categorize
|
||||
local regularFeatures = {}
|
||||
local creditFeatures = {}
|
||||
|
||||
for _, featureId in ipairs(allFeatureIds) do
|
||||
local feature = loadFeature(featureId)
|
||||
|
||||
if feature and not feature.unlimited then
|
||||
local creditCost = getCreditCost(feature, targetFeatureId)
|
||||
|
||||
if featureId == targetFeatureId or creditCost == 1 then
|
||||
table.insert(regularFeatures, { feature = feature, creditCost = 1 })
|
||||
else
|
||||
table.insert(creditFeatures, { feature = feature, creditCost = creditCost })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Combine regular and credit features for deduction order
|
||||
local allFeatures = {}
|
||||
for _, item in ipairs(regularFeatures) do
|
||||
table.insert(allFeatures, item)
|
||||
end
|
||||
for _, item in ipairs(creditFeatures) do
|
||||
table.insert(allFeatures, item)
|
||||
end
|
||||
|
||||
if #allFeatures == 0 then
|
||||
return cjson.encode({
|
||||
success = false,
|
||||
error = "NO_VALID_FEATURES",
|
||||
successCount = 0
|
||||
})
|
||||
end
|
||||
|
||||
-- Separate additions (negative) from deductions (positive)
|
||||
local additions = {}
|
||||
local deductions = {}
|
||||
|
||||
for i, amount in ipairs(amounts) do
|
||||
if amount < 0 then
|
||||
table.insert(additions, { index = i, amount = amount })
|
||||
else
|
||||
table.insert(deductions, { index = i, amount = amount })
|
||||
end
|
||||
end
|
||||
|
||||
-- Reorder: additions first, then deductions
|
||||
local orderedRequests = {}
|
||||
for _, item in ipairs(additions) do
|
||||
table.insert(orderedRequests, item)
|
||||
end
|
||||
for _, item in ipairs(deductions) do
|
||||
table.insert(orderedRequests, item)
|
||||
end
|
||||
|
||||
-- Track accumulated changes per Redis key
|
||||
local keyDeltas = {} -- { [key] = { balance = delta, usage = delta } }
|
||||
|
||||
-- Helper: Add delta to key
|
||||
local function addDelta(key, field, delta)
|
||||
if not keyDeltas[key] then
|
||||
keyDeltas[key] = {}
|
||||
end
|
||||
keyDeltas[key][field] = (keyDeltas[key][field] or 0) + delta
|
||||
end
|
||||
|
||||
-- Helper: Calculate available balance from all features
|
||||
local function calculateTotalAvailable()
|
||||
local available = 0
|
||||
|
||||
for _, featureItem in ipairs(allFeatures) do
|
||||
local feature = featureItem.feature
|
||||
|
||||
-- Rollovers
|
||||
for _, rollover in ipairs(feature.rollovers or {}) do
|
||||
if rollover.balance and rollover.balance > 0 then
|
||||
available = available + rollover.balance
|
||||
end
|
||||
end
|
||||
|
||||
-- Breakdowns
|
||||
for _, breakdown in ipairs(feature.breakdowns or {}) do
|
||||
if breakdown.balance and breakdown.balance > 0 then
|
||||
available = available + breakdown.balance
|
||||
end
|
||||
end
|
||||
|
||||
-- Top-level balance (if no breakdowns)
|
||||
if #(feature.breakdowns or {}) == 0 and feature.balance and feature.balance > 0 then
|
||||
available = feature.balance
|
||||
end
|
||||
|
||||
-- Overage
|
||||
if feature.overage_allowed and feature.usage_limit then
|
||||
local remainingOverage = feature.usage_limit - (feature.usage or 0)
|
||||
if remainingOverage > 0 then
|
||||
available = available + remainingOverage
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return available
|
||||
end
|
||||
|
||||
-- Helper: Apply a single deduction amount across all features
|
||||
local function applyDeduction(amount, featureItem)
|
||||
local feature = featureItem.feature
|
||||
local creditCost = featureItem.creditCost
|
||||
local remaining = amount
|
||||
local featureKey = cacheKey .. ":features:" .. feature.id
|
||||
|
||||
-- Deduct from rollovers
|
||||
for _, rollover in ipairs(feature.rollovers or {}) do
|
||||
if remaining <= 0 then break end
|
||||
|
||||
local rolloverBalance = rollover.balance or 0
|
||||
if rolloverBalance > 0 then
|
||||
local toDeduct = math.min(remaining, rolloverBalance)
|
||||
local actualDeduction = toDeduct * creditCost
|
||||
|
||||
addDelta(rollover._key, "balance", -actualDeduction)
|
||||
|
||||
rollover.balance = rolloverBalance - actualDeduction
|
||||
remaining = remaining - toDeduct
|
||||
end
|
||||
end
|
||||
|
||||
-- Deduct from breakdowns
|
||||
if #(feature.breakdowns or {}) > 0 then
|
||||
for _, breakdown in ipairs(feature.breakdowns) do
|
||||
if remaining <= 0 then break end
|
||||
|
||||
local breakdownBalance = breakdown.balance or 0
|
||||
if breakdownBalance > 0 then
|
||||
local toDeduct = math.min(remaining, breakdownBalance)
|
||||
local actualDeduction = toDeduct * creditCost
|
||||
|
||||
addDelta(breakdown._key, "balance", -actualDeduction)
|
||||
addDelta(breakdown._key, "usage", actualDeduction)
|
||||
addDelta(featureKey, "balance", -actualDeduction)
|
||||
addDelta(featureKey, "usage", actualDeduction)
|
||||
|
||||
breakdown.balance = breakdownBalance - actualDeduction
|
||||
feature.balance = (feature.balance or 0) - actualDeduction
|
||||
feature.usage = (feature.usage or 0) + actualDeduction
|
||||
remaining = remaining - toDeduct
|
||||
end
|
||||
end
|
||||
else
|
||||
-- No breakdowns, deduct from top-level
|
||||
local topLevelBalance = feature.balance or 0
|
||||
if topLevelBalance > 0 then
|
||||
local toDeduct = math.min(remaining, topLevelBalance)
|
||||
local actualDeduction = toDeduct * creditCost
|
||||
|
||||
addDelta(featureKey, "balance", -actualDeduction)
|
||||
addDelta(featureKey, "usage", actualDeduction)
|
||||
|
||||
feature.balance = topLevelBalance - actualDeduction
|
||||
feature.usage = (feature.usage or 0) + actualDeduction
|
||||
remaining = remaining - toDeduct
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle overage
|
||||
if remaining > 0 and feature.overage_allowed and feature.usage_limit then
|
||||
local currentUsage = feature.usage or 0
|
||||
local remainingOverage = feature.usage_limit - currentUsage
|
||||
|
||||
if remainingOverage > 0 then
|
||||
local overageDeduct = math.min(remaining, remainingOverage)
|
||||
local actualOverageDeduction = overageDeduct * creditCost
|
||||
|
||||
addDelta(featureKey, "usage", actualOverageDeduction)
|
||||
|
||||
feature.usage = currentUsage + actualOverageDeduction
|
||||
remaining = remaining - overageDeduct
|
||||
end
|
||||
end
|
||||
|
||||
return remaining == 0
|
||||
end
|
||||
|
||||
-- Process all requests independently
|
||||
local successCount = 0
|
||||
local processedRequests = {} -- Track which original indices succeeded
|
||||
|
||||
for _, request in ipairs(orderedRequests) do
|
||||
local amount = request.amount
|
||||
local originalIndex = request.index
|
||||
local succeeded = false
|
||||
|
||||
-- Additions (negative amounts) always succeed
|
||||
if amount < 0 then
|
||||
-- Apply addition across features (reverse deduction)
|
||||
for _, featureItem in ipairs(allFeatures) do
|
||||
applyDeduction(amount, featureItem)
|
||||
break -- Only apply to first feature for additions
|
||||
end
|
||||
succeeded = true
|
||||
else
|
||||
-- Deductions: check availability
|
||||
local available = calculateTotalAvailable()
|
||||
|
||||
if available >= amount then
|
||||
-- Sufficient balance, apply full deduction
|
||||
local remaining = amount
|
||||
for _, featureItem in ipairs(allFeatures) do
|
||||
if remaining <= 0 then break end
|
||||
if applyDeduction(remaining, featureItem) then
|
||||
remaining = 0
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if remaining == 0 then
|
||||
succeeded = true
|
||||
end
|
||||
elseif overageBehavior == "cap" then
|
||||
-- Cap behavior: deduct what's available (even if 0) and succeed
|
||||
if available > 0 then
|
||||
local remaining = available
|
||||
for _, featureItem in ipairs(allFeatures) do
|
||||
if remaining <= 0 then break end
|
||||
if applyDeduction(remaining, featureItem) then
|
||||
remaining = 0
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
succeeded = true -- Always succeed with cap behavior
|
||||
end
|
||||
-- else: insufficient and reject → don't deduct, don't mark success
|
||||
end
|
||||
|
||||
processedRequests[originalIndex] = succeeded
|
||||
if succeeded then
|
||||
successCount = successCount + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Execute accumulated changes (ONE HINCRBYFLOAT per key per field)
|
||||
for key, deltas in pairs(keyDeltas) do
|
||||
for field, delta in pairs(deltas) do
|
||||
if delta ~= 0 then
|
||||
redis.call("HINCRBYFLOAT", key, field, delta)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Always return success=true (batch executed), individual requests resolved by successCount
|
||||
return cjson.encode({
|
||||
success = true,
|
||||
successCount = successCount,
|
||||
error = successCount < #amounts and "INSUFFICIENT_BALANCE" or nil
|
||||
})
|
||||
|
||||
494
server/src/_luaScripts/archives/getCustomer.backup.lua
Normal file
494
server/src/_luaScripts/archives/getCustomer.backup.lua
Normal file
@@ -0,0 +1,494 @@
|
||||
-- getCustomer.lua
|
||||
-- Atomically retrieves a customer object from Redis, reconstructing from base JSON and feature HSETs
|
||||
-- Merges master customer features with entity features
|
||||
-- KEYS[1]: cache key (e.g., "org_id:env:customer:customer_id")
|
||||
-- ARGV[1]: org_id (for building entity cache keys)
|
||||
-- ARGV[2]: env (for building entity cache keys)
|
||||
-- ARGV[3]: customer_id (for building entity cache keys)
|
||||
|
||||
-- Helper function to safely convert values to numbers for arithmetic
|
||||
-- Returns the value if it's a number, otherwise returns 0
|
||||
local function toNum(value)
|
||||
return type(value) == "number" and value or 0
|
||||
end
|
||||
|
||||
-- Helper function to merge products array by product ID and normalized status
|
||||
-- Groups products by key (product_id:normalized_status) and merges quantities
|
||||
local function mergeProducts(productsArray)
|
||||
if not productsArray or #productsArray == 0 then
|
||||
return {}
|
||||
end
|
||||
|
||||
-- Helper function to get product key for grouping
|
||||
local function getProductKey(product)
|
||||
local status = product.status
|
||||
-- Normalize status: "active" or "past_due" -> "active", otherwise use actual status
|
||||
if status == "active" or status == "past_due" then
|
||||
status = "active"
|
||||
end
|
||||
return product.id .. ":" .. status
|
||||
end
|
||||
|
||||
local record = {}
|
||||
|
||||
for _, curr in ipairs(productsArray) do
|
||||
local key = getProductKey(curr)
|
||||
local latest = record[key]
|
||||
|
||||
local currStartedAt = curr.started_at
|
||||
|
||||
-- Start with latest (or current if no latest exists), then override specific fields
|
||||
local mergedProduct = {}
|
||||
if latest then
|
||||
-- Copy all fields from latest first
|
||||
for k, v in pairs(latest) do
|
||||
mergedProduct[k] = v
|
||||
end
|
||||
else
|
||||
-- Copy all fields from current
|
||||
for k, v in pairs(curr) do
|
||||
mergedProduct[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
-- Apply merge logic for specific fields
|
||||
if latest then
|
||||
-- version: max(latest.version or 1, current.version or 1)
|
||||
local latestVersion = latest.version or 1
|
||||
local currVersion = curr.version or 1
|
||||
mergedProduct.version = math.max(latestVersion, currVersion)
|
||||
|
||||
-- canceled_at: current.canceled_at if exists, else latest.canceled_at, else null
|
||||
if curr.canceled_at and curr.canceled_at ~= cjson.null and curr.canceled_at ~= nil then
|
||||
mergedProduct.canceled_at = curr.canceled_at
|
||||
elseif latest.canceled_at and latest.canceled_at ~= cjson.null and latest.canceled_at ~= nil then
|
||||
mergedProduct.canceled_at = latest.canceled_at
|
||||
else
|
||||
mergedProduct.canceled_at = cjson.null
|
||||
end
|
||||
|
||||
-- started_at: latest.started_at ? min(latest.started_at, current.started_at) : current.started_at
|
||||
if latest.started_at then
|
||||
mergedProduct.started_at = math.min(latest.started_at, currStartedAt)
|
||||
else
|
||||
mergedProduct.started_at = currStartedAt
|
||||
end
|
||||
|
||||
-- quantity: (latest.quantity or 0) + (current.quantity or 0)
|
||||
local latestQuantity = latest.quantity or 0
|
||||
local currQuantity = curr.quantity or 0
|
||||
mergedProduct.quantity = latestQuantity + currQuantity
|
||||
else
|
||||
-- First product in group, ensure defaults
|
||||
mergedProduct.version = curr.version or 1
|
||||
mergedProduct.canceled_at = curr.canceled_at or cjson.null
|
||||
mergedProduct.started_at = currStartedAt
|
||||
mergedProduct.quantity = curr.quantity or 0
|
||||
end
|
||||
|
||||
record[key] = mergedProduct
|
||||
end
|
||||
|
||||
-- Convert record back to array
|
||||
local mergedProducts = {}
|
||||
for _, product in pairs(record) do
|
||||
table.insert(mergedProducts, product)
|
||||
end
|
||||
|
||||
return mergedProducts
|
||||
end
|
||||
|
||||
local cacheKey = KEYS[1]
|
||||
local baseKey = cacheKey
|
||||
local orgId = ARGV[1]
|
||||
local env = ARGV[2]
|
||||
local customerId = ARGV[3]
|
||||
|
||||
-- Get base customer JSON
|
||||
local baseJson = redis.call("GET", baseKey)
|
||||
if not baseJson then
|
||||
return nil
|
||||
end
|
||||
|
||||
local baseCustomer = cjson.decode(baseJson)
|
||||
local featureIds = baseCustomer._featureIds or {}
|
||||
local entityIds = baseCustomer._entityIds or {}
|
||||
|
||||
-- Build features object
|
||||
local features = {}
|
||||
|
||||
for _, featureId in ipairs(featureIds) do
|
||||
local featureKey = cacheKey .. ":features:" .. featureId
|
||||
local featureHash = redis.call("HGETALL", featureKey)
|
||||
|
||||
-- If feature key is missing, return nil (partial eviction detected)
|
||||
if #featureHash == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Convert HGETALL result (flat array) to table
|
||||
local featureData = {}
|
||||
for i = 1, #featureHash, 2 do
|
||||
local key = featureHash[i]
|
||||
local value = featureHash[i + 1]
|
||||
|
||||
-- Check for null first before parsing
|
||||
if value == "null" then
|
||||
featureData[key] = cjson.null
|
||||
elseif key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" or key == "_breakdown_count" or key == "_rollover_count" then
|
||||
featureData[key] = tonumber(value)
|
||||
elseif key == "unlimited" or key == "overage_allowed" then
|
||||
featureData[key] = (value == "true")
|
||||
elseif key == "credit_schema" then
|
||||
-- Parse credit_schema JSON array
|
||||
if value ~= "" then
|
||||
featureData[key] = cjson.decode(value)
|
||||
else
|
||||
featureData[key] = cjson.null
|
||||
end
|
||||
else
|
||||
featureData[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
-- Get rollover count
|
||||
local rolloverCount = featureData._rollover_count or 0
|
||||
featureData._rollover_count = nil -- Remove from final output
|
||||
|
||||
-- Fetch rollover items
|
||||
local rollovers = {}
|
||||
for i = 0, rolloverCount - 1 do
|
||||
local rolloverKey = cacheKey .. ":features:" .. featureId .. ":rollover:" .. 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
|
||||
|
||||
if #rollovers > 0 then
|
||||
featureData.rollovers = rollovers
|
||||
end
|
||||
|
||||
-- Get breakdown count
|
||||
local breakdownCount = featureData._breakdown_count or 0
|
||||
featureData._breakdown_count = nil -- Remove from final output
|
||||
|
||||
-- Fetch breakdown items
|
||||
local breakdown = {}
|
||||
for i = 0, breakdownCount - 1 do
|
||||
local breakdownKey = cacheKey .. ":features:" .. featureId .. ":breakdown:" .. 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 key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" then
|
||||
breakdownData[key] = tonumber(value)
|
||||
elseif key == "overage_allowed" then
|
||||
breakdownData[key] = (value == "true")
|
||||
else
|
||||
breakdownData[key] = value
|
||||
end
|
||||
end
|
||||
table.insert(breakdown, breakdownData)
|
||||
end
|
||||
|
||||
if #breakdown > 0 then
|
||||
featureData.breakdown = breakdown
|
||||
end
|
||||
|
||||
features[featureId] = featureData
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- FETCH AND MERGE ENTITY FEATURES
|
||||
-- ============================================================================
|
||||
|
||||
-- Fetch all entity features and aggregate balances
|
||||
local entityFeatureData = {} -- {[entityId][featureId] = featureData}
|
||||
local entityBaseData = {} -- {[entityId] = entityBase} - Store entity base for product access
|
||||
|
||||
for _, entityId in ipairs(entityIds) do
|
||||
local entityCacheKey = "{" .. orgId .. "}:" .. env .. ":customer:" .. customerId .. ":entity:" .. 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 entityFeatureIds = entityBase._featureIds or {}
|
||||
entityFeatureData[entityId] = {}
|
||||
|
||||
for _, featureId in ipairs(entityFeatureIds) do
|
||||
local entityFeatureKey = entityCacheKey .. ":features:" .. featureId
|
||||
local entityFeatureHash = redis.call("HGETALL", entityFeatureKey)
|
||||
|
||||
if #entityFeatureHash > 0 then
|
||||
-- Parse entity feature
|
||||
local entityFeature = {}
|
||||
for i = 1, #entityFeatureHash, 2 do
|
||||
local key = entityFeatureHash[i]
|
||||
local value = entityFeatureHash[i + 1]
|
||||
|
||||
if value == "null" then
|
||||
entityFeature[key] = cjson.null
|
||||
elseif key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" or key == "_breakdown_count" or key == "_rollover_count" then
|
||||
entityFeature[key] = tonumber(value)
|
||||
elseif key == "unlimited" or key == "overage_allowed" then
|
||||
entityFeature[key] = (value == "true")
|
||||
else
|
||||
entityFeature[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
-- Fetch breakdown items for this entity feature
|
||||
local breakdownCount = entityFeature._breakdown_count or 0
|
||||
entityFeature._breakdown_count = nil
|
||||
entityFeature.breakdowns = {}
|
||||
|
||||
for i = 0, breakdownCount - 1 do
|
||||
local breakdownKey = entityFeatureKey .. ":breakdown:" .. i
|
||||
local breakdownHash = redis.call("HGETALL", breakdownKey)
|
||||
|
||||
if #breakdownHash > 0 then
|
||||
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 key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" then
|
||||
breakdownData[key] = tonumber(value)
|
||||
elseif key == "overage_allowed" then
|
||||
breakdownData[key] = (value == "true")
|
||||
else
|
||||
breakdownData[key] = value
|
||||
end
|
||||
end
|
||||
table.insert(entityFeature.breakdowns, breakdownData)
|
||||
end
|
||||
end
|
||||
|
||||
-- Fetch rollover items for this entity feature
|
||||
local rolloverCount = entityFeature._rollover_count or 0
|
||||
entityFeature._rollover_count = nil
|
||||
entityFeature.rollovers = {}
|
||||
|
||||
for i = 0, rolloverCount - 1 do
|
||||
local rolloverKey = entityFeatureKey .. ":rollover:" .. i
|
||||
local rolloverHash = redis.call("HGETALL", rolloverKey)
|
||||
|
||||
if #rolloverHash > 0 then
|
||||
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(entityFeature.rollovers, rolloverData)
|
||||
end
|
||||
end
|
||||
|
||||
entityFeatureData[entityId][featureId] = entityFeature
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- MERGE ENTITY PRODUCTS INTO CUSTOMER PRODUCTS
|
||||
-- ============================================================================
|
||||
|
||||
-- Collect all products: start with customer's products, then add all entity products
|
||||
local allProducts = {}
|
||||
if baseCustomer.products then
|
||||
for _, product in ipairs(baseCustomer.products) do
|
||||
table.insert(allProducts, product)
|
||||
end
|
||||
end
|
||||
|
||||
-- Add products from each entity
|
||||
for _, entityId in ipairs(entityIds) do
|
||||
local entityBase = entityBaseData[entityId]
|
||||
if entityBase and entityBase.products then
|
||||
for _, product in ipairs(entityBase.products) do
|
||||
table.insert(allProducts, product)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Merge products by product ID and normalized status
|
||||
baseCustomer.products = mergeProducts(allProducts)
|
||||
|
||||
-- ============================================================================
|
||||
-- MERGE ENTITY BALANCES INTO CUSTOMER FEATURES
|
||||
-- ============================================================================
|
||||
|
||||
for featureId, customerFeature in pairs(features) do
|
||||
-- Skip if unlimited
|
||||
if not customerFeature.unlimited then
|
||||
-- Aggregate entity balances for this feature
|
||||
local entityTotalBalance = 0
|
||||
local entityTotalUsage = 0
|
||||
local entityTotalIncludedUsage = 0
|
||||
local entityTotalUsageLimit = 0
|
||||
|
||||
for entityId, entityFeatures in pairs(entityFeatureData) do
|
||||
local entityFeature = entityFeatures[featureId]
|
||||
if entityFeature then
|
||||
entityTotalBalance = entityTotalBalance + toNum(entityFeature.balance)
|
||||
entityTotalUsage = entityTotalUsage + toNum(entityFeature.usage)
|
||||
entityTotalIncludedUsage = entityTotalIncludedUsage + toNum(entityFeature.included_usage)
|
||||
entityTotalUsageLimit = entityTotalUsageLimit + toNum(entityFeature.usage_limit)
|
||||
end
|
||||
end
|
||||
|
||||
-- Merge top-level balance and usage
|
||||
customerFeature.balance = toNum(customerFeature.balance) + entityTotalBalance
|
||||
customerFeature.usage = toNum(customerFeature.usage) + entityTotalUsage
|
||||
customerFeature.included_usage = toNum(customerFeature.included_usage) + entityTotalIncludedUsage
|
||||
customerFeature.usage_limit = toNum(customerFeature.usage_limit) + entityTotalUsageLimit
|
||||
|
||||
-- Merge breakdown balances and usage
|
||||
if customerFeature.breakdown and #customerFeature.breakdown > 0 then
|
||||
for i, breakdown in ipairs(customerFeature.breakdown) do
|
||||
local entityBreakdownBalance = 0
|
||||
local entityBreakdownUsage = 0
|
||||
local entityBreakdownIncludedUsage = 0
|
||||
local entityBreakdownUsageLimit = 0
|
||||
|
||||
for entityId, entityFeatures in pairs(entityFeatureData) do
|
||||
local entityFeature = entityFeatures[featureId]
|
||||
if entityFeature and entityFeature.breakdowns and entityFeature.breakdowns[i] then
|
||||
entityBreakdownBalance = entityBreakdownBalance + toNum(entityFeature.breakdowns[i].balance)
|
||||
entityBreakdownUsage = entityBreakdownUsage + toNum(entityFeature.breakdowns[i].usage)
|
||||
entityBreakdownIncludedUsage = entityBreakdownIncludedUsage + toNum(entityFeature.breakdowns[i].included_usage)
|
||||
entityBreakdownUsageLimit = entityBreakdownUsageLimit + toNum(entityFeature.breakdowns[i].usage_limit)
|
||||
end
|
||||
end
|
||||
|
||||
breakdown.balance = toNum(breakdown.balance) + entityBreakdownBalance
|
||||
breakdown.usage = toNum(breakdown.usage) + entityBreakdownUsage
|
||||
breakdown.included_usage = toNum(breakdown.included_usage) + entityBreakdownIncludedUsage
|
||||
breakdown.usage_limit = toNum(breakdown.usage_limit) + entityBreakdownUsageLimit
|
||||
end
|
||||
end
|
||||
|
||||
-- Merge rollover balances
|
||||
if customerFeature.rollovers and #customerFeature.rollovers > 0 then
|
||||
for i, rollover in ipairs(customerFeature.rollovers) do
|
||||
local entityRolloverBalance = 0
|
||||
|
||||
for entityId, entityFeatures in pairs(entityFeatureData) do
|
||||
local entityFeature = entityFeatures[featureId]
|
||||
if entityFeature and entityFeature.rollovers and entityFeature.rollovers[i] then
|
||||
entityRolloverBalance = entityRolloverBalance + toNum(entityFeature.rollovers[i].balance)
|
||||
end
|
||||
end
|
||||
|
||||
rollover.balance = toNum(rollover.balance) + entityRolloverBalance
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Add entity-only features (features that exist in entities but not in customer)
|
||||
for entityId, entityFeatures in pairs(entityFeatureData) do
|
||||
for featureId, entityFeature in pairs(entityFeatures) do
|
||||
if not features[featureId] then
|
||||
-- This feature doesn't exist in customer, add it
|
||||
-- Initialize with zero balance, then we'll aggregate all entity balances
|
||||
features[featureId] = {
|
||||
id = entityFeature.id,
|
||||
type = entityFeature.type,
|
||||
name = entityFeature.name,
|
||||
interval = entityFeature.interval,
|
||||
interval_count = entityFeature.interval_count,
|
||||
unlimited = entityFeature.unlimited,
|
||||
balance = 0,
|
||||
usage = 0,
|
||||
included_usage = 0,
|
||||
next_reset_at = cjson.null,
|
||||
overage_allowed = entityFeature.overage_allowed,
|
||||
usage_limit = entityFeature.usage_limit,
|
||||
credit_schema = entityFeature.credit_schema
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Now aggregate balances for entity-only features
|
||||
for featureId, customerFeature in pairs(features) do
|
||||
-- Only process if this was an entity-only feature (balance is still 0 from initialization)
|
||||
if customerFeature.balance == 0 and customerFeature.usage == 0 then
|
||||
local entityTotalBalance = 0
|
||||
local entityTotalUsage = 0
|
||||
local entityTotalIncludedUsage = 0
|
||||
local entityTotalUsageLimit = 0
|
||||
local minNextResetAt = nil
|
||||
|
||||
for entityId, entityFeatures in pairs(entityFeatureData) do
|
||||
local entityFeature = entityFeatures[featureId]
|
||||
if entityFeature then
|
||||
entityTotalBalance = entityTotalBalance + toNum(entityFeature.balance)
|
||||
entityTotalUsage = entityTotalUsage + toNum(entityFeature.usage)
|
||||
entityTotalIncludedUsage = entityTotalIncludedUsage + toNum(entityFeature.included_usage)
|
||||
entityTotalUsageLimit = entityTotalUsageLimit + toNum(entityFeature.usage_limit)
|
||||
|
||||
-- Find minimum next_reset_at across all entities
|
||||
if type(entityFeature.next_reset_at) == "number" then
|
||||
if not minNextResetAt or entityFeature.next_reset_at < minNextResetAt then
|
||||
minNextResetAt = entityFeature.next_reset_at
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
customerFeature.balance = entityTotalBalance
|
||||
customerFeature.usage = entityTotalUsage
|
||||
customerFeature.included_usage = entityTotalIncludedUsage
|
||||
customerFeature.usage_limit = entityTotalUsageLimit
|
||||
customerFeature.next_reset_at = minNextResetAt or cjson.null
|
||||
end
|
||||
end
|
||||
|
||||
-- Build final customer object
|
||||
baseCustomer._featureIds = nil -- Remove tracking field
|
||||
baseCustomer._entityIds = nil -- Remove tracking field
|
||||
baseCustomer.features = features
|
||||
|
||||
return cjson.encode(baseCustomer)
|
||||
|
||||
393
server/src/_luaScripts/archives/getEntity.backup.lua
Normal file
393
server/src/_luaScripts/archives/getEntity.backup.lua
Normal file
@@ -0,0 +1,393 @@
|
||||
-- getEntity.lua
|
||||
-- Atomically retrieves an entity object from Redis, reconstructing from base JSON and feature HSETs
|
||||
-- Merges entity features with customer features
|
||||
-- KEYS[1]: cache key (e.g., "{org_id}:env:customer:customer_id:entity:entity_id")
|
||||
-- ARGV[1]: org_id (for building customer cache keys)
|
||||
-- ARGV[2]: env (for building customer cache keys)
|
||||
|
||||
-- Helper function to safely convert values to numbers for arithmetic
|
||||
-- Returns the value if it's a number, otherwise returns 0
|
||||
local function toNum(value)
|
||||
return type(value) == "number" and value or 0
|
||||
end
|
||||
|
||||
-- Helper function to get product key for grouping (product_id:normalized_status)
|
||||
local function getProductKey(product)
|
||||
local status = product.status
|
||||
-- Normalize status: "active" or "past_due" -> "active", otherwise use actual status
|
||||
if status == "active" or status == "past_due" then
|
||||
status = "active"
|
||||
end
|
||||
return product.id .. ":" .. status
|
||||
end
|
||||
|
||||
-- Helper function to merge customer products into entity products
|
||||
-- Adds customer products that don't already exist in entity products (by product key)
|
||||
local function mergeCustomerProductsIntoEntity(entityProducts, customerProducts)
|
||||
if not customerProducts or #customerProducts == 0 then
|
||||
return entityProducts or {}
|
||||
end
|
||||
|
||||
if not entityProducts then
|
||||
entityProducts = {}
|
||||
end
|
||||
|
||||
-- Build a set of existing product keys in entity products
|
||||
local existingKeys = {}
|
||||
for _, product in ipairs(entityProducts) do
|
||||
local key = getProductKey(product)
|
||||
existingKeys[key] = true
|
||||
end
|
||||
|
||||
-- Add customer products that don't exist in entity products
|
||||
local mergedProducts = {}
|
||||
|
||||
-- First, add all entity products
|
||||
for _, product in ipairs(entityProducts) do
|
||||
table.insert(mergedProducts, product)
|
||||
end
|
||||
|
||||
-- Then, add customer products that don't exist
|
||||
for _, customerProduct in ipairs(customerProducts) do
|
||||
local key = getProductKey(customerProduct)
|
||||
if not existingKeys[key] then
|
||||
table.insert(mergedProducts, customerProduct)
|
||||
end
|
||||
end
|
||||
|
||||
return mergedProducts
|
||||
end
|
||||
|
||||
local cacheKey = KEYS[1]
|
||||
local baseKey = cacheKey
|
||||
local orgId = ARGV[1]
|
||||
local env = ARGV[2]
|
||||
|
||||
-- Get base entity JSON
|
||||
local baseJson = redis.call("GET", baseKey)
|
||||
if not baseJson then
|
||||
return nil
|
||||
end
|
||||
|
||||
local baseEntity = cjson.decode(baseJson)
|
||||
local entityFeatureIds = baseEntity._featureIds or {}
|
||||
|
||||
-- ============================================================================
|
||||
-- FETCH ENTITY FEATURES
|
||||
-- ============================================================================
|
||||
local entityFeatures = {}
|
||||
|
||||
for _, featureId in ipairs(entityFeatureIds) do
|
||||
local featureKey = cacheKey .. ":features:" .. featureId
|
||||
local featureHash = redis.call("HGETALL", featureKey)
|
||||
|
||||
-- If feature key is missing, return nil (partial eviction detected)
|
||||
if #featureHash == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Convert HGETALL result (flat array) to table
|
||||
local featureData = {}
|
||||
for i = 1, #featureHash, 2 do
|
||||
local key = featureHash[i]
|
||||
local value = featureHash[i + 1]
|
||||
|
||||
-- Check for null first before parsing
|
||||
if value == "null" then
|
||||
featureData[key] = cjson.null
|
||||
elseif key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" or key == "_breakdown_count" or key == "_rollover_count" then
|
||||
featureData[key] = tonumber(value)
|
||||
elseif key == "unlimited" or key == "overage_allowed" then
|
||||
featureData[key] = (value == "true")
|
||||
elseif key == "credit_schema" then
|
||||
-- Parse credit_schema JSON array
|
||||
if value ~= "" then
|
||||
featureData[key] = cjson.decode(value)
|
||||
else
|
||||
featureData[key] = cjson.null
|
||||
end
|
||||
else
|
||||
featureData[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
-- Get rollover count
|
||||
local rolloverCount = featureData._rollover_count or 0
|
||||
featureData._rollover_count = nil -- Remove from final output
|
||||
|
||||
-- Fetch rollover items
|
||||
local rollovers = {}
|
||||
for i = 0, rolloverCount - 1 do
|
||||
local rolloverKey = cacheKey .. ":features:" .. featureId .. ":rollover:" .. 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
|
||||
|
||||
if #rollovers > 0 then
|
||||
featureData.rollovers = rollovers
|
||||
end
|
||||
|
||||
-- Get breakdown count
|
||||
local breakdownCount = featureData._breakdown_count or 0
|
||||
featureData._breakdown_count = nil -- Remove from final output
|
||||
|
||||
-- Fetch breakdown items
|
||||
local breakdown = {}
|
||||
for i = 0, breakdownCount - 1 do
|
||||
local breakdownKey = cacheKey .. ":features:" .. featureId .. ":breakdown:" .. 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 key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" then
|
||||
breakdownData[key] = tonumber(value)
|
||||
elseif key == "overage_allowed" then
|
||||
breakdownData[key] = (value == "true")
|
||||
else
|
||||
breakdownData[key] = value
|
||||
end
|
||||
end
|
||||
table.insert(breakdown, breakdownData)
|
||||
end
|
||||
|
||||
if #breakdown > 0 then
|
||||
featureData.breakdown = breakdown
|
||||
end
|
||||
|
||||
entityFeatures[featureId] = featureData
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- FETCH CUSTOMER MASTER FEATURES (no entity aggregation)
|
||||
-- ============================================================================
|
||||
local customerFeatures = {}
|
||||
local customerBase = nil -- Store customer base for product access
|
||||
local customerId = baseEntity.customer_id
|
||||
|
||||
if customerId then
|
||||
local customerCacheKey = "{" .. orgId .. "}:" .. env .. ":customer:" .. customerId
|
||||
local customerBaseJson = redis.call("GET", customerCacheKey)
|
||||
|
||||
if customerBaseJson then
|
||||
customerBase = cjson.decode(customerBaseJson)
|
||||
local customerFeatureIds = customerBase._featureIds or {}
|
||||
|
||||
for _, featureId in ipairs(customerFeatureIds) do
|
||||
local customerFeatureKey = customerCacheKey .. ":features:" .. featureId
|
||||
local customerFeatureHash = redis.call("HGETALL", customerFeatureKey)
|
||||
|
||||
if #customerFeatureHash > 0 then
|
||||
-- Parse customer feature
|
||||
local customerFeature = {}
|
||||
for i = 1, #customerFeatureHash, 2 do
|
||||
local key = customerFeatureHash[i]
|
||||
local value = customerFeatureHash[i + 1]
|
||||
|
||||
if value == "null" then
|
||||
customerFeature[key] = cjson.null
|
||||
elseif key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" or key == "_breakdown_count" or key == "_rollover_count" then
|
||||
customerFeature[key] = tonumber(value)
|
||||
elseif key == "unlimited" or key == "overage_allowed" then
|
||||
customerFeature[key] = (value == "true")
|
||||
elseif key == "credit_schema" then
|
||||
if value ~= "" then
|
||||
customerFeature[key] = cjson.decode(value)
|
||||
else
|
||||
customerFeature[key] = cjson.null
|
||||
end
|
||||
else
|
||||
customerFeature[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
-- Fetch rollover items
|
||||
local rolloverCount = customerFeature._rollover_count or 0
|
||||
customerFeature._rollover_count = nil
|
||||
local rollovers = {}
|
||||
|
||||
for i = 0, rolloverCount - 1 do
|
||||
local rolloverKey = customerFeatureKey .. ":rollover:" .. i
|
||||
local rolloverHash = redis.call("HGETALL", rolloverKey)
|
||||
|
||||
if #rolloverHash > 0 then
|
||||
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
|
||||
end
|
||||
|
||||
if #rollovers > 0 then
|
||||
customerFeature.rollovers = rollovers
|
||||
end
|
||||
|
||||
-- Fetch breakdown items
|
||||
local breakdownCount = customerFeature._breakdown_count or 0
|
||||
customerFeature._breakdown_count = nil
|
||||
local breakdown = {}
|
||||
|
||||
for i = 0, breakdownCount - 1 do
|
||||
local breakdownKey = customerFeatureKey .. ":breakdown:" .. i
|
||||
local breakdownHash = redis.call("HGETALL", breakdownKey)
|
||||
|
||||
if #breakdownHash > 0 then
|
||||
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 key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" then
|
||||
breakdownData[key] = tonumber(value)
|
||||
elseif key == "overage_allowed" then
|
||||
breakdownData[key] = (value == "true")
|
||||
else
|
||||
breakdownData[key] = value
|
||||
end
|
||||
end
|
||||
table.insert(breakdown, breakdownData)
|
||||
end
|
||||
end
|
||||
|
||||
if #breakdown > 0 then
|
||||
customerFeature.breakdown = breakdown
|
||||
end
|
||||
|
||||
customerFeatures[featureId] = customerFeature
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- MERGE CUSTOMER AND ENTITY FEATURES
|
||||
-- ============================================================================
|
||||
local mergedFeatures = {}
|
||||
|
||||
-- First, add all customer features (inherited)
|
||||
for featureId, customerFeature in pairs(customerFeatures) do
|
||||
mergedFeatures[featureId] = customerFeature
|
||||
end
|
||||
|
||||
-- Then, merge or add entity features
|
||||
for featureId, entityFeature in pairs(entityFeatures) do
|
||||
local customerFeature = customerFeatures[featureId]
|
||||
|
||||
if customerFeature then
|
||||
-- Both customer and entity have this feature - merge balances
|
||||
if not entityFeature.unlimited and not customerFeature.unlimited then
|
||||
entityFeature.balance = toNum(entityFeature.balance) + toNum(customerFeature.balance)
|
||||
entityFeature.usage = toNum(entityFeature.usage) + toNum(customerFeature.usage)
|
||||
entityFeature.included_usage = toNum(entityFeature.included_usage) + toNum(customerFeature.included_usage)
|
||||
entityFeature.usage_limit = toNum(entityFeature.usage_limit) + toNum(customerFeature.usage_limit)
|
||||
|
||||
-- Use minimum next_reset_at (earliest reset time)
|
||||
if type(entityFeature.next_reset_at) == "number" and type(customerFeature.next_reset_at) == "number" then
|
||||
if customerFeature.next_reset_at < entityFeature.next_reset_at then
|
||||
entityFeature.next_reset_at = customerFeature.next_reset_at
|
||||
end
|
||||
elseif type(customerFeature.next_reset_at) == "number" then
|
||||
entityFeature.next_reset_at = customerFeature.next_reset_at
|
||||
end
|
||||
|
||||
-- Merge breakdown balances
|
||||
if entityFeature.breakdown and customerFeature.breakdown then
|
||||
for i, entityBreakdown in ipairs(entityFeature.breakdown) do
|
||||
local customerBreakdown = customerFeature.breakdown[i]
|
||||
if customerBreakdown then
|
||||
entityBreakdown.balance = toNum(entityBreakdown.balance) + toNum(customerBreakdown.balance)
|
||||
entityBreakdown.usage = toNum(entityBreakdown.usage) + toNum(customerBreakdown.usage)
|
||||
entityBreakdown.included_usage = toNum(entityBreakdown.included_usage) + toNum(customerBreakdown.included_usage)
|
||||
entityBreakdown.usage_limit = toNum(entityBreakdown.usage_limit) + toNum(customerBreakdown.usage_limit)
|
||||
|
||||
-- Use minimum next_reset_at for breakdown
|
||||
if type(entityBreakdown.next_reset_at) == "number" and type(customerBreakdown.next_reset_at) == "number" then
|
||||
if customerBreakdown.next_reset_at < entityBreakdown.next_reset_at then
|
||||
entityBreakdown.next_reset_at = customerBreakdown.next_reset_at
|
||||
end
|
||||
elseif type(customerBreakdown.next_reset_at) == "number" then
|
||||
entityBreakdown.next_reset_at = customerBreakdown.next_reset_at
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Merge rollover balances
|
||||
if entityFeature.rollovers and customerFeature.rollovers then
|
||||
for i, entityRollover in ipairs(entityFeature.rollovers) do
|
||||
local customerRollover = customerFeature.rollovers[i]
|
||||
if customerRollover then
|
||||
entityRollover.balance = toNum(entityRollover.balance) + toNum(customerRollover.balance)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
mergedFeatures[featureId] = entityFeature
|
||||
else
|
||||
-- Only entity has this feature - use entity's feature
|
||||
mergedFeatures[featureId] = entityFeature
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- MERGE CUSTOMER PRODUCTS INTO ENTITY PRODUCTS
|
||||
-- ============================================================================
|
||||
|
||||
-- Get entity products (start with entity's own products)
|
||||
local entityProducts = baseEntity.products or {}
|
||||
|
||||
-- Get customer products if customer base exists
|
||||
local customerProducts = nil
|
||||
if customerBase and customerBase.products then
|
||||
customerProducts = customerBase.products
|
||||
end
|
||||
|
||||
-- Merge customer products into entity products (only add if not exists)
|
||||
baseEntity.products = mergeCustomerProductsIntoEntity(entityProducts, customerProducts)
|
||||
|
||||
-- Build final entity object
|
||||
baseEntity._featureIds = nil -- Remove tracking field
|
||||
baseEntity.features = mergedFeatures
|
||||
|
||||
return cjson.encode(baseEntity)
|
||||
|
||||
Reference in New Issue
Block a user