diff --git a/scripts/testGroups/g1.sh b/scripts/testGroups/g1.sh index bc82a2f42..d09a71e23 100755 --- a/scripts/testGroups/g1.sh +++ b/scripts/testGroups/g1.sh @@ -14,23 +14,26 @@ fi # Run tests using TypeScript runner with compact mode # Adjust --max to control concurrency (default: 6) -# BUN_PARALLEL_COMPACT \ -# 'server/tests/balances/track/concurrency' \ -# 'server/tests/balances/track/basic' \ -# 'server/tests/balances/track/credit-systems' \ -# 'server/tests/balances/track/legacy' \ -# 'server/tests/balances/check/basic' \ -# 'server/tests/balances/check/credit-systems' \ -# 'server/tests/balances/check/misc' \ - BUN_PARALLEL_COMPACT \ - 'server/tests/attach/basic' \ - 'server/tests/attach/entities' \ - 'server/tests/attach/upgrade' \ - 'server/tests/attach/downgrade' \ - 'server/tests/attach/free' \ - 'server/tests/attach/addOn' \ - 'server/tests/attach/entities' \ - 'server/tests/attach/checkout' \ - 'server/tests/attach/misc' \ - --max=6 \ \ No newline at end of file + 'server/tests/balances/track/basic' \ + 'server/tests/balances/track/concurrency' \ + 'server/tests/balances/track/allocated' \ + 'server/tests/balances/track/credit-systems' \ + 'server/tests/balances/track/entity-balances' \ + 'server/tests/balances/track/entity-products' \ + 'server/tests/balances/track/legacy' \ + 'server/tests/balances/check/basic' \ + 'server/tests/balances/check/credit-systems' \ + 'server/tests/balances/check/misc' \ + +# BUN_PARALLEL_COMPACT \ +# 'server/tests/attach/basic' \ +# 'server/tests/attach/entities' \ +# 'server/tests/attach/upgrade' \ +# 'server/tests/attach/downgrade' \ +# 'server/tests/attach/free' \ +# 'server/tests/attach/addOn' \ +# 'server/tests/attach/entities' \ +# 'server/tests/attach/checkout' \ +# 'server/tests/attach/misc' \ +# --max=6 \ \ No newline at end of file diff --git a/scripts/testGroups/g2.sh b/scripts/testGroups/g2.sh index ab1fdccf3..dbe36f7f7 100755 --- a/scripts/testGroups/g2.sh +++ b/scripts/testGroups/g2.sh @@ -12,15 +12,17 @@ if [[ "$1" == *"setup"* ]]; then BUN_SETUP fi -BUN_PARALLEL_COMPACT \ - 'server/tests/attach/migrations' \ - 'server/tests/attach/others' \ - # 'server/tests/attach/newVersion' \ - # 'server/tests/attach/upgradeOld' \ - # 'server/tests/attach/updateEnts' \ - # 'server/tests/advanced/check' \ - # 'server/tests/attach/prepaid' \ - # 'server/tests/interval/upgrade' \ - # 'server/tests/interval/multiSub' \ - # --max=6 + + +# BUN_PARALLEL_COMPACT \ +# 'server/tests/attach/migrations' \ +# 'server/tests/attach/others' \ +# 'server/tests/attach/newVersion' \ +# 'server/tests/attach/upgradeOld' \ +# 'server/tests/attach/updateEnts' \ +# 'server/tests/advanced/check' \ +# 'server/tests/attach/prepaid' \ +# 'server/tests/interval/upgrade' \ +# 'server/tests/interval/multiSub' \ +# --max=6 diff --git a/scripts/testGroups/g3.sh b/scripts/testGroups/g3.sh index 9983bb7f4..3bfcda66f 100755 --- a/scripts/testGroups/g3.sh +++ b/scripts/testGroups/g3.sh @@ -13,9 +13,9 @@ if [[ "$1" == *"setup"* ]]; then fi BUN_PARALLEL_COMPACT \ - 'server/tests/contUse/entities' \ - 'server/tests/contUse/update' \ 'server/tests/contUse/track' \ 'server/tests/contUse/roles' \ + 'server/tests/contUse/update' \ + 'server/tests/contUse/entities' \ --max=6 diff --git a/scripts/testGroups/g4.sh b/scripts/testGroups/g4.sh index 31f2a3858..feee418dd 100755 --- a/scripts/testGroups/g4.sh +++ b/scripts/testGroups/g4.sh @@ -13,17 +13,21 @@ if [[ "$1" == *"setup"* ]]; then fi BUN_PARALLEL_COMPACT \ - 'server/tests/merged/group' \ - 'server/tests/merged/add' \ - 'server/tests/merged/downgrade' \ - 'server/tests/merged/prepaid' \ 'server/tests/merged/separate' \ + 'server/tests/merged/downgrade' \ + 'server/tests/merged/add' \ + 'server/tests/merged/group' \ + 'server/tests/merged/prepaid' \ 'server/tests/merged/upgrade' \ - 'server/tests/merged/trial' \ 'server/tests/merged/addOn' \ + 'server/tests/merged/trial' \ 'server/tests/core/cancel' \ - 'server/tests/core/multiAttach' \ - 'server/tests/core/multiAttach/multiInvoice' \ - 'server/tests/core/multiAttach/multiUpgrade' \ --max=6 + + +# deprecated tests(?) +# 'server/tests/core/multiAttach' \ +# 'server/tests/core/multiAttach/multiInvoice' \ +# 'server/tests/core/multiAttach/multiUpgrade' \ +# 'sever/tests/core/multiAttach/multiReward' diff --git a/scripts/testGroups/g5.sh b/scripts/testGroups/g5.sh index 1bb0caa54..09da2ed1a 100755 --- a/scripts/testGroups/g5.sh +++ b/scripts/testGroups/g5.sh @@ -15,12 +15,23 @@ fi # Note: advanced/multiFeature, advanced/rollovers, advanced/customInterval, # advanced/usageLimit still use Mocha (not migrated yet) + + + BUN_PARALLEL_COMPACT \ 'server/tests/advanced/coupons' \ + 'server/tests/advanced/misc' \ 'server/tests/attach/updateQuantity' \ - 'server/tests/advanced/referrals' \ - 'server/tests/advanced/referrals/paid' \ 'server/tests/attach/multiProduct' \ - 'server/tests/advanced/usage' \ + 'server/tests/advanced/multiFeature' \ + 'server/tests/advanced/referrals' \ + 'server/tests/advanced/rollovers' \ + 'server/tests/advanced/customInterval' \ + 'server/tests/advanced/usageLimit' \ --max=6 + +# BUN_PARALLEL_COMPACT \ +# 'server/tests/advanced/usage' + +# 'server/tests/advanced/referrals/paid' \ \ No newline at end of file diff --git a/server/experiments/redis.ts b/server/experiments/redis.ts index f3db1a266..a0c7a2598 100644 --- a/server/experiments/redis.ts +++ b/server/experiments/redis.ts @@ -1,8 +1,8 @@ import { AppEnv } from "@autumn/shared"; import { globalBatchingManager } from "../src/internal/balances/track/redisTrackUtils/BatchingManager.js"; import { - buildCachedApiCustomerKey, - getCachedApiCustomer, + buildCachedApiCustomerKey, + getCachedApiCustomer, } from "../src/internal/customers/cusUtils/apiCusCacheUtils/getCachedApiCustomer.js"; import { initDrizzle } from "../src/db/initDrizzle.js"; import { initScript } from "../src/utils/scriptUtils/scriptUtils.js"; @@ -12,7 +12,7 @@ const DEDUCTION_COUNT = 100_000; const DEDUCTION_AMOUNT = 1; const logCredits = (label: string, customer: Awaited>) => { - const credits = customer?.features?.credits; + const credits = customer?.apiCustomer?.features?.credits; console.log(`\n${label}`); console.log(` Total Balance: ${credits?.balance ?? "N/A"}`); console.log(` Monthly Credits: ${credits?.breakdown?.[0]?.balance ?? "N/A"}`); diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/checkCacheExists.lua b/server/src/_luaScripts/cusLuaScripts/checkCacheExists.lua similarity index 100% rename from server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/checkCacheExists.lua rename to server/src/_luaScripts/cusLuaScripts/checkCacheExists.lua diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/deleteCustomer.lua b/server/src/_luaScripts/cusLuaScripts/deleteCustomer.lua similarity index 100% rename from server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/deleteCustomer.lua rename to server/src/_luaScripts/cusLuaScripts/deleteCustomer.lua diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/getCustomer.backup.lua b/server/src/_luaScripts/cusLuaScripts/getCustomer.backup.lua similarity index 100% rename from server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/getCustomer.backup.lua rename to server/src/_luaScripts/cusLuaScripts/getCustomer.backup.lua diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/getCustomer.lua b/server/src/_luaScripts/cusLuaScripts/getCustomer.lua similarity index 88% rename from server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/getCustomer.lua rename to server/src/_luaScripts/cusLuaScripts/getCustomer.lua index 1cc900332..4c0599f6f 100644 --- a/server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/getCustomer.lua +++ b/server/src/_luaScripts/cusLuaScripts/getCustomer.lua @@ -1,10 +1,11 @@ -- getCustomer.lua -- Atomically retrieves a customer object from Redis, reconstructing from base JSON and feature HSETs --- Merges master customer features with entity features +-- Merges master customer features with entity features (unless skipEntityMerge is true) -- KEYS[1]: cache key (e.g., "org_id:env:customer:customer_id") -- ARGV[1]: org_id (for building entity cache keys) -- ARGV[2]: env (for building entity cache keys) -- ARGV[3]: customer_id (for building entity cache keys) +-- ARGV[4]: skipEntityMerge (optional, "true" to skip merging with entities) -- Helper function to merge products array by product ID and normalized status -- Groups products by key (product_id:normalized_status) and merges quantities @@ -96,9 +97,20 @@ local cacheKey = KEYS[1] local orgId = ARGV[1] local env = ARGV[2] local customerId = ARGV[3] +local skipEntityMerge = ARGV[4] == "true" + +-- Load features based on merge mode +-- If skipEntityMerge is true, only load customer's own features (no entity merging) +-- If skipEntityMerge is false, load merged features (customer + entities) +local features +if skipEntityMerge then + -- Load only customer's own features without entity merging + features = loadCusFeatures(cacheKey, orgId, env, customerId, "__CUSTOMER_ONLY__") +else + -- Load merged features (customer + entities) + features = loadCusFeatures(cacheKey, orgId, env, customerId) +end --- Use loadCusFeatures to get merged features (customer + entities) -local features = loadCusFeatures(cacheKey, orgId, env, customerId) if not features then return nil -- Customer not in cache or partial eviction detected end diff --git a/server/src/_luaScripts/cusLuaScripts/loadCusFeatures.lua b/server/src/_luaScripts/cusLuaScripts/loadCusFeatures.lua new file mode 100644 index 000000000..cad0f4d0f --- /dev/null +++ b/server/src/_luaScripts/cusLuaScripts/loadCusFeatures.lua @@ -0,0 +1,505 @@ +-- loadCusFeatures.lua +-- Shared function to load customer features with merged balances (customer + entities) +-- Returns: { [featureId] = { balance, usage, unlimited, ... } } or nil if not in cache + +-- Helper function to safely convert values to numbers for arithmetic +local function toNum(value) + return type(value) == "number" and value or 0 +end + +-- Helper function to parse HGETALL result into feature data object +local function parseFeatureHash(featureHash) + local featureData = {} + for i = 1, #featureHash, 2 do + local key = featureHash[i] + local value = featureHash[i + 1] + + -- Check for null first before parsing + if value == "null" then + featureData[key] = cjson.null + elseif key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" or key == "_breakdown_count" or key == "_rollover_count" then + featureData[key] = tonumber(value) + elseif key == "unlimited" or key == "overage_allowed" then + featureData[key] = (value == "true") + elseif key == "credit_schema" then + -- Parse credit_schema JSON array + if value ~= "" then + featureData[key] = cjson.decode(value) + else + featureData[key] = cjson.null + end + else + featureData[key] = value + end + end + return featureData +end + + +-- Helper function to fetch and parse rollover items +-- Returns: array of rollover data objects, or nil if any key is missing (partial eviction) +local function fetchRollovers(baseKey, rolloverCount) + local rollovers = {} + for i = 0, rolloverCount - 1 do + local rolloverKey = baseKey .. ":rollover:" .. i + local rolloverHash = redis.call("HGETALL", rolloverKey) + + -- If rollover key is missing, return nil (partial eviction detected) + if #rolloverHash == 0 then + return nil + end + + local rolloverData = {} + for j = 1, #rolloverHash, 2 do + local key = rolloverHash[j] + local value = rolloverHash[j + 1] + + if value == "null" then + rolloverData[key] = cjson.null + elseif key == "balance" or key == "expires_at" then + rolloverData[key] = tonumber(value) + else + rolloverData[key] = value + end + end + table.insert(rollovers, rolloverData) + end + return rollovers +end + +-- Helper function to fetch and parse breakdown items +-- Returns: array of breakdown data objects, or nil if any key is missing (partial eviction) +local function fetchBreakdown(baseKey, breakdownCount) + local breakdown = {} + for i = 0, breakdownCount - 1 do + local breakdownKey = baseKey .. ":breakdown:" .. i + local breakdownHash = redis.call("HGETALL", breakdownKey) + + -- If breakdown key is missing, return nil (partial eviction detected) + if #breakdownHash == 0 then + return nil + end + + local breakdownData = {} + for j = 1, #breakdownHash, 2 do + local key = breakdownHash[j] + local value = breakdownHash[j + 1] + + if value == "null" then + breakdownData[key] = cjson.null + elseif key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" then + breakdownData[key] = tonumber(value) + elseif key == "overage_allowed" then + breakdownData[key] = (value == "true") + else + breakdownData[key] = value + end + end + table.insert(breakdown, breakdownData) + end + return breakdown +end + +-- Helper function to merge source feature balances into target feature +-- Mutates targetFeature by adding sourceFeature's balances, usage, breakdowns, and rollovers +-- Also handles minimum next_reset_at (earliest reset time) +local function mergeFeatureBalances(targetFeature, sourceFeature) + if not sourceFeature then return end + + -- Merge top-level balance and usage + targetFeature.balance = toNum(targetFeature.balance) + toNum(sourceFeature.balance) + targetFeature.usage = toNum(targetFeature.usage) + toNum(sourceFeature.usage) + targetFeature.included_usage = toNum(targetFeature.included_usage) + toNum(sourceFeature.included_usage) + targetFeature.usage_limit = toNum(targetFeature.usage_limit) + toNum(sourceFeature.usage_limit) + + -- Use minimum next_reset_at (earliest reset time) + if type(sourceFeature.next_reset_at) == "number" then + if type(targetFeature.next_reset_at) == "number" then + if sourceFeature.next_reset_at < targetFeature.next_reset_at then + targetFeature.next_reset_at = sourceFeature.next_reset_at + end + else + targetFeature.next_reset_at = sourceFeature.next_reset_at + end + end + + -- Merge breakdown balances and usage + if targetFeature.breakdown and sourceFeature.breakdowns then + for i, targetBreakdown in ipairs(targetFeature.breakdown) do + local sourceBreakdown = sourceFeature.breakdowns[i] + if sourceBreakdown then + targetBreakdown.balance = toNum(targetBreakdown.balance) + toNum(sourceBreakdown.balance) + targetBreakdown.usage = toNum(targetBreakdown.usage) + toNum(sourceBreakdown.usage) + targetBreakdown.included_usage = toNum(targetBreakdown.included_usage) + toNum(sourceBreakdown.included_usage) + targetBreakdown.usage_limit = toNum(targetBreakdown.usage_limit) + toNum(sourceBreakdown.usage_limit) + + -- Use minimum next_reset_at for breakdown + if type(sourceBreakdown.next_reset_at) == "number" then + if type(targetBreakdown.next_reset_at) == "number" then + if sourceBreakdown.next_reset_at < targetBreakdown.next_reset_at then + targetBreakdown.next_reset_at = sourceBreakdown.next_reset_at + end + else + targetBreakdown.next_reset_at = sourceBreakdown.next_reset_at + end + end + end + end + end + + -- Merge rollover balances + if targetFeature.rollovers and sourceFeature.rollovers then + for i, targetRollover in ipairs(targetFeature.rollovers) do + local sourceRollover = sourceFeature.rollovers[i] + if sourceRollover then + targetRollover.balance = toNum(targetRollover.balance) + toNum(sourceRollover.balance) + end + end + end +end + +-- Load entity-level features (entity + customer merged) +-- Used for entity-level sync mode +-- Parameters: cacheKey (customer cache key), orgId, env, customerId, entityId +-- Returns: merged features table (entity + customer) or nil +local function loadEntityLevelFeatures(cacheKey, orgId, env, customerId, entityId) + -- Build entity cache key + local entityCacheKey = "{" .. orgId .. "}:" .. env .. ":customer:" .. customerId .. ":entity:" .. entityId + + -- Get entity base JSON + local entityBaseJson = redis.call("GET", entityCacheKey) + if not entityBaseJson then + return nil + end + + local entityBase = cjson.decode(entityBaseJson) + local entityFeatureIds = entityBase._featureIds or {} + + -- Load entity features + local entityFeatures = {} + for _, featureId in ipairs(entityFeatureIds) do + local featureKey = entityCacheKey .. ":features:" .. featureId + local featureHash = redis.call("HGETALL", featureKey) + + -- If feature key is missing, return nil (partial eviction detected) + if #featureHash == 0 then + return nil + end + + -- Parse feature hash using helper function + local featureData = parseFeatureHash(featureHash) + + -- Fetch rollovers using helper function + local rolloverCount = featureData._rollover_count or 0 + featureData._rollover_count = nil + + local rollovers = fetchRollovers(featureKey, rolloverCount) + if rollovers == nil then + return nil -- Partial eviction detected + end + + if #rollovers > 0 then + featureData.rollovers = rollovers + end + + -- Fetch breakdown using helper function + local breakdownCount = featureData._breakdown_count or 0 + featureData._breakdown_count = nil + + local breakdown = fetchBreakdown(featureKey, breakdownCount) + if breakdown == nil then + return nil -- Partial eviction detected + end + + if #breakdown > 0 then + featureData.breakdown = breakdown + end + + entityFeatures[featureId] = featureData + end + + -- Load customer features (raw, no entity aggregation) + local customerCacheKey = cacheKey + local customerBaseJson = redis.call("GET", customerCacheKey) + + local customerFeatures = {} + if customerBaseJson then + local customerBase = cjson.decode(customerBaseJson) + local customerFeatureIds = customerBase._featureIds or {} + + for _, featureId in ipairs(customerFeatureIds) do + local featureKey = customerCacheKey .. ":features:" .. featureId + local featureHash = redis.call("HGETALL", featureKey) + + if #featureHash > 0 then + -- Parse feature hash using helper function + local featureData = parseFeatureHash(featureHash) + + -- Fetch rollovers + local rolloverCount = featureData._rollover_count or 0 + featureData._rollover_count = nil + local rollovers = fetchRollovers(featureKey, rolloverCount) or {} + if #rollovers > 0 then + featureData.rollovers = rollovers + end + + -- Fetch breakdown + local breakdownCount = featureData._breakdown_count or 0 + featureData._breakdown_count = nil + local breakdown = fetchBreakdown(featureKey, breakdownCount) or {} + if #breakdown > 0 then + featureData.breakdown = breakdown + end + + customerFeatures[featureId] = featureData + end + end + end + + -- Merge customer and entity features (entity + customer) + local mergedFeatures = {} + + -- First, add all customer features (inherited) + for featureId, customerFeature in pairs(customerFeatures) do + mergedFeatures[featureId] = customerFeature + end + + -- Then, merge or add entity features + for featureId, entityFeature in pairs(entityFeatures) do + local customerFeature = customerFeatures[featureId] + + if customerFeature then + -- Both customer and entity have this feature - merge balances + if not entityFeature.unlimited and not customerFeature.unlimited then + mergeFeatureBalances(entityFeature, customerFeature) + end + mergedFeatures[featureId] = entityFeature + else + -- Only entity has this feature - use entity's feature + mergedFeatures[featureId] = entityFeature + end + end + + return mergedFeatures +end + +-- Load customer features with merged entity balances +-- Parameters: cacheKey, orgId, env, customerId, entityId (optional) +-- If entityId is "__CUSTOMER_ONLY__": returns ONLY customer features (no merging) +-- If entityId is provided (string): returns entity-level merged features (entity + customer) +-- If entityId is nil: returns customer-level merged features (customer + all entities) +-- Returns: merged features table or nil +local function loadCusFeatures(cacheKey, orgId, env, customerId, entityId) + -- Special case: Customer-only mode (no entity merging) + if entityId == "__CUSTOMER_ONLY__" then + local baseJson = redis.call("GET", cacheKey) + if not baseJson then + return nil + end + + local base = cjson.decode(baseJson) + local featureIds = base._featureIds or {} + + -- Load only customer's own features without entity merging + local customerFeatures = {} + for _, featureId in ipairs(featureIds) do + local featureKey = cacheKey .. ":features:" .. featureId + local featureHash = redis.call("HGETALL", featureKey) + + if #featureHash == 0 then + return nil -- Partial eviction detected + end + + -- Parse feature hash + local featureData = parseFeatureHash(featureHash) + featureData.id = featureId + + -- Fetch rollovers + local rollovers = fetchRollovers(featureKey, featureData._rollover_count or 0) + if rollovers == nil then + return nil -- Partial eviction + end + if #rollovers > 0 then + featureData.rollovers = rollovers + end + + -- Fetch breakdown + local breakdown = fetchBreakdown(featureKey, featureData._breakdown_count or 0) + if breakdown == nil then + return nil -- Partial eviction + end + if #breakdown > 0 then + featureData.breakdown = breakdown + end + + -- Remove metadata fields + featureData._breakdown_count = nil + featureData._rollover_count = nil + + customerFeatures[featureId] = featureData + end + + return customerFeatures + end + + -- If entityId is provided, load entity-level features (entity + customer merged) + if entityId then + return loadEntityLevelFeatures(cacheKey, orgId, env, customerId, entityId) + end + + -- Otherwise, load customer-level features (customer + all entities merged) + -- Get base customer JSON + local baseJson = redis.call("GET", cacheKey) + if not baseJson then + return nil + end + + local baseCustomer = cjson.decode(baseJson) + local featureIds = baseCustomer._featureIds or {} + local entityIds = baseCustomer._entityIds or {} + + -- Build features object + local features = {} + + for _, featureId in ipairs(featureIds) do + local featureKey = cacheKey .. ":features:" .. featureId + local featureHash = redis.call("HGETALL", featureKey) + + -- If feature key is missing, return nil (partial eviction detected) + if #featureHash == 0 then + return nil + end + + -- Parse feature hash using helper function + local featureData = parseFeatureHash(featureHash) + + -- Fetch rollovers using helper function + local rolloverCount = featureData._rollover_count or 0 + featureData._rollover_count = nil -- Remove from final output + + local rollovers = fetchRollovers(featureKey, rolloverCount) + if rollovers == nil then + return nil -- Partial eviction detected + end + + if #rollovers > 0 then + featureData.rollovers = rollovers + end + + -- Fetch breakdown using helper function + local breakdownCount = featureData._breakdown_count or 0 + featureData._breakdown_count = nil -- Remove from final output + + local breakdown = fetchBreakdown(featureKey, breakdownCount) + if breakdown == nil then + return nil -- Partial eviction detected + end + + if #breakdown > 0 then + featureData.breakdown = breakdown + end + + features[featureId] = featureData + end + + -- ============================================================================ + -- FETCH AND MERGE ENTITY FEATURES + -- ============================================================================ + + -- Fetch all entity features and aggregate balances + local entityFeatureData = {} -- {[entityId][featureId] = featureData} + local entityBaseData = {} -- {[entityId] = entityBase} - Store entity base for product access + + for _, entityId in ipairs(entityIds) do + local entityCacheKey = "{" .. orgId .. "}:" .. env .. ":customer:" .. customerId .. ":entity:" .. entityId + local entityBaseJson = redis.call("GET", entityCacheKey) + + if entityBaseJson then + local entityBase = cjson.decode(entityBaseJson) + entityBaseData[entityId] = entityBase -- Store entity base for product access + local entityFeatureIds = entityBase._featureIds or {} + entityFeatureData[entityId] = {} + + for _, featureId in ipairs(entityFeatureIds) do + local entityFeatureKey = entityCacheKey .. ":features:" .. featureId + local entityFeatureHash = redis.call("HGETALL", entityFeatureKey) + + if #entityFeatureHash > 0 then + -- Parse entity feature using helper function + local entityFeature = parseFeatureHash(entityFeatureHash) + + -- Fetch breakdown items for this entity feature using helper function + local breakdownCount = entityFeature._breakdown_count or 0 + entityFeature._breakdown_count = nil + entityFeature.breakdowns = fetchBreakdown(entityFeatureKey, breakdownCount) or {} + + -- Fetch rollover items for this entity feature using helper function + local rolloverCount = entityFeature._rollover_count or 0 + entityFeature._rollover_count = nil + entityFeature.rollovers = fetchRollovers(entityFeatureKey, rolloverCount) or {} + + entityFeatureData[entityId][featureId] = entityFeature + end + end + end + end + + + + -- ============================================================================ + -- MERGE ENTITY BALANCES INTO CUSTOMER FEATURES + -- ============================================================================ + + for featureId, customerFeature in pairs(features) do + -- Skip if unlimited + if not customerFeature.unlimited then + -- Merge each entity's feature balances into customer feature + for entityId, entityFeatures in pairs(entityFeatureData) do + local entityFeature = entityFeatures[featureId] + if entityFeature then + mergeFeatureBalances(customerFeature, entityFeature) + end + end + end + end + + -- Add entity-only features (features that exist in entities but not in customer) + for entityId, entityFeatures in pairs(entityFeatureData) do + for featureId, entityFeature in pairs(entityFeatures) do + if not features[featureId] then + -- This feature doesn't exist in customer, add it with zero values + features[featureId] = { + id = entityFeature.id, + type = entityFeature.type, + name = entityFeature.name, + interval = entityFeature.interval, + interval_count = entityFeature.interval_count, + unlimited = entityFeature.unlimited, + balance = 0, + usage = 0, + included_usage = 0, + next_reset_at = cjson.null, + overage_allowed = entityFeature.overage_allowed, + usage_limit = 0, + credit_schema = entityFeature.credit_schema + } + end + end + end + + -- Aggregate balances for entity-only features using mergeFeatureBalances + for featureId, customerFeature in pairs(features) do + -- Only process if this was an entity-only feature (balance is still 0 from initialization) + if customerFeature.balance == 0 and customerFeature.usage == 0 then + for entityId, entityFeatures in pairs(entityFeatureData) do + local entityFeature = entityFeatures[featureId] + if entityFeature then + mergeFeatureBalances(customerFeature, entityFeature) + end + end + end + end + +-- Return merged features +return features +end \ No newline at end of file diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/setCustomer.lua b/server/src/_luaScripts/cusLuaScripts/setCustomer.lua similarity index 100% rename from server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/setCustomer.lua rename to server/src/_luaScripts/cusLuaScripts/setCustomer.lua diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/setCustomerDetails.lua b/server/src/_luaScripts/cusLuaScripts/setCustomerDetails.lua similarity index 100% rename from server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/setCustomerDetails.lua rename to server/src/_luaScripts/cusLuaScripts/setCustomerDetails.lua diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/setCustomerProducts.lua b/server/src/_luaScripts/cusLuaScripts/setCustomerProducts.lua similarity index 100% rename from server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/setCustomerProducts.lua rename to server/src/_luaScripts/cusLuaScripts/setCustomerProducts.lua diff --git a/server/src/internal/balances/track/redisTrackUtils/backupBatchDeduction.lua b/server/src/_luaScripts/deductionLuaScripts/backupBatchDeduction.lua similarity index 100% rename from server/src/internal/balances/track/redisTrackUtils/backupBatchDeduction.lua rename to server/src/_luaScripts/deductionLuaScripts/backupBatchDeduction.lua diff --git a/server/src/internal/balances/track/redisTrackUtils/batchDeduction.lua b/server/src/_luaScripts/deductionLuaScripts/batchDeduction.lua similarity index 91% rename from server/src/internal/balances/track/redisTrackUtils/batchDeduction.lua rename to server/src/_luaScripts/deductionLuaScripts/batchDeduction.lua index 4e22a7c9d..7d0d95adb 100644 --- a/server/src/internal/balances/track/redisTrackUtils/batchDeduction.lua +++ b/server/src/_luaScripts/deductionLuaScripts/batchDeduction.lua @@ -66,6 +66,12 @@ end -- Global delta accumulator: { [redisKey][field] = delta } local keyDeltas = {} +-- Track which entities were modified (set: { [entityId] = true }) +local changedEntityIds = {} + +-- Track if customer (base customer, not entity) was modified +local customerChanged = false + -- ============================================================================ -- HELPER FUNCTIONS -- ============================================================================ @@ -284,16 +290,33 @@ local function deductFromMainBalance(cusFeature, amount) local allowOverage = breakdown.overage_allowed or cusFeature.type == "continuous_use" if allowOverage then - local currentUsage = breakdown.usage or 0 + -- Get current balance AFTER deducting from breakdown balance + local currentBalance = breakdown.balance or 0 + -- Apply state changes to get updated balance + for _, change in ipairs(stateChanges) do + if change.type == "breakdown" and change.index == index and change.field == "balance" then + if change.newValue then + currentBalance = change.newValue + elseif change.delta then + currentBalance = currentBalance + change.delta + end + end + end local toDeduct = remaining -- If usage_limit is defined, cap the overage if breakdown.usage_limit then - local availableOverage = breakdown.usage_limit - currentUsage - if availableOverage > 0 then - toDeduct = math.min(remaining, availableOverage) - else - toDeduct = 0 + local breakdownIncludedUsage = breakdown.included_usage or cusFeature.included_usage or 0 + local minNegativeBalance = breakdownIncludedUsage - breakdown.usage_limit + + -- If min_negative_balance is 0 or positive, skip limit check + if minNegativeBalance < 0 then + local availableOverage = currentBalance - minNegativeBalance + if availableOverage > 0 then + toDeduct = math.min(remaining, availableOverage) + else + toDeduct = 0 + end end end @@ -364,16 +387,33 @@ local function deductFromMainBalance(cusFeature, amount) local allowOverage = cusFeature.overage_allowed or cusFeature.type == "continuous_use" if remaining > 0 and allowOverage then - local currentUsage = cusFeature.usage or 0 + -- Get current balance AFTER deducting from main balance + local currentBalance = cusFeature.balance or 0 + -- Apply state changes to get updated balance + for _, change in ipairs(stateChanges) do + if change.type == "cusFeature" and change.field == "balance" then + if change.newValue then + currentBalance = change.newValue + elseif change.delta then + currentBalance = currentBalance + change.delta + end + end + end local toDeduct = remaining -- If usage_limit is defined, cap the overage if cusFeature.usage_limit then - local availableOverage = cusFeature.usage_limit - currentUsage - if availableOverage > 0 then - toDeduct = math.min(remaining, availableOverage) - else - toDeduct = 0 + local includedUsage = cusFeature.included_usage or 0 + local minNegativeBalance = includedUsage - cusFeature.usage_limit + + -- If min_negative_balance is 0 or positive, skip limit check + if minNegativeBalance < 0 then + local availableOverage = currentBalance - minNegativeBalance + if availableOverage > 0 then + toDeduct = math.min(remaining, availableOverage) + else + toDeduct = 0 + end end end @@ -613,13 +653,14 @@ end -- Helper: Calculate sync deltas for sync mode requests -- In sync mode, we want to adjust cache to match the target balance from Postgres --- This requires loading the MERGED balance (customer + all entities) to calculate the correct delta -local function calculateSyncDeltas(featureDeductions, targetBalance) - -- Load merged customer features (customer + entities) to get accurate current balance - local mergedFeatures = loadCusFeatures(cacheKey, orgId, env, customerId) +-- If entityId is provided, loads entity-level features (entity + customer) +-- If entityId is nil, loads customer-level features (customer + all entities) +local function calculateSyncDeltas(featureDeductions, targetBalance, entityId) + -- Load merged features based on perspective (entity-level or customer-level) + local mergedFeatures = loadCusFeatures(cacheKey, orgId, env, customerId, entityId) if not mergedFeatures then - return -- Customer not in cache, no-op + return -- Customer/entity not in cache, no-op end for _, featureDeduction in ipairs(featureDeductions) do @@ -627,7 +668,7 @@ local function calculateSyncDeltas(featureDeductions, targetBalance) local mergedFeature = mergedFeatures[featureId] if mergedFeature and not mergedFeature.unlimited then - -- Get current MERGED balance (includes entities) + -- Get current MERGED balance (from entity-level or customer-level perspective) local currentBalance = mergedFeature.balance or 0 -- Calculate delta (positive means deduct, negative means refund) @@ -688,7 +729,7 @@ local function processRequest(request, loadedCusFeatures, entityFeatureStates) -- SYNC MODE: Calculate delta to bring cache to target balance -- Note: syncMode requests should only have ONE feature deduction if syncMode and targetBalance then - calculateSyncDeltas(featureDeductions, targetBalance) + calculateSyncDeltas(featureDeductions, targetBalance, entityId) end -- Try to deduct from all features (primary + credit systems) @@ -880,6 +921,13 @@ local function processRequest(request, loadedCusFeatures, entityFeatureStates) for _, stateChange in ipairs(requestStateChanges) do applyStateChanges(stateChange.cusFeature, stateChange.changes) + + -- Track which scopes were modified (customer vs entity) + if stateChange.target == "customer" then + customerChanged = true + elseif stateChange.target == "entity" and stateChange.entityId then + changedEntityIds[stateChange.entityId] = true + end end return { @@ -1035,10 +1083,18 @@ for key, deltas in pairs(keyDeltas) do end end --- Return results +-- Convert changedEntityIds set to array +local changedEntityIdsArray = {} +for entityId, _ in pairs(changedEntityIds) do + table.insert(changedEntityIdsArray, entityId) +end + +-- Return results with changed scopes return cjson.encode({ success = true, - results = results + results = results, + customerChanged = customerChanged, + changedEntityIds = changedEntityIdsArray }) diff --git a/server/src/internal/entities/entityUtils/apiEntityCacheUtils/entityLuaScripts/checkEntityCacheExists.lua b/server/src/_luaScripts/entityLuaScripts/checkEntityCacheExists.lua similarity index 100% rename from server/src/internal/entities/entityUtils/apiEntityCacheUtils/entityLuaScripts/checkEntityCacheExists.lua rename to server/src/_luaScripts/entityLuaScripts/checkEntityCacheExists.lua diff --git a/server/src/internal/entities/entityUtils/apiEntityCacheUtils/entityLuaScripts/getEntity.lua b/server/src/_luaScripts/entityLuaScripts/getEntity.backup.lua similarity index 100% rename from server/src/internal/entities/entityUtils/apiEntityCacheUtils/entityLuaScripts/getEntity.lua rename to server/src/_luaScripts/entityLuaScripts/getEntity.backup.lua diff --git a/server/src/_luaScripts/entityLuaScripts/getEntity.lua b/server/src/_luaScripts/entityLuaScripts/getEntity.lua new file mode 100644 index 000000000..99c4d1d85 --- /dev/null +++ b/server/src/_luaScripts/entityLuaScripts/getEntity.lua @@ -0,0 +1,405 @@ +-- getEntity.lua +-- Atomically retrieves an entity object from Redis, reconstructing from base JSON and feature HSETs +-- Merges entity features with customer features (unless skipCustomerMerge is true) +-- KEYS[1]: cache key (e.g., "{org_id}:env:customer:customer_id:entity:entity_id") +-- ARGV[1]: org_id (for building customer cache keys) +-- ARGV[2]: env (for building customer cache keys) +-- ARGV[3]: customerId +-- ARGV[4]: entityId +-- ARGV[5]: skipCustomerMerge (optional, "true" to skip merging with customer) + +-- Helper function to safely convert values to numbers for arithmetic +-- Returns the value if it's a number, otherwise returns 0 +local function toNum(value) + return type(value) == "number" and value or 0 +end + +-- Helper function to get product key for grouping (product_id:normalized_status) +local function getProductKey(product) + local status = product.status + -- Normalize status: "active" or "past_due" -> "active", otherwise use actual status + if status == "active" or status == "past_due" then + status = "active" + end + return product.id .. ":" .. status +end + +-- Helper function to merge customer products into entity products +-- Adds customer products that don't already exist in entity products (by product key) +local function mergeCustomerProductsIntoEntity(entityProducts, customerProducts) + if not customerProducts or #customerProducts == 0 then + return entityProducts or {} + end + + if not entityProducts then + entityProducts = {} + end + + -- Build a set of existing product keys in entity products + local existingKeys = {} + for _, product in ipairs(entityProducts) do + local key = getProductKey(product) + existingKeys[key] = true + end + + -- Add customer products that don't exist in entity products + local mergedProducts = {} + + -- First, add all entity products + for _, product in ipairs(entityProducts) do + table.insert(mergedProducts, product) + end + + -- Then, add customer products that don't exist + for _, customerProduct in ipairs(customerProducts) do + local key = getProductKey(customerProduct) + if not existingKeys[key] then + table.insert(mergedProducts, customerProduct) + end + end + + return mergedProducts +end + +local cacheKey = KEYS[1] +local baseKey = cacheKey +local orgId = ARGV[1] +local env = ARGV[2] +local customerId = ARGV[3] +local entityId = ARGV[4] +local skipCustomerMerge = ARGV[5] == "true" + +-- Get base entity JSON +local baseJson = redis.call("GET", baseKey) +if not baseJson then + return nil +end + +local baseEntity = cjson.decode(baseJson) +local entityFeatureIds = baseEntity._featureIds or {} + +-- ============================================================================ +-- FETCH ENTITY FEATURES +-- ============================================================================ +local entityFeatures = {} + +for _, featureId in ipairs(entityFeatureIds) do + local featureKey = cacheKey .. ":features:" .. featureId + local featureHash = redis.call("HGETALL", featureKey) + + -- If feature key is missing, return nil (partial eviction detected) + if #featureHash == 0 then + return nil + end + + -- Convert HGETALL result (flat array) to table + local featureData = {} + for i = 1, #featureHash, 2 do + local key = featureHash[i] + local value = featureHash[i + 1] + + -- Check for null first before parsing + if value == "null" then + featureData[key] = cjson.null + elseif key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" or key == "_breakdown_count" or key == "_rollover_count" then + featureData[key] = tonumber(value) + elseif key == "unlimited" or key == "overage_allowed" then + featureData[key] = (value == "true") + elseif key == "credit_schema" then + -- Parse credit_schema JSON array + if value ~= "" then + featureData[key] = cjson.decode(value) + else + featureData[key] = cjson.null + end + else + featureData[key] = value + end + end + + -- Get rollover count + local rolloverCount = featureData._rollover_count or 0 + featureData._rollover_count = nil -- Remove from final output + + -- Fetch rollover items + local rollovers = {} + for i = 0, rolloverCount - 1 do + local rolloverKey = cacheKey .. ":features:" .. featureId .. ":rollover:" .. i + local rolloverHash = redis.call("HGETALL", rolloverKey) + + -- If rollover key is missing, return nil (partial eviction detected) + if #rolloverHash == 0 then + return nil + end + + local rolloverData = {} + for j = 1, #rolloverHash, 2 do + local key = rolloverHash[j] + local value = rolloverHash[j + 1] + + if value == "null" then + rolloverData[key] = cjson.null + elseif key == "balance" or key == "expires_at" then + rolloverData[key] = tonumber(value) + else + rolloverData[key] = value + end + end + table.insert(rollovers, rolloverData) + end + + if #rollovers > 0 then + featureData.rollovers = rollovers + end + + -- Get breakdown count + local breakdownCount = featureData._breakdown_count or 0 + featureData._breakdown_count = nil -- Remove from final output + + -- Fetch breakdown items + local breakdown = {} + for i = 0, breakdownCount - 1 do + local breakdownKey = cacheKey .. ":features:" .. featureId .. ":breakdown:" .. i + local breakdownHash = redis.call("HGETALL", breakdownKey) + + -- If breakdown key is missing, return nil (partial eviction detected) + if #breakdownHash == 0 then + return nil + end + + local breakdownData = {} + for j = 1, #breakdownHash, 2 do + local key = breakdownHash[j] + local value = breakdownHash[j + 1] + + if value == "null" then + breakdownData[key] = cjson.null + elseif key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" then + breakdownData[key] = tonumber(value) + elseif key == "overage_allowed" then + breakdownData[key] = (value == "true") + else + breakdownData[key] = value + end + end + table.insert(breakdown, breakdownData) + end + + if #breakdown > 0 then + featureData.breakdown = breakdown + end + + entityFeatures[featureId] = featureData +end + +-- ============================================================================ +-- FETCH CUSTOMER MASTER FEATURES (no entity aggregation) +-- Skip if skipCustomerMerge is true +-- ============================================================================ +local customerFeatures = {} +local customerBase = nil -- Store customer base for product access + +if not skipCustomerMerge and customerId then + local customerCacheKey = "{" .. orgId .. "}:" .. env .. ":customer:" .. customerId + local customerBaseJson = redis.call("GET", customerCacheKey) + + if customerBaseJson then + customerBase = cjson.decode(customerBaseJson) + local customerFeatureIds = customerBase._featureIds or {} + + for _, featureId in ipairs(customerFeatureIds) do + local customerFeatureKey = customerCacheKey .. ":features:" .. featureId + local customerFeatureHash = redis.call("HGETALL", customerFeatureKey) + + if #customerFeatureHash > 0 then + -- Parse customer feature + local customerFeature = {} + for i = 1, #customerFeatureHash, 2 do + local key = customerFeatureHash[i] + local value = customerFeatureHash[i + 1] + + if value == "null" then + customerFeature[key] = cjson.null + elseif key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" or key == "_breakdown_count" or key == "_rollover_count" then + customerFeature[key] = tonumber(value) + elseif key == "unlimited" or key == "overage_allowed" then + customerFeature[key] = (value == "true") + elseif key == "credit_schema" then + if value ~= "" then + customerFeature[key] = cjson.decode(value) + else + customerFeature[key] = cjson.null + end + else + customerFeature[key] = value + end + end + + -- Fetch rollover items + local rolloverCount = customerFeature._rollover_count or 0 + customerFeature._rollover_count = nil + local rollovers = {} + + for i = 0, rolloverCount - 1 do + local rolloverKey = customerFeatureKey .. ":rollover:" .. i + local rolloverHash = redis.call("HGETALL", rolloverKey) + + if #rolloverHash > 0 then + local rolloverData = {} + for j = 1, #rolloverHash, 2 do + local key = rolloverHash[j] + local value = rolloverHash[j + 1] + + if value == "null" then + rolloverData[key] = cjson.null + elseif key == "balance" or key == "expires_at" then + rolloverData[key] = tonumber(value) + else + rolloverData[key] = value + end + end + table.insert(rollovers, rolloverData) + end + end + + if #rollovers > 0 then + customerFeature.rollovers = rollovers + end + + -- Fetch breakdown items + local breakdownCount = customerFeature._breakdown_count or 0 + customerFeature._breakdown_count = nil + local breakdown = {} + + for i = 0, breakdownCount - 1 do + local breakdownKey = customerFeatureKey .. ":breakdown:" .. i + local breakdownHash = redis.call("HGETALL", breakdownKey) + + if #breakdownHash > 0 then + local breakdownData = {} + for j = 1, #breakdownHash, 2 do + local key = breakdownHash[j] + local value = breakdownHash[j + 1] + + if value == "null" then + breakdownData[key] = cjson.null + elseif key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" then + breakdownData[key] = tonumber(value) + elseif key == "overage_allowed" then + breakdownData[key] = (value == "true") + else + breakdownData[key] = value + end + end + table.insert(breakdown, breakdownData) + end + end + + if #breakdown > 0 then + customerFeature.breakdown = breakdown + end + + customerFeatures[featureId] = customerFeature + end + end + end +end + +-- ============================================================================ +-- MERGE CUSTOMER AND ENTITY FEATURES +-- ============================================================================ +local mergedFeatures = {} + +-- First, add all customer features (inherited) +for featureId, customerFeature in pairs(customerFeatures) do + mergedFeatures[featureId] = customerFeature +end + +-- Then, merge or add entity features +for featureId, entityFeature in pairs(entityFeatures) do + local customerFeature = customerFeatures[featureId] + + if customerFeature then + -- Both customer and entity have this feature - merge balances + if not entityFeature.unlimited and not customerFeature.unlimited then + entityFeature.balance = toNum(entityFeature.balance) + toNum(customerFeature.balance) + entityFeature.usage = toNum(entityFeature.usage) + toNum(customerFeature.usage) + entityFeature.included_usage = toNum(entityFeature.included_usage) + toNum(customerFeature.included_usage) + entityFeature.usage_limit = toNum(entityFeature.usage_limit) + toNum(customerFeature.usage_limit) + + -- Use minimum next_reset_at (earliest reset time) + if type(entityFeature.next_reset_at) == "number" and type(customerFeature.next_reset_at) == "number" then + if customerFeature.next_reset_at < entityFeature.next_reset_at then + entityFeature.next_reset_at = customerFeature.next_reset_at + end + elseif type(customerFeature.next_reset_at) == "number" then + entityFeature.next_reset_at = customerFeature.next_reset_at + end + + -- Merge breakdown balances + if entityFeature.breakdown and customerFeature.breakdown then + for i, entityBreakdown in ipairs(entityFeature.breakdown) do + local customerBreakdown = customerFeature.breakdown[i] + if customerBreakdown then + entityBreakdown.balance = toNum(entityBreakdown.balance) + toNum(customerBreakdown.balance) + entityBreakdown.usage = toNum(entityBreakdown.usage) + toNum(customerBreakdown.usage) + entityBreakdown.included_usage = toNum(entityBreakdown.included_usage) + toNum(customerBreakdown.included_usage) + entityBreakdown.usage_limit = toNum(entityBreakdown.usage_limit) + toNum(customerBreakdown.usage_limit) + + -- Use minimum next_reset_at for breakdown + if type(entityBreakdown.next_reset_at) == "number" and type(customerBreakdown.next_reset_at) == "number" then + if customerBreakdown.next_reset_at < entityBreakdown.next_reset_at then + entityBreakdown.next_reset_at = customerBreakdown.next_reset_at + end + elseif type(customerBreakdown.next_reset_at) == "number" then + entityBreakdown.next_reset_at = customerBreakdown.next_reset_at + end + end + end + end + + -- Merge rollover balances + if entityFeature.rollovers and customerFeature.rollovers then + for i, entityRollover in ipairs(entityFeature.rollovers) do + local customerRollover = customerFeature.rollovers[i] + if customerRollover then + entityRollover.balance = toNum(entityRollover.balance) + toNum(customerRollover.balance) + end + end + end + end + mergedFeatures[featureId] = entityFeature + else + -- Only entity has this feature - use entity's feature + mergedFeatures[featureId] = entityFeature + end +end + +-- ============================================================================ +-- MERGE CUSTOMER PRODUCTS INTO ENTITY PRODUCTS +-- Skip if skipCustomerMerge is true +-- ============================================================================ + +-- Get entity products (start with entity's own products) +local entityProducts = baseEntity.products or {} + +if not skipCustomerMerge then + -- Get customer products if customer base exists + local customerProducts = nil + if customerBase and customerBase.products then + customerProducts = customerBase.products + end + + -- Merge customer products into entity products (only add if not exists) + baseEntity.products = mergeCustomerProductsIntoEntity(entityProducts, customerProducts) +else + -- No merging - just use entity's own products + baseEntity.products = entityProducts +end + +-- Build final entity object +baseEntity._featureIds = nil -- Remove tracking field +baseEntity.features = mergedFeatures + +return cjson.encode(baseEntity) + diff --git a/server/src/internal/entities/entityUtils/apiEntityCacheUtils/entityLuaScripts/setEntitiesBatch.lua b/server/src/_luaScripts/entityLuaScripts/setEntitiesBatch.lua similarity index 100% rename from server/src/internal/entities/entityUtils/apiEntityCacheUtils/entityLuaScripts/setEntitiesBatch.lua rename to server/src/_luaScripts/entityLuaScripts/setEntitiesBatch.lua diff --git a/server/src/internal/entities/entityUtils/apiEntityCacheUtils/entityLuaScripts/setEntity.lua b/server/src/_luaScripts/entityLuaScripts/setEntity.lua similarity index 100% rename from server/src/internal/entities/entityUtils/apiEntityCacheUtils/entityLuaScripts/setEntity.lua rename to server/src/_luaScripts/entityLuaScripts/setEntity.lua diff --git a/server/src/internal/entities/entityUtils/apiEntityCacheUtils/entityLuaScripts/setEntityProducts.lua b/server/src/_luaScripts/entityLuaScripts/setEntityProducts.lua similarity index 100% rename from server/src/internal/entities/entityUtils/apiEntityCacheUtils/entityLuaScripts/setEntityProducts.lua rename to server/src/_luaScripts/entityLuaScripts/setEntityProducts.lua diff --git a/server/src/_luaScripts/luaScripts.ts b/server/src/_luaScripts/luaScripts.ts new file mode 100644 index 000000000..915c85030 --- /dev/null +++ b/server/src/_luaScripts/luaScripts.ts @@ -0,0 +1,105 @@ +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// ============================================================================ +// SHARED LUA FUNCTIONS +// ============================================================================ + +// Load shared feature loading function (used by customer, entity, and deduction scripts) +const LOAD_CUS_FEATURES = readFileSync( + join(__dirname, "cusLuaScripts/loadCusFeatures.lua"), + "utf-8", +); + +// ============================================================================ +// CUSTOMER SCRIPTS +// ============================================================================ + +// Load shared validation function +const CHECK_CACHE_EXISTS = readFileSync( + join(__dirname, "cusLuaScripts/checkCacheExists.lua"), + "utf-8", +); + +// Prepend loadCusFeatures to GET_CUSTOMER_SCRIPT so it can use the function +const getCustomerScript = readFileSync( + join(__dirname, "cusLuaScripts/getCustomer.lua"), + "utf-8", +); +export const GET_CUSTOMER_SCRIPT = `${LOAD_CUS_FEATURES}\n${getCustomerScript}`; + +// Prepend validation function to SET_CUSTOMER_SCRIPT +const setCustomerScript = readFileSync( + join(__dirname, "cusLuaScripts/setCustomer.lua"), + "utf-8", +); +export const SET_CUSTOMER_SCRIPT = `${CHECK_CACHE_EXISTS}\n${setCustomerScript}`; + +export const SET_CUSTOMER_PRODUCTS_SCRIPT = readFileSync( + join(__dirname, "cusLuaScripts/setCustomerProducts.lua"), + "utf-8", +); + +export const SET_CUSTOMER_DETAILS_SCRIPT = readFileSync( + join(__dirname, "cusLuaScripts/setCustomerDetails.lua"), + "utf-8", +); + +export const DELETE_CUSTOMER_SCRIPT = readFileSync( + join(__dirname, "cusLuaScripts/deleteCustomer.lua"), + "utf-8", +); + +// ============================================================================ +// ENTITY SCRIPTS +// ============================================================================ + +// Load shared validation function +const CHECK_ENTITY_CACHE_EXISTS = readFileSync( + join(__dirname, "entityLuaScripts/checkEntityCacheExists.lua"), + "utf-8", +); + +// Prepend loadCusFeatures to GET_ENTITY_SCRIPT so it can use the function +const getEntityScript = readFileSync( + join(__dirname, "entityLuaScripts/getEntity.lua"), + "utf-8", +); +export const GET_ENTITY_SCRIPT = `${LOAD_CUS_FEATURES}\n${getEntityScript}`; + +// Prepend validation function to SET_ENTITY_SCRIPT +const setEntityScript = readFileSync( + join(__dirname, "entityLuaScripts/setEntity.lua"), + "utf-8", +); +export const SET_ENTITY_SCRIPT = `${CHECK_ENTITY_CACHE_EXISTS}\n${setEntityScript}`; + +export const SET_ENTITIES_BATCH_SCRIPT = readFileSync( + join(__dirname, "entityLuaScripts/setEntitiesBatch.lua"), + "utf-8", +); + +export const SET_ENTITY_PRODUCTS_SCRIPT = readFileSync( + join(__dirname, "entityLuaScripts/setEntityProducts.lua"), + "utf-8", +); + +// ============================================================================ +// DEDUCTION SCRIPTS +// ============================================================================ + +// Load batchDeduction script +const batchDeduction = readFileSync( + join(__dirname, "deductionLuaScripts/batchDeduction.lua"), + "utf-8", +); + +export function getBatchDeductionScript(): string { + return `${LOAD_CUS_FEATURES}\n${batchDeduction}`; +} + +export const BATCH_DEDUCTION_SCRIPT = getBatchDeductionScript(); diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts index 43b554c64..2bb796e47 100644 --- a/server/src/external/autumn/autumnCli.ts +++ b/server/src/external/autumn/autumnCli.ts @@ -295,8 +295,27 @@ export class AutumnInt { return data; }, - create: async (customer: { id: string; email?: string; name?: string }) => { - const data = await this.post(`/customers?with_autumn_id=true`, customer); + create: async ({ + id, + email, + name, + withAutumnId = true, + expand = [], + }: { + id: string; + email?: string; + name?: string; + withAutumnId?: boolean; + expand?: CusExpand[]; + }) => { + const data = await this.post( + `/customers?with_autumn_id=${withAutumnId ? "true" : "false"}${expand && expand.length > 0 ? `&expand=${expand.join(",")}` : ""}`, + { + id, + email, + name, + }, + ); return data; }, delete: async ( diff --git a/server/src/internal/api/check/checkUtils/getCheckData.ts b/server/src/internal/api/check/checkUtils/getCheckData.ts index 98bcdd57f..d12de54f7 100644 --- a/server/src/internal/api/check/checkUtils/getCheckData.ts +++ b/server/src/internal/api/check/checkUtils/getCheckData.ts @@ -140,18 +140,17 @@ export const getCheckData = async ({ // }); let apiEntity: ApiCustomer | ApiEntity | undefined; - apiEntity = await getOrCreateApiCustomer({ + const { apiCustomer } = await getOrCreateApiCustomer({ ctx, customerId: customer_id, - withAutumnId: true, }); + apiEntity = apiCustomer; if (entity_id) { const { apiEntity: apiEntityResult } = await getCachedApiEntity({ ctx, customerId: customer_id, entityId: entity_id, - withAutumnId: false, }); apiEntity = apiEntityResult; diff --git a/server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateRewardProgram.ts b/server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateRewardProgram.ts index 9bf020f7e..8a85db91d 100644 --- a/server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateRewardProgram.ts +++ b/server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateRewardProgram.ts @@ -8,6 +8,7 @@ import { constructRewardProgram } from "@/internal/rewards/rewardTriggerUtils.js import RecaseError from "@/utils/errorUtils.js"; import { nullish } from "@/utils/genUtils.js"; import { routeHandler } from "@/utils/routerUtils.js"; +import { RewardService } from "../../../../rewards/RewardService.js"; export default async (req: any, res: any) => routeHandler({ @@ -49,19 +50,29 @@ export default async (req: any, res: any) => }); } - const rewardProgram = constructRewardProgram({ - rewardProgramData: CreateRewardProgram.parse(req.body), + const reward = await RewardService.get({ + db, + idOrInternalId: body.internal_reward_id, orgId, env, }); - // Fetch reward ID - // let reward = await RewardService.get({ - // db, - // id: rewardProgram.internal_reward_id, - // orgId, - // env, - // }); + if (!reward) { + throw new RecaseError({ + message: "Reward not found", + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } + + const rewardProgram = constructRewardProgram({ + rewardProgramData: CreateRewardProgram.parse({ + ...req.body, + internal_reward_id: reward.internal_id, + }), + orgId, + env, + }); if ( rewardProgram.when === RewardTriggerEvent.Checkout && diff --git a/server/src/internal/balances/track/TRACK_IMPLEMENTATION_CHECKLIST.md b/server/src/internal/balances/track/TRACK_IMPLEMENTATION_CHECKLIST.md deleted file mode 100644 index ee435d662..000000000 --- a/server/src/internal/balances/track/TRACK_IMPLEMENTATION_CHECKLIST.md +++ /dev/null @@ -1,26 +0,0 @@ -# Track Implementation Checklist - -## Validation - -### 0. ✅ Validate Deduction -- If overage_allowed: False → Check feature.balance >= amount -- If overage_allowed: True → Check (usage_limit - usage) >= amount OR no usage_limit -- Two-pass atomic validation: Validate ALL features before ANY deductions (all-or-nothing) - -## Deduction Cases - -### 1. ✅ Main Balance Deduction -- With breakdowns: Deduct from breakdown balances, then breakdown overage -- Without breakdowns: Deduct from top-level balance, then top-level overage -- Respect overage_behavior ("cap" | "reject") - -### 2. ✅ Rollover Balance Deduction -- Deduct from rollovers before main balance -- Update top-level balance and usage - -### 3. ⬜ Credit System Deduction -- Deduct from credit features when target feature is insufficient - -### 4. ⬜ Entity-Specific Deduction -- Handle entity-scoped deductions - diff --git a/server/src/internal/balances/track/TRACK_RULES.md b/server/src/internal/balances/track/TRACK_RULES.md new file mode 100644 index 000000000..1945937b5 --- /dev/null +++ b/server/src/internal/balances/track/TRACK_RULES.md @@ -0,0 +1,168 @@ +# Track Implementation Rules + +This guide is concise and has no fluff. It prevents future coding agents from making mistakes with the track implementation. + +## BatchingManager: Customer vs Entity Batching + +**CRITICAL**: Batching must be atomic per customer AND per entity. + +### Batch Key Construction +```typescript +// ❌ WRONG: Batches all deductions for a customer together +const batchKey = cacheKey; // customer cache key only + +// ✅ CORRECT: Separate batches for customer-level vs each entity +const batchKey = entityId + ? buildCachedApiEntityKey({ entityId, customerId, orgId, env }) + : buildCachedApiCustomerKey({ customerId, orgId, env }); +``` + +### Why This Matters +- **Customer-level deduction**: Batch under `{orgId}:env:customer:{customerId}` +- **Entity1 deduction**: Batch under `{orgId}:env:customer:{customerId}:entity:{entity1Id}` +- **Entity2 deduction**: Batch under `{orgId}:env:customer:{customerId}:entity:{entity2Id}` + +Each batch executes atomically. Mixing customer and entity deductions in one batch breaks atomicity. + +### Implementation Details +- `entityId` is stored at the **batch level**, not per-request +- All requests in a batch share the same `entityId` (or all are customer-level) +- The Lua script receives `batch.entityId` for all requests in that batch +- This ensures proper batching by entity and prevents mixed customer/entity batches + +### Example +```typescript +// These should create 3 separate batches: +await track({ customer_id: "cus1", feature_id: "messages", value: 10 }); // Batch 1 +await track({ customer_id: "cus1", entity_id: "ent1", feature_id: "messages", value: 5 }); // Batch 2 +await track({ customer_id: "cus1", entity_id: "ent2", feature_id: "messages", value: 3 }); // Batch 3 +``` + +## Redis vs Postgres Tracking + +### single_use features → Redis only +- Deducted via `runRedisDeduction.ts` → `BatchingManager` → `batchDeduction.lua` +- **MUST** sync Redis → Postgres (only for changed scopes) +- Uses `globalSyncBatchingManager.addSyncPair()` based on `customerChanged` and `changedEntityIds` + +### continuous_use features → Postgres first, then Redis +1. Deduct from Postgres via `runDeductionTx.ts` +2. Get actual deducted amount from SQL result (`actualDeductions`) +3. Deduct same amount from Redis cache via `deductFromCache.ts` +4. Uses direct Lua script call (no batching) to avoid race conditions + +### Rule +Never sync in both directions. Single source of truth: +- `single_use` → Redis is source of truth, sync to Postgres for durability +- `continuous_use` → Postgres is source of truth, Redis is cache + +## Unmerged Cache Access for Syncing + +**CRITICAL**: When syncing from Redis to Postgres, fetch the unmerged balance for that specific scope. + +### Problem +The default cache behavior merges balances: +- `getCustomer`: Returns customer + all entities merged +- `getEntity`: Returns entity + customer merged + +This is correct for API responses, but WRONG for syncing because: +```typescript +// Customer has 10, Entity1 has 5, Entity2 has 5 +// GET /customers/:id returns balance=20 (10+5+5) ✓ correct for API +// But when syncing customer-level, we need ONLY 10 (customer's own balance) +``` + +### Solution +Use `skipEntityMerge` / `skipCustomerMerge` flags when fetching for sync: + +```typescript +// Syncing customer-level +const { apiCustomer } = await getCachedApiCustomer({ + ctx, + customerId, + skipEntityMerge: true, // Returns ONLY customer's balance (not merged with entities) +}); + +// Syncing entity-level +const { apiEntity } = await getCachedApiEntity({ + ctx, + customerId, + entityId, + skipCustomerMerge: true, // Returns ONLY entity's balance (not merged with customer) +}); +``` + +### Implementation +- `getCustomer.lua`: Accepts `ARGV[4]` as `skipEntityMerge` flag +- `getEntity.lua`: Accepts `ARGV[5]` as `skipCustomerMerge` flag +- `loadCusFeatures`: Special mode `"__CUSTOMER_ONLY__"` returns unmerged customer features + +## Selective Sync: Preventing Unnecessary Syncs + +**CRITICAL**: Only sync scopes that were actually modified. + +### Problem +If every track queues a sync for customer + all entities, we get unnecessary syncs and potential race conditions: +```typescript +// ❌ WRONG: Always sync everything +track({ customer_id: "cus1", entity_id: "ent1", feature_id: "messages", value: 1 }); +// Syncs: cus1, ent1 (but ent1 might not have changed if deduction came from customer balance!) +``` + +### Solution +`batchDeduction.lua` tracks which scopes were actually modified: +- `customerChanged`: Boolean flag for customer-level changes +- `changedEntityIds`: Array of entity IDs that had balance changes + +```typescript +// ✅ CORRECT: Only sync what changed +const result = await deduct(...); +if (result.customerChanged) { + addSyncPair({ customerId, featureId, entityId: undefined }); +} +for (const entityId of result.changedEntityIds) { + addSyncPair({ customerId, featureId, entityId }); +} +``` + +### Examples +```typescript +// Customer-level track that deducts from customer balance only +track({ customer_id: "cus1", feature_id: "messages", value: 10 }); +// Result: customerChanged=true, changedEntityIds=[] +// Syncs: cus1 only + +// Entity-level track that deducts from entity first, then customer +track({ customer_id: "cus1", entity_id: "ent1", feature_id: "messages", value: 10 }); +// Result: customerChanged=true, changedEntityIds=["ent1"] +// Syncs: cus1, ent1 + +// Entity-level track that only deducts from entity (customer has unlimited) +track({ customer_id: "cus1", entity_id: "ent1", feature_id: "messages", value: 10 }); +// Result: customerChanged=false, changedEntityIds=["ent1"] +// Syncs: ent1 only +``` + +## Postgres Deduction Order + +`performDeductionV2.sql` processes entitlements in the EXACT order they are passed in the `sorted_entitlements` array. The `jsonb_array_elements()` function preserves array order. + +Use `reverseOrder` config to control deduction order: +- `reverseOrder: false` → Oldest entitlements first +- `reverseOrder: true` → Newest entitlements first + +## Actual Deductions Tracking + +When deducting from Postgres, always track the ACTUAL amount deducted (not the requested amount): + +```typescript +// ❌ WRONG: Using requested amount +const requestedAmount = 10; +await deductFromCache({ amount: requestedAmount }); + +// ✅ CORRECT: Using actual deducted amount from SQL result +const result = await db.execute(sql`...`); +const actualDeducted = result.updates[entId].deducted; +actualDeductions[featureId] = actualDeducted; +await deductFromCache({ amount: actualDeducted }); +``` diff --git a/server/src/internal/balances/track/handleTrack.ts b/server/src/internal/balances/track/handleTrack.ts index dbf693784..a321d5ce0 100644 --- a/server/src/internal/balances/track/handleTrack.ts +++ b/server/src/internal/balances/track/handleTrack.ts @@ -1,11 +1,13 @@ import { ApiVersion, ErrCode, + InsufficientBalanceError, isContUseFeature, RecaseError, SuccessCode, type TrackParams, TrackParamsSchema, + type TrackResponse, } from "@autumn/shared"; import type { RequestContext } from "@/honoUtils/HonoEnv.js"; import { createRoute } from "../../../honoMiddlewares/routeHandler.js"; @@ -30,30 +32,40 @@ const executePostgresTracking = async ({ body: TrackParams; featureDeductions: FeatureDeduction[]; }) => { - const { event } = await runDeductionTx({ - ctx, - customerId: body.customer_id, - entityId: body.entity_id, - deductions: featureDeductions, - overageBehaviour: body.overage_behavior, - eventInfo: { - event_name: body.feature_id || body.event_name!, - value: body.value ?? 1, - properties: body.properties, - timestamp: body.timestamp, - idempotency_key: body.idempotency_key, - }, - refreshCache: true, - }); - - return { - id: event?.id || "", + const response: TrackResponse = { + id: "", code: SuccessCode.EventReceived, customer_id: body.customer_id, entity_id: body.entity_id, feature_id: body.feature_id, event_name: body.event_name, }; + try { + const { event } = await runDeductionTx({ + ctx, + customerId: body.customer_id, + entityId: body.entity_id, + deductions: featureDeductions, + overageBehaviour: body.overage_behavior, + eventInfo: { + event_name: body.feature_id || body.event_name!, + value: body.value ?? 1, + properties: body.properties, + timestamp: body.timestamp, + idempotency_key: body.idempotency_key, + }, + refreshCache: true, + }); + response.id = event?.id || ""; + } catch (error) { + if (error instanceof InsufficientBalanceError) { + response.code = "insufficient_balance"; + } else { + throw error; + } + } + + return response; }; export const handleTrack = createRoute({ diff --git a/server/src/internal/balances/track/redisTrackUtils/BATCHING_ARCHITECTURE.md b/server/src/internal/balances/track/redisTrackUtils/BATCHING_ARCHITECTURE.md deleted file mode 100644 index 97459149a..000000000 --- a/server/src/internal/balances/track/redisTrackUtils/BATCHING_ARCHITECTURE.md +++ /dev/null @@ -1,127 +0,0 @@ -# Batching Architecture - -## Overview -The batching system collects multiple track requests for the same customer within a 10ms window and processes them atomically in a single Lua script execution. - -## Location -All batching-related files are in `server/src/internal/balances/track/redisTrackUtils/`: -- `batchDeduction.lua` - Lua script (processes batch atomically) -- `BatchingManager.ts` - Collects requests and triggers batch execution -- `executeBatchDeduction.ts` - Executes Lua script -- `luaScripts.ts` - Loads Lua script at module initialization -- `runRedisDeduction.ts` - Entry point from track endpoint - -## Data Flow - -``` -runRedisDeduction - ↓ (featureDeductions: [{ featureId, amount }]) -globalBatchingManager.deduct - ↓ (batches by customerId) -executeBatchDeduction - ↓ (single Lua script call) -batchDeduction.lua - ↓ (processes all requests, accumulates deltas) -Redis HINCRBYFLOAT (one command per key per field) -``` - -## New Interface - -### globalBatchingManager.deduct() -```typescript -{ - customerId: string, - featureDeductions: [ - { featureId: "credits", amount: 10 }, - { featureId: "api_calls", amount: 5 } - ], - orgId: string, - env: string, - entityId?: string, - overageBehavior: "cap" | "reject" -} -``` - -### Batching Key -``` -org_id:env:customer:customer_id -``` -- Batches by **customer only** (not per-feature) -- All requests for the same customer in a 10ms window are batched together - -### Lua Script Input (ARGV[1]) -```json -[ - { - "featureDeductions": [ - { "featureId": "credits", "amount": 10 }, - { "featureId": "api_calls", "amount": 5 } - ], - "overageBehavior": "cap" - }, - // ... more requests -] -``` - -### Lua Script Output -```json -{ - "success": true, - "results": [ - { "success": true, "error": null }, - { "success": false, "error": "INSUFFICIENT_BALANCE" } - ] -} -``` - -## Lua Script Structure - -### Two Main Functions: - -1. **processRequest(request)** - Handles one unit of request - - Takes: `{ featureDeductions: [...], overageBehavior: "cap" }` - - Loops through each feature deduction - - Calculates deltas for each feature - - Uses `addDelta()` to accumulate changes - - Returns: `{ success: boolean, error?: string }` - -2. **Top-level loop** - Processes all requests - - Loops through all requests - - Calls `processRequest()` for each - - Applies all accumulated deltas at once with `redis.call("HINCRBYFLOAT", ...)` - -## Delta Accumulation Pattern - -```lua --- Global accumulator -local keyDeltas = {} -- { [redisKey][field] = delta } - --- Helper to add deltas -local function addDelta(key, field, delta) - if not keyDeltas[key] then - keyDeltas[key] = {} - end - keyDeltas[key][field] = (keyDeltas[key][field] or 0) + delta -end - --- Process requests (accumulate deltas in memory) -for _, request in ipairs(requests) do - processRequest(request) -- calls addDelta() internally -end - --- Apply all deltas (ONE Redis write per key per field) -for key, deltas in pairs(keyDeltas) do - for field, delta in pairs(deltas) do - redis.call("HINCRBYFLOAT", key, field, delta) - end -end -``` - -## Performance Benefits - -### Scenario: 1000 concurrent requests for same customer -- **Without batching**: 1000 Lua script calls, 6000 Redis writes (3 keys × 2 fields × 1000) -- **With batching**: 1 Lua script call, 6 Redis writes (3 keys × 2 fields) -- **Improvement**: ~1000x reduction in Redis writes! 🚀 - - diff --git a/server/src/internal/balances/track/redisTrackUtils/BatchingManager.ts b/server/src/internal/balances/track/redisTrackUtils/BatchingManager.ts index 848257a66..6f4de7d74 100644 --- a/server/src/internal/balances/track/redisTrackUtils/BatchingManager.ts +++ b/server/src/internal/balances/track/redisTrackUtils/BatchingManager.ts @@ -1,5 +1,6 @@ import { redis } from "../../../../external/redis/initRedis.js"; import { buildCachedApiCustomerKey } from "../../../customers/cusUtils/apiCusCacheUtils/getCachedApiCustomer.js"; +import { buildCachedApiEntityKey } from "../../../entities/entityUtils/apiEntityCacheUtils/getCachedApiEntity.js"; import { executeBatchDeduction } from "./executeBatchDeduction.js"; interface FeatureDeduction { @@ -7,11 +8,17 @@ interface FeatureDeduction { amount: number; } +interface DeductionResult { + success: boolean; + error?: string; + customerChanged?: boolean; + changedEntityIds?: string[]; +} + interface BatchRequest { featureDeductions: FeatureDeduction[]; overageBehavior: "cap" | "reject"; - entityId?: string; - resolve: (result: { success: boolean; error?: string }) => void; + resolve: (result: DeductionResult) => void; reject: (error: Error) => void; } @@ -56,13 +63,14 @@ export class BatchingManager { env: string; entityId?: string; overageBehavior?: "cap" | "reject"; - }): Promise<{ success: boolean; error?: string }> { - const cacheKey = buildCachedApiCustomerKey({ - customerId, - orgId, - env, - }); - const batchKey = cacheKey; // Batch by customer only + }): Promise { + // CRITICAL: Batch by customer AND entity (if entity-level deduction) + // This ensures entity-level deductions are atomic per entity + // Customer-level: {orgId}:env:customer:{customerId} + // Entity-level: {orgId}:env:customer:{customerId}:entity:{entityId} + const batchKey = entityId + ? buildCachedApiEntityKey({ entityId, customerId, orgId, env }) + : buildCachedApiCustomerKey({ customerId, orgId, env }); return new Promise((resolve, reject) => { // Create batch if it doesn't exist @@ -90,7 +98,6 @@ export class BatchingManager { batch.requests.push({ featureDeductions, overageBehavior, - entityId, resolve, reject, }); @@ -135,26 +142,30 @@ export class BatchingManager { const requests = batch.requests; const batchSize = requests.length; - // Build cache key from batch context + // Build cache key from batch context (always customer cache key for the Lua script) const cacheKey = buildCachedApiCustomerKey({ customerId: batch.customerId, orgId: batch.orgId, env: batch.env, }); + const batchType = batch.entityId + ? `entity ${batch.entityId}` + : "customer-level"; console.log( - `🚀 Executing batch with ${batchSize} requests for customer ${batch.customerId}`, + `🚀 Executing batch with ${batchSize} requests for customer ${batch.customerId} (${batchType})`, ); try { // Execute batch Lua script + // All requests in this batch have the same entityId (batch-level) const result = await executeBatchDeduction({ redis, cacheKey, requests: requests.map((r) => ({ featureDeductions: r.featureDeductions, overageBehavior: r.overageBehavior, - entityId: r.entityId, + entityId: batch.entityId, // Use batch-level entityId (same for all requests) })), orgId: batch.orgId, env: batch.env, @@ -165,15 +176,15 @@ export class BatchingManager { // Resolve each request based on its individual result if (result.success && result.results) { - // TODO: Queue Postgres sync job for successful deductions if needed - // This can be added later when integrating with the sync system - // Match each request with its result + // All requests in this batch get the same customerChanged/changedEntityIds for (let i = 0; i < requests.length; i++) { const requestResult = result.results[i]; requests[i].resolve({ success: requestResult?.success || false, error: requestResult?.error, + customerChanged: result.customerChanged, + changedEntityIds: result.changedEntityIds, }); } } else { diff --git a/server/src/internal/balances/track/redisTrackUtils/syncCacheBalance.ts b/server/src/internal/balances/track/redisTrackUtils/deductFromCache.ts similarity index 59% rename from server/src/internal/balances/track/redisTrackUtils/syncCacheBalance.ts rename to server/src/internal/balances/track/redisTrackUtils/deductFromCache.ts index ff56c2d44..71e3013f3 100644 --- a/server/src/internal/balances/track/redisTrackUtils/syncCacheBalance.ts +++ b/server/src/internal/balances/track/redisTrackUtils/deductFromCache.ts @@ -5,25 +5,25 @@ import { buildCachedApiCustomerKey } from "../../../customers/cusUtils/apiCusCac import { executeBatchDeduction } from "./executeBatchDeduction.js"; /** - * Syncs Redis cache balance to match Postgres balance after a deduction transaction - * Uses sync mode in batchDeduction.lua to calculate delta and apply it + * Deducts from Redis cache to match Postgres deduction + * Called after runDeductionTx to keep cache in sync * - * Use case: After runDeductionTx completes, sync cache to prevent stale data + * Use case: After Postgres deduction completes, apply same deduction to Redis cache * - If cache doesn't exist, no-op (lazy population is fine) - * - If cache exists, calculates delta between current cache and target balance - * - Applies delta to bring cache in sync with Postgres + * - If cache exists, deducts the actual amount from Postgres + * - Uses "cap" behavior since Postgres already validated the deduction */ -export const syncCacheBalance = async ({ +export const deductFromCache = async ({ ctx, customerId, featureId, - targetBalance, + amount, entityId, }: { ctx: AutumnContext; customerId: string; featureId: string; - targetBalance: number; + amount: number; entityId?: string; }): Promise => { const { org, env } = ctx; @@ -34,7 +34,7 @@ export const syncCacheBalance = async ({ env, }); - // Execute Redis sync call directly (no batching) + // Execute Redis deduction directly (no batching to avoid race conditions) await tryRedisWrite(async () => { const result = await executeBatchDeduction({ redis, @@ -44,12 +44,10 @@ export const syncCacheBalance = async ({ featureDeductions: [ { featureId, - amount: 0, // Will be calculated in Lua based on targetBalance + amount, }, ], - overageBehavior: "cap", - syncMode: true, - targetBalance, + overageBehavior: "cap", // Cap since Postgres already handled validation entityId, }, ], @@ -60,8 +58,11 @@ export const syncCacheBalance = async ({ if (!result.success && result.error !== "CUSTOMER_NOT_FOUND") { ctx.logger.warn( - `Failed to sync cache balance for ${customerId}, feature ${featureId}: ${result.error}`, + `Failed to deduct from cache for ${customerId}, feature ${featureId}: ${result.error}`, ); } }); }; + +// Keep the old name for backward compatibility +export const syncCacheBalance = deductFromCache; diff --git a/server/src/internal/balances/track/redisTrackUtils/executeBatchDeduction.ts b/server/src/internal/balances/track/redisTrackUtils/executeBatchDeduction.ts index 2bef486c3..b3e749588 100644 --- a/server/src/internal/balances/track/redisTrackUtils/executeBatchDeduction.ts +++ b/server/src/internal/balances/track/redisTrackUtils/executeBatchDeduction.ts @@ -1,5 +1,5 @@ +import { getBatchDeductionScript } from "@lua/luaScripts.js"; import type { Redis } from "ioredis"; -import { getBatchDeductionScript } from "./luaScripts.js"; interface FeatureDeduction { featureId: string; @@ -23,6 +23,8 @@ interface BatchDeductionResult { success: boolean; results: RequestResult[]; error?: string; + customerChanged?: boolean; // True if customer-level features were modified + changedEntityIds?: string[]; // Array of entity IDs that were modified debug?: any; // For debugging purposes } diff --git a/server/src/internal/balances/track/redisTrackUtils/luaScripts.ts b/server/src/internal/balances/track/redisTrackUtils/luaScripts.ts deleted file mode 100644 index 926f5f77b..000000000 --- a/server/src/internal/balances/track/redisTrackUtils/luaScripts.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Load shared loadCusFeatures function from customer utils -const loadCusFeatures = readFileSync( - join( - __dirname, - "../../../customers/cusUtils/apiCusCacheUtils/cusLuaScripts/loadCusFeatures.lua", - ), - "utf-8", -); - -// Load batchDeduction script -const batchDeduction = readFileSync( - join(__dirname, "batchDeduction.lua"), - "utf-8", -); - -export function getBatchDeductionScript(): string { - return `${loadCusFeatures}\n${batchDeduction}`; -} - -export const BATCH_DEDUCTION_SCRIPT = getBatchDeductionScript(); diff --git a/server/src/internal/balances/track/redisTrackUtils/runRedisDeduction.ts b/server/src/internal/balances/track/redisTrackUtils/runRedisDeduction.ts index c88c74fda..10b7069f2 100644 --- a/server/src/internal/balances/track/redisTrackUtils/runRedisDeduction.ts +++ b/server/src/internal/balances/track/redisTrackUtils/runRedisDeduction.ts @@ -47,10 +47,9 @@ export const runRedisDeduction = async ({ const { org, env } = ctx; // Ensure customer is in cache - const cachedCustomer = await getOrCreateApiCustomer({ + const { apiCustomer: cachedCustomer } = await getOrCreateApiCustomer({ ctx, customerId, - withAutumnId: true, }); // Map feature deductions to the format expected by batching manager @@ -82,14 +81,32 @@ export const runRedisDeduction = async ({ // Redis deduction successful: queue sync jobs and event insertion if (result.success) { + // Only queue sync pairs for scopes that were actually modified + // This prevents unnecessary syncs and race conditions for (const deduction of featureDeductions) { - globalSyncBatchingManager.addSyncPair({ - customerId: customerId, - featureId: deduction.feature.id, - orgId: org.id, - env, - entityId: entityId, - }); + // If customer was changed, queue customer-level sync + if (result.customerChanged) { + globalSyncBatchingManager.addSyncPair({ + customerId: customerId, + featureId: deduction.feature.id, + orgId: org.id, + env, + entityId: undefined, // Customer-level sync + }); + } + + // For each changed entity, queue entity-level sync + if (result.changedEntityIds && result.changedEntityIds.length > 0) { + for (const changedEntityId of result.changedEntityIds) { + globalSyncBatchingManager.addSyncPair({ + customerId: customerId, + featureId: deduction.feature.id, + orgId: org.id, + env, + entityId: changedEntityId, + }); + } + } } // Queue event insertion (skip if skip_event is true) diff --git a/server/src/internal/balances/track/syncUtils/SyncBatchingManager.ts b/server/src/internal/balances/track/syncUtils/SyncBatchingManager.ts index e55f2c4fe..6ebe07df6 100644 --- a/server/src/internal/balances/track/syncUtils/SyncBatchingManager.ts +++ b/server/src/internal/balances/track/syncUtils/SyncBatchingManager.ts @@ -1,3 +1,4 @@ +import type { AppEnv } from "@autumn/shared"; import { JobName } from "@/queue/JobName.js"; import { addTaskToQueue } from "@/queue/queueUtils.js"; @@ -5,7 +6,7 @@ interface SyncPairContext { customerId: string; featureId: string; orgId: string; - env: string; + env: AppEnv; entityId?: string; timestamp: number; } @@ -125,6 +126,8 @@ export class SyncBatchingManager { await addTaskToQueue({ jobName: JobName.SyncBalanceBatch, payload: { + orgId: items?.[0]?.orgId, + env: items?.[0]?.env, items, }, messageGroupId: customerId, diff --git a/server/src/internal/balances/track/syncUtils/runSyncBalanceBatch.ts b/server/src/internal/balances/track/syncUtils/runSyncBalanceBatch.ts index 720c0899e..00af82b1b 100644 --- a/server/src/internal/balances/track/syncUtils/runSyncBalanceBatch.ts +++ b/server/src/internal/balances/track/syncUtils/runSyncBalanceBatch.ts @@ -1,8 +1,4 @@ -import type { AppEnv } from "@autumn/shared"; -import type { Logger } from "pino"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { OrgService } from "@/internal/orgs/OrgService.js"; -import { createWorkerContext } from "@/queue/createWorkerContext.js"; +import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; import { type SyncItem, syncItem } from "./syncItem.js"; interface SyncBatchPayload { @@ -14,56 +10,33 @@ interface SyncBatchPayload { * Groups items by org to minimize DB queries and optimize transactions */ export const runSyncBalanceBatch = async ({ - db, + ctx, payload, - logger, }: { - db: DrizzleCli; + ctx?: AutumnContext; payload: SyncBatchPayload; - logger: Logger; }) => { const { items } = payload; - if (!items || items.length === 0) return; + if (!items || !ctx || items.length === 0) return; + + const { logger } = ctx; // All items belong to the same customer (grouped by messageGroupId in SQS) const firstItem = items[0]; - const { orgId, env, customerId } = firstItem; - - // Fetch org with features once for all items - const orgData = await OrgService.getWithFeatures({ - db, - orgId, - env: env as AppEnv, - }); - - if (!orgData) { - logger.error(`Organization not found: ${orgId}, env: ${env}`); - return; - } - - // Create worker context once - const ctx = createWorkerContext({ - db, - org: orgData.org, - env: env as AppEnv, - features: orgData.features, - logger, - }); + const { customerId } = firstItem; // Sort items by timestamp (oldest first) to maintain chronological order const sortedItems = items.sort((a, b) => a.timestamp - b.timestamp); // Process each item sequentially for this customer let successCount = 0; - let errorCount = 0; for (const item of sortedItems) { try { await syncItem({ item, ctx }); successCount++; } catch (error) { - errorCount++; logger.error( `❌ Failed to sync item ${item.customerId}:${item.featureId}: ${error instanceof Error ? error.message : String(error)}`, ); diff --git a/server/src/internal/balances/track/syncUtils/syncItem.ts b/server/src/internal/balances/track/syncUtils/syncItem.ts index 7675635bb..4a0812761 100644 --- a/server/src/internal/balances/track/syncUtils/syncItem.ts +++ b/server/src/internal/balances/track/syncUtils/syncItem.ts @@ -1,8 +1,11 @@ import { type ApiCustomer, type ApiEntity, + filterEntityLevelCusProducts, + filterOutEntitiesFromCusProducts, getRelevantFeatures, } from "@autumn/shared"; +import chalk from "chalk"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { CusService } from "@/internal/customers/CusService.js"; import { RELEVANT_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js"; @@ -34,19 +37,22 @@ export const syncItem = async ({ const { customerId, featureId, entityId } = item; const { db, org, env } = ctx; - // Get cached customer from Redis + // Get cached customer/entity from Redis WITHOUT merging + // For sync, we need the raw balance for that specific scope (not merged) let redisEntity: ApiCustomer | ApiEntity; if (entityId) { const { apiEntity } = await getCachedApiEntity({ ctx, customerId, entityId, + skipCustomerMerge: true, // Don't merge with customer - we want entity's own balance }); redisEntity = apiEntity; } else { const { apiCustomer } = await getCachedApiCustomer({ ctx, customerId, + skipEntityMerge: true, // Don't merge with entities - we want customer's own balance }); redisEntity = apiCustomer; } @@ -63,6 +69,18 @@ export const syncItem = async ({ entityId, }); + // If entityId provided, deduct entity level cusEnts + if (entityId) { + fullCus.customer_products = filterEntityLevelCusProducts({ + cusProducts: fullCus.customer_products, + }); + } else { + // If entityId NOT provided, JUST deduct customer level cusEnts + fullCus.customer_products = filterOutEntitiesFromCusProducts({ + cusProducts: fullCus.customer_products, + }); + } + const relevantFeatures = getRelevantFeatures({ features: ctx.features, featureId, @@ -81,7 +99,7 @@ export const syncItem = async ({ // Sync from Redis to Postgres - deduct using target balance - await deductFromCusEnts({ + const result = await deductFromCusEnts({ ctx, customerId, entityId, @@ -94,9 +112,12 @@ export const syncItem = async ({ // console.log(logText); // ctx.logger.info(logText); ctx.logger.info( - `[SYNC COMPLETE] customer ${customerId}, feature ${featureId}, target: ${featureDeductions?.[0]?.targetBalance}`, + `[SYNC COMPLETE] (${customerId}${entityId ? `, ${entityId}` : ""}) feature ${featureId}, target: ${chalk.yellow(featureDeductions?.[0]?.targetBalance)}`, ); - ctx.logger.info(`[SYNC COMPLETE] org: ${org.slug}, env: ${env}`); + ctx.logger.info( + `[SYNC COMPLETE], actual deducted: ${chalk.yellow(result.actualDeductions[featureId])}`, + ); + if (process.env.NODE_ENV === "production") { console.log(`synced customer ${customerId}, feature ${featureId}`); console.log(`org: ${org.slug}, env: ${env}`); diff --git a/server/src/internal/balances/track/trackUtils/runDeductionTx.ts b/server/src/internal/balances/track/trackUtils/runDeductionTx.ts index 79eb5e2b6..b38acc0de 100644 --- a/server/src/internal/balances/track/trackUtils/runDeductionTx.ts +++ b/server/src/internal/balances/track/trackUtils/runDeductionTx.ts @@ -14,6 +14,7 @@ import { nullish, updateCusEntInFullCus, } from "@autumn/shared"; +import chalk from "chalk"; import { sql } from "drizzle-orm"; import type { DrizzleCli } from "../../../../db/initDrizzle.js"; import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; @@ -26,6 +27,7 @@ import { getUnlimitedAndUsageAllowed, } from "../../../customers/cusProducts/cusEnts/cusEntUtils.js"; import { getCreditCost } from "../../../features/creditSystemUtils.js"; +import { isPaidContinuousUse } from "../../../features/featureUtils.js"; import { constructEvent, type EventInfo } from "./eventUtils.js"; import type { FeatureDeduction } from "./getFeatureDeductions.js"; @@ -41,6 +43,10 @@ export type DeductionTxParams = { refreshCache?: boolean; // Whether to refresh Redis cache after deduction (default: true for track, false for sync) }; +export type ActualDeductions = { + [featureId: string]: number; // Actual amount deducted from Postgres +}; + export const deductFromCusEnts = async ({ ctx, customerId, @@ -49,7 +55,10 @@ export const deductFromCusEnts = async ({ overageBehaviour = "cap", addToAdjustment = false, fullCus, -}: DeductionTxParams) => { +}: DeductionTxParams): Promise<{ + fullCus: FullCustomer | undefined; + actualDeductions: ActualDeductions; +}> => { const { db, org, env } = ctx; if (!fullCus) { @@ -75,6 +84,21 @@ export const deductFromCusEnts = async ({ })), ); } + + const isPaidAllocated = deductions.some((d) => + isPaidContinuousUse({ + feature: d.feature, + fullCus, + }), + ); + + console.log(`Is paid allocated: ${isPaidAllocated}`); + + if (isPaidAllocated) overageBehaviour = "reject"; + + // Track actual deductions per feature + const actualDeductions: ActualDeductions = {}; + // Need to deduct from customer entitlement... for (const deduction of deductions) { const { feature, deduction: toDeduct, targetBalance } = deduction; @@ -84,6 +108,10 @@ export const deductFromCusEnts = async ({ featureId: feature.id, }); + if (printLogs) { + console.log(`Entity Mode: ${entityId ? "Yes" : "No"}`); + } + const cusEnts = cusProductsToCusEnts({ cusProducts: fullCus.customer_products, featureIds: relevantFeatures.map((f) => f.id), @@ -91,6 +119,17 @@ export const deductFromCusEnts = async ({ entity: fullCus.entity, }); + if (printLogs) { + console.log( + `Cus Ents: `, + cusEnts.map((ce) => ({ + balance: ce.balance, + entity_id: ce.customer_product.entity_id, + cus_ent_id: ce.id, + })), + ); + } + const { unlimited } = getUnlimitedAndUsageAllowed({ cusEnts, internalFeatureId: feature.internal_id!, @@ -121,8 +160,6 @@ export const deductFromCusEnts = async ({ }; }); - // console.log("Cus ent input", cusEntInput); - // Collect and sort rollovers by expires_at (oldest first) const sortedRollovers = cusEnts .flatMap((ce) => ce.rollovers || []) @@ -164,6 +201,11 @@ export const deductFromCusEnts = async ({ remaining: number; }; + // log updates + if (printLogs) { + console.log(`Updates: `, resultJson.updates); + } + if (!resultJson) { throw new InternalError({ message: "Failed to deduct from entitlements", @@ -179,13 +221,17 @@ export const deductFromCusEnts = async ({ }); } + // Calculate total deducted from the updates (sum of all deducted amounts) + const totalDeducted = Object.values(updates).reduce( + (sum, update) => sum + update.deducted, + 0, + ); + + // Store actual deduction for this feature + actualDeductions[feature.id] = totalDeducted; + // Log deduction details if (targetBalance !== undefined) { - // Calculate total deducted from the updates (sum of all deducted amounts) - const totalDeducted = Object.values(updates).reduce( - (sum, update) => sum + update.deducted, - 0, - ); const entityInfo = entityId ? `Entity: ${entityId}` : "Entity: customer-level"; @@ -200,12 +246,10 @@ export const deductFromCusEnts = async ({ }); } else { ctx.logger.info( - `[Track] Deducted ${toDeduct - remaining} from feature ${feature.id}. Updated ${ + `[Track] Deducted ${totalDeducted} from feature ${feature.id}. Updated ${ Object.keys(updates).length } entitlements. Remaining: ${remaining}`, ); - - // Log cus ent ids: } // Bill on Stripe for each updated entitlement @@ -248,10 +292,14 @@ export const deductFromCusEnts = async ({ // Adjust balance based on replaceables let reUpdatedBalance = update.balance; + let replaceableAdjustment = 0; + if (newReplaceables && newReplaceables.length > 0) { reUpdatedBalance = reUpdatedBalance - newReplaceables.length; + replaceableAdjustment = newReplaceables.length; } else if (deletedReplaceables && deletedReplaceables.length > 0) { reUpdatedBalance = reUpdatedBalance + deletedReplaceables.length; + replaceableAdjustment = -deletedReplaceables.length; } if (reUpdatedBalance !== update.balance) { @@ -262,6 +310,10 @@ export const deductFromCusEnts = async ({ balance: reUpdatedBalance, }, }); + + // Adjust the actual deduction to reflect replaceables + actualDeductions[feature.id] = + (actualDeductions[feature.id] || 0) + replaceableAdjustment; } updateCusEntInFullCus({ @@ -272,7 +324,10 @@ export const deductFromCusEnts = async ({ } } - return fullCus; + return { + fullCus, + actualDeductions, + }; }; export const runDeductionTx = async ( @@ -280,12 +335,14 @@ export const runDeductionTx = async ( ): Promise<{ fullCus: FullCustomer | undefined; event: Event | undefined; + actualDeductions: ActualDeductions; }> => { const ctx = params.ctx; - const { db } = ctx; + const { db, logger } = ctx; let fullCus: FullCustomer | undefined; let event: Event | undefined; + let actualDeductions: ActualDeductions = {}; await db.transaction( async (tx) => { @@ -298,12 +355,14 @@ export const runDeductionTx = async ( }, }; - fullCus = await deductFromCusEnts(txParams); + const result = await deductFromCusEnts(txParams); + fullCus = result.fullCus; + actualDeductions = result.actualDeductions; if (!fullCus) return; if (params.eventInfo) { - const newEvent = await constructEvent({ + const newEvent = constructEvent({ ctx: txParams.ctx, eventInfo: params.eventInfo, internalCustomerId: fullCus.internal_id, @@ -317,52 +376,51 @@ export const runDeductionTx = async ( event: newEvent, }); } + + if (params?.refreshCache && fullCus) { + // Deduct the actual amounts from Redis cache (if exists) + // This prevents race conditions by directly deducting the exact Postgres amount + const { deductFromCache } = await import( + "../redisTrackUtils/deductFromCache.js" + ); + + const printLogs = true; + + for (const [featureId, deductedAmount] of Object.entries( + actualDeductions, + )) { + if (deductedAmount !== 0) { + // Only deduct if something was actually deducted + await deductFromCache({ + ctx, + customerId: fullCus.id ?? "", + featureId, + amount: deductedAmount, + entityId: params.entityId, + }); + + if (printLogs) { + logger.info( + `[REDIS] Deduced users from cache: ${chalk.yellow(actualDeductions.users)}`, + ); + // logger.info( + // `[REDIS] balance after deduction for ${featureId}: ${chalk.yellow(balance)}`, + // ); + } + } + } + } }, { isolationLevel: "read committed", }, ); - // Sync cache if requested (default: true for track, false for sync) - if (params?.refreshCache && fullCus) { - // Sync Redis cache for each affected feature - // This prevents race conditions with concurrent Redis track operations - const { syncCacheBalance } = await import( - "../redisTrackUtils/syncCacheBalance.js" - ); - - for (const deduction of params.deductions) { - const feature = deduction.feature; - - // Find the customer entitlement for this feature to get the new balance - const cusEnts = cusProductsToCusEnts({ - cusProducts: fullCus.customer_products, - featureIds: [feature.id], - reverseOrder: false, - entity: fullCus.entity, - }); - - if (cusEnts.length > 0) { - // Calculate total balance across all entitlements for this feature - const totalBalance = cusEnts.reduce( - (sum, ce) => sum + (ce.balance ?? 0), - 0, - ); - - // Sync cache to match Postgres balance - await syncCacheBalance({ - ctx, - customerId: fullCus.id ?? "", - featureId: feature.id, - targetBalance: totalBalance, - entityId: params.entityId, - }); - } - } - } + // Deduct from Redis cache if requested (default: true for track, false for sync) return { fullCus, event, + actualDeductions, }; }; diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts index 94e4bb262..979e1c62b 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts @@ -1,27 +1,26 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; - +import { + type BillingInterval, + BillingType, + CusProductStatus, + cusProductsToCusEnts, + cusProductsToCusPrices, + type FullCusProduct, + intervalsDifferent, + type UsagePriceConfig, +} from "@autumn/shared"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { subToAutumnInterval } from "@/external/stripe/utils.js"; -import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; import { getCusPriceUsage, getRelatedCusEnt, } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js"; -import { cusProductsToCusEnts, cusProductsToCusPrices } from "@autumn/shared"; import { formatPrice, getBillingType, } from "@/internal/products/prices/priceUtils.js"; -import { - FullCusProduct, - UsagePriceConfig, - BillingType, - BillingInterval, - intervalsDifferent, - CusProductStatus, -} from "@autumn/shared"; - -import Stripe from "stripe"; export const getUsageInvoiceItems = async ({ db, @@ -93,7 +92,7 @@ export const getUsageInvoiceItems = async ({ cusEntIds.push(cusEnt.id); - let invoiceItem = { + const invoiceItem = { description, price_data: { product: config.stripe_product_id!, @@ -194,7 +193,7 @@ export const resetUsageBalances = async ({ }, }); - let index = cusProduct.customer_entitlements.findIndex( + const index = cusProduct.customer_entitlements.findIndex( (ce) => ce.id === cusEntId, ); diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts index 76a5d4d0e..c42f9650f 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts @@ -1,7 +1,6 @@ import { type AttachBody, CusProductStatus, - type CustomerData, ErrCode, nullish, } from "@autumn/shared"; @@ -80,7 +79,9 @@ export const getCustomerAndProducts = async ({ getOrCreateCustomer({ req, customerId: attachBody.customer_id, - customerData: attachBody.customer_data as CustomerData, + customerData: { + ...attachBody.customer_data, + }, inStatuses: [ CusProductStatus.Active, CusProductStatus.Scheduled, diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts index 9930b8fc9..c70b5f601 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts @@ -102,7 +102,7 @@ export const getExistingUsages = ({ const ent = cusEnt.entitlement; const key = `${ent.feature_id}-${ent.interval}-${ent.interval_count || 1}`; const feature = ent.feature; - if (feature.type == FeatureType.Boolean) continue; + if (feature.type === FeatureType.Boolean) continue; const { unlimited, usageAllowed } = getUnlimitedAndUsageAllowed({ cusEnts: curCusProduct.customer_entitlements, @@ -223,7 +223,7 @@ export const addExistingUsagesToCusEnts = ({ const fromEntities = existingUsages[key].fromEntities; // if (cusEntKey !== key) continue; - const isSameFeature = cusEnt.feature_id == feature_id; + const isSameFeature = cusEnt.feature_id === feature_id; if (!isSameFeature) continue; diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/loadCusFeatures.lua b/server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/loadCusFeatures.lua deleted file mode 100644 index ad3c1147a..000000000 --- a/server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/loadCusFeatures.lua +++ /dev/null @@ -1,374 +0,0 @@ --- loadCusFeatures.lua --- Shared function to load customer features with merged balances (customer + entities) --- Returns: { [featureId] = { balance, usage, unlimited, ... } } or nil if not in cache - --- Helper function to safely convert values to numbers for arithmetic -local function toNum(value) - return type(value) == "number" and value or 0 -end - --- Load customer features with merged entity balances --- Parameters: cacheKey, orgId, env, customerId --- Returns: merged features table or nil -local function loadCusFeatures(cacheKey, orgId, env, customerId) - -- Get base customer JSON - local baseJson = redis.call("GET", cacheKey) - if not baseJson then - return nil - end - - local baseCustomer = cjson.decode(baseJson) - local featureIds = baseCustomer._featureIds or {} - local entityIds = baseCustomer._entityIds or {} - - -- Build features object - local features = {} - -for _, featureId in ipairs(featureIds) do - local featureKey = cacheKey .. ":features:" .. featureId - local featureHash = redis.call("HGETALL", featureKey) - - -- If feature key is missing, return nil (partial eviction detected) - if #featureHash == 0 then - return nil - end - - -- Convert HGETALL result (flat array) to table - local featureData = {} - for i = 1, #featureHash, 2 do - local key = featureHash[i] - local value = featureHash[i + 1] - - -- Check for null first before parsing - if value == "null" then - featureData[key] = cjson.null - elseif key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" or key == "_breakdown_count" or key == "_rollover_count" then - featureData[key] = tonumber(value) - elseif key == "unlimited" or key == "overage_allowed" then - featureData[key] = (value == "true") - elseif key == "credit_schema" then - -- Parse credit_schema JSON array - if value ~= "" then - featureData[key] = cjson.decode(value) - else - featureData[key] = cjson.null - end - else - featureData[key] = value - end - end - - -- Get rollover count - local rolloverCount = featureData._rollover_count or 0 - featureData._rollover_count = nil -- Remove from final output - - -- Fetch rollover items - local rollovers = {} - for i = 0, rolloverCount - 1 do - local rolloverKey = cacheKey .. ":features:" .. featureId .. ":rollover:" .. i - local rolloverHash = redis.call("HGETALL", rolloverKey) - - -- If rollover key is missing, return nil (partial eviction detected) - if #rolloverHash == 0 then - return nil - end - - local rolloverData = {} - for j = 1, #rolloverHash, 2 do - local key = rolloverHash[j] - local value = rolloverHash[j + 1] - - if value == "null" then - rolloverData[key] = cjson.null - elseif key == "balance" or key == "expires_at" then - rolloverData[key] = tonumber(value) - else - rolloverData[key] = value - end - end - table.insert(rollovers, rolloverData) - end - - if #rollovers > 0 then - featureData.rollovers = rollovers - end - - -- Get breakdown count - local breakdownCount = featureData._breakdown_count or 0 - featureData._breakdown_count = nil -- Remove from final output - - -- Fetch breakdown items - local breakdown = {} - for i = 0, breakdownCount - 1 do - local breakdownKey = cacheKey .. ":features:" .. featureId .. ":breakdown:" .. i - local breakdownHash = redis.call("HGETALL", breakdownKey) - - -- If breakdown key is missing, return nil (partial eviction detected) - if #breakdownHash == 0 then - return nil - end - - local breakdownData = {} - for j = 1, #breakdownHash, 2 do - local key = breakdownHash[j] - local value = breakdownHash[j + 1] - - if value == "null" then - breakdownData[key] = cjson.null - elseif key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" then - breakdownData[key] = tonumber(value) - elseif key == "overage_allowed" then - breakdownData[key] = (value == "true") - else - breakdownData[key] = value - end - end - table.insert(breakdown, breakdownData) - end - - if #breakdown > 0 then - featureData.breakdown = breakdown - end - - features[featureId] = featureData -end - --- ============================================================================ --- FETCH AND MERGE ENTITY FEATURES --- ============================================================================ - --- Fetch all entity features and aggregate balances -local entityFeatureData = {} -- {[entityId][featureId] = featureData} -local entityBaseData = {} -- {[entityId] = entityBase} - Store entity base for product access - -for _, entityId in ipairs(entityIds) do - local entityCacheKey = "{" .. orgId .. "}:" .. env .. ":customer:" .. customerId .. ":entity:" .. entityId - local entityBaseJson = redis.call("GET", entityCacheKey) - - if entityBaseJson then - local entityBase = cjson.decode(entityBaseJson) - entityBaseData[entityId] = entityBase -- Store entity base for product access - local entityFeatureIds = entityBase._featureIds or {} - entityFeatureData[entityId] = {} - - for _, featureId in ipairs(entityFeatureIds) do - local entityFeatureKey = entityCacheKey .. ":features:" .. featureId - local entityFeatureHash = redis.call("HGETALL", entityFeatureKey) - - if #entityFeatureHash > 0 then - -- Parse entity feature - local entityFeature = {} - for i = 1, #entityFeatureHash, 2 do - local key = entityFeatureHash[i] - local value = entityFeatureHash[i + 1] - - if value == "null" then - entityFeature[key] = cjson.null - elseif key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" or key == "_breakdown_count" or key == "_rollover_count" then - entityFeature[key] = tonumber(value) - elseif key == "unlimited" or key == "overage_allowed" then - entityFeature[key] = (value == "true") - else - entityFeature[key] = value - end - end - - -- Fetch breakdown items for this entity feature - local breakdownCount = entityFeature._breakdown_count or 0 - entityFeature._breakdown_count = nil - entityFeature.breakdowns = {} - - for i = 0, breakdownCount - 1 do - local breakdownKey = entityFeatureKey .. ":breakdown:" .. i - local breakdownHash = redis.call("HGETALL", breakdownKey) - - if #breakdownHash > 0 then - local breakdownData = {} - for j = 1, #breakdownHash, 2 do - local key = breakdownHash[j] - local value = breakdownHash[j + 1] - - if value == "null" then - breakdownData[key] = cjson.null - elseif key == "balance" or key == "usage" or key == "included_usage" or key == "usage_limit" or key == "interval_count" or key == "next_reset_at" then - breakdownData[key] = tonumber(value) - elseif key == "overage_allowed" then - breakdownData[key] = (value == "true") - else - breakdownData[key] = value - end - end - table.insert(entityFeature.breakdowns, breakdownData) - end - end - - -- Fetch rollover items for this entity feature - local rolloverCount = entityFeature._rollover_count or 0 - entityFeature._rollover_count = nil - entityFeature.rollovers = {} - - for i = 0, rolloverCount - 1 do - local rolloverKey = entityFeatureKey .. ":rollover:" .. i - local rolloverHash = redis.call("HGETALL", rolloverKey) - - if #rolloverHash > 0 then - local rolloverData = {} - for j = 1, #rolloverHash, 2 do - local key = rolloverHash[j] - local value = rolloverHash[j + 1] - - if value == "null" then - rolloverData[key] = cjson.null - elseif key == "balance" or key == "expires_at" then - rolloverData[key] = tonumber(value) - else - rolloverData[key] = value - end - end - table.insert(entityFeature.rollovers, rolloverData) - end - end - - entityFeatureData[entityId][featureId] = entityFeature - end - end - end -end - - - --- ============================================================================ --- MERGE ENTITY BALANCES INTO CUSTOMER FEATURES --- ============================================================================ - -for featureId, customerFeature in pairs(features) do - -- Skip if unlimited - if not customerFeature.unlimited then - -- Aggregate entity balances for this feature - local entityTotalBalance = 0 - local entityTotalUsage = 0 - local entityTotalIncludedUsage = 0 - local entityTotalUsageLimit = 0 - - for entityId, entityFeatures in pairs(entityFeatureData) do - local entityFeature = entityFeatures[featureId] - if entityFeature then - entityTotalBalance = entityTotalBalance + toNum(entityFeature.balance) - entityTotalUsage = entityTotalUsage + toNum(entityFeature.usage) - entityTotalIncludedUsage = entityTotalIncludedUsage + toNum(entityFeature.included_usage) - entityTotalUsageLimit = entityTotalUsageLimit + toNum(entityFeature.usage_limit) - end - end - - -- Merge top-level balance and usage - customerFeature.balance = toNum(customerFeature.balance) + entityTotalBalance - customerFeature.usage = toNum(customerFeature.usage) + entityTotalUsage - customerFeature.included_usage = toNum(customerFeature.included_usage) + entityTotalIncludedUsage - customerFeature.usage_limit = toNum(customerFeature.usage_limit) + entityTotalUsageLimit - - -- Merge breakdown balances and usage - if customerFeature.breakdown and #customerFeature.breakdown > 0 then - for i, breakdown in ipairs(customerFeature.breakdown) do - local entityBreakdownBalance = 0 - local entityBreakdownUsage = 0 - local entityBreakdownIncludedUsage = 0 - local entityBreakdownUsageLimit = 0 - - for entityId, entityFeatures in pairs(entityFeatureData) do - local entityFeature = entityFeatures[featureId] - if entityFeature and entityFeature.breakdowns and entityFeature.breakdowns[i] then - entityBreakdownBalance = entityBreakdownBalance + toNum(entityFeature.breakdowns[i].balance) - entityBreakdownUsage = entityBreakdownUsage + toNum(entityFeature.breakdowns[i].usage) - entityBreakdownIncludedUsage = entityBreakdownIncludedUsage + toNum(entityFeature.breakdowns[i].included_usage) - entityBreakdownUsageLimit = entityBreakdownUsageLimit + toNum(entityFeature.breakdowns[i].usage_limit) - end - end - - breakdown.balance = toNum(breakdown.balance) + entityBreakdownBalance - breakdown.usage = toNum(breakdown.usage) + entityBreakdownUsage - breakdown.included_usage = toNum(breakdown.included_usage) + entityBreakdownIncludedUsage - breakdown.usage_limit = toNum(breakdown.usage_limit) + entityBreakdownUsageLimit - end - end - - -- Merge rollover balances - if customerFeature.rollovers and #customerFeature.rollovers > 0 then - for i, rollover in ipairs(customerFeature.rollovers) do - local entityRolloverBalance = 0 - - for entityId, entityFeatures in pairs(entityFeatureData) do - local entityFeature = entityFeatures[featureId] - if entityFeature and entityFeature.rollovers and entityFeature.rollovers[i] then - entityRolloverBalance = entityRolloverBalance + toNum(entityFeature.rollovers[i].balance) - end - end - - rollover.balance = toNum(rollover.balance) + entityRolloverBalance - end - end - end -end - --- Add entity-only features (features that exist in entities but not in customer) -for entityId, entityFeatures in pairs(entityFeatureData) do - for featureId, entityFeature in pairs(entityFeatures) do - if not features[featureId] then - -- This feature doesn't exist in customer, add it - -- Initialize with zero balance, then we'll aggregate all entity balances - features[featureId] = { - id = entityFeature.id, - type = entityFeature.type, - name = entityFeature.name, - interval = entityFeature.interval, - interval_count = entityFeature.interval_count, - unlimited = entityFeature.unlimited, - balance = 0, - usage = 0, - included_usage = 0, - next_reset_at = cjson.null, - overage_allowed = entityFeature.overage_allowed, - usage_limit = entityFeature.usage_limit, - credit_schema = entityFeature.credit_schema - } - end - end -end - --- Now aggregate balances for entity-only features -for featureId, customerFeature in pairs(features) do - -- Only process if this was an entity-only feature (balance is still 0 from initialization) - if customerFeature.balance == 0 and customerFeature.usage == 0 then - local entityTotalBalance = 0 - local entityTotalUsage = 0 - local entityTotalIncludedUsage = 0 - local entityTotalUsageLimit = 0 - local minNextResetAt = nil - - for entityId, entityFeatures in pairs(entityFeatureData) do - local entityFeature = entityFeatures[featureId] - if entityFeature then - entityTotalBalance = entityTotalBalance + toNum(entityFeature.balance) - entityTotalUsage = entityTotalUsage + toNum(entityFeature.usage) - entityTotalIncludedUsage = entityTotalIncludedUsage + toNum(entityFeature.included_usage) - entityTotalUsageLimit = entityTotalUsageLimit + toNum(entityFeature.usage_limit) - - -- Find minimum next_reset_at across all entities - if type(entityFeature.next_reset_at) == "number" then - if not minNextResetAt or entityFeature.next_reset_at < minNextResetAt then - minNextResetAt = entityFeature.next_reset_at - end - end - end - end - - customerFeature.balance = entityTotalBalance - customerFeature.usage = entityTotalUsage - customerFeature.included_usage = entityTotalIncludedUsage - customerFeature.usage_limit = entityTotalUsageLimit - customerFeature.next_reset_at = minNextResetAt or cjson.null - end -end - --- Return merged features -return features -end \ No newline at end of file diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/luaScripts.ts b/server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/luaScripts.ts deleted file mode 100644 index 0d934557d..000000000 --- a/server/src/internal/customers/cusUtils/apiCusCacheUtils/cusLuaScripts/luaScripts.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Load shared validation function -const CHECK_CACHE_EXISTS = readFileSync( - join(__dirname, "checkCacheExists.lua"), - "utf-8", -); - -// Load shared feature loading function -const LOAD_CUS_FEATURES = readFileSync( - join(__dirname, "loadCusFeatures.lua"), - "utf-8", -); - -// Load Lua scripts at module initialization -// Prepend loadCusFeatures to GET_CUSTOMER_SCRIPT so it can use the function -const getCustomerScript = readFileSync( - join(__dirname, "getCustomer.lua"), - "utf-8", -); -export const GET_CUSTOMER_SCRIPT = `${LOAD_CUS_FEATURES}\n${getCustomerScript}`; - -// Prepend validation function to SET_CUSTOMER_SCRIPT -const setCustomerScript = readFileSync( - join(__dirname, "setCustomer.lua"), - "utf-8", -); -export const SET_CUSTOMER_SCRIPT = `${CHECK_CACHE_EXISTS}\n${setCustomerScript}`; - -export const SET_CUSTOMER_PRODUCTS_SCRIPT = readFileSync( - join(__dirname, "setCustomerProducts.lua"), - "utf-8", -); - -export const SET_CUSTOMER_DETAILS_SCRIPT = readFileSync( - join(__dirname, "setCustomerDetails.lua"), - "utf-8", -); diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.ts b/server/src/internal/customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.ts index b9d61369e..d1a22c259 100644 --- a/server/src/internal/customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.ts +++ b/server/src/internal/customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.ts @@ -1,14 +1,8 @@ -import { readFileSync } from "node:fs"; -import { join } from "node:path"; +import { DELETE_CUSTOMER_SCRIPT } from "@lua/luaScripts.js"; import { redis } from "@/external/redis/initRedis.js"; import { logger } from "../../../../external/logtail/logtailUtils.js"; import { buildCachedApiCustomerKey } from "./getCachedApiCustomer.js"; -const DELETE_CUSTOMER_SCRIPT = readFileSync( - join(import.meta.dir, "cusLuaScripts", "deleteCustomer.lua"), - "utf-8", -); - /** * Delete all cached ApiCustomer data from Redis * This includes the base customer key and all related feature/breakdown/rollover keys diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/getCachedApiCustomer.ts b/server/src/internal/customers/cusUtils/apiCusCacheUtils/getCachedApiCustomer.ts index f0b69c0fc..f9f850e35 100644 --- a/server/src/internal/customers/cusUtils/apiCusCacheUtils/getCachedApiCustomer.ts +++ b/server/src/internal/customers/cusUtils/apiCusCacheUtils/getCachedApiCustomer.ts @@ -3,7 +3,9 @@ import { ApiCustomerSchema, type AppEnv, type CustomerLegacyData, + filterOutEntitiesFromCusProducts, } from "@autumn/shared"; +import { GET_CUSTOMER_SCRIPT } from "@lua/luaScripts.js"; import { redis } from "../../../../external/redis/initRedis.js"; import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; import { @@ -13,7 +15,6 @@ import { import { CusService } from "../../CusService.js"; import { RELEVANT_STATUSES } from "../../cusProducts/CusProductService.js"; import { getApiCustomerBase } from "../apiCusUtils/getApiCustomerBase.js"; -import { GET_CUSTOMER_SCRIPT } from "./cusLuaScripts/luaScripts.js"; import { setCachedApiCustomer } from "./setCachedApiCustomer.js"; export const buildCachedApiCustomerKey = ({ @@ -36,17 +37,17 @@ export const buildCachedApiCustomerKey = ({ export const getCachedApiCustomer = async ({ ctx, customerId, - withAutumnId = false, skipCache = false, + skipEntityMerge = false, source, }: { ctx: AutumnContext; customerId: string; - withAutumnId?: boolean; skipCache?: boolean; + skipEntityMerge?: boolean; // If true, returns only customer's own features (no entity merging) source?: string; }): Promise<{ apiCustomer: ApiCustomer; legacyData: CustomerLegacyData }> => { - const { org, env, db, logger } = ctx; + const { org, env, db } = ctx; const cacheKey = buildCachedApiCustomerKey({ customerId, @@ -57,7 +58,15 @@ export const getCachedApiCustomer = async ({ // Try to get from cache using Lua script (unless skipCache is true) if (!skipCache) { const cachedResult = await tryRedisRead(() => - redis.eval(GET_CUSTOMER_SCRIPT, 1, cacheKey, org.id, env, customerId), + redis.eval( + GET_CUSTOMER_SCRIPT, + 1, + cacheKey, + org.id, + env, + customerId, + skipEntityMerge ? "true" : "false", + ), ); if (cachedResult) { @@ -69,14 +78,9 @@ export const getCachedApiCustomer = async ({ const { legacyData, ...rest } = cached; - // logger.info(`Customer cache hit:`, rest.features); - return { // ← This returns from getCachedApiCustomer! - apiCustomer: ApiCustomerSchema.parse({ - ...rest, - autumn_id: withAutumnId ? rest.autumn_id : undefined, - }), + apiCustomer: ApiCustomerSchema.parse(rest), legacyData, }; } @@ -101,6 +105,17 @@ export const getCachedApiCustomer = async ({ withAutumnId: true, }); + const { apiCustomer: masterApiCustomer } = await getApiCustomerBase({ + ctx, + fullCus: { + ...fullCus, + customer_products: filterOutEntitiesFromCusProducts({ + cusProducts: fullCus.customer_products, + }), + }, + withAutumnId: true, + }); + // Store customer and entity caches (only if not skipping cache) if (!skipCache) { await setCachedApiCustomer({ @@ -112,7 +127,9 @@ export const getCachedApiCustomer = async ({ } return { - apiCustomer: ApiCustomerSchema.parse(apiCustomer), + apiCustomer: ApiCustomerSchema.parse( + skipEntityMerge ? masterApiCustomer : apiCustomer, + ), legacyData, }; }; diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCusDetails.ts b/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCusDetails.ts index 753aac6fb..96ffba8b5 100644 --- a/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCusDetails.ts +++ b/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCusDetails.ts @@ -1,8 +1,8 @@ import type { ApiCustomer, FullCustomer } from "@autumn/shared"; +import { SET_CUSTOMER_DETAILS_SCRIPT } from "@lua/luaScripts.js"; import { redis } from "../../../../external/redis/initRedis.js"; import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; import { tryRedisWrite } from "../../../../utils/cacheUtils/cacheUtils.js"; -import { SET_CUSTOMER_DETAILS_SCRIPT } from "./cusLuaScripts/luaScripts.js"; import { buildCachedApiCustomerKey } from "./getCachedApiCustomer.js"; /** diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCusProducts.ts b/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCusProducts.ts index 611514a35..971ce34c5 100644 --- a/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCusProducts.ts +++ b/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCusProducts.ts @@ -3,13 +3,15 @@ import { filterCusProductsByEntity, filterOutEntitiesFromCusProducts, } from "@autumn/shared"; +import { + SET_CUSTOMER_PRODUCTS_SCRIPT, + SET_ENTITY_PRODUCTS_SCRIPT, +} from "@lua/luaScripts.js"; import { redis } from "../../../../external/redis/initRedis.js"; import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; import { tryRedisWrite } from "../../../../utils/cacheUtils/cacheUtils.js"; -import { SET_ENTITY_PRODUCTS_SCRIPT } from "../../../entities/entityUtils/apiEntityCacheUtils/entityLuaScripts/luaScripts.js"; import { buildCachedApiEntityKey } from "../../../entities/entityUtils/apiEntityCacheUtils/getCachedApiEntity.js"; import { getApiCusProducts } from "../apiCusUtils/getApiCusProduct/getApiCusProducts.js"; -import { SET_CUSTOMER_PRODUCTS_SCRIPT } from "./cusLuaScripts/luaScripts.js"; import { buildCachedApiCustomerKey } from "./getCachedApiCustomer.js"; /** diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCustomer.ts b/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCustomer.ts index cbc8ecbd5..e7b21e400 100644 --- a/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCustomer.ts +++ b/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCustomer.ts @@ -4,13 +4,15 @@ import { filterEntityLevelCusProducts, filterOutEntitiesFromCusProducts, } from "@autumn/shared"; +import { + SET_CUSTOMER_SCRIPT, + SET_ENTITIES_BATCH_SCRIPT, +} from "@lua/luaScripts.js"; import { redis } from "../../../../external/redis/initRedis.js"; import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; import { tryRedisWrite } from "../../../../utils/cacheUtils/cacheUtils.js"; -import { SET_ENTITIES_BATCH_SCRIPT } from "../../../entities/entityUtils/apiEntityCacheUtils/entityLuaScripts/luaScripts.js"; import { getApiEntityBase } from "../../../entities/entityUtils/apiEntityUtils/getApiEntityBase.js"; import { getApiCustomerBase } from "../apiCusUtils/getApiCustomerBase.js"; -import { SET_CUSTOMER_SCRIPT } from "./cusLuaScripts/luaScripts.js"; import { buildCachedApiCustomerKey } from "./getCachedApiCustomer.js"; /** diff --git a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomer.ts b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomer.ts index 6529d164d..3f5d4ebba 100644 --- a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomer.ts +++ b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomer.ts @@ -20,6 +20,7 @@ export const getApiCustomer = async ({ customerId, fullCus, skipCache = false, + baseData, }: { ctx: RequestContext; expand: CusExpand[]; @@ -27,23 +28,30 @@ export const getApiCustomer = async ({ customerId?: string; fullCus?: FullCustomer; skipCache?: boolean; + baseData?: { apiCustomer: ApiCustomer; legacyData: CustomerLegacyData }; }) => { - // Get base customer (cacheable or direct from DB) - // await redis.del( - // buildCachedApiCustomerKey({ - // customerId: customerId || "", - // orgId: ctx.org.id, - // env: ctx.env, - // }), - // ); + let baseCustomer: ApiCustomer; + let cusLegacyData: CustomerLegacyData; - const { apiCustomer: baseCustomer, legacyData: cusLegacyData } = - await getCachedApiCustomer({ + if (!baseData) { + const { apiCustomer, legacyData } = await getCachedApiCustomer({ ctx, customerId: customerId || "", - withAutumnId, skipCache, }); + baseCustomer = apiCustomer; + cusLegacyData = legacyData; + } else { + baseCustomer = baseData.apiCustomer; + cusLegacyData = baseData.legacyData; + } + + // Clean api customer + baseCustomer = { + ...baseCustomer, + entities: undefined, + autumn_id: withAutumnId ? baseCustomer.autumn_id : undefined, + }; // Get expand fields (not cacheable) const apiCusExpand = await getApiCustomerExpand({ diff --git a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerBase.ts b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerBase.ts index 2015ba488..19bbd9c89 100644 --- a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerBase.ts +++ b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerBase.ts @@ -12,11 +12,12 @@ import { getApiCusProducts } from "./getApiCusProduct/getApiCusProducts.js"; /** * Get base ApiCustomer without expand fields * This is the core customer object that can be cached + * By default, it includes the autumn_id */ export const getApiCustomerBase = async ({ ctx, fullCus, - withAutumnId = false, + withAutumnId = true, }: { ctx: RequestContext; fullCus: FullCustomer; diff --git a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerExpand.ts b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerExpand.ts index a4afc1233..106562f9a 100644 --- a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerExpand.ts +++ b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerExpand.ts @@ -35,6 +35,8 @@ export const getApiCustomerExpand = async ({ orgId: org.id, env, expand, + withEntities: expand.includes(CusExpand.Entities), + withSubs: true, }); } diff --git a/server/src/internal/customers/cusUtils/getOrCreateApiCustomer.ts b/server/src/internal/customers/cusUtils/getOrCreateApiCustomer.ts index 5dc4a346f..6bfac3177 100644 --- a/server/src/internal/customers/cusUtils/getOrCreateApiCustomer.ts +++ b/server/src/internal/customers/cusUtils/getOrCreateApiCustomer.ts @@ -14,18 +14,16 @@ export const getOrCreateApiCustomer = async ({ ctx, customerId, customerData, - withAutumnId = false, }: { ctx: AutumnContext; customerId: string | null; customerData?: CustomerData; - withAutumnId?: boolean; -}): Promise => { +}): Promise<{ apiCustomer: ApiCustomer; legacyData?: CustomerLegacyData }> => { // ======================================== // Phase 1: Get or Create Customer // ======================================== let apiCustomer: ApiCustomer; - let legacyData: CustomerLegacyData; + let legacyData: CustomerLegacyData | undefined; // Path A: customerId is NULL - always create new customer if (!customerId) { @@ -44,7 +42,6 @@ export const getOrCreateApiCustomer = async ({ const res = await getCachedApiCustomer({ ctx, customerId: newCustomer.id || newCustomer.internal_id, - withAutumnId, }); apiCustomer = res.apiCustomer; @@ -59,7 +56,6 @@ export const getOrCreateApiCustomer = async ({ const res = await getCachedApiCustomer({ ctx, customerId, - withAutumnId, }); apiCustomerOrUndefined = res?.apiCustomer; legacyData = res?.legacyData; @@ -89,7 +85,6 @@ export const getOrCreateApiCustomer = async ({ const res = await getCachedApiCustomer({ ctx, customerId: newCustomer.id || newCustomer.internal_id, - withAutumnId, source: "getOrCreateApiCustomer", }); apiCustomerOrUndefined = res?.apiCustomer; @@ -100,7 +95,6 @@ export const getOrCreateApiCustomer = async ({ const res = await getCachedApiCustomer({ ctx, customerId, - withAutumnId, }); apiCustomerOrUndefined = res?.apiCustomer; legacyData = res?.legacyData; @@ -127,11 +121,13 @@ export const getOrCreateApiCustomer = async ({ const res = await getCachedApiCustomer({ ctx, customerId: apiCustomer.id || "", - withAutumnId, }); apiCustomer = res?.apiCustomer; legacyData = res?.legacyData; } - return apiCustomer; + return { + apiCustomer, + legacyData, + }; }; diff --git a/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts b/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts index cd992ff58..5ba4a3710 100644 --- a/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts +++ b/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts @@ -1,7 +1,7 @@ import { - type CreateCustomerParams, CusExpand, CusProductStatus, + type CustomerData, type Entity, type EntityData, type FullCustomer, @@ -33,7 +33,7 @@ export const getOrCreateCustomer = async ({ }: { req: ExtendedRequest; customerId: string | null; - customerData?: CreateCustomerParams; + customerData?: CustomerData; inStatuses?: CusProductStatus[]; skipGet?: boolean; withEntities?: boolean; @@ -89,7 +89,7 @@ export const getOrCreateCustomer = async ({ fingerprint: customerData?.fingerprint, metadata: customerData?.metadata || {}, stripe_id: customerData?.stripe_id, - default_product_id: customerData?.default_product_id, + // default_product_id: customerData?.default_product_id, }, createDefaultProducts: customerData?.disable_default !== true, })) as FullCustomer; diff --git a/server/src/internal/customers/handlers/handlePostCustomerV2.ts b/server/src/internal/customers/handlers/handlePostCustomerV2.ts index c1b21b36e..2fc595f3c 100644 --- a/server/src/internal/customers/handlers/handlePostCustomerV2.ts +++ b/server/src/internal/customers/handlers/handlePostCustomerV2.ts @@ -7,6 +7,7 @@ import { } from "@autumn/shared"; import { z } from "zod/v4"; import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { getApiCustomer } from "../cusUtils/apiCusUtils/getApiCustomer.js"; import { getOrCreateApiCustomer } from "../cusUtils/getOrCreateApiCustomer.js"; export const handlePostCustomer = createRoute({ @@ -19,7 +20,7 @@ export const handlePostCustomer = createRoute({ handler: async (c) => { const ctx = c.get("ctx"); - const { expand = [], with_autumn_id = false } = c.req.valid("query"); + const { expand = [], with_autumn_id } = c.req.valid("query"); const createCusParams = c.req.valid("json"); // SIDE EFFECT @@ -32,11 +33,26 @@ export const handlePostCustomer = createRoute({ expand.push(CusExpand.Invoices); } - const apiCustomer = await getOrCreateApiCustomer({ + const baseData = await getOrCreateApiCustomer({ ctx, customerId: createCusParams.id, customerData: createCusParams, + }); + + console.log("Expand:", expand); + + const apiCustomer = await getApiCustomer({ + ctx, + customerId: createCusParams.id || "", + expand, + skipCache: false, withAutumnId: with_autumn_id, + baseData: { + apiCustomer: baseData.apiCustomer, + legacyData: baseData.legacyData || { + cusProductLegacyData: {}, + }, + }, }); return c.json(apiCustomer); diff --git a/server/src/internal/entities/entityUtils/apiEntityCacheUtils/entityLuaScripts/luaScripts.ts b/server/src/internal/entities/entityUtils/apiEntityCacheUtils/entityLuaScripts/luaScripts.ts deleted file mode 100644 index dcff23e10..000000000 --- a/server/src/internal/entities/entityUtils/apiEntityCacheUtils/entityLuaScripts/luaScripts.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Load shared validation function -const CHECK_ENTITY_CACHE_EXISTS = readFileSync( - join(__dirname, "checkEntityCacheExists.lua"), - "utf-8", -); - -// Load Lua scripts at module initialization -export const GET_ENTITY_SCRIPT = readFileSync( - join(__dirname, "getEntity.lua"), - "utf-8", -); - -// Prepend validation function to SET_ENTITY_SCRIPT -const setEntityScript = readFileSync(join(__dirname, "setEntity.lua"), "utf-8"); -export const SET_ENTITY_SCRIPT = `${CHECK_ENTITY_CACHE_EXISTS}\n${setEntityScript}`; - -export const SET_ENTITIES_BATCH_SCRIPT = readFileSync( - join(__dirname, "setEntitiesBatch.lua"), - "utf-8", -); - -export const SET_ENTITY_PRODUCTS_SCRIPT = readFileSync( - join(__dirname, "setEntityProducts.lua"), - "utf-8", -); diff --git a/server/src/internal/entities/entityUtils/apiEntityCacheUtils/getCachedApiEntity.ts b/server/src/internal/entities/entityUtils/apiEntityCacheUtils/getCachedApiEntity.ts index 98d5cc9b4..0fd96e888 100644 --- a/server/src/internal/entities/entityUtils/apiEntityCacheUtils/getCachedApiEntity.ts +++ b/server/src/internal/entities/entityUtils/apiEntityCacheUtils/getCachedApiEntity.ts @@ -1,4 +1,11 @@ -import { type ApiEntity, ApiEntitySchema, type AppEnv } from "@autumn/shared"; +import { + type ApiEntity, + ApiEntitySchema, + type AppEnv, + type FullCustomer, + filterEntityLevelCusProducts, +} from "@autumn/shared"; +import { GET_ENTITY_SCRIPT } from "@lua/luaScripts.js"; import { redis } from "@/external/redis/initRedis.js"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { CusService } from "@/internal/customers/CusService.js"; @@ -9,7 +16,6 @@ import { } from "@/utils/cacheUtils/cacheUtils.js"; import { setCachedApiCustomer } from "../../../customers/cusUtils/apiCusCacheUtils/setCachedApiCustomer.js"; import { getApiEntityBase } from "../apiEntityUtils/getApiEntityBase.js"; -import { GET_ENTITY_SCRIPT } from "./entityLuaScripts/luaScripts.js"; export const buildCachedApiEntityKey = ({ entityId, @@ -34,14 +40,16 @@ export const getCachedApiEntity = async ({ ctx, customerId, entityId, - withAutumnId = false, skipCache = false, + skipCustomerMerge = false, + fullCus, }: { ctx: AutumnContext; customerId: string; entityId: string; - withAutumnId?: boolean; skipCache?: boolean; + skipCustomerMerge?: boolean; // If true, returns only entity's own features (no customer merging) + fullCus?: FullCustomer; }): Promise<{ apiEntity: ApiEntity }> => { const { org, env, db } = ctx; @@ -61,6 +69,9 @@ export const getCachedApiEntity = async ({ cacheKey, // KEYS[1] org.id, // ARGV[1] env, // ARGV[2] + customerId, // ARGV[3] + entityId, // ARGV[4] + skipCustomerMerge ? "true" : "false", // ARGV[5] ), ); @@ -71,25 +82,24 @@ export const getCachedApiEntity = async ({ ); return { - apiEntity: ApiEntitySchema.parse({ - ...cached, - autumn_id: withAutumnId ? entityId : undefined, - }), + apiEntity: ApiEntitySchema.parse(cached), }; } } // Cache miss or skipCache - fetch from DB - const fullCus = await CusService.getFull({ - db, - idOrInternalId: customerId, - orgId: org.id, - env: env as AppEnv, - inStatuses: RELEVANT_STATUSES, - withEntities: true, - withSubs: true, - entityId, - }); + if (!fullCus) { + fullCus = await CusService.getFull({ + db, + idOrInternalId: customerId, + orgId: org.id, + env: env as AppEnv, + inStatuses: RELEVANT_STATUSES, + withEntities: true, + withSubs: true, + entityId, + }); + } const entity = fullCus.entity; if (!entity) { @@ -104,74 +114,6 @@ export const getCachedApiEntity = async ({ fullCus, customerId, }); - // const { apiCustomer: masterApiCustomer, legacyData } = - // await getApiCustomerBase({ - // ctx, - // fullCus: { - // ...structuredClone(fullCus), - // customer_products: filterOutEntitiesFromCusProducts({ - // cusProducts: fullCus.customer_products, - // }), - // }, - // withAutumnId: !skipCache, - // }); - - // // Build ApiEntity with filtered entity-level products for caching - // const entityCusProducts = filterEntityLevelCusProducts({ - // cusProducts: fullCus.customer_products, - // }); - // const { apiEntity: apiEntityForCache, legacyData: entityLegacyData } = - // await getApiEntityBase({ - // ctx, - // entity, - // fullCus: { - // ...fullCus, - // customer_products: entityCusProducts, - // }, - // withAutumnId: true, - // }); - - // await tryRedisWrite(async () => { - // // Get customer - // const customerCacheKey = buildCachedApiCustomerKey({ - // customerId, - // orgId: org.id, - // env, - // }); - // const cachedCustomer = await redis.eval( - // GET_CUSTOMER_SCRIPT, - // 1, - // customerCacheKey, - // org.id, - // env, - // customerId, - // ); - - // if (!cachedCustomer) { - // await redis.eval( - // SET_CUSTOMER_SCRIPT, - // 1, - // customerCacheKey, - // JSON.stringify({ - // ...masterApiCustomer, - // entities: fullCus.entities, - // legacyData, - // }), - // org.id, - // env, - // ); - // } - - // await redis.eval( - // SET_ENTITY_SCRIPT, - // 1, // number of keys - // cacheKey, // KEYS[1] - // JSON.stringify({ - // ...apiEntityForCache, - // legacyData: entityLegacyData, - // }), // ARGV[1] - // ); - // }); } // Build ApiEntity with full products for return @@ -182,10 +124,21 @@ export const getCachedApiEntity = async ({ withAutumnId: !skipCache, }); + const { apiEntity: pureApiEntity } = await getApiEntityBase({ + ctx, + entity, + fullCus: { + ...fullCus, + customer_products: filterEntityLevelCusProducts({ + cusProducts: fullCus.customer_products, + }), + }, + withAutumnId: true, + }); + return { - apiEntity: ApiEntitySchema.parse({ - ...apiEntity, - autumn_id: withAutumnId ? entity.internal_id : undefined, - }), + apiEntity: ApiEntitySchema.parse( + skipCustomerMerge ? pureApiEntity : apiEntity, + ), }; }; diff --git a/server/src/internal/entities/entityUtils/apiEntityCacheUtils/refreshCachedApiEntity.ts b/server/src/internal/entities/entityUtils/apiEntityCacheUtils/refreshCachedApiEntity.ts index 207f06fb2..742f4eaa1 100644 --- a/server/src/internal/entities/entityUtils/apiEntityCacheUtils/refreshCachedApiEntity.ts +++ b/server/src/internal/entities/entityUtils/apiEntityCacheUtils/refreshCachedApiEntity.ts @@ -1,10 +1,10 @@ import type { ApiEntity, AppEnv } from "@autumn/shared"; +import { SET_ENTITY_SCRIPT } from "@lua/luaScripts.js"; import { redis } from "@/external/redis/initRedis.js"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { CusService } from "@/internal/customers/CusService.js"; import { RELEVANT_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js"; import { getApiEntityBase } from "../apiEntityUtils/getApiEntityBase.js"; -import { SET_ENTITY_SCRIPT } from "./entityLuaScripts/luaScripts.js"; import { buildCachedApiEntityKey } from "./getCachedApiEntity.js"; /** diff --git a/server/src/internal/entities/entityUtils/apiEntityUtils/getApiEntity.ts b/server/src/internal/entities/entityUtils/apiEntityUtils/getApiEntity.ts index 9e7a85b79..d25b346ee 100644 --- a/server/src/internal/entities/entityUtils/apiEntityUtils/getApiEntity.ts +++ b/server/src/internal/entities/entityUtils/apiEntityUtils/getApiEntity.ts @@ -24,14 +24,20 @@ export const getApiEntity = async ({ skipCache?: boolean; }): Promise => { // Get base entity (cacheable or direct from DB) - const { apiEntity: baseEntity } = await getCachedApiEntity({ + let { apiEntity: baseEntity } = await getCachedApiEntity({ ctx, customerId, entityId, - withAutumnId, skipCache, + fullCus, }); + // Clean api entity + baseEntity = { + ...baseEntity, + autumn_id: withAutumnId ? baseEntity.autumn_id : undefined, + }; + // Get expand fields (not cacheable) const apiEntityExpand = await getApiEntityExpand({ ctx, diff --git a/server/src/internal/entities/handlers/handleCreateEntity/createEntityForCusProduct.ts b/server/src/internal/entities/handlers/handleCreateEntity/createEntityForCusProduct.ts index d2bf2055c..e362b8289 100644 --- a/server/src/internal/entities/handlers/handleCreateEntity/createEntityForCusProduct.ts +++ b/server/src/internal/entities/handlers/handleCreateEntity/createEntityForCusProduct.ts @@ -1,5 +1,5 @@ import { - type CreateEntity, + type CreateEntityParams, ErrCode, type FullCusProduct, type FullCustomer, @@ -27,11 +27,13 @@ export const updateLinkedCusEnt = async ({ }: { db: DrizzleCli; linkedCusEnt: FullCustomerEntitlement; - inputEntities: CreateEntity[]; + inputEntities: CreateEntityParams[]; entityToReplacement: Record; }) => { const newEntities = structuredClone(linkedCusEnt.entities) || {}; for (const entity of inputEntities) { + if (!entity.id) continue; + const replaceableId = entityToReplacement[entity.id]; const replaceableInEntities = replaceableId ? newEntities[replaceableId] @@ -73,7 +75,7 @@ export const createEntityForCusProduct = async ({ req: ExtendedRequest; customer: FullCustomer; cusProduct: FullCusProduct; - inputEntities: CreateEntity[]; + inputEntities: CreateEntityParams[]; logger: any; fromAutoCreate?: boolean; }) => { @@ -82,7 +84,7 @@ export const createEntityForCusProduct = async ({ acc[entity.feature_id!] = [...(acc[entity.feature_id!] || []), entity]; return acc; }, - {} as Record, + {} as Record, ); const { db, env, org, features } = req; @@ -161,7 +163,7 @@ export const createEntityForCusProduct = async ({ const entityToReplacement: Record = {}; for (let i = 0; i < deletedReplaceables.length; i++) { const replaceable = deletedReplaceables[i]; - entityToReplacement[inputEntities[i].id] = replaceable.id; + entityToReplacement[inputEntities[i].id!] = replaceable.id; if (i >= inputEntities.length) { break; diff --git a/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity2.ts b/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity2.ts index 687691dc6..e14130935 100644 --- a/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity2.ts +++ b/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity2.ts @@ -12,6 +12,7 @@ import { createRoute } from "../../../../honoMiddlewares/routeHandler.js"; import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; import type { ExtendedRequest } from "../../../../utils/models/Request.js"; import { EntityService } from "../../../api/entities/EntityService.js"; +import { getApiEntity } from "../../entityUtils/apiEntityUtils/getApiEntity.js"; import { constructEntity } from "../../entityUtils/entityUtils.js"; import { createEntityForCusProduct } from "./createEntityForCusProduct.js"; import { validateAndGetInputEntities } from "./getInputEntities.js"; @@ -89,25 +90,26 @@ export const createEntities = async ({ newEntities.push(...insertedEntities); - // // Get api entity for each entity... - // const apiEntities = []; - // for (const entity of newEntities) { - // // Cloned fullCus - // const clonedFullCus = structuredClone(fullCus); - // clonedFullCus.entity = entity; - // const apiEntity = await getApiEntity({ - // ctx, - // expand: [], - // customerId, - // entityId: entity.id, - // fullCus: clonedFullCus, - // withAutumnId, - // }); - // apiEntities.push(apiEntity); - // } - return newEntities; + // Get api entity for each entity... + const apiEntities = []; + for (const entity of newEntities) { + // Cloned fullCus - // return apiEntities; + const clonedFullCus = structuredClone(fullCus); + clonedFullCus.entity = entity; + const apiEntity = await getApiEntity({ + ctx, + expand: [], + customerId, + entityId: entity.id, + fullCus: clonedFullCus, + withAutumnId, + skipCache: true, + }); + apiEntities.push(apiEntity); + } + + return apiEntities; }; export const handleCreateEntity = createRoute({ diff --git a/server/src/internal/entities/handlers/handleGetEntity.ts b/server/src/internal/entities/handlers/handleGetEntity.ts index 3a1dc865f..719ef9f90 100644 --- a/server/src/internal/entities/handlers/handleGetEntity.ts +++ b/server/src/internal/entities/handlers/handleGetEntity.ts @@ -7,7 +7,7 @@ export const handleGetEntity = createRoute({ handler: async (c) => { const { customer_id, entity_id } = c.req.param(); const ctx = c.get("ctx"); - const { expand, skip_cache } = c.req.valid("query"); + const { expand, skip_cache, with_autumn_id } = c.req.valid("query"); const apiEntity = await getApiEntity({ ctx, @@ -15,6 +15,7 @@ export const handleGetEntity = createRoute({ entityId: entity_id, expand, skipCache: skip_cache, + withAutumnId: with_autumn_id, }); return c.json(apiEntity); diff --git a/server/src/queue/bullmq/initBullMqWorkers.ts b/server/src/queue/bullmq/initBullMqWorkers.ts index f598296a4..bae9a97f7 100644 --- a/server/src/queue/bullmq/initBullMqWorkers.ts +++ b/server/src/queue/bullmq/initBullMqWorkers.ts @@ -11,8 +11,9 @@ import { runRewardMigrationTask } from "@/internal/migrations/runRewardMigration import { detectBaseVariant } from "@/internal/products/productUtils/detectProductVariant.js"; import { runTriggerCheckoutReward } from "@/internal/rewards/triggerCheckoutReward.js"; import { generateId } from "@/utils/genUtils.js"; -import { queue, workerRedis } from "./initBullMq.js"; +import { createWorkerContext } from "../createWorkerContext.js"; import { JobName } from "../JobName.js"; +import { queue, workerRedis } from "./initBullMq.js"; const NUM_WORKERS = 10; @@ -38,6 +39,11 @@ const initWorker = ({ id, db }: { id: number; db: DrizzleCli }) => { }, }); + const ctx = await createWorkerContext({ + db, + logger: workerLogger, + }); + try { if (job.name === JobName.DetectBaseVariant) { await detectBaseVariant({ @@ -87,9 +93,8 @@ const initWorker = ({ id, db }: { id: number; db: DrizzleCli }) => { if (job.name === JobName.SyncBalanceBatch) { await runSyncBalanceBatch({ - db, + ctx, payload: job.data, - logger: workerLogger as Logger, }); return; } @@ -167,4 +172,3 @@ export const initWorkers = async () => { return workers; }; - diff --git a/server/src/queue/createWorkerContext.ts b/server/src/queue/createWorkerContext.ts index 9b0d6fc4d..51520b94d 100644 --- a/server/src/queue/createWorkerContext.ts +++ b/server/src/queue/createWorkerContext.ts @@ -1,34 +1,55 @@ -import { - type AppEnv, - AuthType, - createdAtToVersion, - type Feature, - type Organization, -} from "@autumn/shared"; +import { type AppEnv, AuthType, createdAtToVersion } from "@autumn/shared"; import type { DrizzleCli } from "../db/initDrizzle.js"; import type { Logger } from "../external/logtail/logtailUtils.js"; import type { AutumnContext } from "../honoUtils/HonoEnv.js"; +import { OrgService } from "../internal/orgs/OrgService.js"; import { generateId } from "../utils/genUtils.js"; -export const createWorkerContext = ({ +export const createWorkerContext = async ({ db, - org, + orgId, env, - features, + // features, logger, }: { db: DrizzleCli; - org: Organization; - env: AppEnv; - features: Feature[]; + orgId?: string; + env?: AppEnv; + // features: Feature[]; logger: Logger; }) => { + if (!orgId || !env) return; + + // Fetch org with features once for all items + const orgData = await OrgService.getWithFeatures({ + db, + orgId, + env: env as AppEnv, + }); + + if (!orgData) { + throw new Error(`Organization not found: ${orgId}, env: ${env}`); + } + + const { org, features } = orgData; + + const workerLogger = logger.child({ + context: { + context: { + org_id: org?.id, + org_slug: org?.slug, + env: env, + authType: AuthType.Worker, + }, + }, + }); + const ctx: AutumnContext = { org, env, features, db, - logger, + logger: workerLogger, id: generateId("job"), timestamp: Date.now(), diff --git a/server/src/queue/initWorkers.ts b/server/src/queue/initWorkers.ts index c284ae97d..cb8250584 100644 --- a/server/src/queue/initWorkers.ts +++ b/server/src/queue/initWorkers.ts @@ -15,6 +15,7 @@ import { runRewardMigrationTask } from "@/internal/migrations/runRewardMigration import { detectBaseVariant } from "@/internal/products/productUtils/detectProductVariant.js"; import { runTriggerCheckoutReward } from "@/internal/rewards/triggerCheckoutReward.js"; import { generateId } from "@/utils/genUtils.js"; +import { createWorkerContext } from "./createWorkerContext.js"; import { QUEUE_URL, sqs } from "./initSqs.js"; import { JobName } from "./JobName.js"; @@ -56,7 +57,13 @@ const processMessage = async ({ }, }, }); - // workerLogger.info(`Received message ${message.MessageId}`); + + const ctx = await createWorkerContext({ + db, + orgId: job.data.orgId, + env: job.data.env, + logger: workerLogger, + }); try { if (job.name === JobName.DetectBaseVariant) { @@ -109,9 +116,8 @@ const processMessage = async ({ if (job.name === JobName.SyncBalanceBatch) { await runSyncBalanceBatch({ - db, + ctx, payload: job.data, - logger: workerLogger as Logger, }); return; } diff --git a/server/src/queue/queueUtils.ts b/server/src/queue/queueUtils.ts index 28be48431..1385d4a0c 100644 --- a/server/src/queue/queueUtils.ts +++ b/server/src/queue/queueUtils.ts @@ -1,5 +1,5 @@ -import { SendMessageCommand } from "@aws-sdk/client-sqs"; import type { AppEnv, EventInsert, Price } from "@autumn/shared"; +import { SendMessageCommand } from "@aws-sdk/client-sqs"; import { generateId } from "@/utils/genUtils.js"; import { JobName } from "./JobName.js"; @@ -11,6 +11,8 @@ export interface Payloads { env: AppEnv; }; [JobName.SyncBalanceBatch]: { + orgId: string; + env: AppEnv; items: Array<{ customerId: string; featureId: string; @@ -45,7 +47,9 @@ const initializeQueue = async () => { const { queue } = await import("./bullmq/initBullMq.js"); bullmqQueue = queue; } else { - throw new Error("No queue configured. Set either SQS_QUEUE_URL or QUEUE_URL"); + throw new Error( + "No queue configured. Set either SQS_QUEUE_URL or QUEUE_URL", + ); } }; diff --git a/server/src/utils/cacheUtils/cacheUtils.ts b/server/src/utils/cacheUtils/cacheUtils.ts index 1aa4a110b..2fa1b5751 100644 --- a/server/src/utils/cacheUtils/cacheUtils.ts +++ b/server/src/utils/cacheUtils/cacheUtils.ts @@ -49,30 +49,55 @@ export const tryRedisRead = async ( } }; +/** + * Helper function to normalize empty objects {} to empty arrays [] + * Lua's cjson converts empty arrays to empty objects, so we need to fix this + */ +const normalizeArray = (value: unknown): unknown => { + if ( + value && + typeof value === "object" && + !Array.isArray(value) && + Object.keys(value).length === 0 + ) { + return []; + } + return value; +}; + /** * Fix Lua cjson quirks when parsing cached data: - * - Converts products[].items from {} back to [] if it's an empty object + * - Converts empty objects {} back to [] for all array fields * - Converts usage_limit: 0 to undefined (when all sources were undefined) */ export const normalizeCachedData = ( data: T, ): T => { + // Normalize top-level products array if (data.products) { + if (!Array.isArray(data.products)) { + data.products = []; + } + + // Normalize nested arrays in products for (const product of data.products) { - if ( - product.items && - typeof product.items === "object" && - !Array.isArray(product.items) && - Object.keys(product.items).length === 0 - ) { - product.items = []; + // Normalize product.items array + if (product.items) { + product.items = normalizeArray(product.items) as typeof product.items; + } + + // Normalize product.stripe_subscription_ids array + if (product.stripe_subscription_ids) { + product.stripe_subscription_ids = normalizeArray( + product.stripe_subscription_ids, + ) as typeof product.stripe_subscription_ids; } } } - // Convert empty entities to [] - if ("entities" in data && data.entities && !Array.isArray(data.entities)) { - data.entities = []; + // Normalize entities array (included in Lua script) + if ("entities" in data && data.entities) { + data.entities = normalizeArray(data.entities) as typeof data.entities; } // Fix usage_limit: 0 -> undefined @@ -80,7 +105,7 @@ export const normalizeCachedData = ( if (data.features) { for (const featureId in data.features) { const feature = data.features[featureId]; - if (feature.usage_limit === 0) { + if (feature.usage_limit === 0 || feature.usage_limit === null) { feature.usage_limit = undefined; } @@ -110,6 +135,13 @@ export const normalizeCachedData = ( // } } } + + // Normalize feature.credit_schema array + if (feature.credit_schema) { + feature.credit_schema = normalizeArray( + feature.credit_schema, + ) as typeof feature.credit_schema; + } } } diff --git a/server/src/utils/scriptUtils/constructItem.ts b/server/src/utils/scriptUtils/constructItem.ts index 455c48e17..106157624 100644 --- a/server/src/utils/scriptUtils/constructItem.ts +++ b/server/src/utils/scriptUtils/constructItem.ts @@ -76,6 +76,7 @@ export const constructPrepaidItem = ({ rolloverConfig, usageLimit, intervalCount = 1, + resetUsageWhenEnabled, }: { featureId: string; price?: number; @@ -87,6 +88,7 @@ export const constructPrepaidItem = ({ rolloverConfig?: RolloverConfig; usageLimit?: number; intervalCount?: number; + resetUsageWhenEnabled?: boolean; }) => { const item: ProductItem = { feature_id: featureId, @@ -104,6 +106,7 @@ export const constructPrepaidItem = ({ ...(rolloverConfig ? { rollover: rolloverConfig } : {}), }, usage_limit: usageLimit, + reset_usage_when_enabled: resetUsageWhenEnabled, }; return item; diff --git a/server/src/utils/scriptUtils/testUtils/initCustomerV3.ts b/server/src/utils/scriptUtils/testUtils/initCustomerV3.ts index ac960a1fa..2aa8f4ae3 100644 --- a/server/src/utils/scriptUtils/testUtils/initCustomerV3.ts +++ b/server/src/utils/scriptUtils/testUtils/initCustomerV3.ts @@ -58,7 +58,7 @@ export const initCustomerV3 = async ({ name, email, // @ts-expect-error - fingerprint: customerData?.fingerprint || fingerprint_, + fingerprint: customerData?.fingerprint, stripe_id: stripeCus.id, disable_default: !withDefault, default_product_id: defaultProductId, diff --git a/server/tests/advanced/coupons/coupon1.test.ts b/server/tests/advanced/coupons/coupon1.test.ts index 98c6c5095..0d3e760c7 100644 --- a/server/tests/advanced/coupons/coupon1.test.ts +++ b/server/tests/advanced/coupons/coupon1.test.ts @@ -1,36 +1,35 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, - type Customer, + CouponDurationType, + type CreateReward, LegacyVersion, type Organization, + RewardType, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import { addHours, addMonths } from "date-fns"; import type Stripe from "stripe"; -import { rewards } from "tests/global.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; import { timeout } from "tests/utils/genUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; +import { createReward } from "tests/utils/productUtils.js"; import { advanceTestClock, completeCheckoutForm, getDiscount, } from "tests/utils/stripeUtils.js"; -import { - addPrefixToProducts, - getBasePrice, -} from "tests/utils/testProductUtils/testProductUtils.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; const testCase = "coupon1"; @@ -39,6 +38,24 @@ const pro = constructProduct({ items: [constructArrearItem({ featureId: TestFeature.Words })], }); +// Create reward inline - matching rolloverAll config from global.ts +const rewardId = `${testCase}rolloverAll`; +const promoCode = `${testCase}rolloverAllCode`; +const reward: CreateReward = { + id: rewardId, + name: "Rollover All", + type: RewardType.InvoiceCredits, + promo_codes: [{ code: promoCode }], + discount_config: { + discount_value: 1000, + duration_type: CouponDurationType.Forever, + duration_value: 0, + should_rollover: true, + apply_to_all: true, + price_ids: [], + }, +}; + const simulateOneCycle = async ({ customerId, db, @@ -100,13 +117,9 @@ const simulateOneCycle = async ({ expect(cusDiscount).toBeDefined(); - expect(getOriginalCouponId(cusDiscount.coupon?.id)).toBe( - rewards.rolloverAll.id, - ); + expect(getOriginalCouponId(cusDiscount.coupon?.id)).toBe(rewardId); - expect(cusDiscount.coupon?.amount_off).toBe( - Math.round(couponAmount * 100), - ); + expect(cusDiscount.coupon?.amount_off).toBe(Math.round(couponAmount * 100)); return { couponAmount, @@ -121,7 +134,6 @@ describe( () => { const customerId = "coupon1"; let stripeCli: Stripe; - let customer: Customer; let testClockId: string; let db: DrizzleCli; let org: Organization; @@ -129,8 +141,8 @@ describe( const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let couponAmount = rewards.rolloverAll.discount_config.discount_value; - let curUnix = new Date().getTime(); + let couponAmount = reward.discount_config!.discount_value; + let curUnix = Date.now(); beforeAll(async () => { db = ctx.db; @@ -138,26 +150,28 @@ describe( env = ctx.env; stripeCli = ctx.stripeCli; + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + const res = await initCustomerV3({ ctx, customerId, }); - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - products: [pro], + await createReward({ orgId: org.id, env, db, autumn, + reward, + productId: pro.id, }); testClockId = res.testClockId; - customer = res.customer; }); // CYCLE 0 @@ -167,11 +181,7 @@ describe( product_id: pro.id, }); - await completeCheckoutForm( - res.checkout_url, - undefined, - rewards.rolloverAll.id, - ); + await completeCheckoutForm(res.checkout_url, undefined, promoCode); await timeout(10000); @@ -182,15 +192,17 @@ describe( expect(customer.invoices![0].total).toBe(0); + console.log("Customer", customer); + const cusDiscount = await getDiscount({ stripeCli, stripeId: customer.stripe_id!, }); + // console.log("CusDiscount", cusDiscount); + expect(cusDiscount).toBeDefined(); - expect(getOriginalCouponId(cusDiscount.coupon?.id)).toBe( - rewards.rolloverAll.id, - ); + expect(getOriginalCouponId(cusDiscount.coupon?.id)).toBe(rewardId); expect(cusDiscount.coupon?.amount_off).toBe(couponAmount * 100); }); @@ -204,7 +216,7 @@ describe( autumn, testClockId, couponAmount, - curUnix: new Date().getTime(), + curUnix: Date.now(), }); couponAmount = res.couponAmount; @@ -213,7 +225,7 @@ describe( // CYCLE 1 test("should run another cycle and have correct invoice + coupon amount", async () => { - const res = await simulateOneCycle({ + await simulateOneCycle({ customerId, db, org, diff --git a/server/tests/advanced/coupons/coupon2.test.ts b/server/tests/advanced/coupons/coupon2.test.ts index 5293c35c7..14b0678db 100644 --- a/server/tests/advanced/coupons/coupon2.test.ts +++ b/server/tests/advanced/coupons/coupon2.test.ts @@ -1,3 +1,4 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, CouponDurationType, @@ -6,7 +7,6 @@ import { type Organization, RewardType, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import { addHours, addMonths } from "date-fns"; import { Decimal } from "decimal.js"; @@ -18,11 +18,11 @@ import { expectProductAttached } from "tests/utils/expectUtils/expectProductAtta import { timeout } from "tests/utils/genUtils.js"; import { createProducts, createReward } from "tests/utils/productUtils.js"; import { completeCheckoutForm, getDiscount } from "tests/utils/stripeUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { addPrefixToProducts, getBasePrice, } from "tests/utils/testProductUtils/testProductUtils.js"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js"; diff --git a/server/tests/advanced/coupons/coupon3.test.ts b/server/tests/advanced/coupons/coupon3.test.ts index 0781c7250..99711b138 100644 --- a/server/tests/advanced/coupons/coupon3.test.ts +++ b/server/tests/advanced/coupons/coupon3.test.ts @@ -1,3 +1,4 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, CouponDurationType, @@ -6,17 +7,16 @@ import { type Organization, RewardType, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import type Stripe from "stripe"; import { TestFeature } from "tests/setup/v2Features.js"; import { expectAttachCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { createProducts, createReward } from "tests/utils/productUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { addPrefixToProducts, getBasePrice, } from "tests/utils/testProductUtils/testProductUtils.js"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { @@ -25,6 +25,7 @@ import { } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { expectProductAttached } from "../../utils/expectUtils/expectProductAttached.js"; const pro = constructProduct({ type: "pro", @@ -136,7 +137,7 @@ describe(chalk.yellow(`${testCase} - Testing attach coupon`), () => { }); const customer = await autumn.customers.get(customerId); - expectAttachCorrect({ + expectProductAttached({ customer, product: oneOff, }); @@ -155,9 +156,10 @@ describe(chalk.yellow(`${testCase} - Testing attach coupon`), () => { }); const customer = await autumn.customers.get(customerId); - expectAttachCorrect({ + expectProductAttached({ customer, product: oneOff, + quantity: 2, }); expect(customer.invoices!.length).toBe(3); diff --git a/server/tests/advanced/customInterval/customInterval1.test.ts b/server/tests/advanced/customInterval/customInterval1.test.ts index 342aa775a..552c53a5a 100644 --- a/server/tests/advanced/customInterval/customInterval1.test.ts +++ b/server/tests/advanced/customInterval/customInterval1.test.ts @@ -1,19 +1,19 @@ -import { LegacyVersion } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion } from "@autumn/shared"; import chalk from "chalk"; import { addHours, addMonths } from "date-fns"; import type Stripe from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; const testCase = "customInterval1"; @@ -54,6 +54,13 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom interval and interva beforeAll(async () => { stripeCli = ctx.stripeCli; + await initProductsV0({ + ctx, + products: [pro, premium], + prefix: testCase, + customerId, + }); + const { testClockId: testClockId1 } = await initCustomerV3({ ctx, customerId, @@ -62,13 +69,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom interval and interva withTestClock: true, }); - await initProductsV0({ - ctx, - products: [pro, premium], - prefix: testCase, - customerId, - }); - testClockId = testClockId1!; }); diff --git a/server/tests/advanced/customInterval/customInterval2.test.ts b/server/tests/advanced/customInterval/customInterval2.test.ts index 724a48a4f..083102f2b 100644 --- a/server/tests/advanced/customInterval/customInterval2.test.ts +++ b/server/tests/advanced/customInterval/customInterval2.test.ts @@ -1,19 +1,19 @@ -import { LegacyVersion } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion } from "@autumn/shared"; import chalk from "chalk"; import { addHours, addMonths } from "date-fns"; import type Stripe from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; const testCase = "customInterval2"; @@ -37,6 +37,13 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on arrear p beforeAll(async () => { stripeCli = ctx.stripeCli; + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + const { testClockId: testClockId1 } = await initCustomerV3({ ctx, customerId, @@ -45,13 +52,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on arrear p withTestClock: true, }); - await initProductsV0({ - ctx, - products: [pro], - prefix: testCase, - customerId, - }); - testClockId = testClockId1!; }); diff --git a/server/tests/advanced/customInterval/customInterval3.test.ts b/server/tests/advanced/customInterval/customInterval3.test.ts index 36aca4beb..5a99870f7 100644 --- a/server/tests/advanced/customInterval/customInterval3.test.ts +++ b/server/tests/advanced/customInterval/customInterval3.test.ts @@ -1,12 +1,12 @@ -import { LegacyVersion } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion } from "@autumn/shared"; import chalk from "chalk"; import { addDays, addMonths } from "date-fns"; import type Stripe from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; import { @@ -17,9 +17,9 @@ import { constructProduct, constructRawProduct, } from "@/utils/scriptUtils/createTestProducts.js"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; const testCase = "customInterval3"; @@ -57,6 +57,13 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on add on m beforeAll(async () => { stripeCli = ctx.stripeCli; + await initProductsV0({ + ctx, + products: [pro, addOn], + prefix: testCase, + customerId, + }); + const { testClockId: testClockId1 } = await initCustomerV3({ ctx, customerId, @@ -65,13 +72,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on add on m withTestClock: true, }); - await initProductsV0({ - ctx, - products: [pro, addOn], - prefix: testCase, - customerId, - }); - testClockId = testClockId1!; }); diff --git a/server/tests/advanced/customInterval/customInterval4.test.ts b/server/tests/advanced/customInterval/customInterval4.test.ts index c2525a0cf..97a1a2dab 100644 --- a/server/tests/advanced/customInterval/customInterval4.test.ts +++ b/server/tests/advanced/customInterval/customInterval4.test.ts @@ -1,15 +1,15 @@ -import { LegacyVersion } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion } from "@autumn/shared"; import chalk from "chalk"; import { addMonths } from "date-fns"; import type Stripe from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectDowngradeCorrect, expectNextCycleCorrect, } from "tests/utils/expectUtils/expectScheduleUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; @@ -51,6 +51,13 @@ describe(`${chalk.yellowBright(`${testCase}: Testing downgrades for custom inter beforeAll(async () => { stripeCli = ctx.stripeCli; + await initProductsV0({ + ctx, + products: [pro, premium], + prefix: testCase, + customerId, + }); + const { testClockId: testClockId1 } = await initCustomerV3({ ctx, customerId, @@ -59,13 +66,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing downgrades for custom inter withTestClock: true, }); - await initProductsV0({ - ctx, - products: [pro, premium], - prefix: testCase, - customerId, - }); - testClockId = testClockId1!; }); diff --git a/server/tests/advanced/customInterval/customInterval5.test.ts b/server/tests/advanced/customInterval/customInterval5.test.ts index bb3af2919..53311f48a 100644 --- a/server/tests/advanced/customInterval/customInterval5.test.ts +++ b/server/tests/advanced/customInterval/customInterval5.test.ts @@ -1,11 +1,11 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { LegacyVersion } from "@autumn/shared"; import type { Customer } from "autumn-js"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import type Stripe from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; @@ -55,14 +55,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing multi interval features wit beforeAll(async () => { stripeCli = ctx.stripeCli; - const { testClockId: testClockId1 } = await initCustomerV3({ - ctx, - customerId, - customerData: {}, - attachPm: "success", - withTestClock: true, - }); - await initProductsV0({ ctx, products: [pro], @@ -70,6 +62,13 @@ describe(`${chalk.yellowBright(`${testCase}: Testing multi interval features wit customerId, }); + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); testClockId = testClockId1!; }); diff --git a/server/tests/advanced/advancedOthers/advancedOthers1.ts b/server/tests/advanced/misc/advanced-misc1.test.ts similarity index 57% rename from server/tests/advanced/advancedOthers/advancedOthers1.ts rename to server/tests/advanced/misc/advanced-misc1.test.ts index 19b89088c..e28d49990 100644 --- a/server/tests/advanced/advancedOthers/advancedOthers1.ts +++ b/server/tests/advanced/misc/advanced-misc1.test.ts @@ -1,75 +1,60 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; -import { expect } from "chai"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { getMainCusProduct } from "tests/utils/cusProductUtils/cusProductUtils.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; import { timeout } from "@/utils/genUtils.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; + +const testCase = "advanced-misc1"; -// UNCOMMENT FROM HERE const pro = constructProduct({ id: "pro", items: [constructFeatureItem({ featureId: TestFeature.Words })], type: "pro", }); -describe(`${chalk.yellowBright("advancedOthers1: Testing convert collection method from send_invoice")}`, () => { - const customerId = "advancedOthers1"; +describe(`${chalk.yellowBright( + `${testCase}: Testing convert collection method from send_invoice`, +)}`, () => { + const customerId = testCase; const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; let db: DrizzleCli; let org: Organization; let env: AppEnv; - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; + beforeAll(async () => { + db = ctx.db; + org = ctx.org; + env = ctx.env; + stripeCli = ctx.stripeCli; - stripeCli = this.stripeCli; - - addPrefixToProducts({ + await initProductsV0({ + ctx, products: [pro], - prefix: customerId, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro], - db, - orgId: org.id, - env, + prefix: testCase, customerId, }); - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, + await initCustomerV3({ + ctx, customerId, - db, - org, - env, attachPm: "success", }); - - testClockId = testClockId1!; }); - it("should attach pro product and pay for it", async () => { + test("should attach pro product and pay for it", async () => { const res = await autumn.attach({ customer_id: customerId, product_id: pro.id, @@ -77,7 +62,7 @@ describe(`${chalk.yellowBright("advancedOthers1: Testing convert collection meth enable_product_immediately: true, }); - expect(res.invoice).to.exist; + expect(res.invoice).toBeDefined(); const customer = await autumn.customers.get(customerId); expectProductAttached({ customer, @@ -85,12 +70,12 @@ describe(`${chalk.yellowBright("advancedOthers1: Testing convert collection meth }); const invoiceStripeId = res.invoice.stripe_id; - const invoice = await stripeCli.invoices.finalizeInvoice(invoiceStripeId); + await stripeCli.invoices.finalizeInvoice(invoiceStripeId); await stripeCli.invoices.pay(invoiceStripeId); }); - it("should have collection method charge automatically", async () => { + test("should have collection method charge automatically", async () => { await timeout(5000); const cusProduct = await getMainCusProduct({ @@ -98,7 +83,7 @@ describe(`${chalk.yellowBright("advancedOthers1: Testing convert collection meth customerId, orgId: org.id, env, - productGroup: pro.group, + productGroup: pro.group ?? undefined, }); const sub = await cusProductToSub({ @@ -106,6 +91,6 @@ describe(`${chalk.yellowBright("advancedOthers1: Testing convert collection meth stripeCli, }); - expect(sub?.collection_method).to.equal("charge_automatically"); + expect(sub?.collection_method ?? undefined).toBe("charge_automatically"); }); }); diff --git a/server/tests/advanced/multiFeature/multiFeature1.test.ts b/server/tests/advanced/multiFeature/multiFeature1.test.ts new file mode 100644 index 000000000..5cafc417b --- /dev/null +++ b/server/tests/advanced/multiFeature/multiFeature1.test.ts @@ -0,0 +1,236 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { ApiVersion, type AppEnv, LegacyVersion } from "@autumn/shared"; +import chalk from "chalk"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { + getPrepaidCusEnt, + getUsageCusEnt, +} from "tests/utils/cusProductUtils/cusEntSearchUtils.js"; +import { getMainCusProduct } from "tests/utils/cusProductUtils/cusProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { + constructArrearItem, + constructPrepaidItem, +} from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; + +// Scenario 1: prepaid + pay per use monthly -> prepaid + pay per use monthly +const pro = constructProduct({ + id: "multiFeature1Pro", + type: "pro", + excludeBase: true, + items: [ + constructPrepaidItem({ + featureId: TestFeature.Messages, + includedUsage: 50, + price: 10, + billingUnits: 1, + }), + constructArrearItem({ + featureId: TestFeature.Messages, + includedUsage: 0, + price: 0.5, + billingUnits: 1, + }), + ], +}); + +const premium = constructProduct({ + id: "multiFeature1Premium", + type: "premium", + excludeBase: true, + items: [ + // Prepaid + constructPrepaidItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + price: 15, + billingUnits: 1, + resetUsageWhenEnabled: false, + }), + // Pay per use + constructArrearItem({ + featureId: TestFeature.Messages, + includedUsage: 0, + price: 1, + billingUnits: 1, + }), + ], +}); + +export const getPrepaidAndUsageCusEnts = async ({ + customerId, + db, + orgId, + env, + featureId, +}: { + customerId: string; + db: DrizzleCli; + orgId: string; + env: AppEnv; + featureId: string; +}) => { + const mainCusProduct = await getMainCusProduct({ + customerId, + db, + orgId, + env, + }); + + const prepaidCusEnt = getPrepaidCusEnt({ + cusProduct: mainCusProduct!, + featureId, + }); + + const usageCusEnt = getUsageCusEnt({ + cusProduct: mainCusProduct!, + featureId, + }); + + return { prepaidCusEnt, usageCusEnt }; +}; + +const testCase = "multiFeature1"; +describe(`${chalk.yellowBright( + "multiFeature1: Testing prepaid + pay per use -> prepaid + pay per use", +)}`, () => { + const autumn: AutumnInt = new AutumnInt({ + secretKey: ctx.orgSecretKey, + version: ApiVersion.V1_2, + }); + const autumn2: AutumnInt = new AutumnInt({ + secretKey: ctx.orgSecretKey, + version: LegacyVersion.v1_2, + }); + const customerId = testCase; + + let totalUsage = 0; + const prepaidQuantity = 10; + const prepaidAllowance = 50 + prepaidQuantity; // pro.items[0].includedUsage + prepaidQuantity + const premiumPrepaidAllowance = 100 + prepaidQuantity; // premium.items[0].includedUsage + prepaidQuantity + + const optionsList = [ + { + feature_id: TestFeature.Messages, + quantity: prepaidQuantity, + }, + ]; + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro, premium], + prefix: testCase, + customerId, + }); + + await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: false, + }); + }); + + test("should attach pro product to customer", async () => { + await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + options: optionsList, + }); + + const { prepaidCusEnt, usageCusEnt } = await getPrepaidAndUsageCusEnts({ + customerId, + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + featureId: TestFeature.Messages, + }); + + expect(prepaidCusEnt?.balance).toBe(prepaidAllowance); + expect(usageCusEnt?.balance).toBe(0); // pro.items[1].includedUsage + }); + + test("should use prepaid allowance first", async () => { + const value = 60; + + await autumn.track({ + customer_id: customerId, + value, + feature_id: TestFeature.Messages, + }); + + totalUsage += value; + + await timeout(3000); + + const { prepaidCusEnt, usageCusEnt } = await getPrepaidAndUsageCusEnts({ + customerId, + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + featureId: TestFeature.Messages, + }); + + expect(prepaidCusEnt?.balance).toBe(prepaidAllowance - value); + expect(usageCusEnt?.balance).toBe(0); // pro.items[1].includedUsage + }); + + test("should have correct usage / invoice after upgrade", async () => { + const value = 60; + await autumn.track({ + customer_id: customerId, + value, + feature_id: TestFeature.Messages, + }); + + // totalUsage += value; + + await timeout(2500); + + const { usageCusEnt } = await getPrepaidAndUsageCusEnts({ + customerId, + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + featureId: TestFeature.Messages, + }); + + await autumn.attach({ + customer_id: customerId, + product_id: premium.id, + options: optionsList, + }); + + const { prepaidCusEnt, usageCusEnt: newUsageCusEnt } = + await getPrepaidAndUsageCusEnts({ + customerId, + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + featureId: TestFeature.Messages, + }); + + // Check invoice too + const { invoices } = await autumn2.customers.get(customerId); + const invoice1Amount = 15 * prepaidQuantity - 10 * prepaidQuantity; // premium.items[0].price * prepaidQuantity - pro.items[0].price * prepaidQuantity + const invoice0Amount = value * 0.5; // value * pro.items[1].price + const totalAmount = invoice1Amount + invoice0Amount; + expect(invoices![0].total).toBe(totalAmount); + + // const leftover = premiumPrepaidAllowance - totalUsage + value; + // console.log( + // `Premium prepaid allowance: ${premiumPrepaidAllowance} - totalUsage: ${totalUsage}`, + // ); + // console.log(`prepaidCusEnt?.balance: ${prepaidCusEnt?.balance}`); + expect(prepaidCusEnt?.balance).toBe(premiumPrepaidAllowance - totalUsage); + expect(newUsageCusEnt?.balance).toBe(0); + }); +}); diff --git a/server/tests/advanced/multiFeature/multiFeature2.test.ts b/server/tests/advanced/multiFeature/multiFeature2.test.ts new file mode 100644 index 000000000..74678e191 --- /dev/null +++ b/server/tests/advanced/multiFeature/multiFeature2.test.ts @@ -0,0 +1,200 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { ApiVersion, type AppEnv, LegacyVersion } from "@autumn/shared"; +import chalk from "chalk"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { + getLifetimeFreeCusEnt, + getUsageCusEnt, +} from "tests/utils/cusProductUtils/cusEntSearchUtils.js"; +import { getMainCusProduct } from "tests/utils/cusProductUtils/cusProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { + constructArrearItem, + constructFeatureItem, +} from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; + +// Scenario 1: lifetime + pay per use monthly -> pay per use monthly +const pro = constructProduct({ + id: "multiFeature2Pro", + type: "pro", + excludeBase: true, + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 50, + interval: null, + }), + constructArrearItem({ + featureId: TestFeature.Messages, + includedUsage: 0, + price: 0.5, + billingUnits: 1, + }), + ], +}); + +const premium = constructProduct({ + id: "multiFeature2Premium", + type: "premium", + excludeBase: true, + items: [ + // Pay per use + constructArrearItem({ + featureId: TestFeature.Messages, + includedUsage: 0, + price: 1, + billingUnits: 1, + }), + ], +}); + +export const getLifetimeAndUsageCusEnts = async ({ + customerId, + db, + orgId, + env, + featureId, +}: { + customerId: string; + db: DrizzleCli; + orgId: string; + env: AppEnv; + featureId: string; +}) => { + const mainCusProduct = await getMainCusProduct({ + customerId: customerId, + db, + orgId, + env, + }); + + const lifetimeCusEnt = getLifetimeFreeCusEnt({ + cusProduct: mainCusProduct!, + featureId, + }); + + const usageCusEnt = getUsageCusEnt({ + cusProduct: mainCusProduct!, + featureId, + }); + + return { lifetimeCusEnt, usageCusEnt }; +}; + +const testCase = "multiFeature2"; +describe(`${chalk.yellowBright( + "multiFeature2: Testing lifetime + pay per use -> pay per use", +)}`, () => { + const autumn: AutumnInt = new AutumnInt({ + secretKey: ctx.orgSecretKey, + version: ApiVersion.V1_2, + }); + const autumn2: AutumnInt = new AutumnInt({ + secretKey: ctx.orgSecretKey, + version: LegacyVersion.v1_2, + }); + const customerId = testCase; + + let totalUsage = 0; + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro, premium], + prefix: testCase, + customerId, + }); + + await initCustomerV3({ + ctx, + customerId, + attachPm: "success", + withTestClock: false, + }); + }); + + test("should attach pro product to customer", async () => { + await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + }); + + const { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ + customerId, + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + featureId: TestFeature.Messages, + }); + + expect(lifetimeCusEnt?.balance).toBe(50); // pro.items[0].includedUsage + + expect(usageCusEnt?.balance).toBe(0); // pro.items[1].includedUsage + }); + + test("should use lifetime allowance first", async () => { + const value = 50; // pro.items[0].includedUsage + + await autumn.events.send({ + customerId, + value, + featureId: TestFeature.Messages, + }); + + totalUsage += value; + + await timeout(3000); + + const { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ + customerId, + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + featureId: TestFeature.Messages, + }); + + expect(lifetimeCusEnt?.balance).toBe(50 - value); // pro.items[0].includedUsage - value + expect(usageCusEnt?.balance).toBe(0); // pro.items[1].includedUsage + }); + + test("should have correct usage after upgrade", async () => { + const value = 20; + + await autumn.track({ + customer_id: customerId, + value, + feature_id: TestFeature.Messages, + }); + + await autumn.attach({ + customer_id: customerId, + product_id: premium.id, + }); + + // return; + const { lifetimeCusEnt, usageCusEnt: newUsageCusEnt } = + await getLifetimeAndUsageCusEnts({ + customerId, + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + featureId: TestFeature.Messages, + }); + + expect(lifetimeCusEnt).toBeUndefined(); + expect(newUsageCusEnt?.balance).toBe(0); + + // Check invoice too + // const res = await autumn2.customers.get(customerId); + // const invoices = res.invoices; + + // const invoice0Amount = value * 0.5; // value * pro.items[1].price + // expect(invoices![0].total).toBe(invoice0Amount); + }); +}); diff --git a/server/tests/advanced/multiFeature/multiFeature3.test.ts b/server/tests/advanced/multiFeature/multiFeature3.test.ts new file mode 100644 index 000000000..cc10ce442 --- /dev/null +++ b/server/tests/advanced/multiFeature/multiFeature3.test.ts @@ -0,0 +1,177 @@ +/** biome-ignore-all lint/suspicious/noExportsInTest: needed */ + +import { beforeAll, describe, expect, test } from "bun:test"; +import { ApiVersion, type AppEnv } from "@autumn/shared"; +import chalk from "chalk"; +import { addMonths } from "date-fns"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { + getLifetimeFreeCusEnt, + getUsageCusEnt, +} from "tests/utils/cusProductUtils/cusEntSearchUtils.js"; +import { getMainCusProduct } from "tests/utils/cusProductUtils/cusProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { + constructArrearItem, + constructFeatureItem, +} from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; + +// Scenario 1: lifetime + pay per use monthly -> lifetime + pay per use monthly +const pro = constructProduct({ + type: "pro", + excludeBase: true, + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 50, + interval: null, + }), + constructArrearItem({ + featureId: TestFeature.Messages, + includedUsage: 0, + price: 0.5, + billingUnits: 1, + }), + ], +}); + +export const getLifetimeAndUsageCusEnts = async ({ + customerId, + db, + orgId, + env, + featureId, +}: { + customerId: string; + db: DrizzleCli; + orgId: string; + env: AppEnv; + featureId: string; +}) => { + const mainCusProduct = await getMainCusProduct({ + customerId, + db, + orgId, + env, + }); + + const lifetimeCusEnt = getLifetimeFreeCusEnt({ + cusProduct: mainCusProduct!, + featureId, + }); + + const usageCusEnt = getUsageCusEnt({ + cusProduct: mainCusProduct!, + featureId, + }); + + return { lifetimeCusEnt, usageCusEnt }; +}; + +const testCase = "multiFeature3"; +// UNCOMMENT FROM HERE +describe(`${chalk.yellowBright( + `${testCase}: Testing lifetime + pay per use, advance test clock`, +)}`, () => { + const autumn: AutumnInt = new AutumnInt({ + secretKey: ctx.orgSecretKey, + version: ApiVersion.V1_2, + }); + const customerId = testCase; + + let totalUsage = 0; + + let testClockId: string; + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = res.testClockId!; + }); + + test("should attach pro product to customer", async () => { + await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + }); + + const { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ + customerId, + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + featureId: TestFeature.Messages, + }); + + expect(lifetimeCusEnt?.balance).toBe(50); // pro.items[0].includedUsage + + expect(usageCusEnt?.balance).toBe(0); // pro.items[1].includedUsage + }); + + const overageValue = 30; + test("should use lifetime allowance + overage", async () => { + let value = 50; // pro.items[0].includedUsage + value += overageValue; + + await autumn.track({ + customer_id: customerId, + value, + feature_id: TestFeature.Messages, + }); + + totalUsage += value; + + await timeout(3000); + + const { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ + customerId, + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + featureId: TestFeature.Messages, + }); + + expect(lifetimeCusEnt?.balance).toBe(0); + expect(usageCusEnt?.balance).toBe(-overageValue); + }); + + test("cycle 1:should have correct usage after first cycle", async () => { + const advanceTo = addMonths(new Date(), 1).getTime(); + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo, + waitForSeconds: 20, + }); + + const { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ + customerId, + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + featureId: TestFeature.Messages, + }); + + expect(lifetimeCusEnt?.balance).toBe(0); + expect(usageCusEnt?.balance).toBe(0); + }); +}); diff --git a/server/tests/advanced/referrals/referrals1.backup.ts b/server/tests/advanced/referrals/referrals1.backup.ts deleted file mode 100644 index 2ed5f38ad..000000000 --- a/server/tests/advanced/referrals/referrals1.backup.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { - type AppEnv, - ErrCode, - type Organization, - type ReferralCode, - type RewardRedemption, -} from "@autumn/shared"; -import { assert } from "chai"; -import chalk from "chalk"; -import { addDays } from "date-fns"; -import type { Stripe } from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { timeout } from "tests/utils/genUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { products, referralPrograms } from "../../global.js"; - -const pro = constructProduct({ - id: "pro", - items: [constructFeatureItem({ featureId: TestFeature.Words })], - type: "pro", - trial: true, -}); - -// UNCOMMENT FROM HERE -describe(`${chalk.yellowBright( - "referrals1: Testing referrals (on checkout)", -)}`, () => { - const mainCustomerId = "main-referral-1"; - const alternateCustomerId = "alternate-referral-1"; - const redeemers = ["referral1-r1", "referral1-r2", "referral1-r3"]; - const autumn: AutumnInt = new AutumnInt(); - let stripeCli: Stripe; - let testClockId: string; - let referralCode: ReferralCode; - - const redemptions: RewardRedemption[] = []; - let mainCustomer: any; - let db: DrizzleCli; - let org: Organization; - let env: AppEnv; - - before(async function () { - await setupBefore(this); - stripeCli = this.stripeCli; - db = this.db; - org = this.org; - env = this.env; - - addPrefixToProducts({ - products: [pro], - prefix: mainCustomerId, - }); - - await createProducts({ - autumn: this.autumnJs, - products: [pro], - db, - orgId: org.id, - env, - customerId: mainCustomerId, - }); - - const res = await initCustomer({ - autumn: this.autumnJs, - customerId: mainCustomerId, - fingerprint: "main-referral-1", - db, - org, - env, - attachPm: "success", - }); - - mainCustomer = res.customer; - testClockId = res.testClockId; - - await autumn.attach({ - customer_id: mainCustomerId, - product_id: pro.id, - }); - - const batchCreate = []; - for (const redeemer of redeemers) { - batchCreate.push( - initCustomer({ - autumn: this.autumnJs, - customerId: redeemer, - db: this.db, - org: this.org, - env: this.env, - attachPm: "success", - }), - ); - } - - batchCreate.push( - initCustomer({ - autumn: this.autumnJs, - customerId: alternateCustomerId, - fingerprint: "main-referral-1", - db: this.db, - org: this.org, - env: this.env, - attachPm: "success", - }), - ); - await Promise.all(batchCreate); - }); - - it("should create code once", async () => { - referralCode = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.onCheckout.id, - }); - - assert.exists(referralCode.code); - - // Get referral code again - const referralCode2 = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.onCheckout.id, - }); - - assert.equal(referralCode2.code, referralCode.code); - }); - - it("should fail if same customer tries to redeem code again", async () => { - try { - await autumn.referrals.redeem({ - customerId: mainCustomerId, - code: referralCode.code, - }); - assert.fail("Own customer should not be able to redeem code"); - } catch (error) { - assert.instanceOf(error, AutumnError); - assert.equal(error.code, ErrCode.CustomerCannotRedeemOwnCode); - } - - try { - await autumn.referrals.redeem({ - customerId: alternateCustomerId, - code: referralCode.code, - }); - assert.fail( - "Own customer (same fingerprint) should not be able to redeem code", - ); - } catch (error) { - assert.instanceOf(error, AutumnError); - assert.equal(error.code, ErrCode.CustomerCannotRedeemOwnCode); - } - }); - - it("should create redemption for each redeemer and fail if redeemed again", async () => { - for (const redeemer of redeemers) { - const redemption: RewardRedemption = await autumn.referrals.redeem({ - customerId: redeemer, - code: referralCode.code, - }); - - redemptions.push(redemption); - } - - // Try redeem for redeemer1 again - try { - const redemption1 = await autumn.referrals.redeem({ - customerId: redeemers[0], - code: referralCode.code, - }); - assert.fail("Should not be able to redeem again"); - } catch (error) { - assert.instanceOf(error, AutumnError); - assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode); - } - }); - - // return; - - it("should be triggered (and applied) when redeemers check out", async () => { - for (let i = 0; i < redeemers.length; i++) { - const redeemer = redeemers[i]; - - await autumn.attach({ - customer_id: redeemer, - product_id: products.pro.id, - }); - - await timeout(3000); - - // Get redemption object - const redemption = await autumn.redemptions.get(redemptions[i].id); - - // Check if redemption is triggered - const count = i + 1; - - if (count > referralPrograms.onCheckout.max_redemptions) { - assert.equal(redemption.triggered, false); - assert.equal(redemption.applied, false); - } else { - assert.equal(redemption.triggered, true); - assert.equal(redemption.applied, i === 0); - } - - // Check stripe customer - const stripeCus = (await stripeCli.customers.retrieve( - mainCustomer.processor?.id, - )) as Stripe.Customer; - - assert.notEqual(stripeCus.discount, null); - } - }); - - let curTime = new Date(); - it("customer should have discount for first purchase", async () => { - curTime = addDays(addDays(curTime, 7), 4); - await advanceTestClock({ - testClockId, - advanceTo: curTime.getTime(), - stripeCli, - }); - - // 1. Get invoice - const { invoices } = await autumn.customers.get(mainCustomerId); - assert.equal(invoices.length, 2); - assert.equal(invoices[0].total, 0); - }); - - // it("customer should have discount for second purchase", async function () { - // // 2. Check that customer has another discount - // let stripeCus = (await stripeCli.customers.retrieve( - // mainCustomer.processor?.id, - // )) as Stripe.Customer; - - // assert.notEqual(stripeCus.discount, null); - - // // 2. Advance test clock to 1 month from start (trigger discount.deleted event) - // curTime = addHours(addMonths(new Date(), 1), 2); - // await advanceTestClock({ - // testClockId, - // advanceTo: curTime.getTime(), - // stripeCli, - // }); - - // // 3. Advance test clock to 1 month + 12 days from start (trigger new invoice) - // curTime = addDays(curTime, 12); - // await advanceTestClock({ - // testClockId, - // advanceTo: curTime.getTime(), - // stripeCli, - // }); - - // // // 3. Get invoice again - // let { invoices: invoices2 } = await autumn.customers.get(mainCustomerId); - - // assert.equal(invoices2.length, 3); - // assert.equal(invoices2[0].total, 0); - // }); -}); - -// const { testClockId: testClockId1, customer } = -// await initCustomerWithTestClock({ -// customerId: mainCustomerId, -// db: this.db, -// org: this.org, -// env: this.env, -// fingerprint: "main-referral-1", -// }); -// testClockId = testClockId1; -// mainCustomer = customer; - -// await autumn.attach({ -// customer_id: mainCustomerId, -// product_id: products.proWithTrial.id, -// }); - -// initCustomer({ -// customer_data: { -// id: alternateCustomerId, -// name: "Alternate Referral 1", -// email: "alternate-referral-1@example.com", -// fingerprint: "main-referral-1", -// }, -// db: this.db, -// org: this.org, -// env: this.env, -// }) diff --git a/server/tests/advanced/referrals/referrals1.test.ts b/server/tests/advanced/referrals/referrals1.test.ts index 8dee36af0..6a2442adb 100644 --- a/server/tests/advanced/referrals/referrals1.test.ts +++ b/server/tests/advanced/referrals/referrals1.test.ts @@ -1,34 +1,73 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, + CouponDurationType, + type CreateReward, + type CreateRewardProgram, ErrCode, type Organization, type ReferralCode, + RewardReceivedBy, type RewardRedemption, + RewardTriggerEvent, + RewardType, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import { addDays } from "date-fns"; import type { Stripe } from "stripe"; import { TestFeature } from "tests/setup/v2Features.js"; import { timeout } from "tests/utils/genUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; +import { createReferralProgram } from "tests/utils/productUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; -import { products, referralPrograms } from "../../global.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -const pro = constructProduct({ +const testCase = "referrals1"; + +const proWithTrial = constructProduct({ id: "pro", items: [constructFeatureItem({ featureId: TestFeature.Words })], type: "pro", trial: true, }); +const pro = constructProduct({ + id: "proNoTrial", + items: [constructFeatureItem({ featureId: TestFeature.Words })], + type: "pro", +}); + +// Reward: 100% discount for 1 month +const monthOffReward: CreateReward = { + id: `${testCase}MonthOff`, + + name: "Month Off", + type: RewardType.PercentageDiscount, + promo_codes: [], + discount_config: { + discount_value: 100, + duration_type: CouponDurationType.Months, + duration_value: 1, + apply_to_all: true, + price_ids: [], + }, +}; + +// Referral program: triggers on checkout, applies to pro and proWithTrial +const onCheckoutProgram: CreateRewardProgram = { + id: `${testCase}OnCheckout`, + when: RewardTriggerEvent.Checkout, + product_ids: [proWithTrial.id, pro.id], + internal_reward_id: monthOffReward.id, + max_redemptions: 2, + received_by: RewardReceivedBy.Referrer, +}; + describe(`${chalk.yellowBright( "referrals1: Testing referrals (on checkout)", )}`, () => { @@ -52,18 +91,26 @@ describe(`${chalk.yellowBright( org = ctx.org; env = ctx.env; - addPrefixToProducts({ - products: [pro], - prefix: mainCustomerId, + await initProductsV0({ + ctx, + products: [proWithTrial, pro], + prefix: testCase, + customerId: mainCustomerId, }); - await createProducts({ - autumn: new AutumnInt({ secretKey: ctx.orgSecretKey }), - products: [pro], + // Create referral program - product IDs are already prefixed by initProductsV0 + const referralProgram: CreateRewardProgram = { + ...onCheckoutProgram, + product_ids: [proWithTrial.id, pro.id], + }; + + await createReferralProgram({ db, orgId: org.id, env, - customerId: mainCustomerId, + autumn: new AutumnInt({ secretKey: ctx.orgSecretKey }), + reward: monthOffReward, + rewardProgram: referralProgram, }); const res = await initCustomerV3({ @@ -78,7 +125,7 @@ describe(`${chalk.yellowBright( await autumn.attach({ customer_id: mainCustomerId, - product_id: pro.id, + product_id: proWithTrial.id, }); const batchCreate = []; @@ -106,7 +153,7 @@ describe(`${chalk.yellowBright( test("should create code once", async () => { referralCode = await autumn.referrals.createCode({ customerId: mainCustomerId, - referralId: referralPrograms.onCheckout.id, + referralId: onCheckoutProgram.id, }); expect(referralCode.code).toBeDefined(); @@ -114,7 +161,7 @@ describe(`${chalk.yellowBright( // Get referral code again const referralCode2 = await autumn.referrals.createCode({ customerId: mainCustomerId, - referralId: referralPrograms.onCheckout.id, + referralId: onCheckoutProgram.id, }); expect(referralCode2.code).toBe(referralCode.code); @@ -129,7 +176,9 @@ describe(`${chalk.yellowBright( throw new Error("Own customer should not be able to redeem code"); } catch (error) { expect(error).toBeInstanceOf(AutumnError); - expect((error as AutumnError).code).toBe(ErrCode.CustomerCannotRedeemOwnCode); + expect((error as AutumnError).code).toBe( + ErrCode.CustomerCannotRedeemOwnCode, + ); } try { @@ -142,7 +191,9 @@ describe(`${chalk.yellowBright( ); } catch (error) { expect(error).toBeInstanceOf(AutumnError); - expect((error as AutumnError).code).toBe(ErrCode.CustomerCannotRedeemOwnCode); + expect((error as AutumnError).code).toBe( + ErrCode.CustomerCannotRedeemOwnCode, + ); } }); @@ -158,14 +209,16 @@ describe(`${chalk.yellowBright( // Try redeem for redeemer1 again try { - const redemption1 = await autumn.referrals.redeem({ + await autumn.referrals.redeem({ customerId: redeemers[0], code: referralCode.code, }); throw new Error("Should not be able to redeem again"); } catch (error) { expect(error).toBeInstanceOf(AutumnError); - expect((error as AutumnError).code).toBe(ErrCode.CustomerAlreadyRedeemedReferralCode); + expect((error as AutumnError).code).toBe( + ErrCode.CustomerAlreadyRedeemedReferralCode, + ); } }); @@ -175,7 +228,7 @@ describe(`${chalk.yellowBright( await autumn.attach({ customer_id: redeemer, - product_id: products.pro.id, + product_id: pro.id, }); await timeout(3000); @@ -186,7 +239,7 @@ describe(`${chalk.yellowBright( // Check if redemption is triggered const count = i + 1; - if (count > referralPrograms.onCheckout.max_redemptions) { + if (count > onCheckoutProgram.max_redemptions!) { expect(redemption.triggered).toBe(false); expect(redemption.applied).toBe(false); } else { diff --git a/server/tests/advanced/referrals/referrals2.backup.ts b/server/tests/advanced/referrals/referrals2.backup.ts deleted file mode 100644 index 1aa238e1c..000000000 --- a/server/tests/advanced/referrals/referrals2.backup.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { - type AppEnv, - type Customer, - ErrCode, - type Organization, - type ReferralCode, - type RewardRedemption, -} from "@autumn/shared"; -import { assert } from "chai"; -import chalk from "chalk"; -import { addDays } from "date-fns"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { timeout } from "tests/utils/genUtils.js"; -import { initCustomer } from "tests/utils/init.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { createStripeCli } from "@/external/connect/createStripeCli.js"; -import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js"; -import { products, referralPrograms } from "../../global.js"; - -// UNCOMMENT FROM HERE -describe(`${chalk.yellowBright( - "referrals2: Testing referrals (immediate redemption)", -)}`, () => { - const mainCustomerId = "main-referral-2"; - const redeemers = ["referral2-r1", "referral2-r2", "referral2-r3"]; - const autumn: AutumnInt = new AutumnInt(); - let stripeCli: Stripe; - let testClockId: string; - let referralCode: ReferralCode; - - const redemptions: RewardRedemption[] = []; - let mainCustomer: Customer; - let org: Organization; - let env: AppEnv; - before(async function () { - await setupBefore(this); - stripeCli = this.stripeCli; - org = this.org; - env = this.env; - - const { testClockId: testClockId1, customer } = await initCustomerV2({ - customerId: mainCustomerId, - db: this.db, - org: this.org, - env: this.env, - autumn, - }); - testClockId = testClockId1; - mainCustomer = customer; - - const batchCreate = []; - for (const redeemer of redeemers) { - batchCreate.push( - initCustomer({ - customerId: redeemer, - db: this.db, - org: this.org, - env: this.env, - attachPm: true, - }), - ); - } - - await Promise.all(batchCreate); - }); - - it("should create code once", async () => { - referralCode = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.immediate.id, - }); - - assert.exists(referralCode.code); - }); - - it("should create redemption for each redeemer and fail if redeemed again", async () => { - for (let i = 0; i < redeemers.length; i++) { - const redeemer = redeemers[i]; - const count = i + 1; - try { - const redemption: RewardRedemption = await autumn.referrals.redeem({ - customerId: redeemer, - code: referralCode.code, - }); - redemptions.push(redemption); - - if (count > referralPrograms.immediate.max_redemptions) { - assert.equal(redemption.triggered, false); - assert.equal(redemption.applied, false); - } else { - assert.fail("Should not be able to redeem again"); - } - } catch (error) { - if (count > referralPrograms.immediate.max_redemptions) { - assert.instanceOf(error, AutumnError); - assert.equal(error.code, ErrCode.ReferralCodeMaxRedemptionsReached); - } - } - } - - // Check stripe customer - const legacyStripe = createStripeCli({ - org: org, - env: env, - legacyVersion: true, - }); - - const stripeCus = (await legacyStripe.customers.retrieve( - mainCustomer.processor?.id, - { - expand: ["discount"], - }, - )) as Stripe.Customer; - - assert.notEqual(stripeCus.discount, null); - }); - - let curTime = new Date(); - it("customer should have discount for first purchase", async () => { - await autumn.attach({ - customer_id: mainCustomerId, - product_id: products.proWithTrial.id, - }); - - await timeout(3000); - - curTime = addDays(addDays(curTime, 7), 4); - await advanceTestClock({ - testClockId, - advanceTo: curTime.getTime(), - stripeCli, - waitForSeconds: 30, - }); - - // 1. Get invoice - const { invoices } = await autumn.customers.get(mainCustomerId); - - assert.equal(invoices!.length, 2); - assert.equal(invoices![0].total, 0); - }); - - // it("customer should have discount for second purchase", async function () { - // // 2. Check that customer has another discount - // let stripeCus = (await stripeCli.customers.retrieve( - // mainCustomer.processor?.id, - // )) as Stripe.Customer; - - // assert.notEqual(stripeCus.discount, null); - - // // 2. Advance test clock to 1 month from start (trigger discount.deleted event) - // curTime = addHours(addMonths(new Date(), 1), 2); - // await advanceTestClock({ - // testClockId, - // advanceTo: curTime.getTime(), - // stripeCli, - // }); - - // // 3. Advance test clock to 1 month + 7 days from start (trigger new invoice) - // curTime = addDays(curTime, 8); - // await advanceTestClock({ - // testClockId, - // advanceTo: curTime.getTime(), - // stripeCli, - // }); - - // // // 3. Get invoice again - // let { invoices: invoices2 } = await autumn.customers.get(mainCustomerId); - - // assert.equal(invoices2!.length, 3); - // assert.equal(invoices2![0].total, 0); - // }); -}); diff --git a/server/tests/advanced/referrals/referrals2.test.ts b/server/tests/advanced/referrals/referrals2.test.ts index 412e5b8eb..33071a2ce 100644 --- a/server/tests/advanced/referrals/referrals2.test.ts +++ b/server/tests/advanced/referrals/referrals2.test.ts @@ -1,22 +1,66 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, + CouponDurationType, + type CreateReward, + type CreateRewardProgram, type Customer, ErrCode, type Organization, type ReferralCode, + RewardReceivedBy, type RewardRedemption, + RewardTriggerEvent, + RewardType, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import { addDays } from "date-fns"; import type { Stripe } from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; import { timeout } from "tests/utils/genUtils.js"; +import { createReferralProgram } from "tests/utils/productUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; -import { products, referralPrograms } from "../../global.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; + +const testCase = "referrals2"; + +const proWithTrial = constructProduct({ + id: "pro", + items: [constructFeatureItem({ featureId: TestFeature.Words })], + type: "pro", + trial: true, +}); + +// Reward: 100% discount for 1 month +const monthOffReward: CreateReward = { + id: `${testCase}MonthOff`, + name: "Month Off", + type: RewardType.PercentageDiscount, + promo_codes: [], + discount_config: { + discount_value: 100, + duration_type: CouponDurationType.Months, + duration_value: 1, + apply_to_all: true, + price_ids: [], + }, +}; + +// Referral program: triggers immediately on customer creation +const immediateProgram: CreateRewardProgram = { + id: `${testCase}Immediate`, + when: RewardTriggerEvent.CustomerCreation, + product_ids: [], + internal_reward_id: monthOffReward.id, + max_redemptions: 2, + received_by: RewardReceivedBy.Referrer, +}; describe(`${chalk.yellowBright( "referrals2: Testing referrals (immediate redemption)", @@ -38,6 +82,23 @@ describe(`${chalk.yellowBright( org = ctx.org; env = ctx.env; + await initProductsV0({ + ctx, + products: [proWithTrial], + prefix: testCase, + customerId: mainCustomerId, + }); + + // Create referral program + await createReferralProgram({ + db: ctx.db, + orgId: org.id, + env, + autumn: new AutumnInt({ secretKey: ctx.orgSecretKey }), + reward: monthOffReward, + rewardProgram: immediateProgram, + }); + const { testClockId: testClockId1, customer } = await initCustomerV3({ ctx, customerId: mainCustomerId, @@ -62,7 +123,7 @@ describe(`${chalk.yellowBright( test("should create code once", async () => { referralCode = await autumn.referrals.createCode({ customerId: mainCustomerId, - referralId: referralPrograms.immediate.id, + referralId: immediateProgram.id, }); expect(referralCode.code).toBeDefined(); @@ -79,16 +140,18 @@ describe(`${chalk.yellowBright( }); redemptions.push(redemption); - if (count > referralPrograms.immediate.max_redemptions) { + if (count > immediateProgram.max_redemptions!) { expect(redemption.triggered).toBe(false); expect(redemption.applied).toBe(false); } else { throw new Error("Should not be able to redeem again"); } } catch (error) { - if (count > referralPrograms.immediate.max_redemptions) { + if (count > immediateProgram.max_redemptions!) { expect(error).toBeInstanceOf(AutumnError); - expect((error as AutumnError).code).toBe(ErrCode.ReferralCodeMaxRedemptionsReached); + expect((error as AutumnError).code).toBe( + ErrCode.ReferralCodeMaxRedemptionsReached, + ); } } } @@ -114,7 +177,7 @@ describe(`${chalk.yellowBright( test("customer should have discount for first purchase", async () => { await autumn.attach({ customer_id: mainCustomerId, - product_id: products.proWithTrial.id, + product_id: proWithTrial.id, }); await timeout(3000); diff --git a/server/tests/advanced/referrals/referrals3.backup.ts b/server/tests/advanced/referrals/referrals3.backup.ts deleted file mode 100644 index 500f5294c..000000000 --- a/server/tests/advanced/referrals/referrals3.backup.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { - type Customer, - ErrCode, - type ReferralCode, - type RewardRedemption, -} from "@autumn/shared"; -import { assert } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { compareProductEntitlements } from "tests/utils/compare.js"; -import { timeout } from "tests/utils/genUtils.js"; -import { initCustomer } from "tests/utils/init.js"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { features, products, referralPrograms } from "../../global.js"; - -// UNCOMMENT FROM HERE -describe(`${chalk.yellowBright( - "referrals3: Testing free product referrals", -)}`, () => { - const mainCustomerId = "main-referral-3"; - const redeemers = ["referral3-r1", "referral3-r2", "referral3-r3"]; - let autumn: AutumnInt = new AutumnInt(); - let stripeCli: Stripe; - let testClockId: string; - let referralCode: ReferralCode; - - const redemptions: RewardRedemption[] = []; - let mainCustomer: Customer; - - before(async function () { - await setupBefore(this); - autumn = this.autumn; - stripeCli = this.stripeCli; - - const { testClockId: testClockId1, customer } = - await initCustomerWithTestClock({ - customerId: mainCustomerId, - db: this.db, - org: this.org, - env: this.env, - fingerprint: "main-referral-3", - }); - testClockId = testClockId1; - mainCustomer = customer; - - await autumn.attach({ - customer_id: mainCustomerId, - product_id: products.proWithTrial.id, - }); - - const batchCreate = []; - for (const redeemer of redeemers) { - batchCreate.push( - initCustomer({ - customerId: redeemer, - db: this.db, - org: this.org, - env: this.env, - attachPm: true, - }), - ); - } - - await Promise.all(batchCreate); - }); - - it("should create code once", async () => { - referralCode = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.freeProduct.id, - }); - - assert.exists(referralCode.code); - }); - - it("should create redemption for each redeemer and fail if redeemed again", async () => { - for (const redeemer of redeemers) { - const redemption: RewardRedemption = await autumn.referrals.redeem({ - customerId: redeemer, - code: referralCode.code, - }); - - redemptions.push(redemption); - - // assert.equal(redemption.triggered, false); - // assert.equal(redemption.applied, false); - } - - // Try redeem for redeemer1 again - try { - const redemption1 = await autumn.referrals.redeem({ - customerId: redeemers[0], - code: referralCode.code, - }); - assert.fail("Should not be able to redeem again"); - } catch (error) { - assert.instanceOf(error, AutumnError); - assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode); - } - }); - - it("should be triggered (and applied) when redeemers check out", async () => { - for (let i = 0; i < redeemers.length; i++) { - const redeemer = redeemers[i]; - - await autumn.attach({ - customer_id: redeemer, - product_id: products.pro.id, - }); - - await timeout(3000); - - // Get redemption object - const redemption = await autumn.redemptions.get(redemptions[i].id); - - // Check if redemption is triggered - const count = i + 1; - - if (count > referralPrograms.freeProduct.max_redemptions) { - assert.equal(redemption.triggered, false); - assert.equal(redemption.applied, false); - } else { - // 1. Check that main customer has free add on - compareProductEntitlements({ - customerId: mainCustomerId, - product: products.freeAddOn, - features, - quantity: count, - }); - - compareProductEntitlements({ - customerId: redeemer, - product: products.freeAddOn, - features, - }); - } - } - }); -}); diff --git a/server/tests/advanced/referrals/referrals3.test.ts b/server/tests/advanced/referrals/referrals3.test.ts index cc1607016..bded0cde7 100644 --- a/server/tests/advanced/referrals/referrals3.test.ts +++ b/server/tests/advanced/referrals/referrals3.test.ts @@ -1,47 +1,122 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { - type Customer, + ApiVersion, + type CreateReward, + type CreateRewardProgram, ErrCode, type ReferralCode, + RewardReceivedBy, type RewardRedemption, + RewardTriggerEvent, + RewardType, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { compareProductEntitlements } from "tests/utils/compare.js"; +import { TestFeature } from "tests/setup/v2Features.js"; import { timeout } from "tests/utils/genUtils.js"; +import { createReferralProgram } from "tests/utils/productUtils.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; -import { features, products, referralPrograms } from "../../global.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { expectProductAttached } from "../../utils/expectUtils/expectProductAttached.js"; + +const testCase = "referrals3"; + +const proWithTrial = constructProduct({ + id: "pro", + items: [constructFeatureItem({ featureId: TestFeature.Words })], + type: "pro", + trial: true, +}); + +const pro = constructProduct({ + id: "proNoTrial", + items: [constructFeatureItem({ featureId: TestFeature.Words })], + type: "pro", +}); + +const freeAddOn = constructProduct({ + id: "freeAddOn", + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + interval: null, + }), + ], + type: "free", + isAddOn: true, + isDefault: false, +}); + +// Reward: Free product reward +const freeProductReward: CreateReward = { + id: `${testCase}FreeProduct`, + name: "Free Product", + type: RewardType.FreeProduct, + promo_codes: [], + free_product_id: freeAddOn.id, +}; + +// Referral program: triggers on checkout, applies to pro and proWithTrial +const freeProductProgram: CreateRewardProgram = { + id: `${testCase}FreeProduct`, + when: RewardTriggerEvent.Checkout, + product_ids: [proWithTrial.id, pro.id], + internal_reward_id: freeProductReward.id, + max_redemptions: 2, + received_by: RewardReceivedBy.All, +}; describe(`${chalk.yellowBright( "referrals3: Testing free product referrals", )}`, () => { const mainCustomerId = "main-referral-3"; const redeemers = ["referral3-r1", "referral3-r2", "referral3-r3"]; - let autumn: AutumnInt = new AutumnInt(); - let stripeCli: Stripe; - let testClockId: string; + const autumn: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 }); let referralCode: ReferralCode; - const redemptions: RewardRedemption[] = []; - let mainCustomer: Customer; beforeAll(async () => { - autumn = new AutumnInt({ secretKey: ctx.orgSecretKey }); - stripeCli = ctx.stripeCli; + await initProductsV0({ + ctx, + products: [proWithTrial, pro, freeAddOn], + prefix: testCase, + customerId: mainCustomerId, + }); - const { testClockId: testClockId1, customer } = await initCustomerV3({ + // Create referral program - product IDs are already prefixed by initProductsV0 + const referralProgram: CreateRewardProgram = { + ...freeProductProgram, + product_ids: [proWithTrial.id, pro.id], + }; + + // Update reward with prefixed free product ID + const reward: CreateReward = { + ...freeProductReward, + free_product_id: freeAddOn.id, + }; + + await createReferralProgram({ + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + autumn, + reward, + rewardProgram: referralProgram, + }); + + await initCustomerV3({ ctx, customerId: mainCustomerId, customerData: { fingerprint: "main-referral-3" }, }); - testClockId = testClockId1; - mainCustomer = customer; await autumn.attach({ customer_id: mainCustomerId, - product_id: products.proWithTrial.id, + product_id: proWithTrial.id, }); const batchCreate = []; @@ -61,7 +136,7 @@ describe(`${chalk.yellowBright( test("should create code once", async () => { referralCode = await autumn.referrals.createCode({ customerId: mainCustomerId, - referralId: referralPrograms.freeProduct.id, + referralId: freeProductProgram.id, }); expect(referralCode.code).toBeDefined(); @@ -79,14 +154,16 @@ describe(`${chalk.yellowBright( // Try redeem for redeemer1 again try { - const redemption1 = await autumn.referrals.redeem({ + await autumn.referrals.redeem({ customerId: redeemers[0], code: referralCode.code, }); throw new Error("Should not be able to redeem again"); } catch (error) { expect(error).toBeInstanceOf(AutumnError); - expect((error as AutumnError).code).toBe(ErrCode.CustomerAlreadyRedeemedReferralCode); + expect((error as AutumnError).code).toBe( + ErrCode.CustomerAlreadyRedeemedReferralCode, + ); } }); @@ -96,7 +173,7 @@ describe(`${chalk.yellowBright( await autumn.attach({ customer_id: redeemer, - product_id: products.pro.id, + product_id: pro.id, }); await timeout(3000); @@ -107,22 +184,22 @@ describe(`${chalk.yellowBright( // Check if redemption is triggered const count = i + 1; - if (count > referralPrograms.freeProduct.max_redemptions) { + if (count > freeProductProgram.max_redemptions!) { expect(redemption.triggered).toBe(false); expect(redemption.applied).toBe(false); } else { - // 1. Check that main customer has free add on - compareProductEntitlements({ - customerId: mainCustomerId, - product: products.freeAddOn, - features, - quantity: count, + const mainCustomer = await autumn.customers.get(mainCustomerId); + + const redeemerCustomer = await autumn.customers.get(redeemer); + + expectProductAttached({ + customer: mainCustomer, + product: freeAddOn, }); - compareProductEntitlements({ - customerId: redeemer, - product: products.freeAddOn, - features, + expectProductAttached({ + customer: redeemerCustomer, + product: freeAddOn, }); } } diff --git a/server/tests/advanced/referrals/referrals4.backup.ts b/server/tests/advanced/referrals/referrals4.backup.ts deleted file mode 100644 index 2a1133509..000000000 --- a/server/tests/advanced/referrals/referrals4.backup.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { ReferralCode, RewardRedemption } from "@autumn/shared"; -import { assert } from "chai"; -import chalk from "chalk"; -import { addDays, addHours } from "date-fns"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { compareProductEntitlements } from "tests/utils/compare.js"; -import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; -import { timeout } from "tests/utils/genUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { initCustomerV2 } from "../../../src/utils/scriptUtils/initCustomer.js"; -import { features, products, referralPrograms } from "../../global.js"; - -// UNCOMMENT FROM HERE -describe(`${chalk.yellowBright( - "referrals4: Testing free product referrals with trial", -)}`, () => { - const mainCustomerId = "main-referral-4"; - // let redeemers = ["referral4-r1", "referral4-r2"]; - const redeemerId = "referral4-r1"; - - let autumn: AutumnInt = new AutumnInt(); - let stripeCli: Stripe; - let referralCode: ReferralCode; - - const redemptions: RewardRedemption[] = []; - - let testClockId: string; - before(async function () { - await setupBefore(this); - autumn = this.autumn; - stripeCli = this.stripeCli; - - await initCustomerV2({ - autumn, - customerId: mainCustomerId, - org: this.org, - env: this.env, - db: this.db, - attachPm: "success", - }); - - await autumn.attach({ - customer_id: mainCustomerId, - product_id: products.proWithTrial.id, - }); - - const { testClockId: testClockId1 } = await initCustomerV2({ - autumn, - customerId: redeemerId, - db: this.db, - org: this.org, - env: this.env, - attachPm: "success", - }); - - testClockId = testClockId1; - }); - - it("should create referral code", async () => { - referralCode = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.freeProduct.id, - }); - - assert.exists(referralCode.code); - }); - - it("should create redemption for each redeemer and fail if redeemed again", async () => { - const redemption: RewardRedemption = await autumn.referrals.redeem({ - customerId: redeemerId, - code: referralCode.code, - }); - - redemptions.push(redemption); - }); - - it("should not be triggered because of trial", async () => { - await autumn.attach({ - customer_id: redeemerId, - product_id: products.proWithTrial.id, - }); - - await timeout(3000); - - // Get redemption object - const redemption = await autumn.redemptions.get(redemptions[0].id); - - assert.equal(redemption.triggered, false); - }); - - it("should be triggered after trial ends", async () => { - const advanceTo = addHours( - addDays(new Date(), 7), - hoursToFinalizeInvoice, - ).getTime(); - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo, - waitForSeconds: 30, - }); - - const redemption = await autumn.redemptions.get(redemptions[0].id); - - assert.equal(redemption.triggered, true); - - compareProductEntitlements({ - customerId: mainCustomerId, - product: products.freeAddOn, - features, - quantity: 1, - }); - - compareProductEntitlements({ - customerId: redeemerId, - product: products.freeAddOn, - features, - quantity: 1, - }); - }); -}); diff --git a/server/tests/advanced/referrals/referrals4.test.ts b/server/tests/advanced/referrals/referrals4.test.ts index e9c4c3047..ad0d01f88 100644 --- a/server/tests/advanced/referrals/referrals4.test.ts +++ b/server/tests/advanced/referrals/referrals4.test.ts @@ -1,16 +1,71 @@ -import type { Customer, ReferralCode, RewardRedemption } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { + ApiVersion, + type CreateReward, + type CreateRewardProgram, + type ReferralCode, + RewardReceivedBy, + type RewardRedemption, + RewardTriggerEvent, + RewardType, +} from "@autumn/shared"; import chalk from "chalk"; import { addDays, addHours } from "date-fns"; import type { Stripe } from "stripe"; -import { compareProductEntitlements } from "tests/utils/compare.js"; +import { TestFeature } from "tests/setup/v2Features.js"; import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; import { timeout } from "tests/utils/genUtils.js"; +import { createReferralProgram } from "tests/utils/productUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; -import { features, products, referralPrograms } from "../../global.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { expectProductAttached } from "../../utils/expectUtils/expectProductAttached.js"; + +const testCase = "referrals4"; + +const proWithTrial = constructProduct({ + id: "pro", + items: [constructFeatureItem({ featureId: TestFeature.Words })], + type: "pro", + trial: true, +}); + +const freeAddOn = constructProduct({ + id: "freeAddOn", + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + interval: null, + }), + ], + type: "free", + isAddOn: true, + isDefault: false, +}); + +// Reward: Free product reward +const freeProductReward: CreateReward = { + id: `${testCase}FreeProduct`, + name: "Free Product", + type: RewardType.FreeProduct, + promo_codes: [], + free_product_id: freeAddOn.id, +}; + +// Referral program: triggers on checkout +const freeProductProgram: CreateRewardProgram = { + id: `${testCase}FreeProduct`, + when: RewardTriggerEvent.Checkout, + product_ids: [proWithTrial.id], + internal_reward_id: freeProductReward.id, + max_redemptions: 2, + received_by: RewardReceivedBy.All, +}; describe(`${chalk.yellowBright( "referrals4: Testing free product referrals with trial", @@ -23,15 +78,44 @@ describe(`${chalk.yellowBright( let referralCode: ReferralCode; const redemptions: RewardRedemption[] = []; - let mainCustomer: Customer; - let redeemer: Customer; let testClockId: string; beforeAll(async () => { - autumn = new AutumnInt({ secretKey: ctx.orgSecretKey }); + autumn = new AutumnInt({ + secretKey: ctx.orgSecretKey, + version: ApiVersion.V1_2, + }); stripeCli = ctx.stripeCli; + await initProductsV0({ + ctx, + products: [proWithTrial, freeAddOn], + prefix: testCase, + customerId: mainCustomerId, + }); + + // Create referral program - product IDs are already prefixed by initProductsV0 + const referralProgram: CreateRewardProgram = { + ...freeProductProgram, + product_ids: [proWithTrial.id], + }; + + // Update reward with prefixed free product ID + const reward: CreateReward = { + ...freeProductReward, + free_product_id: freeAddOn.id, + }; + + await createReferralProgram({ + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + autumn, + reward, + rewardProgram: referralProgram, + }); + await initCustomerV3({ ctx, customerId: mainCustomerId, @@ -40,22 +124,22 @@ describe(`${chalk.yellowBright( await autumn.attach({ customer_id: mainCustomerId, - product_id: products.proWithTrial.id, + product_id: proWithTrial.id, }); - const { testClockId: testClockId1, customer } = await initCustomerV3({ + const { testClockId: testClockId1 } = await initCustomerV3({ ctx, customerId: redeemerId, + attachPm: "success", }); testClockId = testClockId1; - redeemer = customer; }); test("should create referral code", async () => { referralCode = await autumn.referrals.createCode({ customerId: mainCustomerId, - referralId: referralPrograms.freeProduct.id, + referralId: freeProductProgram.id, }); expect(referralCode.code).toBeDefined(); @@ -73,7 +157,7 @@ describe(`${chalk.yellowBright( test("should not be triggered because of trial", async () => { await autumn.attach({ customer_id: redeemerId, - product_id: products.proWithTrial.id, + product_id: proWithTrial.id, }); await timeout(3000); @@ -85,33 +169,30 @@ describe(`${chalk.yellowBright( }); test("should be triggered after trial ends", async () => { - const advanceTo = addHours( - addDays(new Date(), 7), - hoursToFinalizeInvoice, - ).getTime(); await advanceTestClock({ stripeCli, testClockId, - advanceTo, + advanceTo: addHours( + addDays(new Date(), 7), + hoursToFinalizeInvoice, + ).getTime(), waitForSeconds: 30, }); const redemption = await autumn.redemptions.get(redemptions[0].id); - expect(redemption.triggered).toBe(true); - compareProductEntitlements({ - customerId: mainCustomerId, - product: products.freeAddOn, - features, - quantity: 1, + const mainCustomer = await autumn.customers.get(mainCustomerId); + const redeemer = await autumn.customers.get(redeemerId); + + expectProductAttached({ + customer: mainCustomer, + product: freeAddOn, }); - compareProductEntitlements({ - customerId: redeemerId, - product: products.freeAddOn, - features, - quantity: 1, + expectProductAttached({ + customer: redeemer, + product: freeAddOn, }); }); }); diff --git a/server/tests/advanced/rollovers/rollover1.backup.ts b/server/tests/advanced/rollovers/rollover1.backup.ts deleted file mode 100644 index 9b81e8ecb..000000000 --- a/server/tests/advanced/rollovers/rollover1.backup.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { - type AppEnv, - type Customer, - LegacyVersion, - type LimitedItem, - type Organization, - ProductItemInterval, - RolloverDuration, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { resetAndGetCusEnt } from "./rolloverTestUtils.js"; - -const rolloverConfig = { - max: 500, - length: 1, - duration: RolloverDuration.Month, -}; -const messagesItem = constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 400, - interval: ProductItemInterval.Month, - rolloverConfig, -}) as LimitedItem; - -export const free = constructProduct({ - items: [messagesItem], - type: "free", - isDefault: false, -}); - -const testCase = "rollover1"; -// , per entity and regular - -describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let customer: Customer; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [free], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [free], - customerId, - db, - orgId: org.id, - env, - }); - - const res = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = res.testClockId!; - customer = res.customer; - }); - - it("should attach free product", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: free.id, - }); - }); - - const messageUsage = 250; - let curBalance = messagesItem.included_usage; - - it("should create track messages, reset, and have correct rollover", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: messageUsage, - }); - - await timeout(3000); - - await resetAndGetCusEnt({ - db, - customer, - productGroup: free.group, - featureId: TestFeature.Messages, - }); - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - - const expectedRollover = Math.min( - messagesItem.included_usage - messageUsage, - rolloverConfig.max, - ); - - const expectedBalance = messagesItem.included_usage + expectedRollover; - - expect(msgesFeature).to.exist; - expect(msgesFeature?.balance).to.equal(expectedBalance); - // @ts-expect-error - expect(msgesFeature?.rollovers[0].balance).to.equal(expectedRollover); - curBalance = expectedBalance; - }); - - // let usage2 = 50; - it("should reset again and have correct rollover", async () => { - await resetAndGetCusEnt({ - db, - customer, - productGroup: free.group, - featureId: TestFeature.Messages, - }); - - const expectedRollover = Math.min(curBalance, rolloverConfig.max); - const expectedBalance = messagesItem.included_usage + expectedRollover; - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - - expect(msgesFeature).to.exist; - expect(msgesFeature?.balance).to.equal(expectedBalance); - - // @ts-expect-error (oldest rollover should be 100 (150 - 50)) - expect(msgesFeature?.rollovers[0].balance).to.equal(100); - // @ts-expect-error (newest rollover should be 400 (msges.included_usage)) - expect(msgesFeature?.rollovers[1].balance).to.equal(400); - }); - - it("should track messages and deduct from rollovers first", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: 150, - }); - - await timeout(3000); - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - - // @ts-expect-error - const rollover1 = msgesFeature?.rollovers[0]; - // @ts-expect-error - const rollover2 = msgesFeature?.rollovers[1]; - - expect(rollover1.balance).to.equal(0); - expect(rollover2.balance).to.equal(350); - }); - - it("should track and deduct from rollover + original balance", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: 400, - }); - - await timeout(3000); - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - - // @ts-expect-error - const rollovers = msgesFeature.rollovers; - expect(rollovers![0].balance).to.equal(0); - expect(rollovers![1].balance).to.equal(0); - expect(msgesFeature.balance).to.equal(messagesItem.included_usage - 50); - }); -}); diff --git a/server/tests/advanced/rollovers/rollover1.test.ts b/server/tests/advanced/rollovers/rollover1.test.ts index 16ede8a51..2d9aecf5b 100644 --- a/server/tests/advanced/rollovers/rollover1.test.ts +++ b/server/tests/advanced/rollovers/rollover1.test.ts @@ -1,3 +1,4 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type Customer, LegacyVersion, @@ -5,11 +6,10 @@ import { ProductItemInterval, RolloverDuration, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import type Stripe from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; @@ -92,7 +92,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item` await resetAndGetCusEnt({ db: ctx.db, customer, - productGroup: free.group, + productGroup: free.group!, featureId: TestFeature.Messages, }); @@ -111,6 +111,17 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item` // @ts-expect-error expect(msgesFeature?.rollovers[0].balance).toBe(expectedRollover); curBalance = expectedBalance; + + // Verify non-cached customer balance + await timeout(2000); + const nonCachedCustomer = await autumn.customers.get(customerId, { + skip_cache: "true", + }); + const nonCachedMsgesFeature = + nonCachedCustomer.features[TestFeature.Messages]; + expect(nonCachedMsgesFeature?.balance).toBe(expectedBalance); + // @ts-expect-error + expect(nonCachedMsgesFeature?.rollovers[0].balance).toBe(expectedRollover); }); // let usage2 = 50; @@ -118,7 +129,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item` await resetAndGetCusEnt({ db: ctx.db, customer, - productGroup: free.group, + productGroup: free.group!, featureId: TestFeature.Messages, }); @@ -135,6 +146,19 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item` expect(msgesFeature?.rollovers[0].balance).toBe(100); // @ts-expect-error (newest rollover should be 400 (msges.included_usage)) expect(msgesFeature?.rollovers[1].balance).toBe(400); + + // Verify non-cached customer balance + await timeout(2000); + const nonCachedCustomer = await autumn.customers.get(customerId, { + skip_cache: "true", + }); + const nonCachedMsgesFeature = + nonCachedCustomer.features[TestFeature.Messages]; + expect(nonCachedMsgesFeature?.balance).toBe(expectedBalance); + // @ts-expect-error + expect(nonCachedMsgesFeature?.rollovers[0].balance).toBe(100); + // @ts-expect-error + expect(nonCachedMsgesFeature?.rollovers[1].balance).toBe(400); }); test("should track messages and deduct from rollovers first", async () => { @@ -156,6 +180,20 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item` expect(rollover1.balance).toBe(0); expect(rollover2.balance).toBe(350); + + // Verify non-cached customer balance + await timeout(2000); + const nonCachedCustomer = await autumn.customers.get(customerId, { + skip_cache: "true", + }); + const nonCachedMsgesFeature = + nonCachedCustomer.features[TestFeature.Messages]; + // @ts-expect-error + const nonCachedRollover1 = nonCachedMsgesFeature?.rollovers[0]; + // @ts-expect-error + const nonCachedRollover2 = nonCachedMsgesFeature?.rollovers[1]; + expect(nonCachedRollover1.balance).toBe(0); + expect(nonCachedRollover2.balance).toBe(350); }); test("should track and deduct from rollover + original balance", async () => { @@ -170,10 +208,27 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item` const cus = await autumn.customers.get(customerId); const msgesFeature = cus.features[TestFeature.Messages]; - // @ts-expect-error const rollovers = msgesFeature.rollovers; + // @ts-expect-error (rollovers is an array of rollovers) expect(rollovers![0].balance).toBe(0); + // @ts-expect-error (rollovers is an array of rollovers) expect(rollovers![1].balance).toBe(0); expect(msgesFeature.balance).toBe(messagesItem.included_usage - 50); + + // Verify non-cached customer balance + await timeout(2000); + const nonCachedCustomer = await autumn.customers.get(customerId, { + skip_cache: "true", + }); + const nonCachedMsgesFeature = + nonCachedCustomer.features[TestFeature.Messages]; + const nonCachedRollovers = nonCachedMsgesFeature.rollovers; + // @ts-expect-error + expect(nonCachedRollovers![0].balance).toBe(0); + // @ts-expect-error + expect(nonCachedRollovers![1].balance).toBe(0); + expect(nonCachedMsgesFeature.balance).toBe( + messagesItem.included_usage - 50, + ); }); }); diff --git a/server/tests/advanced/rollovers/rollover1.ts b/server/tests/advanced/rollovers/rollover1.ts deleted file mode 100644 index edbc7d87a..000000000 --- a/server/tests/advanced/rollovers/rollover1.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { - type AppEnv, - type Customer, - LegacyVersion, - type LimitedItem, - type Organization, - ProductItemInterval, - RolloverDuration, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { resetAndGetCusEnt } from "./rolloverTestUtils.js"; - -const rolloverConfig = { - max: 500, - length: 1, - duration: RolloverDuration.Month, -}; -const messagesItem = constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 400, - interval: ProductItemInterval.Month, - rolloverConfig, -}) as LimitedItem; - -export const free = constructProduct({ - items: [messagesItem], - type: "free", - isDefault: false, -}); - -const testCase = "rollover1"; -// , per entity and regular - -describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let customer: Customer; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [free], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [free], - customerId, - db, - orgId: org.id, - env, - }); - - const res = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = res.testClockId!; - customer = res.customer; - }); - - it("should attach free product", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: free.id, - }); - }); - - const messageUsage = 250; - let curBalance = messagesItem.included_usage; - - it("should create track messages, reset, and have correct rollover", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: messageUsage, - }); - - await timeout(3000); - - await resetAndGetCusEnt({ - db, - customer, - productGroup: free.group, - featureId: TestFeature.Messages, - }); - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - - const expectedRollover = Math.min( - messagesItem.included_usage - messageUsage, - rolloverConfig.max, - ); - - const expectedBalance = messagesItem.included_usage + expectedRollover; - - expect(msgesFeature).to.exist; - expect(msgesFeature?.balance).to.equal(expectedBalance); - // @ts-expect-error - expect(msgesFeature?.rollovers[0].balance).to.equal(expectedRollover); - curBalance = expectedBalance; - }); - - // let usage2 = 50; - it("should reset again and have correct rollover", async () => { - await resetAndGetCusEnt({ - db, - customer, - productGroup: free.group, - featureId: TestFeature.Messages, - }); - - const expectedRollover = Math.min(curBalance, rolloverConfig.max); - const expectedBalance = messagesItem.included_usage + expectedRollover; - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - - expect(msgesFeature).to.exist; - expect(msgesFeature?.balance).to.equal(expectedBalance); - - // @ts-expect-error (oldest rollover should be 100 (150 - 50)) - expect(msgesFeature?.rollovers[0].balance).to.equal(100); - // @ts-expect-error (newest rollover should be 400 (msges.included_usage)) - expect(msgesFeature?.rollovers[1].balance).to.equal(400); - }); - - it("should track messages and deduct from rollovers first", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: 150, - }); - - await timeout(3000); - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - - // @ts-expect-error - const rollover1 = msgesFeature?.rollovers[0]; - // @ts-expect-error - const rollover2 = msgesFeature?.rollovers[1]; - - expect(rollover1.balance).to.equal(0); - expect(rollover2.balance).to.equal(350); - }); - return; - - it("should track and deduct from rollover + original balance", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: 400, - }); - - await timeout(3000); - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - - // @ts-expect-error - const rollovers = msgesFeature.rollovers; - expect(rollovers![0].balance).to.equal(0); - expect(rollovers![1].balance).to.equal(0); - expect(msgesFeature.balance).to.equal(messagesItem.included_usage - 50); - }); -}); diff --git a/server/tests/advanced/rollovers/rollover2.backup.ts b/server/tests/advanced/rollovers/rollover2.backup.ts deleted file mode 100644 index bdd212140..000000000 --- a/server/tests/advanced/rollovers/rollover2.backup.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { - type AppEnv, - type Customer, - LegacyVersion, - type LimitedItem, - type Organization, - ProductItemInterval, - RolloverDuration, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { resetAndGetCusEnt } from "./rolloverTestUtils.js"; - -const rolloverConfig = { - max: 500, - length: 1, - duration: RolloverDuration.Month, -}; - -const msgesItem = constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 400, - interval: ProductItemInterval.Month, - rolloverConfig, - entityFeatureId: TestFeature.Users, -}) as LimitedItem; - -export const free = constructProduct({ - items: [msgesItem], - type: "free", - isDefault: false, -}); - -const testCase = "rollover2"; -// , per entity and regular - -describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item (per entity)`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let customer: Customer; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [free], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [free], - customerId, - db, - orgId: org.id, - env, - }); - - const res = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = res.testClockId!; - customer = res.customer; - }); - - const entities: any[] = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - it("should attach pro product", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: free.id, - }); - - await autumn.entities.create(customerId, entities); - }); - - const entity1Id = entities[0].id; - const entity2Id = entities[1].id; - const newEntity1Balance = 300; - const newEntity2Balance = 200; - const includedUsage = msgesItem.included_usage; - const usages = [ - { - entityId: entity1Id, - usage: includedUsage - newEntity1Balance, - rollover: newEntity1Balance, - }, - { - entityId: entity2Id, - usage: includedUsage - newEntity2Balance, - rollover: newEntity2Balance, - }, - ]; - - it("should create track messages, reset, and have correct rollover", async () => { - for (const usage of usages) { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: usage.usage, - entity_id: usage.entityId, - }); - } - - await timeout(3000); - - // Run reset cusEnt on ... - await resetAndGetCusEnt({ - db, - customer, - productGroup: free.group, - featureId: TestFeature.Messages, - }); - - for (const usage of usages) { - const entity = await autumn.entities.get(customerId, usage.entityId); - const msgesFeature = entity.features[TestFeature.Messages]; - const expectedRollover = Math.min(usage.rollover, rolloverConfig.max); - - expect(msgesFeature.rollovers.length).to.equal(1); - expect(msgesFeature.balance).to.equal(includedUsage + expectedRollover); - expect(msgesFeature.rollovers[0].balance).to.equal(expectedRollover); - } - }); - - it("should reset again and have correct rollovers", async () => { - await resetAndGetCusEnt({ - db, - customer, - productGroup: free.group, - featureId: TestFeature.Messages, - }); - - const entity1 = await autumn.entities.get(customerId, entity1Id); - const entity1Msges = entity1.features[TestFeature.Messages]; - // 400, 300 -> 400, 100 (max is 500) - const rollovers = entity1Msges.rollovers; - expect(rollovers[0].balance).to.equal(100); - expect(rollovers[1].balance).to.equal(400); - - const entity2 = await autumn.entities.get(customerId, entity2Id); - const entity2Msges = entity2.features[TestFeature.Messages]; - // 400, 200 -> 400, 0 (max is 500) - const rollovers2 = entity2Msges.rollovers; - expect(rollovers2[0].balance).to.equal(100); - expect(rollovers2[1].balance).to.equal(400); - }); - - it("should track and deduct from oldest rollovers first", async () => { - for (const entity of entities) { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: 150, - entity_id: entity.id, - }); - - await timeout(2000); - const entRes = await autumn.entities.get(customerId, entity.id); - const msgesFeature = entRes.features[TestFeature.Messages]; - const rollovers = msgesFeature.rollovers; - expect(rollovers[0].balance).to.equal(0); - expect(rollovers[1].balance).to.equal(350); - expect(msgesFeature.balance).to.equal(includedUsage + 350); - } - }); - - it("should track past rollovers and deduct from original balance", async () => { - for (const entity of entities) { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: 400, - entity_id: entity.id, - }); - await timeout(2000); - - const entRes = await autumn.entities.get(customerId, entity.id); - const msgesFeature = entRes.features[TestFeature.Messages]; - const rollovers = msgesFeature.rollovers; - expect(rollovers[0].balance).to.equal(0); - expect(rollovers[1].balance).to.equal(0); - expect(msgesFeature.balance).to.equal(includedUsage - 50); - } - }); -}); diff --git a/server/tests/advanced/rollovers/rollover2.test.ts b/server/tests/advanced/rollovers/rollover2.test.ts index 3da0251f7..8ebfce1a8 100644 --- a/server/tests/advanced/rollovers/rollover2.test.ts +++ b/server/tests/advanced/rollovers/rollover2.test.ts @@ -141,6 +141,22 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item expect(msgesFeature.balance).toBe(includedUsage + expectedRollover); expect(msgesFeature.rollovers[0].balance).toBe(expectedRollover); } + + // Verify non-cached entity balances + await timeout(2000); + for (const usage of usages) { + const expectedRollover = Math.min(usage.rollover, rolloverConfig.max); + const nonCachedEntity = await autumn.entities.get( + customerId, + usage.entityId, + { + skip_cache: "true", + }, + ); + const nonCachedMsgesFeature = nonCachedEntity.features[TestFeature.Messages]; + expect(nonCachedMsgesFeature.balance).toBe(includedUsage + expectedRollover); + expect(nonCachedMsgesFeature.rollovers[0].balance).toBe(expectedRollover); + } }); test("should reset again and have correct rollovers", async () => { @@ -164,6 +180,24 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item const rollovers2 = entity2Msges.rollovers; expect(rollovers2[0].balance).toBe(100); expect(rollovers2[1].balance).toBe(400); + + // Verify non-cached entity balances + await timeout(2000); + const nonCachedEntity1 = await autumn.entities.get(customerId, entity1Id, { + skip_cache: "true", + }); + const nonCachedEntity1Msges = nonCachedEntity1.features[TestFeature.Messages]; + const nonCachedRollovers1 = nonCachedEntity1Msges.rollovers; + expect(nonCachedRollovers1[0].balance).toBe(100); + expect(nonCachedRollovers1[1].balance).toBe(400); + + const nonCachedEntity2 = await autumn.entities.get(customerId, entity2Id, { + skip_cache: "true", + }); + const nonCachedEntity2Msges = nonCachedEntity2.features[TestFeature.Messages]; + const nonCachedRollovers2 = nonCachedEntity2Msges.rollovers; + expect(nonCachedRollovers2[0].balance).toBe(100); + expect(nonCachedRollovers2[1].balance).toBe(400); }); test("should track and deduct from oldest rollovers first", async () => { @@ -183,6 +217,19 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item expect(rollovers[1].balance).toBe(350); expect(msgesFeature.balance).toBe(includedUsage + 350); } + + // Verify non-cached entity balances + await timeout(2000); + for (const entity of entities) { + const nonCachedEntity = await autumn.entities.get(customerId, entity.id, { + skip_cache: "true", + }); + const nonCachedMsgesFeature = nonCachedEntity.features[TestFeature.Messages]; + const nonCachedRollovers = nonCachedMsgesFeature.rollovers; + expect(nonCachedRollovers[0].balance).toBe(0); + expect(nonCachedRollovers[1].balance).toBe(350); + expect(nonCachedMsgesFeature.balance).toBe(includedUsage + 350); + } }); test("should track past rollovers and deduct from original balance", async () => { @@ -202,5 +249,18 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item expect(rollovers[1].balance).toBe(0); expect(msgesFeature.balance).toBe(includedUsage - 50); } + + // Verify non-cached entity balances + await timeout(2000); + for (const entity of entities) { + const nonCachedEntity = await autumn.entities.get(customerId, entity.id, { + skip_cache: "true", + }); + const nonCachedMsgesFeature = nonCachedEntity.features[TestFeature.Messages]; + const nonCachedRollovers = nonCachedMsgesFeature.rollovers; + expect(nonCachedRollovers[0].balance).toBe(0); + expect(nonCachedRollovers[1].balance).toBe(0); + expect(nonCachedMsgesFeature.balance).toBe(includedUsage - 50); + } }); }); diff --git a/server/tests/advanced/rollovers/rollover2.ts b/server/tests/advanced/rollovers/rollover2.ts deleted file mode 100644 index 0b8a6cd30..000000000 --- a/server/tests/advanced/rollovers/rollover2.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { - type AppEnv, - type Customer, - LegacyVersion, - type LimitedItem, - type Organization, - ProductItemInterval, - RolloverDuration, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { resetAndGetCusEnt } from "./rolloverTestUtils.js"; - -const rolloverConfig = { - max: 500, - length: 1, - duration: RolloverDuration.Month, -}; - -const msgesItem = constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 400, - interval: ProductItemInterval.Month, - rolloverConfig, - entityFeatureId: TestFeature.Users, -}) as LimitedItem; - -export const free = constructProduct({ - items: [msgesItem], - type: "free", - isDefault: false, -}); - -const testCase = "rollover2"; -// , per entity and regular - -describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item (per entity)`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let customer: Customer; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [free], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [free], - customerId, - db, - orgId: org.id, - env, - }); - - const res = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = res.testClockId!; - customer = res.customer; - }); - - const entities: any[] = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - it("should attach pro product", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: free.id, - }); - - await autumn.entities.create(customerId, entities); - }); - - const entity1Id = entities[0].id; - const entity2Id = entities[1].id; - const newEntity1Balance = 300; - const newEntity2Balance = 200; - const includedUsage = msgesItem.included_usage; - const usages = [ - { - entityId: entity1Id, - usage: includedUsage - newEntity1Balance, - rollover: newEntity1Balance, - }, - { - entityId: entity2Id, - usage: includedUsage - newEntity2Balance, - rollover: newEntity2Balance, - }, - ]; - - it("should create track messages, reset, and have correct rollover", async () => { - for (const usage of usages) { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: usage.usage, - entity_id: usage.entityId, - }); - } - - await timeout(3000); - - // Run reset cusEnt on ... - await resetAndGetCusEnt({ - db, - customer, - productGroup: free.group!, - featureId: TestFeature.Messages, - }); - - for (const usage of usages) { - const entity = await autumn.entities.get(customerId, usage.entityId); - const msgesFeature = entity.features[TestFeature.Messages]; - const expectedRollover = Math.min(usage.rollover, rolloverConfig.max); - - expect(msgesFeature.rollovers.length).to.equal(1); - expect(msgesFeature.balance).to.equal(includedUsage + expectedRollover); - expect(msgesFeature.rollovers[0].balance).to.equal(expectedRollover); - } - }); - - it("should reset again and have correct rollovers", async () => { - await resetAndGetCusEnt({ - db, - customer, - productGroup: free.group!, - featureId: TestFeature.Messages, - }); - - const entity1 = await autumn.entities.get(customerId, entity1Id); - const entity1Msges = entity1.features[TestFeature.Messages]; - // 400, 300 -> 400, 100 (max is 500) - const rollovers = entity1Msges.rollovers; - expect(rollovers[0].balance).to.equal(100); - expect(rollovers[1].balance).to.equal(400); - - const entity2 = await autumn.entities.get(customerId, entity2Id); - const entity2Msges = entity2.features[TestFeature.Messages]; - // 400, 200 -> 400, 0 (max is 500) - const rollovers2 = entity2Msges.rollovers; - expect(rollovers2[0].balance).to.equal(100); - expect(rollovers2[1].balance).to.equal(400); - }); - - it("should track and deduct from oldest rollovers first", async () => { - for (const entity of entities) { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: 150, - entity_id: entity.id, - }); - - await timeout(2000); - const entRes = await autumn.entities.get(customerId, entity.id); - const msgesFeature = entRes.features[TestFeature.Messages]; - const rollovers = msgesFeature.rollovers; - expect(rollovers[0].balance).to.equal(0); - expect(rollovers[1].balance).to.equal(350); - expect(msgesFeature.balance).to.equal(includedUsage + 350); - } - }); - - it("should track past rollovers and deduct from original balance", async () => { - for (const entity of entities) { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: 400, - entity_id: entity.id, - }); - await timeout(2000); - - const entRes = await autumn.entities.get(customerId, entity.id); - const msgesFeature = entRes.features[TestFeature.Messages]; - const rollovers = msgesFeature.rollovers; - expect(rollovers[0].balance).to.equal(0); - expect(rollovers[1].balance).to.equal(0); - expect(msgesFeature.balance).to.equal(includedUsage - 50); - } - }); -}); diff --git a/server/tests/advanced/rollovers/rollover3.backup.ts b/server/tests/advanced/rollovers/rollover3.backup.ts deleted file mode 100644 index 37b0c7590..000000000 --- a/server/tests/advanced/rollovers/rollover3.backup.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { - type AppEnv, - type Customer, - LegacyVersion, - type LimitedItem, - type Organization, - RolloverDuration, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import { addMonths } from "date-fns"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; - -const rolloverConfig = { - max: 500, - length: 1, - duration: RolloverDuration.Month, -}; -const messagesItem = constructArrearProratedItem({ - featureId: TestFeature.Messages, - includedUsage: 400, - rolloverConfig, -}) as LimitedItem; - -export const pro = constructProduct({ - items: [messagesItem], - type: "pro", - isDefault: false, -}); - -const testCase = "rollover3"; - -describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for usage price feature`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let customer: Customer; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const res = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = res.testClockId!; - customer = res.customer; - }); - - it("should attach pro product", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: pro.id, - }); - }); - - const rollover = 250; - let curBalance = messagesItem.included_usage; - - it("should create track messages, reset, and have correct rollover", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: messagesItem.included_usage - rollover, - }); - - await timeout(3000); - - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addMonths(new Date(), 1).getTime(), - waitForSeconds: 20, - }); - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - - const expectedBalance = messagesItem.included_usage + rollover; - - expect(msgesFeature).to.exist; - expect(msgesFeature?.balance).to.equal(expectedBalance); - // @ts-expect-error - expect(msgesFeature?.rollovers[0].balance).to.equal(rollover); - curBalance = expectedBalance; - }); -}); diff --git a/server/tests/advanced/rollovers/rollover3.test.ts b/server/tests/advanced/rollovers/rollover3.test.ts index 13cf4c32f..768491861 100644 --- a/server/tests/advanced/rollovers/rollover3.test.ts +++ b/server/tests/advanced/rollovers/rollover3.test.ts @@ -104,5 +104,15 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for usage price f // @ts-expect-error expect(msgesFeature?.rollovers[0].balance).toBe(rollover); curBalance = expectedBalance; + + // Verify non-cached customer balance + await timeout(2000); + const nonCachedCustomer = await autumn.customers.get(customerId, { + skip_cache: "true", + }); + const nonCachedMsgesFeature = nonCachedCustomer.features[TestFeature.Messages]; + expect(nonCachedMsgesFeature?.balance).toBe(expectedBalance); + // @ts-expect-error + expect(nonCachedMsgesFeature?.rollovers[0].balance).toBe(rollover); }); }); diff --git a/server/tests/advanced/rollovers/rollover3.ts b/server/tests/advanced/rollovers/rollover3.ts deleted file mode 100644 index 37b0c7590..000000000 --- a/server/tests/advanced/rollovers/rollover3.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { - type AppEnv, - type Customer, - LegacyVersion, - type LimitedItem, - type Organization, - RolloverDuration, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import { addMonths } from "date-fns"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; - -const rolloverConfig = { - max: 500, - length: 1, - duration: RolloverDuration.Month, -}; -const messagesItem = constructArrearProratedItem({ - featureId: TestFeature.Messages, - includedUsage: 400, - rolloverConfig, -}) as LimitedItem; - -export const pro = constructProduct({ - items: [messagesItem], - type: "pro", - isDefault: false, -}); - -const testCase = "rollover3"; - -describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for usage price feature`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let customer: Customer; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const res = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = res.testClockId!; - customer = res.customer; - }); - - it("should attach pro product", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: pro.id, - }); - }); - - const rollover = 250; - let curBalance = messagesItem.included_usage; - - it("should create track messages, reset, and have correct rollover", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: messagesItem.included_usage - rollover, - }); - - await timeout(3000); - - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addMonths(new Date(), 1).getTime(), - waitForSeconds: 20, - }); - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - - const expectedBalance = messagesItem.included_usage + rollover; - - expect(msgesFeature).to.exist; - expect(msgesFeature?.balance).to.equal(expectedBalance); - // @ts-expect-error - expect(msgesFeature?.rollovers[0].balance).to.equal(rollover); - curBalance = expectedBalance; - }); -}); diff --git a/server/tests/advanced/rollovers/rollover4.backup.ts b/server/tests/advanced/rollovers/rollover4.backup.ts deleted file mode 100644 index 13e38b224..000000000 --- a/server/tests/advanced/rollovers/rollover4.backup.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { - type AppEnv, - type Customer, - LegacyVersion, - type LimitedItem, - type Organization, - RolloverDuration, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import { addMonths } from "date-fns"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; - -const rolloverConfig = { - max: 400, - length: 1, - duration: RolloverDuration.Month, -}; -const messagesItem = constructPrepaidItem({ - featureId: TestFeature.Messages, - includedUsage: 100, - billingUnits: 300, - price: 10, - rolloverConfig, -}) as LimitedItem; - -export const pro = constructProduct({ - items: [messagesItem], - type: "pro", - isDefault: false, -}); - -const testCase = "rollover4"; - -describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for usage price feature`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let customer: Customer; - let stripeCli: Stripe; - - let curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const res = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = res.testClockId!; - customer = res.customer; - }); - - const paidQuantity = 300; - const balance = paidQuantity + messagesItem.included_usage; - const options = [ - { - feature_id: TestFeature.Messages, - quantity: paidQuantity, - }, - ]; - - it("should attach pro product", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: pro.id, - options, - }); - }); - - const rollover = 50; - it("should create track messages, reset, and have correct rollover", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: balance - rollover, - }); - - await timeout(3000); - - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addMonths(new Date(), 1).getTime(), - waitForSeconds: 20, - }); - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - - // @ts-expect-error - const rollovers = msgesFeature?.rollovers; - - expect(msgesFeature).to.exist; - expect(msgesFeature?.balance).to.equal(balance + rollover); - expect(rollovers[0].balance).to.equal(rollover); - }); - - // let usage2 = 50; - it("should reset again and have correct rollover", async () => { - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addMonths(curUnix, 1).getTime(), - waitForSeconds: 20, - }); - - const newRollover = Math.min(balance + rollover, rolloverConfig.max); - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - // @ts-expect-error - const rollovers = msgesFeature?.rollovers; - - expect(msgesFeature).to.exist; - expect(msgesFeature?.balance).to.equal(balance + newRollover); - expect(rollovers[0].balance).to.equal(0); - expect(rollovers[1].balance).to.equal(400); - }); -}); diff --git a/server/tests/advanced/rollovers/rollover4.test.ts b/server/tests/advanced/rollovers/rollover4.test.ts index 6677c9ab7..8c9084441 100644 --- a/server/tests/advanced/rollovers/rollover4.test.ts +++ b/server/tests/advanced/rollovers/rollover4.test.ts @@ -113,6 +113,17 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for usage price f expect(msgesFeature).toBeDefined(); expect(msgesFeature?.balance).toBe(balance + rollover); expect(rollovers[0].balance).toBe(rollover); + + // Verify non-cached customer balance + await timeout(2000); + const nonCachedCustomer = await autumn.customers.get(customerId, { + skip_cache: "true", + }); + const nonCachedMsgesFeature = nonCachedCustomer.features[TestFeature.Messages]; + // @ts-expect-error + const nonCachedRollovers = nonCachedMsgesFeature?.rollovers; + expect(nonCachedMsgesFeature?.balance).toBe(balance + rollover); + expect(nonCachedRollovers[0].balance).toBe(rollover); }); // let usage2 = 50; @@ -134,5 +145,17 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for usage price f expect(msgesFeature?.balance).toBe(balance + newRollover); expect(rollovers[0].balance).toBe(0); expect(rollovers[1].balance).toBe(400); + + // Verify non-cached customer balance + await timeout(2000); + const nonCachedCustomer = await autumn.customers.get(customerId, { + skip_cache: "true", + }); + const nonCachedMsgesFeature = nonCachedCustomer.features[TestFeature.Messages]; + // @ts-expect-error + const nonCachedRollovers = nonCachedMsgesFeature?.rollovers; + expect(nonCachedMsgesFeature?.balance).toBe(balance + newRollover); + expect(nonCachedRollovers[0].balance).toBe(0); + expect(nonCachedRollovers[1].balance).toBe(400); }); }); diff --git a/server/tests/advanced/rollovers/rollover4.ts b/server/tests/advanced/rollovers/rollover4.ts deleted file mode 100644 index 13e38b224..000000000 --- a/server/tests/advanced/rollovers/rollover4.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { - type AppEnv, - type Customer, - LegacyVersion, - type LimitedItem, - type Organization, - RolloverDuration, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import { addMonths } from "date-fns"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; - -const rolloverConfig = { - max: 400, - length: 1, - duration: RolloverDuration.Month, -}; -const messagesItem = constructPrepaidItem({ - featureId: TestFeature.Messages, - includedUsage: 100, - billingUnits: 300, - price: 10, - rolloverConfig, -}) as LimitedItem; - -export const pro = constructProduct({ - items: [messagesItem], - type: "pro", - isDefault: false, -}); - -const testCase = "rollover4"; - -describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for usage price feature`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let customer: Customer; - let stripeCli: Stripe; - - let curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const res = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = res.testClockId!; - customer = res.customer; - }); - - const paidQuantity = 300; - const balance = paidQuantity + messagesItem.included_usage; - const options = [ - { - feature_id: TestFeature.Messages, - quantity: paidQuantity, - }, - ]; - - it("should attach pro product", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: pro.id, - options, - }); - }); - - const rollover = 50; - it("should create track messages, reset, and have correct rollover", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: balance - rollover, - }); - - await timeout(3000); - - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addMonths(new Date(), 1).getTime(), - waitForSeconds: 20, - }); - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - - // @ts-expect-error - const rollovers = msgesFeature?.rollovers; - - expect(msgesFeature).to.exist; - expect(msgesFeature?.balance).to.equal(balance + rollover); - expect(rollovers[0].balance).to.equal(rollover); - }); - - // let usage2 = 50; - it("should reset again and have correct rollover", async () => { - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addMonths(curUnix, 1).getTime(), - waitForSeconds: 20, - }); - - const newRollover = Math.min(balance + rollover, rolloverConfig.max); - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - // @ts-expect-error - const rollovers = msgesFeature?.rollovers; - - expect(msgesFeature).to.exist; - expect(msgesFeature?.balance).to.equal(balance + newRollover); - expect(rollovers[0].balance).to.equal(0); - expect(rollovers[1].balance).to.equal(400); - }); -}); diff --git a/server/tests/advanced/rollovers/rollover5.backup.ts b/server/tests/advanced/rollovers/rollover5.backup.ts deleted file mode 100644 index 13a77c758..000000000 --- a/server/tests/advanced/rollovers/rollover5.backup.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { - type AppEnv, - type Customer, - LegacyVersion, - type LimitedItem, - type Organization, - RolloverDuration, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { resetAndGetCusEnt } from "./rolloverTestUtils.js"; - -const freeRollover = { max: 1000, length: 1, duration: RolloverDuration.Month }; -const proRollover = { max: 600, length: 1, duration: RolloverDuration.Month }; - -const freeMsges = constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 500, - rolloverConfig: freeRollover, -}) as LimitedItem; - -const proMsges = constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 500, - rolloverConfig: proRollover, -}) as LimitedItem; - -const free = constructProduct({ - items: [freeMsges], - type: "free", - isDefault: false, -}); - -const pro = constructProduct({ - items: [proMsges], - type: "pro", -}); - -const testCase = "rollover5"; - -describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for upgrade`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let customer: Customer; - let stripeCli: Stripe; - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [free, pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [free, pro], - customerId, - db, - orgId: org.id, - env, - }); - - const res = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = res.testClockId!; - customer = res.customer; - }); - - it("should attach free product", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: free.id, - }); - }); - - it("should create rollovers", async () => { - await resetAndGetCusEnt({ - customer, - db, - productGroup: testCase, - featureId: TestFeature.Messages, - }); - await resetAndGetCusEnt({ - customer, - db, - productGroup: testCase, - featureId: TestFeature.Messages, - }); - - // Attach pro - await autumn.attach({ - customer_id: customerId, - product_id: pro.id, - }); - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - const freeRolloverBalance = freeMsges.included_usage * 2; - const proRolloverBalance = Math.min(proRollover.max, freeRolloverBalance); - - expect(msgesFeature).to.exist; - expect(msgesFeature?.balance).to.equal( - proMsges.included_usage + proRolloverBalance, - ); - // @ts-expect-error - const rollovers = msgesFeature?.rollovers; - expect(rollovers[0].balance).to.equal(100); - expect(rollovers[1].balance).to.equal(500); - }); -}); diff --git a/server/tests/advanced/rollovers/rollover5.test.ts b/server/tests/advanced/rollovers/rollover5.test.ts index cb24c58b5..9e86fc6df 100644 --- a/server/tests/advanced/rollovers/rollover5.test.ts +++ b/server/tests/advanced/rollovers/rollover5.test.ts @@ -1,19 +1,20 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type Customer, LegacyVersion, type LimitedItem, RolloverDuration, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import type Stripe from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { timeout } from "../../utils/genUtils.js"; import { resetAndGetCusEnt } from "./rolloverTestUtils.js"; const freeRollover = { max: 1000, length: 1, duration: RolloverDuration.Month }; @@ -110,9 +111,27 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for upgrade`)}`, expect(msgesFeature?.balance).toBe( proMsges.included_usage + proRolloverBalance, ); - // @ts-expect-error const rollovers = msgesFeature?.rollovers; - expect(rollovers[0].balance).toBe(100); + // @ts-expect-error (rollovers is an array of rollovers) + expect(rollovers?.[0].balance).toBe(100); + // @ts-expect-error (rollovers is an array of rollovers) expect(rollovers[1].balance).toBe(500); + + // Verify non-cached customer balance + await timeout(2000); + const nonCachedCustomer = await autumn.customers.get(customerId, { + skip_cache: "true", + }); + const nonCachedMsgesFeature = + nonCachedCustomer.features[TestFeature.Messages]; + expect(nonCachedMsgesFeature?.balance).toBe( + proMsges.included_usage + proRolloverBalance, + ); + + const nonCachedRollovers = nonCachedMsgesFeature?.rollovers; + // @ts-expect-error (rollovers is an array of rollovers) + expect(nonCachedRollovers[0].balance).toBe(100); + // @ts-expect-error (rollovers is an array of rollovers) + expect(nonCachedRollovers[1].balance).toBe(500); }); }); diff --git a/server/tests/advanced/rollovers/rollover5.ts b/server/tests/advanced/rollovers/rollover5.ts deleted file mode 100644 index 13a77c758..000000000 --- a/server/tests/advanced/rollovers/rollover5.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { - type AppEnv, - type Customer, - LegacyVersion, - type LimitedItem, - type Organization, - RolloverDuration, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { resetAndGetCusEnt } from "./rolloverTestUtils.js"; - -const freeRollover = { max: 1000, length: 1, duration: RolloverDuration.Month }; -const proRollover = { max: 600, length: 1, duration: RolloverDuration.Month }; - -const freeMsges = constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 500, - rolloverConfig: freeRollover, -}) as LimitedItem; - -const proMsges = constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 500, - rolloverConfig: proRollover, -}) as LimitedItem; - -const free = constructProduct({ - items: [freeMsges], - type: "free", - isDefault: false, -}); - -const pro = constructProduct({ - items: [proMsges], - type: "pro", -}); - -const testCase = "rollover5"; - -describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for upgrade`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let customer: Customer; - let stripeCli: Stripe; - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [free, pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [free, pro], - customerId, - db, - orgId: org.id, - env, - }); - - const res = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = res.testClockId!; - customer = res.customer; - }); - - it("should attach free product", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: free.id, - }); - }); - - it("should create rollovers", async () => { - await resetAndGetCusEnt({ - customer, - db, - productGroup: testCase, - featureId: TestFeature.Messages, - }); - await resetAndGetCusEnt({ - customer, - db, - productGroup: testCase, - featureId: TestFeature.Messages, - }); - - // Attach pro - await autumn.attach({ - customer_id: customerId, - product_id: pro.id, - }); - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - const freeRolloverBalance = freeMsges.included_usage * 2; - const proRolloverBalance = Math.min(proRollover.max, freeRolloverBalance); - - expect(msgesFeature).to.exist; - expect(msgesFeature?.balance).to.equal( - proMsges.included_usage + proRolloverBalance, - ); - // @ts-expect-error - const rollovers = msgesFeature?.rollovers; - expect(rollovers[0].balance).to.equal(100); - expect(rollovers[1].balance).to.equal(500); - }); -}); diff --git a/server/tests/advanced/rollovers/rollover6.backup.ts b/server/tests/advanced/rollovers/rollover6.backup.ts deleted file mode 100644 index f7c1051d0..000000000 --- a/server/tests/advanced/rollovers/rollover6.backup.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { - type AppEnv, - type Customer, - LegacyVersion, - type LimitedItem, - type Organization, - RolloverDuration, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import { addHours, addMonths } from "date-fns"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; -import { resetAndGetCusEnt } from "./rolloverTestUtils.js"; - -const freeRollover = { max: 600, length: 1, duration: RolloverDuration.Month }; -const proRollover = { max: 1000, length: 1, duration: RolloverDuration.Month }; - -const freeMsges = constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 500, - rolloverConfig: freeRollover, -}) as LimitedItem; - -const proMsges = constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 500, - rolloverConfig: proRollover, -}) as LimitedItem; - -const free = constructProduct({ - items: [freeMsges], - type: "free", - isDefault: false, -}); - -const pro = constructProduct({ - items: [proMsges], - type: "pro", -}); - -const testCase = "rollover6"; - -describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for upgrade`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let customer: Customer; - let stripeCli: Stripe; - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [free, pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [free, pro], - customerId, - db, - orgId: org.id, - env, - }); - - const res = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = res.testClockId!; - customer = res.customer; - }); - - it("should attach free product", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: pro.id, - }); - }); - - it("should create rollovers", async () => { - await resetAndGetCusEnt({ - customer, - db, - productGroup: testCase, - featureId: TestFeature.Messages, - }); - await resetAndGetCusEnt({ - customer, - db, - productGroup: testCase, - featureId: TestFeature.Messages, - }); - - // Attach pro - await autumn.attach({ - customer_id: customerId, - product_id: free.id, - }); - - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addHours( - addMonths(curUnix, 1), - hoursToFinalizeInvoice, - ).getTime(), - waitForSeconds: 20, - }); - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - const proRolloverBalance = proMsges.included_usage * 2; - const freeRolloverBalance = Math.min(freeRollover.max, proRolloverBalance); - - expect(msgesFeature).to.exist; - expect(msgesFeature?.balance).to.equal( - freeMsges.included_usage + freeRolloverBalance, - ); - - // @ts-expect-error - const rollovers = msgesFeature?.rollovers; - expect(rollovers[0].balance).to.equal(100); - expect(rollovers[1].balance).to.equal(500); - }); -}); diff --git a/server/tests/advanced/rollovers/rollover6.test.ts b/server/tests/advanced/rollovers/rollover6.test.ts index 11e6b8d6d..89e370c7f 100644 --- a/server/tests/advanced/rollovers/rollover6.test.ts +++ b/server/tests/advanced/rollovers/rollover6.test.ts @@ -1,22 +1,23 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type Customer, LegacyVersion, type LimitedItem, RolloverDuration, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import { addHours, addMonths } from "date-fns"; import type Stripe from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; +import { timeout } from "../../utils/genUtils.js"; import { resetAndGetCusEnt } from "./rolloverTestUtils.js"; const freeRollover = { max: 600, length: 1, duration: RolloverDuration.Month }; @@ -124,9 +125,27 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for upgrade`)}`, freeMsges.included_usage + freeRolloverBalance, ); - // @ts-expect-error const rollovers = msgesFeature?.rollovers; - expect(rollovers[0].balance).toBe(100); + // @ts-expect-error (rollovers is an array of rollovers) + expect(rollovers?.[0].balance).toBe(100); + // @ts-expect-error (rollovers is an array of rollovers) expect(rollovers[1].balance).toBe(500); + + // Verify non-cached customer balance + await timeout(2000); + const nonCachedCustomer = await autumn.customers.get(customerId, { + skip_cache: "true", + }); + const nonCachedMsgesFeature = + nonCachedCustomer.features[TestFeature.Messages]; + expect(nonCachedMsgesFeature?.balance).toBe( + freeMsges.included_usage + freeRolloverBalance, + ); + + const nonCachedRollovers = nonCachedMsgesFeature?.rollovers; + // @ts-expect-error (rollovers is an array of rollovers) + expect(nonCachedRollovers?.[0].balance).toBe(100); + // @ts-expect-error (rollovers is an array of rollovers) + expect(nonCachedRollovers[1].balance).toBe(500); }); }); diff --git a/server/tests/advanced/rollovers/rollover6.ts b/server/tests/advanced/rollovers/rollover6.ts deleted file mode 100644 index f7c1051d0..000000000 --- a/server/tests/advanced/rollovers/rollover6.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { - type AppEnv, - type Customer, - LegacyVersion, - type LimitedItem, - type Organization, - RolloverDuration, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import { addHours, addMonths } from "date-fns"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; -import { resetAndGetCusEnt } from "./rolloverTestUtils.js"; - -const freeRollover = { max: 600, length: 1, duration: RolloverDuration.Month }; -const proRollover = { max: 1000, length: 1, duration: RolloverDuration.Month }; - -const freeMsges = constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 500, - rolloverConfig: freeRollover, -}) as LimitedItem; - -const proMsges = constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 500, - rolloverConfig: proRollover, -}) as LimitedItem; - -const free = constructProduct({ - items: [freeMsges], - type: "free", - isDefault: false, -}); - -const pro = constructProduct({ - items: [proMsges], - type: "pro", -}); - -const testCase = "rollover6"; - -describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for upgrade`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let customer: Customer; - let stripeCli: Stripe; - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [free, pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [free, pro], - customerId, - db, - orgId: org.id, - env, - }); - - const res = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = res.testClockId!; - customer = res.customer; - }); - - it("should attach free product", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: pro.id, - }); - }); - - it("should create rollovers", async () => { - await resetAndGetCusEnt({ - customer, - db, - productGroup: testCase, - featureId: TestFeature.Messages, - }); - await resetAndGetCusEnt({ - customer, - db, - productGroup: testCase, - featureId: TestFeature.Messages, - }); - - // Attach pro - await autumn.attach({ - customer_id: customerId, - product_id: free.id, - }); - - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addHours( - addMonths(curUnix, 1), - hoursToFinalizeInvoice, - ).getTime(), - waitForSeconds: 20, - }); - - const cus = await autumn.customers.get(customerId); - const msgesFeature = cus.features[TestFeature.Messages]; - const proRolloverBalance = proMsges.included_usage * 2; - const freeRolloverBalance = Math.min(freeRollover.max, proRolloverBalance); - - expect(msgesFeature).to.exist; - expect(msgesFeature?.balance).to.equal( - freeMsges.included_usage + freeRolloverBalance, - ); - - // @ts-expect-error - const rollovers = msgesFeature?.rollovers; - expect(rollovers[0].balance).to.equal(100); - expect(rollovers[1].balance).to.equal(500); - }); -}); diff --git a/server/tests/advanced/rollovers/rolloverTestUtils.ts b/server/tests/advanced/rollovers/rolloverTestUtils.ts index d959a4ddc..57ca36f07 100644 --- a/server/tests/advanced/rollovers/rolloverTestUtils.ts +++ b/server/tests/advanced/rollovers/rolloverTestUtils.ts @@ -1,10 +1,9 @@ +import type { Customer } from "@autumn/shared"; import { resetCustomerEntitlement } from "@/cron/cronUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; -import { getMainCusProduct } from "@/internal/customers/cusProducts/cusProductUtils.js"; import { cusProductToCusEnt } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; -import { Customer } from "@autumn/shared"; -import { TestFeature } from "tests/setup/v2Features.js"; +import { getMainCusProduct } from "@/internal/customers/cusProducts/cusProductUtils.js"; export const resetAndGetCusEnt = async ({ db, diff --git a/server/tests/advanced/usage/usage1.backup.ts b/server/tests/advanced/usage/usage1.backup.ts deleted file mode 100644 index 489c0ea1d..000000000 --- a/server/tests/advanced/usage/usage1.backup.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { Customer } from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; -import { v1ProductToBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; -import { calculateMetered1Price } from "@/external/stripe/utils.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { AutumnCli } from "../../cli/AutumnCli.js"; -import { features, products } from "../../global.js"; -import { compareMainProduct } from "../../utils/compare.js"; -import { timeout } from "../../utils/genUtils.js"; -import { advanceClockForInvoice } from "../../utils/stripeUtils.js"; - -const testCase = "usage1"; - -describe(`${chalk.yellowBright("usage1: Testing basic usage product")}`, () => { - const NUM_EVENTS = 50; - const customerId = testCase; - let testClockId: string; - let customer: Customer; - let stripeCli: Stripe; - - before(async function () { - await setupBefore(this); - stripeCli = this.stripeCli; - - const { customer: customer_, testClockId: testClockId_ } = - await initCustomer({ - customerId, - org: this.org, - env: this.env, - db: this.db, - autumn: this.autumnJs, - attachPm: "success", - }); - - customer = customer_; - testClockId = testClockId_; - }); - - it("should attach usage based product", async () => { - await AutumnCli.attach({ - customerId: customerId, - productId: products.proWithOverage.id, - }); - - const res = await AutumnCli.getCustomer(customerId); - - compareMainProduct({ - sent: products.proWithOverage, - cusRes: res, - }); - }); - - it("usage1: should send metered1 events", async () => { - const batchUpdates = []; - for (let i = 0; i < NUM_EVENTS; i++) { - batchUpdates.push( - AutumnCli.sendEvent({ - customerId: customerId, - eventName: features.metered1.eventName, - }), - ); - } - - await Promise.all(batchUpdates); - await timeout(25000); - }); - - it("should have correct metered1 balance after sending events", async () => { - const res: any = await AutumnCli.entitled(customerId, features.metered1.id); - - expect(res!.allowed).to.be.true; - - const balance = res!.balances.find( - (balance: any) => balance.feature_id === features.metered1.id, - ); - - const proOverageAmt = - products.proWithOverage.entitlements.metered1.allowance; - - expect(res!.allowed, "should be allowed").to.be.true; - - expect(balance?.balance, "should have correct metered1 balance").to.equal( - proOverageAmt! - NUM_EVENTS, - ); - - expect(balance?.usage_allowed, "should have usage_allowed").to.be.true; - }); - - // Check invoice - it("should advance stripe test clock and wait for event", async () => { - await advanceClockForInvoice({ - stripeCli, - testClockId, - waitForMeterUpdate: true, - }); - }); - - it("should have correct invoice amount", async () => { - const cusRes = await AutumnCli.getCustomer(customerId); - const invoices = cusRes!.invoices; - - // calculate price - const price = calculateMetered1Price({ - product: products.proWithOverage, - numEvents: NUM_EVENTS, - metered1Feature: features.metered1, - }); - - expect(invoices.length).to.equal(2); - - const invoice = invoices[0]; - - const basePrice = v1ProductToBasePrice({ - prices: products.proWithOverage.prices, - }); - - expect(invoice.total).to.equal( - price + basePrice, - "invoice total should be usage price + base price", - ); - }); -}); diff --git a/server/tests/advanced/usage/usage1.test.ts b/server/tests/advanced/usage/usage1.test.ts index b6a3d7f1a..05f01c67f 100644 --- a/server/tests/advanced/usage/usage1.test.ts +++ b/server/tests/advanced/usage/usage1.test.ts @@ -1,56 +1,92 @@ -import type { Customer } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { + BillingInterval, + Infinite, + ProductItemInterval, + UsageModel, +} from "@autumn/shared"; import chalk from "chalk"; import type Stripe from "stripe"; -import { v1ProductToBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; -import { calculateMetered1Price } from "@/external/stripe/utils.js"; -import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; -import { AutumnCli } from "../../cli/AutumnCli.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; -import { timeout } from "../../utils/genUtils.js"; -import { advanceClockForInvoice } from "../../utils/stripeUtils.js"; -import { sharedProWithOverage } from "./sharedProducts.js"; +import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.js"; +import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { AutumnCli } from "../../cli/AutumnCli.js"; +import { advanceClockForInvoice } from "../../utils/stripeUtils.js"; const testCase = "usage1"; +const proWithOverage = constructRawProduct({ + id: "pro-with-overage", + items: [ + constructPriceItem({ + price: 10, // $10/month (matches global.ts default for monthly price) + interval: BillingInterval.Month, + }), + { + feature_id: TestFeature.Messages, + usage_model: UsageModel.PayPerUse, + included_usage: 10, + interval: ProductItemInterval.Month, + billing_units: 10, + tiers: [ + { + to: 10, + amount: 0.5, // $0.5 per unit + }, + { + to: Infinite, + amount: 0.25, // $0.25 per unit + }, + ], + reset_usage_when_enabled: true, + }, + ], +}); + describe(`${chalk.yellowBright("usage1: Testing basic usage product")}`, () => { const NUM_EVENTS = 50; const customerId = testCase; - let testClockId: string; - let customer: Customer; + let stripeCli: Stripe; + let testClockId: string; beforeAll(async () => { stripeCli = ctx.stripeCli; - const { customer: customer_, testClockId: testClockId_ } = - await initCustomerV3({ - ctx, - customerId, - customerData: { fingerprint: "test" }, - withTestClock: true, - attachPm: "success", - }); + await initProductsV0({ + ctx, + products: [proWithOverage], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId_ } = await initCustomerV3({ + ctx, + customerId, + withTestClock: true, + attachPm: "success", + }); - customer = customer_; testClockId = testClockId_; }); test("should attach usage based product", async () => { await AutumnCli.attach({ customerId: customerId, - productId: sharedProWithOverage.id, + productId: proWithOverage.id, }); const res = await AutumnCli.getCustomer(customerId); - expectCustomerV0Correct({ - sent: sharedProWithOverage, + await expectCustomerV0Correct({ + sent: proWithOverage, cusRes: res, - ctx, }); }); @@ -60,13 +96,13 @@ describe(`${chalk.yellowBright("usage1: Testing basic usage product")}`, () => { batchUpdates.push( AutumnCli.sendEvent({ customerId: customerId, - eventName: TestFeature.Messages, + featureId: TestFeature.Messages, }), ); } await Promise.all(batchUpdates); - await timeout(25000); + // await timeout(25000); }); test("should have correct metered1 balance after sending events", async () => { @@ -80,13 +116,13 @@ describe(`${chalk.yellowBright("usage1: Testing basic usage product")}`, () => { // Convert V2 product to V1 to access entitlements const productV1 = convertProductV2ToV1({ - productV2: sharedProWithOverage, + productV2: proWithOverage, orgId: ctx.org.id, features: ctx.features, }); const proOverageAmt = - productV1.entitlements.messages.allowance; + productV1.entitlements[TestFeature.Messages]?.allowance; expect(res!.allowed, "should be allowed").toBe(true); @@ -110,30 +146,28 @@ describe(`${chalk.yellowBright("usage1: Testing basic usage product")}`, () => { const cusRes = await AutumnCli.getCustomer(customerId); const invoices = cusRes!.invoices; - // Convert V2 product to V1 for price calculations - const productV1 = convertProductV2ToV1({ - productV2: sharedProWithOverage, - orgId: ctx.org.id, - features: ctx.features, - }); - - // calculate price - const price = calculateMetered1Price({ - product: productV1, - numEvents: NUM_EVENTS, - metered1Feature: ctx.features[TestFeature.Messages], - }); - expect(invoices.length).toBe(2); const invoice = invoices[0]; - const basePrice = v1ProductToBasePrice({ - prices: productV1.prices, + // Calculate expected invoice total using getExpectedInvoiceTotal + const expectedTotal = await getExpectedInvoiceTotal({ + customerId, + productId: proWithOverage.id, + usage: [ + { + featureId: TestFeature.Messages, + value: NUM_EVENTS, + }, + ], + stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, }); - expect(invoice.total, "invoice total should be usage price + base price").toBe( - price + basePrice, + expect(invoice.total, "invoice total should match expected total").toBe( + expectedTotal, ); }); }); diff --git a/server/tests/advanced/usage/usage2.backup.ts b/server/tests/advanced/usage/usage2.backup.ts deleted file mode 100644 index 3c5152fb7..000000000 --- a/server/tests/advanced/usage/usage2.backup.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { expect } from "chai"; -import chalk from "chalk"; -import { Decimal } from "decimal.js"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; -import { advanceClockForInvoice } from "tests/utils/stripeUtils.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { AutumnCli } from "../../cli/AutumnCli.js"; -import { advanceProducts, creditSystems, features } from "../../global.js"; -import { getCreditsUsed } from "../../utils/advancedUsageUtils.js"; -import { compareMainProduct } from "../../utils/compare.js"; -import { timeout } from "../../utils/genUtils.js"; - -// FIRST, REGULAR CHECK GPU STARTER MONTHLY - -const testCase = "usage2"; -describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => { - const customerId = testCase; - const PRECISION = 10; - const ASSERT_INVOICE_AMOUNT = true; - const CREDIT_MULTIPLIER = 100000; - - let testClockId = ""; - let totalCreditsUsed = 0; - - let stripeCli: Stripe; - - before(async function () { - await setupBefore(this); - const { testClockId: createdTestClockId } = await initCustomer({ - customerId, - org: this.org, - env: this.env, - db: this.db, - autumn: this.autumnJs, - attachPm: "success", - }); - - testClockId = createdTestClockId; - - stripeCli = this.stripeCli; - }); - - it("should attach gpu system starter", async () => { - await AutumnCli.attach({ - customerId: customerId, - productId: advanceProducts.gpuSystemStarter.id, - }); - - const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: advanceProducts.gpuSystemStarter, - cusRes: res, - }); - }); - - // Use up events - it("should send events and have correct balance (up to 10 DP)", async () => { - const eventCount = 20; - - const batchEvents = []; - for (let i = 0; i < eventCount; i++) { - const randomVal = new Decimal(Math.random().toFixed(PRECISION)) - .mul(CREDIT_MULTIPLIER) - .mul(Math.random() > 0.2 ? 1 : -1) - .toNumber(); - const gpuId = i % 2 === 0 ? features.gpu1.id : features.gpu2.id; - - const creditsUsed = getCreditsUsed( - creditSystems.gpuCredits, - gpuId, - randomVal, - ); - - totalCreditsUsed = new Decimal(totalCreditsUsed) - .plus(creditsUsed) - .toNumber(); - - batchEvents.push( - AutumnCli.sendEvent({ - customerId: customerId, - eventName: gpuId, - properties: { value: randomVal }, - }), - ); - } - - await Promise.all(batchEvents); - - await timeout(10000); - - const { allowed, balanceObj }: any = await AutumnCli.entitled( - customerId, - creditSystems.gpuCredits.id, - true, - ); - - const creditAllowance = - advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!; - - expect(allowed).to.be.true; - expect(balanceObj!.balance).to.equal( - new Decimal(creditAllowance).minus(totalCreditsUsed).toNumber(), - ); - // console.log(" - Total credits used: ", totalCreditsUsed); - // console.log(" - Balance: ", balanceObj!.balance); - }); - - // Check invoice.created event - it("should have correct invoice amount / updated meter balance", async () => { - await advanceClockForInvoice({ - stripeCli, - testClockId, - waitForMeterUpdate: ASSERT_INVOICE_AMOUNT, - }); - // const res = await AutumnCli.getCustomer(customerId); - // const invoices = res!.invoices; - // if (ASSERT_INVOICE_AMOUNT) { - // await checkUsageInvoiceAmount({ - // invoices, - // totalUsage: totalCreditsUsed, - // product: advanceProducts.gpuSystemStarter, - // featureId: creditSystems.gpuCredits.id, - // }); - // } else { - // const { allowed, balanceObj }: any = await AutumnCli.entitled( - // customerId, - // creditSystems.gpuCredits.id, - // true, - // ); - // const allowance = - // advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!; - // assert.equal(balanceObj.balance, allowance); - // } - }); -}); diff --git a/server/tests/advanced/usage/usage2.test.ts b/server/tests/advanced/usage/usage2.test.ts index ef6357fcf..fa43cf39a 100644 --- a/server/tests/advanced/usage/usage2.test.ts +++ b/server/tests/advanced/usage/usage2.test.ts @@ -1,21 +1,53 @@ import { beforeAll, describe, expect, test } from "bun:test"; +import { + BillingInterval, + ProductItemInterval, + UsageModel, +} from "@autumn/shared"; import chalk from "chalk"; import { Decimal } from "decimal.js"; import type Stripe from "stripe"; -import { advanceClockForInvoice } from "tests/utils/stripeUtils.js"; -import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; -import { AutumnCli } from "../../cli/AutumnCli.js"; -import { advanceProducts, creditSystems, features } from "../../global.js"; -import { getCreditsUsed } from "../../utils/advancedUsageUtils.js"; +import { TestFeature } from "tests/setup/v2Features.js"; import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; -import { timeout } from "../../utils/genUtils.js"; +import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; +import { advanceClockForInvoice } from "tests/utils/stripeUtils.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; - -// NOTE: This test uses GPU products from global.ts (advanceProducts.gpuSystemStarter) -// These products are not yet converted to ProductV2 format in sharedProducts.ts -// The test has been migrated to Bun but still uses ProductV1 from global.ts +import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; +import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.js"; +import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { getCreditCost } from "../../../src/internal/features/creditSystemUtils.js"; +import { AutumnCli } from "../../cli/AutumnCli.js"; const testCase = "usage2"; + +// Find credit system feature from test context +const creditsFeature = ctx.features.find((f) => f.id === TestFeature.Credits); + +if (!creditsFeature) { + throw new Error("Credits feature not found in test context"); +} + +const gpuSystemStarter = constructRawProduct({ + id: "gpu-system-starter", + items: [ + constructPriceItem({ + price: 20, // $20/month + interval: BillingInterval.Month, + }), + { + feature_id: TestFeature.Credits, + usage_model: UsageModel.PayPerUse, + included_usage: 500, + interval: ProductItemInterval.Month, + billing_units: 5, + price: 0.01, + reset_usage_when_enabled: true, + }, + ], +}); + describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => { const customerId = testCase; const PRECISION = 10; @@ -28,10 +60,16 @@ describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => { let stripeCli: Stripe; beforeAll(async () => { + await initProductsV0({ + ctx, + products: [gpuSystemStarter], + prefix: testCase, + customerId, + }); + const { testClockId: createdTestClockId } = await initCustomerV3({ ctx, customerId, - customerData: { fingerprint: "test" }, withTestClock: true, attachPm: "success", }); @@ -44,14 +82,13 @@ describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => { test("should attach gpu system starter", async () => { await AutumnCli.attach({ customerId: customerId, - productId: advanceProducts.gpuSystemStarter.id, + productId: gpuSystemStarter.id, }); const res = await AutumnCli.getCustomer(customerId); - expectCustomerV0Correct({ - sent: advanceProducts.gpuSystemStarter, + await expectCustomerV0Correct({ + sent: gpuSystemStarter, cusRes: res, - ctx, }); }); @@ -65,13 +102,13 @@ describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => { .mul(CREDIT_MULTIPLIER) .mul(Math.random() > 0.2 ? 1 : -1) .toNumber(); - const gpuId = i % 2 === 0 ? features.gpu1.id : features.gpu2.id; + const featureId = i % 2 === 0 ? TestFeature.Action1 : TestFeature.Action2; - const creditsUsed = getCreditsUsed( - creditSystems.gpuCredits, - gpuId, - randomVal, - ); + const creditsUsed = getCreditCost({ + creditSystem: creditsFeature, + featureId: featureId, + amount: randomVal, + }); totalCreditsUsed = new Decimal(totalCreditsUsed) .plus(creditsUsed) @@ -80,7 +117,7 @@ describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => { batchEvents.push( AutumnCli.sendEvent({ customerId: customerId, - eventName: gpuId, + featureId: featureId, properties: { value: randomVal }, }), ); @@ -88,16 +125,23 @@ describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => { await Promise.all(batchEvents); - await timeout(10000); + // await timeout(10000); const { allowed, balanceObj }: any = await AutumnCli.entitled( customerId, - creditSystems.gpuCredits.id, + TestFeature.Credits, true, ); + // Convert V2 product to V1 to get allowance + const productV1 = convertProductV2ToV1({ + productV2: gpuSystemStarter, + orgId: ctx.org.id, + features: ctx.features, + }); + const creditAllowance = - advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!; + productV1.entitlements[TestFeature.Credits]?.allowance!; expect(allowed).toBe(true); expect(balanceObj!.balance).toBe( @@ -112,5 +156,37 @@ describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => { testClockId, waitForMeterUpdate: ASSERT_INVOICE_AMOUNT, }); + + const cusRes = await AutumnCli.getCustomer(customerId); + const invoices = cusRes!.invoices; + + // Calculate expected invoice total using getExpectedInvoiceTotal + // We need to convert credits used to the actual usage value + // Since credits are calculated from Action1/Action2 events, we need to track the actual usage + // For now, we'll use totalCreditsUsed as the value for the Credits feature + const expectedTotal = await getExpectedInvoiceTotal({ + customerId, + productId: gpuSystemStarter.id, + usage: [ + { + featureId: TestFeature.Credits, + value: totalCreditsUsed, + }, + ], + stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + + // Find the invoice that matches our product + const invoice = invoices.find((inv: any) => + inv.product_ids.includes(gpuSystemStarter.id), + ); + + expect(invoice, "Invoice should exist").toBeDefined(); + expect(invoice!.total, "invoice total should match expected total").toBe( + expectedTotal, + ); }); }); diff --git a/server/tests/advanced/usage/usage3.backup.ts b/server/tests/advanced/usage/usage3.backup.ts deleted file mode 100644 index 21d4c76c5..000000000 --- a/server/tests/advanced/usage/usage3.backup.ts +++ /dev/null @@ -1,140 +0,0 @@ -import chalk from "chalk"; -import { advanceProducts } from "../../global.js"; -import { AutumnCli } from "../../cli/AutumnCli.js"; -import { sendGPUEvents } from "../../utils/advancedUsageUtils.js"; -import { advanceTestClock } from "../../utils/stripeUtils.js"; -import { assert, expect } from "chai"; -import { Decimal } from "decimal.js"; -import { compareMainProduct } from "../../utils/compare.js"; -import { checkSubscriptionContainsProducts } from "tests/utils/scheduleCheckUtils.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { setupBefore } from "tests/before.js"; -import Stripe from "stripe"; -import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js"; -import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; -import { getSubsFromCusId } from "tests/utils/expectUtils/expectSubUtils.js"; -import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; - -const testCase = "usage3"; -const ASSERT_INVOICE_AMOUNT = true; - -describe(`${chalk.yellowBright( - "usage3: upgrade from GPU starter monthly to GPU pro monthly", -)}`, () => { - const customerId = "usage3"; - let testClockId = ""; - let totalCreditsUsed = 0; - let stripeCli: Stripe; - let curUnix = 0; - - before(async function () { - await setupBefore(this); - let { testClockId: insertedTestClockId } = await initCustomer({ - customerId, - org: this.org, - env: this.env, - db: this.db, - autumn: this.autumnJs, - attachPm: "success", - }); - - testClockId = insertedTestClockId; - stripeCli = this.stripeCli; - }); - - // 1. Attach GPU starter monthly - it("usage3: should attach GPU starter monthly", async function () { - await AutumnCli.attach({ - customerId: customerId, - productId: advanceProducts.gpuSystemStarter.id, - }); - }); - - // 2. Send 20 events - it("usage3: should send 20 events", async function () { - let eventCount = 20; - const { creditsUsed } = await sendGPUEvents({ - customerId, - eventCount, - }); - - totalCreditsUsed = creditsUsed; - }); - - // 3. Advance test clock by 15 days and upgrade - it("should advance test clock by 15 days and upgrade to GPU pro monthly", async function () { - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - numberOfDays: 15, - }); - - await AutumnCli.attach({ - customerId: customerId, - productId: advanceProducts.gpuSystemPro.id, - }); - - // MAKE SURE STRIPE SUB ONLY HAS GPU PRO - - const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: advanceProducts.gpuSystemPro, - cusRes: res, - }); - - let subscriptionId = res.products[0].subscription_ids![0]!; - await checkSubscriptionContainsProducts({ - db: this.db, - org: this.org, - env: this.env, - subscriptionId, - productIds: [advanceProducts.gpuSystemPro.id], - }); - }); - - // 4. Check invoice for 15 days of starter usage - it("should have invoice for 15 days of starter usage", async function () { - const res = await AutumnCli.getCustomer(customerId); - const invoices = res!.invoices; - - let basePrice1 = advanceProducts.gpuSystemStarter.prices[0].config.amount; - let basePrice2 = advanceProducts.gpuSystemPro.prices[0].config.amount; - - let { subs } = await getSubsFromCusId({ - db: this.db, - org: this.org, - env: this.env, - customerId, - stripeCli, - productId: advanceProducts.gpuSystemPro.id, - }); - - let sub = subs[0]; - - const { start, end } = subToPeriodStartEnd({ sub }); - let baseDiff = calculateProrationAmount({ - periodStart: start * 1000, - periodEnd: end * 1000, - now: curUnix, - amount: basePrice2 - basePrice1, - allowNegative: true, - }); - - let usagePrice = advanceProducts.gpuSystemStarter.prices[1]; - let overage = - totalCreditsUsed - - advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!; - - let overagePrice = priceToInvoiceAmount({ - price: usagePrice, - overage, - }); - - let calculatedTotal = new Decimal(baseDiff) - .plus(overagePrice) - .toDecimalPlaces(2) - .toNumber(); - - expect(invoices[0].total).to.equal(calculatedTotal); - }); -}); diff --git a/server/tests/advanced/usage/usage3.test.ts b/server/tests/advanced/usage/usage3.test.ts index 0e660b3a8..aa1c89bb4 100644 --- a/server/tests/advanced/usage/usage3.test.ts +++ b/server/tests/advanced/usage/usage3.test.ts @@ -1,42 +1,99 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { + BillingInterval, + ProductItemInterval, + UsageModel, +} from "@autumn/shared"; import chalk from "chalk"; -import { advanceProducts } from "../../global.js"; -import { AutumnCli } from "../../cli/AutumnCli.js"; -import { sendGPUEvents } from "../../utils/advancedUsageUtils.js"; -import { advanceTestClock } from "../../utils/stripeUtils.js"; -import { expect } from "bun:test"; import { Decimal } from "decimal.js"; +import type Stripe from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; -import { checkSubscriptionContainsProducts } from "tests/utils/scheduleCheckUtils.js"; -import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; -import { beforeAll, describe, test } from "bun:test"; -import Stripe from "stripe"; -import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js"; -import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; import { getSubsFromCusId } from "tests/utils/expectUtils/expectSubUtils.js"; -import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import { checkSubscriptionContainsProducts } from "tests/utils/scheduleCheckUtils.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; - -// NOTE: This test uses GPU products from global.ts (advanceProducts.gpuSystemStarter, gpuSystemPro) -// These products are not yet converted to ProductV2 format in sharedProducts.ts -// The test has been migrated to Bun but still uses ProductV1 from global.ts +import { v1ProductToBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; +import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import { getCreditCost } from "@/internal/features/creditSystemUtils.js"; +import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; +import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js"; +import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; +import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.js"; +import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { AutumnCli } from "../../cli/AutumnCli.js"; +import { advanceTestClock } from "../../utils/stripeUtils.js"; const testCase = "usage3"; -const ASSERT_INVOICE_AMOUNT = true; +const PRECISION = 10; +const CREDIT_MULTIPLIER = 100000; + +// Find credit system feature from test context +const creditsFeature = ctx.features.find((f) => f.id === TestFeature.Credits); + +if (!creditsFeature) { + throw new Error("Credits feature not found in test context"); +} + +const gpuSystemStarter = constructRawProduct({ + id: "gpu-system-starter", + items: [ + constructPriceItem({ + price: 20, // $20/month + interval: BillingInterval.Month, + }), + { + feature_id: TestFeature.Credits, + usage_model: UsageModel.PayPerUse, + included_usage: 500, + interval: ProductItemInterval.Month, + billing_units: 5, + price: 0.01, + reset_usage_when_enabled: true, + }, + ], +}); + +const gpuSystemPro = constructRawProduct({ + id: "gpu-system-pro", + items: [ + constructPriceItem({ + price: 100, // $100/month + interval: BillingInterval.Month, + }), + { + feature_id: TestFeature.Credits, + usage_model: UsageModel.PayPerUse, + included_usage: 5000, + interval: ProductItemInterval.Month, + billing_units: 1, + price: 0.01, + reset_usage_when_enabled: true, + }, + ], +}); describe(`${chalk.yellowBright( "usage3: upgrade from GPU starter monthly to GPU pro monthly", )}`, () => { - const customerId = "usage3"; + const customerId = testCase; let testClockId = ""; let totalCreditsUsed = 0; let stripeCli: Stripe; let curUnix = 0; beforeAll(async () => { - let { testClockId: insertedTestClockId } = await initCustomerV3({ + await initProductsV0({ + ctx, + products: [gpuSystemStarter, gpuSystemPro], + prefix: testCase, + customerId, + }); + + const { testClockId: insertedTestClockId } = await initCustomerV3({ ctx, customerId, - customerData: { fingerprint: "test" }, withTestClock: true, attachPm: "success", }); @@ -49,19 +106,42 @@ describe(`${chalk.yellowBright( test("usage3: should attach GPU starter monthly", async () => { await AutumnCli.attach({ customerId: customerId, - productId: advanceProducts.gpuSystemStarter.id, + productId: gpuSystemStarter.id, }); }); // 2. Send 20 events test("usage3: should send 20 events", async () => { - let eventCount = 20; - const { creditsUsed } = await sendGPUEvents({ - customerId, - eventCount, - }); + const eventCount = 20; + const batchEvents = []; + for (let i = 0; i < eventCount; i++) { + const randomVal = new Decimal(Math.random().toFixed(PRECISION)) + .mul(CREDIT_MULTIPLIER) + .mul(Math.random() > 0.2 ? 1 : -1) + .toNumber(); + const featureId = i % 2 === 0 ? TestFeature.Action1 : TestFeature.Action2; - totalCreditsUsed = creditsUsed; + const creditsUsed = getCreditCost({ + creditSystem: creditsFeature, + featureId: featureId, + amount: randomVal, + }); + + totalCreditsUsed = new Decimal(totalCreditsUsed) + .plus(creditsUsed) + .toNumber(); + + batchEvents.push( + AutumnCli.sendEvent({ + customerId: customerId, + featureId: featureId, + properties: { value: randomVal }, + }), + ); + } + + await Promise.all(batchEvents); + await new Promise((resolve) => setTimeout(resolve, 15000)); }); // 3. Advance test clock by 15 days and upgrade @@ -74,25 +154,24 @@ describe(`${chalk.yellowBright( await AutumnCli.attach({ customerId: customerId, - productId: advanceProducts.gpuSystemPro.id, + productId: gpuSystemPro.id, }); // MAKE SURE STRIPE SUB ONLY HAS GPU PRO const res = await AutumnCli.getCustomer(customerId); - expectCustomerV0Correct({ - sent: advanceProducts.gpuSystemPro, + await expectCustomerV0Correct({ + sent: gpuSystemPro, cusRes: res, - ctx, }); - let subscriptionId = res.products[0].subscription_ids![0]!; + const subscriptionId = res.products[0].subscription_ids![0]!; await checkSubscriptionContainsProducts({ db: ctx.db, org: ctx.org, env: ctx.env, subscriptionId, - productIds: [advanceProducts.gpuSystemPro.id], + productIds: [gpuSystemPro.id], }); }); @@ -101,22 +180,35 @@ describe(`${chalk.yellowBright( const res = await AutumnCli.getCustomer(customerId); const invoices = res!.invoices; - let basePrice1 = advanceProducts.gpuSystemStarter.prices[0].config.amount; - let basePrice2 = advanceProducts.gpuSystemPro.prices[0].config.amount; + // Convert V2 products to V1 to access prices + const starterV1 = convertProductV2ToV1({ + productV2: gpuSystemStarter, + orgId: ctx.org.id, + features: ctx.features, + }); - let { subs } = await getSubsFromCusId({ + const proV1 = convertProductV2ToV1({ + productV2: gpuSystemPro, + orgId: ctx.org.id, + features: ctx.features, + }); + + const basePrice1 = v1ProductToBasePrice({ prices: starterV1.prices }); + const basePrice2 = v1ProductToBasePrice({ prices: proV1.prices }); + + const { subs } = await getSubsFromCusId({ db: ctx.db, org: ctx.org, env: ctx.env, customerId, stripeCli, - productId: advanceProducts.gpuSystemPro.id, + productId: gpuSystemPro.id, }); - let sub = subs[0]; + const sub = subs[0]; const { start, end } = subToPeriodStartEnd({ sub }); - let baseDiff = calculateProrationAmount({ + const baseDiff = calculateProrationAmount({ periodStart: start * 1000, periodEnd: end * 1000, now: curUnix, @@ -124,17 +216,17 @@ describe(`${chalk.yellowBright( allowNegative: true, }); - let usagePrice = advanceProducts.gpuSystemStarter.prices[1]; - let overage = - totalCreditsUsed - - advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!; + const usagePrice = starterV1.prices[1]; + const starterAllowance = + starterV1.entitlements[TestFeature.Credits]?.allowance!; + const overage = totalCreditsUsed - starterAllowance; - let overagePrice = priceToInvoiceAmount({ + const overagePrice = priceToInvoiceAmount({ price: usagePrice, overage, }); - let calculatedTotal = new Decimal(baseDiff) + const calculatedTotal = new Decimal(baseDiff) .plus(overagePrice) .toDecimalPlaces(2) .toNumber(); diff --git a/server/tests/advanced/usage/usage4.backup.ts b/server/tests/advanced/usage/usage4.backup.ts deleted file mode 100644 index 181e1cd24..000000000 --- a/server/tests/advanced/usage/usage4.backup.ts +++ /dev/null @@ -1,172 +0,0 @@ -import type { Customer } from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { AutumnCli } from "../../cli/AutumnCli.js"; -import { advanceProducts, creditSystems } from "../../global.js"; -import { - checkCreditBalance, - checkUsageInvoiceAmount, - sendGPUEvents, -} from "../../utils/advancedUsageUtils.js"; -import { compareMainProduct } from "../../utils/compare.js"; -import { advanceClockForInvoice } from "../../utils/stripeUtils.js"; - -// THIRD, TEST GPU PRO ANNUAL - -const testCase = "usage4"; - -describe(`${chalk.yellowBright("usage4: GPU starter annual")}`, () => { - const customerId = testCase; - let totalCreditsUsed = 0; - - let testClockId = ""; - let customer: Customer; - let stripeCli: Stripe; - - before(async function () { - await setupBefore(this); - const res = await initCustomer({ - customerId, - org: this.org, - env: this.env, - db: this.db, - autumn: this.autumnJs, - attachPm: "success", - }); - - testClockId = res.testClockId; - customer = res.customer; - stripeCli = this.stripeCli; - }); - - it("should attach GPU starter annual", async () => { - await AutumnCli.attach({ - customerId: customerId, - productId: advanceProducts.gpuStarterAnnual.id, - }); - - const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: advanceProducts.gpuStarterAnnual, - cusRes: res, - }); - - expect(res!.invoices.length).to.equal(1); - }); - - it("should send 20 events and have correct balance", async () => { - const eventCount = 20; - const { creditsUsed } = await sendGPUEvents({ - customerId, - eventCount, - }); - - totalCreditsUsed = creditsUsed; - await checkCreditBalance({ - customerId, - featureId: creditSystems.gpuCredits.id, - totalCreditsUsed, - originalAllowance: - advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!, - }); - }); - - it("should have invoice after a month and correct balance", async () => { - await advanceClockForInvoice({ - stripeCli, - testClockId, - waitForMeterUpdate: true, - }); - - const res = await AutumnCli.getCustomer(customerId); - const invoices = res!.invoices; - - const invoiceIndex = invoices.findIndex((invoice: any) => - invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id), - ); - - await checkUsageInvoiceAmount({ - invoices, - totalUsage: totalCreditsUsed, - product: advanceProducts.gpuStarterAnnual, - featureId: creditSystems.gpuCredits.id, - invoiceIndex, - includeBase: false, - }); - - await checkCreditBalance({ - customerId, - featureId: creditSystems.gpuCredits.id, - totalCreditsUsed: 0, - originalAllowance: - advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!, - }); - }); -}); - -// // Advance by 1 year and check if latest invoice is correct -// it.skip("should have correct invoice after 1 year", async function () { -// const stripeCli = createStripeCli({ org: this.org, env: this.env }); - -// // 1. Advance by 11 months -// let numberOfMonths = 11; -// await advanceMonths({ -// stripeCli, -// testClockId, -// numberOfMonths, -// }); - -// // 2. Send 20 events -// let eventCount = 20; -// const { creditsUsed } = await sendGPUEvents({ -// customerId, -// eventCount, -// }); - -// let totalCreditsUsed = creditsUsed; -// console.log(" - Total credits used: ", totalCreditsUsed); - -// // Advance by a month and check for usage -// await advanceClockForInvoice({ -// stripeCli, -// testClockId, -// waitForMeterUpdate: true, -// startingFrom: addMonths(new Date(), numberOfMonths), -// }); - -// const res = await AutumnCli.getCustomer(customerId); -// const invoices = res!.invoices; - -// let usagePrice = await getUsageInArrearPrice({ -// org: this.org, -// env: this.env, -// productId: advanceProducts.gpuStarterAnnual.id, -// }); - -// // Get billing meter event summary -// let eventSummary = await checkBillingMeterEventSummary({ -// stripeCli, -// startTime: addMonths(new Date(), 11), -// stripeMeterId: usagePrice?.config?.stripe_meter_id, -// stripeCustomerId: customer.processor.id, -// }); - -// try { -// assert.exists(eventSummary); -// assert.equal( -// eventSummary?.aggregated_value, -// Math.round(totalCreditsUsed), -// ); -// assert.equal(invoices.length, 13 + 2); -// } catch (error) { -// console.group(); -// console.log(" - Event summary: ", eventSummary); -// console.log(" - Total credits used: ", totalCreditsUsed); -// console.log(" - Last 3 invoices: ", invoices.slice(-3)); -// console.groupEnd(); -// throw error; -// } -// }); diff --git a/server/tests/advanced/usage/usage4.test.ts b/server/tests/advanced/usage/usage4.test.ts index c950aadce..0945d3cdb 100644 --- a/server/tests/advanced/usage/usage4.test.ts +++ b/server/tests/advanced/usage/usage4.test.ts @@ -1,59 +1,94 @@ -import type { Customer } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { + BillingInterval, + ProductItemInterval, + UsageModel, +} from "@autumn/shared"; import chalk from "chalk"; +import { Decimal } from "decimal.js"; import type Stripe from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; +import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.js"; +import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { getCreditCost } from "../../../src/internal/features/creditSystemUtils.js"; import { AutumnCli } from "../../cli/AutumnCli.js"; -import { advanceProducts, creditSystems } from "../../global.js"; import { checkCreditBalance, - checkUsageInvoiceAmount, - sendGPUEvents, + checkUsageInvoiceAmountV2, } from "../../utils/advancedUsageUtils.js"; -import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; import { advanceClockForInvoice } from "../../utils/stripeUtils.js"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; - -// NOTE: This test uses GPU products from global.ts (advanceProducts.gpuStarterAnnual) -// These products are not yet converted to ProductV2 format in sharedProducts.ts -// The test has been migrated to Bun but still uses ProductV1 from global.ts -// However, it does use checkUsageInvoiceAmountV2 for the V2 helper function const testCase = "usage4"; +// Find credit system feature from test context +const creditsFeature = ctx.features.find((f) => f.id === TestFeature.Credits); + +if (!creditsFeature) { + throw new Error("Credits feature not found in test context"); +} + +const gpuStarterAnnual = constructRawProduct({ + id: "gpu-starter-annual", + items: [ + constructPriceItem({ + price: 200, // $200/year + interval: BillingInterval.Year, + }), + { + feature_id: TestFeature.Credits, + usage_model: UsageModel.PayPerUse, + included_usage: 500, + interval: ProductItemInterval.Month, + billing_units: 1, + price: 0.01, + reset_usage_when_enabled: true, + }, + ], +}); + describe(`${chalk.yellowBright("usage4: GPU starter annual")}`, () => { const customerId = testCase; + const PRECISION = 10; + const CREDIT_MULTIPLIER = 100000; let totalCreditsUsed = 0; let testClockId = ""; - let customer: Customer; let stripeCli: Stripe; beforeAll(async () => { + await initProductsV0({ + ctx, + products: [gpuStarterAnnual], + prefix: testCase, + customerId, + }); + const res = await initCustomerV3({ ctx, customerId, - customerData: { fingerprint: "test" }, withTestClock: true, attachPm: "success", }); testClockId = res.testClockId; - customer = res.customer; stripeCli = ctx.stripeCli; }); test("should attach GPU starter annual", async () => { await AutumnCli.attach({ customerId: customerId, - productId: advanceProducts.gpuStarterAnnual.id, + productId: gpuStarterAnnual.id, }); const res = await AutumnCli.getCustomer(customerId); - expectCustomerV0Correct({ - sent: advanceProducts.gpuStarterAnnual, + await expectCustomerV0Correct({ + sent: gpuStarterAnnual, cusRes: res, - ctx, }); expect(res!.invoices.length).toBe(1); @@ -61,18 +96,53 @@ describe(`${chalk.yellowBright("usage4: GPU starter annual")}`, () => { test("should send 20 events and have correct balance", async () => { const eventCount = 20; - const { creditsUsed } = await sendGPUEvents({ - customerId, - eventCount, + + const batchEvents = []; + for (let i = 0; i < eventCount; i++) { + const randomVal = new Decimal(Math.random().toFixed(PRECISION)) + .mul(CREDIT_MULTIPLIER) + .mul(Math.random() > 0.2 ? 1 : -1) + .toNumber(); + const featureId = i % 2 === 0 ? TestFeature.Action1 : TestFeature.Action2; + + const creditsUsed = getCreditCost({ + creditSystem: creditsFeature, + featureId: featureId, + amount: randomVal, + }); + + totalCreditsUsed = new Decimal(totalCreditsUsed) + .plus(creditsUsed) + .toNumber(); + + batchEvents.push( + AutumnCli.sendEvent({ + customerId: customerId, + featureId: featureId, + properties: { value: randomVal }, + }), + ); + } + + await Promise.all(batchEvents); + await new Promise((resolve) => setTimeout(resolve, 15000)); + + // Convert V2 product to V1 to get allowance + const productV1 = convertProductV2ToV1({ + productV2: gpuStarterAnnual, + orgId: ctx.org.id, + features: ctx.features, }); - totalCreditsUsed = creditsUsed; + const originalAllowance = Object.values(productV1.entitlements).find( + (ent: any) => ent.feature_id === TestFeature.Credits, + )?.allowance!; + await checkCreditBalance({ customerId, - featureId: creditSystems.gpuCredits.id, + featureId: TestFeature.Credits, totalCreditsUsed, - originalAllowance: - advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!, + originalAllowance, }); }); @@ -83,30 +153,47 @@ describe(`${chalk.yellowBright("usage4: GPU starter annual")}`, () => { waitForMeterUpdate: true, }); + // await advanceTestClock({ + // stripeCli, + // testClockId, + // advanceTo: addHours( + // addMonths(new Date(), 1), + // hoursToFinalizeInvoice, + // ).getTime(), + // }); + const res = await AutumnCli.getCustomer(customerId); const invoices = res!.invoices; const invoiceIndex = invoices.findIndex((invoice: any) => - invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id), + invoice.product_ids.includes(gpuStarterAnnual.id), ); - // NOTE: Using checkUsageInvoiceAmount (V1) as gpuStarterAnnual is not yet converted to V2 - // When GPU products are migrated to ProductV2, this should use checkUsageInvoiceAmountV2 - await checkUsageInvoiceAmount({ + await checkUsageInvoiceAmountV2({ invoices, totalUsage: totalCreditsUsed, - product: advanceProducts.gpuStarterAnnual, - featureId: creditSystems.gpuCredits.id, + product: gpuStarterAnnual, + featureId: TestFeature.Credits, invoiceIndex, includeBase: false, }); + // Convert V2 product to V1 to get allowance + const productV1 = convertProductV2ToV1({ + productV2: gpuStarterAnnual, + orgId: ctx.org.id, + features: ctx.features, + }); + + const originalAllowance = Object.values(productV1.entitlements).find( + (ent: any) => ent.feature_id === TestFeature.Credits, + )?.allowance!; + await checkCreditBalance({ customerId, - featureId: creditSystems.gpuCredits.id, + featureId: TestFeature.Credits, totalCreditsUsed: 0, - originalAllowance: - advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!, + originalAllowance, }); }); }); diff --git a/server/tests/advanced/usageLimit/usageLimit1.backup.ts b/server/tests/advanced/usageLimit/usageLimit1.backup.ts deleted file mode 100644 index 417433e12..000000000 --- a/server/tests/advanced/usageLimit/usageLimit1.backup.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { - type AppEnv, - ErrCode, - LegacyVersion, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -const userItem = constructArrearProratedItem({ - featureId: TestFeature.Users, - pricePerUnit: 50, - includedUsage: 0, - usageLimit: 2, -}); - -export const pro = constructProduct({ - items: [userItem], - type: "pro", -}); - -const testCase = "usageLimit1"; - -describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for entities`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - { - id: "3", - name: "Entity 3", - feature_id: TestFeature.Users, - }, - { - id: "4", - name: "Entity 4", - feature_id: TestFeature.Users, - }, - ]; - - it("should attach pro product", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - it("should create more entities than the limit and hit error", async () => { - await expectAutumnError({ - errCode: ErrCode.FeatureLimitReached, - func: async () => { - await autumn.entities.create(customerId, entities); - }, - }); - }); - - it("should create entities one by one, then hit usage limit", async () => { - await autumn.entities.create(customerId, entities[0]); - await autumn.entities.create(customerId, entities[1]); - - await expectAutumnError({ - errCode: ErrCode.FeatureLimitReached, - func: async () => { - await autumn.entities.create(customerId, entities[2]); - }, - }); - }); - - it("should have correct check and get customer value", async () => { - const check = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Users, - }); - const customer = await autumn.customers.get(customerId); - - expect(check.balance).to.equal(-2); - // @ts-expect-error - expect(check.usage_limit).to.equal(userItem.usage_limit); - - // @ts-expect-error - expect(customer.features[TestFeature.Users].usage_limit).to.equal( - userItem.usage_limit, - ); - }); -}); diff --git a/server/tests/advanced/usageLimit/usageLimit1.test.ts b/server/tests/advanced/usageLimit/usageLimit1.test.ts index b93560245..5be589ad3 100644 --- a/server/tests/advanced/usageLimit/usageLimit1.test.ts +++ b/server/tests/advanced/usageLimit/usageLimit1.test.ts @@ -1,14 +1,10 @@ -import { - ErrCode, - LegacyVersion, -} from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { ErrCode, LegacyVersion, type LimitedItem } from "@autumn/shared"; import chalk from "chalk"; -import type Stripe from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; @@ -20,7 +16,7 @@ const userItem = constructArrearProratedItem({ pricePerUnit: 50, includedUsage: 0, usageLimit: 2, -}); +}) as LimitedItem; export const pro = constructProduct({ items: [userItem], @@ -32,14 +28,8 @@ const testCase = "usageLimit1"; describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for entities`)}`, () => { const customerId = testCase; const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); beforeAll(async () => { - stripeCli = ctx.stripeCli; - await initProductsV0({ ctx, products: [pro], @@ -47,15 +37,13 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for entities`) customerId, }); - const { testClockId: testClockId1 } = await initCustomerV3({ + await initCustomerV3({ ctx, customerId, customerData: {}, attachPm: "success", withTestClock: true, }); - - testClockId = testClockId1!; }); const entities = [ @@ -121,12 +109,11 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for entities`) const customer = await autumn.customers.get(customerId); expect(check.balance).toBe(-2); - // @ts-expect-error - expect(check.usage_limit).toBe(userItem.usage_limit); - // @ts-expect-error + expect(check.usage_limit).toBe(userItem.usage_limit!); + expect(customer.features[TestFeature.Users].usage_limit).toBe( - userItem.usage_limit, + userItem.usage_limit!, ); }); }); diff --git a/server/tests/advanced/usageLimit/usageLimit1.ts b/server/tests/advanced/usageLimit/usageLimit1.ts deleted file mode 100644 index 417433e12..000000000 --- a/server/tests/advanced/usageLimit/usageLimit1.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { - type AppEnv, - ErrCode, - LegacyVersion, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -const userItem = constructArrearProratedItem({ - featureId: TestFeature.Users, - pricePerUnit: 50, - includedUsage: 0, - usageLimit: 2, -}); - -export const pro = constructProduct({ - items: [userItem], - type: "pro", -}); - -const testCase = "usageLimit1"; - -describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for entities`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - { - id: "3", - name: "Entity 3", - feature_id: TestFeature.Users, - }, - { - id: "4", - name: "Entity 4", - feature_id: TestFeature.Users, - }, - ]; - - it("should attach pro product", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - it("should create more entities than the limit and hit error", async () => { - await expectAutumnError({ - errCode: ErrCode.FeatureLimitReached, - func: async () => { - await autumn.entities.create(customerId, entities); - }, - }); - }); - - it("should create entities one by one, then hit usage limit", async () => { - await autumn.entities.create(customerId, entities[0]); - await autumn.entities.create(customerId, entities[1]); - - await expectAutumnError({ - errCode: ErrCode.FeatureLimitReached, - func: async () => { - await autumn.entities.create(customerId, entities[2]); - }, - }); - }); - - it("should have correct check and get customer value", async () => { - const check = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Users, - }); - const customer = await autumn.customers.get(customerId); - - expect(check.balance).to.equal(-2); - // @ts-expect-error - expect(check.usage_limit).to.equal(userItem.usage_limit); - - // @ts-expect-error - expect(customer.features[TestFeature.Users].usage_limit).to.equal( - userItem.usage_limit, - ); - }); -}); diff --git a/server/tests/advanced/usageLimit/usageLimit2.backup.ts b/server/tests/advanced/usageLimit/usageLimit2.backup.ts deleted file mode 100644 index 58e2dc733..000000000 --- a/server/tests/advanced/usageLimit/usageLimit2.backup.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { - type AppEnv, - LegacyVersion, - type LimitedItem, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { - constructArrearItem, - constructFeatureItem, -} from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -const messageItem = constructArrearItem({ - featureId: TestFeature.Messages, - includedUsage: 100, - billingUnits: 1, - price: 0.5, - usageLimit: 500, -}) as LimitedItem; - -export const pro = constructProduct({ - items: [messageItem], - type: "pro", -}); - -const addOnMessages = constructFeatureItem({ - featureId: TestFeature.Messages, - interval: null, - includedUsage: 250, -}) as LimitedItem; - -const messageAddOn = constructProduct({ - type: "one_off", - items: [addOnMessages], -}); - -const testCase = "usageLimit2"; - -describe(`${chalk.yellowBright(`${testCase}: Testing usage limits, usage prices`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, messageAddOn], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro, messageAddOn], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - it("should attach pro product", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - - const initialUsage = - messageItem.included_usage + messageItem.usage_limit! + 1000; - - it("should track more messages than limit and not surpass", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: initialUsage, - }); - - await timeout(2000); - - const check = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - }); - const customer = await autumn.customers.get(customerId); - - const expectedBalance = - messageItem.included_usage - messageItem.usage_limit!; - - expect(check.balance).to.equal(expectedBalance); - expect(check.allowed).to.equal(false); - // @ts-expect-error - expect(check.usage_limit!).to.equal(messageItem.usage_limit!); - // @ts-expect-error - expect(customer.features[TestFeature.Messages].usage_limit).to.equal( - messageItem.usage_limit!, - ); - }); - - it("should purchase add ons and have correct check results", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: messageAddOn.id, - }); - - const check = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - }); - const customer = await autumn.customers.get(customerId); - const expectedBalance = - messageItem.included_usage - - messageItem.usage_limit! + - addOnMessages.included_usage; - - expect(check.balance).to.equal(expectedBalance); - expect(check.allowed).to.equal(true); - - // @ts-expect-error - expect(check.usage_limit!).to.equal( - messageItem.usage_limit! + addOnMessages.included_usage, - ); - // @ts-expect-error - expect(customer.features[TestFeature.Messages].usage_limit).to.equal( - messageItem.usage_limit! + addOnMessages.included_usage, - ); - }); - - it("should use up all add ons and have correct check results", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: addOnMessages.included_usage + 500, - }); - - await timeout(2000); - - const check = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - }); - const customer = await autumn.customers.get(customerId); - - const expectedBalance = - messageItem.included_usage - messageItem.usage_limit!; - expect(check.balance).to.equal(expectedBalance); - expect(check.allowed).to.equal(false); - // @ts-expect-error - expect(check.usage_limit!).to.equal( - messageItem.usage_limit! + addOnMessages.included_usage, - ); - // @ts-expect-error - expect(customer.features[TestFeature.Messages].usage_limit).to.equal( - messageItem.usage_limit! + addOnMessages.included_usage, - ); - }); -}); diff --git a/server/tests/advanced/usageLimit/usageLimit2.test.ts b/server/tests/advanced/usageLimit/usageLimit2.test.ts index ba709b8cd..4a3b91d7c 100644 --- a/server/tests/advanced/usageLimit/usageLimit2.test.ts +++ b/server/tests/advanced/usageLimit/usageLimit2.test.ts @@ -1,13 +1,10 @@ -import { - LegacyVersion, - type LimitedItem, -} from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion, type LimitedItem } from "@autumn/shared"; import chalk from "chalk"; import type Stripe from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; import { @@ -95,8 +92,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits, usage prices` value: initialUsage, }); - await timeout(2000); - const check = await autumn.check({ customer_id: customerId, feature_id: TestFeature.Messages, @@ -108,14 +103,15 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits, usage prices` expect(check.balance).toBe(expectedBalance); expect(check.allowed).toBe(false); - // @ts-expect-error + expect(check.usage_limit!).toBe(messageItem.usage_limit!); - // @ts-expect-error expect(customer.features[TestFeature.Messages].usage_limit).toBe( messageItem.usage_limit!, ); }); + return; + test("should purchase add ons and have correct check results", async () => { await autumn.attach({ customer_id: customerId, @@ -135,11 +131,10 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits, usage prices` expect(check.balance).toBe(expectedBalance); expect(check.allowed).toBe(true); - // @ts-expect-error expect(check.usage_limit!).toBe( messageItem.usage_limit! + addOnMessages.included_usage, ); - // @ts-expect-error + expect(customer.features[TestFeature.Messages].usage_limit).toBe( messageItem.usage_limit! + addOnMessages.included_usage, ); @@ -164,11 +159,11 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits, usage prices` messageItem.included_usage - messageItem.usage_limit!; expect(check.balance).toBe(expectedBalance); expect(check.allowed).toBe(false); - // @ts-expect-error + expect(check.usage_limit!).toBe( messageItem.usage_limit! + addOnMessages.included_usage, ); - // @ts-expect-error + expect(customer.features[TestFeature.Messages].usage_limit).toBe( messageItem.usage_limit! + addOnMessages.included_usage, ); diff --git a/server/tests/advanced/usageLimit/usageLimit2.ts b/server/tests/advanced/usageLimit/usageLimit2.ts deleted file mode 100644 index 3ef7327f5..000000000 --- a/server/tests/advanced/usageLimit/usageLimit2.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { - type AppEnv, - LegacyVersion, - type LimitedItem, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { - constructArrearItem, - constructFeatureItem, -} from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -const messageItem = constructArrearItem({ - featureId: TestFeature.Messages, - includedUsage: 100, - billingUnits: 1, - price: 0.5, - usageLimit: 500, -}) as LimitedItem; - -export const pro = constructProduct({ - items: [messageItem], - type: "pro", -}); - -const addOnMessages = constructFeatureItem({ - featureId: TestFeature.Messages, - interval: null, - includedUsage: 250, -}) as LimitedItem; - -const messageAddOn = constructProduct({ - type: "one_off", - items: [addOnMessages], -}); - -const testCase = "usageLimit2"; - -describe(`${chalk.yellowBright(`${testCase}: Testing usage limits, usage prices`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, messageAddOn], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro, messageAddOn], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - it("should attach pro product", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - - const initialUsage = - messageItem.included_usage + messageItem.usage_limit! + 1000; - - it("should track more messages than limit and not surpass", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: initialUsage, - }); - - await timeout(2000); - - const check = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - }); - const customer = await autumn.customers.get(customerId); - - const expectedBalance = - messageItem.included_usage - messageItem.usage_limit!; - - expect(check.balance).to.equal(expectedBalance); - expect(check.allowed).to.equal(false); - expect(check.usage_limit!).to.equal(messageItem.usage_limit!); - expect(customer.features[TestFeature.Messages].usage_limit).to.equal( - messageItem.usage_limit!, - ); - }); - - it("should purchase add ons and have correct check results", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: messageAddOn.id, - }); - - const check = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - }); - const customer = await autumn.customers.get(customerId); - const expectedBalance = - messageItem.included_usage - - messageItem.usage_limit! + - addOnMessages.included_usage; - - expect(check.balance).to.equal(expectedBalance); - expect(check.allowed).to.equal(true); - - expect(check.usage_limit!).to.equal( - messageItem.usage_limit! + addOnMessages.included_usage, - ); - - expect(customer.features[TestFeature.Messages].usage_limit).to.equal( - messageItem.usage_limit! + addOnMessages.included_usage, - ); - }); - - it("should use up all add ons and have correct check results", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: addOnMessages.included_usage + 500, - }); - - await timeout(2000); - - const check = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - }); - const customer = await autumn.customers.get(customerId); - - const expectedBalance = - messageItem.included_usage - messageItem.usage_limit!; - - expect(check.balance).to.equal(expectedBalance); - expect(check.allowed).to.equal(false); - - expect(check.usage_limit!).to.equal( - messageItem.usage_limit! + addOnMessages.included_usage, - ); - - expect(customer.features[TestFeature.Messages].usage_limit).to.equal( - messageItem.usage_limit! + addOnMessages.included_usage, - ); - }); -}); diff --git a/server/tests/advanced/usageLimit/usageLimit3.backup.ts b/server/tests/advanced/usageLimit/usageLimit3.backup.ts deleted file mode 100644 index fc48ebe43..000000000 --- a/server/tests/advanced/usageLimit/usageLimit3.backup.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { - type AppEnv, - ErrCode, - LegacyVersion, - type LimitedItem, - type Organization, -} from "@autumn/shared"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -const messageItem = constructPrepaidItem({ - featureId: TestFeature.Messages, - includedUsage: 50, - billingUnits: 100, - price: 8, - usageLimit: 500, -}) as LimitedItem; - -export const pro = constructProduct({ - items: [messageItem], - type: "pro", -}); - -// const addOnMessages = constructFeatureItem({ -// featureId: TestFeature.Messages, -// interval: null, -// includedUsage: 250, -// }) as LimitedItem; - -// const messageAddOn = constructProduct({ -// type: "one_off", -// items: [addOnMessages], -// }); - -const testCase = "usageLimit3"; - -describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for prepaid`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - it("should attach pro product with quantity exceeding usage limit and get an error", async () => { - expectAutumnError({ - errCode: ErrCode.InvalidOptions, - func: async () => { - return await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - options: [ - { - feature_id: TestFeature.Messages, - quantity: 600, - }, - ], - }); - }, - }); - }); - it("should attach pro product and update quantity with quantity exceeding usage limit and get an error", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: pro.id, - options: [ - { - feature_id: TestFeature.Messages, - quantity: 100, - }, - ], - }); - - expectAutumnError({ - errCode: ErrCode.InvalidOptions, - func: async () => { - return await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - options: [ - { - feature_id: TestFeature.Messages, - quantity: 600, - }, - ], - }); - }, - }); - }); -}); diff --git a/server/tests/advanced/usageLimit/usageLimit3.test.ts b/server/tests/advanced/usageLimit/usageLimit3.test.ts index e90b45d6b..66858627c 100644 --- a/server/tests/advanced/usageLimit/usageLimit3.test.ts +++ b/server/tests/advanced/usageLimit/usageLimit3.test.ts @@ -1,15 +1,11 @@ -import { - ErrCode, - LegacyVersion, - type LimitedItem, -} from "@autumn/shared"; import { beforeAll, describe, test } from "bun:test"; +import { ErrCode, LegacyVersion, type LimitedItem } from "@autumn/shared"; import chalk from "chalk"; import type Stripe from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; @@ -48,8 +44,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for prepaid`)} let testClockId: string; let stripeCli: Stripe; - const curUnix = new Date().getTime(); - beforeAll(async () => { stripeCli = ctx.stripeCli; @@ -73,7 +67,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for prepaid`)} test("should attach pro product with quantity exceeding usage limit and get an error", async () => { expectAutumnError({ - errCode: ErrCode.InvalidOptions, func: async () => { return await attachAndExpectCorrect({ autumn, diff --git a/server/tests/advanced/usageLimit/usageLimit3.ts b/server/tests/advanced/usageLimit/usageLimit3.ts deleted file mode 100644 index fc48ebe43..000000000 --- a/server/tests/advanced/usageLimit/usageLimit3.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { - type AppEnv, - ErrCode, - LegacyVersion, - type LimitedItem, - type Organization, -} from "@autumn/shared"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -const messageItem = constructPrepaidItem({ - featureId: TestFeature.Messages, - includedUsage: 50, - billingUnits: 100, - price: 8, - usageLimit: 500, -}) as LimitedItem; - -export const pro = constructProduct({ - items: [messageItem], - type: "pro", -}); - -// const addOnMessages = constructFeatureItem({ -// featureId: TestFeature.Messages, -// interval: null, -// includedUsage: 250, -// }) as LimitedItem; - -// const messageAddOn = constructProduct({ -// type: "one_off", -// items: [addOnMessages], -// }); - -const testCase = "usageLimit3"; - -describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for prepaid`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - it("should attach pro product with quantity exceeding usage limit and get an error", async () => { - expectAutumnError({ - errCode: ErrCode.InvalidOptions, - func: async () => { - return await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - options: [ - { - feature_id: TestFeature.Messages, - quantity: 600, - }, - ], - }); - }, - }); - }); - it("should attach pro product and update quantity with quantity exceeding usage limit and get an error", async () => { - await autumn.attach({ - customer_id: customerId, - product_id: pro.id, - options: [ - { - feature_id: TestFeature.Messages, - quantity: 100, - }, - ], - }); - - expectAutumnError({ - errCode: ErrCode.InvalidOptions, - func: async () => { - return await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - options: [ - { - feature_id: TestFeature.Messages, - quantity: 600, - }, - ], - }); - }, - }); - }); -}); diff --git a/server/tests/advanced/usageLimit/usageLimit4.backup.ts b/server/tests/advanced/usageLimit/usageLimit4.backup.ts deleted file mode 100644 index 0938b1a32..000000000 --- a/server/tests/advanced/usageLimit/usageLimit4.backup.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { - type AppEnv, - ErrCode, - LegacyVersion, - type LimitedItem, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -const messageItem = constructArrearProratedItem({ - featureId: TestFeature.Users, - includedUsage: 1, - pricePerUnit: 10, - usageLimit: 3, -}) as LimitedItem; - -export const pro = constructProduct({ - items: [messageItem], - type: "pro", -}); - -const testCase = "usageLimit4"; - -describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for cont use item`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - it("should attach pro product with quantity exceeding usage limit and get an error", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - it("should attach pro product and update quantity with quantity exceeding usage limit and get an error", async () => { - await expectAutumnError({ - errCode: ErrCode.InvalidInputs, - func: async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: messageItem.usage_limit! + 1, - }); - }, - }); - - const check = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Users, - }); - - expect(check.balance).to.equal(0); - expect(check.allowed).to.equal(true); - }); -}); diff --git a/server/tests/advanced/usageLimit/usageLimit4.test.ts b/server/tests/advanced/usageLimit/usageLimit4.test.ts index 06ec1e7f0..ffffed6f6 100644 --- a/server/tests/advanced/usageLimit/usageLimit4.test.ts +++ b/server/tests/advanced/usageLimit/usageLimit4.test.ts @@ -1,15 +1,10 @@ -import { - ErrCode, - LegacyVersion, - type LimitedItem, -} from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion, type LimitedItem } from "@autumn/shared"; import chalk from "chalk"; import type Stripe from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; @@ -33,11 +28,8 @@ const testCase = "usageLimit4"; describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for cont use item`)}`, () => { const customerId = testCase; const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; let stripeCli: Stripe; - const curUnix = new Date().getTime(); - beforeAll(async () => { stripeCli = ctx.stripeCli; @@ -48,15 +40,12 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for cont use i customerId, }); - const { testClockId: testClockId1 } = await initCustomerV3({ + await initCustomerV3({ ctx, customerId, - customerData: {}, attachPm: "success", withTestClock: true, }); - - testClockId = testClockId1!; }); test("should attach pro product with quantity exceeding usage limit and get an error", async () => { @@ -70,16 +59,12 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for cont use i env: ctx.env, }); }); - test("should attach pro product and update quantity with quantity exceeding usage limit and get an error", async () => { - await expectAutumnError({ - errCode: ErrCode.InvalidInputs, - func: async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: messageItem.usage_limit! + 1, - }); - }, + + test("should attach pro product and have usage deducted to usage limit", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: messageItem.usage_limit! + 1, }); const check = await autumn.check({ @@ -87,7 +72,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for cont use i feature_id: TestFeature.Users, }); - expect(check.balance).toBe(0); + expect(check.balance).toBe(1); expect(check.allowed).toBe(true); }); }); diff --git a/server/tests/advanced/usageLimit/usageLimit4.ts b/server/tests/advanced/usageLimit/usageLimit4.ts deleted file mode 100644 index c977967ed..000000000 --- a/server/tests/advanced/usageLimit/usageLimit4.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - type AppEnv, - LegacyVersion, - type LimitedItem, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -const messageItem = constructArrearProratedItem({ - featureId: TestFeature.Users, - includedUsage: 1, - pricePerUnit: 10, - usageLimit: 3, -}) as LimitedItem; - -export const pro = constructProduct({ - items: [messageItem], - type: "pro", -}); - -const testCase = "usageLimit4"; - -describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for cont use item`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - it("should attach pro product", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - - it("should track usage exceeding usage limit (for users) and only have usage limit deducted", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: messageItem.usage_limit! + 1, - }); - - const check = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Users, - }); - - expect(check.balance).to.equal( - messageItem.included_usage - messageItem.usage_limit!, - ); - expect(check.allowed).to.equal(false); - }); -}); diff --git a/server/tests/archives/mergedAdd2.test.ts b/server/tests/archives/mergedAdd2.test.ts new file mode 100644 index 000000000..3c48276d8 --- /dev/null +++ b/server/tests/archives/mergedAdd2.test.ts @@ -0,0 +1,162 @@ +// import { beforeAll, describe, expect, test } from "bun:test"; +// import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; +// import chalk from "chalk"; +// import type { Stripe } from "stripe"; +// import { TestFeature } from "tests/setup/v2Features.js"; +// import ctx from "tests/utils/testInitUtils/createTestContext.js"; +// import type { DrizzleCli } from "@/db/initDrizzle.js"; +// import { AutumnInt } from "@/external/autumn/autumnCli.js"; +// import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +// import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +// import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +// import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +// import { getExpectedInvoiceTotal } from "../../utils/expectUtils/expectInvoiceUtils.js"; +// import { timeout } from "../../utils/genUtils.js"; +// import { advanceToNextInvoice } from "../../utils/testAttachUtils/testAttachUtils.js"; +// import { getBasePrice } from "../../utils/testProductUtils/testProductUtils.js"; +// import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; + +// // UNCOMMENT FROM HERE +// const premium = constructProduct({ +// id: "premium", +// items: [constructArrearItem({ featureId: TestFeature.Words })], +// type: "premium", +// }); +// const pro = constructProduct({ +// id: "pro", +// items: [constructArrearItem({ featureId: TestFeature.Words })], +// type: "pro", +// }); + +// const testCase = "mergedAdd2"; +// describe(`${chalk.yellowBright(`${testCase}: Testing merged subs, downgrade`)}`, () => { +// const customerId = testCase; +// const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + +// let stripeCli: Stripe; +// let testClockId: string; +// let curUnix: number; +// let db: DrizzleCli; +// let org: Organization; +// let env: AppEnv; + +// beforeAll(async () => { +// await initProductsV0({ +// ctx, +// products: [premium, pro], +// prefix: testCase, +// customerId, +// }); + +// const res = await initCustomerV3({ +// ctx, +// customerId, +// customerData: {}, +// attachPm: "success", +// withTestClock: true, +// }); + +// stripeCli = ctx.stripeCli; +// db = ctx.db; +// org = ctx.org; +// env = ctx.env; +// testClockId = res.testClockId!; +// }); + +// const entities = [ +// { +// id: "1", +// name: "Entity 1", +// feature_id: TestFeature.Users, +// }, +// { +// id: "2", +// name: "Entity 2", +// feature_id: TestFeature.Users, +// }, +// ]; + +// test("should attach premium, product", async () => { +// await autumn.entities.create(customerId, entities); + +// await autumn.attach({ +// customer_id: customerId, +// product_id: premium.id, +// entity_id: "1", +// }); + +// await autumn.attach({ +// customer_id: customerId, +// product_id: premium.id, +// entity_id: "2", +// }); +// await autumn.attach({ +// customer_id: customerId, +// product_id: pro.id, +// entity_id: "2", +// }); + +// const customer = await autumn.customers.get(customerId); +// const invoice = customer.invoices; + +// await expectSubToBeCorrect({ +// db, +// customerId, +// org, +// env, +// }); +// }); + +// test("should track usage and have correct invoice end of month", async () => { +// const value1 = 110000; +// const value2 = 310000; +// const values = [value1, value2]; +// await autumn.track({ +// customer_id: customerId, +// feature_id: TestFeature.Words, +// value: value1, +// entity_id: "1", +// }); + +// await autumn.track({ +// customer_id: customerId, +// feature_id: TestFeature.Words, +// value: value2, +// entity_id: "2", +// }); + +// await timeout(3000); + +// await advanceToNextInvoice({ +// stripeCli, +// testClockId, +// }); + +// let total = 0; +// for (let i = 0; i < entities.length; i++) { +// const expectedTotal = await getExpectedInvoiceTotal({ +// customerId, +// productId: pro.id, +// usage: [{ featureId: TestFeature.Words, value: values[i] }], +// onlyIncludeUsage: true, +// stripeCli, +// db, +// org, +// env, +// }); +// total += expectedTotal; +// } + +// const basePrice = getBasePrice({ product: pro }); + +// const customer = await autumn.customers.get(customerId); +// const invoice = customer.invoices; +// expect(invoice[0].total).toBe(basePrice * 2 + total); +// }); +// }); + +// // const expectedTotal = await getAttachPreviewTotal({ +// // customerId, +// // productId: pro.id, +// // entityId: "2", +// // }); diff --git a/server/tests/attach/migrations/migration4.test.ts b/server/tests/attach/migrations/migration4.test.ts index 81001e47c..9232ae5a8 100644 --- a/server/tests/attach/migrations/migration4.test.ts +++ b/server/tests/attach/migrations/migration4.test.ts @@ -17,6 +17,7 @@ const wordsItem = constructArrearItem({ }); const pro = constructProduct({ + id: "pro", items: [wordsItem], type: "pro", isDefault: false, @@ -43,7 +44,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro -> pro wi beforeAll(async () => { await initProductsV0({ ctx, - products: [pro, proWithTrial], + products: [pro], prefix: testCase, customerId, }); @@ -71,7 +72,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro -> pro wi test("should update product to new version", async () => { proWithTrial.version = 2; - await autumn.products.update(pro.id, { + await autumn.products.update(proWithTrial.id, { items: proWithTrial.items, free_trial: proWithTrial.free_trial, }); diff --git a/server/tests/attach/multiProduct/multiProduct1.backup.ts b/server/tests/attach/multiProduct/multiProduct1.backup.ts deleted file mode 100644 index 71fcf4e64..000000000 --- a/server/tests/attach/multiProduct/multiProduct1.backup.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { AutumnCli } from "tests/cli/AutumnCli.js"; -import { attachProducts } from "tests/global.js"; -import { compareMainProduct } from "tests/utils/compare.js"; -import chalk from "chalk"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { setupBefore } from "tests/before.js"; -import { Customer } from "@autumn/shared"; - -/* -FLOW: -1. Attach pro group 1 & pro group 2 at once -> should have both products as main -2. Upgrade pro group 1 -> premium group 1 -3. Upgrade pro group 2 -> premium group 2 -*/ - -const testCase = "multiProduct1"; -describe( - chalk.yellowBright(`${testCase}: Testing multi product attach, and upgrade`), - () => { - let customerId = testCase; - let customer: Customer; - before(async function () { - await setupBefore(this); - const res = await initCustomer({ - customerId, - db: this.db, - org: this.org, - env: this.env, - autumn: this.autumnJs, - attachPm: "success", - }); - customer = res.customer; - }); - - it("should attach pro group 1 and pro group 2", async function () { - await AutumnCli.attach({ - customerId: customerId, - productIds: [attachProducts.proGroup1.id, attachProducts.proGroup2.id], - }); - - let cusRes = await AutumnCli.getCustomer(customerId); - compareMainProduct({ sent: attachProducts.proGroup1, cusRes }); - compareMainProduct({ sent: attachProducts.proGroup2, cusRes }); - }); - - it("should upgrade to premium group 1", async function () { - await AutumnCli.attach({ - customerId: customerId, - productId: attachProducts.premiumGroup1.id, - }); - - // 1. Compare main product - const cusRes = await AutumnCli.getCustomer(customerId); - compareMainProduct({ sent: attachProducts.premiumGroup1, cusRes }); - }); - - it("should upgrade to premium group 2", async function () { - await AutumnCli.attach({ - customerId: customerId, - productId: attachProducts.premiumGroup2.id, - }); - - // 1. Compare main product - const cusRes = await AutumnCli.getCustomer(customerId); - compareMainProduct({ sent: attachProducts.premiumGroup2, cusRes }); - }); - }, -); diff --git a/server/tests/attach/multiProduct/multiProduct1.test.ts b/server/tests/attach/multiProduct/multiProduct1.test.ts index d443b1c16..865e27c06 100644 --- a/server/tests/attach/multiProduct/multiProduct1.test.ts +++ b/server/tests/attach/multiProduct/multiProduct1.test.ts @@ -1,16 +1,13 @@ -import { AutumnCli } from "tests/cli/AutumnCli.js"; -import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; +import { beforeAll, describe, test } from "bun:test"; import chalk from "chalk"; -import { beforeAll, describe, expect, test } from "bun:test"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; -import { Customer } from "@autumn/shared"; -import { - sharedProGroup1, - sharedProGroup2, - sharedPremiumGroup1, - sharedPremiumGroup2, -} from "./sharedProducts.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; /* FLOW: @@ -20,53 +17,117 @@ FLOW: */ const testCase = "multiProduct1"; + +// Group 1 products (use Messages feature) +const proGroup1 = constructProduct({ + id: "proGroup1", + group: `${testCase}-g1`, + type: "pro", + items: [ + constructArrearItem({ + includedUsage: 10, + featureId: TestFeature.Messages, + price: 100, // $1.00 per unit (100 cents per billing unit of 1) + billingUnits: 1, + }), + ], +}); + +const premiumGroup1 = constructProduct({ + id: "premiumGroup1", + group: `${testCase}-g1`, + type: "premium", + items: [ + constructArrearItem({ + includedUsage: 100, + featureId: TestFeature.Messages, + price: 200, // $2.00 per unit (200 cents per billing unit of 1) + billingUnits: 1, + }), + ], +}); + +// Group 2 products (use Words feature) +const proGroup2 = constructProduct({ + id: "proGroup2", + group: `${testCase}-g2`, + type: "pro", + items: [ + constructArrearItem({ + includedUsage: 10, + featureId: TestFeature.Words, + price: 60, // $0.60 per unit (60 cents per billing unit of 1) + billingUnits: 1, + }), + ], +}); + +const premiumGroup2 = constructProduct({ + id: "premiumGroup2", + group: `${testCase}-g2`, + type: "premium", + items: [ + constructArrearItem({ + includedUsage: 10, + featureId: TestFeature.Words, + price: 90, // $0.90 per unit (90 cents per billing unit of 1) + billingUnits: 1, + }), + ], +}); + describe( chalk.yellowBright(`${testCase}: Testing multi product attach, and upgrade`), () => { const customerId = testCase; - let customer: Customer; beforeAll(async () => { - const res = await initCustomerV3({ + await initProductsV0({ + ctx, + products: [proGroup1, proGroup2, premiumGroup1, premiumGroup2], + prefix: testCase, + customerId, + }); + + await initCustomerV3({ ctx, customerId, customerData: {}, attachPm: "success", withTestClock: true, }); - customer = res.customer; }); test("should attach pro group 1 and pro group 2", async () => { await AutumnCli.attach({ customerId: customerId, - productIds: [sharedProGroup1.id, sharedProGroup2.id], + productIds: [proGroup1.id, proGroup2.id], }); const cusRes = await AutumnCli.getCustomer(customerId); - expectCustomerV0Correct({ sent: sharedProGroup1, cusRes, ctx }); - expectCustomerV0Correct({ sent: sharedProGroup2, cusRes, ctx }); + await expectCustomerV0Correct({ sent: proGroup1, cusRes }); + await expectCustomerV0Correct({ sent: proGroup2, cusRes }); }); test("should upgrade to premium group 1", async () => { await AutumnCli.attach({ customerId: customerId, - productId: sharedPremiumGroup1.id, + productId: premiumGroup1.id, }); // 1. Compare main product const cusRes = await AutumnCli.getCustomer(customerId); - expectCustomerV0Correct({ sent: sharedPremiumGroup1, cusRes, ctx }); + await expectCustomerV0Correct({ sent: premiumGroup1, cusRes }); }); test("should upgrade to premium group 2", async () => { await AutumnCli.attach({ customerId: customerId, - productId: sharedPremiumGroup2.id, + productId: premiumGroup2.id, }); // 1. Compare main product const cusRes = await AutumnCli.getCustomer(customerId); - expectCustomerV0Correct({ sent: sharedPremiumGroup2, cusRes, ctx }); + await expectCustomerV0Correct({ sent: premiumGroup2, cusRes }); }); }, ); diff --git a/server/tests/attach/multiProduct/multiProduct2.backup.ts b/server/tests/attach/multiProduct/multiProduct2.backup.ts deleted file mode 100644 index c30013b4a..000000000 --- a/server/tests/attach/multiProduct/multiProduct2.backup.ts +++ /dev/null @@ -1,158 +0,0 @@ -import chalk from "chalk"; - -import { Stripe } from "stripe"; -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; -import { CusProductStatus, Customer } from "@autumn/shared"; -import { expect } from "chai"; -import { AutumnCli } from "tests/cli/AutumnCli.js"; -import { attachProducts } from "tests/global.js"; -import { - checkProductIsScheduled, - compareMainProduct, -} from "tests/utils/compare.js"; - -import { searchCusProducts } from "tests/utils/genUtils.js"; -import { checkScheduleContainsProducts } from "tests/utils/scheduleCheckUtils.js"; -import { setupBefore } from "tests/before.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -/* -FLOW: -1. Attach pro group 1 & premium group 2 -2. Downgrade to starter group 1 -3. Downgrade to starter group 2 -4. Change downgrade to pro group 2 -*/ - -const testCase = "multiProduct2"; -describe(`${chalk.yellowBright( - "multiProduct2: premium1->starter1, premium2->starter2, then premium2->pro2, then premium2->free", -)}`, () => { - let customerId = testCase; - let customer: Customer; - let stripeCli: Stripe; - - before(async function () { - await setupBefore(this); - stripeCli = this.stripeCli; - const res = await initCustomer({ - db: this.db, - org: this.org, - customerId, - env: this.env, - autumn: this.autumnJs, - attachPm: "success", - }); - customer = res.customer; - }); - - it("should attach premium group 1 and premium group 2", async function () { - await AutumnCli.attach({ - customerId: customerId, - productIds: [ - attachProducts.premiumGroup1.id, - attachProducts.premiumGroup2.id, - ], - }); - - let cusRes = await AutumnCli.getCustomer(customerId); - compareMainProduct({ sent: attachProducts.premiumGroup1, cusRes }); - compareMainProduct({ sent: attachProducts.premiumGroup2, cusRes }); - }); - - it("should downgrade to starter group 1 and starter group 2", async function () { - await AutumnCli.attach({ - customerId: customerId, - productId: attachProducts.starterGroup1.id, - }); - - await AutumnCli.attach({ - customerId: customerId, - productId: attachProducts.starterGroup2.id, - }); - - // Check starter group 1 scheduled and starter group 2 scheduled - let cusRes = await AutumnCli.getCustomer(customerId); - checkProductIsScheduled({ - product: attachProducts.starterGroup1, - cusRes, - }); - checkProductIsScheduled({ - product: attachProducts.starterGroup2, - cusRes, - }); - - // Check if scheduled id is the same - const cusProducts = await CusProductService.list({ - db: this.db, - internalCustomerId: customer.internal_id, - inStatuses: [ - CusProductStatus.Active, - CusProductStatus.PastDue, - CusProductStatus.Scheduled, - ], - }); - - // 1. Pro group 1: - const starter1 = searchCusProducts({ - cusProducts, - productId: attachProducts.starterGroup1.id, - }); - - const starter2 = searchCusProducts({ - cusProducts, - productId: attachProducts.starterGroup2.id, - }); - - expect(starter1).to.exist; - expect(starter2).to.exist; - expect(starter1?.scheduled_ids![0]).to.equal(starter2?.scheduled_ids![0]); - - const stripeSchedule = await stripeCli.subscriptionSchedules.retrieve( - starter1?.scheduled_ids![0]!, - ); - - // console.log(stripeSchedule); - checkScheduleContainsProducts({ - db: this.db, - schedule: stripeSchedule, - productIds: [ - attachProducts.starterGroup1.id, - attachProducts.starterGroup2.id, - ], - org: this.org, - env: this.env, - }); - }); - - it("should downgrade to free", async function () { - await AutumnCli.attach({ - customerId: customerId, - productId: attachProducts.freeGroup2.id, - }); - - let cusRes = await AutumnCli.getCustomer(customerId); - checkProductIsScheduled({ - product: attachProducts.freeGroup2, - cusRes, - }); - - const cusProducts = await CusProductService.list({ - db: this.db, - internalCustomerId: customer.internal_id, - }); - - const starterGroup2 = searchCusProducts({ - cusProducts, - productId: attachProducts.starterGroup2.id, - }); - - checkScheduleContainsProducts({ - db: this.db, - scheduleId: starterGroup2?.scheduled_ids![0], - productIds: [attachProducts.starterGroup2.id], - org: this.org, - env: this.env, - }); - }); -}); diff --git a/server/tests/attach/multiProduct/multiProduct2.test.ts b/server/tests/attach/multiProduct/multiProduct2.test.ts deleted file mode 100644 index a916ddbf9..000000000 --- a/server/tests/attach/multiProduct/multiProduct2.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import chalk from "chalk"; - -import type { Stripe } from "stripe"; -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; -import { CusProductStatus, Customer } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; -import { AutumnCli } from "tests/cli/AutumnCli.js"; -import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; -import { checkProductIsScheduled } from "tests/utils/compare.js"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; -import { searchCusProducts } from "tests/utils/genUtils.js"; -import { checkScheduleContainsProducts } from "tests/utils/scheduleCheckUtils.js"; -import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; -import { - sharedPremiumGroup1, - sharedPremiumGroup2, - sharedStarterGroup1, - sharedStarterGroup2, - sharedFreeGroup2, -} from "./sharedProducts.js"; - -/* -FLOW: -1. Attach pro group 1 & premium group 2 -2. Downgrade to starter group 1 -3. Downgrade to starter group 2 -4. Change downgrade to pro group 2 -*/ - -const testCase = "multiProduct2"; -describe(`${chalk.yellowBright( - "multiProduct2: premium1->starter1, premium2->starter2, then premium2->pro2, then premium2->free", -)}`, () => { - const customerId = testCase; - let customer: Customer; - let stripeCli: Stripe; - - beforeAll(async () => { - stripeCli = ctx.stripeCli; - const res = await initCustomerV3({ - ctx, - customerId, - customerData: {}, - attachPm: "success", - withTestClock: true, - }); - customer = res.customer; - }); - - test("should attach premium group 1 and premium group 2", async () => { - await AutumnCli.attach({ - customerId: customerId, - productIds: [ - sharedPremiumGroup1.id, - sharedPremiumGroup2.id, - ], - }); - - const cusRes = await AutumnCli.getCustomer(customerId); - expectCustomerV0Correct({ sent: sharedPremiumGroup1, cusRes, ctx }); - expectCustomerV0Correct({ sent: sharedPremiumGroup2, cusRes, ctx }); - }); - - test("should downgrade to starter group 1 and starter group 2", async () => { - await AutumnCli.attach({ - customerId: customerId, - productId: sharedStarterGroup1.id, - }); - - await AutumnCli.attach({ - customerId: customerId, - productId: sharedStarterGroup2.id, - }); - - // Check starter group 1 scheduled and starter group 2 scheduled - const cusRes = await AutumnCli.getCustomer(customerId); - checkProductIsScheduled({ - product: sharedStarterGroup1, - cusRes, - }); - checkProductIsScheduled({ - product: sharedStarterGroup2, - cusRes, - }); - - // Check if scheduled id is the same - const cusProducts = await CusProductService.list({ - db: ctx.db, - internalCustomerId: customer.internal_id, - inStatuses: [ - CusProductStatus.Active, - CusProductStatus.PastDue, - CusProductStatus.Scheduled, - ], - }); - - // 1. Pro group 1: - const starter1 = searchCusProducts({ - cusProducts, - productId: sharedStarterGroup1.id, - }); - - const starter2 = searchCusProducts({ - cusProducts, - productId: sharedStarterGroup2.id, - }); - - expect(starter1).toBeDefined(); - expect(starter2).toBeDefined(); - expect(starter1?.scheduled_ids![0]).toBe(starter2?.scheduled_ids![0]); - - const stripeSchedule = await stripeCli.subscriptionSchedules.retrieve( - starter1?.scheduled_ids![0]!, - ); - - // console.log(stripeSchedule); - checkScheduleContainsProducts({ - db: ctx.db, - schedule: stripeSchedule, - productIds: [ - sharedStarterGroup1.id, - sharedStarterGroup2.id, - ], - org: ctx.org, - env: ctx.env, - }); - }); - - test("should downgrade to free", async () => { - await AutumnCli.attach({ - customerId: customerId, - productId: sharedFreeGroup2.id, - }); - - const cusRes = await AutumnCli.getCustomer(customerId); - checkProductIsScheduled({ - product: sharedFreeGroup2, - cusRes, - }); - - const cusProducts = await CusProductService.list({ - db: ctx.db, - internalCustomerId: customer.internal_id, - }); - - const starterGroup2 = searchCusProducts({ - cusProducts, - productId: sharedStarterGroup2.id, - }); - - checkScheduleContainsProducts({ - db: ctx.db, - scheduleId: starterGroup2?.scheduled_ids![0], - productIds: [sharedStarterGroup2.id], - org: ctx.org, - env: ctx.env, - }); - }); -}); diff --git a/server/tests/attach/multiProduct/multiProduct3.backup.ts b/server/tests/attach/multiProduct/multiProduct3.backup.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/server/tests/attach/others/others5.backup.ts b/server/tests/attach/others/others5.backup.ts deleted file mode 100644 index d403b067f..000000000 --- a/server/tests/attach/others/others5.backup.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { expect } from "chai"; -import chalk from "chalk"; -import { setupBefore } from "tests/before.js"; -import { AutumnCli } from "tests/cli/AutumnCli.js"; -import { features, products } from "tests/global.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { timeout } from "../../utils/genUtils.js"; - -const checkEntitledOnProduct = async ({ - customerId, - product, - totalAllowance, - finish = false, - usageBased = false, - timeoutMs = 8000, -}: { - customerId: string; - product: any; - totalAllowance?: number; - finish?: boolean; - usageBased?: boolean; - timeoutMs?: number; -}) => { - // 1. Send events - const allowance = totalAllowance || product.entitlements.metered1.allowance; - // const randomNum = Math.floor(Math.random() * (allowance - 1)); - const randomNum = 3; - - const batchUpdates = []; - for (let i = 0; i < randomNum; i++) { - batchUpdates.push( - AutumnCli.sendEvent({ - customerId: customerId, - eventName: features.metered1.eventName, - }), - ); - } - - await Promise.all(batchUpdates); - await timeout(timeoutMs); - let used = randomNum; - - // 2. Check entitled - const { allowed, balanceObj }: any = await AutumnCli.entitled( - customerId, - features.metered1.id, - true, - ); - - try { - expect(allowed).to.be.true; - expect(balanceObj!.balance).to.equal(allowance - randomNum); - - if (!finish) { - return used; - } - } catch (error) { - console.group(); - console.group(); - console.log("Allowance: ", allowance, "Random num: ", randomNum); - console.log("Expected balance to be: ", allowance - randomNum); - console.log("Entitled res: ", { allowed, balanceObj }); - console.groupEnd(); - console.groupEnd(); - throw error; - } - - // Finish up - const batchUpdates2 = []; - for (let i = 0; i < allowance - randomNum; i++) { - batchUpdates2.push( - AutumnCli.sendEvent({ - customerId: customerId, - eventName: features.metered1.eventName, - }), - ); - } - await Promise.all(batchUpdates2); - await timeout(timeoutMs); - used += allowance - randomNum; - - // 3. Check entitled again - const { allowed: allowed2, balanceObj: balanceObj2 }: any = - await AutumnCli.entitled(customerId, features.metered1.id, true); - try { - if (usageBased) { - expect(allowed2).to.be.true; - } else { - expect(allowed2).to.be.false; - } - expect(balanceObj2!.balance).to.equal(0); - return used; - } catch (error) { - console.group(); - console.group(); - console.log("Expected balance to be: ", 0); - console.log("Entitled res: ", { allowed2, balanceObj2 }); - console.groupEnd(); - console.groupEnd(); - throw error; - } -}; - -// TODO: Add test case for unlimited feature - -const testCase = "others5"; -describe(`${chalk.yellowBright( - "others5: Testing /events and /entitled, for pro, one time top up", -)}`, () => { - const customerId = testCase; - - let curAllowance = 0; - const oneTimeBillingUnits = - products.oneTimeAddOnMetered1.prices[0].config.billing_units!; - const oneTimeQuantity = 2 * oneTimeBillingUnits; - - before(async function () { - await setupBefore(this); - await initCustomer({ - customerId, - db: this.db, - org: this.org, - env: this.env, - autumn: this.autumnJs, - attachPm: "success", - }); - }); - - // it("should have correct entitlements (free)", async function () { - // await checkEntitledOnProduct({ - // customerId: customerId, - // product: products.free, - // finish: true, - // }); - // }); - - it("should attach pro", async () => { - await AutumnCli.attach({ - customerId: customerId, - productId: products.pro.id, - }); - }); - - it("should have correct entitlements (pro)", async () => { - const used = await checkEntitledOnProduct({ - customerId: customerId, - product: products.pro, - finish: false, - }); - - curAllowance = products.pro.entitlements.metered1.allowance! - used; - }); - - it("should attach one time top up", async () => { - await AutumnCli.attach({ - customerId: customerId, - productId: products.oneTimeAddOnMetered1.id, - options: [ - { - feature_id: features.metered1.id, - quantity: oneTimeQuantity, - }, - ], - }); - }); - - it("should have correct entitlements (one time top up)", async () => { - // const oneTimeAmt = oneTimeBillingUnits * oneTimeQuantity; - - await checkEntitledOnProduct({ - customerId: customerId, - product: products.oneTimeAddOnMetered1, - finish: true, - totalAllowance: curAllowance + oneTimeQuantity, - timeoutMs: 15000, - }); - }); -}); - -describe(`${chalk.yellowBright( - "others5: Testing /entitled & /events, for pro with overage", -)}`, () => { - const customerId = testCase; - - before(async function () { - await setupBefore(this); - await initCustomer({ - customerId, - db: this.db, - org: this.org, - env: this.env, - autumn: this.autumnJs, - attachPm: "success", - }); - }); - - // PRO WITH OVERAGE - it("should attach pro (with overage)", async () => { - await AutumnCli.attach({ - customerId: customerId, - productId: products.proWithOverage.id, - }); - }); - - it("should have correct entitlements (pro with overage)", async () => { - await checkEntitledOnProduct({ - customerId: customerId, - product: products.proWithOverage, - finish: true, - totalAllowance: products.proWithOverage.entitlements.metered1.allowance!, - usageBased: true, - }); - }); - - it("should have correct usage-based balance (balance < 0)", async () => { - const { allowed, balanceObj }: any = await AutumnCli.entitled( - customerId, - features.metered1.id, - true, - ); - - expect(allowed).to.be.true; - expect(balanceObj!.balance).to.equal(0); - - // Sent 5 events - const batchUpdates = []; - for (let i = 0; i < 5; i++) { - batchUpdates.push( - AutumnCli.sendEvent({ - customerId: customerId, - eventName: features.metered1.eventName, - }), - ); - } - - await Promise.all(batchUpdates); - await timeout(10000); - - const { allowed: allowed2, balanceObj: balanceObj2 }: any = - await AutumnCli.entitled(customerId, features.metered1.id, true); - - expect(allowed2).to.be.true; - expect(balanceObj2!.balance).to.equal(-5); - expect(balanceObj2!.usage_allowed).to.be.true; - }); -}); diff --git a/server/tests/attach/others/others5.test.ts b/server/tests/attach/others/others5.test.ts deleted file mode 100644 index ed80130b0..000000000 --- a/server/tests/attach/others/others5.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { beforeAll, describe, expect, test } from "bun:test"; -import type { ProductV2 } from "@autumn/shared"; -import { ProductItemInterval } from "@autumn/shared"; -import chalk from "chalk"; -import { AutumnCli } from "tests/cli/AutumnCli.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; -import { - constructFeatureItem, - constructPrepaidItem, -} from "@/utils/scriptUtils/constructItem.js"; -import { - constructProduct, - constructRawProduct, -} from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; -import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { timeout } from "../../utils/genUtils.js"; - -const checkEntitledOnProduct = async ({ - customerId, - product, - totalAllowance, - finish = false, - usageBased = false, - timeoutMs = 8000, -}: { - customerId: string; - product: ProductV2; - totalAllowance?: number; - finish?: boolean; - usageBased?: boolean; - timeoutMs?: number; -}) => { - // Get allowance from ProductV2 - find the feature item for Messages - const messagesItem = product.items.find( - (item) => item.feature_id === TestFeature.Messages, - ); - const allowance = - totalAllowance || - (messagesItem?.included_usage && - typeof messagesItem.included_usage === "number" - ? messagesItem.included_usage - : 0); - - // const randomNum = Math.floor(Math.random() * (allowance - 1)); - const randomNum = 3; - - const batchUpdates = []; - for (let i = 0; i < randomNum; i++) { - batchUpdates.push( - AutumnCli.sendEvent({ - customerId: customerId, - featureId: TestFeature.Messages, - }), - ); - } - - await Promise.all(batchUpdates); - await timeout(timeoutMs); - let used = randomNum; - - // 2. Check entitled - const { allowed, balanceObj }: any = await AutumnCli.entitled( - customerId, - TestFeature.Messages, - true, - ); - - expect(allowed).toBe(true); - expect( - balanceObj!.balance, - `balance for messages should be ${allowance - randomNum}, but got ${balanceObj!.balance}`, - ).toBe(allowance - randomNum); - - if (!finish) return used; - - // Finish up - const batchUpdates2 = []; - for (let i = 0; i < allowance - randomNum; i++) { - batchUpdates2.push( - AutumnCli.sendEvent({ - customerId: customerId, - featureId: TestFeature.Messages, - }), - ); - } - await Promise.all(batchUpdates2); - await timeout(timeoutMs); - used += allowance - randomNum; - - // 3. Check entitled again - const { allowed: allowed2, balanceObj: balanceObj2 }: any = - await AutumnCli.entitled(customerId, TestFeature.Messages, true); - try { - if (usageBased) { - expect(allowed2).toBe(true); - } else { - expect(allowed2).toBe(false); - } - expect(balanceObj2!.balance).toBe(0); - return used; - } catch (error) { - console.group(); - console.group(); - console.log("Expected balance to be: ", 0); - console.log("Entitled res: ", { allowed2, balanceObj2 }); - console.groupEnd(); - console.groupEnd(); - throw error; - } -}; - -// TODO: Add test case for unlimited feature - -const testCase = "others5"; - -// Pro product - matches global products.pro -const pro = constructProduct({ - type: "pro", - items: [ - constructFeatureItem({ - featureId: TestFeature.Dashboard, - isBoolean: true, - }), - constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 10, - interval: ProductItemInterval.Month, - }), - constructFeatureItem({ - featureId: TestFeature.Admin, - unlimited: true, - }), - ], -}); - -// One-time add-on product - matches global products.oneTimeAddOnMetered1 -const oneTimeAddOnMetered1 = constructRawProduct({ - id: "one-time-add-on-metered-1", - isAddOn: true, - items: [ - constructPrepaidItem({ - featureId: TestFeature.Messages, - isOneOff: true, - billingUnits: 100, - includedUsage: 0, - }), - ], -}); - -describe.skip(`${chalk.yellowBright( - "others5: Testing /events and /entitled, for pro, one time top up", -)}`, () => { - const customerId = testCase; - - let curAllowance = 0; - const oneTimeBillingUnits = 100; // From oneTimeAddOnMetered1 prepaid item - const oneTimeQuantity = 2 * oneTimeBillingUnits; - - beforeAll(async () => { - await initProductsV0({ - ctx, - products: [pro, oneTimeAddOnMetered1], - prefix: testCase, - customerId, - }); - - await initCustomerV3({ - ctx, - customerId, - customerData: {}, - attachPm: "success", - withTestClock: true, - }); - }); - - // test("should have correct entitlements (free)", async function () { - // await checkEntitledOnProduct({ - // customerId: customerId, - // product: free, - // finish: true, - // }); - // }); - - test("should attach pro", async () => { - await AutumnCli.attach({ - customerId: customerId, - productId: pro.id, - }); - }); - - test("should have correct entitlements (pro)", async () => { - const used = await checkEntitledOnProduct({ - customerId: customerId, - product: pro, - finish: false, - }); - - const messagesItem = pro.items.find( - (item) => item.feature_id === TestFeature.Messages, - ); - const proAllowance = - messagesItem?.included_usage && - typeof messagesItem.included_usage === "number" - ? messagesItem.included_usage - : 0; - curAllowance = proAllowance - used; - }); - - test("should attach one time top up", async () => { - await AutumnCli.attach({ - customerId: customerId, - productId: oneTimeAddOnMetered1.id, - options: [ - { - feature_id: TestFeature.Messages, - quantity: oneTimeQuantity, - }, - ], - }); - }); - - test("should have correct entitlements (one time top up)", async () => { - // const oneTimeAmt = oneTimeBillingUnits * oneTimeQuantity; - - await checkEntitledOnProduct({ - customerId: customerId, - product: oneTimeAddOnMetered1, - finish: true, - totalAllowance: curAllowance + oneTimeQuantity, - timeoutMs: 15000, - }); - }); -}); diff --git a/server/tests/attach/updateEnts/expectUpdateEnts.backup.ts b/server/tests/attach/updateEnts/expectUpdateEnts.backup.ts deleted file mode 100644 index d5fb5f2ef..000000000 --- a/server/tests/attach/updateEnts/expectUpdateEnts.backup.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - type AppEnv, - AttachBranch, - type Organization, - type ProductItem, - type ProductV2, -} from "@autumn/shared"; -import { expect } from "chai"; -import type Stripe from "stripe"; -import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js"; -import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js"; -import { - expectSubItemsCorrect, - getSubsFromCusId, -} from "tests/utils/expectUtils/expectSubUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import type { AutumnInt } from "@/external/autumn/autumnCli.js"; - -const runUpdateEntsTest = async ({ - autumn, - stripeCli, - customerId, - customProduct, - newVersion, - db, - org, - env, - customItems, - usage, -}: { - autumn: AutumnInt; - stripeCli: Stripe; - customerId: string; - customProduct: ProductV2; - newVersion?: number; - db: DrizzleCli; - org: Organization; - env: AppEnv; - customItems?: ProductItem[]; - usage?: { - featureId: string; - value: number; - }[]; -}) => { - // 1. Get subs before - - const { subs: subsBefore } = await getSubsFromCusId({ - stripeCli, - customerId, - productId: customProduct.id, - db, - org, - env, - }); - - const preview = await autumn.attachPreview({ - customer_id: customerId, - product_id: customProduct.id, - version: newVersion, - is_custom: customItems ? true : undefined, - items: customItems, - }); - - if (newVersion) { - expect(preview.branch).to.equal(AttachBranch.NewVersion); - } else { - expect(preview.branch).to.equal(AttachBranch.SameCustomEnts); - expect(preview.due_today).to.be.undefined; - } - - await autumn.attach({ - customer_id: customerId, - product_id: customProduct.id, - version: newVersion, - is_custom: customItems ? true : undefined, - items: customItems, - }); - - // 1. Ensure no new invoices created - const { subs: subsAfter, cusProduct } = await getSubsFromCusId({ - stripeCli, - customerId, - productId: customProduct.id, - db, - org, - env, - }); - - const invoicesBefore = subsBefore.map((sub) => sub.latest_invoice); - const invoicesAfter = subsAfter.map((sub) => sub.latest_invoice); - const subIdsBefore = subsBefore.map((sub) => sub.id); - const subIdsAfter = subsAfter.map((sub) => sub.id); - - // let periodEndsBefore = subsBefore.map((sub) => sub.current_period_end); - // let periodEndsAfter = subsAfter.map((sub) => sub.current_period_end); - - expect(invoicesAfter).to.deep.equal(invoicesBefore); - expect(subIdsAfter).to.deep.equal(subIdsBefore); - // expect(periodEndsAfter).to.deep.equal(periodEndsBefore); - - if (customItems) { - expect(cusProduct.is_custom).to.be.true; - } - - const customer = await autumn.customers.get(customerId); - expectFeaturesCorrect({ - customer, - product: customProduct, - usage, - }); - - // 2. Expect product attached - await expectSubItemsCorrect({ - stripeCli, - customerId, - product: customProduct, - db, - org, - env, - }); - - await expectSubToBeCorrect({ - customerId, - db, - org, - env, - }); -}; - -export default runUpdateEntsTest; diff --git a/server/tests/attach/updateEnts/expectUpdateEnts.ts b/server/tests/attach/updateEnts/expectUpdateEnts.ts deleted file mode 100644 index 2eda2d30b..000000000 --- a/server/tests/attach/updateEnts/expectUpdateEnts.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - type AppEnv, - AttachBranch, - type Organization, - type ProductItem, - type ProductV2, -} from "@autumn/shared"; -import { expect } from "bun:test"; -import type Stripe from "stripe"; -import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js"; -import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js"; -import { - expectSubItemsCorrect, - getSubsFromCusId, -} from "tests/utils/expectUtils/expectSubUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import type { AutumnInt } from "@/external/autumn/autumnCli.js"; - -const runUpdateEntsTest = async ({ - autumn, - stripeCli, - customerId, - customProduct, - newVersion, - db, - org, - env, - customItems, - usage, -}: { - autumn: AutumnInt; - stripeCli: Stripe; - customerId: string; - customProduct: ProductV2; - newVersion?: number; - db: DrizzleCli; - org: Organization; - env: AppEnv; - customItems?: ProductItem[]; - usage?: { - featureId: string; - value: number; - }[]; -}) => { - // 1. Get subs before - - const { subs: subsBefore } = await getSubsFromCusId({ - stripeCli, - customerId, - productId: customProduct.id, - db, - org, - env, - }); - - const preview = await autumn.attachPreview({ - customer_id: customerId, - product_id: customProduct.id, - version: newVersion, - is_custom: customItems ? true : undefined, - items: customItems, - }); - - if (newVersion) { - expect(preview.branch).toBe(AttachBranch.NewVersion); - } else { - expect(preview.branch).toBe(AttachBranch.SameCustomEnts); - expect(preview.due_today).toBeUndefined(); - } - - await autumn.attach({ - customer_id: customerId, - product_id: customProduct.id, - version: newVersion, - is_custom: customItems ? true : undefined, - items: customItems, - }); - - // 1. Ensure no new invoices created - const { subs: subsAfter, cusProduct } = await getSubsFromCusId({ - stripeCli, - customerId, - productId: customProduct.id, - db, - org, - env, - }); - - const invoicesBefore = subsBefore.map((sub) => sub.latest_invoice); - const invoicesAfter = subsAfter.map((sub) => sub.latest_invoice); - const subIdsBefore = subsBefore.map((sub) => sub.id); - const subIdsAfter = subsAfter.map((sub) => sub.id); - - // let periodEndsBefore = subsBefore.map((sub) => sub.current_period_end); - // let periodEndsAfter = subsAfter.map((sub) => sub.current_period_end); - - expect(invoicesAfter).toEqual(invoicesBefore); - expect(subIdsAfter).toEqual(subIdsBefore); - // expect(periodEndsAfter).toEqual(periodEndsBefore); - - if (customItems) { - expect(cusProduct.is_custom).toBe(true); - } - - const customer = await autumn.customers.get(customerId); - expectFeaturesCorrect({ - customer, - product: customProduct, - usage, - }); - - // 2. Expect product attached - await expectSubItemsCorrect({ - stripeCli, - customerId, - product: customProduct, - db, - org, - env, - }); - - await expectSubToBeCorrect({ - customerId, - db, - org, - env, - }); -}; - -export default runUpdateEntsTest; diff --git a/server/tests/attach/updateEnts/updateEnts1.backup.ts b/server/tests/attach/updateEnts/updateEnts1.backup.ts deleted file mode 100644 index c4d037394..000000000 --- a/server/tests/attach/updateEnts/updateEnts1.backup.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import { addHours, addMonths } from "date-fns"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { addPrefixToProducts, replaceItems } from "../utils.js"; -import runUpdateEntsTest from "./expectUpdateEnts.js"; - -const testCase = "updateEnts1"; - -export const pro = constructProduct({ - items: [ - constructArrearItem({ - featureId: TestFeature.Words, - includedUsage: 10000, - }), - ], - type: "pro", -}); - -describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing included usage)`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - const numUsers = 0; - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - db, - orgId: org.id, - env, - autumn, - products: [pro], - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - it("should attach pro product", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - - const newItem = constructArrearItem({ - featureId: TestFeature.Words, - includedUsage: 20000, - }); - - const customItems = replaceItems({ - items: pro.items, - featureId: TestFeature.Words, - newItem, - }); - - const usage = 50000; - const overage = 50000 - (newItem.included_usage as number); - - it("should update overage item to have new included usage", async () => { - const customProduct = { - ...pro, - items: customItems, - }; - - await autumn.track({ - customer_id: customerId, - value: usage, - feature_id: TestFeature.Words, - }); - - await timeout(5000); - - await runUpdateEntsTest({ - autumn, - stripeCli, - customerId, - customProduct, - db, - org, - env, - customItems, - usage: [ - { - featureId: TestFeature.Words, - value: usage, - }, - ], - }); - }); - return; - - it("should have correct invoice next cycle", async () => { - const invoiceTotal = await getExpectedInvoiceTotal({ - org, - env, - customerId, - productId: pro.id, - stripeCli, - db, - usage: [ - { - featureId: TestFeature.Words, - value: usage, - }, - ], - }); - - let curUnix = Date.now(); - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addMonths(curUnix, 1).getTime(), - }); - - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(), - waitForSeconds: 10, - }); - - const customer = await autumn.customers.get(customerId); - const invoice = customer.invoices![0]; - expect(invoice.total).to.equal( - invoiceTotal, - "invoice total after 1 cycle should be correct", - ); - }); -}); diff --git a/server/tests/attach/updateEnts/updateEnts1.test.ts b/server/tests/attach/updateEnts/updateEnts1.test.ts index 484125ea5..6e9630abb 100644 --- a/server/tests/attach/updateEnts/updateEnts1.test.ts +++ b/server/tests/attach/updateEnts/updateEnts1.test.ts @@ -1,14 +1,13 @@ -import { LegacyVersion } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion } from "@autumn/shared"; import chalk from "chalk"; import { addHours, addMonths } from "date-fns"; -import type Stripe from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; @@ -114,7 +113,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing inclu ], }); }); - return; test("should have correct invoice next cycle", async () => { const invoiceTotal = await getExpectedInvoiceTotal({ diff --git a/server/tests/attach/updateEnts/updateEnts2.backup.ts b/server/tests/attach/updateEnts/updateEnts2.backup.ts deleted file mode 100644 index daca61d5c..000000000 --- a/server/tests/attach/updateEnts/updateEnts2.backup.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import { addHours, addMonths, addWeeks } from "date-fns"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { addPrefixToProducts, replaceItems } from "../utils.js"; -import runUpdateEntsTest from "./expectUpdateEnts.js"; - -const testCase = "updateEnts2"; - -export const pro = constructProduct({ - items: [ - constructArrearItem({ - featureId: TestFeature.Words, - includedUsage: 10000, - }), - ], - type: "pro", - isAnnual: true, -}); - -/** - * updateEnts2: - * Testing updating entitlements for annual plans - * 1. Start with pro annual plan (usage-based) - * 2. Update included usage amount - * 3. Verify features and usage are updated correctly - * 4. Verify invoice total is correct in next billing cycle - * - * Verifies that updating entitlements works correctly for annual plans - * and that usage/billing is calculated properly - */ - -describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing included usage) for annual plan`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - const numUsers = 0; - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - db, - orgId: org.id, - env, - }); - - testClockId = testClockId1!; - }); - - it("should attach pro annual product", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - - const newItem = constructArrearItem({ - featureId: TestFeature.Words, - includedUsage: 5000, - }); - - const customItems = replaceItems({ - items: pro.items, - featureId: TestFeature.Words, - newItem, - }); - - const usage = 1200500; - - it("should attach custom pro product", async () => { - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addWeeks(curUnix, 2).getTime(), - waitForSeconds: 30, - }); - - const customProduct = { - ...pro, - items: customItems, - }; - - await autumn.track({ - customer_id: customerId, - value: usage, - feature_id: TestFeature.Words, - }); - - await timeout(5000); - - await runUpdateEntsTest({ - autumn, - stripeCli, - customerId, - customProduct, - db, - org, - env, - customItems, - usage: [ - { - featureId: TestFeature.Words, - value: usage, - }, - ], - }); - }); - - it("should have correct invoice usage next cycle", async () => { - const invoiceTotal = await getExpectedInvoiceTotal({ - org, - env, - customerId, - productId: pro.id, - stripeCli, - db, - usage: [ - { - featureId: TestFeature.Words, - value: usage, - }, - ], - onlyIncludeMonthly: true, - }); - - let curUnix = Date.now(); - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addMonths(curUnix, 1).getTime(), - }); - - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(), - waitForSeconds: 10, - }); - - const customer = await autumn.customers.get(customerId); - const invoice = customer.invoices![0]; - expect(invoice.total).to.equal( - invoiceTotal, - "invoice total after 1 cycle should be correct", - ); - }); -}); diff --git a/server/tests/attach/updateEnts/updateEnts3.backup.ts b/server/tests/attach/updateEnts/updateEnts3.backup.ts deleted file mode 100644 index b152a7a47..000000000 --- a/server/tests/attach/updateEnts/updateEnts3.backup.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; -import chalk from "chalk"; -import { addWeeks } from "date-fns"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructFeatureItem } from "@/internal/products/product-items/productItemUtils.js"; -import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { addPrefixToProducts, replaceItems } from "../utils.js"; -import runUpdateEntsTest from "./expectUpdateEnts.js"; - -const testCase = "updateEnts3"; - -export const pro = constructProduct({ - items: [ - constructArrearItem({ - featureId: TestFeature.Words, - includedUsage: 10000, - }), - ], - type: "pro", - isAnnual: true, -}); - -/** - * updateEnts2: - * Testing updating entitlements for annual plans - * 1. Start with pro annual plan (usage-based) - * 2. Update included usage amount - * 3. Verify features and usage are updated correctly - * 4. Verify invoice total is correct in next billing cycle - * - * Verifies that updating entitlements works correctly for annual plans - * and that usage/billing is calculated properly - */ - -describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing feature items)`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - db, - orgId: org.id, - env, - autumn, - products: [pro], - }); - - testClockId = testClockId1!; - }); - - it("should attach pro annual product", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - - const newFeatureItem = constructFeatureItem({ - feature_id: TestFeature.Messages, - included_usage: 500, - }); - - const usage = 1200500; - - const customItems = [...pro.items, newFeatureItem]; - - it("should attach custom pro product with new feature item", async () => { - const customProduct = { - ...pro, - items: customItems, - }; - - await autumn.track({ - customer_id: customerId, - value: usage, - feature_id: TestFeature.Words, - }); - - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addWeeks(curUnix, 2).getTime(), - waitForSeconds: 10, - }); - - await runUpdateEntsTest({ - autumn, - stripeCli, - customerId, - customProduct, - db, - org, - env, - customItems, - usage: [ - { - featureId: TestFeature.Words, - value: usage, - }, - ], - }); - }); - - it("should attach custom pro product with updated feature item", async () => { - const customItems2 = replaceItems({ - items: customItems, - featureId: TestFeature.Messages, - newItem: constructFeatureItem({ - feature_id: TestFeature.Messages, - included_usage: 1000, - }), - }); - - const customProduct = { - ...pro, - items: customItems2, - }; - - await runUpdateEntsTest({ - autumn, - stripeCli, - customerId, - customProduct, - db, - org, - env, - customItems: customItems2, - usage: [ - { - featureId: TestFeature.Words, - value: usage, - }, - ], - }); - }); - - it("should attach custom pro product with removed feature item", async () => { - const customItems2 = customItems.filter( - (item) => item.feature_id != TestFeature.Messages, - ); - - const customProduct = { - ...pro, - items: customItems2, - }; - - await runUpdateEntsTest({ - autumn, - stripeCli, - customerId, - customProduct, - db, - org, - env, - customItems: customItems2, - usage: [ - { - featureId: TestFeature.Words, - value: usage, - }, - ], - }); - }); -}); diff --git a/server/tests/attach/updateEnts/updateEnts4.backup.ts b/server/tests/attach/updateEnts/updateEnts4.backup.ts deleted file mode 100644 index 8f77e158c..000000000 --- a/server/tests/attach/updateEnts/updateEnts4.backup.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { - type AppEnv, - AttachBranch, - BillingInterval, - LegacyVersion, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; -import { nullish } from "@/utils/genUtils.js"; -import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { addPrefixToProducts } from "../utils.js"; - -const testCase = "updateEnts4"; - -export const pro = constructProduct({ - items: [ - constructArrearItem({ - featureId: TestFeature.Words, - includedUsage: 10000, - }), - ], - type: "pro", - isAnnual: true, -}); - -describe(`${chalk.yellowBright(`${testCase}: Checking price changes don't result in update ents func`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - db, - orgId: org.id, - env, - }); - - testClockId = testClockId1!; - }); - - it("should attach pro annual product", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - - it("branch should not be same custom ents if base price updated", async () => { - let customItems = pro.items.filter((item) => !nullish(item.feature_id)); - - customItems = [ - ...customItems, - constructPriceItem({ - price: 10, - interval: BillingInterval.Year, - }), - ]; - - const preview = await autumn.attachPreview({ - customer_id: customerId, - product_id: pro.id, - is_custom: true, - items: customItems, - }); - - expect(preview.branch).to.equal(AttachBranch.SameCustom); - }); -}); diff --git a/server/tests/attach/updateEnts/updateEnts5.ts b/server/tests/attach/updateEnts/updateEnts5.ts deleted file mode 100644 index 79a27aea0..000000000 --- a/server/tests/attach/updateEnts/updateEnts5.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { - type AppEnv, - BillingInterval, - LegacyVersion, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; -import { nullish } from "@/utils/genUtils.js"; -import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { addPrefixToProducts } from "../utils.js"; - -const testCase = "updateEnts5"; - -export const pro = constructRawProduct({ - id: "pro", // Test price is 5/month - items: [ - constructFeatureItem({ - featureId: TestFeature.Words, - includedUsage: 100, - }), - constructPriceItem({ - price: 5, - interval: BillingInterval.Month, - }), - ], -}); - -/** - * updateEnts5: - * Testing update entitlements with base price change and payment method updates across multiple entities - * 1. Create 3 entities and attach pro product (5/month base price) to each - * 2. Attach failed payment method to customer - * 3. Try to update each entity to more expensive base price (10/month) - * 4. Should fail with payment error, not duplicate price error (tests undoSubUpdate rollback) - */ - -describe(`${chalk.yellowBright(`${testCase}: Testing update ents with price change and payment method updates`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - db, - orgId: org.id, - env, - }); - - testClockId = testClockId1!; - }); - - it("should create entities and attach pro product to each", async () => { - await autumn.entities.create(customerId, entities); - - for (const entity of entities) { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - entityId: entity.id, - }); - } - }); - - it("should attach failed payment method and try to upgrade each entity", async () => { - const autumnCus = await CusService.get({ - db, - idOrInternalId: customerId, - orgId: org.id, - env, - }); - - // Attach failed payment method - await attachFailedPaymentMethod({ - stripeCli, - customer: autumnCus!, - }); - - // Create custom items with higher price - let customItems = pro.items.filter((item) => !nullish(item.feature_id)); - customItems = [ - ...customItems, - constructPriceItem({ - price: 10, - interval: BillingInterval.Month, - }), - ]; - - // Try to upgrade each entity - should fail with payment error - for (const entity of entities) { - try { - await autumn.attach({ - customer_id: customerId, - product_id: pro.id, - is_custom: true, - items: customItems, - entity_id: entity.id, - }); - - // If we reach here, the test should fail - throw new Error("Expected upgrade to fail with payment error"); - } catch (error: any) { - // Expect payment failure error, not duplicate price error - expect(error.message).to.include("card"); - expect(error.message).to.not.include("duplicate"); - expect(error.message).to.not.include( - "can't be added to this Subscription", - ); - } - } - }); -}); diff --git a/server/tests/attach/updateQuantity/updateQuantity1.backup.ts b/server/tests/attach/updateQuantity/updateQuantity1.backup.ts deleted file mode 100644 index 8d769f826..000000000 --- a/server/tests/attach/updateQuantity/updateQuantity1.backup.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { - type AppEnv, - AttachErrCode, - LegacyVersion, - type Organization, -} from "@autumn/shared"; -import chalk from "chalk"; -import { addWeeks } from "date-fns"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { addPrefixToProducts } from "../utils.js"; - -const testCase = "updateQuantity1"; - -export const pro = constructProduct({ - items: [ - constructPrepaidItem({ - featureId: TestFeature.Users, - price: 12, - billingUnits: 1, - }), - ], - type: "pro", -}); - -describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid single use`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - let curUnix = new Date().getTime(); - const numUsers = 0; - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - db, - orgId: org.id, - env, - }); - - testClockId = testClockId1!; - }); - - const proOpts = [ - { - feature_id: TestFeature.Users, - quantity: 2, - }, - ]; - - it("should attach pro product (arrear prorated)", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - options: proOpts, - }); - }); - - it("should throw error if try to attach same options", async () => { - await expectAutumnError({ - errCode: AttachErrCode.ProductAlreadyAttached, - func: async () => { - await autumn.attach({ - customer_id: customerId, - product_id: pro.id, - options: proOpts, - }); - }, - }); - }); - - const updatedOpts = [ - { - feature_id: TestFeature.Users, - quantity: 4, - }, - ]; - - it("should update quantity to 4 users and have usage stay the same", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: 2, - }); - await timeout(3000); - - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addWeeks(curUnix, 1).getTime(), - waitForSeconds: 30, - }); - - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - options: updatedOpts, - usage: [ - { - featureId: TestFeature.Users, - value: 2, - }, - ], - waitForInvoice: 15000, - }); - }); -}); diff --git a/server/tests/attach/updateQuantity/updateQuantity1.test.ts b/server/tests/attach/updateQuantity/updateQuantity1.test.ts index 6e074661e..776166a49 100644 --- a/server/tests/attach/updateQuantity/updateQuantity1.test.ts +++ b/server/tests/attach/updateQuantity/updateQuantity1.test.ts @@ -1,10 +1,10 @@ +import { beforeAll, describe, test } from "bun:test"; import { type AppEnv, AttachErrCode, LegacyVersion, type Organization, } from "@autumn/shared"; -import { beforeAll, describe, test } from "bun:test"; import chalk from "chalk"; import { addWeeks } from "date-fns"; import type Stripe from "stripe"; @@ -13,8 +13,8 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js" import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; import { createProducts } from "tests/utils/productUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; diff --git a/server/tests/attach/upgrade/upgrade1.test.ts b/server/tests/attach/upgrade/upgrade1.test.ts index af24ae359..139e7aff4 100644 --- a/server/tests/attach/upgrade/upgrade1.test.ts +++ b/server/tests/attach/upgrade/upgrade1.test.ts @@ -92,7 +92,6 @@ describe(`${chalk.yellowBright("upgrade1: Testing usage upgrades")}`, () => { waitForSeconds: 10, }); - return; await attachAndExpectCorrect({ autumn, customerId, @@ -104,7 +103,6 @@ describe(`${chalk.yellowBright("upgrade1: Testing usage upgrades")}`, () => { }); }); - return; test("should attach growth product", async () => { const wordsUsage = 200000; await autumn.track({ diff --git a/server/tests/attach/upgrade/upgrade2.test.ts b/server/tests/attach/upgrade/upgrade2.test.ts index 23863077a..89b92e8e7 100644 --- a/server/tests/attach/upgrade/upgrade2.test.ts +++ b/server/tests/attach/upgrade/upgrade2.test.ts @@ -101,7 +101,6 @@ describe(`${chalk.yellowBright("upgrade2: Testing usage upgrades with monthly -> testClockId, advanceTo: addWeeks(curUnix, 2).getTime(), }); - return; await attachAndExpectCorrect({ autumn, @@ -113,7 +112,6 @@ describe(`${chalk.yellowBright("upgrade2: Testing usage upgrades with monthly -> env, }); }); - return; test("should attach premium annual product", async () => { await autumn.track({ diff --git a/server/tests/attach/utils.ts b/server/tests/attach/utils.ts index ccbf6dd06..7c9af7308 100644 --- a/server/tests/attach/utils.ts +++ b/server/tests/attach/utils.ts @@ -1,25 +1,5 @@ -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { - AppEnv, - AttachBranch, - BillingInterval, - CreateEntity, - FeatureOptions, - Organization, - ProductItem, - ProductV2, -} from "@autumn/shared"; -import { getAttachTotal } from "tests/utils/testAttachUtils/testAttachUtils.js"; -import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { expectInvoicesCorrect } from "tests/utils/expectUtils/expectProductAttached.js"; -import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js"; -import { notNullish, nullish, timeout, toSnakeCase } from "@/utils/genUtils.js"; -import { expectSubItemsCorrect } from "tests/utils/expectUtils/expectSubUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import Stripe from "stripe"; -import { expect } from "chai"; - -import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js"; +import type { BillingInterval, ProductItem, ProductV2 } from "@autumn/shared"; +import { nullish } from "@/utils/genUtils.js"; // export const runAttachTest = async ({ // autumn, @@ -182,20 +162,20 @@ export const replaceItems = ({ newItem: ProductItem; items: ProductItem[]; }) => { - let newItems = structuredClone(items); + const newItems = structuredClone(items); let index; if (featureId) { - index = newItems.findIndex((item) => item.feature_id == featureId); + index = newItems.findIndex((item) => item.feature_id === featureId); } if (interval) { index = newItems.findIndex( - (item) => item.interval == (interval as any) && nullish(item.feature_id), + (item) => item.interval === (interval as any) && nullish(item.feature_id), ); } - if (index == -1) { + if (index === -1) { throw new Error("Item not found"); } diff --git a/server/tests/balances/track/allocated/track-allocated2.test.ts b/server/tests/balances/track/allocated/track-allocated2.test.ts index 8c8c67c83..5297bd3cb 100644 --- a/server/tests/balances/track/allocated/track-allocated2.test.ts +++ b/server/tests/balances/track/allocated/track-allocated2.test.ts @@ -8,29 +8,34 @@ import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { timeout } from "../../../utils/genUtils.js"; -const testCase = "concurrentTrack2"; +const testCase = "track-allocated2"; const customerId = testCase; -const userItem = constructFeatureItem({ +// Continuous use feature (Postgres track) +const usersItem = constructFeatureItem({ featureId: TestFeature.Users, - includedUsage: 6, + includedUsage: 10, featureType: ProductItemFeatureType.ContinuousUse, }); +// Single use feature (Redis track) const messagesItem = constructFeatureItem({ featureId: TestFeature.Messages, - includedUsage: 100, - featureType: ProductItemFeatureType.ContinuousUse, + includedUsage: 50, + featureType: ProductItemFeatureType.SingleUse, }); const pro = constructProduct({ type: "free", isDefault: false, - items: [userItem, messagesItem], + items: [usersItem, messagesItem], }); -describe(`${chalk.yellowBright(`track-allocated1: Tracking allocated feature concurrently with consumable feature`)}`, () => { +describe(`${chalk.yellowBright( + `track-allocated2: Concurrent tracking of single_use + continuous_use features`, +)}`, () => { const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 }); beforeAll(async () => { @@ -53,70 +58,179 @@ describe(`${chalk.yellowBright(`track-allocated1: Tracking allocated feature con }); }); - test("should have initial balance of 6 for users and 100 for messages", async () => { + test("should have initial balances after attach", async () => { const customer = await autumnV1.customers.get(customerId); const usersBalance = customer.features[TestFeature.Users].balance; const messagesBalance = customer.features[TestFeature.Messages].balance; - expect(usersBalance).toBe(6); - expect(messagesBalance).toBe(100); + expect(usersBalance).toBe(10); + expect(messagesBalance).toBe(50); }); - test("should allow concurrent track with balance of 6 for users and 100 for messages", async () => { - const promises = [ + test("should handle concurrent tracks for both single_use and continuous_use features", async () => { + // Track 5 users (continuous_use via Postgres) + const usersTracks = Array.from({ length: 5 }, () => autumnV1.track({ customer_id: customerId, feature_id: TestFeature.Users, value: 1, }), - ]; + ); + + // Track 20 messages (single_use via Redis) + const messagesTracks = Array.from({ length: 20 }, () => + autumnV1.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: 1, + }), + ); + + // Send all tracks concurrently + await Promise.all([...usersTracks, ...messagesTracks]); + + // Wait for sync to complete + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Verify balances via /customers/:id + const customer = await autumnV1.customers.get(customerId); + + const usersBalance = customer.features[TestFeature.Users].balance; + const messagesBalance = customer.features[TestFeature.Messages].balance; + + expect(usersBalance).toBe(5); // 10 - 5 = 5 + expect(messagesBalance).toBe(30); // 50 - 20 = 30 + + // Verify balances via /check + const usersCheck = await autumnV1.check({ + customer_id: customerId, + feature_id: TestFeature.Users, + }); + const messagesCheck = await autumnV1.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + }); + + expect(usersCheck.balance).toBe(5); + expect(messagesCheck.balance).toBe(30); + + // Wait for sync to complete + await timeout(2000); + // Check non-cached customer + const nonCachedCustomer = await autumnV1.customers.get(customerId, { + skip_cache: "true", + }); + const nonCachedUsersBalance = + nonCachedCustomer.features[TestFeature.Users].balance; + const nonCachedMessagesBalance = + nonCachedCustomer.features[TestFeature.Messages].balance; + + expect(nonCachedUsersBalance).toBe(5); + expect(nonCachedMessagesBalance).toBe(30); }); - // test("should only allow one concurrent track with balance of 1", async () => { - // const promises = [ - // autumnV1.track({ - // customer_id: customerId, - // feature_id: TestFeature.Users, - // value: 1, - // }), - // autumnV1.track({ - // customer_id: customerId, - // feature_id: TestFeature.Users, - // value: 1, - // }), - // autumnV1.track({ - // customer_id: customerId, - // feature_id: TestFeature.Users, - // value: 1, - // }), - // autumnV1.track({ - // customer_id: customerId, - // feature_id: TestFeature.Users, - // value: 1, - // }), - // autumnV1.track({ - // customer_id: customerId, - // feature_id: TestFeature.Users, - // value: 1, - // }), - // ]; + test("should handle more concurrent mixed tracks", async () => { + // Track 3 more users (Postgres) + const usersTracks = Array.from({ length: 3 }, () => + autumnV1.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: 1, + }), + ); - // await Promise.all(promises); + // Track 15 more messages (Redis) + const messagesTracks = Array.from({ length: 15 }, () => + autumnV1.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: 1, + }), + ); - // // console.log(results); - // // return; + // Send all tracks concurrently + await Promise.all([...usersTracks, ...messagesTracks]); - // // const successCount = results.filter((r) => r.status === "fulfilled").length; - // // const rejectedCount = results.filter((r) => r.status === "rejected").length; + // Wait for sync to complete + await new Promise((resolve) => setTimeout(resolve, 1000)); - // // // Only 1 should succeed, 4 should be rejected due to insufficient balance - // // expect(successCount).toBe(1); - // // expect(rejectedCount).toBe(4); + // Verify final balances + const customer = await autumnV1.customers.get(customerId); + const usersBalance = customer.features[TestFeature.Users].balance; + const messagesBalance = customer.features[TestFeature.Messages].balance; - // // Check final balance - // const customer = await autumnV1.customers.get(customerId); - // const finalBalance = customer.features[TestFeature.Users].balance; + expect(usersBalance).toBe(2); // 5 - 3 = 2 + expect(messagesBalance).toBe(15); // 30 - 15 = 15 - // expect(finalBalance).toBe(-4); - // }); + // Double-check with /check + const usersCheck = await autumnV1.check({ + customer_id: customerId, + feature_id: TestFeature.Users, + }); + const messagesCheck = await autumnV1.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + }); + + expect(usersCheck.balance).toBe(2); + expect(messagesCheck.balance).toBe(15); + + // Wait for sync to complete + await timeout(2000); + // Check non-cached customer + const nonCachedCustomer = await autumnV1.customers.get(customerId, { + skip_cache: "true", + }); + const nonCachedUsersBalance = + nonCachedCustomer.features[TestFeature.Users].balance; + const nonCachedMessagesBalance = + nonCachedCustomer.features[TestFeature.Messages].balance; + + expect(nonCachedUsersBalance).toBe(2); + expect(nonCachedMessagesBalance).toBe(15); + }); + + test("should maintain consistency across multiple concurrent batches", async () => { + // Create multiple waves of concurrent tracks + const wave1 = [ + ...Array.from({ length: 2 }, () => + autumnV1.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: 1, + }), + ), + ...Array.from({ length: 10 }, () => + autumnV1.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: 1, + }), + ), + ]; + + await Promise.all(wave1); + await timeout(7000); + + // Verify final state + const customer = await autumnV1.customers.get(customerId); + const usersBalance = customer.features[TestFeature.Users].balance; + const messagesBalance = customer.features[TestFeature.Messages].balance; + + expect(usersBalance).toBe(0); // 2 - 2 = 0 + expect(messagesBalance).toBe(5); // 15 - 10 = 5 + + // Verify via check + const usersCheck = await autumnV1.check({ + customer_id: customerId, + feature_id: TestFeature.Users, + }); + const messagesCheck = await autumnV1.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + }); + + expect(usersCheck.balance).toBe(0); + expect(messagesCheck.balance).toBe(5); + }); }); diff --git a/server/tests/balances/track/allocated/track-allocated3.test.ts b/server/tests/balances/track/allocated/track-allocated3.test.ts new file mode 100644 index 000000000..aac4811cb --- /dev/null +++ b/server/tests/balances/track/allocated/track-allocated3.test.ts @@ -0,0 +1,225 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { ApiVersion, ProductItemFeatureType } from "@autumn/shared"; +import chalk from "chalk"; +import { TestFeature } from "tests/setup/v2Features.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { timeout } from "../../../utils/genUtils.js"; + +const testCase = "track-allocated3"; +const customerId = testCase; + +// Continuous use feature (Postgres track) +const cusUserItem = constructFeatureItem({ + featureId: TestFeature.Workflows, + includedUsage: 10, + featureType: ProductItemFeatureType.ContinuousUse, +}); + +const entUserItem = constructFeatureItem({ + featureId: TestFeature.Workflows, + includedUsage: 5, + featureType: ProductItemFeatureType.ContinuousUse, +}); + +const customerProd = constructProduct({ + type: "free", + isDefault: false, + items: [cusUserItem], +}); + +const entityProd = constructProduct({ + type: "free", + isDefault: false, + id: "entity_prod", + items: [entUserItem], +}); + +describe(`${chalk.yellowBright( + `track-allocated3: Concurrent tracking of consumable + allocated feature at entity / customer level`, +)}`, () => { + const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 }); + + const entity1Id = "track-allocated3-user-1"; + const entity2Id = "track-allocated3-user-2"; + + const entities = [ + { + id: entity1Id, + name: "User 1", + feature_id: TestFeature.Users, + }, + { + id: entity2Id, + name: "User 2", + feature_id: TestFeature.Users, + }, + ]; + + beforeAll(async () => { + await initCustomerV3({ + ctx, + customerId, + withTestClock: false, + }); + + await initProductsV0({ + ctx, + products: [customerProd, entityProd], + prefix: testCase, + }); + + // Attach product to customer + await autumnV1.attach({ + customer_id: customerId, + product_id: customerProd.id, + }); + + await autumnV1.entities.create(customerId, entities); + + for (const entity of entities) { + await autumnV1.attach({ + customer_id: customerId, + entity_id: entity.id, + product_id: entityProd.id, + }); + } + }); + + test("Initial balances after attach", async () => { + // Wait for cache to populate + await timeout(1000); + + const customer = await autumnV1.customers.get(customerId); + + expect(customer.features[TestFeature.Workflows].balance).toBe(20); // 10 + 5 * 2 + + // Verify entity balances + for (const entity of entities) { + const _entity = await autumnV1.entities.get(customerId, entity.id); + expect(_entity.features[TestFeature.Workflows].balance).toBe(10 + 5); // 3 + 10 + } + }); + + test("Random concurrent tracks across all 6 feature combinations", async () => { + // Generate random track amounts for all 6 combinations + const numCustomerWorkflows = Math.floor(Math.random() * 3) + 2; // 1-3 + const numEntity1Workflows = Math.floor(Math.random() * 2) + 1; // 1-2 + const numEntity2Workflows = Math.floor(Math.random() * 2) + 1; // 1-2 + + console.log(` +Table of initial workflow balances and usage to track: ++-----------+-----------+-------------+ +| Scope | Workflows | Used Amount | ++-----------+-----------+-------------+ +| Customer | 10 | ${numCustomerWorkflows} | +| Entity1 | 5 | ${numEntity1Workflows} | +| Entity2 | 5 | ${numEntity2Workflows} | ++-----------+-----------+-------------+ +Total workflows to use: ${numCustomerWorkflows + numEntity1Workflows + numEntity2Workflows} +`); + + // Initial balances (from setup) + const initialCusWorkflows = 10; // Customer-level workflows + const initialEnt1Workflows = 5; // Entity1-level workflows + const initialEnt2Workflows = 5; // Entity2-level workflows + + const trackPromises = []; + + // 2. Customer workflows (Postgres/continuous_use → syncCacheBalance) + for (let i = 0; i < numCustomerWorkflows; i++) { + trackPromises.push( + autumnV1.track({ + customer_id: customerId, + feature_id: TestFeature.Workflows, + value: 1, + }), + ); + } + + // 4. Entity1 workflows (Postgres/continuous_use → syncCacheBalance) + for (let i = 0; i < numEntity1Workflows; i++) { + trackPromises.push( + autumnV1.track({ + customer_id: customerId, + entity_id: entity1Id, + feature_id: TestFeature.Workflows, + value: 1, + }), + ); + } + + // 6. Entity2 workflows (Postgres/continuous_use → syncCacheBalance) + for (let i = 0; i < numEntity2Workflows; i++) { + trackPromises.push( + autumnV1.track({ + customer_id: customerId, + entity_id: entity2Id, + feature_id: TestFeature.Workflows, + value: 1, + }), + ); + } + + await Promise.all(trackPromises); + + // Wait for sync to complete + await timeout(2000); + + // Calculate expected balances after tracking + const expectedCusWorkflows = initialCusWorkflows - numCustomerWorkflows; + const expectedEnt1Workflows = initialEnt1Workflows - numEntity1Workflows; + const expectedEnt2Workflows = initialEnt2Workflows - numEntity2Workflows; + + // Verify customer-level balances + const customer = await autumnV1.customers.get(customerId); + // expect(customer.features[TestFeature.Messages].balance).toBe( + // expectedCustomerTotalMessages, + // ); + expect(customer.features[TestFeature.Workflows].balance).toBe( + expectedCusWorkflows + expectedEnt1Workflows + expectedEnt2Workflows, + ); + + // Check entity balances + const entity1 = await autumnV1.entities.get(customerId, entity1Id); + const entity2 = await autumnV1.entities.get(customerId, entity2Id); + expect(entity1.features[TestFeature.Workflows].balance).toBe( + expectedEnt1Workflows + expectedCusWorkflows, + ); + expect(entity2.features[TestFeature.Workflows].balance).toBe( + expectedEnt2Workflows + expectedCusWorkflows, + ); + + // Non cached + const nonCachedCustomer = await autumnV1.customers.get(customerId, { + skip_cache: "true", + }); + expect(nonCachedCustomer.features[TestFeature.Workflows].balance).toBe( + expectedCusWorkflows + expectedEnt1Workflows + expectedEnt2Workflows, + ); + const nonCachedEntity1 = await autumnV1.entities.get( + customerId, + entity1Id, + { + skip_cache: "true", + }, + ); + expect(nonCachedEntity1.features[TestFeature.Workflows].balance).toBe( + expectedEnt1Workflows + expectedCusWorkflows, + ); + const nonCachedEntity2 = await autumnV1.entities.get( + customerId, + entity2Id, + { + skip_cache: "true", + }, + ); + expect(nonCachedEntity2.features[TestFeature.Workflows].balance).toBe( + expectedEnt2Workflows + expectedCusWorkflows, + ); + }); +}); diff --git a/server/tests/balances/track/allocated/track-allocated4.test.ts b/server/tests/balances/track/allocated/track-allocated4.test.ts new file mode 100644 index 000000000..238e41092 --- /dev/null +++ b/server/tests/balances/track/allocated/track-allocated4.test.ts @@ -0,0 +1,345 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { ApiVersion, ProductItemFeatureType } from "@autumn/shared"; +import chalk from "chalk"; +import { TestFeature } from "tests/setup/v2Features.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { timeout } from "../../../utils/genUtils.js"; + +const testCase = "track-allocated4"; +const customerId = testCase; + +// Continuous use feature (Postgres track) +const cusUserItem = constructFeatureItem({ + featureId: TestFeature.Workflows, + includedUsage: 10, + featureType: ProductItemFeatureType.ContinuousUse, +}); + +// Single use feature (Redis track) +const cusMessagesItem = constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + featureType: ProductItemFeatureType.SingleUse, +}); + +const entUserItem = constructFeatureItem({ + featureId: TestFeature.Workflows, + includedUsage: 5, + featureType: ProductItemFeatureType.ContinuousUse, +}); + +const entMessagesItem = constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 50, + featureType: ProductItemFeatureType.SingleUse, +}); + +const customerProd = constructProduct({ + type: "free", + isDefault: false, + items: [cusUserItem, cusMessagesItem], +}); + +const entityProd = constructProduct({ + type: "free", + isDefault: false, + id: "entity_prod", + items: [entUserItem, entMessagesItem], +}); + +describe(`${chalk.yellowBright( + `track-allocated4: Concurrent tracking of consumable + allocated feature at entity / customer level`, +)}`, () => { + const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 }); + + const entity1Id = "track-allocated4-user-1"; + const entity2Id = "track-allocated4-user-2"; + + const entities = [ + { + id: entity1Id, + name: "User 1", + feature_id: TestFeature.Users, + }, + { + id: entity2Id, + name: "User 2", + feature_id: TestFeature.Users, + }, + ]; + + beforeAll(async () => { + await initCustomerV3({ + ctx, + customerId, + withTestClock: false, + }); + + await initProductsV0({ + ctx, + products: [customerProd, entityProd], + prefix: testCase, + }); + + // Attach product to customer + await autumnV1.attach({ + customer_id: customerId, + product_id: customerProd.id, + }); + + await autumnV1.entities.create(customerId, entities); + + for (const entity of entities) { + await autumnV1.attach({ + customer_id: customerId, + entity_id: entity.id, + product_id: entityProd.id, + }); + } + }); + + test("Initial balances after attach", async () => { + // Wait for cache to populate + await timeout(1000); + + const customer = await autumnV1.customers.get(customerId); + + // Customer level: workflows (10) + messages (50) + // Entity level: workflows (3+3=6) + messages (100+100=200) + expect(customer.features[TestFeature.Workflows].balance).toBe(20); // 10 + 5 * 2 + expect(customer.features[TestFeature.Messages].balance).toBe(200); // 100 + 50 * 2 + + // Verify entity balances + for (const entity of entities) { + const _entity = await autumnV1.entities.get(customerId, entity.id); + expect(_entity.features[TestFeature.Workflows].balance).toBe(10 + 5); // 3 + 10 + expect(_entity.features[TestFeature.Messages].balance).toBe(100 + 50); // 100 + 50 + } + }); + + test("Random concurrent tracks across all 6 feature combinations", async () => { + // Generate random track amounts for all 6 combinations + const numCustomerMessages = Math.floor(Math.random() * 20) + 5; // 5-25 + const numCustomerWorkflows = Math.floor(Math.random() * 3) + 2; // 1-3 + const numEntity1Messages = Math.floor(Math.random() * 10) + 2; // 2-12 + const numEntity1Workflows = Math.floor(Math.random() * 2) + 1; // 1-2 + const numEntity2Messages = Math.floor(Math.random() * 10) + 2; // 2-12 + const numEntity2Workflows = Math.floor(Math.random() * 2) + 1; // 1-2 + + const summedCustomerMessagesResult = + 200 - numCustomerMessages - numEntity1Messages - numEntity2Messages; + + const summedEntity1MessagesResult = + 150 - numCustomerMessages - numEntity1Messages; + + const summedEntity2MessagesResult = + 150 - numCustomerMessages - numEntity2Messages; + + console.log(` +Table of initial customer message balances and deducted messages: ++-----------+-----------+----------+-----------------+----------------------+--------------------------+ +| Scope | Workflows | Messages | Summed Messages | Summed Msg Result | Deducted Messages | ++-----------+-----------+----------+-----------------+----------------------+--------------------------+ +| Customer | 10 | 100 | 200 | ${summedCustomerMessagesResult.toString().padEnd(19)}| ${numCustomerMessages + .toString() + .padEnd(24)}| +| Entity1 | 5 | 50 | 150 | ${summedEntity1MessagesResult.toString().padEnd(19)}| ${numEntity1Messages + .toString() + .padEnd(24)}| +| Entity2 | 5 | 50 | 150 | ${summedEntity2MessagesResult.toString().padEnd(19)}| ${numEntity2Messages + .toString() + .padEnd(24)}| ++-----------+-----------+----------+-----------------+----------------------+--------------------------+ +Total messages: 200, Total workflows: 20 + +(Deducted = how many messages were deducted at each level) +`); + + console.log(`Tracking: + Customer: ${numCustomerMessages} messages, ${numCustomerWorkflows} workflows + Entity1: ${numEntity1Messages} messages, ${numEntity1Workflows} workflows + Entity2: ${numEntity2Messages} messages, ${numEntity2Workflows} workflows`); + + // Initial balances (from setup) + const initialCusMessages = 100; // Customer-level messages + const initialCusWorkflows = 10; // Customer-level workflows + const initialEnt1Messages = 50; // Entity1-level messages + const initialEnt1Workflows = 5; // Entity1-level workflows + const initialEnt2Messages = 50; // Entity2-level messages + const initialEnt2Workflows = 5; // Entity2-level workflows + + const trackPromises = []; + + // 2. Customer workflows (Postgres/continuous_use → syncCacheBalance) + for (let i = 0; i < numCustomerWorkflows; i++) { + trackPromises.push( + autumnV1.track({ + customer_id: customerId, + feature_id: TestFeature.Workflows, + value: 1, + }), + ); + } + + // 4. Entity1 workflows (Postgres/continuous_use → syncCacheBalance) + for (let i = 0; i < numEntity1Workflows; i++) { + trackPromises.push( + autumnV1.track({ + customer_id: customerId, + entity_id: entity1Id, + feature_id: TestFeature.Workflows, + value: 1, + }), + ); + } + + // 6. Entity2 workflows (Postgres/continuous_use → syncCacheBalance) + for (let i = 0; i < numEntity2Workflows; i++) { + trackPromises.push( + autumnV1.track({ + customer_id: customerId, + entity_id: entity2Id, + feature_id: TestFeature.Workflows, + value: 1, + }), + ); + } + + // 1. Customer messages (Redis/single_use) + for (let i = 0; i < numCustomerMessages; i++) { + trackPromises.push( + autumnV1.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: 1, + }), + ); + } + + // 3. Entity1 messages (Redis/single_use) + for (let i = 0; i < numEntity1Messages; i++) { + trackPromises.push( + autumnV1.track({ + customer_id: customerId, + entity_id: entity1Id, + feature_id: TestFeature.Messages, + value: 1, + }), + ); + } + + // 5. Entity2 messages (Redis/single_use) + for (let i = 0; i < numEntity2Messages; i++) { + trackPromises.push( + autumnV1.track({ + customer_id: customerId, + entity_id: entity2Id, + feature_id: TestFeature.Messages, + value: 1, + }), + ); + } + + await Promise.all(trackPromises); + + // Wait for sync to complete + await timeout(2000); + + // Calculate expected balances after tracking + const expectedCusMessages = initialCusMessages - numCustomerMessages; + const expectedCusWorkflows = initialCusWorkflows - numCustomerWorkflows; + + const expectedEnt1Messages = initialEnt1Messages - numEntity1Messages; + const expectedEnt1Workflows = initialEnt1Workflows - numEntity1Workflows; + + const expectedEnt2Messages = initialEnt2Messages - numEntity2Messages; + const expectedEnt2Workflows = initialEnt2Workflows - numEntity2Workflows; + + // Customer-level totals (customer + all entities) + const expectedCustomerTotalMessages = + expectedCusMessages + expectedEnt1Messages + expectedEnt2Messages; + const expectedCustomerTotalWorkflows = + expectedCusWorkflows + expectedEnt1Workflows + expectedEnt2Workflows; + + // Verify customer-level balances + const customer = await autumnV1.customers.get(customerId); + expect(customer.features[TestFeature.Messages].balance).toBe( + expectedCustomerTotalMessages, + ); + expect(customer.features[TestFeature.Workflows].balance).toBe( + expectedCustomerTotalWorkflows, + ); + + await timeout(2000); + + // Verify non-cached to ensure Postgres matches + const nonCachedCustomer = await autumnV1.customers.get(customerId, { + skip_cache: "true", + }); + expect(nonCachedCustomer.features[TestFeature.Messages].balance).toBe( + expectedCustomerTotalMessages, + ); + expect(nonCachedCustomer.features[TestFeature.Workflows].balance).toBe( + expectedCustomerTotalWorkflows, + ); + + // Entity-level totals (entity + customer inherited) + const expectedEntity1TotalMessages = + expectedEnt1Messages + expectedCusMessages; + const expectedEntity1TotalWorkflows = + expectedEnt1Workflows + expectedCusWorkflows; + const expectedEntity2TotalMessages = + expectedEnt2Messages + expectedCusMessages; + const expectedEntity2TotalWorkflows = + expectedEnt2Workflows + expectedCusWorkflows; + + // Verify entity-level balances (entity + customer inherited) + const entity1 = await autumnV1.entities.get(customerId, entity1Id); + const entity2 = await autumnV1.entities.get(customerId, entity2Id); + + expect(entity1.features[TestFeature.Messages].balance).toBe( + expectedEntity1TotalMessages, + ); + expect(entity1.features[TestFeature.Workflows].balance).toBe( + expectedEntity1TotalWorkflows, + ); + expect(entity2.features[TestFeature.Messages].balance).toBe( + expectedEntity2TotalMessages, + ); + expect(entity2.features[TestFeature.Workflows].balance).toBe( + expectedEntity2TotalWorkflows, + ); + + const nonCachedEntity1 = await autumnV1.entities.get( + customerId, + entity1Id, + { + skip_cache: "true", + }, + ); + expect(nonCachedEntity1.features[TestFeature.Messages].balance).toBe( + expectedEntity1TotalMessages, + ); + expect(nonCachedEntity1.features[TestFeature.Workflows].balance).toBe( + expectedEntity1TotalWorkflows, + ); + const nonCachedEntity2 = await autumnV1.entities.get( + customerId, + entity2Id, + { + skip_cache: "true", + }, + ); + expect(nonCachedEntity2.features[TestFeature.Messages].balance).toBe( + expectedEntity2TotalMessages, + ); + expect(nonCachedEntity2.features[TestFeature.Workflows].balance).toBe( + expectedEntity2TotalWorkflows, + ); + }); +}); diff --git a/server/tests/contUse/track/track6.test.ts b/server/tests/balances/track/allocated/track-allocated5.test.ts similarity index 85% rename from server/tests/contUse/track/track6.test.ts rename to server/tests/balances/track/allocated/track-allocated5.test.ts index b24d73d97..9d911bbfc 100644 --- a/server/tests/contUse/track/track6.test.ts +++ b/server/tests/balances/track/allocated/track-allocated5.test.ts @@ -4,7 +4,6 @@ import chalk from "chalk"; import { TestFeature } from "tests/setup/v2Features.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; @@ -21,14 +20,11 @@ export const free = constructProduct({ isDefault: false, }); -const testCase = "track6"; +const testCase = "track-allocated5"; -describe(`${chalk.yellowBright(`${testCase}: Testing track cont use, race condition`)}`, () => { +describe(`${chalk.yellowBright(`${testCase}: Tracking allocated feature with concurrency and +ve / -ve values`)}`, () => { const customerId = testCase; const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - - const curUnix = new Date().getTime(); beforeAll(async () => { await initProductsV0({ @@ -38,15 +34,13 @@ describe(`${chalk.yellowBright(`${testCase}: Testing track cont use, race condit customerId, }); - const { testClockId: testClockId1 } = await initCustomerV3({ + await initCustomerV3({ ctx, customerId, customerData: {}, attachPm: "success", withTestClock: true, }); - - testClockId = testClockId1!; }); test("should track 5 events in a row and have correct balance", async () => { @@ -79,12 +73,9 @@ describe(`${chalk.yellowBright(`${testCase}: Testing track cont use, race condit console.log(`New balance: ${startingBalance}`); const results = await Promise.all(promises); - - await timeout(10000); - const customer = await autumn.customers.get(customerId); const userFeature = customer.features[TestFeature.Users]; - if (userFeature.balance != startingBalance) { + if (userFeature.balance !== startingBalance) { for (let i = 0; i < values.length; i++) { console.log(`Value: ${values[i]}, Event ID: ${results[i].id}`); } diff --git a/server/tests/balances/track/basic/track-basic11.test.ts b/server/tests/balances/track/basic/track-basic11.test.ts index 370f50536..7da32ac88 100644 --- a/server/tests/balances/track/basic/track-basic11.test.ts +++ b/server/tests/balances/track/basic/track-basic11.test.ts @@ -120,7 +120,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing negative values (refunds/cr test("should reflect large refund in non-cached customer after 2s", async () => { // Wait 2 seconds for DB sync - await timeout(2000); + await timeout(5000); // Fetch customer with skip_cache=true const customer = await autumnV1.customers.get(customerId, { diff --git a/server/tests/balances/track/basic/track-basic9.test.ts b/server/tests/balances/track/basic/track-basic9.test.ts index 177acbc51..a8799b833 100644 --- a/server/tests/balances/track/basic/track-basic9.test.ts +++ b/server/tests/balances/track/basic/track-basic9.test.ts @@ -100,7 +100,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing pay-per-use (overage allowe test("should reflect overage balance in non-cached customer after 2s", async () => { // Wait 2 seconds for DB sync - await timeout(2000); + await timeout(5000); // Fetch customer with skip_cache=true const customer = await autumnV1.customers.get(customerId, { diff --git a/server/tests/balances/track/concurrency/concurrent-track4.test.ts b/server/tests/balances/track/concurrency/concurrent-track4.test.ts index a3627980c..2807b5638 100644 --- a/server/tests/balances/track/concurrency/concurrent-track4.test.ts +++ b/server/tests/balances/track/concurrency/concurrent-track4.test.ts @@ -128,7 +128,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage_limits with pay_per_u // Starting balance: 5, usage: 9, final balance: 5 - 9 = -4 // Wait 2 seconds for DB sync - await timeout(2000); + await timeout(5000); // Fetch customer with skip_cache=true const customer = await autumnV1.customers.get(customerId, { diff --git a/server/tests/balances/track/concurrency/concurrent-track6.test.ts b/server/tests/balances/track/concurrency/concurrent-track6.test.ts index 9c87b7a28..35dfa2749 100644 --- a/server/tests/balances/track/concurrency/concurrent-track6.test.ts +++ b/server/tests/balances/track/concurrency/concurrent-track6.test.ts @@ -180,15 +180,24 @@ describe(`${chalk.yellowBright(`${testCase}: Stress test with 10k concurrent req const expectedBalance = Decimal.max( 0, new Decimal(TOTAL_INCLUDED_USAGE).minus(totalUsage), - ).toNumber(); - const actualBalance = customer.features[TestFeature.Messages].balance; + ) + .toDP(5) + .toNumber(); + const actualBalance = new Decimal( + customer.features[TestFeature.Messages].balance ?? 0, + ) + .toDP(5) + .toNumber(); // Usage should be capped at included_usage without overage_allowed - const expectedUsage = Decimal.min( - totalUsage, - TOTAL_INCLUDED_USAGE, - ).toNumber(); - const actualUsage = customer.features[TestFeature.Messages].usage; + const expectedUsage = Decimal.min(totalUsage, TOTAL_INCLUDED_USAGE) + .toDP(5) + .toNumber(); + const actualUsage = new Decimal( + customer.features[TestFeature.Messages].usage ?? 0, + ) + .toDP(5) + .toNumber(); // Verify balance and usage match expectations expect(actualBalance).toEqual(expectedBalance); @@ -201,14 +210,16 @@ describe(`${chalk.yellowBright(`${testCase}: Stress test with 10k concurrent req (sum, b) => new Decimal(sum).plus(b.balance || 0).toNumber(), 0, ); - expect(breakdownBalance).toEqual(actualBalance!); + expect(new Decimal(breakdownBalance).toDP(5).toNumber()).toEqual( + actualBalance!, + ); } } }); test("should have correct non-cached balances for all customers after 2s", async () => { console.log("\n⏳ Waiting 2s for DB sync..."); - await timeout(2000); + await timeout(5000); for (const customerId of customerIds) { const customer = await autumnV1.customers.get(customerId, { @@ -221,15 +232,24 @@ describe(`${chalk.yellowBright(`${testCase}: Stress test with 10k concurrent req const expectedBalance = Decimal.max( 0, new Decimal(TOTAL_INCLUDED_USAGE).minus(totalUsage), - ).toNumber(); - const actualBalance = customer.features[TestFeature.Messages].balance; + ) + .toDP(5) + .toNumber(); + const actualBalance = new Decimal( + customer.features[TestFeature.Messages].balance ?? 0, + ) + .toDP(5) + .toNumber(); // Usage should be capped at included_usage without overage_allowed - const expectedUsage = Decimal.min( - totalUsage, - TOTAL_INCLUDED_USAGE, - ).toNumber(); - const actualUsage = customer.features[TestFeature.Messages].usage; + const expectedUsage = Decimal.min(totalUsage, TOTAL_INCLUDED_USAGE) + .toDP(5) + .toNumber(); + const actualUsage = new Decimal( + customer.features[TestFeature.Messages].usage ?? 0, + ) + .toDP(5) + .toNumber(); // Use Decimal for precise comparisons - expect exact match expect(actualBalance).toEqual(expectedBalance); @@ -245,7 +265,9 @@ describe(`${chalk.yellowBright(`${testCase}: Stress test with 10k concurrent req 0, ); - expect(breakdownBalance).toEqual(actualBalance!); + expect(new Decimal(breakdownBalance).toDP(5).toNumber()).toEqual( + actualBalance!, + ); } } diff --git a/server/tests/balances/track/entity-balances/track-entity-balances1.test.ts b/server/tests/balances/track/entity-balances/track-entity-balances1.test.ts index 70583357d..61aa11d40 100644 --- a/server/tests/balances/track/entity-balances/track-entity-balances1.test.ts +++ b/server/tests/balances/track/entity-balances/track-entity-balances1.test.ts @@ -118,8 +118,8 @@ describe(`${chalk.yellowBright("track-entity-balances1: basic entity cache test" const customerFromCache = await autumnV1.customers.get(customerId); // Customer should match - expect(customerFromDb.features[TestFeature.Dashboard]).toEqual( - customerFromCache.features[TestFeature.Dashboard], + expect(customerFromCache.features[TestFeature.Dashboard]).toMatchObject( + customerFromDb.features[TestFeature.Dashboard], ); // All entities should match @@ -132,8 +132,8 @@ describe(`${chalk.yellowBright("track-entity-balances1: basic entity cache test" entity.id, ); - expect(entityFromDb.features[TestFeature.Dashboard]).toEqual( - entityFromCache.features[TestFeature.Dashboard], + expect(entityFromCache.features[TestFeature.Dashboard]).toMatchObject( + entityFromDb.features[TestFeature.Dashboard], ); } }); diff --git a/server/tests/balances/track/entity-products/track-entity-products3.test.ts b/server/tests/balances/track/entity-products/track-entity-products3.test.ts index 457753726..8f4f99f2f 100644 --- a/server/tests/balances/track/entity-products/track-entity-products3.test.ts +++ b/server/tests/balances/track/entity-products/track-entity-products3.test.ts @@ -1,350 +1,283 @@ -// import { beforeAll, describe, expect, test } from "bun:test"; -// import { ApiVersion, type LimitedItem } from "@autumn/shared"; -// import chalk from "chalk"; -// import { Decimal } from "decimal.js"; -// import { TestFeature } from "tests/setup/v2Features.js"; -// import { timeout } from "tests/utils/genUtils.js"; -// import ctx from "tests/utils/testInitUtils/createTestContext.js"; -// import { AutumnInt } from "@/external/autumn/autumnCli.js"; -// import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; -// import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -// import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; -// import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { beforeAll, describe, expect, test } from "bun:test"; +import { ApiVersion, ProductItemFeatureType } from "@autumn/shared"; +import chalk from "chalk"; +import { TestFeature } from "tests/setup/v2Features.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { timeout } from "../../../utils/genUtils.js"; -// const testCase = "track-entity-products3"; +const testCase = "track-entity-products3"; +const customerId = testCase; -// // Entity-level messages (monthly, per entity) -// const entityMessagesItem = constructFeatureItem({ -// featureId: TestFeature.Messages, -// includedUsage: 5000, -// interval: "month" as any, -// intervalCount: 1, -// }) as LimitedItem; +// Continuous use feature (Postgres track) +const cusUserItem = constructFeatureItem({ + featureId: TestFeature.Workflows, + includedUsage: 10, + featureType: ProductItemFeatureType.ContinuousUse, +}); -// const freeProd = constructProduct({ -// type: "free", -// isDefault: false, -// items: [entityMessagesItem], -// }); +// Single use feature (Redis track) +const cusMessagesItem = constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + featureType: ProductItemFeatureType.SingleUse, +}); -// const NUM_REQUESTS = 5000; -// const NUM_CUSTOMERS = 1; -// const NUM_ENTITIES = 2; +const entUserItem = constructFeatureItem({ + featureId: TestFeature.Workflows, + includedUsage: 5, + featureType: ProductItemFeatureType.ContinuousUse, +}); -// // Helper to generate random decimal between min and max -// const randomDecimal = (min: number, max: number): Decimal => { -// const value = Math.random() * (max - min) + min; -// return new Decimal(value).toDecimalPlaces(2); -// }; +const entMessagesItem = constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 50, + featureType: ProductItemFeatureType.SingleUse, +}); -// // Helper to randomly choose an entity or null (for customer-level) -// const randomEntityOrNull = (entities: { id: string }[]): string | null => { -// // 50% chance customer-level, 50% chance entity-level -// if (Math.random() < 0.5) { -// return null; // Customer-level -// } -// // Randomly pick an entity -// const randomIndex = Math.floor(Math.random() * entities.length); -// return entities[randomIndex].id; -// }; +const customerProd = constructProduct({ + type: "free", + isDefault: false, + items: [cusUserItem, cusMessagesItem], +}); -// describe(`${chalk.yellowBright(`${testCase}: Concurrent entity product tracking`)}`, () => { -// const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 }); +const entityProd = constructProduct({ + type: "free", + isDefault: false, + id: "entity_prod", + items: [entUserItem, entMessagesItem], +}); -// // Create multiple customers with their entities -// const customers = Array.from({ length: NUM_CUSTOMERS }, (_, i) => { -// const customerId = `${testCase}-customer-${i + 1}`; -// return { -// id: customerId, -// entities: Array.from({ length: NUM_ENTITIES }, (_, i) => ({ -// id: `${customerId}-user-${i + 1}`, -// name: `User ${i + 1}`, -// feature_id: TestFeature.Users, -// })), -// }; -// }); +describe(`${chalk.yellowBright( + `track-entity-products3: Tracking customer / entity balance concurrently`, +)}`, () => { + const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 }); -// // Track expected balances per customer -// const expectedCustomerBalances: Record = {}; -// const expectedEntityBalances: Record = {}; + const entity1Id = "track-entity-products3-user-1"; + const entity2Id = "track-entity-products3-user-2"; -// // Initialize expected balances -// for (const customer of customers) { -// expectedCustomerBalances[customer.id] = new Decimal(0); -// for (const entity of customer.entities) { -// expectedEntityBalances[entity.id] = new Decimal(0); -// } -// } + const entities = [ + { + id: entity1Id, + name: "User 1", + feature_id: TestFeature.Users, + }, + { + id: entity2Id, + name: "User 2", + feature_id: TestFeature.Users, + }, + ]; -// beforeAll(async () => { -// // Initialize products once -// await initProductsV0({ -// ctx, -// products: [freeProd], -// prefix: testCase, -// }); + beforeAll(async () => { + await initCustomerV3({ + ctx, + customerId, + withTestClock: false, + }); -// // Initialize all customers and attach products to entities -// for (const customer of customers) { -// await initCustomerV3({ -// ctx, -// customerId: customer.id, -// withTestClock: false, -// }); + await initProductsV0({ + ctx, + products: [customerProd, entityProd], + prefix: testCase, + }); -// await autumnV1.entities.create(customer.id, customer.entities); + // Attach product to customer + await autumnV1.attach({ + customer_id: customerId, + product_id: customerProd.id, + }); -// // Attach product to each entity -// for (const entity of customer.entities) { -// await autumnV1.attach({ -// customer_id: customer.id, -// entity_id: entity.id, -// product_id: freeProd.id, -// }); -// } + await autumnV1.entities.create(customerId, entities); -// // Initialize caches -// await autumnV1.customers.get(customer.id); -// for (const entity of customer.entities) { -// await autumnV1.entities.get(customer.id, entity.id); -// } -// } -// }); + for (const entity of entities) { + await autumnV1.attach({ + customer_id: customerId, + entity_id: entity.id, + product_id: entityProd.id, + }); + } + }); -// test("should have initial balances", async () => { -// for (const customer of customers) { -// const customerData = await autumnV1.customers.get(customer.id); + test("Initial balances after attach", async () => { + // Wait for cache to populate + await timeout(1000); -// console.log(`\n🔍 Initial state for ${customer.id}:`); -// console.log( -// ` Customer balance: ${customerData.features[TestFeature.Messages].balance}`, -// ); -// console.log( -// ` Customer usage: ${customerData.features[TestFeature.Messages].usage}`, -// ); + const customer = await autumnV1.customers.get(customerId); -// // Customer should have: 5000 * NUM_ENTITIES (entity-level products attached to entities) -// expect(customerData.features[TestFeature.Messages].balance).toBe( -// entityMessagesItem.included_usage * NUM_ENTITIES, -// ); + // Customer level: workflows (10) + messages (50) + // Entity level: workflows (3+3=6) + messages (100+100=200) + expect(customer.features[TestFeature.Workflows].balance).toBe(20); // 10 + 5 * 2 + expect(customer.features[TestFeature.Messages].balance).toBe(200); // 100 + 50 * 2 -// // Each entity should have: 5000 (entity-level) -// for (const entity of customer.entities) { -// const _entity = await autumnV1.entities.get(customer.id, entity.id); -// console.log( -// ` Entity ${entity.id} balance: ${_entity.features[TestFeature.Messages].balance}`, -// ); -// expect(_entity.features[TestFeature.Messages].balance).toBe( -// entityMessagesItem.included_usage, -// ); -// } -// } -// }); + // Verify entity balances + for (const entity of entities) { + const _entity = await autumnV1.entities.get(customerId, entity.id); + expect(_entity.features[TestFeature.Workflows].balance).toBe(10 + 5); // 3 + 10 + expect(_entity.features[TestFeature.Messages].balance).toBe(100 + 50); // 100 + 50 + } + }); -// test(`should handle ${NUM_REQUESTS} concurrent requests with mixed entity/customer tracking`, async () => { -// console.log( -// `\n🚀 Starting ${NUM_REQUESTS} concurrent track requests across ${NUM_CUSTOMERS} customers...`, -// ); + test("Random concurrent tracks across all 6 feature combinations", async () => { + // Generate random track amounts for all 6 combinations + const numCustomerMessages = Math.floor(Math.random() * 20) + 5; // 5-25 + const numCustomerWorkflows = Math.floor(Math.random() * 3) + 2; // 1-3 + const numEntity1Messages = Math.floor(Math.random() * 10) + 2; // 2-12 + const numEntity1Workflows = Math.floor(Math.random() * 2) + 1; // 1-2 + const numEntity2Messages = Math.floor(Math.random() * 10) + 2; // 2-12 + const numEntity2Workflows = Math.floor(Math.random() * 2) + 1; // 1-2 -// const allPromises: Promise[] = []; -// const trackingLogs: Record< -// string, -// Array<{ entityId: string | null; value: Decimal }> -// > = {}; + const summedCustomerMessagesResult = + 200 - numCustomerMessages - numEntity1Messages - numEntity2Messages; -// // Initialize tracking logs per customer -// for (const customer of customers) { -// trackingLogs[customer.id] = []; -// } + const summedEntity1MessagesResult = + 150 - numCustomerMessages - numEntity1Messages; -// for (let i = 0; i < NUM_REQUESTS; i++) { -// // Randomly pick a customer -// const customer = customers[Math.floor(Math.random() * customers.length)]; + const summedEntity2MessagesResult = + 150 - numCustomerMessages - numEntity2Messages; -// // Generate random value between 0.01 and 2.00 -// const decimalValue = randomDecimal(0.01, 2.0); -// const value = decimalValue.toNumber(); + console.log(` +Table of initial customer message balances and deducted messages: ++-----------+-----------+----------+-----------------+----------------------+--------------------------+ +| Scope | Workflows | Messages | Summed Messages | Summed Msg Result | Deducted Messages | ++-----------+-----------+----------+-----------------+----------------------+--------------------------+ +| Customer | 10 | 100 | 200 | ${summedCustomerMessagesResult.toString().padEnd(19)}| ${numCustomerMessages + .toString() + .padEnd(24)}| +| Entity1 | 5 | 50 | 150 | ${summedEntity1MessagesResult.toString().padEnd(19)}| ${numEntity1Messages + .toString() + .padEnd(24)}| +| Entity2 | 5 | 50 | 150 | ${summedEntity2MessagesResult.toString().padEnd(19)}| ${numEntity2Messages + .toString() + .padEnd(24)}| ++-----------+-----------+----------+-----------------+----------------------+--------------------------+ +Total messages: 200, Total workflows: 20 -// // Randomly choose entity or customer-level -// const entityId = randomEntityOrNull(customer.entities); +(Deducted = how many messages were deducted at each level) +`); -// // Store for tracking -// trackingLogs[customer.id].push({ entityId, value: decimalValue }); + console.log(`Tracking: + Customer: ${numCustomerMessages} messages, ${numCustomerWorkflows} workflows + Entity1: ${numEntity1Messages} messages, ${numEntity1Workflows} workflows + Entity2: ${numEntity2Messages} messages, ${numEntity2Workflows} workflows`); -// // Create track request -// const promise = autumnV1.track({ -// customer_id: customer.id, -// entity_id: entityId || undefined, -// feature_id: TestFeature.Messages, -// value: value, -// skip_event: true, -// }); + // Initial balances (from setup) + const initialCusMessages = 100; // Customer-level messages + const initialCusWorkflows = 10; // Customer-level workflows + const initialEnt1Messages = 50; // Entity1-level messages + const initialEnt1Workflows = 5; // Entity1-level workflows + const initialEnt2Messages = 50; // Entity2-level messages + const initialEnt2Workflows = 5; // Entity2-level workflows -// allPromises.push(promise); -// } + const trackPromises = []; -// // Execute all requests concurrently -// const startTime = Date.now(); -// await Promise.all(allPromises); -// const endTime = Date.now(); + // 1. Customer messages (Redis/single_use) + for (let i = 0; i < numCustomerMessages; i++) { + trackPromises.push( + autumnV1.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: 1, + }), + ); + } -// console.log( -// `\n✅ Completed ${NUM_REQUESTS} requests in ${endTime - startTime}ms`, -// ); -// console.log( -// ` Average: ${((endTime - startTime) / NUM_REQUESTS).toFixed(2)}ms per request`, -// ); + // 3. Entity1 messages (Redis/single_use) + for (let i = 0; i < numEntity1Messages; i++) { + trackPromises.push( + autumnV1.track({ + customer_id: customerId, + entity_id: entity1Id, + feature_id: TestFeature.Messages, + value: 1, + }), + ); + } -// // Calculate expected balances by simulating deduction logic for each customer -// console.log(`\n📊 Calculating expected balances per customer...`); + // 5. Entity2 messages (Redis/single_use) + for (let i = 0; i < numEntity2Messages; i++) { + trackPromises.push( + autumnV1.track({ + customer_id: customerId, + entity_id: entity2Id, + feature_id: TestFeature.Messages, + value: 1, + }), + ); + } -// for (const customer of customers) { -// const trackingLog = trackingLogs[customer.id]; + await Promise.all(trackPromises); -// console.log(`\n ${customer.id}:`); -// console.log(` Tracks: ${trackingLog.length}`); + // Wait for sync to complete + await timeout(2000); -// // Initialize balances (entity-only, no customer-level entitlements) -// const entityBalances: Record = {}; -// for (const entity of customer.entities) { -// entityBalances[entity.id] = new Decimal( -// entityMessagesItem.included_usage, -// ); -// } + // Calculate expected balances after tracking + const expectedCusMessages = initialCusMessages - numCustomerMessages; + const expectedEnt1Messages = initialEnt1Messages - numEntity1Messages; + const expectedEnt2Messages = initialEnt2Messages - numEntity2Messages; -// let customerLevelTracks = 0; -// let entityLevelTracks = 0; + // Customer-level totals (customer + all entities) + const expectedCustomerTotalMessages = + expectedCusMessages + expectedEnt1Messages + expectedEnt2Messages; -// // Process each track sequentially to calculate expected state -// for (const log of trackingLog) { -// let remaining = log.value; + // Verify customer-level balances + const customer = await autumnV1.customers.get(customerId); + expect(customer.features[TestFeature.Messages].balance).toBe( + expectedCustomerTotalMessages, + ); -// if (log.entityId === null) { -// // Customer-level tracking: deduct from entities in alphabetical order -// customerLevelTracks++; + await timeout(2000); -// const sortedEntityIds = Object.keys(entityBalances).sort(); -// for (const entityId of sortedEntityIds) { -// if (remaining.lte(0)) break; + // Verify non-cached to ensure Postgres matches + const nonCachedCustomer = await autumnV1.customers.get(customerId, { + skip_cache: "true", + }); + expect(nonCachedCustomer.features[TestFeature.Messages].balance).toBe( + expectedCustomerTotalMessages, + ); -// const entityBalance = entityBalances[entityId]; -// const deducted = Decimal.min(entityBalance, remaining); -// entityBalances[entityId] = entityBalance.minus(deducted); -// remaining = remaining.minus(deducted); -// } -// } else { -// // Entity-level tracking: deduct from specific entity's balance -// entityLevelTracks++; + // Entity-level totals (entity + customer inherited) + const expectedEntity1TotalMessages = + expectedEnt1Messages + expectedCusMessages; + const expectedEntity2TotalMessages = + expectedEnt2Messages + expectedCusMessages; -// const entityBalance = entityBalances[log.entityId]; -// const deducted = Decimal.min(entityBalance, remaining); -// entityBalances[log.entityId] = entityBalance.minus(deducted); -// remaining = remaining.minus(deducted); -// } -// } + // Verify entity-level balances (entity + customer inherited) + const entity1 = await autumnV1.entities.get(customerId, entity1Id); + const entity2 = await autumnV1.entities.get(customerId, entity2Id); -// console.log(` Customer-level tracks: ${customerLevelTracks}`); -// console.log(` Entity-level tracks: ${entityLevelTracks}`); -// for (const entity of customer.entities) { -// console.log( -// ` Expected ${entity.id} balance: ${entityBalances[entity.id].toFixed(2)}`, -// ); -// } + expect(entity1.features[TestFeature.Messages].balance).toBe( + expectedEntity1TotalMessages, + ); -// // Store expected values for next test (no separate customer balance) -// expectedCustomerBalances[customer.id] = new Decimal(0); -// for (const entity of customer.entities) { -// expectedEntityBalances[entity.id] = entityBalances[entity.id]; -// } -// } -// }); + expect(entity2.features[TestFeature.Messages].balance).toBe( + expectedEntity2TotalMessages, + ); -// test("should have correct cached balances after concurrent tracking", async () => { -// for (const customer of customers) { -// const customerData = await autumnV1.customers.get(customer.id); + const nonCachedEntity1 = await autumnV1.entities.get( + customerId, + entity1Id, + { + skip_cache: "true", + }, + ); + expect(nonCachedEntity1.features[TestFeature.Messages].balance).toBe( + expectedEntity1TotalMessages, + ); -// console.log(`\n🔍 Final cached state for ${customer.id}:`); - -// // Get expected entity balances for this customer -// const expectedCusEntityBalances = customer.entities.reduce( -// (acc, entity) => { -// acc[entity.id] = expectedEntityBalances[entity.id]; -// return acc; -// }, -// {} as Record, -// ); - -// // Customer cache shows aggregated balance (sum of all entity balances) -// const expectedAggregatedBalance = Object.values( -// expectedCusEntityBalances, -// ).reduce((sum, b) => sum.plus(b), new Decimal(0)); - -// console.log( -// ` Actual customer balance: ${customerData.features[TestFeature.Messages].balance}`, -// ); -// console.log( -// ` Expected customer balance: ${expectedAggregatedBalance.toFixed(2)}`, -// ); - -// expect(customerData.features[TestFeature.Messages].balance).toBe( -// expectedAggregatedBalance.toNumber(), -// ); - -// // Each entity cache shows entity balance only -// for (const entity of customer.entities) { -// const _entity = await autumnV1.entities.get(customer.id, entity.id); -// const expectedEntityBalance = expectedEntityBalances[entity.id]; - -// console.log( -// ` Actual ${entity.id} balance: ${_entity.features[TestFeature.Messages].balance}`, -// ); -// console.log( -// ` Expected ${entity.id} balance: ${expectedEntityBalance.toFixed(2)}`, -// ); - -// expect(_entity.features[TestFeature.Messages].balance).toBe( -// expectedEntityBalance.toNumber(), -// ); -// } -// } -// }); - -// test("verify database state matches cache after all tracking", async () => { -// console.log("\n⏳ Waiting 4s for DB sync..."); -// await timeout(4000); - -// for (const customer of customers) { -// // Read from database (skip cache) -// const customerFromDb = await autumnV1.customers.get(customer.id, { -// skip_cache: "true", -// }); -// const customerFromCache = await autumnV1.customers.get(customer.id); - -// // Customer features should match -// expect(customerFromDb.features[TestFeature.Messages]).toEqual( -// customerFromCache.features[TestFeature.Messages], -// ); - -// // All entities should match -// for (const entity of customer.entities) { -// const entityFromDb = await autumnV1.entities.get( -// customer.id, -// entity.id, -// { -// skip_cache: "true", -// }, -// ); -// const entityFromCache = await autumnV1.entities.get( -// customer.id, -// entity.id, -// ); - -// expect(entityFromDb.features[TestFeature.Messages]).toEqual( -// entityFromCache.features[TestFeature.Messages], -// ); -// } -// } - -// console.log("\n✅ All balances verified successfully!"); -// }); -// }); + const nonCachedEntity2 = await autumnV1.entities.get( + customerId, + entity2Id, + { + skip_cache: "true", + }, + ); + expect(nonCachedEntity2.features[TestFeature.Messages].balance).toBe( + expectedEntity2TotalMessages, + ); + }); +}); diff --git a/server/tests/balances/track/legacy/track-legacy3.test.ts b/server/tests/balances/track/legacy/track-legacy3.test.ts new file mode 100644 index 000000000..c31f6424c --- /dev/null +++ b/server/tests/balances/track/legacy/track-legacy3.test.ts @@ -0,0 +1,138 @@ +import { beforeAll } from "bun:test"; +import { ProductItemInterval } from "@autumn/shared"; +import chalk from "chalk"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { + constructFeatureItem, + constructPrepaidItem, +} from "../../../../src/utils/scriptUtils/constructItem.js"; +import { + constructProduct, + constructRawProduct, +} from "../../../../src/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "../../../../src/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "../../../../src/utils/scriptUtils/testUtils/initProductsV0.js"; +import { AutumnCli } from "../../../cli/AutumnCli.js"; +import { TestFeature } from "../../../setup/v2Features.js"; +import { checkEntitledOnProduct } from "./trackLegacyUtils.js"; + +const testCase = "trackLegacy3"; + +// Pro product - matches global products.pro +const pro = constructProduct({ + type: "pro", + items: [ + constructFeatureItem({ + featureId: TestFeature.Dashboard, + isBoolean: true, + }), + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 10, + interval: ProductItemInterval.Month, + }), + constructFeatureItem({ + featureId: TestFeature.Admin, + unlimited: true, + }), + ], +}); + +// One-time add-on product - matches global products.oneTimeAddOnMetered1 +const oneTimeAddOnMetered1 = constructRawProduct({ + id: "one-time-add-on-metered-1", + isAddOn: true, + items: [ + constructPrepaidItem({ + featureId: TestFeature.Messages, + isOneOff: true, + billingUnits: 100, + includedUsage: 0, + }), + ], +}); + +describe(`${chalk.yellowBright( + "trackLegacy3: Testing /events and /entitled, for pro, one time top up", +)}`, () => { + const customerId = testCase; + + let curAllowance = 0; + const oneTimeBillingUnits = 100; // From oneTimeAddOnMetered1 prepaid item + const oneTimeQuantity = 2 * oneTimeBillingUnits; + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro, oneTimeAddOnMetered1], + prefix: testCase, + customerId, + }); + + await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + }); + + // test("should have correct entitlements (free)", async function () { + // await checkEntitledOnProduct({ + // customerId: customerId, + // product: free, + // finish: true, + // }); + // }); + + test("should attach pro", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: pro.id, + }); + }); + + test("should have correct entitlements (pro)", async () => { + const used = await checkEntitledOnProduct({ + customerId: customerId, + product: pro, + finish: false, + }); + + const messagesItem = pro.items.find( + (item) => item.feature_id === TestFeature.Messages, + ); + const proAllowance = + messagesItem?.included_usage && + typeof messagesItem.included_usage === "number" + ? messagesItem.included_usage + : 0; + curAllowance = proAllowance - used; + }); + + test("should attach one time top up", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: oneTimeAddOnMetered1.id, + options: [ + { + feature_id: TestFeature.Messages, + quantity: oneTimeQuantity, + }, + ], + }); + }); + + test("should have correct entitlements (one time top up)", async () => { + // const oneTimeAmt = oneTimeBillingUnits * oneTimeQuantity; + + await checkEntitledOnProduct({ + customerId: customerId, + product: oneTimeAddOnMetered1, + finish: true, + totalAllowance: curAllowance + oneTimeQuantity, + timeoutMs: 15000, + }); + }); +}); diff --git a/server/tests/contUse/entities/entity1.backup.ts b/server/tests/contUse/entities/entity1.backup.ts deleted file mode 100644 index 12ff6588a..000000000 --- a/server/tests/contUse/entities/entity1.backup.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { - type AppEnv, - LegacyVersion, - OnDecrease, - OnIncrease, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import { addWeeks } from "date-fns"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { addPrefixToProducts } from "../../attach/utils.js"; - -const userItem = constructArrearProratedItem({ - featureId: TestFeature.Users, - pricePerUnit: 50, - includedUsage: 1, - config: { - on_increase: OnIncrease.BillImmediately, - on_decrease: OnDecrease.None, - }, -}); - -export const pro = constructProduct({ - items: [userItem], - type: "pro", -}); - -const testCase = "entity1"; - -// Pro is $20 / month, Seat is $50 / user - -describe(`${chalk.yellowBright(`contUse/${testCase}: Testing create / delete entities`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - let curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - let usage = 0; - const firstEntities = [ - { - id: "1", - name: "test", - feature_id: TestFeature.Users, - }, - ]; - - it("should create entity, then attach pro", async () => { - await autumn.entities.create(customerId, firstEntities); - usage += 1; - - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - usage: [ - { - featureId: TestFeature.Users, - value: 1, - }, - ], - }); - }); - - const entities = [ - { - id: "2", - name: "test", - feature_id: TestFeature.Users, - }, - { - id: "3", - name: "test2", - feature_id: TestFeature.Users, - }, - ]; - - it("should create 2 entities and have correct invoice", async () => { - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addWeeks(new Date(), 2).getTime(), - waitForSeconds: 30, - }); - - await autumn.entities.create(customerId, entities); - await timeout(3000); - - usage += entities.length; - - await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - itemQuantity: usage, - }); - - const customer = await autumn.customers.get(customerId); - const invoices = customer.invoices!; - expect(invoices.length).to.equal(2); - expect(invoices[0].total).to.equal(userItem.price! * entities.length); - }); - - it("should delete 1 entity and have no new invoice", async () => { - await autumn.entities.delete(customerId, entities[0].id); - - const customer = await autumn.customers.get(customerId); - const invoices = customer.invoices!; - expect(invoices.length).to.equal(2); - - await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - numReplaceables: 1, - itemQuantity: usage - 1, - }); - }); - - const newEntities = [ - { - id: "4", - name: "test3", - feature_id: TestFeature.Users, - }, - { - id: "5", - name: "test4", - feature_id: TestFeature.Users, - }, - ]; - - it("should create 2 entities and have correct invoice (only pay for 1)", async () => { - await autumn.entities.create(customerId, newEntities); - await timeout(3000); - usage += 1; - - const customer = await autumn.customers.get(customerId); - const invoices = customer.invoices!; - - expect(invoices.length).to.equal(3); - expect(invoices[0].total).to.equal(userItem.price!); - - await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - itemQuantity: usage, - }); - }); -}); diff --git a/server/tests/contUse/entities/entity1.test.ts b/server/tests/contUse/entities/entity1.test.ts index 92cb82cfc..8cd1d7eec 100644 --- a/server/tests/contUse/entities/entity1.test.ts +++ b/server/tests/contUse/entities/entity1.test.ts @@ -1,15 +1,9 @@ -import { - LegacyVersion, - OnDecrease, - OnIncrease, -} from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; import chalk from "chalk"; -import { addWeeks } from "date-fns"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; @@ -41,7 +35,6 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing create / delete ent const customerId = testCase; const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); let testClockId: string; - let curUnix = new Date().getTime(); beforeAll(async () => { await initProductsV0({ @@ -106,12 +99,12 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing create / delete ent ]; test("should create 2 entities and have correct invoice", async () => { - curUnix = await advanceTestClock({ - stripeCli: ctx.stripeCli, - testClockId, - advanceTo: addWeeks(new Date(), 2).getTime(), - waitForSeconds: 30, - }); + // await advanceTestClock({ + // stripeCli: ctx.stripeCli, + // testClockId, + // advanceTo: addWeeks(new Date(), 2).getTime(), + // waitForSeconds: 30, + // }); await autumn.entities.create(customerId, entities); await timeout(3000); diff --git a/server/tests/contUse/entities/entity2.backup.ts b/server/tests/contUse/entities/entity2.backup.ts deleted file mode 100644 index 001eb441f..000000000 --- a/server/tests/contUse/entities/entity2.backup.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { - type AppEnv, - LegacyVersion, - OnDecrease, - OnIncrease, - type Organization, -} from "@autumn/shared"; -import chalk from "chalk"; -import { addWeeks } from "date-fns"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { - calcProrationAndExpectInvoice, - expectSubQuantityCorrect, -} from "tests/utils/expectUtils/expectContUseUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { addPrefixToProducts } from "../../attach/utils.js"; - -const userItem = constructArrearProratedItem({ - featureId: TestFeature.Users, - pricePerUnit: 50, - includedUsage: 1, - config: { - on_increase: OnIncrease.ProrateImmediately, - on_decrease: OnDecrease.ProrateImmediately, - }, -}); - -const pro = constructProduct({ - items: [userItem], - type: "pro", -}); - -const testCase = "entity2"; - -describe(`${chalk.yellowBright(`contUse/${testCase}: Testing entities, prorate now`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - let curUnix = Date.now(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - let usage = 0; - const firstEntities = [ - { - id: "1", - name: "test", - feature_id: TestFeature.Users, - }, - ]; - - it("should create entity, then attach pro", async () => { - await autumn.entities.create(customerId, firstEntities); - usage += 1; - - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - usage: [ - { - featureId: TestFeature.Users, - value: usage, - }, - ], - }); - }); - - const newEntities = [ - { - id: "2", - name: "test", - feature_id: TestFeature.Users, - }, - { - id: "3", - name: "test2", - feature_id: TestFeature.Users, - }, - ]; - - it("should create 2 entities and have correct invoice", async () => { - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addWeeks(new Date(), 2).getTime(), - waitForSeconds: 30, - }); - - await autumn.entities.create(customerId, newEntities); - usage += newEntities.length; - - const { stripeSubs } = await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - itemQuantity: usage, - }); - - await timeout(5000); - - await calcProrationAndExpectInvoice({ - autumn, - stripeSubs, - customerId, - quantity: newEntities.length, - unitPrice: userItem.price!, - curUnix, - numInvoices: 2, - }); - }); - - it("should delete 1 entity and have correct invoice amount", async () => { - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addWeeks(curUnix, 1).getTime(), - waitForSeconds: 30, - }); - - await timeout(5000); - - await autumn.entities.delete(customerId, newEntities[0].id); - usage -= 1; - - const { stripeSubs } = await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - }); - - await calcProrationAndExpectInvoice({ - autumn, - stripeSubs, - customerId, - quantity: -1, - unitPrice: userItem.price!, - curUnix, - numInvoices: 3, - }); - }); -}); diff --git a/server/tests/contUse/entities/entity2.test.ts b/server/tests/contUse/entities/entity2.test.ts index 36914d714..e7b0744ff 100644 --- a/server/tests/contUse/entities/entity2.test.ts +++ b/server/tests/contUse/entities/entity2.test.ts @@ -1,9 +1,5 @@ -import { - LegacyVersion, - OnDecrease, - OnIncrease, -} from "@autumn/shared"; import { beforeAll, describe, test } from "bun:test"; +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; import chalk from "chalk"; import { addWeeks } from "date-fns"; import { TestFeature } from "tests/setup/v2Features.js"; diff --git a/server/tests/contUse/entities/entity3.backup.ts b/server/tests/contUse/entities/entity3.backup.ts deleted file mode 100644 index 32f8fcde8..000000000 --- a/server/tests/contUse/entities/entity3.backup.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { - type AppEnv, - LegacyVersion, - OnDecrease, - OnIncrease, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import { addHours, addMonths, addWeeks } from "date-fns"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { addPrefixToProducts } from "../../attach/utils.js"; - -const userItem = constructArrearProratedItem({ - featureId: TestFeature.Users, - pricePerUnit: 50, - includedUsage: 1, - config: { - on_increase: OnIncrease.BillImmediately, - on_decrease: OnDecrease.None, - }, -}); - -export const pro = constructProduct({ - items: [userItem], - type: "pro", -}); - -const testCase = "entity3"; - -describe(`${chalk.yellowBright(`contUse/${testCase}: Testing replaceables deleted at end of cycle`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - let curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - let usage = 0; - const firstEntities = [ - { - id: "1", - name: "test", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "test", - feature_id: TestFeature.Users, - }, - { - id: "3", - name: "test", - feature_id: TestFeature.Users, - }, - ]; - - it("should create three entities, then attach pro", async () => { - await autumn.entities.create(customerId, firstEntities); - usage += firstEntities.length; - - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - usage: [ - { - featureId: TestFeature.Users, - value: usage, - }, - ], - }); - }); - - it("should delete 2 entities and have no new invoice", async () => { - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addWeeks(new Date(), 2).getTime(), - waitForSeconds: 30, - }); - - await autumn.entities.delete(customerId, firstEntities[0].id); - await autumn.entities.delete(customerId, firstEntities[1].id); - - const numReplaceables = 2; - await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - numReplaceables, - itemQuantity: usage - numReplaceables, - }); - - const customer = await autumn.customers.get(customerId); - const invoices = customer.invoices!; - expect(invoices.length).to.equal(1); - }); - - it("should advance clock to next cycle and have correct invoice", async () => { - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addHours( - addMonths(new Date(), 1), - hoursToFinalizeInvoice, - ).getTime(), - }); - - usage -= 2; // 2 entities deleted - - const customer = await autumn.customers.get(customerId); - const invoices = customer.invoices; - - const basePrice = getBasePrice({ product: pro }); - expect(invoices.length).to.equal(2); - expect(invoices[0].total).to.equal(basePrice); // 0 entities - - await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - itemQuantity: usage, - numReplaceables: 0, - }); - }); -}); diff --git a/server/tests/contUse/entities/entity3.test.ts b/server/tests/contUse/entities/entity3.test.ts index e981ad002..aaf5bfb0c 100644 --- a/server/tests/contUse/entities/entity3.test.ts +++ b/server/tests/contUse/entities/entity3.test.ts @@ -1,9 +1,5 @@ -import { - LegacyVersion, - OnDecrease, - OnIncrease, -} from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; import chalk from "chalk"; import { addHours, addMonths, addWeeks } from "date-fns"; import { TestFeature } from "tests/setup/v2Features.js"; @@ -11,8 +7,8 @@ import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/contUse/entities/entity4.backup.ts b/server/tests/contUse/entities/entity4.backup.ts deleted file mode 100644 index ad49d3340..000000000 --- a/server/tests/contUse/entities/entity4.backup.ts +++ /dev/null @@ -1,244 +0,0 @@ -// Handling per entity features! - -import { - type AppEnv, - CusExpand, - LegacyVersion, - type LimitedItem, - OnDecrease, - OnIncrease, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { useEntityBalanceAndExpect } from "tests/utils/expectUtils/expectContUse/expectEntityUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { - constructArrearProratedItem, - constructFeatureItem, -} from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { addPrefixToProducts } from "../../attach/utils.js"; - -const userItem = constructArrearProratedItem({ - featureId: TestFeature.Users, - pricePerUnit: 50, - includedUsage: 1, - config: { - on_increase: OnIncrease.BillImmediately, - on_decrease: OnDecrease.None, - }, -}); - -const perEntityItem = constructFeatureItem({ - featureId: TestFeature.Messages, - entityFeatureId: TestFeature.Users, - includedUsage: 500, -}) as LimitedItem; - -export const pro = constructProduct({ - items: [userItem, perEntityItem], - type: "pro", -}); - -const testCase = "entity4"; - -describe(`${chalk.yellowBright(`contUse/${testCase}: Testing per entity features`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - let usage = 0; - const firstEntities = [ - { - id: "1", - name: "test", - feature_id: TestFeature.Users, - }, - ]; - - it("should create one entity, then attach pro", async () => { - await autumn.entities.create(customerId, firstEntities); - usage += firstEntities.length; - - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - usage: [ - { - featureId: TestFeature.Users, - value: usage, - }, - ], - }); - }); - - it("should create 3 entities and have correct message balance", async () => { - const newEntities = [ - { - id: "2", - name: "test", - feature_id: TestFeature.Users, - }, - { - id: "3", - name: "test", - feature_id: TestFeature.Users, - }, - ]; - - await autumn.entities.create(customerId, newEntities); - usage += newEntities.length; - - const customer = await autumn.customers.get(customerId, { - expand: [CusExpand.Entities], - }); - - const res = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - }); - - expect(res.balance).to.equal( - (perEntityItem.included_usage as number) * usage, - ); - - // @ts-expect-error - for (const entity of customer.entities) { - const entRes = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - entity_id: entity.id, - }); - - expect(entRes.balance).to.equal(perEntityItem.included_usage); - } - }); - - return; - - // 1. Use from main balance... - it("should use from top level balance", async () => { - const deduction = 600; - const perEntityIncluded = perEntityItem.included_usage as number; - - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Messages, - value: deduction, - }); - await timeout(5000); - - const { balance } = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - }); - - expect(balance).to.equal(perEntityIncluded * usage - deduction); - }); - - it("should use from entity balance", async () => { - await useEntityBalanceAndExpect({ - autumn, - customerId, - featureId: TestFeature.Messages, - entityId: "2", - }); - - await useEntityBalanceAndExpect({ - autumn, - customerId, - featureId: TestFeature.Messages, - entityId: "3", - }); - }); - - // Delete one entity and create a new one and master balance should be same - const deletedEntityId = "2"; - const newEntity = { - id: "4", - name: "test", - feature_id: TestFeature.Users, - }; - it("should delete one entity and create a new one", async () => { - const { balance: masterBalanceBefore } = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - }); - - const { balance: entityBalanceBefore } = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - entity_id: deletedEntityId, - }); - - await autumn.entities.delete(customerId, deletedEntityId); - await autumn.entities.create(customerId, [newEntity]); - - const { balance: masterBalanceAfter } = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - }); - - expect(masterBalanceAfter).to.equal(masterBalanceBefore); - - const { balance: entityBalanceAfter } = await autumn.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - entity_id: newEntity.id, - }); - - expect(entityBalanceAfter).to.equal(entityBalanceBefore); - }); -}); diff --git a/server/tests/contUse/entities/entity4.test.ts b/server/tests/contUse/entities/entity4.test.ts index ba74c5fed..6ac017a7d 100644 --- a/server/tests/contUse/entities/entity4.test.ts +++ b/server/tests/contUse/entities/entity4.test.ts @@ -1,5 +1,6 @@ // Handling per entity features! +import { beforeAll, describe, expect, test } from "bun:test"; import { CusExpand, LegacyVersion, @@ -7,7 +8,6 @@ import { OnDecrease, OnIncrease, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; @@ -49,8 +49,6 @@ const testCase = "entity4"; describe(`${chalk.yellowBright(`contUse/${testCase}: Testing per entity features`)}`, () => { const customerId = testCase; const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - const curUnix = new Date().getTime(); beforeAll(async () => { await initProductsV0({ @@ -60,15 +58,13 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing per entity features customerId, }); - const { testClockId: testClockId1 } = await initCustomerV3({ + await initCustomerV3({ ctx, customerId, customerData: {}, attachPm: "success", withTestClock: true, }); - - testClockId = testClockId1!; }); let usage = 0; @@ -127,9 +123,7 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing per entity features feature_id: TestFeature.Messages, }); - expect(res.balance).toBe( - (perEntityItem.included_usage as number) * usage, - ); + expect(res.balance).toBe((perEntityItem.included_usage as number) * usage); // @ts-expect-error for (const entity of customer.entities) { @@ -143,8 +137,6 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing per entity features } }); - return; - // 1. Use from main balance... test("should use from top level balance", async () => { const deduction = 600; diff --git a/server/tests/contUse/entities/entity5.backup.ts b/server/tests/contUse/entities/entity5.backup.ts deleted file mode 100644 index 46beea1fb..000000000 --- a/server/tests/contUse/entities/entity5.backup.ts +++ /dev/null @@ -1,185 +0,0 @@ -// test payment failures - -import { - type AppEnv, - LegacyVersion, - OnDecrease, - OnIncrease, - type Organization, -} from "@autumn/shared"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; -import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { addPrefixToProducts } from "../../attach/utils.js"; - -const userItem = constructArrearProratedItem({ - featureId: TestFeature.Users, - pricePerUnit: 50, - includedUsage: 1, - config: { - on_increase: OnIncrease.BillImmediately, - on_decrease: OnDecrease.None, - }, -}); - -export const pro = constructProduct({ - items: [userItem], - type: "pro", -}); - -const testCase = "entity5"; - -describe(`${chalk.yellowBright(`contUse/${testCase}: Testing create entity payment fail`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - let usage = 0; - const firstEntities = [ - { - id: "1", - name: "test", - feature_id: TestFeature.Users, - }, - ]; - - it("should create one entity, then attach pro", async () => { - await autumn.entities.create(customerId, firstEntities); - usage += firstEntities.length; - - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - usage: [ - { - featureId: TestFeature.Users, - value: usage, - }, - ], - }); - }); - - it("should attach failed payment method", async () => { - const fullCus = await CusService.getFull({ - db, - idOrInternalId: customerId, - orgId: org.id, - env, - }); - - await attachFailedPaymentMethod({ - stripeCli, - customer: fullCus, - }); - }); - - it("should try to create entities and fail", async () => { - await expectAutumnError({ - errMessage: "Your card was declined.", - func: async () => { - await autumn.entities.create(customerId, [ - { - id: "2", - name: "test", - feature_id: TestFeature.Users, - }, - { - id: "3", - name: "test", - feature_id: TestFeature.Users, - }, - ]); - }, - }); - - await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - numReplaceables: 0, - }); - }); - - it("should track usage for users and fail", async () => { - await expectAutumnError({ - errMessage: "(Stripe Error) Your card was declined.", - func: async () => { - return await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: 2, - }); - }, - }); - - await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - numReplaceables: 0, - }); - }); -}); diff --git a/server/tests/contUse/entities/entity5.test.ts b/server/tests/contUse/entities/entity5.test.ts index 823890340..f9648e5e4 100644 --- a/server/tests/contUse/entities/entity5.test.ts +++ b/server/tests/contUse/entities/entity5.test.ts @@ -1,11 +1,7 @@ // test payment failures -import { - LegacyVersion, - OnDecrease, - OnIncrease, -} from "@autumn/shared"; import { beforeAll, describe, test } from "bun:test"; +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; import chalk from "chalk"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; @@ -40,8 +36,6 @@ const testCase = "entity5"; describe(`${chalk.yellowBright(`contUse/${testCase}: Testing create entity payment fail`)}`, () => { const customerId = testCase; const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - const curUnix = new Date().getTime(); beforeAll(async () => { await initProductsV0({ @@ -51,15 +45,13 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing create entity payme customerId, }); - const { testClockId: testClockId1 } = await initCustomerV3({ + await initCustomerV3({ ctx, customerId, customerData: {}, attachPm: "success", withTestClock: true, }); - - testClockId = testClockId1!; }); let usage = 0; @@ -108,7 +100,7 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing create entity payme test("should try to create entities and fail", async () => { await expectAutumnError({ - errMessage: "(Stripe Error) Your card was declined.", + errMessage: "card was declined.", func: async () => { await autumn.entities.create(customerId, [ { @@ -139,7 +131,7 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing create entity payme test("should track usage for users and fail", async () => { await expectAutumnError({ - errMessage: "(Stripe Error) Your card was declined.", + errMessage: "card was declined.", func: async () => { return await autumn.track({ customer_id: customerId, diff --git a/server/tests/contUse/roles/role1.test.ts b/server/tests/contUse/roles/role1.test.ts index 7ddfcdf68..f9dc2dcc4 100644 --- a/server/tests/contUse/roles/role1.test.ts +++ b/server/tests/contUse/roles/role1.test.ts @@ -1,11 +1,11 @@ // Handling per entity features! +import { beforeAll, describe, expect, test } from "bun:test"; import { LegacyVersion, type LimitedItem, type ProductItem, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import { Decimal } from "decimal.js"; import { TestFeature } from "tests/setup/v2Features.js"; @@ -188,8 +188,12 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing roles`)}`, () => { entity_id: adminId, }); - expect(adminBalance).toBe(adminMessages.included_usage); - expect(userBalance).toBe(expectedUserBalance); + expect(new Decimal(adminBalance ?? 0).toDP(5).toNumber()).toBe( + new Decimal(adminMessages.included_usage).toDP(5).toNumber(), + ); + expect(new Decimal(userBalance ?? 0).toDP(5).toNumber()).toBe( + new Decimal(expectedUserBalance).toDP(5).toNumber(), + ); }); const adminUsage = Math.random() * 50; @@ -217,7 +221,11 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing roles`)}`, () => { entity_id: userId, }); - expect(adminBalance).toBe(expectedAdminBalance); - expect(userBalance).toBe(expectedUserBalance); + expect(new Decimal(adminBalance ?? 0).toDP(5).toNumber()).toBe( + new Decimal(expectedAdminBalance).toDP(5).toNumber(), + ); + expect(new Decimal(userBalance ?? 0).toDP(5).toNumber()).toBe( + new Decimal(expectedUserBalance).toDP(5).toNumber(), + ); }); }); diff --git a/server/tests/contUse/track/track1.backup.ts b/server/tests/contUse/track/track1.backup.ts deleted file mode 100644 index 00ae39b49..000000000 --- a/server/tests/contUse/track/track1.backup.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { - type AppEnv, - LegacyVersion, - OnDecrease, - OnIncrease, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import { addWeeks } from "date-fns"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -const userItem = constructArrearProratedItem({ - featureId: TestFeature.Users, - pricePerUnit: 50, - includedUsage: 1, - config: { - on_increase: OnIncrease.BillImmediately, - on_decrease: OnDecrease.None, - }, -}); - -export const pro = constructProduct({ - items: [userItem], - type: "pro", -}); - -const testCase = "track1"; - -describe(`${chalk.yellowBright(`contUse/${testCase}: Testing track usage for cont use`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - let curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - let usage = 0; - it("should attach pro", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - - it("should create track +3 usage and have correct invoice", async () => { - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addWeeks(new Date(), 2).getTime(), - waitForSeconds: 5, - }); - - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: 3, - }); - - await timeout(15000); - - usage += 3; - - await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - }); - - const customer = await autumn.customers.get(customerId); - const invoices = customer.invoices; - expect(invoices.length).to.equal(2); - expect(invoices[0].total).to.equal(userItem.price! * 2); - }); - - it("should track -3 and have no new invoice", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: -3, - }); - - await timeout(5000); - - const customer = await autumn.customers.get(customerId); - const invoices = customer.invoices; - expect(invoices.length).to.equal(2); - - await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - numReplaceables: 3, - itemQuantity: usage - 3, - }); - }); - - it("should track +3 and have no new invoice", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: 3, - }); - - await timeout(5000); - - const customer = await autumn.customers.get(customerId); - const invoices = customer.invoices; - expect(invoices.length).to.equal(2); - - await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - }); - }); -}); diff --git a/server/tests/contUse/track/track2.backup.ts b/server/tests/contUse/track/track2.backup.ts deleted file mode 100644 index ae3fb410c..000000000 --- a/server/tests/contUse/track/track2.backup.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { - type AppEnv, - LegacyVersion, - OnDecrease, - OnIncrease, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -const userItem = constructArrearProratedItem({ - featureId: TestFeature.Users, - pricePerUnit: 50, - includedUsage: 1, - config: { - on_increase: OnIncrease.BillImmediately, - on_decrease: OnDecrease.None, - }, -}); - -export const pro = constructProduct({ - items: [userItem], - type: "pro", -}); - -const testCase = "track2"; - -describe(`${chalk.yellowBright(`contUse/${testCase}: Testing track usage for cont use (without overage)`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - let usage = 0; - it("should attach pro", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - - it("should track +1 and have no new invoice", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: 1, - }); - - usage += 1; - - await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - }); - - const customer = await autumn.customers.get(customerId); - const invoices = customer.invoices; - expect(invoices.length).to.equal(1); - }); - - it("should track -1 and have no new invoice", async () => { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: -1, - }); - - usage -= 1; - - await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - }); - }); -}); diff --git a/server/tests/contUse/track/track3.backup.ts b/server/tests/contUse/track/track3.backup.ts deleted file mode 100644 index 4f7ff6c88..000000000 --- a/server/tests/contUse/track/track3.backup.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { - type AppEnv, - LegacyVersion, - OnDecrease, - OnIncrease, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import { addWeeks } from "date-fns"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { - expectSubQuantityCorrect, - expectUpcomingItemsCorrect, -} from "tests/utils/expectUtils/expectContUseUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -const userItem = constructArrearProratedItem({ - featureId: TestFeature.Users, - pricePerUnit: 50, - includedUsage: 1, - config: { - on_increase: OnIncrease.ProrateNextCycle, - on_decrease: OnDecrease.ProrateNextCycle, - }, -}); - -export const pro = constructProduct({ - items: [userItem], - type: "pro", -}); - -const testCase = "track3"; - -describe(`${chalk.yellowBright(`contUse/${testCase}: Testing track usage for cont use, prorate next cycle`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - let curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - let usage = 0; - it("should attach pro", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - - it("should create track +3 usage and have correct invoice", async () => { - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addWeeks(new Date(), 2).getTime(), - waitForSeconds: 5, - }); - - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: 3, - }); - - await timeout(15000); - - usage += 3; - - const { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - }); - - await expectUpcomingItemsCorrect({ - stripeCli, - fullCus, - stripeSubs, - curUnix, - expectedNumItems: 1, - unitPrice: userItem.price!, - quantity: 2, - }); - - const customer = await autumn.customers.get(customerId); - const invoices = customer.invoices; - expect(invoices.length).to.equal(1); - }); - - it("should track -1 and have no new invoice", async () => { - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addWeeks(curUnix, 1).getTime(), - waitForSeconds: 5, - }); - - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: -1, - }); - - usage -= 1; - - const { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - }); - - await expectUpcomingItemsCorrect({ - stripeCli, - fullCus, - stripeSubs, - unitPrice: userItem.price!, - curUnix, - expectedNumItems: 2, - quantity: -1, - }); - - const customer = await autumn.customers.get(customerId); - const invoices = customer.invoices; - expect(invoices.length).to.equal(1); - }); - - it("should track -1 and have no new invoice", async () => { - const quantity = 2; - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: quantity, - }); - - usage += quantity; - - const { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - }); - - await expectUpcomingItemsCorrect({ - stripeCli, - fullCus, - stripeSubs, - unitPrice: userItem.price!, - curUnix, - expectedNumItems: 3, - quantity, - }); - - const customer = await autumn.customers.get(customerId); - const invoices = customer.invoices; - expect(invoices.length).to.equal(1); - }); -}); diff --git a/server/tests/contUse/track/track4.backup.ts b/server/tests/contUse/track/track4.backup.ts deleted file mode 100644 index 32ca63b5a..000000000 --- a/server/tests/contUse/track/track4.backup.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { - type AppEnv, - LegacyVersion, - OnDecrease, - OnIncrease, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import { addWeeks } from "date-fns"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { - expectSubQuantityCorrect, - expectUpcomingItemsCorrect, -} from "tests/utils/expectUtils/expectContUseUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -const userItem = constructArrearProratedItem({ - featureId: TestFeature.Users, - pricePerUnit: 50, - includedUsage: 1, - config: { - on_increase: OnIncrease.ProrateNextCycle, - on_decrease: OnDecrease.ProrateNextCycle, - }, -}); - -export const pro = constructProduct({ - items: [userItem], - type: "pro", -}); - -const testCase = "track4"; - -describe(`${chalk.yellowBright(`contUse/${testCase}: Testing set usage for cont use, prorate next cycle`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - let curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - let usage = 0; - it("should attach pro", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - - it("should create set usage to 3 and have correct invoice", async () => { - // curUnix = await advanceTestClock({ - // stripeCli, - // testClockId, - // advanceTo: addWeeks(curUnix, 2).getTime(), - // waitForSeconds: 15, - // }); - - await autumn.usage({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: 3, - }); - - await timeout(15000); - - usage += 3; - - const { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - }); - - await expectUpcomingItemsCorrect({ - stripeCli, - fullCus, - stripeSubs, - curUnix, - expectedNumItems: 1, - unitPrice: userItem.price!, - quantity: 2, - }); - - const customer = await autumn.customers.get(customerId); - const invoices = customer.invoices; - expect(invoices.length).to.equal(1); - }); - - it("should set usage to 2 and have no new invoice", async () => { - const newUsage = 2; - - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addWeeks(curUnix, 1).getTime(), - waitForSeconds: 15, - }); - - await autumn.usage({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: newUsage, - }); - - const { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage: newUsage, - }); - - await expectUpcomingItemsCorrect({ - stripeCli, - fullCus, - stripeSubs, - unitPrice: userItem.price!, - curUnix, - expectedNumItems: 2, - quantity: -1, - }); - - const customer = await autumn.customers.get(customerId); - const invoices = customer.invoices; - expect(invoices.length).to.equal(1); - }); - - it("should set usage to 4 and have no new invoice", async () => { - const newUsage = 4; - await autumn.usage({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: newUsage, - }); - - usage = newUsage; - - const { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({ - stripeCli, - productId: pro.id, - db, - org, - env, - customerId, - usage, - }); - - await expectUpcomingItemsCorrect({ - stripeCli, - fullCus, - stripeSubs, - unitPrice: userItem.price!, - curUnix, - expectedNumItems: 3, - quantity: 2, - }); - - const customer = await autumn.customers.get(customerId); - const invoices = customer.invoices; - expect(invoices.length).to.equal(1); - }); -}); diff --git a/server/tests/contUse/track/track5.backup.ts b/server/tests/contUse/track/track5.backup.ts deleted file mode 100644 index c169e902d..000000000 --- a/server/tests/contUse/track/track5.backup.ts +++ /dev/null @@ -1,258 +0,0 @@ -import chalk from "chalk"; -import Stripe from "stripe"; - -import { expect } from "chai"; -import { features } from "tests/global.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; - -import { addDays, addHours } from "date-fns"; - -import { Decimal } from "decimal.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { setupBefore } from "tests/before.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { AppEnv, OnDecrease, OnIncrease, Organization } from "@autumn/shared"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; -import { - addPrefixToProducts, - getBasePrice, -} from "tests/utils/testProductUtils/testProductUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { defaultApiVersion } from "tests/constants.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { getSubsFromCusId } from "tests/utils/expectUtils/expectSubUtils.js"; -import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; -import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; -import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; - -const seatsItem = constructArrearProratedItem({ - featureId: features.seats.id, - pricePerUnit: 20, - includedUsage: 3, - config: { - on_increase: OnIncrease.ProrateNextCycle, - on_decrease: OnDecrease.ProrateNextCycle, - }, -}); - -const seatsProduct = constructProduct({ - type: "pro", - items: [seatsItem], -}); - -const testCase = "track5"; -const includedUsage = seatsItem.included_usage as number; - -const simulateOneCycle = async ({ - customerId, - db, - org, - env, - stripeCli, - curUnix, - usageValues, - autumn, - testClockId, -}: { - customerId: string; - db: DrizzleCli; - org: Organization; - env: AppEnv; - stripeCli: Stripe; - curUnix: number; - usageValues: number[]; - autumn: AutumnInt; - testClockId: string; -}) => { - const { subs } = await getSubsFromCusId({ - customerId, - db, - org, - env, - stripeCli, - productId: seatsProduct.id, - }); - - let sub = subs[0]; - - let accruedPrice = 0; - for (const usageValue of usageValues) { - let daysToAdvance = Math.round(Math.random() * 10) + 1; - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addDays(curUnix, daysToAdvance).getTime(), - waitForSeconds: 10, - }); - - let customer = await autumn.customers.get(customerId); - let prevBalance = customer.features[seatsItem.feature_id!].balance!; - let prevUsage = includedUsage - prevBalance; - - let usageDiff = usageValue - prevUsage; - - let value1 = Math.floor(usageDiff / 2); - let value2 = usageDiff - value1; - - await autumn.track({ - customer_id: customerId, - feature_id: seatsItem.feature_id!, - value: value1, - }); - - await autumn.track({ - customer_id: customerId, - feature_id: seatsItem.feature_id!, - value: value2, - }); - - let newBalance = includedUsage - usageValue; - let prevOverage = Math.max(0, -prevBalance); - let newOverage = Math.max(0, -newBalance); - - let newPrice = (newOverage - prevOverage) * seatsItem.price!; - - const { start, end } = subToPeriodStartEnd({ sub }); - let proratedPrice = calculateProrationAmount({ - periodStart: start * 1000, - periodEnd: end * 1000, - now: curUnix, - amount: newPrice, - allowNegative: true, - }); - - accruedPrice = new Decimal(accruedPrice).plus(proratedPrice).toNumber(); - } - - let customer = await autumn.customers.get(customerId); - let balance = customer.features[seatsItem.feature_id!].balance!; - - let overage = Math.min(0, includedUsage - balance); - let usagePrice = overage * seatsItem.price!; - let basePrice = getBasePrice({ product: seatsProduct }); - - const totalPrice = new Decimal(accruedPrice) - .plus(usagePrice) - .plus(basePrice) - .toDecimalPlaces(2) - .toNumber(); - - const { start, end } = subToPeriodStartEnd({ sub }); - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addHours(end * 1000, hoursToFinalizeInvoice).getTime(), - waitForSeconds: 30, - }); - - let cusAfter = await autumn.customers.get(customerId); - let invoices = cusAfter.invoices; - let invoice = invoices[0]; - - expect(invoice.total).to.approximately( - totalPrice, - 0.01, - `Invoice total should be ${totalPrice} +/- 0.01`, - ); - - return { - curUnix, - }; -}; - -describe(`${chalk.yellowBright("conUse/track5: Testing update cont use through /usage")}`, () => { - const customerId = testCase; - - let stripeCli: Stripe; - - let testClockId = ""; - let db: DrizzleCli; - let org: Organization; - let env: AppEnv; - let autumn = new AutumnInt({ version: defaultApiVersion }); - let curUnix = Date.now(); - - before(async function () { - await setupBefore(this); - org = this.org; - env = this.env; - db = this.db; - - let res = await initCustomer({ - customerId, - org, - env, - db, - autumn: this.autumnJs, - attachPm: "success", - }); - - addPrefixToProducts({ - products: [seatsProduct], - prefix: testCase, - }); - - await createProducts({ - products: [seatsProduct], - orgId: org.id, - env, - db, - autumn, - }); - - testClockId = res.testClockId; - - db = this.db; - org = this.org; - env = this.env; - stripeCli = this.stripeCli; - }); - - it("should attach in arrear prorated seats", async () => { - await attachAndExpectCorrect({ - customerId, - product: seatsProduct, - db, - org, - env, - autumn, - stripeCli, - }); - }); - - // return; - - it("simulate first cycle and have correct invoice / balance", async () => { - let res = await simulateOneCycle({ - customerId, - db, - org, - env, - stripeCli, - curUnix, - usageValues: [8, 2], - autumn, - testClockId, - }); - - curUnix = res.curUnix; - }); - - it("simulate second cycle and have correct invoice / balance", async () => { - let res = await simulateOneCycle({ - customerId, - db, - org, - env, - stripeCli, - curUnix, - usageValues: [12, 3], - autumn, - testClockId, - }); - - curUnix = res.curUnix; - }); -}); diff --git a/server/tests/contUse/track/track5.test.ts b/server/tests/contUse/track/track5.test.ts index 331650538..66b7e0d66 100644 --- a/server/tests/contUse/track/track5.test.ts +++ b/server/tests/contUse/track/track5.test.ts @@ -139,7 +139,8 @@ const simulateOneCycle = async ({ const invoices = cusAfter.invoices; const invoice = invoices[0]; - expect(invoice.total).toBeCloseTo(totalPrice, 2); + expect(invoice.total).toBeLessThanOrEqual(totalPrice + 0.01); + expect(invoice.total).toBeGreaterThanOrEqual(totalPrice - 0.01); return { curUnix, diff --git a/server/tests/contUse/track/track6.backup.ts b/server/tests/contUse/track/track6.backup.ts deleted file mode 100644 index 505c61c57..000000000 --- a/server/tests/contUse/track/track6.backup.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { - type AppEnv, - LegacyVersion, - type LimitedItem, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { timeout } from "@/utils/genUtils.js"; -import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -const userItem = constructFeatureItem({ - featureId: TestFeature.Users, - includedUsage: 5, -}) as LimitedItem; - -export const free = constructProduct({ - items: [userItem], - type: "free", - isDefault: false, -}); - -const testCase = "track6"; - -describe(`${chalk.yellowBright(`${testCase}: Testing track cont use, race condition`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - const curUnix = new Date().getTime(); - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [free], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [free], - customerId, - db, - orgId: org.id, - env, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - it("should track 5 events in a row and have correct balance", async () => { - let startingBalance = userItem.included_usage; - await autumn.attach({ - customer_id: customerId, - product_id: free.id, - }); - - const promises = []; - for (let i = 0; i < 2; i++) { - console.log("--------------------------------"); - console.log(`Cycle ${i}`); - console.log(`Starting balance: ${startingBalance}`); - const values = []; - for (let i = 0; i < 10; i++) { - const randomVal = - Math.floor(Math.random() * 5) * (Math.random() < 0.3 ? -1 : 1); - promises.push( - autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: randomVal, - }), - ); - startingBalance -= randomVal; - values.push(randomVal); - } - - console.log(`New balance: ${startingBalance}`); - - const results = await Promise.all(promises); - - await timeout(10000); - - const customer = await autumn.customers.get(customerId); - const userFeature = customer.features[TestFeature.Users]; - if (userFeature.balance != startingBalance) { - for (let i = 0; i < values.length; i++) { - console.log(`Value: ${values[i]}, Event ID: ${results[i].id}`); - } - } - expect(userFeature.balance).to.equal(startingBalance); - } - }); -}); diff --git a/server/tests/core/cancel/cancel1.test.ts b/server/tests/core/cancel/cancel1.test.ts deleted file mode 100644 index cc5f8451d..000000000 --- a/server/tests/core/cancel/cancel1.test.ts +++ /dev/null @@ -1,276 +0,0 @@ -// import chalk from "chalk"; -// import { setupBefore } from "tests/before.js"; -// import { Stripe } from "stripe"; -// import { createProducts } from "tests/utils/productUtils.js"; -// import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -// import { TestFeature } from "tests/setup/v2Features.js"; -// import { AutumnInt } from "@/external/autumn/autumnCli.js"; -// import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -// import { -// LegacyVersion, -// AppEnv, -// CusProductStatus, -// Organization, -// priceToInvoiceAmount, -// Proration, -// } from "@autumn/shared"; -// import { -// constructArrearItem, -// constructArrearProratedItem, -// constructPrepaidItem, -// } from "@/utils/scriptUtils/constructItem.js"; -// import { DrizzleCli } from "@/db/initDrizzle.js"; -// import { -// addPrefixToProducts, -// getBasePrice, -// } from "tests/utils/testProductUtils/testProductUtils.js"; -// import { expect } from "chai"; -// import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; -// import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -// import { advanceTestClock } from "tests/utils/stripeUtils.js"; -// import { addWeeks } from "date-fns"; -// import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; -// import { formatUnixToDate, timeout } from "@/utils/genUtils.js"; -// import { CusService } from "@/internal/customers/CusService.js"; -// import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; -// import { isPrepaidPrice } from "@shared/utils/productUtils/priceUtils.js"; -// import { isContUsePrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js"; -// import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; -// import { Decimal } from "decimal.js"; - -// let premium = constructProduct({ -// id: "premium", -// items: [ -// constructArrearItem({ featureId: TestFeature.Words }), -// constructPrepaidItem({ featureId: TestFeature.Messages }), -// constructArrearProratedItem({ featureId: TestFeature.Users }), -// ], -// type: "premium", -// }); - -// const creditsQuantity = 500; -// const usersOverage = 1; -// const wordsUsage = 300000; -// const ops = [ -// { -// entityId: "1", -// product: premium, -// results: [{ product: premium, status: CusProductStatus.Active }], -// options: [ -// { -// feature_id: TestFeature.Messages, -// quantity: creditsQuantity, -// }, -// ], -// usage: [ -// { -// featureId: TestFeature.Users, -// value: usersOverage + 1, -// }, -// ], -// }, -// ]; - -// const testCase = "cancel1"; -// describe(`${chalk.yellowBright("cancel1: Testing cancelling singular product")}`, () => { -// let customerId = testCase; -// let autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - -// let stripeCli: Stripe; -// let testClockId: string; -// let curUnix: number; -// let db: DrizzleCli; -// let org: Organization; -// let env: AppEnv; - -// before(async function () { -// await setupBefore(this); -// const { autumnJs } = this; -// db = this.db; -// org = this.org; -// env = this.env; - -// stripeCli = this.stripeCli; - -// addPrefixToProducts({ -// products: [premium], -// prefix: testCase, -// }); - -// await createProducts({ -// autumn: autumnJs, -// products: [premium], -// db, -// orgId: org.id, -// env, -// customerId, -// }); - -// const { testClockId: testClockId1 } = await initCustomer({ -// autumn: autumnJs, -// customerId, -// db, -// org, -// env, -// attachPm: "success", -// }); - -// testClockId = testClockId1!; -// }); - -// const entities = [ -// { -// id: "1", -// name: "Entity 1", -// feature_id: TestFeature.Users, -// }, -// { -// id: "2", -// name: "Entity 2", -// feature_id: TestFeature.Users, -// }, -// ]; - -// it("should run operations", async function () { -// await autumn.entities.create(customerId, entities); - -// for (let index = 0; index < ops.length; index++) { -// const op = ops[index]; -// try { -// await attachAndExpectCorrect({ -// autumn, -// customerId, -// product: op.product, -// stripeCli, -// db, -// org, -// env, -// options: op.options, -// usage: op.usage, -// }); -// } catch (error) { -// console.log( -// `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}` -// ); -// throw error; -// } -// } -// }); - -// it("should advance test clock and upgrade entity 1 to premium, and have correct invoice", async function () { -// const cus1 = await autumn.customers.get(customerId); -// const prod = cus1.products.find((p) => p.id === premium.id); -// const proration = { -// start: prod?.current_period_start!, -// end: prod?.current_period_end!, -// }; - -// await autumn.track({ -// customer_id: customerId, -// feature_id: TestFeature.Words, -// value: wordsUsage, -// }); - -// await timeout(3000); - -// curUnix = await advanceTestClock({ -// stripeCli, -// testClockId, -// advanceTo: addWeeks(Date.now(), 2).getTime(), -// waitForSeconds: 30, -// }); - -// await autumn.cancel({ -// customer_id: customerId, -// product_id: premium.id, -// cancel_immediately: true, -// // @ts-expect-error -// prorate: true, -// }); - -// // 1. Get full customer -// const fullCus = await CusService.getFull({ -// db, -// orgId: org.id, -// env, -// idOrInternalId: customerId, -// inStatuses: [CusProductStatus.Expired, CusProductStatus.Active], -// }); - -// // 2. Calculate base price proration -// const basePrice = getBasePrice({ product: premium }); -// const baseProration = calculateProrationAmount({ -// periodStart: proration.start, -// periodEnd: proration.end, -// now: curUnix, -// amount: basePrice, -// allowNegative: true, -// }); - -// const cusProduct = fullCus.customer_products.find( -// (cusProduct) => cusProduct.product.id === premium.id -// ); - -// // 3. Calculate prepaid and cont use prices -// const prices = cusProductToPrices({ cusProduct: cusProduct! }); -// const creditsPrice = prices.find((price) => isPrepaidPrice({ price })); -// const usersPrice = prices.find((price) => isContUsePrice({ price })); - -// const creditsPriceAmount = priceToInvoiceAmount({ -// price: creditsPrice!, -// quantity: creditsQuantity, -// proration, -// now: curUnix, -// }); - -// const usersPriceAmount = priceToInvoiceAmount({ -// price: usersPrice!, -// overage: usersOverage, -// proration, -// now: curUnix, -// }); - -// // 4. Calculate words amount -// const wordsAmount = await getExpectedInvoiceTotal({ -// db, -// org, -// env, -// onlyIncludeArrear: true, -// usage: [ -// { -// featureId: TestFeature.Words, -// value: wordsUsage, -// }, -// ], -// stripeCli, -// customerId, -// productId: premium.id, -// expectExpired: true, -// }); - -// const totalPrice = new Decimal(wordsAmount) -// .minus(baseProration) -// .minus(creditsPriceAmount) -// .minus(usersPriceAmount) -// .toDecimalPlaces(2) -// .toNumber(); - -// // console.log("BASE PRORATION", baseProration); -// // console.log("CREDITS PRORATION", creditsPriceAmount); -// // console.log("USERS PRORATION", usersPriceAmount); -// // console.log("WORDS AMOUNT", wordsAmount); -// // console.log("TOTAL PRICE", totalPrice); - -// // Get upcoming invoice -// await timeout(5000); // for webhook to trigger -// const upcomingInvoices = await stripeCli.invoices.list({ -// customer: fullCus.processor?.id, -// limit: 1, -// status: "draft", -// }); -// // console.log("INVOICE TOTAL", upcomingInvoices.data[0].total); -// // console.log("INVOICE ID", upcomingInvoices.data[0].id); - -// expect(upcomingInvoices.data[0].total).to.equal(totalPrice * 100); -// }); -// }); diff --git a/server/tests/crud/customers/create-customer1.test.ts b/server/tests/crud/customers/create-customer1.test.ts new file mode 100644 index 000000000..b0c9eecb9 --- /dev/null +++ b/server/tests/crud/customers/create-customer1.test.ts @@ -0,0 +1,63 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { ApiVersion, CusExpand } from "@autumn/shared"; +import chalk from "chalk"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; + +const testCase = "create-customer1"; +const customerId = testCase; + +describe(`${chalk.yellowBright("create-customer1: Testing create customer")}`, () => { + const autumnV1 = new AutumnInt({ + secretKey: ctx.orgSecretKey, + version: ApiVersion.V1_2, + }); + + beforeAll(async () => { + try { + await autumnV1.customers.delete(customerId); + } catch {} + }); + + test("should create customer with expand params", async () => { + const data = await autumnV1.customers.create({ + id: customerId, + name: customerId, + email: `${customerId}@example.com`, + withAutumnId: false, + }); + + expect(data.id).toBe(customerId); + expect(data.name).toBe(customerId); + expect(data.email).toBe(`${customerId}@example.com`); + expect(data.autumn_id).toBeUndefined(); + }); + + test("should return customer when call again", async () => { + const data = await autumnV1.customers.create({ + id: customerId, + name: customerId, + email: `${customerId}@example.com`, + withAutumnId: false, + }); + + expect(data.id).toBe(customerId); + expect(data.name).toBe(customerId); + expect(data.email).toBe(`${customerId}@example.com`); + expect(data.autumn_id).toBeUndefined(); + }); + + test("should return expanded params if provided", async () => { + const data = await autumnV1.customers.create({ + id: customerId, + name: customerId, + email: `${customerId}@example.com`, + withAutumnId: false, + expand: [CusExpand.Invoices, CusExpand.TrialsUsed, CusExpand.Entities], + }); + + expect(data.invoices).toEqual([]); + expect(data.trials_used).toEqual([]); + expect(data.entities).toEqual([]); + }); +}); diff --git a/server/tests/interval/multiSub/multiSubInterval1.test.ts b/server/tests/interval/multiSub/multiSubInterval1.test.ts index 4a17ab3e4..442410bae 100644 --- a/server/tests/interval/multiSub/multiSubInterval1.test.ts +++ b/server/tests/interval/multiSub/multiSubInterval1.test.ts @@ -35,6 +35,12 @@ describe(`${chalk.yellowBright("multiSubInterval1: Should attach pro and pro ann let testClockId: string; beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro, proAnnual], + prefix: testCase, + customerId, + }); const { testClockId: testClockId1 } = await initCustomerV3({ ctx, customerId, @@ -43,13 +49,6 @@ describe(`${chalk.yellowBright("multiSubInterval1: Should attach pro and pro ann }); testClockId = testClockId1!; - - await initProductsV0({ - ctx, - products: [pro, proAnnual], - prefix: testCase, - customerId, - }); }); const entities = [ diff --git a/server/tests/interval/multiSub/multiSubInterval2.test.ts b/server/tests/interval/multiSub/multiSubInterval2.test.ts index 18f60aaa8..d1898d796 100644 --- a/server/tests/interval/multiSub/multiSubInterval2.test.ts +++ b/server/tests/interval/multiSub/multiSubInterval2.test.ts @@ -1,5 +1,5 @@ -import { LegacyVersion } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion } from "@autumn/shared"; import chalk from "chalk"; import { addMonths, addYears, differenceInDays } from "date-fns"; import { TestFeature } from "tests/setup/v2Features.js"; @@ -9,9 +9,9 @@ import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js"; import { toMilliseconds } from "@/utils/timeUtils.js"; const pro = constructProduct({ @@ -36,21 +36,20 @@ describe(`${chalk.yellowBright("multiSubInterval2: Should attach pro and pro ann let curUnix: number; beforeAll(async () => { - const { testClockId: testClockId1 } = await initCustomerV3({ - ctx, - customerId, - attachPm: "success", - withTestClock: true, - }); - - testClockId = testClockId1!; - await initProductsV0({ ctx, products: [pro, proAnnual], prefix: testCase, customerId, }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + attachPm: "success", + }); + + testClockId = testClockId1!; }); const entities = [ @@ -94,8 +93,9 @@ describe(`${chalk.yellowBright("multiSubInterval2: Should attach pro and pro ann }); expect(checkoutRes.next_cycle).toBeDefined(); - const expectedDate = addYears(curUnix, 1).getTime(); + const expectedDate = addYears(Date.now(), 1).getTime(); const actualDate = checkoutRes.next_cycle?.starts_at!; + const daysDiff = Math.abs(differenceInDays(expectedDate, actualDate)); expect(daysDiff).toBeLessThanOrEqual(1); diff --git a/server/tests/interval/multiSub/multiSubInterval2.test.ts.backup b/server/tests/interval/multiSub/multiSubInterval2.test.ts.backup deleted file mode 100644 index 1f744921e..000000000 --- a/server/tests/interval/multiSub/multiSubInterval2.test.ts.backup +++ /dev/null @@ -1,151 +0,0 @@ -import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import { addMonths, addYears, differenceInDays } from "date-fns"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js"; -import { toMilliseconds } from "@/utils/timeUtils.js"; - -const pro = constructProduct({ - id: "pro", - items: [constructFeatureItem({ featureId: TestFeature.Words })], - type: "pro", -}); - -const proAnnual = constructProduct({ - id: "proAnnual", - items: [constructFeatureItem({ featureId: TestFeature.Words })], - type: "pro", - isAnnual: true, -}); - -const testCase = "multiSubInterval2"; -describe(`${chalk.yellowBright("multiSubInterval2: Should attach pro and pro annual to entity mid cycle and have correct next cycle at")}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - - let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; - let db: DrizzleCli; - let org: Organization; - let env: AppEnv; - - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, proAnnual], - prefix: testCase, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, proAnnual], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - const entities = [ - { - id: "1", - name: "entity1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "entity2", - feature_id: TestFeature.Users, - }, - ]; - - it("should attach pro and advance test clock", async () => { - await autumn.entities.create(customerId, entities); - - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addMonths(new Date(), 1.5).getTime(), - }); - }); - - it("should attach pro annual to entity 2 and have correct next cycle at", async () => { - const checkoutRes = await autumn.checkout({ - customer_id: customerId, - product_id: proAnnual.id, - entity_id: entities[1].id, - }); - - expect(checkoutRes.next_cycle).to.exist; - expect(checkoutRes.next_cycle?.starts_at).to.approximately( - addYears(new Date(), 1).getTime(), - toMilliseconds.days(1), // +- 1 day - ); - - await autumn.attach({ - customer_id: customerId, - product_id: proAnnual.id, - entity_id: entities[1].id, - }); - - const sub = await getCusSub({ - db, - org, - customerId, - productId: proAnnual.id, - }); - - const periodEndExists = sub!.items.data.some( - (item) => - Math.abs( - differenceInDays( - item.current_period_end * 1000, - checkoutRes.next_cycle?.starts_at!, - ), - ) < 1, - ); - - expect(periodEndExists).to.be.true; - }); -}); diff --git a/server/tests/interval/multiSub/multiSubInterval3.test.ts b/server/tests/interval/multiSub/multiSubInterval3.test.ts index 6e402a96c..82e6c5df7 100644 --- a/server/tests/interval/multiSub/multiSubInterval3.test.ts +++ b/server/tests/interval/multiSub/multiSubInterval3.test.ts @@ -1,5 +1,5 @@ -import { LegacyVersion } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion } from "@autumn/shared"; import chalk from "chalk"; import { addMonths, addYears, differenceInDays } from "date-fns"; import { TestFeature } from "tests/setup/v2Features.js"; @@ -12,9 +12,9 @@ import { constructFeatureItem, } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js"; import { toMilliseconds } from "@/utils/timeUtils.js"; const pro = constructProduct({ @@ -41,6 +41,13 @@ describe(`${chalk.yellowBright("multiSubInterval3: Should attach pro and pro ann let testClockId: string; beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro, proAnnual], + prefix: testCase, + customerId, + }); + const { testClockId: testClockId1 } = await initCustomerV3({ ctx, customerId, @@ -49,13 +56,6 @@ describe(`${chalk.yellowBright("multiSubInterval3: Should attach pro and pro ann }); testClockId = testClockId1!; - - await initProductsV0({ - ctx, - products: [pro, proAnnual], - prefix: testCase, - customerId, - }); }); const entities = [ diff --git a/server/tests/interval/multiSub/multiSubInterval3.test.ts.backup b/server/tests/interval/multiSub/multiSubInterval3.test.ts.backup deleted file mode 100644 index 43633c589..000000000 --- a/server/tests/interval/multiSub/multiSubInterval3.test.ts.backup +++ /dev/null @@ -1,157 +0,0 @@ -import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import { addMonths, addYears, differenceInDays } from "date-fns"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { - constructArrearItem, - constructFeatureItem, -} from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js"; -import { toMilliseconds } from "@/utils/timeUtils.js"; - -const pro = constructProduct({ - id: "pro", - items: [constructFeatureItem({ featureId: TestFeature.Words })], - type: "pro", -}); - -const proAnnual = constructProduct({ - id: "proAnnual", - items: [ - constructArrearItem({ featureId: TestFeature.Credits }), - constructFeatureItem({ featureId: TestFeature.Words }), - ], - type: "pro", - isAnnual: true, -}); - -const testCase = "multiSubInterval3"; -describe(`${chalk.yellowBright("multiSubInterval3: Should attach pro and pro annual (with monthly usage price) to entity mid cycle and have correct next cycle at")}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - - let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; - let db: DrizzleCli; - let org: Organization; - let env: AppEnv; - - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, proAnnual], - prefix: testCase, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, proAnnual], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - const entities = [ - { - id: "1", - name: "entity1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "entity2", - feature_id: TestFeature.Users, - }, - ]; - - it("should attach pro and advance test clock", async () => { - await autumn.entities.create(customerId, entities); - - await attachAndExpectCorrect({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addMonths(new Date(), 1.5).getTime(), - }); - }); - - it("should attach pro annual to entity 2 and have correct next cycle at", async () => { - const checkoutRes = await autumn.checkout({ - customer_id: customerId, - product_id: proAnnual.id, - entity_id: entities[1].id, - }); - - expect(checkoutRes.next_cycle).to.exist; - expect(checkoutRes.next_cycle?.starts_at).to.approximately( - addYears(new Date(), 1).getTime(), - toMilliseconds.days(1), // +- 1 day - ); - - await autumn.attach({ - customer_id: customerId, - product_id: proAnnual.id, - entity_id: entities[1].id, - }); - - const sub = await getCusSub({ - db, - org, - customerId, - productId: proAnnual.id, - }); - - const periodEndExists = sub!.items.data.some( - (item) => - Math.abs( - differenceInDays( - item.current_period_end * 1000, - checkoutRes.next_cycle?.starts_at!, - ), - ) < 1, - ); - - expect(periodEndExists).to.be.true; - }); -}); diff --git a/server/tests/interval/upgrade/interval1.test.ts b/server/tests/interval/upgrade/interval1.test.ts index 97dc12bbe..f2d850eba 100644 --- a/server/tests/interval/upgrade/interval1.test.ts +++ b/server/tests/interval/upgrade/interval1.test.ts @@ -1,8 +1,7 @@ -import { LegacyVersion } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion } from "@autumn/shared"; import chalk from "chalk"; import { addWeeks, addYears } from "date-fns"; -import { defaultApiVersion } from "tests/constants.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; @@ -10,9 +9,9 @@ import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js"; import { toMilliseconds } from "@/utils/timeUtils.js"; const pro = constructProduct({ @@ -36,21 +35,20 @@ describe(`${chalk.yellowBright("interval1: Should upgrade from pro to pro annual let testClockId: string; beforeAll(async () => { - const { testClockId: testClockId1 } = await initCustomerV3({ - ctx, - customerId, - attachPm: "success", - withTestClock: true, - }); - - testClockId = testClockId1!; - await initProductsV0({ ctx, products: [pro, proAnnual], prefix: testCase, customerId, }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + attachPm: "success", + }); + + testClockId = testClockId1!; }); test("should attach pro and advance test clock", async () => { @@ -70,6 +68,7 @@ describe(`${chalk.yellowBright("interval1: Should upgrade from pro to pro annual advanceTo: addWeeks(new Date(), 2).getTime(), }); }); + return; test("should upgrade to pro annual and have correct next cycle at", async () => { const checkoutRes = await autumn.checkout({ diff --git a/server/tests/interval/upgrade/interval2.test.ts b/server/tests/interval/upgrade/interval2.test.ts index 2a00856a6..c024d800a 100644 --- a/server/tests/interval/upgrade/interval2.test.ts +++ b/server/tests/interval/upgrade/interval2.test.ts @@ -1,5 +1,5 @@ -import { LegacyVersion } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion } from "@autumn/shared"; import chalk from "chalk"; import { addMonths, addWeeks, addYears } from "date-fns"; import { TestFeature } from "tests/setup/v2Features.js"; @@ -9,9 +9,9 @@ import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js"; import { toMilliseconds } from "@/utils/timeUtils.js"; const pro = constructProduct({ @@ -35,6 +35,13 @@ describe(`${chalk.yellowBright("interval2: Should upgrade from pro to pro annual let testClockId: string; beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro, proAnnual], + prefix: testCase, + customerId, + }); + const { testClockId: testClockId1 } = await initCustomerV3({ ctx, customerId, @@ -43,13 +50,6 @@ describe(`${chalk.yellowBright("interval2: Should upgrade from pro to pro annual }); testClockId = testClockId1!; - - await initProductsV0({ - ctx, - products: [pro, proAnnual], - prefix: testCase, - customerId, - }); }); test("should attach pro and advance test clock", async () => { diff --git a/server/tests/interval/upgrade/interval3.test.ts b/server/tests/interval/upgrade/interval3.test.ts index 9b1f98787..46ddfd19f 100644 --- a/server/tests/interval/upgrade/interval3.test.ts +++ b/server/tests/interval/upgrade/interval3.test.ts @@ -1,5 +1,5 @@ -import { LegacyVersion } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion } from "@autumn/shared"; import chalk from "chalk"; import { addDays } from "date-fns"; import { TestFeature } from "tests/setup/v2Features.js"; @@ -9,9 +9,9 @@ import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js"; import { toMilliseconds } from "@/utils/timeUtils.js"; const pro = constructProduct({ @@ -37,6 +37,13 @@ describe(`${chalk.yellowBright("interval3: Should upgrade from pro trial to prem let curUnix: number; beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro, premium], + prefix: testCase, + customerId, + }); + const { testClockId: testClockId1 } = await initCustomerV3({ ctx, customerId, @@ -45,13 +52,6 @@ describe(`${chalk.yellowBright("interval3: Should upgrade from pro trial to prem }); testClockId = testClockId1!; - - await initProductsV0({ - ctx, - products: [pro, premium], - prefix: testCase, - customerId, - }); }); test("should attach pro and advance test clock", async () => { diff --git a/server/tests/merged/add/mergedAdd1.test.ts b/server/tests/merged/add/mergedAdd1.test.ts index 3dec6c89c..490a1d4aa 100644 --- a/server/tests/merged/add/mergedAdd1.test.ts +++ b/server/tests/merged/add/mergedAdd1.test.ts @@ -1,24 +1,21 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; -import { expect } from "chai"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; import { getAttachPreviewTotal } from "tests/utils/testAttachUtils/getAttachPreviewTotal.js"; import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; -import { - addPrefixToProducts, - getBasePrice, -} from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { expectSubToBeCorrect } from "../mergeUtils.test.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; // UNCOMMENT FROM HERE const pro = constructProduct({ @@ -38,39 +35,26 @@ describe(`${chalk.yellowBright("mergedAdd1: Testing merged subs, with track")}`, let org: Organization; let env: AppEnv; - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ + beforeAll(async () => { + await initProductsV0({ + ctx, products: [pro], prefix: customerId, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro], - db, - orgId: org.id, - env, customerId, }); - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, + const res = await initCustomerV3({ + ctx, customerId, - db, - org, - env, attachPm: "success", + withTestClock: true, }); - testClockId = testClockId1!; + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; + testClockId = res.testClockId!; }); const entities = [ @@ -86,7 +70,7 @@ describe(`${chalk.yellowBright("mergedAdd1: Testing merged subs, with track")}`, }, ]; - it("should attach pro product", async () => { + test("should attach pro product", async () => { await autumn.entities.create(customerId, entities); await autumn.attach({ @@ -110,7 +94,7 @@ describe(`${chalk.yellowBright("mergedAdd1: Testing merged subs, with track")}`, const customer = await autumn.customers.get(customerId); const invoice = customer.invoices; - expect(invoice[0].total).to.equal(expectedTotal); + expect(invoice[0].total).toBe(expectedTotal); await expectSubToBeCorrect({ db, @@ -120,7 +104,7 @@ describe(`${chalk.yellowBright("mergedAdd1: Testing merged subs, with track")}`, }); }); - it("should track usage and have correct invoice end of month", async () => { + test("should track usage and have correct invoice end of month", async () => { const value1 = 110000; const value2 = 310000; const values = [value1, value2]; @@ -164,6 +148,6 @@ describe(`${chalk.yellowBright("mergedAdd1: Testing merged subs, with track")}`, const customer = await autumn.customers.get(customerId); const invoice = customer.invoices; - expect(invoice[0].total).to.equal(basePrice * 2 + total); + expect(invoice[0].total).toBe(basePrice * 2 + total); }); }); diff --git a/server/tests/merged/add/mergedAdd2.test.ts b/server/tests/merged/add/mergedAdd2.test.ts deleted file mode 100644 index b76ee34aa..000000000 --- a/server/tests/merged/add/mergedAdd2.test.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -// UNCOMMENT FROM HERE -const premium = constructProduct({ - id: "premium", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "premium", -}); -const pro = constructProduct({ - id: "pro", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "pro", -}); - -const testCase = "mergedAdd2"; -describe(`${chalk.yellowBright(`${testCase}: Testing merged subs, downgrade`)}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - - let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; - let db: DrizzleCli; - let org: Organization; - let env: AppEnv; - - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [premium, pro], - prefix: testCase, - }); - - await createProducts({ - autumn: autumnJs, - products: [premium, pro], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - it("should attach pro product", async () => { - await autumn.entities.create(customerId, entities); - - await autumn.attach({ - customer_id: customerId, - product_id: premium.id, - entity_id: "1", - }); - - await autumn.attach({ - customer_id: customerId, - product_id: premium.id, - entity_id: "2", - }); - await autumn.attach({ - customer_id: customerId, - product_id: pro.id, - entity_id: "2", - }); - // await autumn.attach({ - // customer_id: customerId, - // product_id: pro.id, - // entity_id: "2", - // }); - - // const customer = await autumn.customers.get(customerId); - // const invoice = customer.invoices; - - // await expectSubToBeCorrect({ - // db, - // customerId, - // org, - // env, - // }); - }); - - return; - - it("should track usage and have correct invoice end of month", async () => { - const value1 = 110000; - const value2 = 310000; - const values = [value1, value2]; - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Words, - value: value1, - entity_id: "1", - }); - - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Words, - value: value2, - entity_id: "2", - }); - - // await timeout(3000); - - // await advanceToNextInvoice({ - // stripeCli, - // testClockId, - // }); - - // let total = 0; - // for (let i = 0; i < entities.length; i++) { - // const expectedTotal = await getExpectedInvoiceTotal({ - // customerId, - // productId: pro.id, - // usage: [{ featureId: TestFeature.Words, value: values[i] }], - // onlyIncludeUsage: true, - // stripeCli, - // db, - // org, - // env, - // }); - // total += expectedTotal; - // } - - // const basePrice = getBasePrice({ product: pro }); - - // const customer = await autumn.customers.get(customerId); - // const invoice = customer.invoices; - // expect(invoice[0].total).to.equal(basePrice * 2 + total); - }); -}); - -// const expectedTotal = await getAttachPreviewTotal({ -// customerId, -// productId: pro.id, -// entityId: "2", -// }); diff --git a/server/tests/merged/add/mergedAdd3.test.ts b/server/tests/merged/add/mergedAdd3.test.ts index 60e9a109f..b3a68cfb5 100644 --- a/server/tests/merged/add/mergedAdd3.test.ts +++ b/server/tests/merged/add/mergedAdd3.test.ts @@ -1,3 +1,4 @@ +import { beforeAll, describe, test } from "bun:test"; import { type AppEnv, CusProductStatus, @@ -6,17 +7,16 @@ import { } from "@autumn/shared"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; // UNCOMMENT FROM HERE const premium = constructProduct({ @@ -62,47 +62,10 @@ describe(`${chalk.yellowBright(`${testCase}: Testing scheduled, and merged add t const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; let db: DrizzleCli; let org: Organization; let env: AppEnv; - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [premium, pro], - prefix: testCase, - }); - - await createProducts({ - autumn: autumnJs, - products: [premium, pro], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - const entities = [ { id: "1", @@ -121,11 +84,32 @@ describe(`${chalk.yellowBright(`${testCase}: Testing scheduled, and merged add t }, ]; - it("should run operations", async () => { - await autumn.entities.create(customerId, entities); + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [premium, pro], + prefix: testCase, + customerId, + }); - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; + await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; + + await autumn.entities.create(customerId, entities); + }); + + for (const op of ops) { + test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => { await attachAndExpectCorrect({ autumn, customerId, @@ -146,6 +130,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing scheduled, and merged add t status: result.status, }); } - } - }); + }); + } }); diff --git a/server/tests/merged/downgrade/mergedDowngrade1.backup.ts b/server/tests/merged/downgrade/mergedDowngrade1.backup.ts deleted file mode 100644 index 66306eff2..000000000 --- a/server/tests/merged/downgrade/mergedDowngrade1.backup.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { - type AppEnv, - CusProductStatus, - LegacyVersion, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; - -// OPERATIONS: -// Premium, Premium -// Pro, Pro -// Premium, Premium - -// UNCOMMENT FROM HERE -const premium = constructProduct({ - id: "premium", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "premium", -}); - -const pro = constructProduct({ - id: "pro", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "pro", -}); - -const init = [ - { entityId: "1", product: premium }, // upgrade to premium - { entityId: "2", product: premium }, // upgrade to premium -]; - -const ops1 = [ - { - entityId: "1", - product: pro, - results: [ - { product: premium, status: CusProductStatus.Active }, - { product: pro, status: CusProductStatus.Scheduled }, - ], - }, - { - entityId: "2", - product: pro, - results: [ - { product: premium, status: CusProductStatus.Active }, - { product: pro, status: CusProductStatus.Scheduled }, - ], - }, -]; - -// Renew -const ops2 = [ - { - entityId: "1", - product: premium, - results: [{ product: premium, status: CusProductStatus.Active }], - }, - { - entityId: "2", - product: premium, - results: [{ product: premium, status: CusProductStatus.Active }], - }, -]; - -describe(`${chalk.yellowBright("mergedDowngrade1: Testing merged subs, downgrade 2 pros")}`, () => { - const customerId = "mergedDowngrade1"; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - - let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; - let db: DrizzleCli; - let org: Organization; - let env: AppEnv; - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, premium], - prefix: customerId, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, premium], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - it("should attach pro product to both entities", async () => { - await autumn.entities.create(customerId, entities); - - for (const op of init) { - await autumn.attach({ - customer_id: customerId, - product_id: op.product.id, - entity_id: op.entityId, - }); - } - }); - - it("should downgrade both entities to pro and have correct sub + schedule", async () => { - for (const op of ops1) { - await autumn.attach({ - customer_id: customerId, - product_id: pro.id, - entity_id: op.entityId, - }); - - const entity = await autumn.entities.get(customerId, op.entityId); - for (const result of op.results) { - expectProductAttached({ - customer: entity, - product: result.product, - entityId: op.entityId, - }); - } - expect( - entity.products.filter((p: any) => p.group == premium.group).length, - ).to.equal(op.results.length); - - await expectSubToBeCorrect({ - db, - customerId, - org, - env, - }); - } - }); - - it("should renew both entities and have correct sub + schedule", async () => { - for (const op of ops2) { - await autumn.attach({ - customer_id: customerId, - product_id: op.product.id, - entity_id: op.entityId, - }); - - const entity = await autumn.entities.get(customerId, op.entityId); - for (const result of op.results) { - expectProductAttached({ - customer: entity, - product: result.product, - entityId: op.entityId, - }); - } - expect( - entity.products.filter((p: any) => p.group == premium.group).length, - ).to.equal(op.results.length); - - await expectSubToBeCorrect({ - db, - customerId, - org, - env, - }); - } - }); -}); diff --git a/server/tests/merged/downgrade/mergedDowngrade1.test.ts b/server/tests/merged/downgrade/mergedDowngrade1.test.ts index 90b94d11d..eed192a02 100644 --- a/server/tests/merged/downgrade/mergedDowngrade1.test.ts +++ b/server/tests/merged/downgrade/mergedDowngrade1.test.ts @@ -1,16 +1,15 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, CusProductStatus, LegacyVersion, type Organization, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; @@ -86,6 +85,19 @@ describe(`${chalk.yellowBright("mergedDowngrade1: Testing merged subs, downgrade let org: Organization; let env: AppEnv; + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + beforeAll(async () => { await initProductsV0({ ctx, @@ -107,35 +119,22 @@ describe(`${chalk.yellowBright("mergedDowngrade1: Testing merged subs, downgrade org = ctx.org; env = ctx.env; testClockId = res.testClockId!; + + await autumn.entities.create(customerId, entities); }); - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - test("should attach pro product to both entities", async () => { - await autumn.entities.create(customerId, entities); - - for (const op of init) { + for (const op of init) { + test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => { await autumn.attach({ customer_id: customerId, product_id: op.product.id, entity_id: op.entityId, }); - } - }); + }); + } - test("should downgrade both entities to pro and have correct sub + schedule", async () => { - for (const op of ops1) { + for (const op of ops1) { + test(`should downgrade entity ${op.entityId} to pro and have correct sub + schedule`, async () => { await autumn.attach({ customer_id: customerId, product_id: pro.id, @@ -151,7 +150,7 @@ describe(`${chalk.yellowBright("mergedDowngrade1: Testing merged subs, downgrade }); } expect( - entity.products.filter((p: any) => p.group == premium.group).length, + entity.products.filter((p: any) => p.group === premium.group).length, ).toBe(op.results.length); await expectSubToBeCorrect({ @@ -160,11 +159,11 @@ describe(`${chalk.yellowBright("mergedDowngrade1: Testing merged subs, downgrade org, env, }); - } - }); + }); + } - test("should renew both entities and have correct sub + schedule", async () => { - for (const op of ops2) { + for (const op of ops2) { + test(`should renew entity ${op.entityId} and have correct sub + schedule`, async () => { await autumn.attach({ customer_id: customerId, product_id: op.product.id, @@ -180,7 +179,7 @@ describe(`${chalk.yellowBright("mergedDowngrade1: Testing merged subs, downgrade }); } expect( - entity.products.filter((p: any) => p.group == premium.group).length, + entity.products.filter((p: any) => p.group === premium.group).length, ).toBe(op.results.length); await expectSubToBeCorrect({ @@ -189,6 +188,6 @@ describe(`${chalk.yellowBright("mergedDowngrade1: Testing merged subs, downgrade org, env, }); - } - }); + }); + } }); diff --git a/server/tests/merged/downgrade/mergedDowngrade2.backup.ts b/server/tests/merged/downgrade/mergedDowngrade2.backup.ts deleted file mode 100644 index 32fc26dea..000000000 --- a/server/tests/merged/downgrade/mergedDowngrade2.backup.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { - type AppEnv, - CusProductStatus, - LegacyVersion, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { - constructArrearItem, - constructFeatureItem, -} from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; - -// OPERATIONS: -// Premium -// Free -// Free, Premium -// Free, Pro - -const free = constructProduct({ - id: "free", - items: [constructFeatureItem({ featureId: TestFeature.Words })], - type: "free", - isDefault: false, -}); - -const premium = constructProduct({ - id: "premium", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "premium", -}); - -const pro = constructProduct({ - id: "pro", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "pro", -}); - -const ops = [ - { - entityId: "1", - product: premium, - results: [{ product: premium, status: CusProductStatus.Active }], - }, - { - entityId: "1", - product: free, - results: [ - { product: premium, status: CusProductStatus.Active }, - { product: free, status: CusProductStatus.Scheduled }, - ], - shouldBeCanceled: true, - }, - { - entityId: "2", - product: premium, - results: [{ product: premium, status: CusProductStatus.Active }], - }, - { - entityId: "2", - product: pro, - results: [ - { product: premium, status: CusProductStatus.Active }, - { product: pro, status: CusProductStatus.Scheduled }, - ], - }, -]; - -const testCase = "mergedDowngrade2"; -describe(`${chalk.yellowBright("mergedDowngrade2: Testing merged subs, downgrade free 1, add premium 2")}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - - let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; - let db: DrizzleCli; - let org: Organization; - let env: AppEnv; - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, premium, free], - prefix: testCase, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, premium, free], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - it("should run operations", async () => { - await autumn.entities.create(customerId, entities); - - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; - try { - await autumn.attach({ - customer_id: customerId, - product_id: op.product.id, - entity_id: op.entityId, - }); - - const entity = await autumn.entities.get(customerId, op.entityId); - for (const result of op.results) { - expectProductAttached({ - customer: entity, - product: result.product, - entityId: op.entityId, - }); - } - expect( - entity.products.filter((p: any) => p.group == premium.group).length, - ).to.equal(op.results.length); - - await expectSubToBeCorrect({ - db, - customerId, - org, - env, - shouldBeCanceled: op.shouldBeCanceled, - }); - } catch (error) { - console.log( - `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, - ); - throw error; - } - } - }); - // return; - - it("should advance test clock and have correct products for entity 1 & 2", async () => { - await advanceToNextInvoice({ - stripeCli, - testClockId, - }); - - const results = [ - { entityId: "1", product: free, status: CusProductStatus.Active }, - { entityId: "2", product: pro, status: CusProductStatus.Active }, - ]; - - for (const result of results) { - const entity = await autumn.entities.get(customerId, result.entityId); - expectProductAttached({ - customer: entity, - product: result.product, - status: result.status, - }); - - const products = entity.products.filter( - (p: any) => p.group == result.product.group, - ); - expect(products.length).to.equal(1); - } - - await expectSubToBeCorrect({ - db, - customerId, - org, - env, - }); - }); - - it("should attach premium to entity 1 (which is free) and have correct products", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: premium, - stripeCli, - db, - org, - env, - entityId: "1", - }); - }); -}); diff --git a/server/tests/merged/downgrade/mergedDowngrade2.test.ts b/server/tests/merged/downgrade/mergedDowngrade2.test.ts index 2c0e97349..714e00a9f 100644 --- a/server/tests/merged/downgrade/mergedDowngrade2.test.ts +++ b/server/tests/merged/downgrade/mergedDowngrade2.test.ts @@ -1,18 +1,17 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, CusProductStatus, LegacyVersion, type Organization, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { @@ -91,6 +90,19 @@ describe(`${chalk.yellowBright("mergedDowngrade2: Testing merged subs, downgrade let org: Organization; let env: AppEnv; + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + beforeAll(async () => { await initProductsV0({ ctx, @@ -112,26 +124,13 @@ describe(`${chalk.yellowBright("mergedDowngrade2: Testing merged subs, downgrade org = ctx.org; env = ctx.env; testClockId = res.testClockId!; + + await autumn.entities.create(customerId, entities); }); - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - test("should run operations", async () => { - await autumn.entities.create(customerId, entities); - - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => { try { await autumn.attach({ customer_id: customerId, @@ -148,7 +147,7 @@ describe(`${chalk.yellowBright("mergedDowngrade2: Testing merged subs, downgrade }); } expect( - entity.products.filter((p: any) => p.group == premium.group).length, + entity.products.filter((p: any) => p.group === premium.group).length, ).toBe(op.results.length); await expectSubToBeCorrect({ @@ -164,8 +163,8 @@ describe(`${chalk.yellowBright("mergedDowngrade2: Testing merged subs, downgrade ); throw error; } - } - }); + }); + } // return; test("should advance test clock and have correct products for entity 1 & 2", async () => { @@ -188,7 +187,7 @@ describe(`${chalk.yellowBright("mergedDowngrade2: Testing merged subs, downgrade }); const products = entity.products.filter( - (p: any) => p.group == result.product.group, + (p: any) => p.group === result.product.group, ); expect(products.length).toBe(1); } diff --git a/server/tests/merged/downgrade/mergedDowngrade3.backup.ts b/server/tests/merged/downgrade/mergedDowngrade3.backup.ts deleted file mode 100644 index f9ff2e621..000000000 --- a/server/tests/merged/downgrade/mergedDowngrade3.backup.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { - type AppEnv, - CusProductStatus, - LegacyVersion, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { - constructArrearItem, - constructFeatureItem, -} from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; - -// OPERATIONS: -// Pro, Pro -// Free, Premium - -const free = constructProduct({ - id: "free", - items: [constructFeatureItem({ featureId: TestFeature.Words })], - type: "free", - isDefault: false, -}); - -const premium = constructProduct({ - id: "premium", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "premium", -}); - -const pro = constructProduct({ - id: "pro", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "pro", -}); - -const ops = [ - { - entityId: "1", - product: pro, - results: [{ product: pro, status: CusProductStatus.Active }], - }, - { - entityId: "2", - product: pro, - results: [{ product: pro, status: CusProductStatus.Active }], - }, - { - entityId: "1", - product: free, - results: [ - { product: pro, status: CusProductStatus.Active }, - { product: free, status: CusProductStatus.Scheduled }, - ], - }, - { - entityId: "2", - product: premium, - results: [{ product: premium, status: CusProductStatus.Active }], - }, -]; - -const testCase = "mergedDowngrade3"; -describe(`${chalk.yellowBright("mergedDowngrade3: Testing merged subs, pro 1, pro 2, downgrade free pro 1, upgrade pro 2 ")}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - - let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; - let db: DrizzleCli; - let org: Organization; - let env: AppEnv; - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, premium, free], - prefix: testCase, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, premium, free], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - it("should run operations", async () => { - await autumn.entities.create(customerId, entities); - - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; - try { - await autumn.attach({ - customer_id: customerId, - product_id: op.product.id, - entity_id: op.entityId, - }); - - const entity = await autumn.entities.get(customerId, op.entityId); - for (const result of op.results) { - expectProductAttached({ - customer: entity, - product: result.product, - entityId: op.entityId, - }); - } - expect( - entity.products.filter((p: any) => p.group == premium.group).length, - ).to.equal(op.results.length); - - await expectSubToBeCorrect({ - db, - customerId, - org, - env, - }); - } catch (error) { - console.log( - `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, - ); - throw error; - } - } - }); -}); diff --git a/server/tests/merged/downgrade/mergedDowngrade3.test.ts b/server/tests/merged/downgrade/mergedDowngrade3.test.ts index d9ec317de..75b143e06 100644 --- a/server/tests/merged/downgrade/mergedDowngrade3.test.ts +++ b/server/tests/merged/downgrade/mergedDowngrade3.test.ts @@ -1,16 +1,15 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, CusProductStatus, LegacyVersion, type Organization, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { @@ -83,6 +82,19 @@ describe(`${chalk.yellowBright("mergedDowngrade3: Testing merged subs, pro 1, pr let org: Organization; let env: AppEnv; + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + beforeAll(async () => { await initProductsV0({ ctx, @@ -104,26 +116,13 @@ describe(`${chalk.yellowBright("mergedDowngrade3: Testing merged subs, pro 1, pr org = ctx.org; env = ctx.env; testClockId = res.testClockId!; + + await autumn.entities.create(customerId, entities); }); - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - test("should run operations", async () => { - await autumn.entities.create(customerId, entities); - - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => { try { await autumn.attach({ customer_id: customerId, @@ -140,7 +139,7 @@ describe(`${chalk.yellowBright("mergedDowngrade3: Testing merged subs, pro 1, pr }); } expect( - entity.products.filter((p: any) => p.group == premium.group).length, + entity.products.filter((p: any) => p.group === premium.group).length, ).toBe(op.results.length); await expectSubToBeCorrect({ @@ -155,6 +154,6 @@ describe(`${chalk.yellowBright("mergedDowngrade3: Testing merged subs, pro 1, pr ); throw error; } - } - }); + }); + } }); diff --git a/server/tests/merged/downgrade/mergedDowngrade4.backup.ts b/server/tests/merged/downgrade/mergedDowngrade4.backup.ts deleted file mode 100644 index 557484a01..000000000 --- a/server/tests/merged/downgrade/mergedDowngrade4.backup.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { - type AppEnv, - CusProductStatus, - LegacyVersion, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; - -// OPERATIONS: -// PremiumAnnual, Premium -// PremiumAnnual, Pro - -const premiumAnnual = constructProduct({ - id: "premiumAnnual", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "premium", - isAnnual: true, -}); - -const premium = constructProduct({ - id: "premium", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "premium", -}); - -const pro = constructProduct({ - id: "pro", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "pro", -}); - -const ops = [ - { - entityId: "1", - product: premiumAnnual, - results: [{ product: premiumAnnual, status: CusProductStatus.Active }], - }, - { - entityId: "2", - product: premium, - results: [{ product: premium, status: CusProductStatus.Active }], - }, - { - entityId: "2", - product: pro, - results: [ - { product: premium, status: CusProductStatus.Active }, - { product: pro, status: CusProductStatus.Scheduled }, - ], - }, -]; - -const testCase = "mergedDowngrade4"; -describe(`${chalk.yellowBright("mergedDowngrade4: Testing advance clock, schedule activates")}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - - let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; - let db: DrizzleCli; - let org: Organization; - let env: AppEnv; - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, premium, premiumAnnual], - prefix: testCase, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, premium, premiumAnnual], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - it("should run operations", async () => { - await autumn.entities.create(customerId, entities); - - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; - try { - await autumn.attach({ - customer_id: customerId, - product_id: op.product.id, - entity_id: op.entityId, - }); - - const entity = await autumn.entities.get(customerId, op.entityId); - for (const result of op.results) { - expectProductAttached({ - customer: entity, - product: result.product, - entityId: op.entityId, - }); - } - expect( - entity.products.filter((p: any) => p.group == premium.group).length, - ).to.equal(op.results.length); - - await expectSubToBeCorrect({ - db, - customerId, - org, - env, - }); - } catch (error) { - console.log( - `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, - ); - throw error; - } - } - }); - - it("should advance test clock and have correct premium downgraded for entity 2", async () => { - await advanceToNextInvoice({ - stripeCli, - testClockId, - }); - - // 1. Check that only - const results = [ - { - entityId: "1", - product: premiumAnnual, - status: CusProductStatus.Active, - }, - { entityId: "2", product: pro, status: CusProductStatus.Active }, - ]; - - for (const result of results) { - const entity = await autumn.entities.get(customerId, result.entityId); - expectProductAttached({ - customer: entity, - product: result.product, - status: result.status, - }); - - const products = entity.products.filter( - (p: any) => p.group == result.product.group, - ); - expect(products.length).to.equal(1); - } - }); -}); diff --git a/server/tests/merged/downgrade/mergedDowngrade4.test.ts b/server/tests/merged/downgrade/mergedDowngrade4.test.ts index 133e27df6..81479dc29 100644 --- a/server/tests/merged/downgrade/mergedDowngrade4.test.ts +++ b/server/tests/merged/downgrade/mergedDowngrade4.test.ts @@ -1,17 +1,16 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, CusProductStatus, LegacyVersion, type Organization, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; @@ -76,6 +75,19 @@ describe(`${chalk.yellowBright("mergedDowngrade4: Testing advance clock, schedul let org: Organization; let env: AppEnv; + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + beforeAll(async () => { await initProductsV0({ ctx, @@ -97,26 +109,13 @@ describe(`${chalk.yellowBright("mergedDowngrade4: Testing advance clock, schedul org = ctx.org; env = ctx.env; testClockId = res.testClockId!; + + await autumn.entities.create(customerId, entities); }); - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - test("should run operations", async () => { - await autumn.entities.create(customerId, entities); - - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => { try { await autumn.attach({ customer_id: customerId, @@ -133,7 +132,7 @@ describe(`${chalk.yellowBright("mergedDowngrade4: Testing advance clock, schedul }); } expect( - entity.products.filter((p: any) => p.group == premium.group).length, + entity.products.filter((p: any) => p.group === premium.group).length, ).toBe(op.results.length); await expectSubToBeCorrect({ @@ -148,8 +147,8 @@ describe(`${chalk.yellowBright("mergedDowngrade4: Testing advance clock, schedul ); throw error; } - } - }); + }); + } test("should advance test clock and have correct premium downgraded for entity 2", async () => { await advanceToNextInvoice({ @@ -176,7 +175,7 @@ describe(`${chalk.yellowBright("mergedDowngrade4: Testing advance clock, schedul }); const products = entity.products.filter( - (p: any) => p.group == result.product.group, + (p: any) => p.group === result.product.group, ); expect(products.length).toBe(1); } diff --git a/server/tests/merged/downgrade/mergedDowngrade5.test.ts b/server/tests/merged/downgrade/mergedDowngrade5.test.ts index 4bfafb940..02939a0fe 100644 --- a/server/tests/merged/downgrade/mergedDowngrade5.test.ts +++ b/server/tests/merged/downgrade/mergedDowngrade5.test.ts @@ -1,3 +1,4 @@ +import { beforeAll, describe, test } from "bun:test"; import { type AppEnv, CusProductStatus, @@ -6,12 +7,10 @@ import { } from "@autumn/shared"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { @@ -19,7 +18,8 @@ import { constructFeatureItem, } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; // OPERATIONS: // Premium, Premium @@ -94,47 +94,10 @@ describe(`${chalk.yellowBright("mergedDowngrade5: Testing downgrade to free")}`, const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; let db: DrizzleCli; let org: Organization; let env: AppEnv; - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, free, premium], - prefix: testCase, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, free, premium], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - const entities = [ { id: "1", @@ -148,12 +111,32 @@ describe(`${chalk.yellowBright("mergedDowngrade5: Testing downgrade to free")}`, }, ]; - it("should run operations", async () => { + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro, free, premium], + prefix: testCase, + customerId, + }); + + await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; + await autumn.entities.create(customerId, entities); + }); - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; - + for (const op of ops) { + test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => { await attachAndExpectCorrect({ autumn, customerId, @@ -175,9 +158,8 @@ describe(`${chalk.yellowBright("mergedDowngrade5: Testing downgrade to free")}`, status: result.status, }); } - } - }); - return; + }); + } // it("should advance test clock and have correct premium downgraded for entity 2", async function () { // await advanceToNextInvoice({ diff --git a/server/tests/merged/downgrade/mergedDowngrade6.test.ts b/server/tests/merged/downgrade/mergedDowngrade6.test.ts index dd4d6b97f..e01e603aa 100644 --- a/server/tests/merged/downgrade/mergedDowngrade6.test.ts +++ b/server/tests/merged/downgrade/mergedDowngrade6.test.ts @@ -1,3 +1,4 @@ +import { beforeAll, describe, test } from "bun:test"; import { type AppEnv, CusProductStatus, @@ -6,17 +7,16 @@ import { } from "@autumn/shared"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; // OPERATIONS: // Growth, Growth @@ -106,41 +106,6 @@ describe(`${chalk.yellowBright("mergedDowngrade6: Testing downgrade changes")}`, let org: Organization; let env: AppEnv; - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, free, premium, growth], - prefix: testCase, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, free, premium, growth], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - const entities = [ { id: "1", @@ -154,11 +119,33 @@ describe(`${chalk.yellowBright("mergedDowngrade6: Testing downgrade changes")}`, }, ]; - it("should run operations", async () => { - await autumn.entities.create(customerId, entities); + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro, free, premium, growth], + prefix: testCase, + customerId, + }); - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; + const res = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; + testClockId = res.testClockId!; + + await autumn.entities.create(customerId, entities); + }); + + for (const op of ops) { + test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => { await attachAndExpectCorrect({ autumn, customerId, @@ -179,6 +166,6 @@ describe(`${chalk.yellowBright("mergedDowngrade6: Testing downgrade changes")}`, status: result.status, }); } - } - }); + }); + } }); diff --git a/server/tests/merged/downgrade/mergedDowngrade8.backup.ts b/server/tests/merged/downgrade/mergedDowngrade8.backup.ts deleted file mode 100644 index b9ea55b4a..000000000 --- a/server/tests/merged/downgrade/mergedDowngrade8.backup.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { - type AppEnv, - CusProductStatus, - LegacyVersion, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; - -// UNCOMMENT FROM HERE -const premium = constructProduct({ - id: "premium", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "premium", -}); - -const premiumAnnual = constructProduct({ - id: "premiumAnnual", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "premium", - isAnnual: true, -}); - -const pro = constructProduct({ - id: "pro", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "pro", -}); - -// const init = [ -// { entityId: "1", product: premiumAnnual }, // upgrade to premium -// { entityId: "2", product: premium }, // upgrade to premium -// ]; - -const ops = [ - { - entityId: "1", - product: premiumAnnual, - results: [{ product: premiumAnnual, status: CusProductStatus.Active }], - }, - { - entityId: "2", - product: premium, - results: [{ product: premium, status: CusProductStatus.Active }], - }, - { - entityId: "1", - product: pro, - results: [ - { product: premiumAnnual, status: CusProductStatus.Active }, - { product: pro, status: CusProductStatus.Scheduled }, - ], - }, - { - entityId: "2", - product: pro, - results: [ - { product: premium, status: CusProductStatus.Active }, - { product: pro, status: CusProductStatus.Scheduled }, - ], - }, - { - entityId: "1", - product: premiumAnnual, - results: [{ product: premiumAnnual, status: CusProductStatus.Active }], - }, - { - entityId: "2", - product: premium, - results: [{ product: premium, status: CusProductStatus.Active }], - }, -]; - -const testCase = "mergedDowngrade8"; -describe(`${chalk.yellowBright("mergedDowngrade8: Testing merged subs, downgrade 2 monthly + annual")}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - - let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; - let db: DrizzleCli; - let org: Organization; - let env: AppEnv; - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, premium, premiumAnnual], - prefix: customerId, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, premium, premiumAnnual], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - it("should run operations", async () => { - await autumn.entities.create(customerId, entities); - - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; - try { - await autumn.attach({ - customer_id: customerId, - product_id: op.product.id, - entity_id: op.entityId, - }); - - const entity = await autumn.entities.get(customerId, op.entityId); - for (const result of op.results) { - expectProductAttached({ - customer: entity, - product: result.product, - entityId: op.entityId, - }); - } - expect( - entity.products.filter((p: any) => p.group == premium.group).length, - ).to.equal(op.results.length); - - await expectSubToBeCorrect({ - db, - customerId, - org, - env, - }); - } catch (error) { - console.log( - `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, - ); - throw error; - } - } - }); -}); diff --git a/server/tests/merged/downgrade/mergedDowngrade8.test.ts b/server/tests/merged/downgrade/mergedDowngrade8.test.ts index b1afa32dc..7529b3d0d 100644 --- a/server/tests/merged/downgrade/mergedDowngrade8.test.ts +++ b/server/tests/merged/downgrade/mergedDowngrade8.test.ts @@ -1,16 +1,15 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, CusProductStatus, LegacyVersion, type Organization, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; @@ -95,6 +94,19 @@ describe(`${chalk.yellowBright("mergedDowngrade8: Testing merged subs, downgrade let org: Organization; let env: AppEnv; + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + beforeAll(async () => { await initProductsV0({ ctx, @@ -116,26 +128,13 @@ describe(`${chalk.yellowBright("mergedDowngrade8: Testing merged subs, downgrade org = ctx.org; env = ctx.env; testClockId = res.testClockId!; + + await autumn.entities.create(customerId, entities); }); - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - test("should run operations", async () => { - await autumn.entities.create(customerId, entities); - - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => { try { await autumn.attach({ customer_id: customerId, @@ -152,7 +151,7 @@ describe(`${chalk.yellowBright("mergedDowngrade8: Testing merged subs, downgrade }); } expect( - entity.products.filter((p: any) => p.group == premium.group).length, + entity.products.filter((p: any) => p.group === premium.group).length, ).toBe(op.results.length); await expectSubToBeCorrect({ @@ -167,6 +166,6 @@ describe(`${chalk.yellowBright("mergedDowngrade8: Testing merged subs, downgrade ); throw error; } - } - }); + }); + } }); diff --git a/server/tests/merged/downgrade/mergedDowngrade9.backup.ts b/server/tests/merged/downgrade/mergedDowngrade9.backup.ts deleted file mode 100644 index e249ef7eb..000000000 --- a/server/tests/merged/downgrade/mergedDowngrade9.backup.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { - type AppEnv, - CusProductStatus, - LegacyVersion, - type Organization, -} from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -// UNCOMMENT FROM HERE -const premium = constructProduct({ - id: "premium", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "premium", -}); - -const premiumAnnual = constructProduct({ - id: "premiumAnnual", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "premium", - isAnnual: true, -}); - -const pro = constructProduct({ - id: "pro", - items: [constructArrearItem({ featureId: TestFeature.Words })], - type: "pro", -}); - -// const init = [ -// { entityId: "1", product: premiumAnnual }, // upgrade to premium -// { entityId: "2", product: premium }, // upgrade to premium -// ]; - -const ops = [ - { - entityId: "1", - product: premiumAnnual, - results: [{ product: premiumAnnual, status: CusProductStatus.Active }], - }, - { - entityId: "2", - product: premium, - results: [{ product: premium, status: CusProductStatus.Active }], - }, - { - entityId: "1", - product: pro, - results: [ - { product: premiumAnnual, status: CusProductStatus.Active }, - { product: pro, status: CusProductStatus.Scheduled }, - ], - }, - { - entityId: "2", - product: pro, - results: [ - { product: premium, status: CusProductStatus.Active }, - { product: pro, status: CusProductStatus.Scheduled }, - ], - }, -]; - -const testCase = "mergedDowngrade9"; -describe(`${chalk.yellowBright("mergedDowngrade9: Testing merged subs, downgrade 2 monthly + annual & advance test clock")}`, () => { - const customerId = testCase; - const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - - let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; - let db: DrizzleCli; - let org: Organization; - let env: AppEnv; - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, premium, premiumAnnual], - prefix: customerId, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, premium, premiumAnnual], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - it("should run operations", async () => { - await autumn.entities.create(customerId, entities); - - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; - try { - await attachAndExpectCorrect({ - autumn, - customerId, - product: op.product, - stripeCli, - db, - org, - env, - entityId: op.entityId, - }); - // await autumn.attach({ - // customer_id: customerId, - // product_id: op.product.id, - // entity_id: op.entityId, - // }); - // const entity = await autumn.entities.get(customerId, op.entityId); - // for (const result of op.results) { - // expectProductAttached({ - // customer: entity, - // product: result.product, - // entityId: op.entityId, - // }); - // } - // expect( - // entity.products.filter((p: any) => p.group == premium.group).length - // ).to.equal(op.results.length); - // await expectSubToBeCorrect({ - // db, - // customerId, - // org, - // env, - // }); - } catch (error) { - console.log( - `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, - ); - throw error; - } - } - }); - - it("should advance test clock and have correct products for entity 1 & 2", async () => { - const results = [ - { - entityId: "1", - products: [ - { product: premiumAnnual, status: CusProductStatus.Active }, - { product: pro, status: CusProductStatus.Scheduled }, - ], - }, - { - entityId: "2", - products: [{ product: pro, status: CusProductStatus.Active }], - }, - ]; - - await advanceToNextInvoice({ - stripeCli, - testClockId, - }); - - for (const result of results) { - const entity = await autumn.entities.get(customerId, result.entityId); - for (const product of result.products) { - expectProductAttached({ - customer: entity, - product: product.product, - status: product.status, - }); - } - const products = entity.products.filter( - (p: any) => p.group == premium.group, - ); - expect(products.length).to.equal(result.products.length); - } - }); - - it("should attach premium to entity 2 and have correct products", async () => { - await attachAndExpectCorrect({ - autumn, - customerId, - product: premium, - stripeCli, - db, - org, - env, - entityId: "2", - }); - }); -}); diff --git a/server/tests/merged/downgrade/mergedDowngrade9.test.ts b/server/tests/merged/downgrade/mergedDowngrade9.test.ts index d91335610..60c3f33a2 100644 --- a/server/tests/merged/downgrade/mergedDowngrade9.test.ts +++ b/server/tests/merged/downgrade/mergedDowngrade9.test.ts @@ -1,18 +1,17 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, CusProductStatus, LegacyVersion, type Organization, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; @@ -81,11 +80,23 @@ describe(`${chalk.yellowBright("mergedDowngrade9: Testing merged subs, downgrade let stripeCli: Stripe; let testClockId: string; - let curUnix: number; let db: DrizzleCli; let org: Organization; let env: AppEnv; + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + beforeAll(async () => { await initProductsV0({ ctx, @@ -107,26 +118,13 @@ describe(`${chalk.yellowBright("mergedDowngrade9: Testing merged subs, downgrade org = ctx.org; env = ctx.env; testClockId = res.testClockId!; + + await autumn.entities.create(customerId, entities); }); - const entities = [ - { - id: "1", - name: "Entity 1", - feature_id: TestFeature.Users, - }, - { - id: "2", - name: "Entity 2", - feature_id: TestFeature.Users, - }, - ]; - - test("should run operations", async () => { - await autumn.entities.create(customerId, entities); - - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => { try { await attachAndExpectCorrect({ autumn, @@ -138,36 +136,14 @@ describe(`${chalk.yellowBright("mergedDowngrade9: Testing merged subs, downgrade env, entityId: op.entityId, }); - // await autumn.attach({ - // customer_id: customerId, - // product_id: op.product.id, - // entity_id: op.entityId, - // }); - // const entity = await autumn.entities.get(customerId, op.entityId); - // for (const result of op.results) { - // expectProductAttached({ - // customer: entity, - // product: result.product, - // entityId: op.entityId, - // }); - // } - // expect( - // entity.products.filter((p: any) => p.group == premium.group).length - // ).to.equal(op.results.length); - // await expectSubToBeCorrect({ - // db, - // customerId, - // org, - // env, - // }); } catch (error) { console.log( `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, ); throw error; } - } - }); + }); + } test("should advance test clock and have correct products for entity 1 & 2", async () => { const results = [ @@ -199,7 +175,7 @@ describe(`${chalk.yellowBright("mergedDowngrade9: Testing merged subs, downgrade }); } const products = entity.products.filter( - (p: any) => p.group == premium.group, + (p: any) => p.group === premium.group, ); expect(products.length).toBe(result.products.length); } diff --git a/server/tests/merged/group/mergedGroup1.test.ts b/server/tests/merged/group/mergedGroup1.test.ts index 9fbbc056c..b4ff4e5d5 100644 --- a/server/tests/merged/group/mergedGroup1.test.ts +++ b/server/tests/merged/group/mergedGroup1.test.ts @@ -1,3 +1,4 @@ +import { beforeAll, describe, test } from "bun:test"; import { type AppEnv, CusProductStatus, @@ -6,17 +7,17 @@ import { } from "@autumn/shared"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { expectSubToBeCorrect } from "../mergeUtils.test.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; // UNCOMMENT FROM HERE const g1Pro = constructProduct({ @@ -86,47 +87,34 @@ describe(`${chalk.yellowBright("mergedGroup1: Testing products from diff groups" const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; let db: DrizzleCli; let org: Organization; let env: AppEnv; - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - await createProducts({ - autumn: autumnJs, + beforeAll(async () => { + await initProductsV0({ + ctx, products: [g1Pro, g2Pro, g1Premium, g2Premium], - db, - orgId: org.id, - env, + // prefix: customerId, customerId, }); - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, + await initCustomerV3({ + ctx, customerId, - db, - org, - env, + attachPm: "success", + withTestClock: true, }); - testClockId = testClockId1!; + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; }); - it("should attach pro product", async () => { - for (const op of ops) { - // console.log( - // `Op: ${op.product.id}, Other Products: ${op.otherProducts?.map((p) => p.id).join(", ")}`, - // ); + for (const op of ops) { + test(`should attach ${op.product.id}, other products: ${op.otherProducts?.map((p) => p.id).join(", ")}`, async () => { await attachAndExpectCorrect({ autumn, customerId, @@ -136,7 +124,7 @@ describe(`${chalk.yellowBright("mergedGroup1: Testing products from diff groups" db, org, env, - skipFeatureCheck: op.skipFeatureCheck, + // skipFeatureCheck: op.skipFeatureCheck, }); const customer = await autumn.customers.get(customerId); @@ -147,10 +135,10 @@ describe(`${chalk.yellowBright("mergedGroup1: Testing products from diff groups" status: result.status, }); } - } - }); + }); + } - it("should cancel scheduled product (g1Pro)", async () => { + test("should cancel scheduled product (g1Pro)", async () => { await autumn.cancel({ customer_id: customerId, product_id: g1Pro.id, diff --git a/server/tests/merged/group/mergedGroup2.test.ts b/server/tests/merged/group/mergedGroup2.test.ts index 4efd87394..ad8b56935 100644 --- a/server/tests/merged/group/mergedGroup2.test.ts +++ b/server/tests/merged/group/mergedGroup2.test.ts @@ -1,3 +1,4 @@ +import { beforeAll, describe, test } from "bun:test"; import { type AppEnv, CusProductStatus, @@ -6,16 +7,16 @@ import { } from "@autumn/shared"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; // UNCOMMENT FROM HERE const g1Pro = constructProduct({ @@ -77,44 +78,34 @@ describe(`${chalk.yellowBright("mergedGroup2: Testing products from diff groups" const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; let db: DrizzleCli; let org: Organization; let env: AppEnv; - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - await createProducts({ - autumn: autumnJs, + beforeAll(async () => { + await initProductsV0({ + ctx, products: [g1Pro, g2Pro, g1Premium, g2Premium], - db, - orgId: org.id, - env, + // prefix: customerId, customerId, }); - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, + await initCustomerV3({ + ctx, customerId, - db, - org, - env, + // customerData: {}, attachPm: "success", + withTestClock: true, }); - testClockId = testClockId1!; + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; }); - it("should attach pro product", async () => { - for (const op of ops) { + for (const op of ops) { + test(`should attach ${op.product.id}, other products: ${op.otherProducts?.map((p) => p.id).join(", ")}`, async () => { await attachAndExpectCorrect({ autumn, customerId, @@ -135,11 +126,10 @@ describe(`${chalk.yellowBright("mergedGroup2: Testing products from diff groups" status: result.status, }); } - } - }); + }); + } - return; - it("should cancel scheduled product (g1Pro)", async () => { + test("should cancel scheduled product (g1Pro)", async () => { await autumn.cancel({ customer_id: customerId, product_id: g1Pro.id, diff --git a/server/tests/merged/prepaid/mergedPrepaid1.test.ts b/server/tests/merged/prepaid/mergedPrepaid1.test.ts index 3831af414..acf990bcf 100644 --- a/server/tests/merged/prepaid/mergedPrepaid1.test.ts +++ b/server/tests/merged/prepaid/mergedPrepaid1.test.ts @@ -1,16 +1,15 @@ +import { beforeAll, describe, test } from "bun:test"; import { type AppEnv, CusProductStatus, LegacyVersion, type Organization, } from "@autumn/shared"; -import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; @@ -94,8 +93,6 @@ describe(`${chalk.yellowBright("mergedPrepaid1: Testing merged subs, upgrade 1 & const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; let db: DrizzleCli; let org: Organization; let env: AppEnv; @@ -108,7 +105,7 @@ describe(`${chalk.yellowBright("mergedPrepaid1: Testing merged subs, upgrade 1 & customerId, }); - const res = await initCustomerV3({ + await initCustomerV3({ ctx, customerId, customerData: {}, @@ -120,7 +117,6 @@ describe(`${chalk.yellowBright("mergedPrepaid1: Testing merged subs, upgrade 1 & db = ctx.db; org = ctx.org; env = ctx.env; - testClockId = res.testClockId!; }); const entities = [ diff --git a/server/tests/merged/separate/separate1.test.ts b/server/tests/merged/separate/separate1.test.ts index e29ae77cb..cb63e9a27 100644 --- a/server/tests/merged/separate/separate1.test.ts +++ b/server/tests/merged/separate/separate1.test.ts @@ -1,18 +1,16 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; -import { expect } from "chai"; import chalk from "chalk"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; import { TestFeature } from "tests/setup/v2Features.js"; -import { createProducts } from "tests/utils/productUtils.js"; import { completeInvoiceCheckout } from "tests/utils/stripeUtils/completeInvoiceCheckout.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { CusService } from "@/internal/customers/CusService.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; export const pro = constructProduct({ @@ -63,49 +61,34 @@ const testCase = "separate1"; describe(`${chalk.yellowBright(`${testCase}: Testing separate subscriptions because of invoice checkout`)}`, () => { const customerId = testCase; const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_2 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - const curUnix = new Date().getTime(); + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ + beforeAll(async () => { + await initProductsV0({ + ctx, products: [pro, premium], prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro, premium], customerId, - db, - orgId: org.id, - env, }); - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, + await initCustomerV3({ + ctx, customerId, - db, - org, - env, - // attachPm: "success", + customerData: {}, + withTestClock: true, }); - testClockId = testClockId1!; + db = ctx.db; + org = ctx.org; + env = ctx.env; + + await autumn.entities.create(customerId, entities); }); const subIds: string[] = []; - it("should attach pro product", async () => { - await autumn.entities.create(customerId, entities); + test("should attach pro product", async () => { for (const op of ops) { const res = await autumn.attach({ customer_id: customerId, @@ -132,7 +115,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing separate subscriptions beca const entity1SubId = entity1Prod?.subscription_ids?.[0]; const entity2SubId = entity2Prod?.subscription_ids?.[0]; - expect(entity1SubId).to.not.equal(entity2SubId); + expect(entity1SubId).not.toBe(entity2SubId); subIds.push(entity1SubId!); subIds.push(entity2SubId!); @@ -146,7 +129,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing separate subscriptions beca }); }); - it("should upgrade both entities to premium", async () => { + test("should upgrade both entities to premium", async () => { await autumn.attach({ customer_id: customerId, product_id: premium.id, diff --git a/server/tests/merged/separate/separate2.test.ts b/server/tests/merged/separate/separate2.test.ts index fa7cefd27..c70b7a268 100644 --- a/server/tests/merged/separate/separate2.test.ts +++ b/server/tests/merged/separate/separate2.test.ts @@ -1,12 +1,10 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; -import { expect } from "chai"; import chalk from "chalk"; import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; import { TestFeature } from "tests/setup/v2Features.js"; -import { createProducts } from "tests/utils/productUtils.js"; import { completeCheckoutForm } from "tests/utils/stripeUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { CusService } from "@/internal/customers/CusService.js"; @@ -18,7 +16,8 @@ import { constructProduct, constructRawProduct, } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; export const pro = constructProduct({ @@ -83,49 +82,38 @@ const testCase = "separate2"; describe(`${chalk.yellowBright(`${testCase}: Testing separate subscriptions because of force checkout`)}`, () => { const customerId = testCase; const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_2 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; let stripeCli: Stripe; - const curUnix = new Date().getTime(); + let testClockId: string; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ + beforeAll(async () => { + await initProductsV0({ + ctx, products: [pro, premium, addOn], prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro, premium, addOn], customerId, - db, - orgId: org.id, - env, }); - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, + const res = await initCustomerV3({ + ctx, customerId, - db, - org, - env, - // attachPm: "success", + customerData: {}, + withTestClock: true, }); - testClockId = testClockId1!; + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; + testClockId = res.testClockId!; + + await autumn.entities.create(customerId, entities); }); const subIds: string[] = []; - it("should attach pro product", async () => { - await autumn.entities.create(customerId, entities); + test("should attach pro product", async () => { for (const op of ops) { const res = await autumn.attach({ customer_id: customerId, @@ -134,7 +122,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing separate subscriptions beca entity_id: op.entityId, }); - expect(res.checkout_url).to.exist; + expect(res.checkout_url).toBeDefined(); await completeCheckoutForm(res.checkout_url); } @@ -152,7 +140,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing separate subscriptions beca const entity1SubId = entity1Prod?.subscription_ids?.[0]; const entity2SubId = entity2Prod?.subscription_ids?.[0]; - expect(entity1SubId).to.not.equal(entity2SubId); + expect(entity1SubId).not.toBe(entity2SubId); subIds.push(entity1SubId!); subIds.push(entity2SubId!); @@ -166,7 +154,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing separate subscriptions beca }); }); - it("should upgrade both entities to premium", async () => { + test("should upgrade both entities to premium", async () => { for (const id of ["1", "2"]) { await autumn.attach({ customer_id: customerId, @@ -191,7 +179,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing separate subscriptions beca } }); - it("should attach add on to entity 2 and correct sub", async () => { + test("should attach add on to entity 2 and correct sub", async () => { await autumn.attach({ customer_id: customerId, product_id: addOn.id, @@ -213,9 +201,9 @@ describe(`${chalk.yellowBright(`${testCase}: Testing separate subscriptions beca const cusProducts = fullCus.customer_products; const addOnProd = cusProducts.find((cp) => cp.product.id === addOn.id); - expect(addOnProd).to.exist; + expect(addOnProd).toBeDefined(); const addOnSubId = addOnProd?.subscription_ids?.[0]; - expect(addOnSubId).to.equal(subIds[1]); + expect(addOnSubId).toBe(subIds[1]); await expectSubToBeCorrect({ db, diff --git a/server/tests/merged/trial/trial1.test.ts b/server/tests/merged/trial/trial1.test.ts index f7162b059..1c7988cbd 100644 --- a/server/tests/merged/trial/trial1.test.ts +++ b/server/tests/merged/trial/trial1.test.ts @@ -1,3 +1,4 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, AttachBranch, @@ -5,23 +6,21 @@ import { LegacyVersion, type Organization, } from "@autumn/shared"; -import { expect } from "chai"; import chalk from "chalk"; import { addDays } from "date-fns"; import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; // Premium, Premium // Cancel End, Cancel Immediately @@ -65,42 +64,29 @@ describe(`${chalk.yellowBright("trial1: Testing main trial branch, upgrade from let org: Organization; let env: AppEnv; - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ + beforeAll(async () => { + await initProductsV0({ + ctx, products: [pro, premium], prefix: testCase, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, premium], - db, - orgId: org.id, - env, customerId, }); - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, + const res = await initCustomerV3({ + ctx, customerId, - db, - org, - env, attachPm: "success", + withTestClock: true, }); - testClockId = testClockId1!; + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; + testClockId = res.testClockId!; }); - it("should attach first trial, and advance clock past trial", async () => { + test("should attach first trial, and advance clock past trial", async () => { for (const op of ops) { await attachAndExpectCorrect({ autumn, @@ -121,20 +107,19 @@ describe(`${chalk.yellowBright("trial1: Testing main trial branch, upgrade from }); }); - it("should advance test clock to before trial ends and attach premium", async () => { + test("should advance test clock to before trial ends and attach premium", async () => { curUnix = await advanceTestClock({ stripeCli, testClockId, advanceTo: addDays(new Date(), 2).getTime(), }); - // return; const attachPreview = await autumn.attachPreview({ customer_id: customerId, product_id: premium.id, }); - expect(attachPreview?.branch).to.equal(AttachBranch.MainIsTrial); + expect(attachPreview?.branch).toBe(AttachBranch.MainIsTrial); await autumn.attach({ customer_id: customerId, @@ -148,8 +133,10 @@ describe(`${chalk.yellowBright("trial1: Testing main trial branch, upgrade from status: CusProductStatus.Trialing, }); const product = customer.products.find((p) => p.id === premium.id)!; - expect(product.current_period_end).to.be.approximately( - addDays(curUnix, 7).getTime(), + expect(product.current_period_end).toBeDefined(); + expect( + Math.abs(product.current_period_end! - addDays(curUnix, 7).getTime()), + ).toBeLessThanOrEqual( 1000 * 60 * 30, // 30 minutes ); diff --git a/server/tests/merged/trial/trial2.test.ts b/server/tests/merged/trial/trial2.test.ts index 850505790..71a71bb45 100644 --- a/server/tests/merged/trial/trial2.test.ts +++ b/server/tests/merged/trial/trial2.test.ts @@ -1,3 +1,4 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, AttachBranch, @@ -5,25 +6,23 @@ import { LegacyVersion, type Organization, } from "@autumn/shared"; -import { expect } from "chai"; import chalk from "chalk"; import { addDays } from "date-fns"; import { Decimal } from "decimal.js"; import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; // Pro Trial // Trial Finishes @@ -67,42 +66,29 @@ describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from let org: Organization; let env: AppEnv; - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ + beforeAll(async () => { + await initProductsV0({ + ctx, products: [pro, premium], prefix: testCase, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, premium], - db, - orgId: org.id, - env, customerId, }); - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, + const res = await initCustomerV3({ + ctx, customerId, - db, - org, - env, attachPm: "success", + withTestClock: true, }); - testClockId = testClockId1!; + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; + testClockId = res.testClockId!; }); - it("should attach first trial", async () => { + test("should attach first trial", async () => { for (const op of ops) { await attachAndExpectCorrect({ autumn, @@ -123,7 +109,7 @@ describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from }); }); - it("should advance test clock to past trial ends and attach premium", async () => { + test("should advance test clock to past trial ends and attach premium", async () => { curUnix = await advanceTestClock({ stripeCli, testClockId, @@ -140,7 +126,7 @@ describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from product_id: premium.id, }); - expect(attachPreview?.branch).to.equal(AttachBranch.Upgrade); + expect(attachPreview?.branch).toBe(AttachBranch.Upgrade); await autumn.attach({ customer_id: customerId, @@ -156,12 +142,14 @@ describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from status: CusProductStatus.Trialing, }); const product = customer.products.find((p) => p.id === premium.id)!; - expect(product.current_period_end).to.be.approximately( - addDays(curUnix, 7).getTime(), + expect(product.current_period_end).toBeDefined(); + expect( + Math.abs(product.current_period_end! - addDays(curUnix, 7).getTime()), + ).toBeLessThanOrEqual( 1000 * 60 * 30, // 30 minutes ); - expect(customer.invoices[0].total).to.equal( + expect(customer.invoices[0].total).toBe( new Decimal(checkoutRes.total).toDP(2).toNumber(), ); diff --git a/server/tests/merged/upgrade/mergedUpgrade1.test.ts b/server/tests/merged/upgrade/mergedUpgrade1.test.ts index ba27d3f00..97a30f235 100644 --- a/server/tests/merged/upgrade/mergedUpgrade1.test.ts +++ b/server/tests/merged/upgrade/mergedUpgrade1.test.ts @@ -1,29 +1,26 @@ +import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, CusProductStatus, LegacyVersion, type Organization, } from "@autumn/shared"; -import { expect } from "chai"; import chalk from "chalk"; import { addWeeks } from "date-fns"; import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; -import { - addPrefixToProducts, - getBasePrice, -} from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; const premium = constructProduct({ id: "premium", @@ -63,46 +60,10 @@ describe(`${chalk.yellowBright("mergedUpgrade1: Testing merged subs, upgrade 1 & let stripeCli: Stripe; let testClockId: string; - let curUnix: number; let db: DrizzleCli; let org: Organization; let env: AppEnv; - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, premium, premiumAnnual], - prefix: testCase, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, premium, premiumAnnual], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - const entities = [ { id: "1", @@ -116,11 +77,34 @@ describe(`${chalk.yellowBright("mergedUpgrade1: Testing merged subs, upgrade 1 & }, ]; - it("should run operations", async () => { - await autumn.entities.create(customerId, entities); + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro, premium, premiumAnnual], + prefix: testCase, + customerId, + }); - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; + const res = await initCustomerV3({ + ctx, + customerId, + + attachPm: "success", + withTestClock: true, + }); + + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; + testClockId = res.testClockId!; + + await autumn.entities.create(customerId, entities); + }); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => { try { await attachAndExpectCorrect({ autumn, @@ -138,13 +122,13 @@ describe(`${chalk.yellowBright("mergedUpgrade1: Testing merged subs, upgrade 1 & ); throw error; } - } - }); + }); + } const entity1Val = 100000; const entity2Val = 300000; - it("should advance test clock and upgrade entity 1 to premium, and have correct invoice", async () => { + test("should advance test clock and upgrade entity 1 to premium, and have correct invoice", async () => { await autumn.track({ customer_id: customerId, feature_id: TestFeature.Words, @@ -178,7 +162,7 @@ describe(`${chalk.yellowBright("mergedUpgrade1: Testing merged subs, upgrade 1 & }); }); - it("should advance to next invoice and have correct invoice", async () => { + test("should advance to next invoice and have correct invoice", async () => { await advanceToNextInvoice({ stripeCli, testClockId, @@ -204,6 +188,6 @@ describe(`${chalk.yellowBright("mergedUpgrade1: Testing merged subs, upgrade 1 & const invoice = customer.invoices[0]; const basePrice = getBasePrice({ product: pro }) + getBasePrice({ product: premium }); - expect(invoice.total).to.equal(basePrice + expectedTotal); + expect(invoice.total).toBe(basePrice + expectedTotal); }); }); diff --git a/server/tests/merged/upgrade/mergedUpgrade2.test.ts b/server/tests/merged/upgrade/mergedUpgrade2.test.ts index bbbaed2c6..0da26a440 100644 --- a/server/tests/merged/upgrade/mergedUpgrade2.test.ts +++ b/server/tests/merged/upgrade/mergedUpgrade2.test.ts @@ -1,3 +1,4 @@ +import { beforeAll, describe, test } from "bun:test"; import { type AppEnv, CusProductStatus, @@ -6,17 +7,16 @@ import { } from "@autumn/shared"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; // UNCOMMENT FROM HERE @@ -76,47 +76,10 @@ describe(`${chalk.yellowBright("mergedUpgrade2: Upgrading when there's a schedul const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; let db: DrizzleCli; let org: Organization; let env: AppEnv; - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, free, premium, growth], - prefix: testCase, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, free, premium, growth], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - const entities = [ { id: "1", @@ -130,11 +93,31 @@ describe(`${chalk.yellowBright("mergedUpgrade2: Upgrading when there's a schedul }, ]; - it("should run operations", async () => { - await autumn.entities.create(customerId, entities); + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro, free, premium, growth], + prefix: testCase, + customerId, + }); - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; + await initCustomerV3({ + ctx, + customerId, + attachPm: "success", + withTestClock: true, + }); + + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; + + await autumn.entities.create(customerId, entities); + }); + + for (const op of ops) { + test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => { await attachAndExpectCorrect({ autumn, customerId, @@ -155,6 +138,6 @@ describe(`${chalk.yellowBright("mergedUpgrade2: Upgrading when there's a schedul status: result.status, }); } - } - }); + }); + } }); diff --git a/server/tests/merged/upgrade/mergedUpgrade3.test.ts b/server/tests/merged/upgrade/mergedUpgrade3.test.ts index f6a956a13..04d9e8a85 100644 --- a/server/tests/merged/upgrade/mergedUpgrade3.test.ts +++ b/server/tests/merged/upgrade/mergedUpgrade3.test.ts @@ -1,3 +1,4 @@ +import { beforeAll, describe, test } from "bun:test"; import { type AppEnv, CusProductStatus, @@ -6,17 +7,16 @@ import { } from "@autumn/shared"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; // UNCOMMENT FROM HERE @@ -85,47 +85,10 @@ describe(`${chalk.yellowBright("mergedUpgrade3: Upgrading when there's a schedul const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; let db: DrizzleCli; let org: Organization; let env: AppEnv; - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, free, premium, growth], - prefix: testCase, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, free, premium, growth], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - const entities = [ { id: "1", @@ -139,11 +102,31 @@ describe(`${chalk.yellowBright("mergedUpgrade3: Upgrading when there's a schedul }, ]; - it("should run operations", async () => { - await autumn.entities.create(customerId, entities); + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro, free, premium, growth], + prefix: testCase, + customerId, + }); - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; + await initCustomerV3({ + ctx, + customerId, + attachPm: "success", + withTestClock: true, + }); + + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; + + await autumn.entities.create(customerId, entities); + }); + + for (const op of ops) { + test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => { await attachAndExpectCorrect({ autumn, customerId, @@ -164,6 +147,6 @@ describe(`${chalk.yellowBright("mergedUpgrade3: Upgrading when there's a schedul status: result.status, }); } - } - }); + }); + } }); diff --git a/server/tests/merged/upgrade/mergedUpgrade4.test.ts b/server/tests/merged/upgrade/mergedUpgrade4.test.ts index 1f9cd5f5d..4a0a9ed68 100644 --- a/server/tests/merged/upgrade/mergedUpgrade4.test.ts +++ b/server/tests/merged/upgrade/mergedUpgrade4.test.ts @@ -1,3 +1,4 @@ +import { beforeAll, describe, test } from "bun:test"; import { type AppEnv, CusProductStatus, @@ -6,17 +7,16 @@ import { } from "@autumn/shared"; import chalk from "chalk"; import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; // OPERATIONS: // Pro, Pro @@ -79,47 +79,10 @@ describe(`${chalk.yellowBright("mergedUpgrade4: Upgrading when there's a cancel" const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); let stripeCli: Stripe; - let testClockId: string; - let curUnix: number; let db: DrizzleCli; let org: Organization; let env: AppEnv; - beforeAll(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ - products: [pro, free, premium, growth], - prefix: testCase, - }); - - await createProducts({ - autumn: autumnJs, - products: [pro, free, premium, growth], - db, - orgId: org.id, - env, - customerId, - }); - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - testClockId = testClockId1!; - }); - const entities = [ { id: "1", @@ -133,11 +96,31 @@ describe(`${chalk.yellowBright("mergedUpgrade4: Upgrading when there's a cancel" }, ]; - it("should run operations", async () => { - await autumn.entities.create(customerId, entities); + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro, free, premium, growth], + prefix: testCase, + customerId, + }); - for (let index = 0; index < ops.length; index++) { - const op = ops[index]; + await initCustomerV3({ + ctx, + customerId, + attachPm: "success", + withTestClock: true, + }); + + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; + + await autumn.entities.create(customerId, entities); + }); + + for (const op of ops) { + test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => { await attachAndExpectCorrect({ autumn, customerId, @@ -158,6 +141,6 @@ describe(`${chalk.yellowBright("mergedUpgrade4: Upgrading when there's a cancel" status: result.status, }); } - } - }); + }); + } }); diff --git a/server/tests/setup/v2Features.ts b/server/tests/setup/v2Features.ts index d0ef9bdd1..e58869877 100644 --- a/server/tests/setup/v2Features.ts +++ b/server/tests/setup/v2Features.ts @@ -13,6 +13,7 @@ export enum TestFeature { Dashboard = "dashboard", // boolean feature Messages = "messages", // single use (prepaid) Users = "users", // cont use + Workflows = "workflows", // cont use Admin = "admin", // cont use AdminRights = "admin_rights", // cont use Words = "words", // single use (pay per use) @@ -31,6 +32,12 @@ export const getFeatures = ({ orgId }: { orgId: string }) => ({ orgId, env: AppEnv.Sandbox, }), + [TestFeature.Workflows]: constructMeteredFeature({ + featureId: TestFeature.Workflows, + orgId, + env: AppEnv.Sandbox, + usageType: FeatureUsageType.Continuous, + }), [TestFeature.AdminRights]: constructBooleanFeature({ featureId: TestFeature.AdminRights, orgId, diff --git a/server/tests/testRunner/TestRunnerUI.tsx b/server/tests/testRunner/TestRunnerUI.tsx index b5f928cbb..f9a8b3600 100644 --- a/server/tests/testRunner/TestRunnerUI.tsx +++ b/server/tests/testRunner/TestRunnerUI.tsx @@ -1,6 +1,6 @@ import { Box, Text, render } from "ink"; import Spinner from "ink-spinner"; -import React, { useEffect, useState } from "react"; +import React from "react"; export type TestFileStatus = "pending" | "running" | "passed" | "failed"; diff --git a/server/tests/utils/expectUtils/expectAttach.ts b/server/tests/utils/expectUtils/expectAttach.ts index 4a54c74e1..7b83c091e 100644 --- a/server/tests/utils/expectUtils/expectAttach.ts +++ b/server/tests/utils/expectUtils/expectAttach.ts @@ -242,10 +242,12 @@ export const expectAttachCorrect = async ({ customer, product, entityId, + otherProducts, }: { customer: Customer; product: ProductV2; entityId?: string; + otherProducts?: ProductV2[]; }) => { expectProductAttached({ customer, @@ -256,5 +258,6 @@ export const expectAttachCorrect = async ({ expectFeaturesCorrect({ customer, product, + otherProducts, }); }; diff --git a/server/tests/utils/expectUtils/expectErrUtils.ts b/server/tests/utils/expectUtils/expectErrUtils.ts index 4029c1460..63fc42ad4 100644 --- a/server/tests/utils/expectUtils/expectErrUtils.ts +++ b/server/tests/utils/expectUtils/expectErrUtils.ts @@ -1,4 +1,7 @@ -import { assert, expect } from "chai"; +// import { assert, expect } from "chai"; + +import { expect } from "bun:test"; +import assert from "node:assert"; import AutumnError from "@/external/autumn/autumnCli.js"; export const expectAutumnError = async ({ @@ -11,7 +14,9 @@ export const expectAutumnError = async ({ func: () => Promise; }) => { try { - const result = await func(); + const res = await func(); + + console.log("Res: ", res); assert.fail( `Expected to receive autumn error ${errCode}, but received none`, @@ -19,19 +24,19 @@ export const expectAutumnError = async ({ } catch (error: any) { // 1. Expect error to be instance of AutumnError - expect(error, "Error should be instance of AutumnError").to.be.instanceOf( + expect(error, "Error should be instance of AutumnError").toBeInstanceOf( AutumnError, ); if (errMessage) { - expect(error.message, `Error message should be ${errMessage}`).to.equal( + expect(error.message, `Error message should be ${errMessage}`).toInclude( errMessage, ); } if (errCode) { // 2. Expect error code to be the same as the one passed in - expect(error.code, `Error code should be ${errCode}`).to.equal(errCode); + expect(error.code, `Error code should be ${errCode}`).toBe(errCode); } } }; diff --git a/server/tests/utils/expectUtils/expectInvoiceUtils.ts b/server/tests/utils/expectUtils/expectInvoiceUtils.ts index d5791386b..54c9e2f27 100644 --- a/server/tests/utils/expectUtils/expectInvoiceUtils.ts +++ b/server/tests/utils/expectUtils/expectInvoiceUtils.ts @@ -1,20 +1,21 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { cusProductToPrices, cusProductToEnts } from "@autumn/shared"; -import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js"; -import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js"; import { - Organization, BillingInterval, - UsagePriceConfig, + cusProductToEnts, + cusProductToPrices, + type Organization, + type UsagePriceConfig, } from "@autumn/shared"; -import { AppEnv } from "autumn-js"; +import type { AppEnv } from "autumn-js"; import { Decimal } from "decimal.js"; -import Stripe from "stripe"; -import { getSubsFromCusId } from "./expectSubUtils.js"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js"; import { isArrearPrice, isFixedPrice, } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js"; +import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js"; +import { getSubsFromCusId } from "./expectSubUtils.js"; export const getExpectedInvoiceTotal = async ({ customerId, @@ -60,7 +61,7 @@ export const getExpectedInvoiceTotal = async ({ let total = new Decimal(0); for (const price of prices) { - if (onlyIncludeMonthly && price.config.interval != BillingInterval.Month) { + if (onlyIncludeMonthly && price.config.interval !== BillingInterval.Month) { continue; } @@ -74,8 +75,10 @@ export const getExpectedInvoiceTotal = async ({ const usageAmount = usage.find( (u) => - u.featureId == featureId && - (u.entityFeatureId ? u.entityFeatureId == ent.entity_feature_id : true), + u.featureId === featureId && + (u.entityFeatureId + ? u.entityFeatureId === ent.entity_feature_id + : true), )?.value; const overage = diff --git a/server/tests/utils/expectUtils/expectProductAttached.ts b/server/tests/utils/expectUtils/expectProductAttached.ts index b5ca0abce..0b0b39a8e 100644 --- a/server/tests/utils/expectUtils/expectProductAttached.ts +++ b/server/tests/utils/expectUtils/expectProductAttached.ts @@ -31,12 +31,10 @@ export const expectProductAttached = ({ p.id === finalProductId && (entityId ? p.entity_id === entityId : true), ); - if (!productAttached) { - console.log(`product ${finalProductId} not attached`); - console.log(cusProducts); - } - - expect(productAttached, `product ${finalProductId} is attached`).to.exist; + expect( + productAttached, + `product ${finalProductId} not attached to ${customer.id}`, + ).to.exist; if (status) { expect(productAttached?.status).to.equal( diff --git a/server/tests/utils/productUtils.ts b/server/tests/utils/productUtils.ts index 014c17910..79b370078 100644 --- a/server/tests/utils/productUtils.ts +++ b/server/tests/utils/productUtils.ts @@ -1,4 +1,9 @@ -import { type AppEnv, type CreateReward, isUsagePrice } from "@autumn/shared"; +import { + type AppEnv, + type CreateReward, + type CreateRewardProgram, + isUsagePrice, +} from "@autumn/shared"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import type { AutumnInt } from "@/external/autumn/autumnCli.js"; import { ProductService } from "@/internal/products/ProductService.js"; @@ -116,21 +121,22 @@ export const createReward = async ({ env: AppEnv; autumn: AutumnInt; reward: CreateReward; - productId: string; + productId?: string; onlyUsage?: boolean; }) => { - const fullProduct = await ProductService.getFull({ - db, - orgId, - env, - idOrInternalId: productId!, - }); + // Only fetch product if we need usage prices + if (onlyUsage && productId) { + const fullProduct = await ProductService.getFull({ + db, + orgId, + env, + idOrInternalId: productId, + }); - const usagePrices = fullProduct?.prices.filter((price) => - isUsagePrice({ price }), - ); + const usagePrices = fullProduct?.prices.filter((price) => + isUsagePrice({ price }), + ); - if (onlyUsage) { reward.discount_config!.price_ids = usagePrices?.map((price) => price.id); } @@ -140,3 +146,49 @@ export const createReward = async ({ await autumn.rewards.create(reward); }; + +export const createReferralProgram = async ({ + db, + orgId, + env, + autumn, + reward, + rewardProgram, + productId, + onlyUsage = false, +}: { + db: DrizzleCli; + orgId: string; + env: AppEnv; + autumn: AutumnInt; + reward: CreateReward; + rewardProgram: CreateRewardProgram; + productId?: string; + onlyUsage?: boolean; +}) => { + // Create reward first + await createReward({ + db, + orgId, + env, + autumn, + reward, + productId, + onlyUsage, + }); + + // Create referral program (will fail if already exists, but that's ok) + try { + await autumn.rewardPrograms.create(rewardProgram); + } catch (error: any) { + // If program already exists (race condition), silently continue + if ( + error?.message?.includes("already exists") || + error?.message?.includes("duplicate") || + error?.code === "REWARD_PROGRAM_EXISTS" + ) { + return; + } + throw error; + } +}; diff --git a/server/tsconfig.json b/server/tsconfig.json index b1f45ca10..68cc1b6d6 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -23,6 +23,7 @@ "@shared/*": ["../shared/*"], "@scripts/*": ["scripts/*"], "@emails/*": ["emails/*"], + "@lua/*": ["src/_luaScripts/*"], } }, "include": ["src", "tests", "scripts", "emails", "experiments"], diff --git a/shared/api/common/customerData.ts b/shared/api/common/customerData.ts index fbf050261..4ea50947b 100644 --- a/shared/api/common/customerData.ts +++ b/shared/api/common/customerData.ts @@ -20,6 +20,10 @@ export const CustomerDataSchema = z stripe_id: z.string().nullish().meta({ description: "Stripe customer ID if you already have one", }), + disable_default: z.boolean().optional().meta({ + description: + "Disable default products from being attached to the customer", + }), }) .meta({ id: "CustomerData", diff --git a/shared/api/entities/entityOpModels.ts b/shared/api/entities/entityOpModels.ts index 7af76887c..b52e0680f 100644 --- a/shared/api/entities/entityOpModels.ts +++ b/shared/api/entities/entityOpModels.ts @@ -21,6 +21,7 @@ export const CreateEntityParamsSchema = z.object({ export const GetEntityQuerySchema = z.object({ expand: queryStringArray(z.enum(EntityExpand)).default([]), skip_cache: z.boolean().optional(), + with_autumn_id: z.boolean().optional(), }); export const CreateEntityQuerySchema = z.object({ diff --git a/shared/api/models.ts b/shared/api/models.ts index 0779417d2..4a3d8f1e1 100644 --- a/shared/api/models.ts +++ b/shared/api/models.ts @@ -62,6 +62,7 @@ export * from "./referrals/referralsOpenApi.js"; export * from "./balances/check/previousVersions/CheckResponseV0.js"; export * from "./balances/trackModels.js"; export * from "./balances/usageModels.js"; +export * from "./common/customerData.js"; export * from "./common/entityData.js"; // Errors export * from "./errors/index.js"; diff --git a/shared/models/analyticsModels/actionEnums.ts b/shared/models/analyticsModels/actionEnums.ts index 61eb862a9..eccaceb64 100644 --- a/shared/models/analyticsModels/actionEnums.ts +++ b/shared/models/analyticsModels/actionEnums.ts @@ -4,6 +4,7 @@ export enum AuthType { Dashboard = "dashboard", Stripe = "stripe", Unknown = "unknown", + Worker = "worker", } export enum ActionType { diff --git a/shared/models/cusModels/cusModels.ts b/shared/models/cusModels/cusModels.ts index abf4c2ffc..115dd39bb 100644 --- a/shared/models/cusModels/cusModels.ts +++ b/shared/models/cusModels/cusModels.ts @@ -65,19 +65,19 @@ export const CreateCustomerSchema = z.object({ stripe_id: z.string().nullish(), }); -export const CustomerDataSchema = z.object({ - name: z.string().nullish(), - email: z.string().nullish(), - fingerprint: z.string().nullish(), - metadata: z.record(z.any(), z.any()).nullish(), - stripe_id: z.string().nullish(), -}); +// export const CustomerDataSchema = z.object({ +// name: z.string().nullish(), +// email: z.string().nullish(), +// fingerprint: z.string().nullish(), +// metadata: z.record(z.any(), z.any()).nullish(), +// stripe_id: z.string().nullish(), +// }); export const CustomerResponseSchema = CustomerSchema.omit({ org_id: true, }); export type Customer = z.infer; -export type CustomerData = z.infer; +// export type CustomerData = z.infer; export type CustomerResponse = z.infer; export type CreateCustomer = z.infer; diff --git a/shared/models/rewardModels/rewardProgramModels/rewardProgramModels.ts b/shared/models/rewardModels/rewardProgramModels/rewardProgramModels.ts index efd3fe5ba..816938278 100644 --- a/shared/models/rewardModels/rewardProgramModels/rewardProgramModels.ts +++ b/shared/models/rewardModels/rewardProgramModels/rewardProgramModels.ts @@ -29,7 +29,7 @@ export const CreateRewardProgram = z.object({ exclude_trial: z.boolean().optional(), internal_reward_id: z.string(), max_redemptions: z.number().optional(), - received_by: z.nativeEnum(RewardReceivedBy), + received_by: z.enum(["referrer", "all"]), }); export const UpdateRewardProgram = z.object({ diff --git a/shared/utils/cusProductUtils/convertCusProduct.ts b/shared/utils/cusProductUtils/convertCusProduct.ts index 945318152..7f53ebc99 100644 --- a/shared/utils/cusProductUtils/convertCusProduct.ts +++ b/shared/utils/cusProductUtils/convertCusProduct.ts @@ -48,7 +48,7 @@ export const cusProductsToCusPrices = ({ export const cusProductsToCusEnts = ({ cusProducts, - inStatuses = [CusProductStatus.Active], + inStatuses = [CusProductStatus.Active, CusProductStatus.PastDue], reverseOrder = false, featureId, featureIds, @@ -64,9 +64,7 @@ export const cusProductsToCusEnts = ({ let cusEnts: FullCusEntWithFullCusProduct[] = []; for (const cusProduct of cusProducts) { - if (!inStatuses.includes(cusProduct.status)) { - continue; - } + if (!inStatuses.includes(cusProduct.status)) continue; cusEnts.push( ...cusProduct.customer_entitlements.map((cusEnt) => ({