From ad8e9dfca1e33be39626db26ab0857ede346ed8b Mon Sep 17 00:00:00 2001 From: John Yeo Date: Wed, 27 Aug 2025 17:13:58 -0700 Subject: [PATCH] fix: tests --- server/shell/g1.sh | 3 +- server/shell/g4.sh | 32 +- server/shell/g5.sh | 32 +- server/shell/g6.sh | 6 + server/src/external/autumn/autumnCli.ts | 5 + server/src/external/stripe/stripeSubUtils.ts | 9 + .../updateStripeSub/createProrationinvoice.ts | 26 -- .../webhookHandlers/handleInvoicePaid.ts | 14 +- .../webhookHandlers/handleSubUpdated.ts | 4 +- .../handleSubUpdated/handleSubCanceled.ts | 47 +++ .../handleSubUpdated/handleSubRenewed.ts | 37 ++- .../src/internal/api/rewards/rewardRouter.ts | 23 ++ .../addProductFlow/handleOneOffFunction.ts | 10 +- .../scheduleFlow/handleScheduleFlow2.ts | 26 +- .../handleQuantityDowngrade.ts | 11 +- .../handleQuantityUpgrade.ts | 11 +- .../upgradeFlow/handleUpgradeFlow.ts | 32 +- .../upgradeFlow/handleUpgradeFlowSchedule.ts | 18 +- .../upgradeFlow/updateStripeSub2.ts | 13 +- .../priceToUnusedPreviewItem.ts | 20 +- .../attach/attachUtils/convertAttachParams.ts | 6 +- .../getUpgradeProductPreview.ts | 15 + .../customers/attach/mergeUtils/mergeUtils.ts | 20 +- .../mergeUtils/paramsToScheduleItems.ts | 46 +-- .../attach/mergeUtils/paramsToSubItems.ts | 5 +- server/src/internal/invoices/invoiceUtils.ts | 20 ++ .../previewItemUtils/getItemsForCurProduct.ts | 2 + .../migrationUtils/runMigrationAttach.ts | 3 +- server/tests/attach/checkout/checkout5.ts | 15 +- server/tests/attach/free/free2.ts | 10 +- server/tests/attach/migrations/migration4.ts | 2 +- .../attach/migrations/runMigrationTest.ts | 10 + server/tests/attach/prepaid/prepaid3.ts | 2 +- server/tests/core/cancel/cancel1.test.ts | 276 ++++++++++++++++++ server/tests/core/cancel/cancel1.ts | 276 ------------------ .../cancel/{cancel2.ts => cancel2.test.ts} | 0 .../cancel/{cancel3.ts => cancel3.test.ts} | 0 .../cancel/{cancel4.ts => cancel4.test.ts} | 0 server/tests/core/cancel/cancel5.test.ts | 125 ++++++++ .../core/multiAttach/multiAttach1.test.ts | 21 +- .../multiInvoice/multiInvoice1.test.ts | 180 ++++++++++++ .../multiReward/multiReward3.test.ts | 1 + .../multiReward/multiRewardUtils.test.ts | 33 ++- .../{mergedAdd1.ts => add/mergedAdd1.test.ts} | 2 +- .../{mergedAdd2.ts => add/mergedAdd2.test.ts} | 2 +- .../tests/merged/addOn/mergedAddOn1.test.ts | 3 +- .../tests/merged/addOn/mergedAddOn3.test.ts | 1 + .../merged/downgrade/mergedDowngrade4.ts | 2 +- .../merged/mergeUtils/expectSubCorrect.ts | 64 ++-- server/tests/merged/trial/trial2.test.ts | 17 +- .../merged/upgrade/mergedUpgrade3.test.ts | 1 + .../tests/utils/expectUtils/expectAttach.ts | 3 +- .../utils/expectUtils/expectMultiAttach.ts | 11 + .../expectUtils/expectProductAttached.ts | 8 + .../utils/expectUtils/expectScheduleUtils.ts | 68 +++-- .../tests/utils/expectUtils/expectSubUtils.ts | 33 ++- .../multi-attach/MultiAttachDialog.tsx | 24 +- 57 files changed, 1081 insertions(+), 605 deletions(-) create mode 100755 server/shell/g6.sh create mode 100644 server/tests/core/cancel/cancel1.test.ts delete mode 100644 server/tests/core/cancel/cancel1.ts rename server/tests/core/cancel/{cancel2.ts => cancel2.test.ts} (100%) rename server/tests/core/cancel/{cancel3.ts => cancel3.test.ts} (100%) rename server/tests/core/cancel/{cancel4.ts => cancel4.test.ts} (100%) create mode 100644 server/tests/core/cancel/cancel5.test.ts create mode 100644 server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts rename server/tests/merged/{mergedAdd1.ts => add/mergedAdd1.test.ts} (98%) rename server/tests/merged/{mergedAdd2.ts => add/mergedAdd2.test.ts} (98%) diff --git a/server/shell/g1.sh b/server/shell/g1.sh index 9bf27833d..38e990def 100755 --- a/server/shell/g1.sh +++ b/server/shell/g1.sh @@ -13,7 +13,8 @@ $MOCHA_CMD \ 'tests/attach/basic/*.ts' \ 'tests/attach/upgrade/*.ts' \ 'tests/attach/downgrade/*.ts' \ -'tests/attach/addOn/*.ts' +'tests/attach/addOn/*.ts' \ +'tests/attach/cancel/*.ts' $MOCHA_CMD \ 'tests/attach/checkout/*.ts' \ diff --git a/server/shell/g4.sh b/server/shell/g4.sh index 5249f482e..4b56a1332 100755 --- a/server/shell/g4.sh +++ b/server/shell/g4.sh @@ -8,19 +8,23 @@ if [[ "$1" == *"setup"* ]]; then MOCHA_PARALLEL=true $MOCHA_SETUP fi -$MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \ - 'tests/advanced/coupons/*.ts' \ - 'tests/attach/updateQuantity/*.ts' \ - 'tests/advanced/referrals/*.ts' \ - 'tests/advanced/rollovers/*.ts' \ - 'tests/advanced/customInterval/*.ts' -$MOCHA_CMD 'tests/advanced/referrals/*.ts' \ - 'tests/advanced/rollovers/*.ts' \ - 'tests/advanced/customInterval/*.ts' - -$MOCHA_CMD 'tests/attach/multiProduct/*.ts' \ - 'tests/advanced/usageLimit/*.ts' -$MOCHA_CMD 'tests/advanced/usage/*.ts' - \ No newline at end of file + +# $MOCHA_CMD 'tests/merged/add/*.ts' \ +# 'tests/merged/downgrade/*.ts' \ +# 'tests/merged/prepaid/*.ts' \ +# 'tests/merged/separate/*.ts' \ +# 'tests/merged/upgrade/*.ts' \ +# 'tests/merged/trial/*.ts' + + +$MOCHA_CMD 'tests/merged/addOn/*.ts' \ +'tests/core/cancel/*.ts' \ +'tests/core/multiAttach/*.ts' \ +'tests/core/multiAttach/multiInvoice/*.ts' \ +'tests/core/multiAttach/multiUpgrade/*.ts' + +# $MOCHA_CMD 'tests/core/multiAttach/multiReward/multiReward1.test.ts' +# $MOCHA_CMD 'tests/core/multiAttach/multiReward/multiReward2.test.ts' +# $MOCHA_CMD 'tests/core/multiAttach/multiReward/multiReward3.test.ts' diff --git a/server/shell/g5.sh b/server/shell/g5.sh index 85746dd66..979c837ae 100755 --- a/server/shell/g5.sh +++ b/server/shell/g5.sh @@ -1,6 +1,28 @@ -# npx mocha 'tests/alex/00_setup.ts' --timeout 10000000 +#!/bin/bash -MOCHA_PARALLEL=true npx mocha --parallel --timeout 10000000 \ - 'tests/alex/01_free.ts' 'tests/alex/02_pro.ts' 'tests/alex/03_premium.ts' \ - 'tests/alex/04_topups.ts' 'tests/alex/05_cancel.ts' 'tests/alex/06_switch.ts' \ - --ignore 'tests/alex/00_setup.ts' \ No newline at end of file +# Source shared configuration +source "$(dirname "$0")/config.sh" + +# MOCHA_PARALLEL=true $MOCHA_SETUP +if [[ "$1" == *"setup"* ]]; then + MOCHA_PARALLEL=true $MOCHA_SETUP +fi + +$MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \ + 'tests/advanced/coupons/*.ts' \ + 'tests/attach/updateQuantity/*.ts' \ + 'tests/advanced/referrals/*.ts' \ + 'tests/advanced/rollovers/*.ts' \ + 'tests/advanced/customInterval/*.ts' + +$MOCHA_CMD 'tests/advanced/referrals/*.ts' \ + 'tests/advanced/rollovers/*.ts' \ + 'tests/advanced/customInterval/*.ts' + +$MOCHA_CMD 'tests/attach/multiProduct/*.ts' \ + 'tests/advanced/usageLimit/*.ts' + +$MOCHA_CMD 'tests/advanced/usage/*.ts' + + + \ No newline at end of file diff --git a/server/shell/g6.sh b/server/shell/g6.sh new file mode 100755 index 000000000..85746dd66 --- /dev/null +++ b/server/shell/g6.sh @@ -0,0 +1,6 @@ +# npx mocha 'tests/alex/00_setup.ts' --timeout 10000000 + +MOCHA_PARALLEL=true npx mocha --parallel --timeout 10000000 \ + 'tests/alex/01_free.ts' 'tests/alex/02_pro.ts' 'tests/alex/03_premium.ts' \ + 'tests/alex/04_topups.ts' 'tests/alex/05_cancel.ts' 'tests/alex/06_switch.ts' \ + --ignore 'tests/alex/00_setup.ts' \ No newline at end of file diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts index 487eb1111..56a5e87d4 100644 --- a/server/src/external/autumn/autumnCli.ts +++ b/server/src/external/autumn/autumnCli.ts @@ -352,6 +352,11 @@ export class AutumnInt { }; rewards = { + get: async (rewardId: string) => { + const data = await this.get(`/rewards/${rewardId}`); + return data; + }, + create: async (reward: any) => { const data = await this.post(`/rewards?legacyStripe=true`, reward); return data; diff --git a/server/src/external/stripe/stripeSubUtils.ts b/server/src/external/stripe/stripeSubUtils.ts index b47c2b58c..01ec9fc23 100644 --- a/server/src/external/stripe/stripeSubUtils.ts +++ b/server/src/external/stripe/stripeSubUtils.ts @@ -13,6 +13,7 @@ import { differenceInSeconds } from "date-fns"; import { SubService } from "@/internal/subscriptions/SubService.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; import { getEarliestPeriodEnd } from "./stripeSubUtils/convertSubUtils.js"; +import { notNullish } from "@/utils/genUtils.js"; export const getFullStripeSub = async ({ stripeCli, @@ -293,3 +294,11 @@ export const getStripeProrationBehavior = ({ ? behaviourMap[ProrationBehavior.Immediately] : behaviourMap[ProrationBehavior.NextBilling]; }; + +export const subIsCanceled = ({ sub }: { sub: Stripe.Subscription }) => { + return ( + notNullish(sub.canceled_at) || + notNullish(sub.cancel_at) || + sub.cancel_at_period_end + ); +}; diff --git a/server/src/external/stripe/stripeSubUtils/updateStripeSub/createProrationinvoice.ts b/server/src/external/stripe/stripeSubUtils/updateStripeSub/createProrationinvoice.ts index e588f5ea3..ad0558396 100644 --- a/server/src/external/stripe/stripeSubUtils/updateStripeSub/createProrationinvoice.ts +++ b/server/src/external/stripe/stripeSubUtils/updateStripeSub/createProrationinvoice.ts @@ -64,37 +64,11 @@ export const createProrationInvoice = async ({ pending: true, }); - console.log("Items:", items.data); if (items.data.length == 0) { logger.info(`No items to prorate, skipping invoice creation`); return null; } - // console.log( - // "Upcoming invoice:", - // items.data.map((item) => item.description) - // ); - - // throw new Error("Not implemented"); - - // const proratedItems = items.data.filter( - // (item) => item.proration || item.parent?.type === "invoiceitem" - // ); - - // console.log("Preview invoice items:", items.lines.data); - // let items = await stripeCli.invoices.listUpcomingLines({ - // subscription: curSub.id, - // }); - - // let proratedItems = items.data.filter( - // (item) => item.proration || item.type === "invoiceitem", - // ); - - // if (proratedItems.length == 0) { - // logger.info(`No items to prorate, skipping invoice creation`); - // return null; - // } - let invoice = await stripeCli.invoices.create({ customer: customer.processor.id, subscription: curSub.id, diff --git a/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts b/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts index 9322436cc..9abaaab75 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts @@ -24,10 +24,7 @@ import { InvoiceService } from "@/internal/invoices/InvoiceService.js"; import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js"; import { handleInvoicePaidDiscount } from "./handleInvoicePaidDiscount.js"; import { handleInvoiceCheckoutPaid } from "@/internal/customers/attach/attachFunctions/invoiceCheckoutPaid/handleInvoiceCheckoutPaid.js"; -import { - lineItemInCusProduct, - subItemInCusProduct, -} from "../stripeSubUtils/stripeSubItemUtils.js"; +import { lineItemInCusProduct } from "../stripeSubUtils/stripeSubItemUtils.js"; const handleOneOffInvoicePaid = async ({ db, @@ -214,11 +211,10 @@ export const handleInvoicePaid = async ({ }); } - // let updated = await updateInvoiceIfExists({ - // db, - // invoice, - // }); - const updated = false; + const updated = await updateInvoiceIfExists({ + db, + invoice, + }); if (!updated) { let invoiceItems = await getInvoiceItems({ diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts index 43462b5fa..599348ad9 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts @@ -79,8 +79,8 @@ export const handleSubscriptionUpdated = async ({ stripeSubId: subscription.id, updates: { status: subStatusMap[subscription.status] || CusProductStatus.Unknown, - canceled_at: canceled ? canceledAt : null, collection_method: fullSub.collection_method as CollectionMethod, + // canceled_at: canceled ? canceledAt : null, // trial_ends_at: // previousAttributes.status === "trialing" && // subscription.status === "active" @@ -89,6 +89,8 @@ export const handleSubscriptionUpdated = async ({ }, }); + // 2. Update canceled & canceled_at IF sub has no schedule...? + if (updatedCusProducts.length > 0) { logger.info( `✅ Updated ${updatedCusProducts.length} customer product${updatedCusProducts.length === 1 ? "" : "s"} (${updatedCusProducts.map((cp) => cp.id).join(", ")}) - Status: ${updatedCusProducts[0].status}${updatedCusProducts[0].canceled_at ? `, Canceled: ${new Date(updatedCusProducts[0].canceled_at).toISOString()}` : ""}` diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts index 34802beb8..5c5c78a7d 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts @@ -12,6 +12,8 @@ import { getLatestPeriodEnd, subToPeriodStartEnd, } from "../../stripeSubUtils/convertSubUtils.js"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const isSubCanceled = ({ previousAttributes, @@ -43,6 +45,33 @@ export const isSubCanceled = ({ }; }; +const updateCusProductCanceled = async ({ + db, + sub, + canceledAt, + logger, +}: { + db: DrizzleCli; + sub: Stripe.Subscription; + canceledAt?: number | null; + logger: any; +}) => { + // 1. Check if sub has schedule + if (sub.schedule) { + return; + } + + logger.info( + `Updating cus products for sub ${sub.id} to canceled | canceled_at: ${canceledAt}` + ); + + await CusProductService.updateByStripeSubId({ + db, + stripeSubId: sub.id, + updates: { canceled_at: canceledAt || Date.now(), canceled: true }, + }); +}; + export const handleSubCanceled = async ({ req, previousAttributes, @@ -74,6 +103,24 @@ export const handleSubCanceled = async ({ return; } + await updateCusProductCanceled({ + db, + sub, + canceledAt, + logger, + }); + + // 2. Update canceled & canceled_at IF sub has no schedule...? + + // await CusProductService.updateByStripeSubId({ + // db, + // stripeSubId: sub.id, + // updates: { + // canceled_at: canceled ? canceledAt : null, + // canceled: true, + // }, + // }); + let allDefaultProducts = await ProductService.listDefault({ db, orgId: org.id, diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts index 140aec195..218dd0ea1 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts @@ -8,6 +8,7 @@ import Stripe from "stripe"; import { createStripeCli } from "../../utils.js"; import { cancelFutureProductSchedule } from "@/internal/customers/change-product/scheduleUtils.js"; import { isMultiProductSub } from "@/internal/customers/attach/mergeUtils/mergeUtils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; const isSubRenewed = ({ previousAttributes, sub, @@ -30,6 +31,27 @@ const isSubRenewed = ({ renewedAt: Date.now(), }; }; + +const updateCusProductRenewed = async ({ + db, + sub, +}: { + db: DrizzleCli; + sub: Stripe.Subscription; +}) => { + if (sub.schedule) { + return; + } + + await CusProductService.updateByStripeSubId({ + db, + stripeSubId: sub.id, + updates: { canceled_at: null, canceled: false }, + }); + + return; +}; + export const handleSubRenewed = async ({ req, prevAttributes, @@ -43,7 +65,7 @@ export const handleSubRenewed = async ({ }) => { const { db, org, env, logtail: logger } = req; - const { renewed, renewedAt } = isSubRenewed({ + const { renewed } = isSubRenewed({ previousAttributes: prevAttributes, sub, }); @@ -51,19 +73,18 @@ export const handleSubRenewed = async ({ if (!renewed || updatedCusProducts.length == 0) return; const customer = updatedCusProducts[0].customer; - let cusProducts = await CusProductService.list({ db, internalCustomerId: customer!.internal_id, }); - if (isMultiProductSub({ sub, cusProducts })) return; + if (isMultiProductSub({ sub, cusProducts }) || sub.schedule) return; - // Sub renewed... if multi sub flow - // console.log( - // `Checking sub renewed: ${sub.id}, Is multi sub: ${isMultiProductSub({ sub, cusProducts })}` - // ); - // console.log("Cus products:", cusProducts.map((cp) => `${cp.product.name}`)); + await CusProductService.updateByStripeSubId({ + db, + stripeSubId: sub.id, + updates: { canceled_at: null, canceled: false }, + }); let { curScheduledProduct } = getExistingCusProducts({ product: updatedCusProducts[0].product, diff --git a/server/src/internal/api/rewards/rewardRouter.ts b/server/src/internal/api/rewards/rewardRouter.ts index e0cb61243..0fd4e3df3 100644 --- a/server/src/internal/api/rewards/rewardRouter.ts +++ b/server/src/internal/api/rewards/rewardRouter.ts @@ -210,4 +210,27 @@ rewardRouter.post("/:internalId", async (req: any, res: any) => { } }); +rewardRouter.get("/:id", async (req: any, res: any) => { + try { + const { id } = req.params; + const { orgId, env, db } = req; + + const reward = await RewardService.get({ + db, + idOrInternalId: id, + orgId, + env, + }); + + res.status(200).json(reward); + } catch (error) { + handleRequestError({ + error, + res, + req, + action: "get reward", + }); + } +}); + export default rewardRouter; diff --git a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleOneOffFunction.ts b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleOneOffFunction.ts index 657448dff..04d39dd8c 100644 --- a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleOneOffFunction.ts +++ b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleOneOffFunction.ts @@ -42,7 +42,7 @@ export const handleOneOffFunction = async ({ prices, entitlements, optionsList, - reward, + rewards, } = attachParams; const { invoiceOnly } = config; @@ -112,13 +112,7 @@ export const handleOneOffFunction = async ({ customer: customer.processor.id!, auto_advance: false, currency: org.default_currency!, - discounts: reward - ? [ - { - coupon: reward.id, - }, - ] - : undefined, + discounts: rewards ? rewards.map((r) => ({ coupon: r.id })) : undefined, collection_method: attachParams.invoiceOnly ? "send_invoice" : undefined, days_until_due: attachParams.invoiceOnly ? 30 : undefined, }); diff --git a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts index fe8d169cd..063fbe69d 100644 --- a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts +++ b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts @@ -46,14 +46,10 @@ export const handleScheduleFunction2 = async ({ }) => { const logger = req.logtail; const product = attachParams.products[0]; - const { stripeCli, customer: fullCus, prices } = attachParams; + const { stripeCli } = attachParams; const curCusProduct = attachParamsToCurCusProduct({ attachParams }); const curSub = await paramsToCurSub({ attachParams }); - const curPrices = curCusProduct - ? cusProductToPrices({ cusProduct: curCusProduct }) - : []; - const subItems = curSub?.items.data.filter((item) => subItemInCusProduct({ cusProduct: curCusProduct!, subItem: item }) ); @@ -66,12 +62,6 @@ export const handleScheduleFunction2 = async ({ const newProductFree = isFreeProduct(attachParams.prices); if (schedule) { - console.log("CURRENT SCHEDULE ITEMS:"); - await logPhases({ - phases: schedule.phases as any, - db: req.db, - }); - const newItems = await paramsToScheduleItems({ req, schedule: schedule!, @@ -80,20 +70,15 @@ export const handleScheduleFunction2 = async ({ billingPeriodEnd: expectedEnd!, }); - console.log("NEW SCHEDULE ITEMS:"); - await logPhases({ - phases: newItems.phases as any, - db: req.db, - }); - - // Should release schedule... const currentPhaseIndex = getCurrentPhaseIndex({ schedule: { phases: newItems.phases } as any, now: attachParams.now, }); if (currentPhaseIndex == newItems.phases.length - 1) { - console.log(`NO SUBSEQUENT PHASES, RELEASING SCHEDULE`); + logger.info( + `SCHEDULE FLOW: no subsequent phases, releasing schedule ${schedule?.id}` + ); await stripeCli.subscriptionSchedules.release(schedule!.id); await CusProductService.updateByStripeScheduledId({ db: req.db, @@ -112,6 +97,7 @@ export const handleScheduleFunction2 = async ({ }); schedule = undefined; } else { + logger.info(`SCHEDULE FLOW: updating schedule ${schedule?.id}`); schedule = await updateCurSchedule({ req, attachParams, @@ -132,6 +118,7 @@ export const handleScheduleFunction2 = async ({ }); } } else { + logger.info(`SCHEDULE FLOW: no schedule, creating new schedule`); schedule = await subToNewSchedule({ req, sub: curSub!, @@ -152,6 +139,7 @@ export const handleScheduleFunction2 = async ({ } if (!schedule) { + logger.info(`SCHEDULE FLOW: no schedule, canceling sub ${curSub?.id}`); await stripeCli.subscriptions.update(curSub!.id, { cancel_at: expectedEnd!, }); diff --git a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts index be624d608..34f85811d 100644 --- a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts +++ b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts @@ -25,6 +25,7 @@ import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils import { cusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js"; +import { notNullish } from "@/utils/genUtils.js"; const onDecreaseToStripeProration: Record = { [OnDecrease.ProrateImmediately]: "always_invoice", @@ -67,12 +68,20 @@ export const handleQuantityDowngrade = async ({ .minus(oldOptions.quantity) .toNumber(); + const subItemDifference = new Decimal(newOptions.quantity) + .minus( + notNullish(oldOptions.upcoming_quantity) + ? oldOptions.upcoming_quantity! + : oldOptions.quantity + ) + .toNumber(); + const diffWithBillingUnits = new Decimal(difference) .mul((cusPrice.price.config as UsagePriceConfig).billing_units || 1) .toNumber(); const newSubItemQuantity = new Decimal(subItem.quantity || 0) - .plus(difference) + .plus(subItemDifference) .toNumber(); const stripeProration = onDecreaseToStripeProration[ diff --git a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts index 86b01f1dd..406429b17 100644 --- a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts +++ b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts @@ -25,6 +25,7 @@ import { Decimal } from "decimal.js"; import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; import { InvoiceService } from "@/internal/invoices/InvoiceService.js"; import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js"; +import { notNullish } from "@/utils/genUtils.js"; export const handleQuantityUpgrade = async ({ req, @@ -55,6 +56,14 @@ export const handleQuantityUpgrade = async ({ .minus(oldOptions.quantity) .toNumber(); + const subItemDifference = new Decimal(newOptions.quantity) + .minus( + notNullish(oldOptions.upcoming_quantity) + ? oldOptions.upcoming_quantity! + : oldOptions.quantity + ) + .toNumber(); + const onIncrease = cusPrice.price.proration_config?.on_increase || OnIncrease.ProrateImmediately; @@ -146,7 +155,7 @@ export const handleQuantityUpgrade = async ({ await stripeCli.subscriptionItems.update(subItem.id, { // quantity: newOptions.quantity, - quantity: (subItem.quantity || 0) + difference, + quantity: (subItem.quantity || 0) + subItemDifference, proration_behavior: "none", }); diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts index 9e22bb1d7..379d20242 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts @@ -88,11 +88,11 @@ export const handleUpgradeFlow = async ({ let canceled = false; // SCENARIO 1, NO SUB: if (!curSub) { - console.log("UPGRADE FLOW, NO SUB (FROM CANCEL MAYBE...?)"); + logger.info("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}` + logger.info( + `UPGRADE FLOW: canceling sub ${curSub!.id}, proration: ${config.proration}` ); canceled = true; const { stripeCli } = attachParams; @@ -104,7 +104,7 @@ export const handleUpgradeFlow = async ({ }, }); } else if (subItems.length > 0) { - console.log(`UPGRADE FLOW, UPDATING SUB ${curSub!.id}`); + logger.info(`UPGRADE FLOW, updating sub ${curSub!.id}`); itemSet.subItems = subItems; const res = await updateStripeSub2({ @@ -116,11 +116,22 @@ export const handleUpgradeFlow = async ({ fromCreate: attachParams.products.length === 0, // just for now, if no products, it comes from cancel product... }); + if (res?.latestInvoice) { + logger.info(`UPGRADE FLOW: inserting invoice ${res.latestInvoice.id}`); + await insertInvoiceFromAttach({ + db: req.db, + attachParams, + stripeInvoice: res.latestInvoice, + logger, + }); + } + const schedule = await paramsToCurSubSchedule({ attachParams }); if (schedule) { await handleUpgradeFlowSchedule({ req, + logger, attachParams, config, schedule, @@ -133,7 +144,7 @@ export const handleUpgradeFlow = async ({ latestInvoice = res.latestInvoice; } - logger.info(`2. Expiring previous cus product`); + logger.info(`UPGRADE FLOW: expiring previous cus product`); await CusProductService.update({ db: req.db, cusProductId: curCusProduct!.id, @@ -143,17 +154,8 @@ export const handleUpgradeFlow = async ({ }, }); - if (latestInvoice) { - await insertInvoiceFromAttach({ - db: req.db, - attachParams, - stripeInvoice: latestInvoice, - logger, - }); - } - if (attachParams.products.length > 0) { - logger.info(`3. Creating new cus product`); + logger.info(`UPGRADE FLOW: creating new cus product`); const anchorToUnix = sub ? getEarliestPeriodEnd({ sub }) * 1000 : undefined; await createFullCusProduct({ db: req.db, diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlowSchedule.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlowSchedule.ts index 166f87a6e..d254e0276 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlowSchedule.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlowSchedule.ts @@ -23,6 +23,7 @@ export const handleUpgradeFlowSchedule = async ({ schedule, curSub, removeCusProducts, + logger, }: { req: ExtendedRequest; attachParams: AttachParams; @@ -30,8 +31,9 @@ export const handleUpgradeFlowSchedule = async ({ schedule: Stripe.SubscriptionSchedule; curSub: Stripe.Subscription; removeCusProducts?: FullCusProduct[]; + logger: any; }) => { - console.log(`UPGRADE FLOW, UPDATING SCHEDULE ${schedule.id}`); + logger.info(`UPGRADE FLOW, updating schedule ${schedule?.id}`); const { stripeCli, customer, prices } = attachParams; const curCusProduct = attachParamsToCurCusProduct({ attachParams }); @@ -70,7 +72,9 @@ export const handleUpgradeFlowSchedule = async ({ // 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`); + logger.info( + `UPGRADE FLOW: no subsequent phases, releasing schedule ${schedule?.id}` + ); await stripeCli.subscriptionSchedules.release(schedule!.id); await CusProductService.updateByStripeScheduledId({ db: req.db, @@ -91,7 +95,7 @@ export const handleUpgradeFlowSchedule = async ({ .every((cp) => cp.canceled) && isFreeProduct(prices); if (shouldCancelSub) { - console.log(`UPGRADE FLOW, CANCELING SUB: ${curSub.id}`); + logger.info(`UPGRADE FLOW: canceling sub ${curSub?.id}`); await stripeCli.subscriptions.update(curSub.id, { cancel_at_period_end: true, }); @@ -100,10 +104,10 @@ export const handleUpgradeFlowSchedule = async ({ return; } - await logPhases({ - phases: newItems.phases, - db: req.db, - }); + // await logPhases({ + // phases: newItems.phases, + // db: req.db, + // }); await updateCurSchedule({ req, diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts index 8b2456cf5..277656295 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts @@ -54,12 +54,13 @@ export const updateStripeSub2 = async ({ let updatedSub = await stripeCli.subscriptions.update(curSub.id, { items: sanitizeSubItems(itemSet.subItems), - // proration_behavior: - // proration == ProrationBehavior.None - // ? "none" - // : fromCreate - // ? "always_invoice" - // : "create_prorations", + proration_behavior: + proration == ProrationBehavior.None + ? "none" + : fromCreate + ? "always_invoice" + : "create_prorations", + // proration_behavior: "create_prorations", trial_end: trialEnd, // default_payment_method: paymentMethod?.id, add_invoice_items: itemSet.invoiceItems, diff --git a/server/src/internal/customers/attach/attachPreviewUtils/priceToUnusedPreviewItem.ts b/server/src/internal/customers/attach/attachPreviewUtils/priceToUnusedPreviewItem.ts index 3aafa6e6e..c3a72f942 100644 --- a/server/src/internal/customers/attach/attachPreviewUtils/priceToUnusedPreviewItem.ts +++ b/server/src/internal/customers/attach/attachPreviewUtils/priceToUnusedPreviewItem.ts @@ -64,7 +64,7 @@ export const priceToUnusedPreviewItem = ({ now?: number; org?: Organization; subDiscounts?: Stripe.Discount[]; - latestInvoice: Stripe.Invoice; + latestInvoice?: Stripe.Invoice; }) => { now = now || Date.now(); const onTrial = isTrialing({ cusProduct, now }); @@ -77,7 +77,7 @@ export const priceToUnusedPreviewItem = ({ const invoiceItem = findStripeItemForPrice({ price, - invoiceLineItems: latestInvoice.lines.data, + invoiceLineItems: latestInvoice?.lines.data || [], stripeProdId: cusProduct?.product.processor?.id, }) as Stripe.InvoiceLineItem | undefined; @@ -115,24 +115,10 @@ export const priceToUnusedPreviewItem = ({ now, }); - console.log("Invoice item qty: ", invoiceItem?.quantity); - // console.log( - // "Sub discounts: ", - // subDiscounts?.map((d) => d.id) - // ); - - // const discountsApplied = getDiscountsApplied({ - // invoiceItem, - // subDiscounts, - // }); - - // console.log("Discounts applied: ", discountsApplied); - const ratio = new Decimal(quantity) .div(invoiceItem?.quantity || 1) .toNumber(); - console.log("Ratio: ", ratio); - console.log("Discount amounts: ", invoiceItem?.discount_amounts); + amount = -getUnusedAmountAfterDiscount({ amount, discountAmounts: invoiceItem?.discount_amounts || [], diff --git a/server/src/internal/customers/attach/attachUtils/convertAttachParams.ts b/server/src/internal/customers/attach/attachUtils/convertAttachParams.ts index ae1f3bd96..b6dd3e30a 100644 --- a/server/src/internal/customers/attach/attachUtils/convertAttachParams.ts +++ b/server/src/internal/customers/attach/attachUtils/convertAttachParams.ts @@ -174,7 +174,11 @@ export const paramsToCurSub = async ({ } const sub = await stripeCli.subscriptions.retrieve(subIds[0], { - expand: ["items.data.price.tiers"], + expand: [ + "items.data.price.tiers", + "latest_invoice", + "discounts.coupon.applies_to", + ], }); return sub; diff --git a/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts index 00ea21017..caf47061c 100644 --- a/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts +++ b/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts @@ -23,6 +23,7 @@ import { AttachConfig, UsagePriceConfig, OnDecrease, + OnIncrease, } from "@autumn/shared"; import { addBillingIntervalUnix, @@ -128,6 +129,18 @@ const filterNoProratePrepaidItems = ({ ); filteredItems = items.filter((item) => item.price_id !== curPrice?.id); } + + const onIncrease = curPrice?.proration_config?.on_increase; + if ( + onIncrease == OnIncrease.ProrateNextCycle && + prevQuantity && + quantity > prevQuantity + ) { + console.log( + `Quantity for ${feature_id} increased from ${prevQuantity} to ${quantity}, Removing price: ${curPrice?.id}` + ); + filteredItems = items.filter((item) => item.price_id !== curPrice?.id); + } } return filteredItems; }; @@ -200,6 +213,8 @@ export const getUpgradeProductPreview = async ({ freeTrial = curCusProduct.free_trial; } + // console.log("Disable tiral:", config?.disableTrial); + // console.log("Free trial:", freeTrial); const newPreviewItems = await getItemsForNewProduct({ newProduct, attachParams, diff --git a/server/src/internal/customers/attach/mergeUtils/mergeUtils.ts b/server/src/internal/customers/attach/mergeUtils/mergeUtils.ts index 8f1e0c8f4..1c05c2b20 100644 --- a/server/src/internal/customers/attach/mergeUtils/mergeUtils.ts +++ b/server/src/internal/customers/attach/mergeUtils/mergeUtils.ts @@ -12,7 +12,10 @@ import Stripe from "stripe"; import { getExistingUsageFromCusProducts } from "../../cusProducts/cusEnts/cusEntUtils.js"; import { cusProductToEnts } from "../../cusProducts/cusProductUtils/convertCusProduct.js"; import { AttachParams } from "../../cusProducts/AttachParams.js"; -import { getCustomerSub } from "../attachUtils/convertAttachParams.js"; +import { + attachParamsToCurCusProduct, + getCustomerSub, +} from "../attachUtils/convertAttachParams.js"; export const isMultiProductSub = ({ sub, @@ -83,6 +86,21 @@ export const willMergeSub = async ({ return false; } + const cusProducts = attachParams.customer.customer_products; + const curCusProduct = attachParamsToCurCusProduct({ attachParams }); + + // Case where upgrading to free trial... + if ( + subId && + curCusProduct?.subscription_ids?.includes(subId!) && + !cusProducts.some( + (cp) => cp.subscription_ids?.includes(subId!) && cp.id != curCusProduct.id + ) && + attachParams.freeTrial + ) { + return false; + } + if (subId) return true; return false; diff --git a/server/src/internal/customers/attach/mergeUtils/paramsToScheduleItems.ts b/server/src/internal/customers/attach/mergeUtils/paramsToScheduleItems.ts index 2caf29c57..af599120c 100644 --- a/server/src/internal/customers/attach/mergeUtils/paramsToScheduleItems.ts +++ b/server/src/internal/customers/attach/mergeUtils/paramsToScheduleItems.ts @@ -197,10 +197,10 @@ const computeUpdatedScheduleItems = async ({ removeCusProducts || getCusProductsToRemove({ attachParams, includeScheduled: true }); - console.log( - "REMOVING CUS PRODUCTS:", - cusProductsToRemove?.map((cp) => `${cp.product.id} (E: ${cp.entity_id})`) - ); + // console.log( + // "REMOVING CUS PRODUCTS:", + // cusProductsToRemove?.map((cp) => `${cp.product.id} (E: ${cp.entity_id})`) + // ); const allCusProducts = attachParams.customer.customer_products; @@ -347,18 +347,6 @@ export const paramsToScheduleItems = async ({ const mergedPhases = mergeAdjacentPhasesWithSameItems(newPhases as any); - // console.log(`Merged Phases:`); - // for (const phase of mergedPhases) { - // console.log( - // `Phase ${formatUnixToDateTime(Number(phase.start_date || 0) * 1000)}:` - // ); - // await logPhaseItems({ - // db: req.db, - // items: phase.items, - // }); - // } - // console.log("--------------------------------"); - return { phases: mergedPhases, invoiceItems: itemSet.invoiceItems, @@ -366,29 +354,3 @@ export const paramsToScheduleItems = async ({ }; } }; - -// let newScheduleItems: any[] = mergeNewScheduleItems({ -// itemSet, -// curScheduleItems, -// }); - -// let cusProductsToRemove = -// removeCusProducts || getCusProductsToRemove({ attachParams }); - -// const allCusProducts = attachParams.customer.customer_products; - -// for (const cusProduct of cusProductsToRemove) { -// newScheduleItems = removeCusProductFromScheduleItems({ -// curScheduleItems, -// updateScheduleItems: newScheduleItems, -// allCusProducts, -// cusProduct, -// itemSet, -// }); -// } - -// return { -// items: newScheduleItems, -// invoiceItems: itemSet.invoiceItems, -// usageFeatures: itemSet.usageFeatures, -// }; diff --git a/server/src/internal/customers/attach/mergeUtils/paramsToSubItems.ts b/server/src/internal/customers/attach/mergeUtils/paramsToSubItems.ts index a5860340b..bc5690635 100644 --- a/server/src/internal/customers/attach/mergeUtils/paramsToSubItems.ts +++ b/server/src/internal/customers/attach/mergeUtils/paramsToSubItems.ts @@ -124,11 +124,10 @@ export const paramsToSubItems = async ({ curSubItems, }); - console.log("New sub items:", newSubItems); const allCusProducts = attachParams.customer.customer_products; // 3. Remove items related to cus products to remove - const printRemoveLogs = true; + const printRemoveLogs = false; for (const cusProduct of cusProductsToRemove) { const prices = cusProductToPrices({ cusProduct }); @@ -141,7 +140,7 @@ export const paramsToSubItems = async ({ price, stripeItems: curSubItems, stripeProdId: cusProduct.product.processor?.id, - }); + }) as Stripe.SubscriptionItem | undefined; if (printRemoveLogs) { console.log("Price:", formatPrice({ price })); diff --git a/server/src/internal/invoices/invoiceUtils.ts b/server/src/internal/invoices/invoiceUtils.ts index 3f76ee668..b5c53f5a7 100644 --- a/server/src/internal/invoices/invoiceUtils.ts +++ b/server/src/internal/invoices/invoiceUtils.ts @@ -41,6 +41,16 @@ export const insertInvoiceFromAttach = async ({ }); if (invoice) { + // console.log("UPDATING INVOICE FROM ATTACH:"); + // console.log( + // "Product IDs:", + // attachParams.products.map((p) => p.id) + // ); + // console.log( + // "Internal Product IDs:", + // attachParams.products.map((p) => p.internal_id) + // ); + await InvoiceService.updateByStripeId({ db, stripeId: stripeInvoice.id!, @@ -50,6 +60,16 @@ export const insertInvoiceFromAttach = async ({ }, }); } else { + // console.log("INSERTING INVOICE FROM ATTACH:"); + // console.log( + // "Product IDs:", + // attachParams.products.map((p) => p.id) + // ); + // console.log( + // "Internal Product IDs:", + // attachParams.products.map((p) => p.internal_id) + // ); + await InvoiceService.createInvoiceFromStripe({ db, stripeInvoice, diff --git a/server/src/internal/invoices/previewItemUtils/getItemsForCurProduct.ts b/server/src/internal/invoices/previewItemUtils/getItemsForCurProduct.ts index 96ea01dcd..fdf1a7732 100644 --- a/server/src/internal/invoices/previewItemUtils/getItemsForCurProduct.ts +++ b/server/src/internal/invoices/previewItemUtils/getItemsForCurProduct.ts @@ -62,6 +62,8 @@ export const getItemsForCurProduct = async ({ cusProduct: curCusProduct, org: attachParams.org, now, + latestInvoice: sub?.latest_invoice as Stripe.Invoice, + subDiscounts: sub?.discounts as Stripe.Discount[], }); if (!previewLineItem) continue; diff --git a/server/src/internal/migrations/migrationUtils/runMigrationAttach.ts b/server/src/internal/migrations/migrationUtils/runMigrationAttach.ts index 3cb4e12b0..5a17f1e3e 100644 --- a/server/src/internal/migrations/migrationUtils/runMigrationAttach.ts +++ b/server/src/internal/migrations/migrationUtils/runMigrationAttach.ts @@ -1,7 +1,5 @@ import { handleAddProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.js"; -import { handleUpgradeDiffInterval } from "@/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/handleUpgradeDiffInt.js"; import { handleUpgradeFlow } from "@/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.js"; -import { handleUpgradeSameInterval } from "@/internal/customers/attach/attachFunctions/upgradeSameIntFlow/handleUpgradeSameInt.js"; import { intervalsAreSame } from "@/internal/customers/attach/attachUtils/getAttachConfig.js"; import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { isFreeProduct } from "@/internal/products/productUtils.js"; @@ -54,6 +52,7 @@ export const runMigrationAttach = async ({ sameIntervals, carryTrial: true, invoiceCheckout: false, + finalizeInvoice: true, }; let attachFunction = await getAttachFunction({ attachParams }); diff --git a/server/tests/attach/checkout/checkout5.ts b/server/tests/attach/checkout/checkout5.ts index 9dfbb7b21..0a4d51abc 100644 --- a/server/tests/attach/checkout/checkout5.ts +++ b/server/tests/attach/checkout/checkout5.ts @@ -1,20 +1,17 @@ +import chalk from "chalk"; +import Stripe from "stripe"; + import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; import { APIVersion, AppEnv, Organization } from "@autumn/shared"; -import chalk from "chalk"; -import Stripe from "stripe"; + import { DrizzleCli } from "@/db/initDrizzle.js"; import { setupBefore } from "tests/before.js"; -import { createProducts, createReward } from "tests/utils/productUtils.js"; +import { createProducts } from "tests/utils/productUtils.js"; import { addPrefixToProducts } from "../utils.js"; -import { - constructCoupon, - constructProduct, -} from "@/utils/scriptUtils/createTestProducts.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { TestFeature } from "tests/setup/v2Features.js"; -import { completeCheckoutForm } from "tests/utils/stripeUtils.js"; -import { timeout } from "@/utils/genUtils.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; import { expect } from "chai"; import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; diff --git a/server/tests/attach/free/free2.ts b/server/tests/attach/free/free2.ts index d2a6a4dd8..9409e4f77 100644 --- a/server/tests/attach/free/free2.ts +++ b/server/tests/attach/free/free2.ts @@ -4,11 +4,7 @@ import { APIVersion, AppEnv, BillingInterval, - CreateFreeTrialSchema, - CusProductStatus, - FreeTrialDuration, Organization, - organizations, } from "@autumn/shared"; import chalk from "chalk"; import Stripe from "stripe"; @@ -19,11 +15,7 @@ import { addPrefixToProducts } from "../utils.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { addDays } from "date-fns"; -import { expect } from "chai"; -import { eq } from "drizzle-orm"; -import { CacheManager } from "@/external/caching/CacheManager.js"; -import { clearOrgCache } from "@/internal/orgs/orgUtils/clearOrgCache.js"; + import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; diff --git a/server/tests/attach/migrations/migration4.ts b/server/tests/attach/migrations/migration4.ts index 86732ebc3..fd9413066 100644 --- a/server/tests/attach/migrations/migration4.ts +++ b/server/tests/attach/migrations/migration4.ts @@ -83,7 +83,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro -> pro wi testClockId = testClockId1!; }); - it("should attach free product", async function () { + it("should attach pro product", async function () { await attachAndExpectCorrect({ autumn, customerId, diff --git a/server/tests/attach/migrations/runMigrationTest.ts b/server/tests/attach/migrations/runMigrationTest.ts index e47ccd3ba..9d53e0e4b 100644 --- a/server/tests/attach/migrations/runMigrationTest.ts +++ b/server/tests/attach/migrations/runMigrationTest.ts @@ -14,6 +14,7 @@ import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduc import { expectTrialEndsAtCorrect } from "tests/utils/expectUtils/expectAttach/expectTrialEndsAt.js"; import { timeout } from "@/utils/genUtils.js"; import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js"; export const expectSubsSame = ({ subsBefore, @@ -111,6 +112,15 @@ export const runMigrationTest = async ({ env, }); + if (!isFreeProductV2({ product: toProduct })) { + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + } + // if (!isFreeProductV2({ product: toProduct })) { // expect(cusAfter.invoices.length).to.equal(numInvoices); // } diff --git a/server/tests/attach/prepaid/prepaid3.ts b/server/tests/attach/prepaid/prepaid3.ts index d40fef563..9fd8b75e5 100644 --- a/server/tests/attach/prepaid/prepaid3.ts +++ b/server/tests/attach/prepaid/prepaid3.ts @@ -150,7 +150,7 @@ describe(`${chalk.yellowBright(`attach/${testCase}: upgrade quantity, prorate ne testClockId, advanceTo: addHours( addMonths(new Date(), 1), - hoursToFinalizeInvoice, + hoursToFinalizeInvoice ).getTime(), waitForSeconds: 10, }); diff --git a/server/tests/core/cancel/cancel1.test.ts b/server/tests/core/cancel/cancel1.test.ts new file mode 100644 index 000000000..7380bc07e --- /dev/null +++ b/server/tests/core/cancel/cancel1.test.ts @@ -0,0 +1,276 @@ +// 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, +// Proration, +// } 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 { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; +// 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 { formatUnixToDate, 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"; + +// let premium = constructProduct({ +// id: "premium", +// items: [ +// constructArrearItem({ featureId: TestFeature.Words }), +// constructPrepaidItem({ featureId: TestFeature.Messages }), +// constructArrearProratedItem({ featureId: TestFeature.Users }), +// ], +// type: "premium", +// }); + +// const creditsQuantity = 500; +// const usersOverage = 1; +// const wordsUsage = 300000; +// const ops = [ +// { +// entityId: "1", +// product: premium, +// results: [{ product: premium, status: CusProductStatus.Active }], +// options: [ +// { +// feature_id: TestFeature.Messages, +// quantity: creditsQuantity, +// }, +// ], +// usage: [ +// { +// featureId: TestFeature.Users, +// value: usersOverage + 1, +// }, +// ], +// }, +// ]; + +// const testCase = "cancel1"; +// describe(`${chalk.yellowBright("cancel1: Testing cancelling singular 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], +// 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, +// options: op.options, +// usage: op.usage, +// }); +// } catch (error) { +// console.log( +// `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}` +// ); +// throw error; +// } +// } +// }); + +// it("should advance test clock and upgrade entity 1 to premium, 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 timeout(3000); + +// curUnix = await advanceTestClock({ +// stripeCli, +// testClockId, +// advanceTo: addWeeks(Date.now(), 2).getTime(), +// waitForSeconds: 30, +// }); + +// await autumn.cancel({ +// customer_id: customerId, +// product_id: premium.id, +// cancel_immediately: true, +// // @ts-ignore +// prorate: true, +// }); + +// // 1. Get full customer +// const fullCus = await CusService.getFull({ +// db, +// orgId: org.id, +// env, +// idOrInternalId: customerId, +// inStatuses: [CusProductStatus.Expired, CusProductStatus.Active], +// }); + +// // 2. Calculate base price proration +// const basePrice = getBasePrice({ product: premium }); +// const baseProration = calculateProrationAmount({ +// periodStart: proration.start, +// periodEnd: proration.end, +// now: curUnix, +// amount: basePrice, +// allowNegative: true, +// }); + +// const cusProduct = fullCus.customer_products.find( +// (cusProduct) => cusProduct.product.id === premium.id +// ); + +// // 3. Calculate prepaid and cont use prices +// const prices = cusProductToPrices({ cusProduct: cusProduct! }); +// const creditsPrice = prices.find((price) => isPrepaidPrice({ price })); +// const usersPrice = prices.find((price) => isContUsePrice({ price })); + +// const creditsPriceAmount = priceToInvoiceAmount({ +// price: creditsPrice!, +// quantity: creditsQuantity, +// proration, +// now: curUnix, +// }); + +// const usersPriceAmount = priceToInvoiceAmount({ +// price: usersPrice!, +// overage: usersOverage, +// proration, +// now: curUnix, +// }); + +// // 4. Calculate words amount +// const wordsAmount = await getExpectedInvoiceTotal({ +// db, +// org, +// env, +// onlyIncludeArrear: true, +// usage: [ +// { +// featureId: TestFeature.Words, +// value: wordsUsage, +// }, +// ], +// stripeCli, +// customerId, +// productId: premium.id, +// expectExpired: true, +// }); + +// const totalPrice = new Decimal(wordsAmount) +// .minus(baseProration) +// .minus(creditsPriceAmount) +// .minus(usersPriceAmount) +// .toDecimalPlaces(2) +// .toNumber(); + +// // console.log("BASE PRORATION", baseProration); +// // console.log("CREDITS PRORATION", creditsPriceAmount); +// // console.log("USERS PRORATION", usersPriceAmount); +// // console.log("WORDS AMOUNT", wordsAmount); +// // console.log("TOTAL PRICE", totalPrice); + +// // Get upcoming invoice +// await timeout(5000); // for webhook to trigger +// const upcomingInvoices = await stripeCli.invoices.list({ +// customer: fullCus.processor?.id, +// limit: 1, +// status: "draft", +// }); +// // console.log("INVOICE TOTAL", upcomingInvoices.data[0].total); +// // console.log("INVOICE ID", upcomingInvoices.data[0].id); + +// expect(upcomingInvoices.data[0].total).to.equal(totalPrice * 100); +// }); +// }); diff --git a/server/tests/core/cancel/cancel1.ts b/server/tests/core/cancel/cancel1.ts deleted file mode 100644 index 943bdfc46..000000000 --- a/server/tests/core/cancel/cancel1.ts +++ /dev/null @@ -1,276 +0,0 @@ -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, - Proration, -} 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 { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; -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 { formatUnixToDate, 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"; - -let premium = constructProduct({ - id: "premium", - items: [ - constructArrearItem({ featureId: TestFeature.Words }), - constructPrepaidItem({ featureId: TestFeature.Messages }), - constructArrearProratedItem({ featureId: TestFeature.Users }), - ], - type: "premium", -}); - -const creditsQuantity = 500; -const usersOverage = 1; -const wordsUsage = 300000; -const ops = [ - { - entityId: "1", - product: premium, - results: [{ product: premium, status: CusProductStatus.Active }], - options: [ - { - feature_id: TestFeature.Messages, - quantity: creditsQuantity, - }, - ], - usage: [ - { - featureId: TestFeature.Users, - value: usersOverage + 1, - }, - ], - }, -]; - -const testCase = "cancel1"; -describe(`${chalk.yellowBright("cancel1: Testing cancelling singular 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], - 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, - options: op.options, - usage: op.usage, - }); - } catch (error) { - console.log( - `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}` - ); - throw error; - } - } - }); - - it("should advance test clock and upgrade entity 1 to premium, 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 timeout(3000); - - curUnix = await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addWeeks(Date.now(), 2).getTime(), - waitForSeconds: 30, - }); - - await autumn.cancel({ - customer_id: customerId, - product_id: premium.id, - cancel_immediately: true, - // @ts-ignore - prorate: true, - }); - - // 1. Get full customer - const fullCus = await CusService.getFull({ - db, - orgId: org.id, - env, - idOrInternalId: customerId, - inStatuses: [CusProductStatus.Expired, CusProductStatus.Active], - }); - - // 2. Calculate base price proration - const basePrice = getBasePrice({ product: premium }); - const baseProration = calculateProrationAmount({ - periodStart: proration.start, - periodEnd: proration.end, - now: curUnix, - amount: basePrice, - allowNegative: true, - }); - - const cusProduct = fullCus.customer_products.find( - (cusProduct) => cusProduct.product.id === premium.id - ); - - // 3. Calculate prepaid and cont use prices - const prices = cusProductToPrices({ cusProduct: cusProduct! }); - const creditsPrice = prices.find((price) => isPrepaidPrice({ price })); - const usersPrice = prices.find((price) => isContUsePrice({ price })); - - const creditsPriceAmount = priceToInvoiceAmount({ - price: creditsPrice!, - quantity: creditsQuantity, - proration, - now: curUnix, - }); - - const usersPriceAmount = priceToInvoiceAmount({ - price: usersPrice!, - overage: usersOverage, - proration, - now: curUnix, - }); - - // 4. Calculate words amount - const wordsAmount = await getExpectedInvoiceTotal({ - db, - org, - env, - onlyIncludeArrear: true, - usage: [ - { - featureId: TestFeature.Words, - value: wordsUsage, - }, - ], - stripeCli, - customerId, - productId: premium.id, - expectExpired: true, - }); - - const totalPrice = new Decimal(wordsAmount) - .minus(baseProration) - .minus(creditsPriceAmount) - .minus(usersPriceAmount) - .toDecimalPlaces(2) - .toNumber(); - - // console.log("BASE PRORATION", baseProration); - // console.log("CREDITS PRORATION", creditsPriceAmount); - // console.log("USERS PRORATION", usersPriceAmount); - // console.log("WORDS AMOUNT", wordsAmount); - // console.log("TOTAL PRICE", totalPrice); - - // Get upcoming invoice - await timeout(5000); // for webhook to trigger - const upcomingInvoices = await stripeCli.invoices.list({ - customer: fullCus.processor?.id, - limit: 1, - status: "draft", - }); - // console.log("INVOICE TOTAL", upcomingInvoices.data[0].total); - // console.log("INVOICE ID", upcomingInvoices.data[0].id); - - expect(upcomingInvoices.data[0].total).to.equal(totalPrice * 100); - }); -}); diff --git a/server/tests/core/cancel/cancel2.ts b/server/tests/core/cancel/cancel2.test.ts similarity index 100% rename from server/tests/core/cancel/cancel2.ts rename to server/tests/core/cancel/cancel2.test.ts diff --git a/server/tests/core/cancel/cancel3.ts b/server/tests/core/cancel/cancel3.test.ts similarity index 100% rename from server/tests/core/cancel/cancel3.ts rename to server/tests/core/cancel/cancel3.test.ts diff --git a/server/tests/core/cancel/cancel4.ts b/server/tests/core/cancel/cancel4.test.ts similarity index 100% rename from server/tests/core/cancel/cancel4.ts rename to server/tests/core/cancel/cancel4.test.ts diff --git a/server/tests/core/cancel/cancel5.test.ts b/server/tests/core/cancel/cancel5.test.ts new file mode 100644 index 000000000..fa21447ef --- /dev/null +++ b/server/tests/core/cancel/cancel5.test.ts @@ -0,0 +1,125 @@ +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 { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { addDays } from "date-fns"; +import { expectMultiAttachCorrect } from "tests/utils/expectUtils/expectMultiAttach.js"; +import { products } from "tests/global.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; +import { timeout } from "@/utils/genUtils.js"; +import { expect } from "chai"; + +const testCase = "cancel1"; +describe(`${chalk.yellowBright("cancel1: Testing cancel for trial products")}`, () => { + 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; + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + testClockId = testClockId1!; + }); + + it("should attach pro", async function () { + await autumn.attach({ + customer_id: customerId, + product_id: products.pro.id, + }); + + const customer = await autumn.customers.get(customerId); + expectProductAttached({ + customer, + productId: products.pro.id, + }); + }); + + let sub: Stripe.Subscription | undefined; + + it("should cancel pro product through stripe CLI", async function () { + const fullCus = await CusService.getFull({ + db, + idOrInternalId: customerId, + orgId: org.id, + env, + }); + + sub = await cusProductToSub({ + cusProduct: fullCus.customer_products?.[0], + stripeCli, + }); + + await stripeCli.subscriptions.update(sub!.id, { + cancel_at_period_end: true, + }); + + await timeout(4000); + + const customer = await autumn.customers.get(customerId); + expectProductAttached({ + customer, + productId: products.pro.id, + isCanceled: true, + }); + + expectProductAttached({ + customer, + productId: products.free.id, + status: CusProductStatus.Scheduled, + }); + }); + + it("should renew pro produce through stripe CLI and have it update correctly", async function () { + await stripeCli.subscriptions.update(sub!.id, { + cancel_at_period_end: false, + }); + + await timeout(4000); + + const customer = await autumn.customers.get(customerId); + expectProductAttached({ + customer, + productId: products.pro.id, + status: CusProductStatus.Active, + }); + + expect(customer.products.length).to.equal(1); + }); +}); diff --git a/server/tests/core/multiAttach/multiAttach1.test.ts b/server/tests/core/multiAttach/multiAttach1.test.ts index b3f886c5d..38b3a4d5f 100644 --- a/server/tests/core/multiAttach/multiAttach1.test.ts +++ b/server/tests/core/multiAttach/multiAttach1.test.ts @@ -12,24 +12,11 @@ import { CusProductStatus, Organization, } from "@autumn/shared"; -import { - constructArrearItem, - constructFeatureItem, -} from "@/utils/scriptUtils/constructItem.js"; +import { constructFeatureItem } 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 { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { - advanceTestClock, - completeCheckoutForm, -} from "tests/utils/stripeUtils.js"; -import { addDays, addWeeks } from "date-fns"; -import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { addDays } from "date-fns"; import { expectMultiAttachCorrect } from "tests/utils/expectUtils/expectMultiAttach.js"; let growth = constructProduct({ diff --git a/server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts b/server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts new file mode 100644 index 000000000..27f71bb95 --- /dev/null +++ b/server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts @@ -0,0 +1,180 @@ +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, + getBasePrice, +} from "tests/utils/testProductUtils/testProductUtils.js"; +import { expect } from "chai"; +import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { + advanceTestClock, + completeCheckoutForm, +} from "tests/utils/stripeUtils.js"; +import { addDays, addWeeks } from "date-fns"; +import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; +import { expectMultiAttachCorrect } from "tests/utils/expectUtils/expectMultiAttach.js"; + +let premium = constructProduct({ + id: "premium", + items: [ + constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }), + ], + type: "premium", + trial: true, +}); + +let pro = constructProduct({ + id: "pro", + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage: 300, + }), + ], + type: "pro", + trial: true, +}); + +const ops = [ + { + entityId: "1", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + }, +]; + +const testCase = "multiInvoice1"; +describe(`${chalk.yellowBright("multiInvoice1: Testing multi attach through invoice flow")}`, () => { + 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 run multi attach through checkout and have correct sub", async function () { + const productsList = [ + { + product_id: pro.id, + quantity: 3, + product: pro, + status: CusProductStatus.Trialing, + }, + { + product_id: premium.id, + quantity: 3, + product: premium, + status: CusProductStatus.Trialing, + }, + ]; + + await expectMultiAttachCorrect({ + customerId, + products: productsList, + results: productsList, + attachParams: { + invoice: true, + enable_product_immediately: true, + }, + db, + org, + env, + }); + }); + + it("should update premium & pro while trialing", async function () { + const newProducts = [ + { + product_id: premium.id, + quantity: 2, + }, + { + product_id: pro.id, + quantity: 4, + }, + ]; + + const results = [ + { + product: pro, + quantity: 4, + status: CusProductStatus.Trialing, + }, + { + product: premium, + quantity: 2, + status: CusProductStatus.Trialing, + }, + ]; + + await expectMultiAttachCorrect({ + customerId, + products: newProducts, + results, + db, + org, + env, + }); + }); +}); diff --git a/server/tests/core/multiAttach/multiReward/multiReward3.test.ts b/server/tests/core/multiAttach/multiReward/multiReward3.test.ts index 15b2fdb70..3b7c40da4 100644 --- a/server/tests/core/multiAttach/multiReward/multiReward3.test.ts +++ b/server/tests/core/multiAttach/multiReward/multiReward3.test.ts @@ -141,6 +141,7 @@ describe(`${chalk.yellowBright("multiReward3: Testing multi attach with rewards stripeCli, testClockId, advanceTo: addDays(new Date(), 12).getTime(), + waitForSeconds: 30, }); await expectSubToBeCorrect({ diff --git a/server/tests/core/multiAttach/multiReward/multiRewardUtils.test.ts b/server/tests/core/multiAttach/multiReward/multiRewardUtils.test.ts index 1f4132987..abc6e5f73 100644 --- a/server/tests/core/multiAttach/multiReward/multiRewardUtils.test.ts +++ b/server/tests/core/multiAttach/multiReward/multiRewardUtils.test.ts @@ -98,6 +98,12 @@ export const setupMultiRewardBefore = async ({ ]) { // let res = await autumn.products.get(product.id); + // if (res.code === "product_not_found") { + // try { + // await autumn.products.create(product); + // } catch (error) {} + // } + try { await autumn.products.delete(product.id); } catch (error) { @@ -128,17 +134,26 @@ export const setupMultiRewardBefore = async ({ ]; for (const reward of [proReward, premiumReward]) { + let rewardRes = null; + // try { + // rewardRes = await autumn.rewards.get(reward.id); + // } catch (error) {} + try { await autumn.rewards.delete(reward.id); } catch (error) {} - try { - await autumn.rewards.create({ - ...reward, - discount_config: { - ...reward.discount_config, - price_ids: reward.id == proReward.id ? proPriceIds : premiumPriceIds, - }, - }); - } catch (error) {} + + if (!rewardRes) { + try { + await autumn.rewards.create({ + ...reward, + discount_config: { + ...reward.discount_config, + price_ids: + reward.id == proReward.id ? proPriceIds : premiumPriceIds, + }, + }); + } catch (error) {} + } } }; diff --git a/server/tests/merged/mergedAdd1.ts b/server/tests/merged/add/mergedAdd1.test.ts similarity index 98% rename from server/tests/merged/mergedAdd1.ts rename to server/tests/merged/add/mergedAdd1.test.ts index a0c5f2dfa..a8f20c8df 100644 --- a/server/tests/merged/mergedAdd1.ts +++ b/server/tests/merged/add/mergedAdd1.test.ts @@ -14,7 +14,7 @@ import { getBasePrice, } from "tests/utils/testProductUtils/testProductUtils.js"; import { expect } from "chai"; -import { expectSubToBeCorrect } from "./mergeUtils.test.js"; +import { expectSubToBeCorrect } from "../mergeUtils.test.js"; import { getAttachPreviewTotal } from "tests/utils/testAttachUtils/getAttachPreviewTotal.js"; import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; diff --git a/server/tests/merged/mergedAdd2.ts b/server/tests/merged/add/mergedAdd2.test.ts similarity index 98% rename from server/tests/merged/mergedAdd2.ts rename to server/tests/merged/add/mergedAdd2.test.ts index f5fe5d49d..6b7373748 100644 --- a/server/tests/merged/mergedAdd2.ts +++ b/server/tests/merged/add/mergedAdd2.test.ts @@ -14,7 +14,7 @@ import { getBasePrice, } from "tests/utils/testProductUtils/testProductUtils.js"; import { expect } from "chai"; -import { expectSubToBeCorrect } from "./mergeUtils.test.js"; +import { expectSubToBeCorrect } from "../mergeUtils.test.js"; import { getAttachPreviewTotal } from "tests/utils/testAttachUtils/getAttachPreviewTotal.js"; import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; diff --git a/server/tests/merged/addOn/mergedAddOn1.test.ts b/server/tests/merged/addOn/mergedAddOn1.test.ts index d737000fa..041c68d28 100644 --- a/server/tests/merged/addOn/mergedAddOn1.test.ts +++ b/server/tests/merged/addOn/mergedAddOn1.test.ts @@ -14,7 +14,6 @@ import { AppEnv, CusProductStatus, Organization, - ProductItemInterval, } from "@autumn/shared"; import { constructArrearItem, @@ -223,6 +222,6 @@ describe(`${chalk.yellowBright("mergedAddOn1: Adding an add on")}`, () => { status: CusProductStatus.Active, }); const products = customer.products.filter((p) => p.group === addOn.group); - expect(products.length).to.equal(0); + expect(products.length).to.equal(1); }); }); diff --git a/server/tests/merged/addOn/mergedAddOn3.test.ts b/server/tests/merged/addOn/mergedAddOn3.test.ts index 6471216ff..8b6784dfb 100644 --- a/server/tests/merged/addOn/mergedAddOn3.test.ts +++ b/server/tests/merged/addOn/mergedAddOn3.test.ts @@ -187,6 +187,7 @@ describe(`${chalk.yellowBright("mergedAddOn3: testing add ons between multiple e await autumn.cancel({ customer_id: customerId, product_id: addOn.id, + entity_id: "1", cancel_immediately: true, }); diff --git a/server/tests/merged/downgrade/mergedDowngrade4.ts b/server/tests/merged/downgrade/mergedDowngrade4.ts index 306239ec5..1630f6e22 100644 --- a/server/tests/merged/downgrade/mergedDowngrade4.ts +++ b/server/tests/merged/downgrade/mergedDowngrade4.ts @@ -181,7 +181,7 @@ describe(`${chalk.yellowBright("mergedDowngrade4: Testing advance clock, schedul product: premiumAnnual, status: CusProductStatus.Active, }, - { entityId: "2", product: premium, status: CusProductStatus.Active }, + { entityId: "2", product: pro, status: CusProductStatus.Active }, ]; for (const result of results) { diff --git a/server/tests/merged/mergeUtils/expectSubCorrect.ts b/server/tests/merged/mergeUtils/expectSubCorrect.ts index 52168fdb9..118b02775 100644 --- a/server/tests/merged/mergeUtils/expectSubCorrect.ts +++ b/server/tests/merged/mergeUtils/expectSubCorrect.ts @@ -36,6 +36,8 @@ import { } from "@/internal/customers/attach/mergeUtils/phaseUtils/phaseUtils.js"; import { PriceService } from "@/internal/products/prices/PriceService.js"; import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; +import { subIsCanceled } from "@/external/stripe/stripeSubUtils.js"; +import { defaultApiVersion } from "tests/constants.js"; const compareActualItems = async ({ actualItems, @@ -131,6 +133,7 @@ const compareActualItems = async ({ export const expectSubToBeCorrect = async ({ db, customerId, + entityId, org, env, shouldBeCanceled = false, @@ -141,6 +144,7 @@ export const expectSubToBeCorrect = async ({ }: { db: DrizzleCli; customerId: string; + entityId?: string; org: Organization; env: AppEnv; shouldBeCanceled?: boolean; @@ -200,6 +204,7 @@ export const expectSubToBeCorrect = async ({ // Add to schedules const scheduleIndexes: number[] = []; + const apiVersion = cusProduct.api_version || defaultApiVersion; if (isFreeProduct(product.prices)) { expect(cusProduct.subscription_ids, "free product should have no subs").to @@ -292,9 +297,9 @@ export const expectSubToBeCorrect = async ({ org, options, existingUsage, - withEntity: true, + withEntity: !!entityId, isCheckout: false, - apiVersion: APIVersion.v1_4, + apiVersion, productOptions: cusProduct.quantity ? { product_id: product.id, @@ -303,6 +308,12 @@ export const expectSubToBeCorrect = async ({ : undefined, }); + if (res?.lineItem && nullish(res.lineItem.quantity)) { + res.lineItem.quantity = 0; + } + + // console.log("API VERSION:", apiVersion); + // console.log("LINE ITEM:", res?.lineItem); if (options?.upcoming_quantity && res?.lineItem) { res.lineItem.quantity = options.upcoming_quantity; } @@ -316,7 +327,7 @@ export const expectSubToBeCorrect = async ({ if (existingIndex !== -1) { // @ts-ignore - supposedSubItems[existingIndex].quantity += lineItem.quantity!; + supposedSubItems[existingIndex].quantity += lineItem.quantity; } else { supposedSubItems.push({ ...res.lineItem, @@ -384,7 +395,8 @@ export const expectSubToBeCorrect = async ({ if (shouldBeCanceled) { expect(sub.schedule, "sub should NOT have a schedule").to.be.null; - expect(sub.cancel_at, "sub should be canceled").to.exist; + // expect(sub.cancel_at, "sub should be canceled").to.exist; + expect(subIsCanceled({ sub }), "sub should be canceled").to.be.true; return; } @@ -395,40 +407,24 @@ export const expectSubToBeCorrect = async ({ }) : null; - console.log("--------------------------------"); - console.log("Supposed phases:"); - await logPhases({ - phases: supposedPhases, - db, - }); - // for (const phase of supposedPhases) { - // console.log(`Phase ${formatUnixToDateTime(phase.start_date)}:`); - // await logPhaseItems({ - // db, - // items: phase.items, - // }); - // } - console.log("--------------------------------"); - console.log("Actual phases:"); + // console.log("--------------------------------"); + // console.log("Supposed phases:"); + // await logPhases({ + // phases: supposedPhases, + // db, + // }); - await logPhases({ - phases: (schedule?.phases as any) || [], - db, - }); - // for (const phase of schedule?.phases || []) { - // console.log(`Phase ${formatUnixToDateTime(phase.start_date * 1000)}:`); - // await logPhaseItems({ - // db, - // items: phase.items.map((item) => ({ - // price: (item.price as Stripe.Price).id, - // quantity: item.quantity, - // })), - // }); - // } + // console.log("--------------------------------"); + // console.log("Actual phases:"); + + // await logPhases({ + // phases: (schedule?.phases as any) || [], + // db, + // }); for (let i = 0; i < supposedPhases.length; i++) { const supposedPhase = supposedPhases[i]; - console.log("Supposed phase items:", supposedPhase.items); + if (supposedPhase.items.length === 0) continue; const actualPhase = schedule?.phases?.[i + 1]; diff --git a/server/tests/merged/trial/trial2.test.ts b/server/tests/merged/trial/trial2.test.ts index b8d3f29a0..3ee2d6dee 100644 --- a/server/tests/merged/trial/trial2.test.ts +++ b/server/tests/merged/trial/trial2.test.ts @@ -23,6 +23,8 @@ import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.j import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { addDays } from "date-fns"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { timeout } from "@/utils/genUtils.js"; +import { Decimal } from "decimal.js"; // Pro Trial // Trial Finishes @@ -101,7 +103,7 @@ describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from testClockId = testClockId1!; }); - it("should attach first trial, and advance clock past trial", async function () { + it("should attach first trial", async function () { for (const op of ops) { await attachAndExpectCorrect({ autumn, @@ -122,7 +124,7 @@ describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from }); }); - it("should advance test clock to before trial ends and attach premium", async function () { + it("should advance test clock to past trial ends and attach premium", async function () { curUnix = await advanceTestClock({ stripeCli, testClockId, @@ -134,6 +136,11 @@ describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from product_id: premium.id, }); + const checkoutRes = await autumn.checkout({ + customer_id: customerId, + product_id: premium.id, + }); + expect(attachPreview?.branch).to.equal(AttachBranch.Upgrade); await autumn.attach({ @@ -141,6 +148,8 @@ describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from product_id: premium.id, }); + await timeout(5000); + const customer = await autumn.customers.get(customerId); expectProductAttached({ customer, @@ -153,6 +162,10 @@ describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from 1000 * 60 * 30 // 30 minutes ); + expect(customer.invoices[0].total).to.equal( + new Decimal(checkoutRes.total).toDP(2).toNumber() + ); + await expectSubToBeCorrect({ db, customerId, diff --git a/server/tests/merged/upgrade/mergedUpgrade3.test.ts b/server/tests/merged/upgrade/mergedUpgrade3.test.ts index d87087386..e77279d83 100644 --- a/server/tests/merged/upgrade/mergedUpgrade3.test.ts +++ b/server/tests/merged/upgrade/mergedUpgrade3.test.ts @@ -75,6 +75,7 @@ const ops = [ entityId: "2", product: growth, results: [{ product: growth, status: CusProductStatus.Active }], + // waitForInvoice: 5000, }, ]; diff --git a/server/tests/utils/expectUtils/expectAttach.ts b/server/tests/utils/expectUtils/expectAttach.ts index 6f81b85fb..fc6473f9f 100644 --- a/server/tests/utils/expectUtils/expectAttach.ts +++ b/server/tests/utils/expectUtils/expectAttach.ts @@ -89,7 +89,7 @@ export const attachAndExpectCorrect = async ({ ...attachParams, }); - const logCheckoutRes = true; + const logCheckoutRes = false; if (logCheckoutRes) { console.log("Checkout res:"); for (const line of checkoutRes.lines) { @@ -210,6 +210,7 @@ export const attachAndExpectCorrect = async ({ flags: { checkNotTrialing, }, + entityId, }); // await expectSubItemsCorrect({ diff --git a/server/tests/utils/expectUtils/expectMultiAttach.ts b/server/tests/utils/expectUtils/expectMultiAttach.ts index 2685e2ea6..903f75660 100644 --- a/server/tests/utils/expectUtils/expectMultiAttach.ts +++ b/server/tests/utils/expectUtils/expectMultiAttach.ts @@ -30,6 +30,7 @@ import { AttachParams, Customer } from "autumn-js"; import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js"; import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js"; import { Decimal } from "decimal.js"; +import { completeInvoiceCheckout } from "../stripeUtils/completeInvoiceCheckout.js"; export const expectMultiAttachCorrect = async ({ customerId, @@ -38,6 +39,7 @@ export const expectMultiAttachCorrect = async ({ results, rewards, expectedRewards, + attachParams, db, org, env, @@ -52,6 +54,7 @@ export const expectMultiAttachCorrect = async ({ }[]; rewards?: string[]; expectedRewards?: string[]; + attachParams?: any; db: DrizzleCli; org: Organization; env: AppEnv; @@ -63,6 +66,7 @@ export const expectMultiAttachCorrect = async ({ entity_id: entityId, // @ts-ignore reward: rewards, + ...attachParams, }); const attachRes = await autumn.attach({ @@ -71,9 +75,16 @@ export const expectMultiAttachCorrect = async ({ entity_id: entityId, // @ts-ignore reward: rewards, + ...attachParams, }); if (attachRes.checkout_url) { + if (attachParams?.invoice) { + await completeInvoiceCheckout({ + url: attachRes.checkout_url, + isLocal: true, + }); + } await completeCheckoutForm(attachRes.checkout_url); await timeout(5000); } diff --git a/server/tests/utils/expectUtils/expectProductAttached.ts b/server/tests/utils/expectUtils/expectProductAttached.ts index 4da58bb3c..d984ab027 100644 --- a/server/tests/utils/expectUtils/expectProductAttached.ts +++ b/server/tests/utils/expectUtils/expectProductAttached.ts @@ -10,12 +10,14 @@ export const expectProductAttached = ({ productId, status, entityId, + isCanceled = false, }: { customer: Customer; product?: ProductV2; productId?: string; status?: CusProductStatus; entityId?: string; + isCanceled?: boolean; }) => { const cusProducts = customer.products; const finalProductId = productId || product?.id; @@ -47,6 +49,12 @@ export const expectProductAttached = ({ // @ts-ignore expect(productAttached?.entity_id).to.equal(entityId); } + + if (isCanceled) { + expect(productAttached?.canceled_at).to.exist; + // @ts-ignore + expect(productAttached?.canceled).to.be.true; + } }; export const expectInvoicesCorrect = ({ diff --git a/server/tests/utils/expectUtils/expectScheduleUtils.ts b/server/tests/utils/expectUtils/expectScheduleUtils.ts index 2f70bb894..e0958ce70 100644 --- a/server/tests/utils/expectUtils/expectScheduleUtils.ts +++ b/server/tests/utils/expectUtils/expectScheduleUtils.ts @@ -9,6 +9,7 @@ import { AppEnv, AttachBranch, AttachPreview, + CusProductStatus, FullCusProduct, FullCustomer, Organization, @@ -22,6 +23,7 @@ import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduc import { advanceTestClock } from "../stripeUtils.js"; import { hoursToFinalizeInvoice } from "../constants.js"; import { addHours } from "date-fns"; +import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js"; export const expectNextCycleCorrect = async ({ autumn, @@ -119,42 +121,50 @@ export const expectDowngradeCorrect = async ({ expectProductAttached({ customer, product: curProduct, - }); - - const { fullCus } = await expectSubItemsCorrect({ - stripeCli, - customerId, - product: curProduct, - db, - org, - env, isCanceled: true, }); + // const { fullCus } = await expectSubItemsCorrect({ + // stripeCli, + // customerId, + // product: curProduct, + // db, + // org, + // env, + // subCanceled: isFreeProductV2({ product: newProduct }), + // isCanceled: true, + // }); + const newProductIsFree = isFreeProductV2({ product: newProduct }); if (newProductIsFree) { - let res = await stripeCli.subscriptionSchedules.list({ - customer: fullCus.processor?.id, - }); - - let data = res.data.filter((s) => s.status != "canceled"); - expect(data.length, "should have no sub schedules").to.equal(0); - - await expectSubScheduleCorrect({ - stripeCli, - customerId, - productId: newProduct.id, - db, - org, - env, - }); - - expectProductAttached({ - customer, - product: newProduct, - }); + // let res = await stripeCli.subscriptionSchedules.list({ + // customer: fullCus.processor?.id, + // }); + // let data = res.data.filter((s) => s.status != "canceled"); + // expect(data.length, "should have no sub schedules").to.equal(0); + // await expectSubScheduleCorrect({ + // stripeCli, + // customerId, + // productId: newProduct.id, + // db, + // org, + // env, + // }); } + expectProductAttached({ + customer, + product: newProduct, + status: CusProductStatus.Scheduled, + }); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + shouldBeCanceled: newProductIsFree, + }); expect(preview.branch).to.equal(AttachBranch.Downgrade); diff --git a/server/tests/utils/expectUtils/expectSubUtils.ts b/server/tests/utils/expectUtils/expectSubUtils.ts index 26db1c7ce..9b2345b1c 100644 --- a/server/tests/utils/expectUtils/expectSubUtils.ts +++ b/server/tests/utils/expectUtils/expectSubUtils.ts @@ -12,7 +12,7 @@ import { CusService } from "@/internal/customers/CusService.js"; import { getBillingType } from "@/internal/products/prices/priceUtils.js"; import { isV4Usage } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js"; import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js"; -import { nullish } from "@/utils/genUtils.js"; +import { notNullish, nullish } from "@/utils/genUtils.js"; import { AppEnv, BillingType, @@ -128,6 +128,14 @@ export const getSubsFromCusId = async ({ // }; // }; +const subIsCanceled = ({ sub }: { sub: Stripe.Subscription }) => { + return ( + notNullish(sub.canceled_at) || + notNullish(sub.cancel_at) || + sub.cancel_at_period_end + ); +}; + export const expectSubItemsCorrect = async ({ stripeCli, customerId, @@ -135,6 +143,7 @@ export const expectSubItemsCorrect = async ({ db, org, env, + subCanceled = false, isCanceled = false, entityId, }: { @@ -144,6 +153,7 @@ export const expectSubItemsCorrect = async ({ db: DrizzleCli; org: Organization; env: AppEnv; + subCanceled?: boolean; isCanceled?: boolean; entityId?: string; }) => { @@ -167,13 +177,21 @@ export const expectSubItemsCorrect = async ({ if (isCanceled) { expect( cusProduct.canceled_at, - `cus product ${cusProduct.product.id} should be canceled` + `cus product ${cusProduct.product.id} have field 'canceled_at' set` ).to.exist; + expect( + cusProduct.canceled, + `cus product ${cusProduct.product.id} have field 'canceled' set to true` + ).to.be.true; } else { expect( cusProduct.canceled_at, - `cus product ${cusProduct.product.id} should not be canceled` + `cus product ${cusProduct.product.id} should not have field 'canceled_at' set` ).to.not.exist; + expect( + cusProduct.canceled, + `cus product ${cusProduct.product.id} should not have field 'canceled' set` + ).to.be.false; } if (isFreeProductV2({ product })) { @@ -192,10 +210,10 @@ export const expectSubItemsCorrect = async ({ }); for (const sub of subs) { - if (isCanceled) { - expect(sub.canceled_at, "sub should be canceled").to.exist; + if (subCanceled) { + expect(subIsCanceled({ sub }), "sub should be canceled").to.be.true; } else { - expect(sub.canceled_at, "sub should not be canceled").to.be.null; + expect(subIsCanceled({ sub }), "sub should not be canceled").to.be.false; } } @@ -218,9 +236,6 @@ export const expectSubItemsCorrect = async ({ const usagePriceConfig = price.config as UsagePriceConfig; - // console.log("Sub item:", subItem); - // console.log("Usage price config:", usagePriceConfig); - expect( nullish(subItem) || (subItem?.quantity === 0 && diff --git a/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx b/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx index e83a7f650..d82ca9b4d 100644 --- a/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx +++ b/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx @@ -4,20 +4,11 @@ import { CustomDialogFooter, } from "@/components/general/modal-components/DialogContentWrapper"; import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { MainDialogBodyWrapper } from "@/views/products/product/product-item/product-item-config/AdvancedConfigSidebar"; +import { Dialog, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { InvoiceCustomerButton } from "../components/InvoiceCustomerButton"; import FieldLabel from "@/components/general/modal-components/FieldLabel"; -import { ArrowUpRightFromSquare, Loader2, Minus, Plus, X } from "lucide-react"; -import { - DropdownMenu, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; +import { ArrowUpRightFromSquare, Loader2, Plus, X } from "lucide-react"; + import { useCustomerContext } from "../../CustomerContext"; import { Select, @@ -58,6 +49,7 @@ export const MultiAttachDialog = ({ const [checkoutResult, setCheckoutResult] = useState( null ); + const [attachLoading, setAttachLoading] = useState(false); const defaultCurrency = org?.default_currency || "usd"; @@ -97,7 +89,6 @@ export const MultiAttachDialog = ({ } } - console.log("attachRewards", attachRewards); for (const reward of attachRewards) { if (!reward.reward_id) { return false; @@ -333,20 +324,21 @@ export const MultiAttachDialog = ({ 0} />