Merge branch 'fix/cache-merging' into staging

This commit is contained in:
John Yeo
2025-12-02 10:46:55 +00:00
3 changed files with 132 additions and 4 deletions

View File

@@ -45,6 +45,72 @@ local function legacyDataToBreakdownItem(featureData)
}
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:
@@ -257,6 +323,7 @@ local function getCustomerObject(orgId, env, customerId, skipEntityMerge)
local entityBase = entityBaseData[entityId]
if entityBase and entityBase.legacyData then
mergeCusFeatureLegacyData(baseCustomer.legacyData, entityBase.legacyData)
mergeCusProductLegacyData(baseCustomer.legacyData, entityBase.legacyData)
end
end
end
@@ -343,6 +410,7 @@ local function getEntityObject(orgId, env, customerId, entityId, skipCustomerMer
-- ============================================================================
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

View File

@@ -739,9 +739,11 @@ local function loadBalances(cacheKey, orgId, env, customerId, entityId)
for featureId, entityBalance in pairs(entityBalances) do
if not balances[featureId] then
-- This balance doesn't exist in customer, add it with zero values
-- Copy plan_id from entity so breakdown keys match when merging
balances[featureId] = {
feature_id = featureId,
feature = entityBalance.feature,
plan_id = entityBalance.plan_id,
unlimited = entityBalance.unlimited,
granted_balance = 0,
purchased_balance = 0,
@@ -750,7 +752,7 @@ local function loadBalances(cacheKey, orgId, env, customerId, entityId)
max_purchase = entityBalance.max_purchase or 0,
overage_allowed = entityBalance.overage_allowed,
reset = entityBalance.reset,
breakdown = {}
breakdown = nil
}
end
end

View File

@@ -189,6 +189,35 @@ describe(`${chalk.yellowBright("track-entity-products2: entity product tracking
customerFromCache.features[TestFeature.Messages],
);
// EXPECT CACHE TO MATCH DB (CUSTOMER LEVEL)
const cacheBalance = customerFromCache.features[TestFeature.Messages];
expect(customerFromDb.features[TestFeature.Messages]).toMatchObject({
id: cacheBalance.id,
balance: cacheBalance.balance,
included_usage: cacheBalance.included_usage,
interval: cacheBalance.interval,
name: cacheBalance.name,
next_reset_at: cacheBalance.next_reset_at,
overage_allowed: cacheBalance.overage_allowed,
type: cacheBalance.type,
unlimited: cacheBalance.unlimited,
usage: cacheBalance.usage,
});
if (cacheBalance.breakdown) {
for (let i = 0; i < cacheBalance.breakdown.length; i++) {
const breakdown = cacheBalance.breakdown?.[i];
expect(
customerFromDb.features[TestFeature.Messages].breakdown?.[i],
).toMatchObject({
balance: breakdown?.balance,
included_usage: breakdown?.included_usage,
interval: breakdown?.interval,
usage: breakdown?.usage,
});
}
}
// Verify each entity's balance
let totalEntityBalanceFromDb = 0;
let totalEntityBalanceFromCache = 0;
@@ -203,9 +232,38 @@ describe(`${chalk.yellowBright("track-entity-products2: entity product tracking
);
// Each entity should have some messages deducted
expect(entityFromDb.features[TestFeature.Messages]).toMatchObject(
entityFromCache.features[TestFeature.Messages],
);
// expect(entityFromDb.features[TestFeature.Messages]).toMatchObject(
// entityFromCache.features[TestFeature.Messages],
// );
// EXPECT DB TO MATCH CACHE (ENTITY LEVEL)
expect(entityFromDb.features[TestFeature.Messages]).toMatchObject({
balance: entityFromCache.features[TestFeature.Messages].balance,
included_usage:
entityFromCache.features[TestFeature.Messages].included_usage,
interval: entityFromCache.features[TestFeature.Messages].interval,
name: entityFromCache.features[TestFeature.Messages].name,
type: entityFromCache.features[TestFeature.Messages].type,
unlimited: entityFromCache.features[TestFeature.Messages].unlimited,
});
if (entityFromCache.features[TestFeature.Messages].breakdown) {
for (
let i = 0;
i < entityFromCache.features[TestFeature.Messages].breakdown.length;
i++
) {
const breakdown =
entityFromCache.features[TestFeature.Messages].breakdown[i];
expect(
entityFromDb.features[TestFeature.Messages].breakdown?.[i],
).toMatchObject({
balance: breakdown?.balance,
included_usage: breakdown?.included_usage,
interval: breakdown?.interval,
usage: breakdown?.usage,
});
}
}
totalEntityBalanceFromDb +=
entityFromDb.features[TestFeature.Messages].balance;