428 lines
19 KiB
Lua
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
|
|
|