fix: canceling product from Stripe billing portal schedules free product

This commit is contained in:
John Yeo
2025-04-26 22:41:27 +01:00
parent 72ece97d69
commit 569a25f3c4
7 changed files with 90 additions and 46 deletions

39
package-lock.json generated
View File

@@ -13867,6 +13867,7 @@
"@types/express": "^5.0.0",
"@unkey/api": "^0.29.0",
"ai": "^4.3.10",
"autumn-js": "^0.0.12",
"body-parser": "^1.20.3",
"bullmq": "^5.31.1",
"chai": "^5.1.2",
@@ -14299,6 +14300,20 @@
"npm": ">= 8.6.0"
}
},
"server/node_modules/@supabase/ssr": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.6.1.tgz",
"integrity": "sha512-QtQgEMvaDzr77Mk3vZ3jWg2/y+D8tExYF7vcJT+wQ8ysuvOeGGjYbZlvj5bHYsj/SpC0bihcisnwPrM4Gp5G4g==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"cookie": "^1.0.1"
},
"peerDependencies": {
"@supabase/supabase-js": "^2.43.4"
}
},
"server/node_modules/@types/body-parser": {
"version": "1.19.5",
"license": "MIT",
@@ -14453,6 +14468,30 @@
"node": ">= 14"
}
},
"server/node_modules/autumn-js": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/autumn-js/-/autumn-js-0.0.12.tgz",
"integrity": "sha512-NUjAlrJZb5QG4I+Vjuv64z0H7MsSYvdr5kNBDQ3YuW0p+Dh1XmxF75nQ51wQh56o/LQb4KrPfvrWefDZ3uIOBQ==",
"license": "MIT",
"peerDependencies": {
"@clerk/nextjs": "^6.16.0",
"@supabase/ssr": "^0.6.1",
"better-auth": "^1.2.7",
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@clerk/nextjs": {
"optional": true
},
"@supabase/ssr": {
"optional": true
},
"better-auth": {
"optional": true
}
}
},
"server/node_modules/body-parser": {
"version": "1.20.3",
"license": "MIT",

View File

@@ -45,6 +45,7 @@
"@types/express": "^5.0.0",
"@unkey/api": "^0.29.0",
"ai": "^4.3.10",
"autumn-js": "^0.0.12",
"body-parser": "^1.20.3",
"bullmq": "^5.31.1",
"chai": "^5.1.2",

View File

@@ -83,7 +83,10 @@ export const handleSubscriptionUpdated = async ({
nullish(previousAttributes?.canceled_at) &&
!nullish(subscription.canceled_at);
if (isCanceled && updatedCusProducts.length > 0) {
let comment = subscription.cancellation_details?.comment;
let isAutumnDowngrade = comment === "autumn_downgrade";
if (isCanceled && updatedCusProducts.length > 0 && !isAutumnDowngrade) {
let allDefaultProducts = await ProductService.getFullDefaultProducts({
sb,
orgId: org.id,
@@ -98,8 +101,11 @@ export const handleSubscriptionUpdated = async ({
inStatuses: [CusProductStatus.Scheduled],
});
// Default products to activate...
let defaultProducts = allDefaultProducts.filter((p) =>
cusProducts.some((cp: FullCusProduct) => cp.product.group == p.group)
updatedCusProducts.some(
(cp: FullCusProduct) => cp.product.group == p.group
)
);
let customer = updatedCusProducts[0].customer;
@@ -114,7 +120,7 @@ export const handleSubscriptionUpdated = async ({
for (let product of defaultProducts) {
let alreadyScheduled = cusProducts.some(
(cp: FullCusProduct) => cp.product.id == product.id
(cp: FullCusProduct) => cp.product.group == product.group
);
if (alreadyScheduled) {

View File

@@ -159,10 +159,16 @@ export const handleDowngrade = async ({
if (differenceInDays(latestEndDate, curEndDate) > 10) {
await stripeCli.subscriptions.update(sub.id, {
cancel_at: latestPeriodEnd,
cancellation_details: {
comment: "autumn_downgrade",
},
});
} else {
await stripeCli.subscriptions.update(sub.id, {
cancel_at_period_end: true,
cancellation_details: {
comment: "autumn_downgrade",
},
});
}
}
@@ -265,7 +271,7 @@ export const handleDowngrade = async ({
},
});
// 3. Update cus product
// 4. Update cus product
logger.info("3. Inserting new full cus product (starts at period end)");
await createFullCusProduct({
sb: req.sb,
@@ -277,28 +283,6 @@ export const handleDowngrade = async ({
isDowngrade: true,
});
// 4. For scheduled products that are not in same interval, remove from schedule
for (const scheduleObj of schedules) {
const { schedule, interval } = scheduleObj;
let intervalsToRemove = [];
if (!itemSets.some((itemSet) => itemSet.interval === interval)) {
intervalsToRemove.push(interval);
}
await cancelFutureProductSchedule({
sb: req.sb,
org: attachParams.org,
stripeCli,
cusProducts: attachParams.cusProducts!,
product: product,
includeOldItems: false,
logger: req.logtail,
inIntervals: intervalsToRemove,
env: attachParams.customer.env,
});
}
if (attachParams.org.api_version! >= APIVersion.v1_1) {
res.status(200).json(
AttachResultSchema.parse({

View File

@@ -255,13 +255,6 @@ export const handleNewProductItems = async ({
`Ents: new(${newEnts.length}), updated(${updatedEnts.length}), deleted(${deletedEnts.length})`
);
console.log("newEnts", newEnts);
console.log("newPrices", newPrices);
console.log("updatedEnts", updatedEnts);
console.log("updatedPrices", updatedPrices);
console.log("deletedEnts", deletedEnts);
console.log("deletedPrices", deletedPrices);
if (newFeatures.length > 0) {
await FeatureService.insert({
sb,

View File

@@ -5,11 +5,11 @@ MOCHA_CMD="npx mocha --parallel --timeout 10000000 --ignore tests/00_setup.ts"
# TEST PARALLEL
if [ "$1" == "basic-parallel" ]; then
MOCHA_PARALLEL=true $MOCHA_SETUP && $MOCHA_CMD \
tests/basic/*.ts \
tests/basic/multi-feature/*.ts \
tests/basic/entities/*.ts \
# tests/basic/referrals/*.ts \
# tests/attach/**/*.ts \
tests/basic/referrals/*.ts \
tests/attach/**/*.ts \
# tests/basic/*.ts \
# tests/basic/multi-feature/*.ts \
# tests/basic/entities/*.ts \
elif [ "$1" == "advanced-parallel" ]; then

View File

@@ -49,12 +49,7 @@ describe(`${chalk.yellowBright(
});
customer = await initCustomer({
customer_data: {
id: customerId,
name: "Test Customer",
email: "test@test.com",
fingerprint: "fp1",
},
customerId,
sb: this.sb,
org: this.org,
env: this.env,
@@ -103,9 +98,35 @@ describe(`${chalk.yellowBright(
assert.isNotNull(resPro);
});
// Advance time 1 month
it("Advancing stripe clock and seeing if pro is attached", async function () {
// Attach premium to see if scheduled product is removed
it("checking if attach premium will remove scheduled product", async function () {
this.timeout(30000);
await AutumnCli.attach({
customerId: customerId,
productId: products.premium.id,
});
const res = await AutumnCli.getCustomer(customerId);
const resPro = res.products.find(
(p: any) =>
p.id === products.pro.id && p.status === CusProductStatus.Scheduled
);
assert.isUndefined(resPro);
compareMainProduct({
sent: products.premium,
cusRes: res,
});
});
// Advance time 1 month
it("Attaching pro, advancing stripe clock and seeing if pro is attached", async function () {
this.timeout(30000);
await AutumnCli.attach({
customerId: customerId,
productId: products.pro.id,
});
const stripeCli = createStripeCli({
org: this.org,
env: this.env,