feat: new attach / update subscription params
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { ExistingRollover } from "@autumn/shared";
|
||||
import { type ExistingRollover, RolloverExpiryDurationType } from "@autumn/shared";
|
||||
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements";
|
||||
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
||||
import chalk from "chalk";
|
||||
@@ -16,6 +16,7 @@ describe(chalk.yellowBright("applyExistingRollovers"), () => {
|
||||
featureName: "Words",
|
||||
allowance: 5000,
|
||||
balance: 5000,
|
||||
rollover: { max: null, duration: RolloverExpiryDurationType.Month, length: 1 },
|
||||
});
|
||||
|
||||
const newCusProduct = customerProducts.create({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { ExistingRollover } from "@autumn/shared";
|
||||
import { type ExistingRollover, RolloverExpiryDurationType } from "@autumn/shared";
|
||||
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements";
|
||||
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
||||
import chalk from "chalk";
|
||||
@@ -19,6 +19,7 @@ describe(
|
||||
featureName: "Words",
|
||||
allowance: 5000,
|
||||
balance: 5000,
|
||||
rollover: { max: null, duration: RolloverExpiryDurationType.Month, length: 1 },
|
||||
});
|
||||
|
||||
const newCusProduct = customerProducts.create({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { ExistingRollover } from "@autumn/shared";
|
||||
import { type ExistingRollover, RolloverExpiryDurationType } from "@autumn/shared";
|
||||
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements";
|
||||
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
||||
import chalk from "chalk";
|
||||
@@ -15,6 +15,7 @@ describe(
|
||||
featureName: "Feature A",
|
||||
allowance: 100,
|
||||
balance: 100,
|
||||
rollover: { max: null, duration: RolloverExpiryDurationType.Month, length: 1 },
|
||||
});
|
||||
|
||||
const cusEntB = customerEntitlements.create({
|
||||
@@ -23,6 +24,7 @@ describe(
|
||||
featureName: "Feature B",
|
||||
allowance: 200,
|
||||
balance: 200,
|
||||
rollover: { max: null, duration: RolloverExpiryDurationType.Month, length: 1 },
|
||||
});
|
||||
|
||||
const newCusProduct = customerProducts.create({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { ExistingRollover } from "@autumn/shared";
|
||||
import { type ExistingRollover, RolloverExpiryDurationType } from "@autumn/shared";
|
||||
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements";
|
||||
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
||||
import chalk from "chalk";
|
||||
@@ -18,6 +18,7 @@ describe(
|
||||
featureName: "Words",
|
||||
allowance: 100,
|
||||
balance: 100,
|
||||
rollover: { max: null, duration: RolloverExpiryDurationType.Month, length: 1 },
|
||||
});
|
||||
|
||||
const cusEntSecond = customerEntitlements.create({
|
||||
@@ -27,6 +28,7 @@ describe(
|
||||
featureName: "Words",
|
||||
allowance: 200,
|
||||
balance: 200,
|
||||
rollover: { max: null, duration: RolloverExpiryDurationType.Month, length: 1 },
|
||||
});
|
||||
|
||||
const newCusProduct = customerProducts.create({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { ExistingRollover } from "@autumn/shared";
|
||||
import { type ExistingRollover, RolloverExpiryDurationType } from "@autumn/shared";
|
||||
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements";
|
||||
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
||||
import chalk from "chalk";
|
||||
@@ -19,6 +19,7 @@ describe(
|
||||
featureName: "Seats",
|
||||
allowance: 10,
|
||||
balance: 10,
|
||||
rollover: { max: null, duration: RolloverExpiryDurationType.Month, length: 1 },
|
||||
});
|
||||
|
||||
const newCusProduct = customerProducts.create({
|
||||
|
||||
@@ -45,7 +45,7 @@ describe(
|
||||
customerPrices: [customerPrice],
|
||||
});
|
||||
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// Usage should be 150: 100 from included + 50 from overage
|
||||
// Formula: usage = includedUsage - balance = 100 - (-50) = 150
|
||||
@@ -83,7 +83,7 @@ describe(
|
||||
customerPrices: [customerPrice],
|
||||
});
|
||||
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// Usage should be 70: allowance 100 - balance 30 = 70
|
||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||
@@ -121,7 +121,7 @@ describe(
|
||||
customerPrices: [customerPrice],
|
||||
});
|
||||
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// Usage should be 200: all from overage (0 - (-200) = 200)
|
||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||
@@ -158,7 +158,7 @@ describe(
|
||||
customerPrices: [customerPrice],
|
||||
});
|
||||
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// Usage should be 100: exactly at the limit
|
||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||
@@ -201,7 +201,7 @@ describe(
|
||||
customerPrices: [customerPrice],
|
||||
});
|
||||
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// Usage should be 3: 5 included - 2 remaining = 3 used
|
||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||
@@ -238,7 +238,7 @@ describe(
|
||||
customerPrices: [customerPrice],
|
||||
});
|
||||
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// Usage should be 5: 3 - (-2) = 5 total seats used
|
||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||
@@ -275,7 +275,7 @@ describe(
|
||||
customerPrices: [customerPrice],
|
||||
});
|
||||
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// Usage should be 10: 0 - (-10) = 10
|
||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||
@@ -320,7 +320,7 @@ describe(
|
||||
customerPrices: [customerPrice],
|
||||
});
|
||||
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// For prepaid, usage = purchasedQuantity - balance
|
||||
// purchasedQuantity from 10 billing units = 1000
|
||||
@@ -362,7 +362,7 @@ describe(
|
||||
customerPrices: [customerPrice],
|
||||
});
|
||||
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||
});
|
||||
@@ -399,7 +399,7 @@ describe(
|
||||
customerPrices: [customerPrice],
|
||||
});
|
||||
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||
// Overage should be captured
|
||||
@@ -471,7 +471,7 @@ describe(
|
||||
],
|
||||
});
|
||||
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// Messages: 100 - (-20) = 120 used
|
||||
expect(existingUsages[messagesFeatureId]).toBeDefined();
|
||||
@@ -550,7 +550,7 @@ describe(
|
||||
],
|
||||
});
|
||||
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// Messages: 50 - (-150) = 200 used
|
||||
expect(existingUsages[messagesFeatureId]).toBeDefined();
|
||||
@@ -601,7 +601,7 @@ describe(
|
||||
customerPrices: [customerPrice],
|
||||
});
|
||||
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// Usage should be 0
|
||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||
@@ -638,7 +638,7 @@ describe(
|
||||
customerPrices: [customerPrice],
|
||||
});
|
||||
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// Usage should be 10100: 100 - (-10000) = 10100
|
||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||
@@ -675,7 +675,7 @@ describe(
|
||||
customerPrices: [customerPrice],
|
||||
});
|
||||
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// Usage should be 150.5
|
||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||
|
||||
@@ -37,7 +37,7 @@ describe(chalk.yellowBright("cusProductToExistingUsages"), () => {
|
||||
});
|
||||
|
||||
// Act
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// Assert: Total usage should be 40 (20 + 20)
|
||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||
@@ -78,7 +78,7 @@ describe(chalk.yellowBright("cusProductToExistingUsages"), () => {
|
||||
});
|
||||
|
||||
// Act
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// Assert
|
||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||
@@ -130,7 +130,7 @@ describe(chalk.yellowBright("cusProductToExistingUsages"), () => {
|
||||
});
|
||||
|
||||
// Act
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
||||
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||
|
||||
// Assert: Usage should be 20 (allowance 100 - balance 80)
|
||||
// NOT 50 (allowance 100 + rollover 30 - balance 80)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { UpdateSubscriptionV0Params } from "@autumn/shared";
|
||||
import type { UpdateSubscriptionV1Params } from "@autumn/shared";
|
||||
import { contexts } from "@tests/utils/fixtures/db/contexts";
|
||||
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
||||
import { entitlements } from "@tests/utils/fixtures/db/entitlements";
|
||||
import { features } from "@tests/utils/fixtures/db/features";
|
||||
import { prices } from "@tests/utils/fixtures/db/prices";
|
||||
import { products } from "@tests/utils/fixtures/db/products";
|
||||
@@ -18,12 +19,23 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Credits",
|
||||
});
|
||||
|
||||
const entitlement = entitlements.create({
|
||||
id: "ent_credits",
|
||||
featureId: "credits",
|
||||
featureName: "Credits",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
const price = prices.createPrepaid({
|
||||
id: "price_credits",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({ prices: [price] });
|
||||
const fullProduct = products.createFull({
|
||||
prices: [price],
|
||||
entitlements: [entitlement],
|
||||
});
|
||||
const cusProduct = customerProducts.create({
|
||||
options: [
|
||||
{
|
||||
@@ -35,7 +47,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
customerPrices: [prices.createCustomer({ price })],
|
||||
});
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
// No options provided
|
||||
@@ -61,15 +73,26 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Credits",
|
||||
});
|
||||
|
||||
const entitlement = entitlements.create({
|
||||
id: "ent_credits",
|
||||
featureId: "credits",
|
||||
featureName: "Credits",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
const price = prices.createPrepaid({
|
||||
id: "price_credits",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({ prices: [price] });
|
||||
const fullProduct = products.createFull({
|
||||
prices: [price],
|
||||
entitlements: [entitlement],
|
||||
});
|
||||
const cusProduct = customerProducts.create({ options: [] });
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
options: [{ feature_id: "credits", quantity: 50 }],
|
||||
@@ -95,12 +118,23 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Credits",
|
||||
});
|
||||
|
||||
const entitlement = entitlements.create({
|
||||
id: "ent_credits",
|
||||
featureId: "credits",
|
||||
featureName: "Credits",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
const price = prices.createPrepaid({
|
||||
id: "price_credits",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({ prices: [price] });
|
||||
const fullProduct = products.createFull({
|
||||
prices: [price],
|
||||
entitlements: [entitlement],
|
||||
});
|
||||
const cusProduct = customerProducts.create({
|
||||
options: [
|
||||
{
|
||||
@@ -111,7 +145,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
],
|
||||
});
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
options: [{ feature_id: "credits", quantity: 200 }],
|
||||
@@ -147,21 +181,44 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Storage",
|
||||
});
|
||||
|
||||
const creditsEnt = entitlements.create({
|
||||
id: "ent_credits",
|
||||
featureId: "credits",
|
||||
featureName: "Credits",
|
||||
allowance: 0,
|
||||
});
|
||||
const seatsEnt = entitlements.create({
|
||||
id: "ent_seats",
|
||||
featureId: "seats",
|
||||
featureName: "Seats",
|
||||
allowance: 0,
|
||||
});
|
||||
const storageEnt = entitlements.create({
|
||||
id: "ent_storage",
|
||||
featureId: "storage",
|
||||
featureName: "Storage",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
const creditsPrice = prices.createPrepaid({
|
||||
id: "price_credits",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
});
|
||||
const seatsPrice = prices.createPrepaid({
|
||||
id: "price_seats",
|
||||
featureId: "seats",
|
||||
entitlementId: "ent_seats",
|
||||
});
|
||||
const storagePrice = prices.createPrepaid({
|
||||
id: "price_storage",
|
||||
featureId: "storage",
|
||||
entitlementId: "ent_storage",
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({
|
||||
prices: [creditsPrice, seatsPrice, storagePrice],
|
||||
entitlements: [creditsEnt, seatsEnt, storageEnt],
|
||||
});
|
||||
|
||||
const cusProduct = customerProducts.create({
|
||||
@@ -189,7 +246,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
],
|
||||
});
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
options: [{ feature_id: "seats", quantity: 10 }], // Only updating seats
|
||||
@@ -225,16 +282,27 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Credits",
|
||||
});
|
||||
|
||||
const entitlement = entitlements.create({
|
||||
id: "ent_credits",
|
||||
featureId: "credits",
|
||||
featureName: "Credits",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
const price = prices.createPrepaid({
|
||||
id: "price_credits",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
billingUnits: 100, // Billing in units of 100
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({ prices: [price] });
|
||||
const fullProduct = products.createFull({
|
||||
prices: [price],
|
||||
entitlements: [entitlement],
|
||||
});
|
||||
const cusProduct = customerProducts.create({ options: [] });
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
options: [{ feature_id: "credits", quantity: 150 }], // Should round up to 200
|
||||
@@ -261,16 +329,27 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Credits",
|
||||
});
|
||||
|
||||
const entitlement = entitlements.create({
|
||||
id: "ent_credits",
|
||||
featureId: "credits",
|
||||
featureName: "Credits",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
const price = prices.createPrepaid({
|
||||
id: "price_credits",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
billingUnits: 50,
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({ prices: [price] });
|
||||
const fullProduct = products.createFull({
|
||||
prices: [price],
|
||||
entitlements: [entitlement],
|
||||
});
|
||||
const cusProduct = customerProducts.create({ options: [] });
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
options: [{ feature_id: "credits", quantity: 200 }], // Exact multiple
|
||||
@@ -296,16 +375,27 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Credits",
|
||||
});
|
||||
|
||||
const entitlement = entitlements.create({
|
||||
id: "ent_credits",
|
||||
featureId: "credits",
|
||||
featureName: "Credits",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
const price = prices.createPrepaid({
|
||||
id: "price_credits",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
billingUnits: 1000,
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({ prices: [price] });
|
||||
const fullProduct = products.createFull({
|
||||
prices: [price],
|
||||
entitlements: [entitlement],
|
||||
});
|
||||
const cusProduct = customerProducts.create({ options: [] });
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
options: [{ feature_id: "credits", quantity: 1 }], // Should round to 1000
|
||||
@@ -334,10 +424,18 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Credits",
|
||||
});
|
||||
|
||||
const entitlement = entitlements.create({
|
||||
id: "ent_credits",
|
||||
featureId: "credits",
|
||||
featureName: "Credits",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
// Old price: billing units of 100
|
||||
const oldPrice = prices.createPrepaid({
|
||||
id: "price_credits_old",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
billingUnits: 100,
|
||||
});
|
||||
|
||||
@@ -345,10 +443,14 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
const newPrice = prices.createPrepaid({
|
||||
id: "price_credits_new",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
billingUnits: 250,
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({ prices: [newPrice] });
|
||||
const fullProduct = products.createFull({
|
||||
prices: [newPrice],
|
||||
entitlements: [entitlement],
|
||||
});
|
||||
|
||||
const cusProduct = customerProducts.create({
|
||||
options: [
|
||||
@@ -361,7 +463,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
customerPrices: [prices.createCustomer({ price: oldPrice })],
|
||||
});
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
// No options - should inherit from current
|
||||
@@ -393,19 +495,31 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Credits",
|
||||
});
|
||||
|
||||
const entitlement = entitlements.create({
|
||||
id: "ent_credits",
|
||||
featureId: "credits",
|
||||
featureName: "Credits",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
const oldPrice = prices.createPrepaid({
|
||||
id: "price_credits_old",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
billingUnits: 100,
|
||||
});
|
||||
|
||||
const newPrice = prices.createPrepaid({
|
||||
id: "price_credits_new",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
billingUnits: 250,
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({ prices: [newPrice] });
|
||||
const fullProduct = products.createFull({
|
||||
prices: [newPrice],
|
||||
entitlements: [entitlement],
|
||||
});
|
||||
|
||||
const cusProduct = customerProducts.create({
|
||||
options: [
|
||||
@@ -418,7 +532,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
customerPrices: [prices.createCustomer({ price: oldPrice })],
|
||||
});
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
};
|
||||
@@ -447,13 +561,24 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Credits",
|
||||
});
|
||||
|
||||
const entitlement = entitlements.create({
|
||||
id: "ent_credits",
|
||||
featureId: "credits",
|
||||
featureName: "Credits",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
const newPrice = prices.createPrepaid({
|
||||
id: "price_credits_new",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
billingUnits: 250,
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({ prices: [newPrice] });
|
||||
const fullProduct = products.createFull({
|
||||
prices: [newPrice],
|
||||
entitlements: [entitlement],
|
||||
});
|
||||
|
||||
const cusProduct = customerProducts.create({
|
||||
options: [
|
||||
@@ -466,7 +591,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
// No customerPrices - can't interpret stored quantity
|
||||
});
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
};
|
||||
@@ -493,19 +618,28 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Credits",
|
||||
});
|
||||
|
||||
const entitlement = entitlements.create({
|
||||
id: "ent_credits",
|
||||
featureId: "credits",
|
||||
featureName: "Credits",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
const fixedPrice = prices.createFixed({ id: "price_fixed" });
|
||||
const prepaidPrice = prices.createPrepaid({
|
||||
id: "price_credits",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({
|
||||
prices: [fixedPrice, prepaidPrice],
|
||||
entitlements: [entitlement],
|
||||
});
|
||||
|
||||
const cusProduct = customerProducts.create({ options: [] });
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
options: [{ feature_id: "credits", quantity: 50 }],
|
||||
@@ -528,10 +662,10 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
|
||||
describe("edge cases", () => {
|
||||
test("empty prices array returns empty array", () => {
|
||||
const fullProduct = products.createFull({ prices: [] });
|
||||
const fullProduct = products.createFull({ prices: [], entitlements: [] });
|
||||
const cusProduct = customerProducts.create({ options: [] });
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
};
|
||||
@@ -548,21 +682,25 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("throws error when feature not found for price", () => {
|
||||
test("throws error when entitlement not found for price", () => {
|
||||
const price = prices.createPrepaid({
|
||||
id: "price_credits",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({ prices: [price] });
|
||||
// No entitlements provided - entitlement won't be found
|
||||
const fullProduct = products.createFull({
|
||||
prices: [price],
|
||||
entitlements: [],
|
||||
});
|
||||
const cusProduct = customerProducts.create({ options: [] });
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
};
|
||||
|
||||
// Empty features array - feature won't be found
|
||||
const ctx = contexts.create({ features: [] });
|
||||
|
||||
expect(() =>
|
||||
@@ -572,7 +710,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
fullProduct,
|
||||
currentCustomerProduct: cusProduct,
|
||||
}),
|
||||
).toThrow("Feature not found for price");
|
||||
).toThrow("Entitlement not found for price");
|
||||
});
|
||||
|
||||
test("neither current nor new has quantity → feature not included", () => {
|
||||
@@ -581,15 +719,26 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Credits",
|
||||
});
|
||||
|
||||
const entitlement = entitlements.create({
|
||||
id: "ent_credits",
|
||||
featureId: "credits",
|
||||
featureName: "Credits",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
const price = prices.createPrepaid({
|
||||
id: "price_credits",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({ prices: [price] });
|
||||
const fullProduct = products.createFull({
|
||||
prices: [price],
|
||||
entitlements: [entitlement],
|
||||
});
|
||||
const cusProduct = customerProducts.create({ options: [] }); // No current options
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
// No options in params either
|
||||
@@ -614,12 +763,23 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Credits",
|
||||
});
|
||||
|
||||
const entitlement = entitlements.create({
|
||||
id: "ent_credits",
|
||||
featureId: "credits",
|
||||
featureName: "Credits",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
const price = prices.createPrepaid({
|
||||
id: "price_credits",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({ prices: [price] });
|
||||
const fullProduct = products.createFull({
|
||||
prices: [price],
|
||||
entitlements: [entitlement],
|
||||
});
|
||||
const cusProduct = customerProducts.create({
|
||||
options: [
|
||||
{
|
||||
@@ -631,7 +791,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
customerPrices: [prices.createCustomer({ price })],
|
||||
});
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
options: [], // Explicitly empty
|
||||
@@ -657,12 +817,23 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Credits",
|
||||
});
|
||||
|
||||
const entitlement = entitlements.create({
|
||||
id: "ent_credits",
|
||||
featureId: "credits",
|
||||
featureName: "Credits",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
const price = prices.createPrepaid({
|
||||
id: "price_credits",
|
||||
featureId: "credits",
|
||||
entitlementId: "ent_credits",
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({ prices: [price] });
|
||||
const fullProduct = products.createFull({
|
||||
prices: [price],
|
||||
entitlements: [entitlement],
|
||||
});
|
||||
const cusProduct = customerProducts.create({
|
||||
options: [
|
||||
{
|
||||
@@ -674,7 +845,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
customerPrices: [prices.createCustomer({ price })],
|
||||
});
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
options: [{ feature_id: "credits", quantity: 0 }],
|
||||
@@ -700,13 +871,25 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Credits",
|
||||
});
|
||||
|
||||
const entitlement = entitlements.create({
|
||||
id: "ent_credits",
|
||||
featureId: "credits",
|
||||
internalFeatureId: "internal_credits_v2",
|
||||
featureName: "Credits",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
const price = prices.createPrepaid({
|
||||
id: "price_credits",
|
||||
featureId: "credits",
|
||||
internalFeatureId: "internal_credits_v2",
|
||||
entitlementId: "ent_credits",
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({ prices: [price] });
|
||||
const fullProduct = products.createFull({
|
||||
prices: [price],
|
||||
entitlements: [entitlement],
|
||||
});
|
||||
const cusProduct = customerProducts.create({
|
||||
options: [
|
||||
{
|
||||
@@ -718,7 +901,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
customerPrices: [prices.createCustomer({ price })],
|
||||
});
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
};
|
||||
@@ -742,12 +925,23 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
name: "Seats",
|
||||
});
|
||||
|
||||
const entitlement = entitlements.create({
|
||||
id: "ent_seats",
|
||||
featureId: "seats",
|
||||
featureName: "Seats",
|
||||
allowance: 0,
|
||||
});
|
||||
|
||||
const price = prices.createPrepaid({
|
||||
id: "price_seats",
|
||||
featureId: "seats",
|
||||
entitlementId: "ent_seats",
|
||||
});
|
||||
|
||||
const fullProduct = products.createFull({ prices: [price] });
|
||||
const fullProduct = products.createFull({
|
||||
prices: [price],
|
||||
entitlements: [entitlement],
|
||||
});
|
||||
const cusProduct = customerProducts.create({
|
||||
options: [
|
||||
{
|
||||
@@ -759,7 +953,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
customerPrices: [prices.createCustomer({ price })],
|
||||
});
|
||||
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
// No options provided - should carry over from current
|
||||
|
||||
@@ -180,15 +180,15 @@ export const createCustomerPricesForProduct = ({
|
||||
|
||||
/**
|
||||
* Gets all stripe price IDs from a product helper result.
|
||||
* Entity-level products use stripeEmptyPriceId for consumable prices.
|
||||
* Note: As of current implementation, withEntity is hardcoded to false in customerProductToStripeItemSpecs,
|
||||
* so consumable prices always use the regular (non-empty) price ID.
|
||||
*/
|
||||
export const getStripePriceIds = (
|
||||
product: ReturnType<typeof createProductWithAllPriceTypes>,
|
||||
{ isEntityLevel = false }: { isEntityLevel?: boolean } = {},
|
||||
{ isEntityLevel: _isEntityLevel = false }: { isEntityLevel?: boolean } = {},
|
||||
): string[] => {
|
||||
const consumablePriceId = isEntityLevel
|
||||
? `stripe_${product.product.id}_consumable_empty`
|
||||
: `stripe_${product.product.id}_consumable`;
|
||||
// withEntity is hardcoded to false, so always use regular consumable price
|
||||
const consumablePriceId = `stripe_${product.product.id}_consumable`;
|
||||
|
||||
return [
|
||||
`stripe_${product.product.id}_fixed`,
|
||||
@@ -232,21 +232,16 @@ export const expectPhaseItems = (
|
||||
export const getExpectedPhaseItems = (
|
||||
product: ReturnType<typeof createProductWithAllPriceTypes>,
|
||||
{
|
||||
isEntityLevel = false,
|
||||
isEntityLevel: _isEntityLevel = false,
|
||||
fixedQuantityMultiplier = 1,
|
||||
}: { isEntityLevel?: boolean; fixedQuantityMultiplier?: number } = {},
|
||||
): ExpectedPhaseItem[] => {
|
||||
const productId = product.product.id;
|
||||
const { expectedQuantities } = product;
|
||||
|
||||
const consumablePriceId = isEntityLevel
|
||||
? `stripe_${productId}_consumable_empty`
|
||||
: `stripe_${productId}_consumable`;
|
||||
|
||||
// Entity consumable uses empty price with quantity 0
|
||||
const consumableQuantity = isEntityLevel
|
||||
? expectedQuantities.consumableEntity
|
||||
: expectedQuantities.consumable;
|
||||
// withEntity is hardcoded to false, so always use regular consumable price
|
||||
const consumablePriceId = `stripe_${productId}_consumable`;
|
||||
const consumableQuantity = expectedQuantities.consumable;
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -298,24 +293,19 @@ export type ExpectedSubscriptionItemUpdate = {
|
||||
* Gets expected subscription item updates for creating a new product.
|
||||
* Uses the expectedQuantities from the product helper.
|
||||
*
|
||||
* @param isEntityLevel - If true, uses stripeEmptyPriceId for consumable (quantity 0)
|
||||
* Note: As of current implementation, withEntity is hardcoded to false in customerProductToStripeItemSpecs,
|
||||
* so consumable prices always use the regular (non-empty) price ID.
|
||||
*/
|
||||
export const getExpectedNewProductItems = (
|
||||
product: ReturnType<typeof createProductWithAllPriceTypes>,
|
||||
{ isEntityLevel = false }: { isEntityLevel?: boolean } = {},
|
||||
{ isEntityLevel: _isEntityLevel = false }: { isEntityLevel?: boolean } = {},
|
||||
): ExpectedSubscriptionItemUpdate[] => {
|
||||
const productId = product.product.id;
|
||||
const { expectedQuantities } = product;
|
||||
|
||||
const consumablePriceId = isEntityLevel
|
||||
? `stripe_${productId}_consumable_empty`
|
||||
: `stripe_${productId}_consumable`;
|
||||
|
||||
// Entity consumable uses empty price with quantity 0
|
||||
// Customer consumable is metered (no quantity)
|
||||
const consumableQuantity = isEntityLevel
|
||||
? expectedQuantities.consumableEntity
|
||||
: expectedQuantities.consumable;
|
||||
// withEntity is hardcoded to false, so always use regular consumable price (metered, no quantity)
|
||||
const consumablePriceId = `stripe_${productId}_consumable`;
|
||||
const consumableQuantity = expectedQuantities.consumable;
|
||||
|
||||
const items: ExpectedSubscriptionItemUpdate[] = [
|
||||
{
|
||||
@@ -370,17 +360,17 @@ export const expectSubscriptionItemsUpdate = (
|
||||
* Creates stripe subscription items from product's expected prices.
|
||||
* Useful for setting up existing subscription state.
|
||||
*
|
||||
* Note: For customer-level (non-entity) products, metered consumable prices
|
||||
* are excluded because they don't have a meaningful quantity to compare.
|
||||
* Entity-level products use the empty price with quantity 0.
|
||||
* Note: As of current implementation, withEntity is hardcoded to false in customerProductToStripeItemSpecs,
|
||||
* so consumable prices always use the regular (non-empty) price ID. Metered consumable prices
|
||||
* are excluded by default because they don't have a meaningful quantity to compare.
|
||||
*
|
||||
* @param includeMetered - If true, includes metered consumable even for customer-level.
|
||||
* Use this when testing scenarios that need the full item set.
|
||||
* @param includeMetered - If true, includes metered consumable. Use this when testing scenarios
|
||||
* that need the full item set.
|
||||
*/
|
||||
export const createStripeItemsFromProduct = (
|
||||
product: ReturnType<typeof createProductWithAllPriceTypes>,
|
||||
{
|
||||
isEntityLevel = false,
|
||||
isEntityLevel: _isEntityLevel = false,
|
||||
itemIdPrefix = "si",
|
||||
includeMetered = false,
|
||||
}: {
|
||||
@@ -410,15 +400,9 @@ export const createStripeItemsFromProduct = (
|
||||
},
|
||||
];
|
||||
|
||||
// For entity-level, always include empty price with quantity 0
|
||||
// For customer-level, only include metered if explicitly requested
|
||||
if (isEntityLevel) {
|
||||
items.push({
|
||||
id: `${itemIdPrefix}_${productId}_consumable`,
|
||||
priceId: `stripe_${productId}_consumable_empty`,
|
||||
quantity: expectedQuantities.consumableEntity ?? 0,
|
||||
});
|
||||
} else if (includeMetered) {
|
||||
// Only include metered if explicitly requested (withEntity is hardcoded to false,
|
||||
// so always use regular consumable price)
|
||||
if (includeMetered) {
|
||||
items.push({
|
||||
id: `${itemIdPrefix}_${productId}_consumable`,
|
||||
priceId: `stripe_${productId}_consumable`,
|
||||
|
||||
@@ -105,12 +105,13 @@ describe(
|
||||
);
|
||||
expect(prepaidItem?.quantity).toBe(200);
|
||||
|
||||
// Consumable (entities use empty price): 0 + 0 = 0
|
||||
// Consumable (metered, withEntity hardcoded to false): no quantity
|
||||
const consumableItem = result.find((item) =>
|
||||
item.price?.includes("consumable"),
|
||||
);
|
||||
expect(consumableItem?.price).toBe("stripe_pro_consumable_empty");
|
||||
expect(consumableItem?.quantity).toBe(0);
|
||||
expect(consumableItem?.price).toBe("stripe_pro_consumable");
|
||||
// Metered prices should NOT have quantity
|
||||
expect("quantity" in (consumableItem ?? {})).toBe(false);
|
||||
|
||||
// Allocated: 5 + 5 = 10
|
||||
const allocatedItem = result.find((item) =>
|
||||
@@ -293,8 +294,8 @@ describe(
|
||||
finalCustomerProducts: [entity1ProProduct, entity2ProProduct],
|
||||
});
|
||||
|
||||
// Should update quantities (double them)
|
||||
expect(result).toHaveLength(3); // fixed, prepaid, allocated (consumable stays 0)
|
||||
// Should update quantities (double them) + add metered consumable
|
||||
expect(result).toHaveLength(4); // fixed, prepaid, allocated, consumable (metered)
|
||||
|
||||
const fixedItem = result.find((item) => item.id?.includes("fixed"));
|
||||
expect(fixedItem?.quantity).toBe(2);
|
||||
@@ -306,6 +307,12 @@ describe(
|
||||
item.id?.includes("allocated"),
|
||||
);
|
||||
expect(allocatedItem?.quantity).toBe(10);
|
||||
|
||||
// Metered consumable needs to be added
|
||||
const consumableItem = result.find((item) =>
|
||||
item.price?.includes("consumable"),
|
||||
);
|
||||
expect(consumableItem).toBeDefined();
|
||||
});
|
||||
|
||||
test("Remove one entity from two-entity subscription", () => {
|
||||
@@ -334,7 +341,7 @@ describe(
|
||||
subscriptionIds: ["sub_123"],
|
||||
});
|
||||
|
||||
// Current subscription has 2 entities worth of quantities
|
||||
// Current subscription has 2 entities worth of quantities (no consumable since withEntity is false)
|
||||
const stripeSubscription = stripeSubscriptions.create({
|
||||
id: "sub_123",
|
||||
items: [
|
||||
@@ -348,11 +355,6 @@ describe(
|
||||
priceId: "stripe_pro_prepaid",
|
||||
quantity: 200,
|
||||
},
|
||||
{
|
||||
id: "si_pro_consumable",
|
||||
priceId: "stripe_pro_consumable_empty",
|
||||
quantity: 0,
|
||||
},
|
||||
{
|
||||
id: "si_pro_allocated",
|
||||
priceId: "stripe_pro_allocated",
|
||||
@@ -373,8 +375,8 @@ describe(
|
||||
finalCustomerProducts: [entity1ProProduct],
|
||||
});
|
||||
|
||||
// Should update quantities (halve them)
|
||||
expect(result).toHaveLength(3); // fixed, prepaid, allocated
|
||||
// Should update quantities (halve them) + add metered consumable
|
||||
expect(result).toHaveLength(4); // fixed, prepaid, allocated, consumable
|
||||
|
||||
const fixedItem = result.find((item) => item.id?.includes("fixed"));
|
||||
expect(fixedItem?.quantity).toBe(1);
|
||||
@@ -386,6 +388,12 @@ describe(
|
||||
item.id?.includes("allocated"),
|
||||
);
|
||||
expect(allocatedItem?.quantity).toBe(5);
|
||||
|
||||
// Metered consumable needs to be added
|
||||
const consumableItem = result.find((item) =>
|
||||
item.price?.includes("consumable"),
|
||||
);
|
||||
expect(consumableItem).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -524,13 +532,12 @@ describe(
|
||||
finalCustomerProducts: [customerProProduct, entityProProduct],
|
||||
});
|
||||
|
||||
// Should have 5 items:
|
||||
// Should have 4 items (withEntity hardcoded to false, so both use metered):
|
||||
// - fixed (1+1=2)
|
||||
// - prepaid (100+50=150)
|
||||
// - allocated (5+3=8)
|
||||
// - consumable metered (customer)
|
||||
// - consumable empty (entity)
|
||||
expect(result).toHaveLength(5);
|
||||
// - consumable metered (both customer and entity use regular price)
|
||||
expect(result).toHaveLength(4);
|
||||
|
||||
// Fixed: 1 + 1 = 2
|
||||
const fixedItem = result.find((item) => item.price?.includes("fixed"));
|
||||
@@ -548,16 +555,11 @@ describe(
|
||||
);
|
||||
expect(allocatedItem?.quantity).toBe(8);
|
||||
|
||||
// Both consumable prices should be present
|
||||
// Only one consumable price (metered, withEntity is false)
|
||||
const consumableMetered = result.find(
|
||||
(item) => item.price === "stripe_pro_consumable",
|
||||
);
|
||||
const consumableEmpty = result.find(
|
||||
(item) => item.price === "stripe_pro_consumable_empty",
|
||||
);
|
||||
expect(consumableMetered).toBeDefined();
|
||||
expect(consumableEmpty).toBeDefined();
|
||||
expect(consumableEmpty?.quantity).toBe(0);
|
||||
// Metered should not have quantity
|
||||
expect("quantity" in (consumableMetered ?? {})).toBe(false);
|
||||
});
|
||||
@@ -588,7 +590,7 @@ describe(
|
||||
subscriptionIds: ["sub_123"],
|
||||
});
|
||||
|
||||
// Current subscription has both customer and entity
|
||||
// Current subscription has customer and entity (both use regular metered price - withEntity is false)
|
||||
const stripeSubscription = stripeSubscriptions.create({
|
||||
id: "sub_123",
|
||||
items: [
|
||||
@@ -598,16 +600,6 @@ describe(
|
||||
priceId: "stripe_pro_prepaid",
|
||||
quantity: 200,
|
||||
},
|
||||
{
|
||||
id: "si_pro_consumable",
|
||||
priceId: "stripe_pro_consumable",
|
||||
quantity: 0,
|
||||
},
|
||||
{
|
||||
id: "si_pro_consumable_empty",
|
||||
priceId: "stripe_pro_consumable_empty",
|
||||
quantity: 0,
|
||||
},
|
||||
{
|
||||
id: "si_pro_allocated",
|
||||
priceId: "stripe_pro_allocated",
|
||||
@@ -628,7 +620,7 @@ describe(
|
||||
finalCustomerProducts: [entityProProduct],
|
||||
});
|
||||
|
||||
// Should update quantities and remove metered consumable
|
||||
// Should update quantities + add metered consumable (wasn't in subscription)
|
||||
// Fixed: 2 -> 1
|
||||
const fixedItem = result.find((item) => item.id === "si_pro_fixed");
|
||||
expect(fixedItem?.quantity).toBe(1);
|
||||
@@ -643,11 +635,11 @@ describe(
|
||||
);
|
||||
expect(allocatedItem?.quantity).toBe(5);
|
||||
|
||||
// Metered consumable should be deleted
|
||||
const deletedMetered = result.find(
|
||||
(item) => item.id === "si_pro_consumable" && item.deleted,
|
||||
// Metered consumable should be added (wasn't in subscription)
|
||||
const consumableItem = result.find(
|
||||
(item) => item.price === "stripe_pro_consumable",
|
||||
);
|
||||
expect(deletedMetered).toBeDefined();
|
||||
expect(consumableItem).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -483,7 +483,7 @@ describe(
|
||||
expect(result[0].price).toBe("stripe_premium_consumable");
|
||||
});
|
||||
|
||||
test("Entity product, same quantities returns empty array", () => {
|
||||
test("Entity product, same quantities returns only metered addition", () => {
|
||||
const premium = createProductWithAllPriceTypes({
|
||||
productId: "premium",
|
||||
productName: "Premium",
|
||||
@@ -508,7 +508,8 @@ describe(
|
||||
entityId: "entity_1",
|
||||
});
|
||||
|
||||
// Entity-level includes the empty price with quantity 0
|
||||
// Entity-level subscription has 3 items (no metered consumable)
|
||||
// Note: withEntity is hardcoded to false, so entity products use regular metered prices
|
||||
const stripeSubscription = stripeSubscriptions.create({
|
||||
id: "sub_123",
|
||||
items: createStripeItemsFromProduct(premium, { isEntityLevel: true }),
|
||||
@@ -526,8 +527,9 @@ describe(
|
||||
finalCustomerProducts: [entityCustomerProduct],
|
||||
});
|
||||
|
||||
// No changes needed - all quantities match
|
||||
expect(result).toHaveLength(0);
|
||||
// Only the metered consumable needs to be added (wasn't in existing subscription)
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].price).toBe("stripe_premium_consumable");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -568,14 +570,14 @@ describe(
|
||||
]);
|
||||
});
|
||||
|
||||
test("Remove entity product marks all items including consumable as deleted", () => {
|
||||
test("Remove entity product marks all items as deleted", () => {
|
||||
const premium = createProductWithAllPriceTypes({
|
||||
productId: "premium",
|
||||
productName: "Premium",
|
||||
customerProductId: "cus_prod_premium_entity",
|
||||
});
|
||||
|
||||
// Entity-level subscription has 4 items (including empty consumable)
|
||||
// Entity-level subscription has 3 items (no metered consumable - withEntity is hardcoded to false)
|
||||
const stripeSubscription = stripeSubscriptions.create({
|
||||
id: "sub_123",
|
||||
items: createStripeItemsFromProduct(premium, { isEntityLevel: true }),
|
||||
@@ -593,12 +595,11 @@ describe(
|
||||
finalCustomerProducts: [],
|
||||
});
|
||||
|
||||
// Should delete all 4 items
|
||||
expect(result).toHaveLength(4);
|
||||
// Should delete all 3 items (fixed, prepaid, allocated)
|
||||
expect(result).toHaveLength(3);
|
||||
expectSubscriptionItemsUpdate(result, [
|
||||
{ id: "si_premium_fixed", deleted: true },
|
||||
{ id: "si_premium_prepaid", deleted: true },
|
||||
{ id: "si_premium_consumable", deleted: true },
|
||||
{ id: "si_premium_allocated", deleted: true },
|
||||
]);
|
||||
});
|
||||
@@ -753,7 +754,7 @@ describe(
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("Entity-level consumable uses stripe_empty_price_id with quantity 0", () => {
|
||||
test("Entity-level consumable uses regular metered price (withEntity hardcoded to false)", () => {
|
||||
const premium = createProductWithAllPriceTypes({
|
||||
productId: "premium",
|
||||
productName: "Premium",
|
||||
@@ -787,13 +788,14 @@ describe(
|
||||
finalCustomerProducts: [entityCustomerProduct],
|
||||
});
|
||||
|
||||
// Find the consumable item
|
||||
// Find the consumable item - now uses regular metered price (withEntity is hardcoded to false)
|
||||
const consumableItem = result.find((item) =>
|
||||
item.price?.includes("consumable"),
|
||||
);
|
||||
expect(consumableItem).toBeDefined();
|
||||
expect(consumableItem?.price).toBe("stripe_premium_consumable_empty");
|
||||
expect(consumableItem?.quantity).toBe(0);
|
||||
expect(consumableItem?.price).toBe("stripe_premium_consumable");
|
||||
// Metered prices should NOT have quantity
|
||||
expect("quantity" in (consumableItem ?? {})).toBe(false);
|
||||
});
|
||||
|
||||
test("Customer-level consumable (metered) has no quantity", () => {
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { UpdateSubscriptionV0Params } from "@autumn/shared";
|
||||
import type { UpdateSubscriptionV1Params } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
computeUpdateSubscriptionIntent,
|
||||
UpdateSubscriptionIntent,
|
||||
} from "@/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionIntent";
|
||||
|
||||
const baseParams: UpdateSubscriptionV0Params = {
|
||||
const baseParams: UpdateSubscriptionV1Params = {
|
||||
customer_id: "cus_test",
|
||||
product_id: "prod_test",
|
||||
};
|
||||
@@ -23,7 +23,7 @@ const baseParams: UpdateSubscriptionV0Params = {
|
||||
describe(chalk.yellowBright("computeUpdateSubscriptionIntent"), () => {
|
||||
describe(chalk.cyan("Version parameter priority"), () => {
|
||||
test("returns UpdatePlan when version is specified", () => {
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
...baseParams,
|
||||
version: 2,
|
||||
};
|
||||
@@ -34,7 +34,7 @@ describe(chalk.yellowBright("computeUpdateSubscriptionIntent"), () => {
|
||||
});
|
||||
|
||||
test("returns UpdatePlan when version is specified even with options", () => {
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
...baseParams,
|
||||
version: 3,
|
||||
options: [{ feature_id: "seats", quantity: 10 }],
|
||||
@@ -46,7 +46,7 @@ describe(chalk.yellowBright("computeUpdateSubscriptionIntent"), () => {
|
||||
});
|
||||
|
||||
test("returns UpdatePlan when version is 0", () => {
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
...baseParams,
|
||||
version: 0,
|
||||
};
|
||||
@@ -58,8 +58,8 @@ describe(chalk.yellowBright("computeUpdateSubscriptionIntent"), () => {
|
||||
});
|
||||
|
||||
describe(chalk.cyan("UpdateQuantity intent"), () => {
|
||||
test("returns UpdateQuantity when options provided without items", () => {
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
test("returns UpdateQuantity when options provided without customize", () => {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
...baseParams,
|
||||
options: [{ feature_id: "seats", quantity: 5 }],
|
||||
};
|
||||
@@ -70,7 +70,7 @@ describe(chalk.yellowBright("computeUpdateSubscriptionIntent"), () => {
|
||||
});
|
||||
|
||||
test("returns UpdateQuantity with multiple options", () => {
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
...baseParams,
|
||||
options: [
|
||||
{ feature_id: "seats", quantity: 5 },
|
||||
@@ -85,10 +85,12 @@ describe(chalk.yellowBright("computeUpdateSubscriptionIntent"), () => {
|
||||
});
|
||||
|
||||
describe(chalk.cyan("UpdatePlan intent (default)"), () => {
|
||||
test("returns UpdatePlan when items provided", () => {
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
test("returns UpdatePlan when customize provided", () => {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
...baseParams,
|
||||
items: [{ feature_id: "seats", included_usage: 10 }],
|
||||
customize: {
|
||||
items: [{ feature_id: "seats", included: 10 }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = computeUpdateSubscriptionIntent(params);
|
||||
@@ -96,11 +98,13 @@ describe(chalk.yellowBright("computeUpdateSubscriptionIntent"), () => {
|
||||
expect(result).toBe(UpdateSubscriptionIntent.UpdatePlan);
|
||||
});
|
||||
|
||||
test("returns UpdatePlan when both options and items provided", () => {
|
||||
const params: UpdateSubscriptionV0Params = {
|
||||
test("returns UpdatePlan when both options and customize provided", () => {
|
||||
const params: UpdateSubscriptionV1Params = {
|
||||
...baseParams,
|
||||
options: [{ feature_id: "seats", quantity: 5 }],
|
||||
items: [{ feature_id: "seats", included_usage: 10 }],
|
||||
customize: {
|
||||
items: [{ feature_id: "seats", included: 10 }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = computeUpdateSubscriptionIntent(params);
|
||||
|
||||
Reference in New Issue
Block a user