diff --git a/server/run.sh b/server/run.sh index aefdee53e..e12b460ec 100755 --- a/server/run.sh +++ b/server/run.sh @@ -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" diff --git a/server/src/_luaScripts/deductionLuaScripts/batchDeduction.lua b/server/src/_luaScripts/deductionLuaScripts/batchDeduction.lua index b2d9853e6..ac80a8383 100644 --- a/server/src/_luaScripts/deductionLuaScripts/batchDeduction.lua +++ b/server/src/_luaScripts/deductionLuaScripts/batchDeduction.lua @@ -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 diff --git a/server/src/internal/api/check/checkUtils/apiBalanceToAllowed.ts b/server/src/internal/api/check/checkUtils/apiBalanceToAllowed.ts index ef4bd1f34..77b1a4bb4 100644 --- a/server/src/internal/api/check/checkUtils/apiBalanceToAllowed.ts +++ b/server/src/internal/api/check/checkUtils/apiBalanceToAllowed.ts @@ -47,6 +47,7 @@ export const apiBalanceToAllowed = ({ } // 4. Balance >= required balance + if (new Decimal(apiBalance.current_balance).gte(requiredBalance)) { return true; } diff --git a/server/src/internal/balances/track/trackUtils/executePostgresTracking.ts b/server/src/internal/balances/track/trackUtils/executePostgresTracking.ts index 3b4c9e3be..e17fd1808 100644 --- a/server/src/internal/balances/track/trackUtils/executePostgresTracking.ts +++ b/server/src/internal/balances/track/trackUtils/executePostgresTracking.ts @@ -52,6 +52,7 @@ export const executePostgresTracking = async ({ }, refreshCache: true, fullCus, + skipAdditionalBalance: true, }); if (updatedFullCus) { diff --git a/server/src/internal/balances/track/trackUtils/runDeductionTx.ts b/server/src/internal/balances/track/trackUtils/runDeductionTx.ts index ab93c1c6b..bbba96cf2 100644 --- a/server/src/internal/balances/track/trackUtils/runDeductionTx.ts +++ b/server/src/internal/balances/track/trackUtils/runDeductionTx.ts @@ -60,7 +60,7 @@ export const deductFromCusEnts = async ({ deductions, overageBehaviour = "cap", addToAdjustment = false, - skipAdditionalBalance = false, + skipAdditionalBalance = true, alterGrantedBalance = false, fullCus, sortParams, diff --git a/server/tests/_temp/temp1.test.ts b/server/tests/_temp/temp1.test.ts index 242e9fce8..bc1c61f29 100644 --- a/server/tests/_temp/temp1.test.ts +++ b/server/tests/_temp/temp1.test.ts @@ -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); - }); });