fix: deducting from purchased balance when value < 0

This commit is contained in:
John Yeo
2025-11-17 23:08:50 +00:00
parent 65fc1e882f
commit 449c5b830e
6 changed files with 134 additions and 73 deletions

View File

@@ -312,11 +312,13 @@ local function deductFromCurrentBalance(cusFeature, amount, adjustGrantedBalance
}
end
-- Deduct from overage (second pass - increments purchased_balance up to max_purchase)
-- Deduct from overage (handles purchased_balance adjustments)
-- For positive amounts: increments purchased_balance up to max_purchase
-- For negative amounts (refunds): decrements purchased_balance down to 0
-- Only applies if overage_allowed is true
-- Parameters:
-- cusFeature: Balance object to deduct from
-- amount: Remaining amount to cover with overage
-- amount: Amount to handle (positive for deduction, negative for refund)
-- adjustGrantedBalance: If true, decrement granted_balance instead of incrementing usage
-- Returns: { remaining: number, deltas: [{key, field, delta}], stateChanges: [{type, index, field, newValue/delta}] }
local function deductFromOverage(cusFeature, amount, adjustGrantedBalance)
@@ -333,8 +335,8 @@ local function deductFromOverage(cusFeature, amount, adjustGrantedBalance)
}
end
-- Only proceed if there's remaining amount and overage is allowed
if remaining <= 0 then
-- Early return if no amount to process
if remaining == 0 then
return {
remaining = remaining,
deltas = deltas,
@@ -354,8 +356,10 @@ local function deductFromOverage(cusFeature, amount, adjustGrantedBalance)
}
end
-- If cusFeature has breakdowns, deduct from breakdown overage
if cusFeature.breakdown and #cusFeature.breakdown > 0 then
-- POSITIVE AMOUNT: Increment purchased_balance up to max_purchase
if remaining > 0 then
-- If cusFeature has breakdowns, deduct from breakdown overage
if cusFeature.breakdown and #cusFeature.breakdown > 0 then
for index, breakdown in ipairs(cusFeature.breakdown) do
if remaining <= 0 then break end
@@ -484,6 +488,49 @@ local function deductFromOverage(cusFeature, amount, adjustGrantedBalance)
remaining = remaining - toIncrement
end
end
-- NEGATIVE AMOUNT (REFUND): Decrement purchased_balance down to 0
else
-- If cusFeature has breakdowns, refund from breakdown overage
if cusFeature.breakdown and #cusFeature.breakdown > 0 then
for index, breakdown in ipairs(cusFeature.breakdown) do
if remaining >= 0 then break end
local breakdownAllowOverage = breakdown.overage_allowed == true
if breakdownAllowOverage then
local breakdownPurchasedBalance = breakdown.purchased_balance or 0
local toDecrement = math.min(-remaining, breakdownPurchasedBalance)
if toDecrement > 0 then
table.insert(deltas, {key = breakdown._key, field = "purchased_balance", delta = -toDecrement})
table.insert(deltas, {key = cusFeature._key, field = "purchased_balance", delta = -toDecrement})
table.insert(deltas, {key = breakdown._key, field = "usage", delta = -toDecrement})
table.insert(deltas, {key = cusFeature._key, field = "usage", delta = -toDecrement})
table.insert(stateChanges, {type = "breakdown", index = index, field = "purchased_balance", delta = -toDecrement})
table.insert(stateChanges, {type = "breakdown", index = index, field = "usage", delta = -toDecrement})
table.insert(stateChanges, {type = "cusFeature", field = "purchased_balance", delta = -toDecrement})
table.insert(stateChanges, {type = "cusFeature", field = "usage", delta = -toDecrement})
remaining = remaining + toDecrement
end
end
end
else
-- No breakdowns: refund from top-level overage
local topLevelPurchasedBalance = cusFeature.purchased_balance or 0
local toDecrement = math.min(-remaining, topLevelPurchasedBalance)
if toDecrement > 0 then
table.insert(deltas, {key = cusFeature._key, field = "purchased_balance", delta = -toDecrement})
table.insert(deltas, {key = cusFeature._key, field = "usage", delta = -toDecrement})
table.insert(stateChanges, {type = "cusFeature", field = "purchased_balance", delta = -toDecrement})
table.insert(stateChanges, {type = "cusFeature", field = "usage", delta = -toDecrement})
remaining = remaining + toDecrement
end
end
end
return {
@@ -495,6 +542,8 @@ end
-- Deduct from main balance (handles both breakdown and non-breakdown scenarios)
-- Handles both positive (deduct) and negative (refund) amounts
-- For positive amounts: deducts from current_balance, then overage
-- For negative amounts: refunds from overage (purchased_balance), then current_balance
-- Parameters:
-- cusFeature: Balance object to deduct from
-- amount: Amount to deduct (can be negative for refunds)
@@ -505,30 +554,58 @@ local function deductFromMainBalance(cusFeature, amount, adjustGrantedBalance)
local allStateChanges = {}
local remaining = amount
-- Pass 1: Deduct from current_balance (only deducts from positive balances)
local currentBalanceResult = deductFromCurrentBalance(cusFeature, remaining, adjustGrantedBalance)
remaining = currentBalanceResult.remaining
-- POSITIVE AMOUNT (DEDUCTION): current_balance → overage
local isPaidAllocated = cusFeature.feature and cusFeature.feature.type == "metered" and cusFeature.feature.consumable == false and cusFeature.overage_allowed == true
-- Collect current balance deltas and state changes
for _, delta in ipairs(currentBalanceResult.deltas) do
table.insert(allDeltas, delta)
end
for _, stateChange in ipairs(currentBalanceResult.stateChanges) do
table.insert(allStateChanges, stateChange)
end
-- Pass 2: Deduct from overage (increments purchased_balance up to max_purchase)
if remaining > 0 then
if remaining > 0 or isPaidAllocated then
-- Pass 1: Deduct from current_balance
local currentBalanceResult = deductFromCurrentBalance(cusFeature, remaining, adjustGrantedBalance)
remaining = currentBalanceResult.remaining
for _, delta in ipairs(currentBalanceResult.deltas) do
table.insert(allDeltas, delta)
end
for _, stateChange in ipairs(currentBalanceResult.stateChanges) do
table.insert(allStateChanges, stateChange)
end
-- Pass 2: Deduct from overage (increments purchased_balance up to max_purchase)
if remaining > 0 then
local overageResult = deductFromOverage(cusFeature, remaining, adjustGrantedBalance)
remaining = overageResult.remaining
for _, delta in ipairs(overageResult.deltas) do
table.insert(allDeltas, delta)
end
for _, stateChange in ipairs(overageResult.stateChanges) do
table.insert(allStateChanges, stateChange)
end
end
-- NEGATIVE AMOUNT (REFUND): overage → current_balance
else
-- Pass 1: Refund from overage (decrements purchased_balance down to 0)
local overageResult = deductFromOverage(cusFeature, remaining, adjustGrantedBalance)
remaining = overageResult.remaining
-- Collect overage deltas and state changes
for _, delta in ipairs(overageResult.deltas) do
table.insert(allDeltas, delta)
end
for _, stateChange in ipairs(overageResult.stateChanges) do
table.insert(allStateChanges, stateChange)
end
-- Pass 2: Refund to current_balance (increments current_balance)
if remaining < 0 then
local currentBalanceResult = deductFromCurrentBalance(cusFeature, remaining, adjustGrantedBalance)
remaining = currentBalanceResult.remaining
for _, delta in ipairs(currentBalanceResult.deltas) do
table.insert(allDeltas, delta)
end
for _, stateChange in ipairs(currentBalanceResult.stateChanges) do
table.insert(allStateChanges, stateChange)
end
end
end
return {
@@ -1059,19 +1136,19 @@ for _, request in ipairs(requests) do
end
end
-- Helper function to apply legacy continuous_use logic
-- Legacy case: continuous_use features always allow overage
local function applyContinuousUseLegacy(balance)
if balance.feature and balance.feature.type == "metered" and balance.feature.consumable == false then
balance.overage_allowed = true
-- Apply to breakdowns as well
if balance.breakdown then
for _, breakdown in ipairs(balance.breakdown) do
breakdown.overage_allowed = true
end
end
end
end
-- -- Helper function to apply legacy continuous_use logic
-- -- Legacy case: continuous_use features always allow overage
-- local function applyContinuousUseLegacy(balance)
-- if balance.feature and balance.feature.type == "metered" and balance.feature.consumable == false and remaining > 0 then
-- balance.overage_allowed = true
-- -- Apply to breakdowns as well
-- if balance.breakdown then
-- for _, breakdown in ipairs(balance.breakdown) do
-- breakdown.overage_allowed = true
-- end
-- end
-- end
-- end
-- Get list of all customer feature IDs
local baseJson = redis.call("GET", cacheKey)
@@ -1089,8 +1166,8 @@ for _, featureId in ipairs(allFeatureIds) do
-- Add id field for compatibility with existing code
balance.id = featureId
-- Apply legacy continuous_use logic
applyContinuousUseLegacy(balance)
-- -- Apply legacy continuous_use logic
-- applyContinuousUseLegacy(balance)
loadedCusFeatures[featureId] = balance
end
@@ -1118,8 +1195,8 @@ for _, entityId in ipairs(entityIds) do
-- Add id field for compatibility with existing code
balance.id = featureId
-- Apply legacy continuous_use logic
applyContinuousUseLegacy(balance)
-- -- Apply legacy continuous_use logic
-- applyContinuousUseLegacy(balance)
entityFeatureStates[entityId][featureId] = balance
end