-- ============================================================================ -- CONTEXT UTILITIES -- Functions for managing in-memory context during deductions -- ============================================================================ --[[ init_context(params) Initializes context object with current balances for all customer_entitlements and builds a rollover index for fast lookups. Reads from Redis once upfront to avoid multiple reads during passes. params: cache_key: string customer_entitlement_ids: array of customer_entitlement IDs full_customer: decoded FullCustomer object (nil when path index is available) pathidx_key: string (path index Redis Hash key) has_pathidx: boolean (true when path index exists) Returns: context table with: customer_entitlements: { [cus_ent_id]: { base_path, balance, adjustment, entities } } rollovers: { [rollover_id]: { base_path, cus_ent_id, balance, usage, entities } } mutation_logs: {} (ordered mutation items for receipts and replay) pending_writes: {} (empty array to queue writes) logs: {} (debug logs) logger: { log(fmt, ...): function } (logger that appends to logs) ]] local function init_context(params) local logs = {} local has_pathidx = params.has_pathidx local pathidx_key = params.pathidx_key local context = { customer_entitlements = {}, rollovers = {}, cache_key = params.cache_key, full_customer = params.full_customer, pathidx_key = pathidx_key, has_pathidx = has_pathidx, mutation_logs = {}, pending_writes = {}, logs = logs, logger = { log = function(fmt, ...) table.insert(logs, string.format(fmt, ...)) end }, } for _, ent_id in ipairs(params.customer_entitlement_ids or {}) do local base_path local has_entity_scope local is_loose local adjustment local unlimited local cus_ent_rollovers local cus_ent_balance local cus_ent_entities if has_pathidx then local result = get_customer_entitlement_via_index({ pathidx_key = pathidx_key, cache_key = params.cache_key, cus_ent_id = ent_id, }) if result then base_path = result.base_path has_entity_scope = result.has_entity_scope is_loose = result.is_loose local sub = result.sub adjustment = safe_number(sub.adjustment or 0) unlimited = sub.unlimited cus_ent_rollovers = sub.rollovers cus_ent_balance = safe_number(sub.balance or 0) cus_ent_entities = safe_table(sub.entities) end else -- Fallback path: decode full customer + nested loop search local cus_ent, cus_product, ce_idx, cp_idx = find_entitlement(params.full_customer, ent_id) if cus_ent then is_loose = (cp_idx == nil) if is_loose then local ece_idx_0 = ce_idx - 1 base_path = '$.extra_customer_entitlements[' .. ece_idx_0 .. ']' else local cp_idx_0 = cp_idx - 1 local ce_idx_0 = ce_idx - 1 base_path = '$.customer_products[' .. cp_idx_0 .. '].customer_entitlements[' .. ce_idx_0 .. ']' end local entitlement = cus_ent.entitlement has_entity_scope = not is_nil(entitlement) and not is_nil(entitlement.entity_feature_id) adjustment = cus_ent.adjustment or 0 unlimited = cus_ent.unlimited cus_ent_rollovers = cus_ent.rollovers cus_ent_balance = safe_number(cus_ent.balance or 0) cus_ent_entities = safe_table(cus_ent.entities) end end if base_path then local ent_data = { base_path = base_path, has_entity_scope = has_entity_scope, adjustment = adjustment, unlimited = unlimited, is_loose = is_loose, } if has_entity_scope then ent_data.balance = 0 ent_data.entities = cus_ent_entities or {} else ent_data.balance = cus_ent_balance or 0 ent_data.entities = nil end context.customer_entitlements[ent_id] = ent_data if cus_ent_rollovers and type(cus_ent_rollovers) == 'table' then for r_idx, rollover in ipairs(cus_ent_rollovers) do if rollover and rollover.id then local r_idx_0 = r_idx - 1 local rollover_path = base_path .. '.rollovers[' .. r_idx_0 .. ']' context.rollovers[rollover.id] = { base_path = rollover_path, cus_ent_id = ent_id, balance = safe_number(rollover.balance or 0), usage = safe_number(rollover.usage or 0), entities = safe_table(rollover.entities), } end end end end end return context end --[[ append_mutation_log(params) Appends one ordered mutation log entry for later receipt persistence and replay. ]] local function append_mutation_log(params) local context = params.context table.insert(context.mutation_logs, { target_type = params.target_type, customer_entitlement_id = params.customer_entitlement_id or cjson.null, rollover_id = params.rollover_id or cjson.null, entity_id = params.entity_id or cjson.null, credit_cost = params.credit_cost or 1, balance_delta = params.balance_delta or 0, adjustment_delta = params.adjustment_delta or 0, usage_delta = params.usage_delta or 0, value_delta = params.value_delta or 0, }) end --[[ update_in_memory_customer_entitlement_mutation(params) Applies an arbitrary balance/adjustment mutation to an in-memory customer_entitlement target. ]] local function update_in_memory_customer_entitlement_mutation(params) local target = params.target local entity_id = params.entity_id local balance_delta = params.balance_delta local adjustment_delta = params.adjustment_delta if balance_delta == nil then balance_delta = params.delta or 0 end if adjustment_delta == nil then adjustment_delta = params.alter_granted_balance and balance_delta or 0 end if entity_id then if not target[entity_id] then target[entity_id] = { balance = 0, adjustment = 0 } end target[entity_id].balance = (target[entity_id].balance or 0) + balance_delta target[entity_id].adjustment = (target[entity_id].adjustment or 0) + adjustment_delta return end target.balance = (target.balance or 0) + balance_delta target.adjustment = (target.adjustment or 0) + adjustment_delta end --[[ update_in_memory_rollover_mutation(params) Applies an arbitrary balance/usage mutation to an in-memory rollover target. ]] local function update_in_memory_rollover_mutation(params) local target = params.target local entity_id = params.entity_id local balance_delta = params.balance_delta or 0 local usage_delta = params.usage_delta or 0 if entity_id then if not target[entity_id] then target[entity_id] = { balance = 0, usage = 0 } end target[entity_id].balance = (target[entity_id].balance or 0) + balance_delta target[entity_id].usage = (target[entity_id].usage or 0) + usage_delta return end target.balance = (target.balance or 0) + balance_delta target.usage = (target.usage or 0) + usage_delta end --[[ queue_customer_entitlement_mutation(params) Queues a generic customer_entitlement mutation into pending_writes and mutation_logs. ]] local function queue_customer_entitlement_mutation(params) local context = params.context local path = params.path local balance_delta = params.balance_delta local adjustment_delta = params.adjustment_delta if balance_delta == nil then balance_delta = params.delta or 0 end if adjustment_delta == nil then adjustment_delta = params.alter_granted_balance and balance_delta or 0 end if balance_delta ~= 0 then table.insert(context.pending_writes, { path = path .. '.balance', delta = balance_delta }) end if adjustment_delta ~= 0 then table.insert(context.pending_writes, { path = path .. '.adjustment', delta = adjustment_delta }) end append_mutation_log({ context = context, target_type = 'customer_entitlement', customer_entitlement_id = params.customer_entitlement_id, rollover_id = nil, entity_id = params.entity_id, credit_cost = params.credit_cost or 1, balance_delta = balance_delta, adjustment_delta = adjustment_delta, usage_delta = 0, value_delta = params.value_delta or 0, }) end --[[ queue_rollover_mutation(params) Queues a generic rollover mutation into pending_writes and mutation_logs. ]] local function queue_rollover_mutation(params) local context = params.context local path = params.path local balance_delta = params.balance_delta or 0 local usage_delta = params.usage_delta or 0 local rollover_id = params.rollover_id if balance_delta ~= 0 then table.insert(context.pending_writes, { path = path .. '.balance', delta = balance_delta }) end if usage_delta ~= 0 then table.insert(context.pending_writes, { path = path .. '.usage', delta = usage_delta }) end local rollover_data = context.rollovers[rollover_id] append_mutation_log({ context = context, target_type = 'rollover', customer_entitlement_id = rollover_data and rollover_data.cus_ent_id or nil, rollover_id = rollover_id, entity_id = params.entity_id, credit_cost = params.credit_cost or 1, balance_delta = balance_delta, adjustment_delta = 0, usage_delta = usage_delta, value_delta = params.value_delta or 0, }) end --[[ queue_rollover_update(params) Queues a rollover balance/usage update to pending_writes. Rollovers track both balance (decrements) and usage (increments). ]] local function queue_rollover_update(params) local deduct_amount = params.deduct_amount queue_rollover_mutation({ context = params.context, path = params.path, rollover_id = params.rollover_id, entity_id = params.entity_id, credit_cost = params.credit_cost, balance_delta = -deduct_amount, usage_delta = deduct_amount, value_delta = params.value_delta or 0, }) end --[[ update_in_memory_rollover(params) Backwards-compatible wrapper for main deduction paths. ]] local function update_in_memory_rollover(params) update_in_memory_rollover_mutation({ target = params.target, entity_id = params.entity_id, balance_delta = -(params.deduct_amount or 0), usage_delta = params.deduct_amount or 0, }) end --[[ apply_pending_writes(cache_key, context) Applies all queued writes to Redis. Called only after validation passes. ]] local function apply_pending_writes(cache_key, context) for _, write in ipairs(context.pending_writes) do redis.call('JSON.NUMINCRBY', cache_key, write.path, write.delta) end end