diff --git a/server/src/internal/billing/v2/providers/stripe/utils/common/initStripeResourcesForProducts.ts b/server/src/internal/billing/v2/providers/stripe/utils/common/initStripeResourcesForProducts.ts index 2ede4b31f..19ab2076e 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/common/initStripeResourcesForProducts.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/common/initStripeResourcesForProducts.ts @@ -1,5 +1,9 @@ -import type { AutumnBillingPlan, BillingContext } from "@autumn/shared"; -import { cusProductToProduct } from "@autumn/shared"; +import { + type AutumnBillingPlan, + type BillingContext, + cusProductToProduct, + nullish, +} from "@autumn/shared"; import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { checkStripeProductExists } from "@/internal/products/productUtils"; @@ -22,14 +26,26 @@ export const initStripeResourcesForBillingPlan = async ({ cusProductToProduct({ cusProduct: cp }), ); - const existingProducts = fullCustomer.customer_products.map((cp) => - cusProductToProduct({ cusProduct: cp }), - ); + const existingProducts = fullCustomer.customer_products + .map((customerProduct) => + cusProductToProduct({ cusProduct: customerProduct }), + ) + .map((product) => ({ + ...product, + prices: product.prices.filter((price) => + nullish(price.config.stripe_price_id), + ), + })) + .filter( + (product) => nullish(product.processor?.id) || product.prices.length > 0, + ); const allProducts = [...newProducts, ...existingProducts]; const batchProductUpdates = []; for (const product of allProducts) { + if (product.processor?.id != null) continue; + batchProductUpdates.push( checkStripeProductExists({ db, diff --git a/server/tests/integration/balances/auto-topup/auto-topup-credit-systems.test.ts b/server/tests/integration/balances/auto-topup/auto-topup-credit-systems.test.ts index b545274a5..85399f694 100644 --- a/server/tests/integration/balances/auto-topup/auto-topup-credit-systems.test.ts +++ b/server/tests/integration/balances/auto-topup/auto-topup-credit-systems.test.ts @@ -74,18 +74,19 @@ test.concurrent(`${chalk.yellowBright("auto-topup cs1: action track depletes cre }); // Action1 costs 0.2 credits per unit - // Track 850 units of action1 → 850 × 0.2 = 170 credits deducted - // Balance: 200 - 170 = 30 → AT threshold (>= 30) → does NOT trigger + // Track 845 units → 845 × 0.2 = 169 credits deducted + // Balance: 200 - 169 = 31 → strictly above threshold (30) → does NOT trigger + // (exact threshold uses <= in code, so landing on 30 would fire auto top-up) const action1Cost = getCreditCost({ featureId: TestFeature.Action1, creditSystem: creditFeature!, - amount: 850, + amount: 845, }); await autumnV2_1.track({ customer_id: customerId, feature_id: TestFeature.Action1, - value: 850, + value: 845, }); await timeout(AUTO_TOPUP_WAIT_MS); @@ -93,13 +94,10 @@ test.concurrent(`${chalk.yellowBright("auto-topup cs1: action track depletes cre const mid = await autumnV2_1.customers.get(customerId); const midCredits = mid.balances[TestFeature.Credits]?.remaining; const expectedMid = new Decimal(200).sub(action1Cost).toNumber(); - expect(midCredits).toBe(expectedMid); // 30, no top-up + expect(midCredits).toBe(expectedMid); // 31, no top-up - // Now track 100 units of action2 → 100 × 0.6 = 60 credits deducted - // Balance: 30 - 60 = -30... but balance can't go negative with prepaid - // Actually, the deduction will bring it below threshold → auto top-up fires - // Let's track a smaller amount: 10 units of action1 → 10 × 0.2 = 2 credits - // Balance: 30 - 2 = 28 → below threshold (28 < 30) → auto top-up fires → balance = 28 + 100 = 128 + // Track 10 units of action1 → 10 × 0.2 = 2 credits + // Balance: 31 - 2 = 29 → 29 <= threshold → auto top-up fires → 29 + 100 = 129 const action1CostSmall = getCreditCost({ featureId: TestFeature.Action1, creditSystem: creditFeature!, diff --git a/server/tests/integration/balances/auto-topup/auto-topup-failure-modes.test.ts b/server/tests/integration/balances/auto-topup/auto-topup-failure-modes.test.ts index c1d7461ee..46b74c0fa 100644 --- a/server/tests/integration/balances/auto-topup/auto-topup-failure-modes.test.ts +++ b/server/tests/integration/balances/auto-topup/auto-topup-failure-modes.test.ts @@ -153,7 +153,7 @@ test.concurrent(`${chalk.yellowBright("auto-topup fm3: monthly (non-one-off) fea expect(after.balances[TestFeature.Messages].remaining).toBe(15); }); -test.concurrent(`${chalk.yellowBright("auto-topup fm4: track exactly to threshold — no trigger")}`, async () => { +test.concurrent(`${chalk.yellowBright("auto-topup fm4: track exactly to threshold — trigger")}`, async () => { const oneOffItem = items.oneOffMessages({ includedUsage: 0, billingUnits: 100, @@ -197,7 +197,7 @@ test.concurrent(`${chalk.yellowBright("auto-topup fm4: track exactly to threshol await timeout(AUTO_TOPUP_WAIT_MS); const after = await autumnV2_1.customers.get(customerId); - expect(after.balances[TestFeature.Messages].remaining).toBe(20); + expect(after.balances[TestFeature.Messages].remaining).toBe(120); }); test.concurrent(`${chalk.yellowBright("auto-topup fm5: insufficient balance rejection does NOT double-trigger")}`, async () => {