enforce windowed usage cap in V2 deduction lua

This commit is contained in:
Owen Greenhalgh
2026-06-02 19:21:38 +01:00
parent 8eab888024
commit 2b63e7de1a
15 changed files with 678 additions and 28 deletions

View File

@@ -18,29 +18,61 @@ local function read_subject_balances(params)
local balances_by_id = {}
local missing_customer_entitlement_ids = {}
local entries_by_balance_key = {}
local seen_ids_by_balance_key = {}
-- Anchor-only cus_ents (usage-window owners not in the deduction set) must
-- not abort the deduction when absent; a missing anchor fails closed later.
local anchor_only_ids = {}
local balance_keys_by_feature_id = safe_table(params.balance_keys_by_feature_id)
local function queue_balance_read(customer_entitlement_id, feature_id)
if not (customer_entitlement_id and feature_id) then
return false
end
local balance_key = balance_keys_by_feature_id[feature_id]
if not balance_key then
return false
end
if entries_by_balance_key[balance_key] == nil then
entries_by_balance_key[balance_key] = {
feature_id = feature_id,
customer_entitlement_ids = {},
}
seen_ids_by_balance_key[balance_key] = {}
end
if seen_ids_by_balance_key[balance_key][customer_entitlement_id] then
return true
end
seen_ids_by_balance_key[balance_key][customer_entitlement_id] = true
table.insert(
entries_by_balance_key[balance_key].customer_entitlement_ids,
customer_entitlement_id
)
return true
end
local deduction_ids = {}
for _, ent_obj in ipairs(params.customer_entitlement_deductions or {}) do
local customer_entitlement_id = ent_obj.customer_entitlement_id
local feature_id = ent_obj.feature_id
if customer_entitlement_id and feature_id then
local balance_key = balance_keys_by_feature_id[feature_id]
if not balance_key then
if customer_entitlement_id then
local queued = queue_balance_read(customer_entitlement_id, ent_obj.feature_id)
if not queued then
table.insert(missing_customer_entitlement_ids, customer_entitlement_id)
else
if entries_by_balance_key[balance_key] == nil then
entries_by_balance_key[balance_key] = {
feature_id = feature_id,
customer_entitlement_ids = {},
}
end
table.insert(
entries_by_balance_key[balance_key].customer_entitlement_ids,
customer_entitlement_id
)
end
deduction_ids[customer_entitlement_id] = true
end
end
-- A deduction target must never be marked anchor_only, or a cache miss on it is
-- swallowed instead of surfacing as missing + triggering the Postgres fallback.
for _, anchor in ipairs(params.anchor_entitlements or {}) do
local customer_entitlement_id = anchor.customer_entitlement_id
if customer_entitlement_id and not deduction_ids[customer_entitlement_id] then
anchor_only_ids[customer_entitlement_id] = true
queue_balance_read(customer_entitlement_id, anchor.feature_id)
end
end
@@ -57,16 +89,19 @@ local function read_subject_balances(params)
local subject_balance = decode_subject_balance(raw_value)
if subject_balance == nil then
table.insert(
missing_customer_entitlement_ids,
customer_entitlement_id
)
if not anchor_only_ids[customer_entitlement_id] then
table.insert(
missing_customer_entitlement_ids,
customer_entitlement_id
)
end
else
balances_by_id[customer_entitlement_id] = {
balance_key = balance_key,
customer_entitlement_id = customer_entitlement_id,
feature_id = entry.feature_id,
subject_balance = subject_balance,
anchor_only = anchor_only_ids[customer_entitlement_id] or nil,
}
end
end