diff --git a/package-lock.json b/package-lock.json index e023bd029..c2f51761e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/server/package.json b/server/package.json index 627716a55..b21ba8190 100644 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts index 4723c0aa6..68488407d 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts @@ -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) { diff --git a/server/src/internal/customers/change-product/handleDowngrade.ts b/server/src/internal/customers/change-product/handleDowngrade.ts index 4988c6fb5..0ed6c8e2e 100644 --- a/server/src/internal/customers/change-product/handleDowngrade.ts +++ b/server/src/internal/customers/change-product/handleDowngrade.ts @@ -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({ diff --git a/server/src/internal/products/product-items/productItemInitUtils.ts b/server/src/internal/products/product-items/productItemInitUtils.ts index 24aa25873..5cff5bc43 100644 --- a/server/src/internal/products/product-items/productItemInitUtils.ts +++ b/server/src/internal/products/product-items/productItemInitUtils.ts @@ -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, diff --git a/server/test.sh b/server/test.sh index 584602694..fbd6d9c71 100755 --- a/server/test.sh +++ b/server/test.sh @@ -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 diff --git a/server/tests/basic/07_downgrade.ts b/server/tests/basic/07_downgrade.ts index 6d01b2188..e57081209 100644 --- a/server/tests/basic/07_downgrade.ts +++ b/server/tests/basic/07_downgrade.ts @@ -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,