working on trials

This commit is contained in:
John Yeo
2025-08-23 13:19:31 -07:00
parent 040957d380
commit 1e8b032ad2
36 changed files with 1940 additions and 126 deletions

View File

@@ -81,11 +81,11 @@ export const handleSubscriptionUpdated = async ({
status: subStatusMap[subscription.status] || CusProductStatus.Unknown,
canceled_at: canceled ? canceledAt : null,
collection_method: fullSub.collection_method as CollectionMethod,
trial_ends_at:
previousAttributes.status === "trialing" &&
subscription.status === "active"
? null
: undefined,
// trial_ends_at:
// previousAttributes.status === "trialing" &&
// subscription.status === "active"
// ? null
// : undefined,
},
});

View File

@@ -32,6 +32,8 @@ import { getCustomerSub } from "../../attachUtils/convertAttachParams.js";
import { paramsToSubItems } from "../../mergeUtils/paramsToSubItems.js";
import { updateStripeSub2 } from "../upgradeFlow/updateStripeSub2.js";
import { subToNewSchedule } from "../../mergeUtils/subToNewSchedule.js";
import { isTrialing } from "@/internal/customers/cusProducts/cusProductUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
export const handlePaidProduct = async ({
req,
@@ -69,19 +71,42 @@ export const handlePaidProduct = async ({
let subscriptions: Stripe.Subscription[] = [];
// Only merge if no free trials
let mergeCusProduct = undefined;
if (!config.disableMerge && !freeTrial) {
mergeCusProduct = cusProducts?.find((cp) =>
products.some((p) => p.group == cp.product.group)
);
}
const mergeSub = await getCustomerSub({ attachParams });
// if (!config.disableMerge && !freeTrial) {
// mergeCusProduct = cusProducts?.find((cp) =>
// products.some((p) => p.group == cp.product.group)
// );
// }
const { sub: mergeSub, cusProduct: mergeCusProduct } = await getCustomerSub({
attachParams,
});
let sub: Stripe.Subscription | null = null;
let schedule: Stripe.SubscriptionSchedule | null = null;
let trialEndsAt = undefined;
console.log("Merge sub:", mergeSub?.id);
console.log("Merge cus product:", mergeCusProduct?.product.id);
console.log(
"Trial ends at:",
formatUnixToDate(mergeCusProduct?.trial_ends_at || 0)
);
throw new Error("test");
// 1. If merge sub
if (mergeSub) {
if (mergeCusProduct?.free_trial) {
if (isTrialing({ cusProduct: mergeCusProduct, now: attachParams.now })) {
attachParams.freeTrial = mergeCusProduct.free_trial;
trialEndsAt = mergeCusProduct.trial_ends_at;
} else {
attachParams.freeTrial = null;
trialEndsAt = undefined;
}
}
// 1. If merged sub is canceled, also add to current schedule
const newItemSet = await paramsToSubItems({
req,
@@ -194,6 +219,7 @@ export const handlePaidProduct = async ({
anchorToUnix,
carryExistingUsages: config.carryUsage,
scenario: AttachScenario.New,
trialEndsAt: trialEndsAt || undefined,
logger,
})
);

View File

@@ -104,11 +104,19 @@ export const handleScheduleFunction2 = async ({
if (currentPhaseIndex == newItems.phases.length - 1) {
console.log(`NO SUBSEQUENT PHASES, RELEASING SCHEDULE`);
await stripeCli.subscriptionSchedules.release(schedule!.id);
await CusProductService.updateByStripeScheduledId({
db: req.db,
stripeScheduledId: schedule!.id,
updates: { scheduled_ids: [] },
});
await CusProductService.update({
db: req.db,
cusProductId: curCusProduct!.id,
updates: {
scheduled_ids: [],
canceled: true,
canceled_at: Date.now(),
ended_at: expectedEnd * 1000,
},
});
schedule = undefined;
@@ -126,6 +134,7 @@ export const handleScheduleFunction2 = async ({
cusProductId: curCusProduct!.id,
updates: {
scheduled_ids: [schedule!.id],
canceled_at: Date.now(),
canceled: true,
ended_at: expectedEnd * 1000,
},

View File

@@ -26,14 +26,9 @@ import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSu
import { updateStripeSub2 } from "./updateStripeSub2.js";
import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { paramsToSubItems } from "../../mergeUtils/paramsToSubItems.js";
import { paramsToScheduleItems } from "../../mergeUtils/paramsToScheduleItems.js";
import { updateCurSchedule } from "../../mergeUtils/updateCurSchedule.js";
import {
getCurrentPhaseIndex,
logPhases,
} from "../../mergeUtils/phaseUtils/phaseUtils.js";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import { shouldCancelSub } from "./upgradeFlowUtils.js";
import { handleUpgradeFlowSchedule } from "./handleUpgradeFlowSchedule.js";
export const handleUpgradeFlow = async ({
req,
@@ -65,7 +60,7 @@ export const handleUpgradeFlow = async ({
const newItemSet = await paramsToSubItems({
req,
sub: curSub!,
sub: curSub,
attachParams,
config,
});
@@ -90,9 +85,12 @@ export const handleUpgradeFlow = async ({
}
}
// Cancel sub...
let canceled = false;
if (shouldCancelSub({ sub: curSub!, newSubItems: subItems })) {
// SCENARIO 1, NO SUB:
if (!curSub) {
console.log("UPGRADE FLOW, NO SUB (FROM CANCEL MAYBE...?)");
// Do something about current sub...
} else if (shouldCancelSub({ sub: curSub!, newSubItems: subItems })) {
console.log(
`UPGRADE FLOW, CANCELLING SUB ${curSub!.id}, PRORATE: ${config.proration}`
);
@@ -106,8 +104,8 @@ export const handleUpgradeFlow = async ({
},
});
} else if (subItems.length > 0) {
console.log(`UPGRADE FLOW, UPDATING SUB ${curSub!.id}`);
itemSet.subItems = subItems;
console.log("New sub items:", subItems);
const res = await updateStripeSub2({
req,
@@ -119,45 +117,14 @@ export const handleUpgradeFlow = async ({
});
const schedule = await paramsToCurSubSchedule({ attachParams });
// Add to schedule?
if (schedule) {
console.log("CUR ITEMS:");
await logPhases({
phases: schedule.phases as any,
db: req.db,
});
const currentPhaseIndex = getCurrentPhaseIndex({
await handleUpgradeFlowSchedule({
req,
attachParams,
config,
schedule,
now: attachParams.now,
curSub,
});
const nextPhaseIndex = currentPhaseIndex + 1;
if (nextPhaseIndex < schedule.phases.length) {
const newItems = await paramsToScheduleItems({
req,
schedule,
attachParams,
config,
billingPeriodEnd: schedule?.phases?.[nextPhaseIndex]?.start_date,
});
console.log("NEW ITEMS:");
await logPhases({
phases: newItems.phases,
db: req.db,
});
await updateCurSchedule({
req,
attachParams,
schedule,
newPhases: newItems.phases,
sub: curSub!,
});
}
}
attachParams.replaceables = res.replaceables || [];
@@ -227,9 +194,3 @@ export const handleUpgradeFlow = async ({
}
}
};
// const newSubItems = await removeCurCusProductItems({
// sub: curSub,
// cusProduct: curCusProduct!,
// subItems: itemSet.subItems,
// });

View File

@@ -0,0 +1,112 @@
import { ExtendedRequest } from "@/utils/models/Request.js";
import { AttachConfig } from "@autumn/shared";
import Stripe from "stripe";
import { paramsToScheduleItems } from "../../mergeUtils/paramsToScheduleItems.js";
import {
logPhases,
getCurrentPhaseIndex,
} from "../../mergeUtils/phaseUtils/phaseUtils.js";
import { updateCurSchedule } from "../../mergeUtils/updateCurSchedule.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
ACTIVE_STATUSES,
CusProductService,
} from "@/internal/customers/cusProducts/CusProductService.js";
import { attachParamsToCurCusProduct } from "../../attachUtils/convertAttachParams.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
export const handleUpgradeFlowSchedule = async ({
req,
attachParams,
config,
schedule,
curSub,
}: {
req: ExtendedRequest;
attachParams: AttachParams;
config: AttachConfig;
schedule: Stripe.SubscriptionSchedule;
curSub: Stripe.Subscription;
}) => {
console.log(`UPGRADE FLOW, UPDATING SCHEDULE ${schedule.id}`);
const { stripeCli, customer, prices } = attachParams;
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
// console.log("CUR ITEMS:");
// await logPhases({
// phases: schedule.phases as any,
// db: req.db,
// });
const currentPhaseIndex = getCurrentPhaseIndex({
schedule,
now: attachParams.now,
});
const nextPhaseIndex = currentPhaseIndex + 1;
if (currentPhaseIndex == -1 || nextPhaseIndex >= schedule.phases.length)
return;
const newItems = await paramsToScheduleItems({
req,
schedule,
attachParams,
config,
billingPeriodEnd: schedule?.phases?.[nextPhaseIndex]?.start_date,
});
// Should release schedule...
const newCurPhaseIndex = getCurrentPhaseIndex({
schedule: { phases: newItems.phases } as any,
now: attachParams.now,
});
// If there are no subsequent phases, release schedule...
// Example: mergedUpgrade4.test.ts, mergedCancel2.test.ts
// pro, pro -> free, pro -> premium, pro (need to cancel initial schedule)
if (newCurPhaseIndex == newItems.phases.length - 1) {
console.log(`NO SUBSEQUENT PHASES, RELEASING SCHEDULE`);
await stripeCli.subscriptionSchedules.release(schedule!.id);
await CusProductService.updateByStripeScheduledId({
db: req.db,
stripeScheduledId: schedule!.id,
updates: { scheduled_ids: [] },
});
// Should we cancel the sub...?
// If all other products are canceled, and new product is free, cancel the sub...
const shouldCancelSub =
customer.customer_products
.filter(
(cp) =>
cp.id !== curCusProduct?.id &&
cp.subscription_ids?.includes(curSub.id) &&
ACTIVE_STATUSES.includes(cp.status)
)
.every((cp) => cp.canceled) && isFreeProduct(prices);
if (shouldCancelSub) {
console.log(`UPGRADE FLOW, CANCELING SUB: ${curSub.id}`);
await stripeCli.subscriptions.update(curSub.id, {
cancel_at_period_end: true,
});
}
return;
}
await logPhases({
phases: newItems.phases,
db: req.db,
});
await updateCurSchedule({
req,
attachParams,
schedule,
newPhases: newItems.phases,
sub: curSub!,
});
};

View File

@@ -40,15 +40,13 @@ export const updateStripeSub2 = async ({
});
}
let trialEnd = config.disableTrial
? undefined
: freeTrialToStripeTimestamp({
freeTrial: attachParams.freeTrial,
now: attachParams.now,
});
console.log("Sub items:", sanitizeSubItems(itemSet.subItems));
console.log("Invoice items:", itemSet.invoiceItems);
let trialEnd =
config.disableTrial || config.carryTrial
? undefined
: freeTrialToStripeTimestamp({
freeTrial: attachParams.freeTrial,
now: attachParams.now,
});
// 1. Update subscription
let updatedSub = await stripeCli.subscriptions.update(curSub.id, {

View File

@@ -34,7 +34,7 @@ export const priceToUnusedPreviewItem = ({
org?: Organization;
}) => {
now = now || Date.now();
const onTrial = isTrialing(cusProduct);
const onTrial = isTrialing({ cusProduct, now });
// 1. Get price from stripe items
const subItem = findStripeItemForPrice({

View File

@@ -3,6 +3,7 @@ import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExi
import { CusProductStatus } from "@autumn/shared";
import Stripe from "stripe";
import { cusProductToProduct } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { isTrialing } from "../../cusProducts/cusProductUtils.js";
export const attachParamsToCurCusProduct = ({
attachParams,
@@ -118,17 +119,34 @@ export const getCustomerSub = async ({
return 0;
});
const subId = cusProducts.flatMap((cp) => cp.subscription_ids || [])?.[0];
// const subId = cusProducts.flatMap((cp) => cp.subscription_ids || [])?.[0];
const cusProduct = cusProducts.find(
(cp) => cp.subscription_ids && cp.subscription_ids.length > 0
);
if (!subId) {
return undefined;
if (!cusProduct) return { sub: undefined, cusProduct: undefined };
const subId = cusProduct.subscription_ids![0];
// If there's only one customer product on sub, and it's still trialing, return undefined, because should just replace sub.
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
const cusProductsOnSub = cusProducts.filter(
(cp) =>
cp.subscription_ids?.includes(cusProduct.subscription_ids![0]) &&
curCusProduct?.id == cp.id
);
if (
cusProductsOnSub.length === 1 &&
isTrialing({ cusProduct, now: attachParams.now })
) {
return { sub: undefined, cusProduct: undefined };
}
const sub = await stripeCli.subscriptions.retrieve(subId, {
expand: ["items.data.price.tiers"],
});
return sub;
return { sub, cusProduct };
};
export const paramsToCurSub = async ({

View File

@@ -288,7 +288,7 @@ const getChangeProductBranch = async ({
let isUpgrade = isProductUpgrade({ prices1: curPrices, prices2: newPrices });
if (isUpgrade) {
if (isTrialing(curMainProduct!)) {
if (isTrialing({ cusProduct: curMainProduct!, now: attachParams.now })) {
return AttachBranch.MainIsTrial;
}

View File

@@ -172,15 +172,6 @@ export const runAttachFunction = async ({
});
}
// // 1. Cancel future schedule before creating a new one...
// await deleteCurrentScheduledProduct({
// req,
// org,
// attachParams,
// attachFunc: attachFunction,
// logger,
// });
// 2. If main is trial, cancel it...
if (branch == AttachBranch.MainIsTrial) {
await CusProductService.update({
@@ -271,3 +262,12 @@ export const runAttachFunction = async ({
});
}
};
// // 1. Cancel future schedule before creating a new one...
// await deleteCurrentScheduledProduct({
// req,
// org,
// attachParams,
// attachFunc: attachFunction,
// logger,
// });

View File

@@ -26,6 +26,8 @@ import {
import { isFreeProduct } from "@/internal/products/productUtils.js";
import { getMergeCusProduct } from "../attachFunctions/addProductFlow/getMergeCusProduct.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
import { isTrialing } from "../../cusProducts/cusProductUtils.js";
const getNextCycleItems = async ({
newProduct,
@@ -35,6 +37,7 @@ const getNextCycleItems = async ({
withPrepaid,
logger,
config,
trialEnds,
}: {
newProduct: FullProduct;
attachParams: AttachParams;
@@ -43,6 +46,7 @@ const getNextCycleItems = async ({
withPrepaid?: boolean;
logger: any;
config: AttachConfig;
trialEnds?: number | null;
}) => {
// 1. If one off, return null
if (branch == AttachBranch.OneOff) return null;
@@ -50,11 +54,15 @@ const getNextCycleItems = async ({
// 2. If free trial
let nextCycleAt = undefined;
if (attachParams.freeTrial) {
nextCycleAt =
freeTrialToStripeTimestamp({
freeTrial: attachParams.freeTrial,
now: attachParams.now,
})! * 1000;
if (trialEnds) {
nextCycleAt = trialEnds;
} else {
nextCycleAt =
freeTrialToStripeTimestamp({
freeTrial: attachParams.freeTrial,
now: attachParams.now,
})! * 1000;
}
} else if (anchorToUnix) {
// Yearly one
const largestInterval = getLargestInterval({ prices: newProduct.prices });
@@ -113,17 +121,35 @@ export const getNewProductPreview = async ({
// products: [newProduct],
// config,
// });
const mergeSub = await getCustomerSub({ attachParams });
const { sub: mergeSub, cusProduct: mergeCusProduct } = await getCustomerSub({
attachParams,
});
// console.log("Merge sub:", mergeSub);
// console.log("Merge sub:", mergeSub?.id);
// console.log("Merge cus product:", mergeCusProduct?.product.id);
// console.log(
// "Trial ends at:",
// formatUnixToDate(mergeCusProduct?.trial_ends_at || 0)
// );
let trialEnds = undefined;
if (mergeSub) {
const { start } = subToPeriodStartEnd({ sub: mergeSub });
if (mergeCusProduct?.free_trial) {
// 1. If still on trial
if (isTrialing({ cusProduct: mergeCusProduct, now: attachParams.now })) {
trialEnds = mergeCusProduct.trial_ends_at;
attachParams.freeTrial = mergeCusProduct.free_trial;
} else {
attachParams.freeTrial = null;
}
}
const smallestInterval = getSmallestInterval({
prices: newProduct.prices,
excludeOneOff: true,
});
if (smallestInterval) {
anchorToUnix = addBillingIntervalUnix({
unixTimestamp: start * 1000,
@@ -154,6 +180,7 @@ export const getNewProductPreview = async ({
withPrepaid,
logger,
config,
trialEnds,
});
// console.log("Due next cycle", dueNextCycle);

View File

@@ -60,7 +60,7 @@ const getNextCycleAt = ({
if (
branch == AttachBranch.NewVersion &&
curCusProduct &&
isTrialing(curCusProduct)
isTrialing({ cusProduct: curCusProduct, now })
) {
return {
next_cycle_at: curCusProduct.trial_ends_at,

View File

@@ -102,17 +102,6 @@ export const paramsToSubItems = async ({
}) => {
const { logger } = req;
let curSubItems = sub?.items.data || [];
// if (scheduleSet) {
// const scheduleItems = scheduleSet.schedule.phases[0].items.map((item) => ({
// id: item.price,
// price: {
// id: item.price,
// },
// quantity: item.quantity,
// }));
// curSubItems = scheduleItems as any;
// }
const itemSet = await getStripeSubItems2({
attachParams,
@@ -127,13 +116,6 @@ export const paramsToSubItems = async ({
curSubItems,
});
// console.log("New sub items:");
// await logPhaseItems({
// db: req.db,
// items: newSubItems as any,
// });
// console.log("--------------------------------");
const allCusProducts = attachParams.customer.customer_products;
// 3. Remove items related to cus products to remove

View File

@@ -23,6 +23,7 @@ import { handleRenewProduct } from "../attach/attachFunctions/handleRenewProduct
import { getDefaultAttachConfig } from "../attach/attachUtils/getAttachConfig.js";
import { handleScheduleFunction2 } from "../attach/attachFunctions/scheduleFlow/handleScheduleFlow2.js";
import { handleUpgradeFlow } from "../attach/attachFunctions/upgradeFlow/handleUpgradeFlow.js";
import { activateDefaultProduct } from "../cusProducts/cusProductUtils.js";
export const handleCancelProduct = async ({
req,
@@ -181,6 +182,14 @@ export const handleCancelProduct = async ({
: ProrationBehavior.None,
},
});
// Activate default product
await activateDefaultProduct({
req,
productGroup: cusProduct.product.group,
fullCus,
curCusProduct: cusProduct,
});
return;
// Expire product immediately

View File

@@ -579,8 +579,16 @@ export const searchCusProducts = ({
});
};
export const isTrialing = (cusProduct: FullCusProduct) => {
return cusProduct.trial_ends_at && cusProduct.trial_ends_at > Date.now();
export const isTrialing = ({
cusProduct,
now,
}: {
cusProduct: FullCusProduct;
now?: number;
}) => {
return (
cusProduct.trial_ends_at && cusProduct.trial_ends_at > (now || Date.now())
);
};
export const getMainCusProduct = async ({

View File

@@ -0,0 +1,5 @@
import { FullCusProduct } from "@autumn/shared";
export const isCanceled = ({ cusProduct }: { cusProduct: FullCusProduct }) => {
return cusProduct.canceled;
};

View File

@@ -161,8 +161,8 @@ export const cusProductToSchedule = async ({
}
);
if (schedule.status == "canceled") {
return null;
if (schedule.status == "canceled" || schedule.status == "released") {
return undefined;
}
return schedule;

View File

@@ -21,7 +21,7 @@ import {
subYears,
} from "date-fns";
import { UTCDate } from "@date-fns/utc";
import { formatUnixToDate, formatUnixToDateTime } from "@/utils/genUtils.js";
import { formatUnixToDateTime } from "@/utils/genUtils.js";
export const subtractBillingIntervalUnix = ({
unixTimestamp,

View File

@@ -6,6 +6,7 @@ import {
isProductUpgrade,
} from "../../productUtils.js";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import { isCanceled } from "@/internal/customers/cusProducts/cusProductUtils/classifyCusProduct.js";
export const getAttachScenario = ({
fullCus,
@@ -19,6 +20,7 @@ export const getAttachScenario = ({
let { curMainProduct, curScheduledProduct } = getExistingCusProducts({
product: fullProduct,
cusProducts: fullCus?.customer_products || [],
internalEntityId: fullCus?.entity?.internal_id,
});
if (!curMainProduct || fullProduct.is_add_on) return AttachScenario.New;
@@ -29,7 +31,7 @@ export const getAttachScenario = ({
// 1. If current product is the same as the product, return active
if (curMainProduct?.product.id == fullProduct.id) {
if (curMainProduct.canceled_at != null) {
if (isCanceled({ cusProduct: curMainProduct })) {
return AttachScenario.Renew;
} else return AttachScenario.Active;
}

View File

@@ -0,0 +1,185 @@
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 {
APIVersion,
AppEnv,
CusProductStatus,
Organization,
priceToInvoiceAmount,
} 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 { 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 { 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";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
let premium = constructProduct({
id: "premium",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
});
const wordsUsage = 300000;
const ops = [
{
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
},
];
const testCase = "cancel2";
describe(`${chalk.yellowBright("cancel2: Testing cancel at period end (with usage)")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.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,
});
} catch (error) {
console.log(`Operation failed: ${op.product.id}, index: ${index}`);
throw error;
}
}
});
it("should track usage cancel, advance test clock 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 autumn.cancel({
customer_id: customerId,
product_id: premium.id,
cancel_immediately: false,
});
await advanceToNextInvoice({
stripeCli,
testClockId,
});
const wordsAmount = await getExpectedInvoiceTotal({
db,
org,
env,
onlyIncludeArrear: true,
usage: [
{
featureId: TestFeature.Words,
value: wordsUsage,
},
],
stripeCli,
customerId,
productId: premium.id,
expectExpired: true,
});
const cus = await autumn.customers.get(customerId);
const prods = cus.products.filter((p) => p.group == premium.group);
expect(prods.length).to.equal(0);
expect(cus.invoices.length).to.equal(2);
expect(cus.invoices[0].total).to.equal(wordsAmount);
});
});

View File

@@ -0,0 +1,161 @@
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 {
APIVersion,
AppEnv,
CusProductStatus,
Organization,
priceToInvoiceAmount,
} from "@autumn/shared";
import {
constructArrearItem,
constructArrearProratedItem,
constructFeatureItem,
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 { 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 { 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";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
let premium = constructProduct({
id: "premium",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
});
let free = constructProduct({
id: "free",
items: [
constructFeatureItem({
featureId: TestFeature.Words,
}),
],
type: "free",
isDefault: false,
});
const ops = [
{
product: free,
results: [{ product: free, status: CusProductStatus.Active }],
skipSubCheck: true,
},
];
const testCase = "cancel3";
describe(`${chalk.yellowBright("cancel3: Cancelling free product")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.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: [free],
prefix: testCase,
});
await createProducts({
autumn: autumnJs,
products: [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 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,
skipSubCheck: op.skipSubCheck,
});
} catch (error) {
console.log(`Operation failed: ${op.product.id}, index: ${index}`);
throw error;
}
}
});
it("should track usage cancel, advance test clock and have correct invoice", async function () {
const cus1 = await autumn.customers.get(customerId);
await autumn.cancel({
customer_id: customerId,
product_id: free.id,
cancel_immediately: true,
});
const cus = await autumn.customers.get(customerId);
const prods = cus.products.filter((p) => p.group == free.group);
expect(prods.length).to.equal(0);
});
});

View File

@@ -0,0 +1,152 @@
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 {
APIVersion,
AppEnv,
CusProductStatus,
Organization,
} from "@autumn/shared";
import {
constructArrearItem,
constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
let premium = constructProduct({
id: "premium",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
});
let addOn = constructProduct({
id: "free_add_on",
items: [constructFeatureItem({ featureId: TestFeature.Credits })],
type: "free",
isAddOn: true,
isDefault: false,
});
const ops = [
{
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
skipSubCheck: true,
},
{
product: addOn,
results: [
{ product: premium, status: CusProductStatus.Active },
{ product: addOn, status: CusProductStatus.Active },
],
},
];
const testCase = "cancel4";
describe(`${chalk.yellowBright("cancel4: Cancelling free add on product")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.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, addOn],
prefix: testCase,
});
await createProducts({
autumn: autumnJs,
products: [premium, addOn],
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,
skipSubCheck: op.skipSubCheck,
});
} catch (error) {
console.log(`Operation failed: ${op.product.id}, index: ${index}`);
throw error;
}
}
});
it("should track usage cancel, advance test clock and have correct invoice", async function () {
await autumn.cancel({
customer_id: customerId,
product_id: addOn.id,
cancel_immediately: true,
});
const cus = await autumn.customers.get(customerId);
expectProductAttached({
customer: cus,
product: premium,
});
const products = cus.products.filter((p) => p.group == addOn.group);
expect(products.length).to.equal(1);
});
});

View File

@@ -0,0 +1,200 @@
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 {
APIVersion,
AppEnv,
AttachBranch,
AttachScenario,
CusProductStatus,
Organization,
} from "@autumn/shared";
import {
constructArrearItem,
constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
let premium = constructProduct({
id: "premium",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
});
const ops = [
{
entityId: "1",
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
skipSubCheck: true,
},
{
entityId: "2",
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
},
];
const cancels = [
{
entityId: "1",
product: premium,
},
{
entityId: "2",
product: premium,
shouldBeCanceled: true,
},
];
const renewals = [
{
entityId: "1",
product: premium,
},
{
entityId: "2",
product: premium,
},
];
const testCase = "mergedCancel1";
describe(`${chalk.yellowBright("mergedCancel1: Merged cancel")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.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,
skipSubCheck: op.skipSubCheck,
entityId: op.entityId,
});
} catch (error) {
console.log(`Operation failed: ${op.product.id}, index: ${index}`);
throw error;
}
}
});
it("should track usage cancel, advance test clock and have correct invoice", async function () {
for (const cancel of cancels) {
await autumn.cancel({
customer_id: customerId,
product_id: cancel.product.id,
entity_id: cancel.entityId,
cancel_immediately: false,
});
await expectSubToBeCorrect({
db,
customerId,
org,
env,
shouldBeCanceled: cancel.shouldBeCanceled,
});
}
});
it("should renew both entities", async function () {
for (const renewal of renewals) {
const checkout = await autumn.checkout({
customer_id: customerId,
product_id: renewal.product.id,
entity_id: renewal.entityId,
});
expect(checkout.product.scenario).to.equal(AttachScenario.Renew);
expect(checkout.total).to.equal(0);
const attach = await autumn.attach({
customer_id: customerId,
product_id: renewal.product.id,
entity_id: renewal.entityId,
});
await expectSubToBeCorrect({
db,
customerId,
org,
env,
});
}
});
});

View File

@@ -0,0 +1,160 @@
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 {
APIVersion,
AppEnv,
CusProductStatus,
Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
// Premium, Premium
// Cancel End, Cancel Immediately
// Results: Canceled sub
let premium = constructProduct({
id: "premium",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
});
const ops = [
{
entityId: "1",
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
},
{
entityId: "2",
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
},
];
const cancels = [
{
entityId: "1",
product: premium,
},
{
entityId: "2",
product: premium,
cancelImmediately: true,
shouldBeCanceled: true,
},
];
const testCase = "mergedCancel2";
describe(`${chalk.yellowBright("mergedCancel2: Testing cancel immediately")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.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,
entityId: op.entityId,
});
} catch (error) {
console.log(`Operation failed: ${op.product.id}, index: ${index}`);
throw error;
}
}
});
it("should track usage cancel, advance test clock and have correct invoice", async function () {
for (const cancel of cancels) {
await autumn.cancel({
customer_id: customerId,
product_id: cancel.product.id,
entity_id: cancel.entityId,
cancel_immediately: cancel.cancelImmediately ?? false,
});
await expectSubToBeCorrect({
db,
customerId,
org,
env,
shouldBeCanceled: cancel.shouldBeCanceled,
});
}
});
});

View File

@@ -0,0 +1,165 @@
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 {
APIVersion,
AppEnv,
AttachScenario,
CusProductStatus,
Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
// Premium, Premium
// Cancel Immediately, Cancel Immediately
// Results: No sub
let premium = constructProduct({
id: "premium",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
});
const ops = [
{
entityId: "1",
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
},
{
entityId: "2",
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
},
];
const cancels = [
{
entityId: "1",
product: premium,
cancelImmediately: true,
},
{
entityId: "2",
product: premium,
cancelImmediately: true,
shouldBeCanceled: true,
skipSubCheck: true,
},
];
const testCase = "mergedCancel3";
describe(`${chalk.yellowBright("mergedCancel3: Testing cancel immediately")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.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,
entityId: op.entityId,
});
} catch (error) {
console.log(`Operation failed: ${op.product.id}, index: ${index}`);
throw error;
}
}
});
it("should track usage cancel, advance test clock and have correct invoice", async function () {
for (const cancel of cancels) {
await autumn.cancel({
customer_id: customerId,
product_id: cancel.product.id,
entity_id: cancel.entityId,
cancel_immediately: cancel.cancelImmediately ?? false,
});
if (cancel.skipSubCheck) continue;
await expectSubToBeCorrect({
db,
customerId,
org,
env,
shouldBeCanceled: cancel.shouldBeCanceled,
});
}
});
});

View File

@@ -19,6 +19,11 @@ import { expect } from "chai";
import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
// OPERATIONS:
// Premium, Premium
// Pro, Pro
// Premium, Premium
// UNCOMMENT FROM HERE
let premium = constructProduct({
id: "premium",

View File

@@ -24,7 +24,12 @@ import { expect } from "chai";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
// UNCOMMENT FROM HERE
// OPERATIONS:
// Premium
// Free
// Free, Premium
// Free, Pro
let free = constructProduct({
id: "free",
items: [constructFeatureItem({ featureId: TestFeature.Words })],

View File

@@ -22,7 +22,10 @@ import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { expect } from "chai";
// UNCOMMENT FROM HERE
// OPERATIONS:
// Pro, Pro
// Free, Premium
let free = constructProduct({
id: "free",
items: [constructFeatureItem({ featureId: TestFeature.Words })],

View File

@@ -25,7 +25,10 @@ import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addMonths } from "date-fns";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
// UNCOMMENT FROM HERE
// OPERATIONS:
// PremiumAnnual, Premium
// PremiumAnnual, Pro
let premiumAnnual = constructProduct({
id: "premiumAnnual",
items: [constructArrearItem({ featureId: TestFeature.Words })],

View File

@@ -24,7 +24,10 @@ import { expect } from "chai";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
// UNCOMMENT FROM HERE
// OPERATIONS:
// Premium, Premium
// Free, Free
// Pro, Free
let pro = constructProduct({
id: "pro",

View File

@@ -21,7 +21,12 @@ import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUti
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
// UNCOMMENT FROM HERE
// OPERATIONS:
// Growth, Growth
// Free
// Pro
// Premium
// Free
let pro = constructProduct({
id: "pro",

View File

@@ -0,0 +1,154 @@
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 {
APIVersion,
AppEnv,
CusProductStatus,
Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addDays } from "date-fns";
// Premium, Premium
// Cancel End, Cancel Immediately
// Results: Canceled sub
let premium = constructProduct({
id: "premium",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
trial: true,
});
const ops = [
{
entityId: "1",
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
},
// {
// entityId: "2",
// product: premium,
// results: [{ product: premium, status: CusProductStatus.Active }],
// },
];
const testCase = "mergedTrial1";
describe(`${chalk.yellowBright("mergedTrial1: Testing trial")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.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 attach first trial, and advance clock past trial", async function () {
await autumn.entities.create(customerId, entities);
await autumn.attach({
customer_id: customerId,
product_id: premium.id,
entity_id: "1",
});
await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addDays(new Date(), 2).getTime(),
});
const entity1 = await autumn.entities.get(customerId, "1");
const premium1 = entity1.products.find((p: any) => p.id == premium.id);
const checkout = await autumn.checkout({
customer_id: customerId,
product_id: premium.id,
entity_id: "2",
});
const nextCycle = checkout.next_cycle;
expect(nextCycle?.starts_at);
expect(nextCycle?.starts_at).to.approximately(
premium1?.current_period_end,
60000
); // 1 min
await autumn.attach({
customer_id: customerId,
product_id: premium.id,
entity_id: "2",
});
const entity2 = await autumn.entities.get(customerId, "2");
const premium2 = entity2.products.find((p: any) => p.id == premium.id);
expect(premium2?.status).to.equal(CusProductStatus.Trialing);
expect(premium2?.current_period_end).to.approximately(
premium1?.current_period_end,
60000
); // 1 min
});
});

View File

@@ -0,0 +1,150 @@
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 {
APIVersion,
AppEnv,
CusProductStatus,
Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addDays } from "date-fns";
let premium = constructProduct({
id: "premium",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
trial: true,
});
const ops = [
{
entityId: "1",
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
},
// {
// entityId: "2",
// product: premium,
// results: [{ product: premium, status: CusProductStatus.Active }],
// },
];
const testCase = "mergedTrial2";
describe(`${chalk.yellowBright("mergedTrial2: Testing add second trial product after first trial ends")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.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 attach first trial, and advance clock past trial", async function () {
await autumn.entities.create(customerId, entities);
await autumn.attach({
customer_id: customerId,
product_id: premium.id,
entity_id: "1",
});
await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addDays(new Date(), 8).getTime(),
});
return;
const entity1 = await autumn.entities.get(customerId, "1");
const premium1 = entity1.products.find((p: any) => p.id == premium.id);
const checkout = await autumn.checkout({
customer_id: customerId,
product_id: premium.id,
entity_id: "2",
});
const nextCycle = checkout.next_cycle;
expect(nextCycle?.starts_at);
expect(nextCycle?.starts_at).to.approximately(
premium1?.current_period_end,
60000
); // 1 min
await autumn.attach({
customer_id: customerId,
product_id: premium.id,
entity_id: "2",
});
const entity2 = await autumn.entities.get(customerId, "2");
const premium2 = entity2.products.find((p: any) => p.id == premium.id);
expect(premium2?.status).to.equal(CusProductStatus.Trialing);
expect(premium2?.current_period_end).to.approximately(
premium1?.current_period_end,
60000
); // 1 min
});
});

View File

@@ -0,0 +1,143 @@
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 {
APIVersion,
AppEnv,
CusProductStatus,
Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addDays } from "date-fns";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
// Premium, Premium
// Cancel End, Cancel Immediately
// Results: Canceled sub
let pro = constructProduct({
id: "pro",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "pro",
trial: true,
});
let premium = constructProduct({
id: "premium",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
trial: true,
});
const ops = [
{
product: pro,
results: [{ product: pro, status: CusProductStatus.Trialing }],
},
// {
// entityId: "2",
// product: premium,
// results: [{ product: premium, status: CusProductStatus.Active }],
// },
];
const testCase = "trial1";
describe(`${chalk.yellowBright("trial1: Testing trial")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.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: testCase,
});
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!;
});
it("should attach first trial, and advance clock past trial", async function () {
for (const op of ops) {
await attachAndExpectCorrect({
autumn,
customerId,
product: op.product,
stripeCli,
db,
org,
env,
});
}
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
product: pro,
status: CusProductStatus.Trialing,
});
});
it("should advance test clock to before trial ends and attach premium", async function () {
curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addDays(new Date(), 2).getTime(),
});
const checkout = await autumn.checkout({
customer_id: customerId,
product_id: premium.id,
});
expect(checkout.next_cycle?.starts_at).to.be.approximately(
addDays(curUnix, 7).getTime(),
60000
);
expect(checkout.total).to.equal(0);
});
});

View File

@@ -0,0 +1,163 @@
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 {
APIVersion,
AppEnv,
CusProductStatus,
Organization,
} from "@autumn/shared";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
// OPERATIONS:
// Pro, Pro
// Free
// Premium
let pro = constructProduct({
id: "pro",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "pro",
});
let free = constructProduct({
id: "free",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "free",
isDefault: false,
});
const premium = constructProduct({
id: "premium",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "premium",
});
const growth = constructProduct({
id: "growth",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "growth",
});
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: "1",
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
},
];
const testCase = "mergedUpgrade4";
describe(`${chalk.yellowBright("mergedUpgrade4: Upgrading when there's a cancel")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.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, 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",
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];
await attachAndExpectCorrect({
autumn,
customerId,
product: op.product,
stripeCli,
db,
org,
env,
entities,
entityId: op.entityId,
});
for (const result of op.results) {
const entity = await autumn.entities.get(customerId, op.entityId);
expectProductAttached({
customer: entity,
product: result.product,
status: result.status,
});
}
}
});
});