fix: deducting from purchased balance when value < 0
This commit is contained in:
@@ -17,6 +17,9 @@ elif [[ "$filename" == *"/tests/"* ]]; then
|
||||
|
||||
elif [[ "$filename" == *".sh"* ]]; then
|
||||
"$filename"
|
||||
elif [[ "$filename" == *"/scripts/"* ]]; then
|
||||
# Run scripts with infisical prod environment
|
||||
infisical run --env=prod -- bun "$filename"
|
||||
else
|
||||
# NODE_ENV=development npx tsx $filename
|
||||
NODE_ENV=development bun "$filename"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -47,6 +47,7 @@ export const apiBalanceToAllowed = ({
|
||||
}
|
||||
|
||||
// 4. Balance >= required balance
|
||||
|
||||
if (new Decimal(apiBalance.current_balance).gte(requiredBalance)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export const executePostgresTracking = async ({
|
||||
},
|
||||
refreshCache: true,
|
||||
fullCus,
|
||||
skipAdditionalBalance: true,
|
||||
});
|
||||
|
||||
if (updatedFullCus) {
|
||||
|
||||
@@ -60,7 +60,7 @@ export const deductFromCusEnts = async ({
|
||||
deductions,
|
||||
overageBehaviour = "cap",
|
||||
addToAdjustment = false,
|
||||
skipAdditionalBalance = false,
|
||||
skipAdditionalBalance = true,
|
||||
alterGrantedBalance = false,
|
||||
fullCus,
|
||||
sortParams,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { beforeAll, describe, test } from "bun:test";
|
||||
import { BillingInterval, LegacyVersion } from "@autumn/shared";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
} from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "../../src/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "../../src/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
import { expectProductAttached } from "../utils/expectUtils/expectProductAttached.js";
|
||||
import { replaceItems } from "../utils/testProductUtils/testProductUtils.js";
|
||||
|
||||
// UNCOMMENT FROM HERE
|
||||
@@ -23,10 +22,14 @@ const pro = constructProduct({
|
||||
type: "pro",
|
||||
|
||||
items: [
|
||||
constructArrearItem({
|
||||
featureId: TestFeature.Words,
|
||||
includedUsage: 0,
|
||||
price: 0.2,
|
||||
// constructArrearItem({
|
||||
// featureId: TestFeature.Words,
|
||||
// includedUsage: 0,
|
||||
// price: 0.2,
|
||||
// }),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Users,
|
||||
includedUsage: 5,
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -72,11 +75,12 @@ describe(`${chalk.yellowBright("temp: Testing add ons")}`, () => {
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: basic.id,
|
||||
});
|
||||
// await autumn.attach({
|
||||
// customer_id: customerId,
|
||||
// product_id: basic.id,
|
||||
// });
|
||||
});
|
||||
return;
|
||||
|
||||
test("should attach pro product", async () => {
|
||||
// newPro = structuredClone(pro);
|
||||
@@ -102,29 +106,4 @@ describe(`${chalk.yellowBright("temp: Testing add ons")}`, () => {
|
||||
items: newItems,
|
||||
});
|
||||
});
|
||||
|
||||
test("should checkout and have correct scenario", async () => {
|
||||
const res = await autumn.checkout({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
expect(res.current_product?.scenario).toBe("renew");
|
||||
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
expectProductAttached({
|
||||
customer,
|
||||
product: pro,
|
||||
});
|
||||
|
||||
const products = customer.products;
|
||||
expect(products.length).toBe(1);
|
||||
expect(products[0].id).toBe(pro.id);
|
||||
expect(products[0].version).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user