Files
cfw-autumn/server/src/_luaScripts/luaUtils/getCustomerEntityUtils.lua
2025-12-02 10:46:11 +00:00

428 lines
19 KiB
Lua

-- ============================================================================
-- GET CUSTOMER/ENTITY UTILITY FUNCTIONS
-- ============================================================================
-- ============================================================================
-- LEGACY DATA MERGE UTILITIES
-- ============================================================================
-- Helper function to merge breakdown_legacy_data by key
-- Sums prepaid_quantity for matching keys, adds new items for non-matching keys
-- Mutates targetBreakdown by merging sourceBreakdown into it
local function mergeBreakdownLegacyData(targetBreakdown, sourceBreakdown)
if not sourceBreakdown then return end
if not targetBreakdown then return end
for _, sourceItem in ipairs(sourceBreakdown) do
local foundMatch = false
-- Try to find matching item by key
for _, targetItem in ipairs(targetBreakdown) do
if sourceItem.key and targetItem.key and sourceItem.key == targetItem.key then
-- Found match - sum prepaid_quantity
targetItem.prepaid_quantity = (targetItem.prepaid_quantity or 0) + (sourceItem.prepaid_quantity or 0)
foundMatch = true
break
end
end
-- If no match found, add as new item
if not foundMatch then
table.insert(targetBreakdown, {
key = sourceItem.key,
prepaid_quantity = sourceItem.prepaid_quantity or 0
})
end
end
end
-- Helper function to create a virtual breakdown item from top-level legacy data
-- Used when legacy data has no breakdown_legacy_data array but needs to be merged
local function legacyDataToBreakdownItem(featureData)
return {
key = featureData.key or "",
prepaid_quantity = featureData.prepaid_quantity or 0
}
end
-- Helper function to merge cusProductLegacyData from source into target
-- Merges by plan_id: keeps subscription_id from either, merges options arrays
-- Mutates targetLegacyData
local function mergeCusProductLegacyData(targetLegacyData, sourceLegacyData)
if not sourceLegacyData then return end
if not targetLegacyData then return end
local targetCusProduct = targetLegacyData.cusProductLegacyData
local sourceCusProduct = sourceLegacyData.cusProductLegacyData
if not sourceCusProduct then return end
-- Initialize target cusProductLegacyData if nil
if not targetCusProduct then
targetLegacyData.cusProductLegacyData = {}
targetCusProduct = targetLegacyData.cusProductLegacyData
end
-- Merge each plan_id from source into target
for planId, sourceProductData in pairs(sourceCusProduct) do
local targetProductData = targetCusProduct[planId]
if targetProductData then
-- Plan exists in target - merge values
-- subscription_id: keep target's if exists, otherwise use source's
if not targetProductData.subscription_id or targetProductData.subscription_id == cjson.null then
targetProductData.subscription_id = sourceProductData.subscription_id
end
-- options: merge by feature_id (add source options that don't exist in target)
if sourceProductData.options and #sourceProductData.options > 0 then
if not targetProductData.options then
targetProductData.options = {}
end
-- Build set of existing feature_ids in target options
local existingFeatureIds = {}
for _, option in ipairs(targetProductData.options) do
if option.feature_id then
existingFeatureIds[option.feature_id] = true
end
end
-- Add source options that don't exist in target
for _, sourceOption in ipairs(sourceProductData.options) do
if sourceOption.feature_id and not existingFeatureIds[sourceOption.feature_id] then
table.insert(targetProductData.options, sourceOption)
end
end
end
else
-- Plan doesn't exist in target - add it (deep copy)
local newProductData = {
subscription_id = sourceProductData.subscription_id,
options = {}
}
if sourceProductData.options then
for _, option in ipairs(sourceProductData.options) do
table.insert(newProductData.options, option)
end
end
targetCusProduct[planId] = newProductData
end
end
end
-- Helper function to merge cusFeatureLegacyData from source into target
-- Merges by feature_id: sums prepaid_quantity, merges breakdown_legacy_data by key
-- Similar to balance breakdown merging:
-- - Each legacy data without breakdown_legacy_data is treated as a single "virtual" breakdown item
-- - Breakdown items are matched by key
-- - If keys match: sum prepaid_quantity
-- - If keys differ: create separate breakdown items
-- Mutates targetLegacyData
local function mergeCusFeatureLegacyData(targetLegacyData, sourceLegacyData)
if not sourceLegacyData then return end
if not targetLegacyData then return end
local targetCusFeature = targetLegacyData.cusFeatureLegacyData
local sourceCusFeature = sourceLegacyData.cusFeatureLegacyData
if not sourceCusFeature then return end
-- Initialize target cusFeatureLegacyData if nil
if not targetCusFeature then
targetLegacyData.cusFeatureLegacyData = {}
targetCusFeature = targetLegacyData.cusFeatureLegacyData
end
-- Merge each feature_id from source into target
for featureId, sourceFeatureData in pairs(sourceCusFeature) do
local targetFeatureData = targetCusFeature[featureId]
if targetFeatureData then
-- Feature exists in target - merge values
-- ============================================================================
-- CAPTURE VIRTUAL BREAKDOWN ITEMS BEFORE MUTATING TOP-LEVEL FIELDS
-- ============================================================================
-- Similar to balance merging: capture breakdown items BEFORE summing
-- Get effective breakdown from target
local targetBreakdowns = targetFeatureData.breakdown_legacy_data
local targetHadBreakdown = targetBreakdowns and #targetBreakdowns > 0
if not targetHadBreakdown then
-- Target has no breakdown - create virtual item from top-level fields
targetBreakdowns = { legacyDataToBreakdownItem(targetFeatureData) }
end
-- Get effective breakdown from source
local sourceBreakdowns = sourceFeatureData.breakdown_legacy_data
if not sourceBreakdowns or #sourceBreakdowns == 0 then
-- Source has no breakdown - create virtual item from top-level fields
sourceBreakdowns = { legacyDataToBreakdownItem(sourceFeatureData) }
end
-- ============================================================================
-- NOW MERGE TOP-LEVEL PREPAID_QUANTITY
-- ============================================================================
targetFeatureData.prepaid_quantity = (targetFeatureData.prepaid_quantity or 0) + (sourceFeatureData.prepaid_quantity or 0)
-- ============================================================================
-- MERGE BREAKDOWN ITEMS BY KEY
-- ============================================================================
for _, sourceItem in ipairs(sourceBreakdowns) do
local foundMatch = false
for _, targetItem in ipairs(targetBreakdowns) do
if sourceItem.key and targetItem.key and sourceItem.key == targetItem.key then
-- Found match - sum prepaid_quantity
targetItem.prepaid_quantity = (targetItem.prepaid_quantity or 0) + (sourceItem.prepaid_quantity or 0)
foundMatch = true
break
end
end
-- If no match found, add as new item
if not foundMatch then
table.insert(targetBreakdowns, {
key = sourceItem.key,
prepaid_quantity = sourceItem.prepaid_quantity or 0
})
end
end
-- Set breakdown_legacy_data if we have multiple items or originally had breakdown
if #targetBreakdowns > 1 or targetHadBreakdown then
targetFeatureData.breakdown_legacy_data = targetBreakdowns
-- Clear top-level key since it's now in breakdown items
targetFeatureData.key = nil
end
else
-- Feature doesn't exist in target - add it (deep copy)
local newFeatureData = {
key = sourceFeatureData.key,
prepaid_quantity = sourceFeatureData.prepaid_quantity or 0,
breakdown_legacy_data = {}
}
if sourceFeatureData.breakdown_legacy_data and #sourceFeatureData.breakdown_legacy_data > 0 then
for _, item in ipairs(sourceFeatureData.breakdown_legacy_data) do
table.insert(newFeatureData.breakdown_legacy_data, {
key = item.key,
prepaid_quantity = item.prepaid_quantity or 0
})
end
end
targetCusFeature[featureId] = newFeatureData
end
end
end
-- ============================================================================
-- CUSTOMER/ENTITY RETRIEVAL FUNCTIONS
-- ============================================================================
-- Get customer object with merged balances and subscriptions
-- Parameters:
-- orgId: Organization ID
-- env: Environment
-- customerId: Customer ID
-- skipEntityMerge: If true, only load customer's own balances (no entity merging)
-- Returns: customer object table (not JSON encoded), or nil if not found
local function getCustomerObject(orgId, env, customerId, skipEntityMerge)
-- Build versioned cache key using shared utility
local cacheKey = buildCustomerCacheKey(orgId, env, customerId)
-- Load balances based on merge mode
-- If skipEntityMerge is true, only load customer's own balances (no entity merging)
-- If skipEntityMerge is false, load merged balances (customer + entities)
local balances
if skipEntityMerge then
-- Load only customer's own balances without entity merging
balances = loadBalances(cacheKey, orgId, env, customerId, "__CUSTOMER_ONLY__")
else
-- Load merged balances (customer + entities)
balances = loadBalances(cacheKey, orgId, env, customerId)
end
if not balances then
return nil -- Customer not in cache or partial eviction detected
end
-- Get base customer JSON for subscriptions and metadata
local baseJson = redis.call("GET", cacheKey)
if not baseJson then
return nil
end
local baseCustomer = cjson.decode(baseJson)
local entityIds = baseCustomer._entityIds or {}
-- ============================================================================
-- MERGE ENTITY SUBSCRIPTIONS INTO CUSTOMER SUBSCRIPTIONS
-- ============================================================================
-- Build entity base data map for subscription access
local entityBaseData = {}
for _, entityId in ipairs(entityIds) do
local entityCacheKey = buildEntityCacheKey(orgId, env, customerId, entityId)
local entityBaseJson = redis.call("GET", entityCacheKey)
if entityBaseJson then
entityBaseData[entityId] = cjson.decode(entityBaseJson)
end
end
-- Collect all subscriptions: start with customer's subscriptions, then add all entity subscriptions
local allSubscriptions = {}
if baseCustomer.subscriptions then
for _, subscription in ipairs(baseCustomer.subscriptions) do
table.insert(allSubscriptions, subscription)
end
end
-- Add subscriptions from each entity
for _, entityId in ipairs(entityIds) do
local entityBase = entityBaseData[entityId]
if entityBase and entityBase.subscriptions then
for _, subscription in ipairs(entityBase.subscriptions) do
table.insert(allSubscriptions, subscription)
end
end
end
-- Merge subscriptions by plan ID and normalized status
baseCustomer.subscriptions = mergeSubscriptions(allSubscriptions)
-- Collect all scheduled_subscriptions: start with customer's scheduled_subscriptions, then add all entity scheduled_subscriptions
local allScheduledSubscriptions = {}
if baseCustomer.scheduled_subscriptions then
for _, subscription in ipairs(baseCustomer.scheduled_subscriptions) do
table.insert(allScheduledSubscriptions, subscription)
end
end
-- Add scheduled_subscriptions from each entity
for _, entityId in ipairs(entityIds) do
local entityBase = entityBaseData[entityId]
if entityBase and entityBase.scheduled_subscriptions then
for _, subscription in ipairs(entityBase.scheduled_subscriptions) do
table.insert(allScheduledSubscriptions, subscription)
end
end
end
-- Merge scheduled_subscriptions by plan ID and normalized status
baseCustomer.scheduled_subscriptions = mergeSubscriptions(allScheduledSubscriptions)
-- ============================================================================
-- MERGE ENTITY LEGACY DATA INTO CUSTOMER LEGACY DATA
-- ============================================================================
if not skipEntityMerge and baseCustomer.legacyData then
for _, entityId in ipairs(entityIds) do
local entityBase = entityBaseData[entityId]
if entityBase and entityBase.legacyData then
mergeCusFeatureLegacyData(baseCustomer.legacyData, entityBase.legacyData)
mergeCusProductLegacyData(baseCustomer.legacyData, entityBase.legacyData)
end
end
end
-- Merge invoices
-- Build final customer object
baseCustomer.invoices = baseCustomer.invoices or nil
baseCustomer._balanceFeatureIds = nil -- Remove tracking field
baseCustomer._entityIds = nil -- Remove tracking field
baseCustomer.balances = balances
return baseCustomer
end
-- Get entity object with merged balances and subscriptions
-- Parameters:
-- orgId: Organization ID
-- env: Environment
-- customerId: Customer ID
-- entityId: Entity ID
-- skipCustomerMerge: If true, only load entity's own balances (no customer merging)
-- Returns: entity object table (not JSON encoded), or nil if not found
local function getEntityObject(orgId, env, customerId, entityId, skipCustomerMerge)
-- Build versioned entity cache key using shared utility
local entityCacheKey = buildEntityCacheKey(orgId, env, customerId, entityId)
-- Get base entity JSON
local baseJson = redis.call("GET", entityCacheKey)
if not baseJson then
return nil
end
local baseEntity = cjson.decode(baseJson)
-- Build customer cache key for balance loading
local customerCacheKey = buildCustomerCacheKey(orgId, env, customerId)
-- ============================================================================
-- LOAD BALANCES USING loadBalances
-- ============================================================================
local mergedBalances
if skipCustomerMerge then
-- Load only entity's own balances (no customer merging)
-- We'll use loadBalances with "__CUSTOMER_ONLY__" mode on the entity cache key
-- This is a bit of a hack but works with the current structure
mergedBalances = loadBalances(entityCacheKey, orgId, env, customerId, "__CUSTOMER_ONLY__")
else
-- Load entity-level merged balances (entity + customer)
-- loadBalances handles this when entityId is provided
mergedBalances = loadBalances(customerCacheKey, orgId, env, customerId, entityId)
end
-- If balances loading failed (partial eviction), return nil
if not mergedBalances then
return nil
end
-- ============================================================================
-- MERGE CUSTOMER SUBSCRIPTIONS INTO ENTITY SUBSCRIPTIONS
-- Skip if skipCustomerMerge is true
-- ============================================================================
-- Get entity subscriptions (start with entity's own subscriptions)
local entitySubscriptions = baseEntity.subscriptions or {}
local entityScheduledSubscriptions = baseEntity.scheduled_subscriptions or {}
if not skipCustomerMerge then
-- Get customer base data for subscriptions and legacy data
local customerBase = nil
local customerBaseJson = redis.call("GET", customerCacheKey)
if customerBaseJson then
customerBase = cjson.decode(customerBaseJson)
end
-- Merge customer subscriptions into entity subscriptions (only add if not exists)
local customerSubscriptions = customerBase and customerBase.subscriptions or nil
local customerScheduledSubscriptions = customerBase and customerBase.scheduled_subscriptions or nil
baseEntity.subscriptions = mergeCustomerSubscriptionsIntoEntity(entitySubscriptions, customerSubscriptions)
baseEntity.scheduled_subscriptions = mergeCustomerSubscriptionsIntoEntity(entityScheduledSubscriptions, customerScheduledSubscriptions)
-- ============================================================================
-- MERGE CUSTOMER LEGACY DATA INTO ENTITY LEGACY DATA
-- ============================================================================
if baseEntity.legacyData and customerBase and customerBase.legacyData then
mergeCusFeatureLegacyData(baseEntity.legacyData, customerBase.legacyData)
mergeCusProductLegacyData(baseEntity.legacyData, customerBase.legacyData)
end
else
-- No merging - just use entity's own subscriptions
baseEntity.subscriptions = entitySubscriptions
baseEntity.scheduled_subscriptions = entityScheduledSubscriptions
end
-- Build final entity object
baseEntity._balanceFeatureIds = nil -- Remove tracking field
baseEntity.balances = mergedBalances
return baseEntity
end