diff --git a/server/run.sh b/server/run.sh index 42bf60833..313e0e56d 100755 --- a/server/run.sh +++ b/server/run.sh @@ -3,8 +3,11 @@ # npx tsx scripts/alex.ts filename=$1 -# Check if the file path contains "/tests/" -if [[ $filename == *"/tests/"* ]]; then + +# Check if the file path contains "shell" +if [[ $filename == *"shell"* ]]; then + $filename +elif [[ $filename == *"/tests/"* ]]; then # Extract everything after "/tests/" path_after_tests=$(echo "$filename" | sed 's/.*\/tests\///') # Remove .ts extension if present diff --git a/server/shell/config.sh b/server/shell/config.sh new file mode 100644 index 000000000..ab16c3b1c --- /dev/null +++ b/server/shell/config.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +MOCHA_SETUP="npx mocha tests/00_setup.ts" +MOCHA_CMD="npx mocha --parallel --timeout 10000000 --ignore tests/00_setup.ts" \ No newline at end of file diff --git a/server/shell/g1.sh b/server/shell/g1.sh new file mode 100755 index 000000000..5149a001b --- /dev/null +++ b/server/shell/g1.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Source shared configuration +source "$(dirname "$0")/config.sh" + +MOCHA_PARALLEL=true $MOCHA_SETUP \ +&& $MOCHA_CMD \ +'tests/attach/basic/*.ts' \ +'tests/attach/upgrade/*.ts' \ +'tests/attach/downgrade/*.ts' \ No newline at end of file diff --git a/server/shell/g2.sh b/server/shell/g2.sh new file mode 100755 index 000000000..cdf6bf9ec --- /dev/null +++ b/server/shell/g2.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Source shared configuration +source "$(dirname "$0")/config.sh" + +MOCHA_PARALLEL=true $MOCHA_SETUP && $MOCHA_CMD \ +'tests/attach/upgradeOld/*.ts' \ +'tests/attach/entities/*.ts' \ +'tests/attach/migrations/*.ts' \ +'tests/attach/newVersion/*.ts' \ +'tests/attach/others/*.ts' \ +'tests/attach/updateEnts/*.ts' \ + + diff --git a/server/shell/g3.sh b/server/shell/g3.sh new file mode 100755 index 000000000..61d9a5803 --- /dev/null +++ b/server/shell/g3.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Source shared configuration +source "$(dirname "$0")/config.sh" + +MOCHA_PARALLEL=true $MOCHA_SETUP + +$MOCHA_CMD 'tests/contUse/entities/*.ts' + +$MOCHA_CMD 'tests/contUse/update/*.ts' + +$MOCHA_CMD 'tests/contUse/track/*.ts' \ No newline at end of file diff --git a/server/shell/g4.sh b/server/shell/g4.sh new file mode 100755 index 000000000..3ef7c5133 --- /dev/null +++ b/server/shell/g4.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Source shared configuration +source "$(dirname "$0")/config.sh" + +MOCHA_PARALLEL=true $MOCHA_SETUP + +$MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \ + 'tests/advanced/coupons/*.ts' \ + 'tests/attach/updateQuantity/*.ts' \ + 'tests/advanced/referrals/*.ts' + +# $MOCHA_CMD 'tests/attach/multiProduct/*.ts' + +# $MOCHA_CMD 'tests/advanced/usage/*.ts' + \ No newline at end of file diff --git a/server/shell/g5.sh b/server/shell/g5.sh new file mode 100755 index 000000000..52d062577 --- /dev/null +++ b/server/shell/g5.sh @@ -0,0 +1,4 @@ +MOCHA_PARALLEL=true npx mocha 'tests/alex/00_setup.ts' && 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 21d7e2447..569558c87 100644 --- a/server/src/external/autumn/autumnCli.ts +++ b/server/src/external/autumn/autumnCli.ts @@ -7,11 +7,13 @@ import { CusExpand, EntityExpand, ErrCode, + Invoice, } from "@autumn/shared"; import { CancelParams, CheckParams, CheckResult, + Customer, TrackParams, UsageParams, } from "autumn-js"; @@ -214,7 +216,11 @@ export class AutumnInt { params?: { expand?: CusExpand[]; }, - ) => { + ): Promise< + Customer & { + invoices: any[]; + } + > => { const queryParams = new URLSearchParams(); const defaultParams = { expand: [CusExpand.Invoices], @@ -335,6 +341,11 @@ export class AutumnInt { const data = await this.post(`/rewards`, reward); return data; }, + + delete: async (rewardId: string) => { + const data = await this.delete(`/rewards/${rewardId}`); + return data; + }, }; rewardPrograms = { @@ -422,7 +433,7 @@ export class AutumnInt { return data; }; - cancel = async (params: CancelParams) => { + cancel = async (params: CancelParams & { expire_immediately?: boolean }) => { const data = await this.post(`/cancel`, params); return data; }; diff --git a/server/src/external/stripe/priceToStripeItem/priceToUsageInAdvance.ts b/server/src/external/stripe/priceToStripeItem/priceToUsageInAdvance.ts index d4f4db7f3..95621fa8d 100644 --- a/server/src/external/stripe/priceToStripeItem/priceToUsageInAdvance.ts +++ b/server/src/external/stripe/priceToStripeItem/priceToUsageInAdvance.ts @@ -60,6 +60,8 @@ export const priceToUsageInAdvance = ({ let optionsQuantity = options?.quantity; let finalQuantity = optionsQuantity; + console.log("options", options); + // 1. If adjustable quantity is set, use that, else if quantity is undefined, adjustable is true, else false let adjustable = !nullish(options?.adjustable_quantity) ? options!.adjustable_quantity diff --git a/server/src/external/stripe/stripeProductUtils.ts b/server/src/external/stripe/stripeProductUtils.ts index 7b21738de..6b67f06ca 100644 --- a/server/src/external/stripe/stripeProductUtils.ts +++ b/server/src/external/stripe/stripeProductUtils.ts @@ -7,7 +7,7 @@ import { StatusCodes } from "http-status-codes"; export const createStripeProduct = async ( org: Organization, env: AppEnv, - product: Product + product: Product, ) => { try { const stripe = createStripeCli({ org, env }); @@ -33,7 +33,7 @@ export const createStripeProduct = async ( export const deleteStripeProduct = async ( org: Organization, env: AppEnv, - product: Product + product: Product, ) => { const stripe = createStripeCli({ org, env }); @@ -85,14 +85,14 @@ export const deactivateStripeMeters = async ({ } } - const batchSize = 10; + const batchSize = 40; for (let i = 0; i < allStripeMeters.length; i += batchSize) { const batch = allStripeMeters.slice(i, i + batchSize); await Promise.all( - batch.map((meter) => stripeCli.billing.meters.deactivate(meter.id)) + batch.map((meter) => stripeCli.billing.meters.deactivate(meter.id)), ); console.log( - `Deactivated ${i + batch.length}/${allStripeMeters.length} meters` + `Deactivated ${i + batch.length}/${allStripeMeters.length} meters`, ); await new Promise((resolve) => setTimeout(resolve, 1000)); } @@ -125,7 +125,7 @@ export const deleteAllStripeProducts = async ({ }); } - let batchSize = 10; + let batchSize = 50; for (let i = 0; i < stripeProducts.data.length; i += batchSize) { let batch = stripeProducts.data.slice(i, i + batchSize); await Promise.all( @@ -137,10 +137,10 @@ export const deleteAllStripeProducts = async ({ active: false, }); } - }) + }), ); console.log( - `Deleted ${i + batch.length}/${stripeProducts.data.length} products` + `Deleted ${i + batch.length}/${stripeProducts.data.length} products`, ); await new Promise((resolve) => setTimeout(resolve, 1000)); } diff --git a/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts b/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts index 238eb48ef..cc44e97be 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts @@ -40,6 +40,10 @@ export const handleCusProductDeleted = async ({ prematurelyCanceled: boolean; }) => { const { org, env } = req; + // const customerId = cusProduct.customer!.id; + // const orgId = org.id; + // const lockKey = `attach_${customerId}_${orgId}_${env}`; + const { scheduled_ids } = cusProduct; const customer = await CusService.getFull({ @@ -84,6 +88,7 @@ export const handleCusProductDeleted = async ({ } if (cusProduct.status === CusProductStatus.Expired) { + // When attaching eg. main is trial, canceled in attach function, don't handle... return; } diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts index ce247f9e7..25fc90c76 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts @@ -1,19 +1,13 @@ -import { AttachScenario } from "@autumn/shared"; - -import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; - -import { CusProductStatus, FullCusProduct } from "@autumn/shared"; import Stripe from "stripe"; +import { AttachScenario } from "@autumn/shared"; +import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; +import { CusProductStatus, FullCusProduct } from "@autumn/shared"; import { formatUnixToDateTime, nullish } from "@/utils/genUtils.js"; import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js"; import { CusService } from "@/internal/customers/CusService.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { ExtendedRequest } from "@/utils/models/Request.js"; -import { - webhookToAttachParams, - webhookToInsertParams, -} from "../../webhookUtils/webhookUtils.js"; -import { attachToInsertParams } from "@/internal/products/productUtils.js"; +import { productToInsertParams } from "@/internal/customers/attach/attachUtils/attachParams/convertToParams.js"; export const handleSubCanceled = async ({ req, @@ -36,7 +30,7 @@ export const handleSubCanceled = async ({ const canceledFromPortal = isCanceled && !isAutumnDowngrade; - const { db, org, env, features, logtail: logger } = req; + const { db, org, env, logtail: logger } = req; if (!canceledFromPortal || updatedCusProducts.length == 0) { return; @@ -86,10 +80,10 @@ export const handleSubCanceled = async ({ continue; } - let insertParams = webhookToInsertParams({ + let insertParams = productToInsertParams({ req, - cusProduct: updatedCusProducts[0], fullCus, + newProduct: product, entities, }); diff --git a/server/src/external/stripe/webhookUtils/webhookUtils.ts b/server/src/external/stripe/webhookUtils/webhookUtils.ts index c2a3c21e7..b0ff31854 100644 --- a/server/src/external/stripe/webhookUtils/webhookUtils.ts +++ b/server/src/external/stripe/webhookUtils/webhookUtils.ts @@ -48,35 +48,3 @@ export const webhookToAttachParams = ({ return params; }; - -export const webhookToInsertParams = ({ - req, - cusProduct, - fullCus, - entities, -}: { - req: ExtendedRequest; - cusProduct: FullCusProduct; - fullCus: FullCustomer; - entities?: Entity[]; -}): InsertCusProductParams => { - const fullProduct = cusProductToProduct({ cusProduct }); - - const params: InsertCusProductParams = { - customer: fullCus, - org: req.org, - product: fullProduct, - prices: cusProductToPrices({ cusProduct }), - entitlements: cusProductToEnts({ cusProduct }), - features: req.features, - freeTrial: cusProduct.free_trial || null, - optionsList: cusProduct.options, - cusProducts: [cusProduct], - - internalEntityId: cusProduct.internal_entity_id || undefined, - entities: entities || [], - replaceables: [], - }; - - return params; -}; diff --git a/server/src/internal/api/events/EventService.ts b/server/src/internal/api/events/EventService.ts index b43d58e3f..b50d912da 100644 --- a/server/src/internal/api/events/EventService.ts +++ b/server/src/internal/api/events/EventService.ts @@ -55,7 +55,7 @@ export class EventService { event_name: events.event_name, value: events.value, created_at: events.created_at, - // timestamp: events.timestamp, + timestamp: events.timestamp, idempotency_key: events.idempotency_key, properties: events.properties, set_usage: events.set_usage, diff --git a/server/src/internal/api/products/productRouter.ts b/server/src/internal/api/products/productRouter.ts index d6760768f..cf240d3df 100644 --- a/server/src/internal/api/products/productRouter.ts +++ b/server/src/internal/api/products/productRouter.ts @@ -44,12 +44,16 @@ productApiRouter.post("/all/init_stripe", async (req: any, res) => { OrgService.getFromReq(req), ]); + console.log( + "fullProducts", + fullProducts.map((p) => p.id), + ); + const stripeCli = createStripeCli({ org, env, }); - const batchProductInit: Promise[] = []; const productBatchSize = 5; for (let i = 0; i < fullProducts.length; i += productBatchSize) { const batch = fullProducts.slice(i, i + productBatchSize); diff --git a/server/src/internal/customers/add-product/handleCreateCheckout.ts b/server/src/internal/customers/add-product/handleCreateCheckout.ts index ae701faaf..b39e832cf 100644 --- a/server/src/internal/customers/add-product/handleCreateCheckout.ts +++ b/server/src/internal/customers/add-product/handleCreateCheckout.ts @@ -1,17 +1,14 @@ -import { createStripeCli } from "@/external/stripe/utils.js"; - -import { pricesContainRecurring } from "@/internal/products/prices/priceUtils.js"; - -import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; import { AttachParams, AttachResultSchema, } from "../cusProducts/AttachParams.js"; +import { createStripeCli } from "@/external/stripe/utils.js"; +import { pricesContainRecurring } from "@/internal/products/prices/priceUtils.js"; +import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js"; import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; - import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; import { ErrCode } from "@/errors/errCodes.js"; -import RecaseError from "@/utils/errorUtils.js"; import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js"; import { APIVersion } from "@autumn/shared"; import { SuccessCode } from "@autumn/shared"; @@ -49,6 +46,7 @@ export const handleCreateCheckout = async ({ // Handle first item set const { items } = itemSets[0]; + attachParams.itemSets = itemSets; const isRecurring = pricesContainRecurring(attachParams.prices); diff --git a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.ts b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.ts index a8b56ce6c..4e78927bf 100644 --- a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.ts +++ b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.ts @@ -29,6 +29,7 @@ export const handleAddProduct = async ({ const defaultConfig: AttachConfig = getDefaultAttachConfig(); // 1. If paid product + if (prices.length > 0) { await handlePaidProduct({ req, diff --git a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFunction.ts b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFunction.ts index 8cb207f58..c7b1d25d4 100644 --- a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFunction.ts +++ b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFunction.ts @@ -1,3 +1,4 @@ +import Stripe from "stripe"; import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; import { cancelCurSubs } from "@/internal/customers/change-product/handleDowngrade/cancelCurSubs.js"; import { updateScheduledSubWithNewItems } from "@/internal/customers/change-product/scheduleUtils/updateScheduleWithNewItems.js"; @@ -13,9 +14,7 @@ import { isFreeProduct, } from "@/internal/products/productUtils.js"; import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js"; - import { cusProductsToSchedules } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; -import Stripe from "stripe"; import { attachParamToCusProducts } from "../../attachUtils/convertAttachParams.js"; import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js"; diff --git a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/scheduleStripeSub.ts b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/scheduleStripeSub.ts index f833629a3..a64f95523 100644 --- a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/scheduleStripeSub.ts +++ b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/scheduleStripeSub.ts @@ -71,10 +71,12 @@ export const scheduleStripeSub = async ({ export const updateOtherCusProdsWithNewSchedule = async ({ db, attachParams, + newSchedule, otherSub, }: { db: DrizzleCli; attachParams: AttachParams; + newSchedule: Stripe.SubscriptionSchedule; otherSub: Stripe.Subscription; }) => { const otherCusProducts = getCusProductsWithStripeSubId({ @@ -86,7 +88,7 @@ export const updateOtherCusProdsWithNewSchedule = async ({ for (const otherCusProduct of otherCusProducts) { let newScheduledIds = [ ...(otherCusProduct.scheduled_ids || []), - otherSub.id, + newSchedule.id, ]; await CusProductService.update({ @@ -138,6 +140,7 @@ export const handleNewScheduleForItemSet = async ({ await updateOtherCusProdsWithNewSchedule({ db, attachParams, + newSchedule: stripeSchedule, otherSub, }); diff --git a/server/src/internal/customers/attach/attachRouter.ts b/server/src/internal/customers/attach/attachRouter.ts index 67ad802a7..5481f7a57 100644 --- a/server/src/internal/customers/attach/attachRouter.ts +++ b/server/src/internal/customers/attach/attachRouter.ts @@ -198,10 +198,7 @@ export const createStripePrices = async ({ req: any; logger: any; }) => { - const { prices, entitlements, products, org, internalEntityId } = - attachParams; - - const stripeCli = createStripeCli({ org, env: attachParams.customer.env }); + const { prices, entitlements, products, org, stripeCli } = attachParams; const batchPriceUpdates = []; for (const price of prices) { diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts b/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts index fd6ef35b0..d14994780 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts @@ -56,31 +56,28 @@ export const webhookToAttachParams = ({ return params; }; -export const webhookToInsertParams = ({ +export const productToInsertParams = ({ req, - cusProduct, fullCus, + newProduct, entities, }: { req: ExtendedRequest; - cusProduct: FullCusProduct; fullCus: FullCustomer; + newProduct: FullProduct; entities?: Entity[]; }): InsertCusProductParams => { - const fullProduct = cusProductToProduct({ cusProduct }); - const params: InsertCusProductParams = { customer: fullCus, org: req.org, - product: fullProduct, - prices: cusProductToPrices({ cusProduct }), - entitlements: cusProductToEnts({ cusProduct }), + product: newProduct, + prices: newProduct.prices, + entitlements: newProduct.entitlements, features: req.features, - freeTrial: cusProduct.free_trial || null, - optionsList: cusProduct.options, - cusProducts: [cusProduct], - - internalEntityId: cusProduct.internal_entity_id || undefined, + cusProducts: fullCus.customer_products, + freeTrial: null, + optionsList: [], + internalEntityId: undefined, entities: entities || [], replaceables: [], }; diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts b/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts index 7188a2628..42cfb538d 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts @@ -28,6 +28,7 @@ import { getEntsWithFeature } from "@/internal/products/entitlements/entitlement import { isMainProduct } from "@/internal/products/productUtils/classifyProduct.js"; import { createStripeCli } from "@/external/stripe/utils.js"; import { getStripeCusData } from "./attachParamsUtils/getStripeCusData.js"; +import { isOneOff } from "@/internal/products/productUtils.js"; const getProductsForAttach = async ({ req, @@ -60,7 +61,8 @@ const getProductsForAttach = async ({ let otherProd = products.find( (p) => p.group === prod.group && !p.is_add_on && p.id !== prod.id, ); - if (otherProd && !otherProd.is_add_on) { + + if (otherProd && !otherProd.is_add_on && !isOneOff(prod.prices)) { throw new RecaseError({ message: "Can't attach multiple products from the same group that are not add-ons", @@ -145,8 +147,7 @@ const getPricesAndEnts = async ({ optionsInput, features, prices, - // to check if it fails for multi prod attach... - curCusProduct: prodIsMain ? curMainProduct : curSameProduct, + curCusProduct: curMainProduct, }), prices, entitlements, @@ -205,7 +206,7 @@ const getPricesAndEnts = async ({ optionsInput, features, prices, - curCusProduct: prodIsMain ? curMainProduct : curSameProduct, + curCusProduct: curMainProduct, }), prices, entitlements: getEntsWithFeature({ diff --git a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts index 7b1211701..5b8905470 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts @@ -229,21 +229,34 @@ const getChangeProductBranch = async ({ return AttachBranch.MainIsFree; } - if (isTrialing(curMainProduct!)) { - if (isFreeProduct(attachParams.prices)) { - return AttachBranch.Downgrade; - } - - return AttachBranch.MainIsTrial; - } - // 2. If main product is paid, check if upgrade or downgrade // Check if upgrade or downgrade let curPrices = cusProductToPrices({ cusProduct: curMainProduct! }); let newPrices = attachParams.prices; + // if (isTrialing(curMainProduct!)) { + // if (isFreeProduct(attachParams.prices)) { + // return AttachBranch.Downgrade; + // } + + // let isUpgrade = isProductUpgrade({ + // prices1: curPrices, + // prices2: newPrices, + // }); + + // if (!isUpgrade) { + // return AttachBranch.Downgrade; + // } + + // return AttachBranch.MainIsTrial; + // } + let isUpgrade = isProductUpgrade({ prices1: curPrices, prices2: newPrices }); if (isUpgrade) { + if (isTrialing(curMainProduct!)) { + return AttachBranch.MainIsTrial; + } + return AttachBranch.Upgrade; } diff --git a/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts b/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts index c5d343020..7e038e33b 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts @@ -8,6 +8,7 @@ import { ProrationBehavior } from "@autumn/shared"; import { attachParamsToProduct } from "./convertAttachParams.js"; import { attachParamToCusProducts } from "./convertAttachParams.js"; import { cusProductToPrices } from "../../cusProducts/cusProductUtils/convertCusProduct.js"; +import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js"; export const intervalsAreSame = ({ attachParams, @@ -80,15 +81,21 @@ export const getAttachConfig = async ({ let sameIntervals = intervalsAreSame({ attachParams }); + let disableMerge = + branch == AttachBranch.MainIsTrial || + org.config.merge_billing_cycles === false; + + const onlyCheckout = + isPublic || forceCheckout || (noPaymentMethod && !invoiceOnly && !isFree); + let config: AttachConfig = { branch, - onlyCheckout: - (isPublic || forceCheckout || noPaymentMethod) && !invoiceOnly && !isFree, + onlyCheckout, carryUsage, proration, disableTrial, invoiceOnly: flags.invoiceOnly, - disableMerge: org.config.merge_billing_cycles === false, + disableMerge, sameIntervals, carryTrial, }; diff --git a/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts b/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts index 4cf281cba..378205709 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts @@ -3,7 +3,7 @@ import { AttachParams, AttachResultSchema, } from "../../cusProducts/AttachParams.js"; -import { AttachBranch, AttachFunction } from "@autumn/shared"; +import { AttachBranch, AttachFunction, CusProductStatus } from "@autumn/shared"; import { handleUpgradeDiffInterval } from "../attachFunctions/upgradeDiffIntFlow/handleUpgradeDiffInt.js"; import { handleCreateCheckout } from "../../add-product/handleCreateCheckout.js"; import { handleAddProduct } from "../attachFunctions/addProductFlow/handleAddProduct.js"; @@ -16,6 +16,7 @@ import { attachParamToCusProducts } from "./convertAttachParams.js"; import { deleteCurrentScheduledProduct } from "./deleteCurrentScheduledProduct.js"; import { handleOneOffFunction } from "../attachFunctions/addProductFlow/handleOneOffFunction.js"; import { handleUpgradeSameInterval } from "../attachFunctions/upgradeSameIntFlow/handleUpgradeSameInt.js"; +import { CusProductService } from "../../cusProducts/CusProductService.js"; /* 1. If from new version, free trial should just carry over @@ -103,7 +104,7 @@ export const runAttachFunction = async ({ attachBody: AttachBody; config: AttachConfig; }) => { - const { logtail: logger } = req; + const { logtail: logger, db } = req; const { stripeCli } = attachParams; const attachFunction = await getAttachFunction({ @@ -160,8 +161,20 @@ export const runAttachFunction = async ({ // 2. If main is trial, cancel it... if (branch == AttachBranch.MainIsTrial) { + await CusProductService.update({ + db, + cusProductId: curMainProduct!.id, + updates: { + status: CusProductStatus.Expired, + }, + }); + for (const subId of curMainProduct?.subscription_ids || []) { - await stripeCli.subscriptions.cancel(subId); + await stripeCli.subscriptions.cancel(subId, { + cancellation_details: { + comment: "autumn_downgrade", + }, + }); } } diff --git a/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts b/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts index f1be47b9e..6b3f90bdf 100644 --- a/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts +++ b/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts @@ -2,12 +2,7 @@ import RecaseError from "@/utils/errorUtils.js"; import { ErrCode } from "@/errors/errCodes.js"; import { StatusCodes } from "http-status-codes"; import { AttachParams } from "../../cusProducts/AttachParams.js"; -import { - AttachBranch, - AttachErrCode, - entitlements, - UsagePriceConfig, -} from "@autumn/shared"; +import { AttachBranch, AttachErrCode, UsagePriceConfig } from "@autumn/shared"; import { AttachBody } from "../models/AttachBody.js"; import { AttachConfig, AttachFlags } from "../models/AttachFlags.js"; import { @@ -233,7 +228,7 @@ export const handleAttachErrors = async ({ useCheckout: onlyCheckout, }); - await handleUpdateQuantityErrors({ - attachParams, - }); + // await handleUpdateQuantityErrors({ + // attachParams, + // }); }; diff --git a/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts index 724dea747..6f82f5948 100644 --- a/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts +++ b/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts @@ -15,6 +15,7 @@ import { AttachBranch, BillingInterval, FreeTrial, + PreviewLineItem, Price, UsageModel, } from "@autumn/shared"; @@ -179,12 +180,23 @@ export const getUpgradeProductPreview = async ({ ); } + let dueToday: + | { + line_items: PreviewLineItem[]; + total: number; + } + | undefined = { + line_items: items, + total: dueTodayAmt, + }; + + if (branch == AttachBranch.SameCustomEnts) { + dueToday = undefined; + } + return { currency: attachParams.org.default_currency, - due_today: { - line_items: items, - total: dueTodayAmt, - }, + due_today: dueToday, due_next_cycle: { line_items: nextCycleItems, due_at: nextCycleAt.next_cycle_at, diff --git a/server/src/internal/customers/cusProducts/cusProductUtils.ts b/server/src/internal/customers/cusProducts/cusProductUtils.ts index 4ec1ad422..9144e6a5c 100644 --- a/server/src/internal/customers/cusProducts/cusProductUtils.ts +++ b/server/src/internal/customers/cusProducts/cusProductUtils.ts @@ -3,14 +3,11 @@ import { AppEnv, AttachScenario, CusProductResponseSchema, - CusProductSchema, CusProductStatus, Customer, Entity, FixedPriceConfig, FullCusProduct, - FullCustomerEntitlement, - FullCustomerPrice, Organization, PriceType, Subscription, @@ -31,7 +28,6 @@ import { getStripeSubs, subIsPrematurelyCanceled, } from "@/external/stripe/stripeSubUtils.js"; -import { sortCusEntsForDeduction } from "./cusEnts/cusEntUtils.js"; import { getRelatedCusEnt } from "./cusPrices/cusPriceUtils.js"; import { notNullish } from "@/utils/genUtils.js"; import { BREAK_API_VERSION } from "@/utils/constants.js"; @@ -39,7 +35,6 @@ import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/han import { DrizzleCli } from "@/db/initDrizzle.js"; import { getExistingCusProducts } from "./cusProductUtils/getExistingCusProducts.js"; import { ExtendedRequest } from "@/utils/models/Request.js"; -import { webhookToInsertParams } from "@/external/stripe/webhookUtils/webhookUtils.js"; export const isActiveStatus = (status: CusProductStatus) => { return ( diff --git a/server/src/internal/customers/cusProducts/cusProductUtils/findCusProduct.ts b/server/src/internal/customers/cusProducts/cusProductUtils/findCusProduct.ts new file mode 100644 index 000000000..ef238a7f5 --- /dev/null +++ b/server/src/internal/customers/cusProducts/cusProductUtils/findCusProduct.ts @@ -0,0 +1,22 @@ +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { CusProductStatus, FullCusProduct } from "@autumn/shared"; +import { CusProductService } from "../CusProductService.js"; + +export const findCusProductById = async ({ + db, + internalCustomerId, + productId, +}: { + db: DrizzleCli; + internalCustomerId: string; + productId: string; +}) => { + let cusProducts = await CusProductService.list({ + db, + internalCustomerId, + }); + + return cusProducts.find( + (cusProduct: FullCusProduct) => cusProduct.product.id === productId, + ); +}; diff --git a/server/src/internal/customers/cusUtils/createNewCustomer.ts b/server/src/internal/customers/cusUtils/createNewCustomer.ts index 5aae17d51..a9273d840 100644 --- a/server/src/internal/customers/cusUtils/createNewCustomer.ts +++ b/server/src/internal/customers/cusUtils/createNewCustomer.ts @@ -13,7 +13,6 @@ import { ErrCode, BillingInterval, AttachScenario, - AttachBranch, } from "@autumn/shared"; import { AppEnv, Customer } from "@autumn/shared"; import { createFullCusProduct } from "../add-product/createFullCusProduct.js"; @@ -21,8 +20,7 @@ import { handleAddProduct } from "../attach/attachFunctions/addProductFlow/handl import { CusService } from "../CusService.js"; import { initStripeCusAndProducts } from "../handlers/handleCreateCustomer.js"; import { generateId } from "@/utils/genUtils.js"; -import { newCusToFullCus } from "./cusUtils.js"; -import { webhookToInsertParams } from "@/external/stripe/webhookUtils/webhookUtils.js"; + import { newCusToAttachParams, newCusToInsertParams, diff --git a/server/src/internal/products/ProductService.ts b/server/src/internal/products/ProductService.ts index 67db0f705..d7f8a0f9a 100644 --- a/server/src/internal/products/ProductService.ts +++ b/server/src/internal/products/ProductService.ts @@ -323,7 +323,7 @@ export class ProductService { internalId: string; update: any; }) { - const data = await db + await db .update(products) .set(update) .where(eq(products.internal_id, internalId)); diff --git a/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts b/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts index a60d88c28..9c3e73f7a 100644 --- a/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts +++ b/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts @@ -227,8 +227,6 @@ export const handleNewProductItems = async ({ features, }); - console.log("Updated price", updatedPrice); - if (newPrice) { newPrices.push(newPrice); } diff --git a/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts b/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts index c33590d5f..b2047d329 100644 --- a/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts +++ b/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts @@ -220,7 +220,6 @@ export const toFeatureAndPrice = ({ on_increase: onIncrease, on_decrease: onDecrease, }; - console.log("Proration config", prorationConfig); } let price: Price = { diff --git a/server/src/internal/products/productUtils.ts b/server/src/internal/products/productUtils.ts index e229f4759..6a24ad5d8 100644 --- a/server/src/internal/products/productUtils.ts +++ b/server/src/internal/products/productUtils.ts @@ -239,6 +239,7 @@ export const checkStripeProductExists = async ({ let stripeProduct = await stripeCli.products.retrieve( product.processor!.id, ); + if (!stripeProduct.active) { createNew = true; } @@ -261,6 +262,10 @@ export const checkStripeProductExists = async ({ }, }); + console.log( + `Updated product ${product.name} with stripe product ${stripeProduct.id}`, + ); + product.processor = { id: stripeProduct.id, type: ProcessorType.Stripe, diff --git a/server/src/utils/scriptUtils/createTestProducts.ts b/server/src/utils/scriptUtils/createTestProducts.ts index c2839a222..f88bba9b1 100644 --- a/server/src/utils/scriptUtils/createTestProducts.ts +++ b/server/src/utils/scriptUtils/createTestProducts.ts @@ -61,13 +61,25 @@ export const initFeature = ({ } }; -// enum TestItemType { -// Prepaid = "prepaid", -// Arrear = "arrear", -// ArrearProrated = "arrear_prorated", -// FixedPrice = "fixed_price", -// } - +export const constructRawProduct = ({ + id, + isAddOn = false, + items, +}: { + id: string; + isAddOn?: boolean; + items: ProductItem[]; +}) => { + return { + id, + name: keyToTitle(id), + items, + is_add_on: isAddOn, + is_default: false, + version: 1, + group: "", + }; +}; export const constructProduct = ({ id, items, diff --git a/server/src/utils/scriptUtils/initCustomer.ts b/server/src/utils/scriptUtils/initCustomer.ts index 198f26f13..7b8a22c8a 100644 --- a/server/src/utils/scriptUtils/initCustomer.ts +++ b/server/src/utils/scriptUtils/initCustomer.ts @@ -90,12 +90,13 @@ export const initCustomer = async ({ })) as Customer; const stripeCli = createStripeCli({ org: org, env: env }); - // if (withTestClock) { - const testClock = await stripeCli.testHelpers.testClocks.create({ - frozen_time: Math.floor(Date.now() / 1000), - }); - testClockId = testClock.id; - // } + let testClockId = ""; + if (withTestClock) { + const testClock = await stripeCli.testHelpers.testClocks.create({ + frozen_time: Math.floor(Date.now() / 1000), + }); + testClockId = testClock.id; + } if (attachPm) { await attachPmToCus({ diff --git a/server/src/utils/scriptUtils/testClockUtils.ts b/server/src/utils/scriptUtils/testClockUtils.ts index e27726a7f..a2a073489 100644 --- a/server/src/utils/scriptUtils/testClockUtils.ts +++ b/server/src/utils/scriptUtils/testClockUtils.ts @@ -7,6 +7,7 @@ import { format, } from "date-fns"; import { Stripe } from "stripe"; +import { timeout } from "../genUtils.js"; export const getStripeNow = async ({ stripeCli, @@ -57,6 +58,7 @@ export const advanceTestClock = async ({ numberOfHours, numberOfMonths, advanceTo, + waitForSeconds, }: { stripeCli: Stripe; testClockId: string; @@ -66,6 +68,7 @@ export const advanceTestClock = async ({ numberOfHours?: number; numberOfMonths?: number; advanceTo?: number; + waitForSeconds?: number; }) => { if (!startingFrom) { startingFrom = new Date(); @@ -96,6 +99,10 @@ export const advanceTestClock = async ({ frozen_time: Math.floor(advanceTo / 1000), }); + if (waitForSeconds) { + await timeout(waitForSeconds * 1000); + } + return advanceTo; // await timeout( diff --git a/server/test.sh b/server/test.sh index aa9156860..d297cf094 100755 --- a/server/test.sh +++ b/server/test.sh @@ -2,25 +2,53 @@ MOCHA_SETUP="npx mocha tests/00_setup.ts" MOCHA_CMD="npx mocha --parallel --timeout 10000000 --ignore tests/00_setup.ts" -# TEST PARALLEL -if [ "$1" == "basic-parallel" ]; then - MOCHA_PARALLEL=true $MOCHA_SETUP && $MOCHA_CMD \ - tests/basic/*.ts \ - tests/basic/multi-feature/*.ts \ - tests/basic/entities/*.ts \ - # && $MOCHA_CMD \ - # 'tests/basic/referrals/*.ts' 'tests/attach/**/*.ts' \ - # 'tests/basic/referrals/*.ts' 'tests/attach/**/*.ts' \ - -elif [ "$1" == "advanced-parallel" ]; then - MOCHA_PARALLEL=true \ +# Group 1 +if [ "$1" == "group1" ]; then $MOCHA_SETUP \ - && $MOCHA_CMD 'tests/advanced/usage/*.ts' \ - && $MOCHA_CMD 'tests/advanced/arrear_prorated/*.ts' 'tests/advanced/coupons/*.ts'\ - # && $MOCHA_CMD 'tests/advanced/coupons/*.ts'\ + && $MOCHA_CMD \ + 'tests/attach/basic/*.ts' \ + 'tests/attach/upgrade/*.ts' \ + 'tests/attach/downgrade/*.ts' +fi + +# Group 2 +if [ "$1" == "g2" ]; then + $MOCHA_SETUP && $MOCHA_CMD \ + 'tests/attach/upgradeOld/*.ts' \ + 'tests/attach/entities/*.ts' \ + 'tests/attach/migrations/*.ts' \ + 'tests/attach/newVersion/*.ts' \ + 'tests/attach/others/*.ts' \ + 'tests/attach/updateEnts/*.ts' \ + exit 0 +fi + +if [ "$1" == "g3" ]; then + $MOCHA_SETUP \ + && $MOCHA_CMD 'tests/contUse/entities/*.ts' \ + && $MOCHA_CMD 'tests/contUse/update/*.ts'\ + && $MOCHA_CMD 'tests/contUse/track/*.ts' \ + exit 0 +fi + +if [ "$1" == "g4" ]; then + $MOCHA_SETUP && $MOCHA_CMD \ + 'tests/attach/updateQuantity/*.ts' \ + 'tests/attach/multiProduct/*.ts' \ + 'tests/advanced/multiFeature/*.ts' \ + 'tests/advanced/referrals/*.ts' \ + 'tests/advanced/coupons/*.ts' +fi + +# Group 4 +if [ "$1" == "g4" ]; then + $MOCHA_SETUP && $MOCHA_CMD \ + 'tests/advanced/usage/*.ts' +fi -elif [ "$1" == "alex-parallel" ]; then + +if [ "$1" == "alex-parallel" ]; then MOCHA_PARALLEL=true npx mocha 'tests/alex/00_setup.ts' && 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' \ @@ -54,3 +82,39 @@ fi + + + +# # TEST PARALLEL +# if [ "$1" == "basic-parallel" ]; then +# MOCHA_PARALLEL=true $MOCHA_SETUP \ +# && $MOCHA_CMD \ +# 'tests/attach/basic/*.ts' \ +# 'tests/attach/upgrade/*.ts' \ +# 'tests/attach/downgrade/*.ts' \ +# && $MOCHA_CMD \ +# 'tests/attach/upgradeOld/*.ts' \ +# 'tests/attach/entities/*.ts' \ +# 'tests/attach/migrations/*.ts' \ +# 'tests/attach/multiProduct/*.ts' \ +# 'tests/attach/newVersion/*.ts' \ +# 'tests/attach/others/*.ts' \ +# 'tests/attach/updateEnts/*.ts' \ +# 'tests/attach/updateQuantity/*.ts' \ +# 'tests/contUse/entities/*.ts' \ +# 'tests/contUse/track/*.ts' \ +# 'tests/contUse/update/*.ts' \ +# && $MOCHA_CMD \ +# 'tests/advanced/multiFeature/*.ts' \ +# 'tests/advanced/referrals/*.ts' \ +# 'tests/advanced/coupons/*.ts' \ + + +# elif [ "$1" == "advanced-parallel" ]; then +# MOCHA_PARALLEL=true \ +# $MOCHA_SETUP \ +# && $MOCHA_CMD 'tests/advanced/arrear_prorated/*.ts' 'tests/advanced/coupons/*.ts'\ +# # && $MOCHA_CMD 'tests/advanced/usage/*.ts' \ +# # && $MOCHA_CMD 'tests/advanced/coupons/*.ts'\ + + diff --git a/server/tests/advanced/coupons/coupon1.ts b/server/tests/advanced/coupons/coupon1.ts index 975283730..0001b222b 100644 --- a/server/tests/advanced/coupons/coupon1.ts +++ b/server/tests/advanced/coupons/coupon1.ts @@ -1,13 +1,12 @@ import chalk from "chalk"; import Stripe from "stripe"; -import { Customer } from "@autumn/shared"; +import { APIVersion, AppEnv, Customer, Organization } from "@autumn/shared"; import { createStripeCli } from "@/external/stripe/utils.js"; import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js"; import { getPriceForOverage } from "@/internal/products/prices/priceUtils.js"; import { expect } from "chai"; import { addHours, addMonths } from "date-fns"; import { features, products, rewards } from "tests/global.js"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; import { getFixedPriceAmount, timeout } from "tests/utils/genUtils.js"; import { AutumnCli } from "tests/cli/AutumnCli.js"; import { compareMainProduct } from "tests/utils/compare.js"; @@ -18,42 +17,161 @@ import { completeCheckoutForm, getDiscount, } from "tests/utils/stripeUtils.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { setupBefore } from "tests/before.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { + addPrefixToProducts, + getBasePrice, +} from "tests/utils/testProductUtils/testProductUtils.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; + +const testCase = "coupon1"; + +const pro = constructProduct({ + type: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], +}); + +const simulateOneCycle = async ({ + customerId, + db, + org, + env, + stripeCli, + autumn, + testClockId, + couponAmount, + curUnix, +}: { + customerId: string; + db: DrizzleCli; + org: Organization; + env: AppEnv; + stripeCli: Stripe; + autumn: AutumnInt; + testClockId: string; + couponAmount: number; + curUnix: number; +}) => { + const usage = Math.random() * 100000 + 10000; + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Words, + value: usage, + }); + + // Expected invoice total + let expectedTotal = await getExpectedInvoiceTotal({ + usage: [{ featureId: TestFeature.Words, value: usage }], + customerId, + productId: pro.id, + db, + org, + env, + stripeCli, + }); + + couponAmount -= expectedTotal; + + curUnix = await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addHours( + addMonths(curUnix, 1), + hoursToFinalizeInvoice, + ).getTime(), + waitForSeconds: 30, + }); + + const customer = await autumn.customers.get(customerId); + expect(customer.invoices![0].total).to.equal(0); + + const cusDiscount = await getDiscount({ + stripeCli: stripeCli, + stripeId: customer.stripe_id!, + }); + + expect(cusDiscount).to.exist; + + expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal( + rewards.rolloverAll.id, + ); + + expect(cusDiscount.coupon?.amount_off).to.equal( + Math.round(couponAmount * 100), + `Expected stripe cus to have coupon amount ${couponAmount * 100}`, + ); + + return { + couponAmount, + curUnix, + }; +}; describe( - chalk.yellow("coupon1 -- Testing one-off rollover, apply to all"), + chalk.yellow( + `${testCase} - Testing invoice credits reward, apply to all product`, + ), () => { let customerId = "coupon1"; let stripeCli: Stripe; let customer: Customer; let testClockId: string; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); let couponAmount = rewards.rolloverAll.discount_config.discount_value; + let curUnix = new Date().getTime(); before(async function () { - const { testClockId: testClockId1, customer: customer1 } = - await initCustomerWithTestClock({ - customerId, - org: this.org, - env: this.env, - db: this.db, - }); - testClockId = testClockId1; - customer = customer1; + await setupBefore(this); + db = this.db; + org = this.org; + env = this.env; + stripeCli = this.stripeCli; - stripeCli = createStripeCli({ - org: this.org, - env: this.env, + const res = await initCustomer({ + customerId, + org, + env, + db, + autumn: this.autumnJs, }); + + addPrefixToProducts({ + products: [pro], + prefix: testCase, + }); + + await createProducts({ + products: [pro], + orgId: org.id, + env, + db, + autumn, + }); + + testClockId = res.testClockId; + customer = res.customer; }); // CYCLE 0 - it("CYCLE 0: should attach pro with overage (through checkout)", async () => { - couponAmount -= getFixedPriceAmount(products.proWithOverage); - - const res = await AutumnCli.attach({ - customerId, - productId: products.proWithOverage.id, - forceCheckout: true, + it("should attach pro", async () => { + const res = await autumn.attach({ + customer_id: customerId, + product_id: pro.id, }); await completeCheckoutForm( @@ -62,117 +180,57 @@ describe( rewards.rolloverAll.id, ); - await timeout(20000); + await timeout(10000); - const cusRes = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.proWithOverage, - cusRes, - }); - }); + couponAmount -= getBasePrice({ product: pro }); - it("CYCLE 0: should have $0 invoice and correct remaining coupon amount", async () => { - const cusRes = await AutumnCli.getCustomer(customerId); - expect(cusRes.invoices[0].total).to.equal(0); + const customer = await autumn.customers.get(customerId); + expectProductAttached({ customer, product: pro }); + + expect(customer.invoices![0].total).to.equal(0); const cusDiscount = await getDiscount({ - stripeCli: stripeCli, - customer: cusRes.customer, + stripeCli, + stripeId: customer.stripe_id!, }); - try { - expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal( - rewards.rolloverAll.id, - ); + expect(cusDiscount).to.exist; + expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal( + rewards.rolloverAll.id, + ); + expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100); + }); - // Expect amount to be original amount - pro price - expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100); - } catch (error) { - console.error("--------------------------------"); - console.error( - "Expected stripe cus to have coupon", - rewards.rolloverAll, - ); - console.error("Actual stripe cus discount", cusDiscount); - throw error; - } + it("should run one cycle and have correct invoice + coupon amount", async () => { + const res = await simulateOneCycle({ + customerId, + db, + org, + env, + stripeCli, + autumn, + testClockId, + couponAmount, + curUnix: new Date().getTime(), + }); + + couponAmount = res.couponAmount; + curUnix = res.curUnix; }); // CYCLE 1 - it("CYCLE 1: should set usage to -100 and advance clock by 1 month", async () => { - const usage = 100; - const res = await AutumnCli.usage({ + it("should run another cycle and have correct invoice + coupon amount", async () => { + const res = await simulateOneCycle({ customerId, - featureId: features.metered1.id, - value: usage, - }); - - // Price - const price = getPriceForOverage( - products.proWithOverage.prices[1], - -(products.proWithOverage.entitlements.metered1.allowance! - usage), - ); - - couponAmount = - couponAmount - (price + getFixedPriceAmount(products.proWithOverage)); - - await advanceClockForInvoice({ + db, + org, + env, stripeCli, + autumn, testClockId, - waitForMeterUpdate: true, + couponAmount, + curUnix, }); }); - - it("CYCLE 1: should have $0 invoice and correct new coupon amount", async () => { - const cusRes = await AutumnCli.getCustomer(customerId); - expect(cusRes.invoices[0].total).to.equal(0); - - const cusDiscount = await getDiscount({ - stripeCli: stripeCli, - customer, - }); - - try { - expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal( - rewards.rolloverAll.id, - ); - expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100); - } catch (error) { - console.log("--------------------------------"); - console.log("coupon1, cycle 1 failed"); - console.log("Expected stripe cus to have coupon", rewards.rolloverAll); - console.log("Actual stripe cus discount", cusDiscount); - throw error; - } - }); - - // CYCLE 2 - it("CYCLE 2: should have $0 invoice and correct new coupon amount after 2nd cycle", async () => { - await timeout(20000); - let advanceTo = addHours(addMonths(new Date(), 2), 2); - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: advanceTo.getTime(), - }); - - const cusDiscount = await getDiscount({ - stripeCli: stripeCli, - customer, - }); - - const newCouponAmount = - couponAmount - getFixedPriceAmount(products.proWithOverage); - - try { - expect(cusDiscount.coupon?.amount_off).to.equal(newCouponAmount * 100); - } catch (error) { - console.log("--------------------------------"); - console.log("coupon1, cycle 2 failed"); - console.log("Expected coupon amount", newCouponAmount * 100); - console.log("Stripe cus discount", cusDiscount); - throw error; - } - }); }, ); diff --git a/server/tests/advanced/coupons/coupon2.ts b/server/tests/advanced/coupons/coupon2.ts index 4e81c8978..703f352bf 100644 --- a/server/tests/advanced/coupons/coupon2.ts +++ b/server/tests/advanced/coupons/coupon2.ts @@ -1,156 +1,206 @@ -import { createLogtailWithContext } from "@/external/logtail/logtailUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js"; -import { getPriceForOverage } from "@/internal/products/prices/priceUtils.js"; -import { Customer } from "@autumn/shared"; -import { expect } from "chai"; -import chalk from "chalk"; -import Stripe from "stripe"; -import { AutumnCli } from "tests/cli/AutumnCli.js"; -import { features, products, rewards } from "tests/global.js"; -import { compareMainProduct } from "tests/utils/compare.js"; -import { getFixedPriceAmount, timeout } from "tests/utils/genUtils.js"; import { advanceClockForInvoice, completeCheckoutForm, getDiscount, } from "tests/utils/stripeUtils.js"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; + +import chalk from "chalk"; +import Stripe from "stripe"; + +import { expect } from "chai"; +import { + APIVersion, + AppEnv, + CouponDurationType, + CreateReward, + Organization, + RewardType, +} from "@autumn/shared"; +import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js"; +import { getPriceForOverage } from "@/internal/products/prices/priceUtils.js"; + +import { timeout } from "tests/utils/genUtils.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { setupBefore } from "tests/before.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { + addPrefixToProducts, + getBasePrice, +} from "tests/utils/testProductUtils/testProductUtils.js"; +import { createProducts, createReward } from "tests/utils/productUtils.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { Decimal } from "decimal.js"; +import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; +import { addHours, addMonths } from "date-fns"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; + +const pro = constructProduct({ + type: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], +}); + +const testCase = "coupon2"; + +// Create reward input +const reward: CreateReward = { + id: "usage", + name: "usage", + promo_codes: [{ code: "usage" }], + type: RewardType.InvoiceCredits, + discount_config: { + discount_value: 10000, + duration_type: CouponDurationType.Forever, + duration_value: 1, + should_rollover: true, + apply_to_all: false, + price_ids: [], + }, +}; describe( - chalk.yellow("coupon2 -- Testing one-off rollover, apply to usage only"), + chalk.yellow(`${testCase} - Testing one-off rollover, apply to usage only`), () => { let logger: any; - let customerId = "coupon2"; + let customerId = testCase; let stripeCli: Stripe; - let customer: Customer; let testClockId: string; - let couponAmount = rewards.rolloverUsage.discount_config.discount_value; + let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + let org: Organization; + let env: AppEnv; + let db: DrizzleCli; + + let couponAmount = reward.discount_config!.discount_value; before(async function () { - const { testClockId: testClockId1, customer: customer1 } = - await initCustomerWithTestClock({ - customerId, - org: this.org, - env: this.env, - db: this.db, - }); - testClockId = testClockId1; - customer = customer1; + await setupBefore(this); - logger = createLogtailWithContext({ - test: "coupon2 -- Testing one-off rollover, apply to usage only", + org = this.org; + env = this.env; + db = this.db; + stripeCli = this.stripeCli; + + const { testClockId: testClockId1 } = await initCustomer({ customerId, - }); - stripeCli = createStripeCli({ org: this.org, env: this.env, + db: this.db, + autumn: this.autumnJs, + }); + + testClockId = testClockId1; + + addPrefixToProducts({ + products: [pro], + prefix: testCase, + }); + + await createProducts({ + orgId: this.org.id, + env: this.env, + db: this.db, + autumn, + products: [pro], + }); + + await createReward({ + orgId: org.id, + env, + db, + autumn, + reward, + productId: pro.id, + onlyUsage: true, }); }); // CYCLE 0 - it("should attach pro with overage (through checkout)", async () => { - const res = await AutumnCli.attach({ - customerId, - productId: products.proWithOverage.id, - forceCheckout: true, + it("should attach pro with promo code", async () => { + const res = await autumn.attach({ + customer_id: customerId, + product_id: pro.id, }); - await completeCheckoutForm( - res.checkout_url, - undefined, - rewards.rolloverUsage.id, - ); + await completeCheckoutForm(res.checkout_url, undefined, reward.id); await timeout(10000); - const cusRes = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.proWithOverage, - cusRes, + const customer = await autumn.customers.get(customerId); + expectProductAttached({ + customer, + product: pro, }); }); it("should have fixed price invoice and correct remaining coupon amount", async () => { - const cusRes = await AutumnCli.getCustomer(customerId); - const fixedPrice = getFixedPriceAmount(products.proWithOverage); - expect(cusRes.invoices[0].total).to.equal(fixedPrice); + const customer = await autumn.customers.get(customerId); + const fixedPrice = getBasePrice({ product: pro }); + expect(customer.invoices![0].total).to.equal(fixedPrice); const cusDiscount = await getDiscount({ - stripeCli: stripeCli, - customer: cusRes.customer, + stripeCli, + stripeId: customer.stripe_id!, }); - try { - expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal( - rewards.rolloverUsage.id, - ); - - // Expect amount to be original amount - pro price - expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100); - } catch (error) { - logger.error("--------------------------------"); - logger.error( - "Expected stripe cus to have coupon", - rewards.rolloverUsage, - ); - logger.error("Actual stripe cus discount", cusDiscount); - throw error; - } + expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(reward.id); + expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100); }); // CYCLE 1 - it("should set usage to -100 and advance clock by 1 month", async () => { - const usage = Math.min(Math.floor(Math.random() * 1000), 100); + it("should track usage and have correct invoice amount", async () => { + const usage = new Decimal(Math.random() * 1250120 + 10000) + .toDecimalPlaces(2) + .toNumber(); - const res = await AutumnCli.usage({ - customerId, - featureId: features.metered1.id, + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Words, value: usage, }); - // Price - const price = getPriceForOverage( - products.proWithOverage.prices[1], - -(products.proWithOverage.entitlements.metered1.allowance! - usage), - ); + let usageTotal = await getExpectedInvoiceTotal({ + org, + env, + db, + customerId, + productId: pro.id, + usage: [{ featureId: TestFeature.Words, value: usage }], + stripeCli, + onlyIncludeUsage: true, + }); - couponAmount = couponAmount - price; + let basePrice = getBasePrice({ product: pro }); - await advanceClockForInvoice({ + couponAmount = couponAmount - usageTotal; + + await advanceTestClock({ stripeCli, testClockId, - waitForMeterUpdate: true, + advanceTo: addHours( + addMonths(new Date(), 1), + hoursToFinalizeInvoice, + ).getTime(), + waitForSeconds: 20, }); - }); - it("should have $0 invoice and correct new coupon amount", async () => { - const cusRes = await AutumnCli.getCustomer(customerId); - expect(cusRes.invoices[0].total).to.equal( - getFixedPriceAmount(products.proWithOverage), - ); + const customer = await autumn.customers.get(customerId); + expect(customer.invoices![0].total).to.equal(basePrice); const cusDiscount = await getDiscount({ - stripeCli: stripeCli, - customer: cusRes.customer, + stripeCli, + stripeId: customer.stripe_id!, }); - try { - expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal( - rewards.rolloverUsage.id, - ); - expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100); - } catch (error) { - logger.error("--------------------------------"); - logger.error("coupon2, cycle 1 failed"); - logger.error( - "Expected stripe cus to have coupon", - rewards.rolloverUsage, - ); - logger.error("Actual stripe cus discount", cusDiscount); - throw error; - } + expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(reward.id); + + expect(cusDiscount.coupon?.amount_off).to.equal( + Math.round(couponAmount * 100), + ); }); }, ); diff --git a/server/tests/basic/multi-feature/multi_feature1.ts b/server/tests/advanced/multiFeature/multiFeature1.ts similarity index 80% rename from server/tests/basic/multi-feature/multi_feature1.ts rename to server/tests/advanced/multiFeature/multiFeature1.ts index 2428f6797..4e0c6f0a4 100644 --- a/server/tests/basic/multi-feature/multi_feature1.ts +++ b/server/tests/advanced/multiFeature/multiFeature1.ts @@ -1,23 +1,22 @@ import { expect } from "chai"; import chalk from "chalk"; -import { Autumn } from "@/external/autumn/autumnCli.js"; import { features } from "tests/global.js"; import { setupBefore } from "tests/before.js"; -import { initCustomer } from "tests/utils/init.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; import { AppEnv, BillingInterval, ProductItemFeatureType, UsageModel, } from "@autumn/shared"; -import { createProduct } from "tests/utils/productUtils.js"; +import { createProducts } from "tests/utils/productUtils.js"; import { getMainCusProduct } from "tests/utils/cusProductUtils/cusProductUtils.js"; import { getUsageCusEnt } from "tests/utils/cusProductUtils/cusEntSearchUtils.js"; import { getPrepaidCusEnt } from "tests/utils/cusProductUtils/cusEntSearchUtils.js"; import { constructFeaturePriceItem } from "@/internal/products/product-items/productItemUtils.js"; -import { SupabaseClient } from "@supabase/supabase-js"; import { timeout } from "@/utils/genUtils.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; // Scenario 1: prepaid + pay per use monthly -> prepaid + pay per use monthly let pro = { @@ -102,15 +101,15 @@ export const getPrepaidAndUsageCusEnts = async ({ return { prepaidCusEnt, usageCusEnt }; }; -// UNCOMMENT FROM HERE +const testCase = "multiFeature1"; describe(`${chalk.yellowBright( - "multi-feature/multi_feature1: Testing prepaid + pay per use -> prepaid + pay per use", + "multiFeature1: Testing prepaid + pay per use -> prepaid + pay per use", )}`, () => { - let autumn: Autumn; - let customerId = "multiFeature1Customer"; + let autumn: AutumnInt = new AutumnInt(); + let customerId = testCase; + let prepaidQuantity = 10; let prepaidAllowance = pro.items.prepaid.included_usage + prepaidQuantity; - let totalUsage = 0; let premiumPrepaidAllowance = @@ -127,30 +126,29 @@ describe(`${chalk.yellowBright( await setupBefore(this); await initCustomer({ + autumn: this.autumnJs, customerId, db: this.db, org: this.org, env: this.env, - attachPm: true, + attachPm: "success", }); autumn = this.autumn; - await createProduct({ + await createProducts({ autumn, - product: pro, - }); - - await createProduct({ - autumn, - product: premium, + products: [pro, premium], + db: this.db, + orgId: this.org.id, + env: this.env, }); }); it("should attach pro product to customer", async function () { await autumn.attach({ - customerId, - productId: pro.id, + customer_id: customerId, + product_id: pro.id, options: optionsList, }); @@ -172,10 +170,10 @@ describe(`${chalk.yellowBright( it("should use prepaid allowance first", async function () { let value = 60; - await autumn.events.send({ - customerId, + await autumn.track({ + customer_id: customerId, value, - featureId: features.metered1.id, + feature_id: features.metered1.id, }); totalUsage += value; @@ -196,15 +194,15 @@ describe(`${chalk.yellowBright( it("should have correct usage / invoice after upgrade", async function () { let value = 60; - await autumn.events.send({ - customerId, + await autumn.track({ + customer_id: customerId, value, - featureId: features.metered1.id, + feature_id: features.metered1.id, }); totalUsage += value; - await timeout(3000); + await timeout(5000); let { usageCusEnt } = await getPrepaidAndUsageCusEnts({ customerId, @@ -214,11 +212,9 @@ describe(`${chalk.yellowBright( featureId: features.metered1.id, }); - // totalUsage = totalUsage + value - (usageCusEnt?.balance ?? 0); - await autumn.attach({ - customerId, - productId: premium.id, + customer_id: customerId, + product_id: premium.id, options: optionsList, }); @@ -233,8 +229,6 @@ describe(`${chalk.yellowBright( // Check invoice too let { invoices } = await autumn.customers.get(customerId); - // 1. Let invoices[1] be 10 * premium prepaid price - pro prepaid price - // 2. Let invoices[0] be value * pro pay per use price let invoice1Amount = (premium.items.prepaid.price ?? 0) * prepaidQuantity - @@ -242,16 +236,12 @@ describe(`${chalk.yellowBright( let invoice0Amount = value * (pro.items.payPerUse.price ?? 0); - expect(invoices[1].total).to.equal(invoice1Amount); - expect(invoices[0].total).to.equal(invoice0Amount); + let totalAmount = invoice1Amount + invoice0Amount; - // console.log("Total usage", totalUsage); - // console.log("Premium prepaid allowance", premiumPrepaidAllowance); - // console.log("Value", value); + expect(invoices![0].total).to.equal(totalAmount); let leftover = premiumPrepaidAllowance - totalUsage + value; expect(prepaidCusEnt?.balance).to.equal(Math.max(0, leftover)); - expect(newUsageCusEnt?.balance).to.equal(0); }); }); diff --git a/server/tests/basic/multi-feature/multi_feature2.ts b/server/tests/advanced/multiFeature/multiFeature2.ts similarity index 80% rename from server/tests/basic/multi-feature/multi_feature2.ts rename to server/tests/advanced/multiFeature/multiFeature2.ts index bf4bc8c61..075a4b021 100644 --- a/server/tests/basic/multi-feature/multi_feature2.ts +++ b/server/tests/advanced/multiFeature/multiFeature2.ts @@ -1,9 +1,8 @@ import { expect } from "chai"; import chalk from "chalk"; -import { Autumn } from "@/external/autumn/autumnCli.js"; import { features } from "tests/global.js"; import { setupBefore } from "tests/before.js"; -import { initCustomer } from "tests/utils/init.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; import { AppEnv, BillingInterval, @@ -22,9 +21,10 @@ import { constructFeatureItem, constructFeaturePriceItem, } from "@/internal/products/product-items/productItemUtils.js"; -import { SupabaseClient } from "@supabase/supabase-js"; + import { timeout } from "@/utils/genUtils.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; // Scenario 1: prepaid + pay per use monthly -> prepaid + pay per use monthly let pro = { @@ -51,15 +51,6 @@ let premium = { id: "multiFeature2Premium", name: "Multi Feature 2 Premium", items: { - // // Prepaid - // prepaid: constructFeaturePriceItem({ - // feature_id: features.metered1.id, - // included_usage: 100, - // amount: 15, - // interval: BillingInterval.Month, - // usage_model: UsageModel.Prepaid, - // }), - // Pay per use payPerUse: constructFeaturePriceItem({ feature_id: features.metered1.id, @@ -86,7 +77,7 @@ export const getLifetimeAndUsageCusEnts = async ({ featureId: string; }) => { let mainCusProduct = await getMainCusProduct({ - customerId, + customerId: customerId, db, orgId, env, @@ -105,12 +96,12 @@ export const getLifetimeAndUsageCusEnts = async ({ return { lifetimeCusEnt, usageCusEnt }; }; -// UNCOMMENT FROM HERE +const testCase = "multiFeature2"; describe(`${chalk.yellowBright( - "multi-feature/multi_feature2: Testing lifetime + pay per use -> pay per use", + "multiFeature2: Testing lifetime + pay per use -> pay per use", )}`, () => { - let autumn: Autumn; - let customerId = "multiFeature2Customer"; + let autumn: AutumnInt = new AutumnInt(); + let customerId = testCase; let totalUsage = 0; @@ -118,11 +109,12 @@ describe(`${chalk.yellowBright( await setupBefore(this); await initCustomer({ + autumn: this.autumnJs, customerId, db: this.db, org: this.org, env: this.env, - attachPm: true, + attachPm: "success", }); autumn = this.autumn; @@ -130,18 +122,24 @@ describe(`${chalk.yellowBright( await createProduct({ autumn, product: pro, + db: this.db, + orgId: this.org.id, + env: this.env, }); await createProduct({ autumn, product: premium, + db: this.db, + orgId: this.org.id, + env: this.env, }); }); it("should attach pro product to customer", async function () { await autumn.attach({ - customerId, - productId: pro.id, + customer_id: customerId, + product_id: pro.id, }); let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ @@ -187,16 +185,17 @@ describe(`${chalk.yellowBright( it("should have correct usage after upgrade", async function () { let value = 20; - await autumn.events.send({ - customerId, + await autumn.track({ + customer_id: customerId, value, - featureId: features.metered1.id, + feature_id: features.metered1.id, }); + await timeout(3000); await autumn.attach({ - customerId, - productId: premium.id, + customer_id: customerId, + product_id: premium.id, }); let { lifetimeCusEnt, usageCusEnt: newUsageCusEnt } = @@ -208,18 +207,18 @@ describe(`${chalk.yellowBright( featureId: features.metered1.id, }); - // Check invoice too - let { invoices } = await autumn.customers.get(customerId); - // 1. Let invoices[1] be 10 * premium prepaid price - pro prepaid price - // 2. Let invoices[0] be value * pro pay per use price - - let invoice0Amount = value * (pro.items.payPerUse.price ?? 0); - expect(invoices[0].total).to.equal(invoice0Amount); - expect(lifetimeCusEnt).to.not.exist; - expect(newUsageCusEnt?.balance).to.equal( premium.items.payPerUse.included_usage, ); + + // Check invoice too + let res = await autumn.customers.get(customerId); + let invoices = res.invoices; + let invoice0Amount = value * (pro.items.payPerUse.price ?? 0); + expect(invoices![0].total).to.equal( + invoice0Amount, + "Invoice 0 should be 0", + ); }); }); diff --git a/server/tests/basic/multi-feature/multi_feature3.ts b/server/tests/advanced/multiFeature/multiFeature3.ts similarity index 89% rename from server/tests/basic/multi-feature/multi_feature3.ts rename to server/tests/advanced/multiFeature/multiFeature3.ts index 89b4d3ffe..33a85718c 100644 --- a/server/tests/basic/multi-feature/multi_feature3.ts +++ b/server/tests/advanced/multiFeature/multiFeature3.ts @@ -1,6 +1,6 @@ -import { expect } from "chai"; import chalk from "chalk"; -import { Autumn } from "@/external/autumn/autumnCli.js"; +import { expect } from "chai"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { features } from "tests/global.js"; import { setupBefore } from "tests/before.js"; @@ -11,7 +11,7 @@ import { ProductItemFeatureType, UsageModel, } from "@autumn/shared"; -import { createProduct } from "tests/utils/productUtils.js"; +import { createProducts } from "tests/utils/productUtils.js"; import { getMainCusProduct } from "tests/utils/cusProductUtils/cusProductUtils.js"; import { getLifetimeFreeCusEnt, @@ -22,11 +22,10 @@ import { constructFeatureItem, constructFeaturePriceItem, } from "@/internal/products/product-items/productItemUtils.js"; -import { SupabaseClient } from "@supabase/supabase-js"; import { timeout } from "@/utils/genUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; -import { addDays, addMonths } from "date-fns"; +import { addMonths } from "date-fns"; import { DrizzleCli } from "@/db/initDrizzle.js"; // Scenario 1: prepaid + pay per use monthly -> prepaid + pay per use monthly @@ -87,7 +86,7 @@ export const getLifetimeAndUsageCusEnts = async ({ describe(`${chalk.yellowBright( "multi-feature/multi_feature3: Testing lifetime + pay per use, advance test clock", )}`, () => { - let autumn: Autumn; + let autumn: AutumnInt = new AutumnInt(); let customerId = "multiFeature3Customer"; let totalUsage = 0; @@ -108,16 +107,19 @@ describe(`${chalk.yellowBright( autumn = this.autumn; - await createProduct({ + await createProducts({ autumn, - product: pro, + products: [pro], + db: this.db, + orgId: this.org.id, + env: this.env, }); }); it("should attach pro product to customer", async function () { await autumn.attach({ - customerId, - productId: pro.id, + customer_id: customerId, + product_id: pro.id, }); let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ @@ -138,10 +140,10 @@ describe(`${chalk.yellowBright( let value = pro.items.lifetime.included_usage as number; value += overageValue; - await autumn.events.send({ - customerId, + await autumn.track({ + customer_id: customerId, value, - featureId: features.metered1.id, + feature_id: features.metered1.id, }); totalUsage += value; diff --git a/server/tests/basic/referrals/referrals1.ts b/server/tests/advanced/referrals/referrals1.ts similarity index 96% rename from server/tests/basic/referrals/referrals1.ts rename to server/tests/advanced/referrals/referrals1.ts index 5f5e4d7b3..cff8a140b 100644 --- a/server/tests/basic/referrals/referrals1.ts +++ b/server/tests/advanced/referrals/referrals1.ts @@ -1,7 +1,7 @@ import { products, referralPrograms } from "../../global.js"; import { assert } from "chai"; import chalk from "chalk"; -import AutumnError, { Autumn } from "@/external/autumn/autumnCli.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; import { setupBefore } from "tests/before.js"; import { Customer, @@ -23,7 +23,7 @@ describe(`${chalk.yellowBright( let mainCustomerId = "main-referral-1"; let alternateCustomerId = "alternate-referral-1"; let redeemers = ["referral1-r1", "referral1-r2", "referral1-r3"]; - let autumn: Autumn; + let autumn: AutumnInt = new AutumnInt(); let stripeCli: Stripe; let testClockId: string; let referralCode: ReferralCode; @@ -33,7 +33,6 @@ describe(`${chalk.yellowBright( before(async function () { await setupBefore(this); - autumn = this.autumn; stripeCli = this.stripeCli; const { testClockId: testClockId1, customer } = @@ -48,8 +47,8 @@ describe(`${chalk.yellowBright( mainCustomer = customer; await autumn.attach({ - customerId: mainCustomerId, - productId: products.proWithTrial.id, + customer_id: mainCustomerId, + product_id: products.proWithTrial.id, }); let batchCreate = []; @@ -155,8 +154,8 @@ describe(`${chalk.yellowBright( let redeemer = redeemers[i]; await autumn.attach({ - customerId: redeemer, - productId: products.pro.id, + customer_id: redeemer, + product_id: products.pro.id, }); await timeout(3000); diff --git a/server/tests/basic/referrals/referrals2.ts b/server/tests/advanced/referrals/referrals2.ts similarity index 93% rename from server/tests/basic/referrals/referrals2.ts rename to server/tests/advanced/referrals/referrals2.ts index 8affbf803..93afa103c 100644 --- a/server/tests/basic/referrals/referrals2.ts +++ b/server/tests/advanced/referrals/referrals2.ts @@ -1,7 +1,7 @@ import { products, referralPrograms } from "../../global.js"; import { assert } from "chai"; import chalk from "chalk"; -import AutumnError, { Autumn } from "@/external/autumn/autumnCli.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; import { setupBefore } from "tests/before.js"; import { Customer, @@ -22,7 +22,7 @@ describe(`${chalk.yellowBright( )}`, () => { let mainCustomerId = "main-referral-2"; let redeemers = ["referral2-r1", "referral2-r2", "referral2-r3"]; - let autumn: Autumn; + let autumn: AutumnInt = new AutumnInt(); let stripeCli: Stripe; let testClockId: string; let referralCode: ReferralCode; @@ -32,7 +32,6 @@ describe(`${chalk.yellowBright( before(async function () { await setupBefore(this); - autumn = this.autumn; stripeCli = this.stripeCli; const { testClockId: testClockId1, customer } = @@ -118,8 +117,8 @@ describe(`${chalk.yellowBright( let curTime = new Date(); it("customer should have discount for first purchase", async function () { await autumn.attach({ - customerId: mainCustomerId, - productId: products.proWithTrial.id, + customer_id: mainCustomerId, + product_id: products.proWithTrial.id, }); await timeout(3000); @@ -134,8 +133,8 @@ describe(`${chalk.yellowBright( // 1. Get invoice let { invoices } = await autumn.customers.get(mainCustomerId); - assert.equal(invoices.length, 2); - assert.equal(invoices[0].total, 0); + assert.equal(invoices!.length, 2); + assert.equal(invoices![0].total, 0); }); it("customer should have discount for second purchase", async function () { @@ -165,7 +164,7 @@ describe(`${chalk.yellowBright( // // 3. Get invoice again let { invoices: invoices2 } = await autumn.customers.get(mainCustomerId); - assert.equal(invoices2.length, 3); - assert.equal(invoices2[0].total, 0); + assert.equal(invoices2!.length, 3); + assert.equal(invoices2![0].total, 0); }); }); diff --git a/server/tests/basic/referrals/referrals3.ts b/server/tests/advanced/referrals/referrals3.ts similarity index 93% rename from server/tests/basic/referrals/referrals3.ts rename to server/tests/advanced/referrals/referrals3.ts index 7903f6a1a..0e31ca541 100644 --- a/server/tests/basic/referrals/referrals3.ts +++ b/server/tests/advanced/referrals/referrals3.ts @@ -1,7 +1,7 @@ import { features, products, referralPrograms } from "../../global.js"; import { assert } from "chai"; import chalk from "chalk"; -import AutumnError, { Autumn } from "@/external/autumn/autumnCli.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; import { setupBefore } from "tests/before.js"; import { Customer, @@ -21,7 +21,7 @@ describe(`${chalk.yellowBright( )}`, () => { let mainCustomerId = "main-referral-3"; let redeemers = ["referral3-r1", "referral3-r2", "referral3-r3"]; - let autumn: Autumn; + let autumn: AutumnInt = new AutumnInt(); let stripeCli: Stripe; let testClockId: string; let referralCode: ReferralCode; @@ -46,8 +46,8 @@ describe(`${chalk.yellowBright( mainCustomer = customer; await autumn.attach({ - customerId: mainCustomerId, - productId: products.proWithTrial.id, + customer_id: mainCustomerId, + product_id: products.proWithTrial.id, }); let batchCreate = []; @@ -106,8 +106,8 @@ describe(`${chalk.yellowBright( let redeemer = redeemers[i]; await autumn.attach({ - customerId: redeemer, - productId: products.pro.id, + customer_id: redeemer, + product_id: products.pro.id, }); await timeout(3000); diff --git a/server/tests/basic/referrals/referrals4.ts b/server/tests/advanced/referrals/referrals4.ts similarity index 90% rename from server/tests/basic/referrals/referrals4.ts rename to server/tests/advanced/referrals/referrals4.ts index 0b4e41de5..51d31569e 100644 --- a/server/tests/basic/referrals/referrals4.ts +++ b/server/tests/advanced/referrals/referrals4.ts @@ -1,14 +1,8 @@ import { features, products, referralPrograms } from "../../global.js"; import { assert } from "chai"; import chalk from "chalk"; -import AutumnError, { Autumn } from "@/external/autumn/autumnCli.js"; import { setupBefore } from "tests/before.js"; -import { - Customer, - ErrCode, - ReferralCode, - RewardRedemption, -} from "@autumn/shared"; +import { Customer, ReferralCode, RewardRedemption } from "@autumn/shared"; import { timeout } from "tests/utils/genUtils.js"; import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; import { Stripe } from "stripe"; @@ -17,6 +11,7 @@ import { compareProductEntitlements } from "tests/utils/compare.js"; import { addDays, addHours } from "date-fns"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; // UNCOMMENT FROM HERE describe(`${chalk.yellowBright( @@ -26,7 +21,7 @@ describe(`${chalk.yellowBright( // let redeemers = ["referral4-r1", "referral4-r2"]; let redeemerId = "referral4-r1"; - let autumn: Autumn; + let autumn: AutumnInt = new AutumnInt(); let stripeCli: Stripe; let referralCode: ReferralCode; @@ -49,8 +44,8 @@ describe(`${chalk.yellowBright( }); await autumn.attach({ - customerId: mainCustomerId, - productId: products.proWithTrial.id, + customer_id: mainCustomerId, + product_id: products.proWithTrial.id, }); let { testClockId: testClockId1, customer } = @@ -85,8 +80,8 @@ describe(`${chalk.yellowBright( it("should not be triggered because of trial", async function () { await autumn.attach({ - customerId: redeemerId, - productId: products.proWithTrial.id, + customer_id: redeemerId, + product_id: products.proWithTrial.id, }); await timeout(3000); diff --git a/server/tests/advanced/usage/group_by.ts b/server/tests/advanced/usage/group_by.ts deleted file mode 100644 index 0900a13b1..000000000 --- a/server/tests/advanced/usage/group_by.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { expect } from "chai"; -import { initCustomer } from "../../utils/init.js"; -import { AutumnCli } from "../../cli/AutumnCli.js"; -import { - advanceProducts, - creditSystems, - features, - products, -} from "../../global.js"; -import { compareMainProduct } from "../../utils/compare.js"; -import { timeout } from "../../utils/genUtils.js"; -import { Decimal } from "decimal.js"; -import { sendGPUEvents } from "../../utils/advancedUsageUtils.js"; -import chalk from "chalk"; - -const PRECISION = 12; -describe.skip(`${chalk.yellowBright( - "Testing group by -- regular metered1 feature", -)}`, () => { - let customerId = "group-by-basic-metered"; - before(async function () { - await initCustomer({ - customer_data: { - id: customerId, - name: "Group by basic metered", - email: "group-by-basic-metered@example.com", - }, - attachPm: true, - db: this.db, - org: this.org, - env: this.env, - }); - }); - - it("should attach pro product to customer", async () => { - await AutumnCli.attach({ - customerId, - productId: products.pro.id, - }); - - const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.pro, - cusRes: res, - }); - }); - - it("should send events for three different groups", async () => { - // const users = ["null", "123", "abc"]; - const users = ["null", "123", "abc"]; - - let results: any = {}; - let numEventsPerUser = 5; - let groupProperty = features.metered1.config.group_by.property; - let multiplier = 1.5; - - for (const user of users) { - let batchEvents = []; - let totalValue = 0; - for (let i = 0; i < numEventsPerUser; i++) { - let randomVal = new Decimal(Math.random().toFixed(PRECISION)) - .mul(multiplier) - .toNumber(); - totalValue = new Decimal(totalValue).plus(randomVal).toNumber(); - - batchEvents.push( - AutumnCli.sendEvent({ - customerId, - eventName: features.metered1.eventName, - properties: { - [groupProperty]: user == "null" ? null : user, - value: randomVal, - }, - }), - ); - } - - await Promise.all(batchEvents); - await timeout(3000); - - results[user] = totalValue; - } - - let metered1Allowance = products.pro.entitlements.metered1.allowance!; - - for (const user of users) { - const { allowed, balanceObj }: any = await AutumnCli.entitled( - customerId, - features.metered1.id, - true, - user == "null" ? undefined : user, - ); - - let expectedBalance = new Decimal(metered1Allowance) - .minus(results[user]) - .toNumber(); - - if (expectedBalance < 1) { - expect(allowed).to.be.false; - } else { - expect(allowed).to.be.true; - } - - expect(balanceObj!.balance).to.equal(expectedBalance); - - // Check balance for GET /customers/:customerId - const res = await AutumnCli.getCustomer(customerId, { - [groupProperty]: user == "null" ? undefined : user, - }); - const entitlements = res.entitlements; - const metered1 = entitlements.find( - (e: any) => e.feature_id === features.metered1.id, - ); - expect(metered1.balance).to.equal(expectedBalance); - } - }); -}); - -// TO ADD SUPPORT FOR IN THE FUTURE -describe.skip(`${chalk.yellowBright( - "Testing group by -- advanced GPU usage", -)}`, () => { - let customerId = "group-by-advanced-gpu-usage-metered"; - before(async function () { - await initCustomer({ - customer_data: { - id: customerId, - name: "Group by advanced GPU usage", - email: "group-by-advanced-gpu-usage@example.com", - }, - attachPm: true, - db: this.db, - org: this.org, - env: this.env, - }); - }); - - it("should attach GPU system starter product to customer", async () => { - await AutumnCli.attach({ - customerId, - productId: advanceProducts.gpuSystemStarter.id, - }); - - const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: advanceProducts.gpuSystemStarter, - cusRes: res, - }); - }); - - it("should send events (advanced GPU usage) for three different groups", async () => { - const users = ["null", "123", "abc"]; - - let numEventsPerUser = 20; - let groupProperty = features.gpu1.config.group_by.property; - - const results: any = {}; - for (const user of users) { - let { creditsUsed } = await sendGPUEvents({ - customerId, - eventCount: numEventsPerUser, - groupObj: user == "null" ? undefined : { [groupProperty]: user }, - }); - results[user] = creditsUsed; - } - - let creditAllowance = - advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!; - - console.log("Results", results); - console.log("Starting allowance", creditAllowance); - - for (const user of users) { - const { allowed, balanceObj }: any = await AutumnCli.entitled( - customerId, - creditSystems.gpuCredits.id, - true, - user == "null" ? undefined : user, - ); - - let expectedCreditAllowance = new Decimal(creditAllowance) - .minus(results[user]) - .toNumber(); - - expect(balanceObj.balance).to.equal(expectedCreditAllowance); - } - }); -}); diff --git a/server/tests/advanced/usage/multi_interval1.ts b/server/tests/advanced/usage/multi_interval1.ts deleted file mode 100644 index 124c07e47..000000000 --- a/server/tests/advanced/usage/multi_interval1.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { Customer } from "@autumn/shared"; -import chalk from "chalk"; -import { initCustomerWithTestClock } from "../../utils/testInitUtils.js"; -import { compareMainProduct } from "../../utils/compare.js"; -import { AutumnCli } from "../../cli/AutumnCli.js"; -import { advanceProducts, creditSystems } from "../../global.js"; -import { timeout } from "../../utils/genUtils.js"; -import { assert, expect } from "chai"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { - advanceClockForInvoice, - advanceMonths, - checkBillingMeterEventSummary, - getUsageInArrearPrice, -} from "../../utils/stripeUtils.js"; -import { addMonths } from "date-fns"; -import { - sendGPUEvents, - checkCreditBalance, - checkUsageInvoiceAmount, -} from "../../utils/advancedUsageUtils.js"; - -// THIRD, TEST GPU PRO ANNUAL -describe(`${chalk.yellowBright("multi_interval1: GPU starter annual")}`, () => { - const customerId = "advancedUsageAnnual"; - let testClockId = ""; - let totalCreditsUsed = 0; - let customer: Customer; - before(async function () { - this.timeout(30000); - - let { testClockId: insertedTestClockId, customer: insertedCustomer } = - await initCustomerWithTestClock({ - customerId, - org: this.org, - env: this.env, - db: this.db, - }); - - testClockId = insertedTestClockId; - customer = insertedCustomer; - console.log("Testing multi interval 1"); - }); - - it("should attach GPU starter annual", async function () { - this.timeout(30000); - - const res = await AutumnCli.attach({ - customerId: customerId, - productId: advanceProducts.gpuStarterAnnual.id, - }); - - await timeout(3000); - }); - - it("should have GPU starter annual product", async function () { - this.timeout(30000); - - const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: advanceProducts.gpuStarterAnnual, - cusRes: res, - }); - - // Should have 2 invoices (one for annual, one for monthly) - expect(res!.invoices.length).to.equal(2); - }); - - it("should send 20 events and have correct balance", async function () { - this.timeout(30000); - - let eventCount = 20; - const { creditsUsed } = await sendGPUEvents({ - customerId, - eventCount, - }); - - totalCreditsUsed = creditsUsed; - console.log(" - Total credits used: ", totalCreditsUsed); - await checkCreditBalance({ - customerId, - featureId: creditSystems.gpuCredits.id, - totalCreditsUsed, - originalAllowance: - advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!, - }); - }); - - // Advance by a month and check if latest invoice is correct - it("should have invoice after a month and correct balance", async function () { - const stripeCli = createStripeCli({ org: this.org, env: this.env }); - await advanceClockForInvoice({ - stripeCli, - testClockId, - waitForMeterUpdate: true, - }); - - const res = await AutumnCli.getCustomer(customerId); - const invoices = res!.invoices; - - let invoiceIndex = invoices.findIndex((invoice: any) => - invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id), - ); - - await checkUsageInvoiceAmount({ - invoices, - totalUsage: totalCreditsUsed, - product: advanceProducts.gpuStarterAnnual, - featureId: creditSystems.gpuCredits.id, - invoiceIndex, - includeBase: false, - }); - - await checkCreditBalance({ - customerId, - featureId: creditSystems.gpuCredits.id, - totalCreditsUsed: 0, - originalAllowance: - advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!, - }); - }); - - // Advance by 1 year and check if latest invoice is correct - it.skip("should have correct invoice after 1 year", async function () { - const stripeCli = createStripeCli({ org: this.org, env: this.env }); - - // 1. Advance by 11 months - let numberOfMonths = 11; - await advanceMonths({ - stripeCli, - testClockId, - numberOfMonths, - }); - - // 2. Send 20 events - let eventCount = 20; - const { creditsUsed } = await sendGPUEvents({ - customerId, - eventCount, - }); - - let totalCreditsUsed = creditsUsed; - console.log(" - Total credits used: ", totalCreditsUsed); - - // Advance by a month and check for usage - await advanceClockForInvoice({ - stripeCli, - testClockId, - waitForMeterUpdate: true, - startingFrom: addMonths(new Date(), numberOfMonths), - }); - - const res = await AutumnCli.getCustomer(customerId); - const invoices = res!.invoices; - - let usagePrice = await getUsageInArrearPrice({ - org: this.org, - sb: this.sb, - env: this.env, - productId: advanceProducts.gpuStarterAnnual.id, - }); - - // Get billing meter event summary - let eventSummary = await checkBillingMeterEventSummary({ - stripeCli, - startTime: addMonths(new Date(), 11), - stripeMeterId: usagePrice?.config?.stripe_meter_id, - stripeCustomerId: customer.processor.id, - }); - - try { - assert.exists(eventSummary); - assert.equal( - eventSummary?.aggregated_value, - Math.round(totalCreditsUsed), - ); - assert.equal(invoices.length, 13 + 2); - } catch (error) { - console.group(); - console.log(" - Event summary: ", eventSummary); - console.log(" - Total credits used: ", totalCreditsUsed); - console.log(" - Last 3 invoices: ", invoices.slice(-3)); - console.groupEnd(); - throw error; - } - }); -}); diff --git a/server/tests/advanced/usage/multi_interval2.ts b/server/tests/advanced/usage/multi_interval2.ts deleted file mode 100644 index 53bfaaf61..000000000 --- a/server/tests/advanced/usage/multi_interval2.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { Customer } from "@autumn/shared"; -import chalk from "chalk"; -import { initCustomerWithTestClock } from "../../utils/testInitUtils.js"; -import { compareMainProduct } from "../../utils/compare.js"; -import { AutumnCli } from "../../cli/AutumnCli.js"; -import { advanceProducts, creditSystems } from "../../global.js"; -import { timeout } from "../../utils/genUtils.js"; -import { assert, expect } from "chai"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { - advanceClockForInvoice, - advanceMonths, - advanceTestClock, - checkBillingMeterEventSummary, - getUsageInArrearPrice, -} from "../../utils/stripeUtils.js"; -import { addMonths } from "date-fns"; -import { - sendGPUEvents, - checkUsageInvoiceAmount, -} from "../../utils/advancedUsageUtils.js"; -import { Decimal } from "decimal.js"; - -// FOURTH, TEST GPU STARTER ANNUAL UPGRADE TO GPU PRO -describe(`${chalk.yellowBright( - "multi_interval2: GPU starter annual upgrade to GPU pro annual", -)}`, () => { - const customerId = "multi_interval2"; - let testClockId = ""; - let totalCreditsUsed = 0; - let customer: Customer; - - let curTime = new Date(); - before(async function () { - let { testClockId: insertedTestClockId, customer: insertedCustomer } = - await initCustomerWithTestClock({ - customerId, - org: this.org, - env: this.env, - db: this.db, - }); - - testClockId = insertedTestClockId; - customer = insertedCustomer; - // console.log("MULTI INTERVAL 2, INITIALIZED CUSTOMER"); - }); - - it("should attach GPU starter annual", async function () { - const res = await AutumnCli.attach({ - customerId: customerId, - productId: advanceProducts.gpuStarterAnnual.id, - }); - - await timeout(5000); - }); - - it("should have GPU starter annual product", async function () { - const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: advanceProducts.gpuStarterAnnual, - cusRes: res, - }); - }); - - let numberOfMonths = 0; - it(`should advance ${numberOfMonths} months and upgrade to GPU pro monthly`, async function () { - const stripeCli = createStripeCli({ org: this.org, env: this.env }); - await advanceMonths({ - stripeCli, - testClockId, - numberOfMonths, - }); - - curTime = addMonths(curTime, numberOfMonths); - - // Send 20 events - let eventCount = 20; - const { creditsUsed } = await sendGPUEvents({ - customerId, - eventCount, - }); - - totalCreditsUsed = creditsUsed; - console.log(" - Total credits used: ", creditsUsed); - - await AutumnCli.attach({ - customerId: customerId, - productId: advanceProducts.gpuProAnnual.id, - }); - - await advanceTestClock({ - stripeCli, - testClockId, - numberOfDays: 10, - startingFrom: curTime, - }); - }); - - it("should have GPU pro annual product and 2 Stripe subscriptions", async function () { - const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: advanceProducts.gpuProAnnual, - cusRes: res, - }); - - // Should have 2 subscriptions - const stripeCli = createStripeCli({ org: this.org, env: this.env }); - const subs = await stripeCli.subscriptions.list({ - customer: customer.processor.id, - }); - - await timeout(5000); - - expect(subs.data.length).to.equal(2); - }); - - it("should have correct invoice for GPU starter annual (bill for remaining usages)", async function () { - const res = await AutumnCli.getCustomer(customerId); - const invoices = res!.invoices; - - let invoiceIndex = invoices.findIndex((invoice: any) => - invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id), - ); - - await checkUsageInvoiceAmount({ - invoices, - totalUsage: totalCreditsUsed, - product: advanceProducts.gpuStarterAnnual, - featureId: creditSystems.gpuCredits.id, - invoiceIndex, - includeBase: false, - }); - }); - - it("should send 20 events (on GPU pro annual)", async function () { - const stripeCli = createStripeCli({ org: this.org, env: this.env }); - // Send 20 events - let eventCount = 20; - const { creditsUsed } = await sendGPUEvents({ - customerId, - eventCount, - }); - - totalCreditsUsed = creditsUsed; - - console.log(" - Total credits used: ", totalCreditsUsed); - - await advanceClockForInvoice({ - stripeCli, - testClockId, - waitForMeterUpdate: true, - startingFrom: curTime, - }); - }); - - it("should have correct billing meter event summary for GPU pro annual", async function () { - const res = await AutumnCli.getCustomer(customerId); - const invoices = res!.invoices; - - // Think I have to use Stripe metered event summary to check this - let usagePrice = await getUsageInArrearPrice({ - org: this.org, - sb: this.sb, - env: this.env, - productId: advanceProducts.gpuProAnnual.id, - }); - - const stripeCli = createStripeCli({ org: this.org, env: this.env }); - // console.log(" - Usage price: ", usagePrice); - // console.log(" - Config: ", usagePrice?.config); - let eventSummary = await checkBillingMeterEventSummary({ - stripeCli, - startTime: curTime, // Wrong date? - stripeMeterId: usagePrice?.config?.stripe_meter_id, - stripeCustomerId: customer.processor.id, - }); - - let roundedFirst = Math.ceil( - new Decimal(totalCreditsUsed) - .div(usagePrice?.config?.billing_units!) - .toNumber(), - ); - let roundedTotalCreditsUsed = new Decimal(roundedFirst) - .mul(usagePrice?.config?.billing_units!) - .toNumber(); - try { - assert.exists(eventSummary); - assert.equal(eventSummary?.aggregated_value, roundedTotalCreditsUsed); - } catch (error) { - console.group(); - console.log(" - Event summary: ", eventSummary); - console.log(" - Total credits used: ", totalCreditsUsed); - console.groupEnd(); - throw error; - } - - // await checkUsageInvoiceAmount({ - // invoices, - // totalUsage: totalCreditsUsed, - // product: advanceProducts.gpuProAnnual, - // featureId: creditSystems.gpuCredits.id, - // invoiceIndex: 0, - // includeBase: false, - // }); - }); -}); diff --git a/server/tests/advanced/usage/usage1.ts b/server/tests/advanced/usage/usage1.ts index 3b6c5aecf..860d29a68 100644 --- a/server/tests/advanced/usage/usage1.ts +++ b/server/tests/advanced/usage/usage1.ts @@ -1,50 +1,50 @@ import { expect } from "chai"; +import { v1ProductToBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; import { AutumnCli } from "../../cli/AutumnCli.js"; import { features, products } from "../../global.js"; import { compareMainProduct } from "../../utils/compare.js"; -import { initCustomer } from "../../utils/init.js"; -import { - advanceClockForInvoice, - completeCheckoutForm, -} from "../../utils/stripeUtils.js"; +import { advanceClockForInvoice } from "../../utils/stripeUtils.js"; import { timeout } from "../../utils/genUtils.js"; -import { - calculateMetered1Price, - createStripeCli, -} from "@/external/stripe/utils.js"; +import { calculateMetered1Price } from "@/external/stripe/utils.js"; import chalk from "chalk"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; import { Customer } from "@autumn/shared"; -describe(`${chalk.yellowBright("usage1: Pro with overage")}`, () => { +import { setupBefore } from "tests/before.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import Stripe from "stripe"; + +const testCase = "usage1"; + +describe(`${chalk.yellowBright("usage1: Testing basic usage product")}`, () => { const NUM_EVENTS = 50; - const customerId = "usage1"; + const customerId = testCase; let testClockId: string; let customer: Customer; + let stripeCli: Stripe; + before(async function () { + await setupBefore(this); + stripeCli = this.stripeCli; + const { customer: customer_, testClockId: testClockId_ } = - await initCustomerWithTestClock({ + await initCustomer({ customerId, org: this.org, env: this.env, db: this.db, + autumn: this.autumnJs, + attachPm: "success", }); customer = customer_; testClockId = testClockId_; }); - it("usage1: should create a usage based entitlement", async function () { - const res = await AutumnCli.attach({ + it("should attach usage based product", async function () { + await AutumnCli.attach({ customerId: customerId, productId: products.proWithOverage.id, - forceCheckout: true, }); - await completeCheckoutForm(res.checkout_url); - await timeout(10000); - }); - - it("usage1: should have correct product", async function () { const res = await AutumnCli.getCustomer(customerId); compareMainProduct({ @@ -68,7 +68,7 @@ describe(`${chalk.yellowBright("usage1: Pro with overage")}`, () => { await timeout(10000); }); - it("usage1: should have correct metered1 balance after sending events", async function () { + it("should have correct metered1 balance after sending events", async function () { const res: any = await AutumnCli.entitled(customerId, features.metered1.id); expect(res!.allowed).to.be.true; @@ -80,23 +80,17 @@ describe(`${chalk.yellowBright("usage1: Pro with overage")}`, () => { const proOverageAmt = products.proWithOverage.entitlements.metered1.allowance; - try { - expect(res!.allowed).to.be.true; - expect(balance?.balance).to.equal(proOverageAmt! - NUM_EVENTS); - expect(balance?.usage_allowed).to.be.true; - } catch (error) { - console.group(); - console.log("Entitled res", res); - console.group(); - throw error; - } + expect(res!.allowed, "should be allowed").to.be.true; + + expect(balance?.balance, "should have correct metered1 balance").to.equal( + proOverageAmt! - NUM_EVENTS, + ); + + expect(balance?.usage_allowed, "should have usage_allowed").to.be.true; }); // Check invoice - it("usage1: advance stripe test clock and wait for event", async function () { - // this.timeout(1000 * 60 * 10); - - const stripeCli = createStripeCli({ org: this.org, env: this.env }); + it("should advance stripe test clock and wait for event", async function () { await advanceClockForInvoice({ stripeCli, testClockId, @@ -104,7 +98,7 @@ describe(`${chalk.yellowBright("usage1: Pro with overage")}`, () => { }); }); - it("usage1: should have correct invoice amount", async function () { + it("should have correct invoice amount", async function () { const cusRes = await AutumnCli.getCustomer(customerId); const invoices = cusRes!.invoices; @@ -115,22 +109,17 @@ describe(`${chalk.yellowBright("usage1: Pro with overage")}`, () => { metered1Feature: features.metered1, }); - try { - expect(invoices.length).to.equal(2); + expect(invoices.length).to.equal(2); - const invoice2 = invoices[0]; - expect(invoice2.total).to.equal( - price + products.proWithOverage.prices[0].config.amount, - ); - } catch (error) { - console.group(); - console.log( - "Expected invoices[0] to have total of: ", - price + products.proWithOverage.prices[0].config.amount, - ); - console.log("Invoices", invoices); - console.group(); - throw error; - } + const invoice2 = invoices[0]; + + const basePrice = v1ProductToBasePrice({ + prices: products.proWithOverage.prices, + }); + + expect(invoice2.total).to.equal( + price + basePrice, + "invoice total should be usage price + base price", + ); }); }); diff --git a/server/tests/advanced/usage/usage2.ts b/server/tests/advanced/usage/usage2.ts index cd2632223..545878ac6 100644 --- a/server/tests/advanced/usage/usage2.ts +++ b/server/tests/advanced/usage/usage2.ts @@ -2,7 +2,6 @@ import { assert, expect } from "chai"; import { AutumnCli } from "../../cli/AutumnCli.js"; import { advanceProducts, creditSystems, features } from "../../global.js"; import { compareMainProduct } from "../../utils/compare.js"; -import { initCustomer } from "../../utils/init.js"; import { advanceClockForInvoice } from "../../utils/stripeUtils.js"; import { timeout } from "../../utils/genUtils.js"; import { createStripeCli } from "@/external/stripe/utils.js"; @@ -14,11 +13,15 @@ import { } from "../../utils/advancedUsageUtils.js"; import chalk from "chalk"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; +import { setupBefore } from "tests/before.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import Stripe from "stripe"; // FIRST, REGULAR CHECK GPU STARTER MONTHLY -describe(`${chalk.yellowBright("usage2: GPU starter monthly")}`, () => { - const customerId = "usage2"; + +const testCase = "usage2"; +describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => { + const customerId = testCase; const PRECISION = 10; const ASSERT_INVOICE_AMOUNT = true; const CREDIT_MULTIPLIER = 100000; @@ -26,34 +29,30 @@ describe(`${chalk.yellowBright("usage2: GPU starter monthly")}`, () => { let testClockId = ""; let totalCreditsUsed = 0; + let stripeCli: Stripe; + before(async function () { - const { testClockId: createdTestClockId } = await initCustomerWithTestClock( - { - customerId, - org: this.org, - env: this.env, - db: this.db, - }, - ); + await setupBefore(this); + const { testClockId: createdTestClockId } = await initCustomer({ + customerId, + org: this.org, + env: this.env, + db: this.db, + autumn: this.autumnJs, + attachPm: "success", + }); testClockId = createdTestClockId; + + stripeCli = this.stripeCli; }); - it("usage2: should attach monthly starter", async function () { - this.timeout(30000); - - const res = await AutumnCli.attach({ + it("should attach gpu system starter", async function () { + await AutumnCli.attach({ customerId: customerId, productId: advanceProducts.gpuSystemStarter.id, }); - await timeout(3000); - console.log(" - Attached product"); - }); - - it("usage2: should have monthly starter product", async function () { - this.timeout(30000); - const res = await AutumnCli.getCustomer(customerId); compareMainProduct({ sent: advanceProducts.gpuSystemStarter, @@ -62,9 +61,7 @@ describe(`${chalk.yellowBright("usage2: GPU starter monthly")}`, () => { }); // Use up events - it("usage2: should have correct balance after events (up to 10 DP)", async function () { - this.timeout(30000); - + it("should send events and have correct balance (up to 10 DP)", async function () { let eventCount = 20; const batchEvents = []; @@ -107,28 +104,16 @@ describe(`${chalk.yellowBright("usage2: GPU starter monthly")}`, () => { let creditAllowance = advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!; - try { - expect(allowed).to.be.true; - expect(balanceObj!.balance).to.equal( - new Decimal(creditAllowance).minus(totalCreditsUsed).toNumber(), - ); - console.log(" - Total credits used: ", totalCreditsUsed); - console.log(" - Balance: ", balanceObj!.balance); - } catch (error) { - console.log("Total credits used: ", totalCreditsUsed); - console.log("Credit allowance: ", creditAllowance); - console.log("Expected balance: ", creditAllowance - totalCreditsUsed); - console.log("Received balance: ", balanceObj!.balance); - console.group(); - throw error; - } + expect(allowed).to.be.true; + expect(balanceObj!.balance).to.equal( + new Decimal(creditAllowance).minus(totalCreditsUsed).toNumber(), + ); + // console.log(" - Total credits used: ", totalCreditsUsed); + // console.log(" - Balance: ", balanceObj!.balance); }); // Check invoice.created event - it("usage2: should have correct invoice amount / updated meter balance", async function () { - this.timeout(100 * 60 * 1000); - - const stripeCli = createStripeCli({ org: this.org, env: this.env }); + it("should have correct invoice amount / updated meter balance", async function () { await advanceClockForInvoice({ stripeCli, testClockId, diff --git a/server/tests/advanced/usage/usage3.ts b/server/tests/advanced/usage/usage3.ts index 853a6ca0a..2cb52f549 100644 --- a/server/tests/advanced/usage/usage3.ts +++ b/server/tests/advanced/usage/usage3.ts @@ -1,6 +1,4 @@ import chalk from "chalk"; -import { addDays, addMonths, differenceInDays } from "date-fns"; -import { initCustomerWithTestClock } from "../../utils/testInitUtils.js"; import { advanceProducts, creditSystems } from "../../global.js"; import { AutumnCli } from "../../cli/AutumnCli.js"; import { @@ -17,63 +15,62 @@ import { assert, expect } from "chai"; import { Decimal } from "decimal.js"; import { compareMainProduct } from "../../utils/compare.js"; import { checkSubscriptionContainsProducts } from "tests/utils/scheduleCheckUtils.js"; -import { getPriceForOverage } from "@/internal/products/prices/priceUtils.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { setupBefore } from "tests/before.js"; +import Stripe from "stripe"; +import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js"; +import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; +import { getSubsFromCusId } from "tests/utils/expectUtils/expectSubUtils.js"; +const testCase = "usage3"; const ASSERT_INVOICE_AMOUNT = true; -// SECOND, UPGRADE TO GPU PRO MONTHLY describe(`${chalk.yellowBright( "usage3: upgrade from GPU starter monthly to GPU pro monthly", )}`, () => { const customerId = "usage3"; let testClockId = ""; let totalCreditsUsed = 0; - let daysAdd1 = 15; - // let daysAdd2 = differenceInDays( - // addMonths(new Date(), 1), - // addDays(new Date(), daysAdd1) - // ); + let stripeCli: Stripe; + let curUnix = 0; before(async function () { - let { testClockId: insertedTestClockId } = await initCustomerWithTestClock({ + await setupBefore(this); + let { testClockId: insertedTestClockId } = await initCustomer({ customerId, org: this.org, env: this.env, db: this.db, + autumn: this.autumnJs, + attachPm: "success", }); testClockId = insertedTestClockId; + stripeCli = this.stripeCli; }); // 1. Attach GPU starter monthly it("usage3: should attach GPU starter monthly", async function () { - const res = await AutumnCli.attach({ + await AutumnCli.attach({ customerId: customerId, productId: advanceProducts.gpuSystemStarter.id, }); - - await timeout(3000); }); // 2. Send 20 events it("usage3: should send 20 events", async function () { - this.timeout(30000); - // console.log(" Sending 20 events"); - let eventCount = 20; const { creditsUsed } = await sendGPUEvents({ customerId, eventCount, }); - console.log(" - Total credits used: ", creditsUsed); totalCreditsUsed = creditsUsed; }); // 3. Advance test clock by 15 days and upgrade - it("usage3: should advance test clock by 15 days and upgrade to GPU pro monthly", async function () { - const stripeCli = createStripeCli({ org: this.org, env: this.env }); - await advanceTestClock({ + it("should advance test clock by 15 days and upgrade to GPU pro monthly", async function () { + curUnix = await advanceTestClock({ stripeCli, testClockId, numberOfDays: 15, @@ -84,12 +81,7 @@ describe(`${chalk.yellowBright( productId: advanceProducts.gpuSystemPro.id, }); - await timeout(3000); - }); - - // 4. Check product attached - it("usage3: should have correct product attached (GPU pro monthly)", async function () { - this.timeout(30000); + // MAKE SURE STRIPE SUB ONLY HAS GPU PRO const res = await AutumnCli.getCustomer(customerId); compareMainProduct({ @@ -97,10 +89,7 @@ describe(`${chalk.yellowBright( cusRes: res, }); - // MAKE SURE STRIPE SUB ONLY HAS GPU PRO - const stripeCli = createStripeCli({ org: this.org, env: this.env }); let subscriptionId = res.products[0].subscription_ids![0]!; - await checkSubscriptionContainsProducts({ db: this.db, org: this.org, @@ -110,82 +99,48 @@ describe(`${chalk.yellowBright( }); }); - // 5. Check invoice for 15 days of starter usage - it("usage3: should have invoice for 15 days of starter usage", async function () { + // 4. Check invoice for 15 days of starter usage + it("should have invoice for 15 days of starter usage", async function () { const res = await AutumnCli.getCustomer(customerId); const invoices = res!.invoices; - let invoiceIndex = invoices.findIndex((invoice: any) => - invoice.product_ids.includes(advanceProducts.gpuSystemStarter.id), - ); + let basePrice1 = advanceProducts.gpuSystemStarter.prices[0].config.amount; + let basePrice2 = advanceProducts.gpuSystemPro.prices[0].config.amount; - // console.log("Total usage: ", totalCreditsUsed); - - await checkUsageInvoiceAmount({ - invoices, - totalUsage: totalCreditsUsed, - product: advanceProducts.gpuSystemStarter, - featureId: creditSystems.gpuCredits.id, - invoiceIndex, - includeBase: false, - }); - }); - - return; - - // 6. Advance another 15 days and check invoice for pro usage - it("usage3: should 20 send events (GPU pro monthly)", async function () { - let eventCount = 20; - const { creditsUsed } = await sendGPUEvents({ + let { subs } = await getSubsFromCusId({ + db: this.db, + org: this.org, + env: this.env, customerId, - eventCount, - }); - - totalCreditsUsed = creditsUsed; - console.log(" - Total credits used: ", totalCreditsUsed); - - // Check entitled - const { allowed, balanceObj }: any = await AutumnCli.entitled( - customerId, - creditSystems.gpuCredits.id, - true, - ); - - let proAllowance = - advanceProducts.gpuSystemPro.entitlements.gpuCredits.allowance!; - try { - assert.equal(allowed, true); - assert.equal( - balanceObj.balance, - new Decimal(proAllowance).minus(totalCreditsUsed).toNumber(), - ); - } catch (error) { - console.group(); - console.log(" - Total credits used: ", totalCreditsUsed); - console.log(" - Pro allowance: ", proAllowance); - console.log(" - Balance: ", balanceObj.balance); - console.groupEnd(); - throw error; - } - }); - - // 7. Advance another 15 days and check invoice for pro usage - it("usage3: should have invoice for 15 days of pro usage", async function () { - const stripeCli = createStripeCli({ org: this.org, env: this.env }); - await advanceClockForInvoice({ stripeCli, - testClockId, - waitForMeterUpdate: ASSERT_INVOICE_AMOUNT, + productId: advanceProducts.gpuSystemPro.id, }); - const res = await AutumnCli.getCustomer(customerId); - const invoices = res!.invoices; + let sub = subs[0]; - await checkUsageInvoiceAmount({ - invoices, - totalUsage: totalCreditsUsed, - product: advanceProducts.gpuSystemPro, - featureId: creditSystems.gpuCredits.id, + let baseDiff = calculateProrationAmount({ + periodStart: sub.current_period_start * 1000, + periodEnd: sub.current_period_end * 1000, + now: curUnix, + amount: basePrice2 - basePrice1, + allowNegative: true, }); + + let usagePrice = advanceProducts.gpuSystemStarter.prices[1]; + let overage = + totalCreditsUsed - + advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!; + + let overagePrice = priceToInvoiceAmount({ + price: usagePrice, + overage, + }); + + let calculatedTotal = new Decimal(baseDiff) + .plus(overagePrice) + .toDecimalPlaces(2) + .toNumber(); + + expect(invoices[0].total).to.equal(calculatedTotal); }); }); diff --git a/server/tests/advanced/usage/usage4.ts b/server/tests/advanced/usage/usage4.ts new file mode 100644 index 000000000..06dd0e215 --- /dev/null +++ b/server/tests/advanced/usage/usage4.ts @@ -0,0 +1,175 @@ +import { Customer } from "@autumn/shared"; +import chalk from "chalk"; +import { compareMainProduct } from "../../utils/compare.js"; +import { AutumnCli } from "../../cli/AutumnCli.js"; +import { advanceProducts, creditSystems } from "../../global.js"; + +import { expect } from "chai"; +import { advanceClockForInvoice } from "../../utils/stripeUtils.js"; + +import { + sendGPUEvents, + checkCreditBalance, + checkUsageInvoiceAmount, +} from "../../utils/advancedUsageUtils.js"; +import Stripe from "stripe"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { setupBefore } from "tests/before.js"; + +// THIRD, TEST GPU PRO ANNUAL + +const testCase = "usage4"; + +describe(`${chalk.yellowBright("usage4: GPU starter annual")}`, () => { + const customerId = testCase; + let totalCreditsUsed = 0; + + let testClockId = ""; + let customer: Customer; + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + let res = await initCustomer({ + customerId, + org: this.org, + env: this.env, + db: this.db, + autumn: this.autumnJs, + attachPm: "success", + }); + + testClockId = res.testClockId; + customer = res.customer; + stripeCli = this.stripeCli; + }); + + it("should attach GPU starter annual", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: advanceProducts.gpuStarterAnnual.id, + }); + + const res = await AutumnCli.getCustomer(customerId); + compareMainProduct({ + sent: advanceProducts.gpuStarterAnnual, + cusRes: res, + }); + + expect(res!.invoices.length).to.equal(2); + }); + + it("should send 20 events and have correct balance", async function () { + let eventCount = 20; + const { creditsUsed } = await sendGPUEvents({ + customerId, + eventCount, + }); + + totalCreditsUsed = creditsUsed; + await checkCreditBalance({ + customerId, + featureId: creditSystems.gpuCredits.id, + totalCreditsUsed, + originalAllowance: + advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!, + }); + }); + + it("should have invoice after a month and correct balance", async function () { + await advanceClockForInvoice({ + stripeCli, + testClockId, + waitForMeterUpdate: true, + }); + + const res = await AutumnCli.getCustomer(customerId); + const invoices = res!.invoices; + + let invoiceIndex = invoices.findIndex((invoice: any) => + invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id), + ); + + await checkUsageInvoiceAmount({ + invoices, + totalUsage: totalCreditsUsed, + product: advanceProducts.gpuStarterAnnual, + featureId: creditSystems.gpuCredits.id, + invoiceIndex, + includeBase: false, + }); + + await checkCreditBalance({ + customerId, + featureId: creditSystems.gpuCredits.id, + totalCreditsUsed: 0, + originalAllowance: + advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!, + }); + }); +}); + +// // Advance by 1 year and check if latest invoice is correct +// it.skip("should have correct invoice after 1 year", async function () { +// const stripeCli = createStripeCli({ org: this.org, env: this.env }); + +// // 1. Advance by 11 months +// let numberOfMonths = 11; +// await advanceMonths({ +// stripeCli, +// testClockId, +// numberOfMonths, +// }); + +// // 2. Send 20 events +// let eventCount = 20; +// const { creditsUsed } = await sendGPUEvents({ +// customerId, +// eventCount, +// }); + +// let totalCreditsUsed = creditsUsed; +// console.log(" - Total credits used: ", totalCreditsUsed); + +// // Advance by a month and check for usage +// await advanceClockForInvoice({ +// stripeCli, +// testClockId, +// waitForMeterUpdate: true, +// startingFrom: addMonths(new Date(), numberOfMonths), +// }); + +// const res = await AutumnCli.getCustomer(customerId); +// const invoices = res!.invoices; + +// let usagePrice = await getUsageInArrearPrice({ +// org: this.org, +// sb: this.sb, +// env: this.env, +// productId: advanceProducts.gpuStarterAnnual.id, +// }); + +// // Get billing meter event summary +// let eventSummary = await checkBillingMeterEventSummary({ +// stripeCli, +// startTime: addMonths(new Date(), 11), +// stripeMeterId: usagePrice?.config?.stripe_meter_id, +// stripeCustomerId: customer.processor.id, +// }); + +// try { +// assert.exists(eventSummary); +// assert.equal( +// eventSummary?.aggregated_value, +// Math.round(totalCreditsUsed), +// ); +// assert.equal(invoices.length, 13 + 2); +// } catch (error) { +// console.group(); +// console.log(" - Event summary: ", eventSummary); +// console.log(" - Total credits used: ", totalCreditsUsed); +// console.log(" - Last 3 invoices: ", invoices.slice(-3)); +// console.groupEnd(); +// throw error; +// } +// }); diff --git a/server/tests/alex/05_cancel.ts b/server/tests/alex/05_cancel.ts index a70636a51..5d9ecef61 100644 --- a/server/tests/alex/05_cancel.ts +++ b/server/tests/alex/05_cancel.ts @@ -173,35 +173,35 @@ describe(chalk.yellowBright("05_cancel"), () => { await timeout(5000); }); - // TODO: Edit so that it doesn't auto cancel for unit test org - it("should have expired / past_dued pro product", async function () { - const cusRes = await AutumnCli.getCustomer(customerId); + // // TODO: Edit so that it doesn't auto cancel for unit test org + // it("should have expired / past_dued pro product", async function () { + // const cusRes = await AutumnCli.getCustomer(customerId); - let org = this.org; + // let org = this.org; - console.log("Cancel on past due:", org.config.cancel_on_past_due); - if (org.config.cancel_on_past_due) { - const proProduct = getProductFromCusRes({ - cusRes, - productId: alexProducts.pro.id, - }); + // console.log("Cancel on past due:", org.config.cancel_on_past_due); + // if (org.config.cancel_on_past_due) { + // const proProduct = getProductFromCusRes({ + // cusRes, + // productId: alexProducts.pro.id, + // }); - expect(proProduct).to.not.exist; + // expect(proProduct).to.not.exist; - const freeProduct = getProductFromCusRes({ - cusRes, - productId: alexProducts.free.id, - }); + // const freeProduct = getProductFromCusRes({ + // cusRes, + // productId: alexProducts.free.id, + // }); - expect(freeProduct).to.exist; - expect(freeProduct.status).to.equal(CusProductStatus.Active); - } else { - compareMainProduct({ - sent: alexProducts.pro, - cusRes, - status: CusProductStatus.PastDue, - }); - } - }); + // expect(freeProduct).to.exist; + // expect(freeProduct.status).to.equal(CusProductStatus.Active); + // } else { + // compareMainProduct({ + // sent: alexProducts.pro, + // cusRes, + // status: CusProductStatus.PastDue, + // }); + // } + // }); }); }); diff --git a/server/tests/alex/07_team.ts b/server/tests/alex/07_team.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/server/tests/basic/01_product.ts b/server/tests/archives/01_product.ts similarity index 100% rename from server/tests/basic/01_product.ts rename to server/tests/archives/01_product.ts diff --git a/server/tests/archives/03_cancel.ts b/server/tests/archives/03_cancel.ts new file mode 100644 index 000000000..82d1c0fdb --- /dev/null +++ b/server/tests/archives/03_cancel.ts @@ -0,0 +1,210 @@ +// import { createStripeCli } from "@/external/stripe/utils.js"; +// import { AutumnCli } from "../cli/AutumnCli.js"; +// import { features, products } from "../global.js"; +// import { initCustomer } from "../utils/init.js"; +// import { timeout } from "../utils/genUtils.js"; +// import chalk from "chalk"; +// import { +// checkFeatureHasCorrectBalance, +// compareMainProduct, +// } from "../utils/compare.js"; +// import { expect } from "chai"; +// import { +// advanceTestClock, +// completeCheckoutForm, +// } from "../utils/stripeUtils.js"; +// import { CusProductStatus } from "@autumn/shared"; +// import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; +// import { addDays, addMonths } from "date-fns"; + +// describe(`${chalk.yellowBright( +// "03_cancel: Testing cancel (at period end and now)", +// )}`, () => { +// const customerId = "cancelCustomer"; + +// before(async function () { +// this.timeout(30000); + +// await initCustomer({ +// customer_data: { +// id: customerId, +// name: "Test Customer", +// email: "test@test.com", +// }, +// db: this.db, +// org: this.org, +// env: this.env, +// }); +// }); + +// it("should attach pro product", async function () { +// this.timeout(30000); + +// const res: any = await AutumnCli.attach({ +// customerId: customerId, +// productId: products.pro.id, +// }); + +// await completeCheckoutForm(res.checkout_url); +// await timeout(5000); +// console.log(` ${chalk.greenBright("Attached pro product")}`); +// }); + +// // 1. Cancel pro product +// it("should cancel pro product (at period end)", async function () { +// this.timeout(10000); + +// const stripeCli = createStripeCli({ org: this.org, env: this.env }); + +// // 1. Cancel pro product +// const cusRes: any = await AutumnCli.getCustomer(customerId); + +// const proProduct = cusRes.products.find( +// (p: any) => p.id === products.pro.id, +// ); + +// for (const subId of proProduct.subscription_ids) { +// await stripeCli.subscriptions.update(subId, { +// cancel_at_period_end: true, +// }); +// } + +// await timeout(3000); +// console.log(` ${chalk.greenBright("Cancelled pro product")}`); +// }); + +// it("should have pro product active, and canceled_at != null", async function () { +// this.timeout(10000); + +// const cusRes: any = await AutumnCli.getCustomer(customerId); +// compareMainProduct({ +// sent: products.pro, +// cusRes: cusRes, +// }); + +// const proProduct = cusRes.products.find( +// (p: any) => p.id === products.pro.id, +// ); +// expect(proProduct.canceled_at).to.not.equal(null); +// expect(proProduct.status).to.equal(CusProductStatus.Active); +// }); + +// // CANCEL SUB NOW, SUBSCRIPTION.DELETED WEBHOOK +// it("should cancel pro product (now)", async function () { +// this.timeout(10000); + +// const stripeCli = createStripeCli({ org: this.org, env: this.env }); + +// const cusRes: any = await AutumnCli.getCustomer(customerId); +// const proProduct = cusRes.products.find( +// (p: any) => p.id === products.pro.id, +// ); + +// for (const subId of proProduct.subscription_ids) { +// await stripeCli.subscriptions.cancel(subId); +// } + +// await timeout(3000); +// console.log(` ${chalk.greenBright("Cancelled pro product now")}`); +// }); + +// it("should have free product active, and pro product not returned", async function () { +// this.timeout(10000); + +// const cusRes: any = await AutumnCli.getCustomer(customerId); +// compareMainProduct({ +// sent: products.free, +// cusRes: cusRes, +// }); +// }); + +// it("should have correct entitlements (for free)", async function () { +// for (const entitlement of Object.values(products.free.entitlements)) { +// let feature = features[entitlement.feature_id!]; +// await checkFeatureHasCorrectBalance({ +// customerId, +// feature: feature, +// entitlement, +// expectedBalance: entitlement.allowance || 0, +// }); +// } +// }); +// }); + +// describe(`${chalk.yellowBright( +// "03_cancel: Testing subscription past_due", +// )}`, () => { +// const customerId = "03_cancel_past_due"; + +// before(async function () { +// this.timeout(30000); + +// const stripeCli = createStripeCli({ org: this.org, env: this.env }); + +// const testClock = await stripeCli.testHelpers.testClocks.create({ +// frozen_time: Math.round(Date.now() / 1000), +// }); + +// this.testClockId = testClock.id; + +// await initCustomer({ +// customer_data: { +// id: customerId, +// name: "Test Customer", +// email: "test@test.com", +// }, +// db: this.db, +// org: this.org, +// env: this.env, +// attachPm: true, +// testClockId: testClock.id, +// }); +// }); + +// it("should attach pro product", async function () { +// this.timeout(10000); + +// await AutumnCli.attach({ +// customerId: customerId, +// productId: products.pro.id, +// }); +// }); + +// it("should attach failed payment method and advance to next billing date", async function () { +// // 1. Swap customer's card +// const stripeCli = createStripeCli({ org: this.org, env: this.env }); + +// const cusRes: any = await AutumnCli.getCustomer(customerId); + +// await attachFailedPaymentMethod({ +// stripeCli, +// customer: cusRes.customer, +// }); + +// // const advanceDate = addDays(addMonths(new Date(), 1), 1); +// // await stripeCli.testHelpers.testClocks.advance(this.testClockId, { +// // frozen_time: Math.round(advanceDate.getTime() / 1000), +// // }); + +// await advanceTestClock({ +// stripeCli, +// testClockId: this.testClockId, +// advanceTo: addDays(addMonths(new Date(), 1), 1).getTime(), +// }); +// }); + +// it("should have free product active and correct entitlements", async function () { +// const cusRes: any = await AutumnCli.getCustomer(customerId); +// // compareMainProduct({ +// // sent: products.free, +// // cusRes: cusRes, +// // }); + +// // TODO: Check why this line messes up the test +// // compareProductEntitlements({ +// // customerId, +// // product: products.free, +// // features, +// // }); +// }); +// }); diff --git a/server/tests/basic/04_entitled.ts b/server/tests/archives/04_entitled.ts similarity index 100% rename from server/tests/basic/04_entitled.ts rename to server/tests/archives/04_entitled.ts diff --git a/server/tests/basic/08_pkey.ts b/server/tests/archives/08_pkey.ts similarity index 96% rename from server/tests/basic/08_pkey.ts rename to server/tests/archives/08_pkey.ts index 7854520f3..6274f65d2 100644 --- a/server/tests/basic/08_pkey.ts +++ b/server/tests/archives/08_pkey.ts @@ -9,7 +9,7 @@ import { compareMainProduct } from "../utils/compare.js"; import { AutumnCli } from "../cli/AutumnCli.js"; import chalk from "chalk"; -describe(`${chalk.yellowBright("Testing pkey")}`, () => { +describe(`${chalk.yellowBright("08_pkey: Testing publishable key")}`, () => { // 1. Initialize customer with card let customerId = "pkeyTestCustomer"; const bearerPublicAxios = getPublicAxiosInstance({ @@ -99,8 +99,6 @@ describe(`${chalk.yellowBright("Testing pkey")}`, () => { }); it("should return error if try to downgrade to free", async function () { - this.timeout(30000); - try { await bearerPublicAxios.post("/v1/attach", { customer_id: customerId, @@ -116,7 +114,6 @@ describe(`${chalk.yellowBright("Testing pkey")}`, () => { // Next, check entitled for pro it("should return correct metered1 amount for pro", async function () { - this.timeout(30000); const { data } = await bearerPublicAxios.post("/v1/entitled", { customer_id: customerId, feature_id: features.metered1.id, @@ -133,7 +130,6 @@ describe(`${chalk.yellowBright("Testing pkey")}`, () => { }); it("should return same balance for entitled with bearer and x-publishable-key", async function () { - this.timeout(30000); const { data } = await bearerPublicAxios.post("/v1/entitled", { customer_id: customerId, feature_id: features.metered1.id, @@ -150,7 +146,6 @@ describe(`${chalk.yellowBright("Testing pkey")}`, () => { }); it("should return error when try to send event", async function () { - this.timeout(30000); try { await bearerPublicAxios.post("/v1/events", { customer_id: customerId, diff --git a/server/tests/advanced/arrear_prorated/arrear_prorated2.ts b/server/tests/archives/arrear_prorated/arrear_prorated2.ts similarity index 100% rename from server/tests/advanced/arrear_prorated/arrear_prorated2.ts rename to server/tests/archives/arrear_prorated/arrear_prorated2.ts diff --git a/server/tests/advanced/arrear_prorated/arrear_prorated3.ts b/server/tests/archives/arrear_prorated/arrear_prorated3.ts similarity index 98% rename from server/tests/advanced/arrear_prorated/arrear_prorated3.ts rename to server/tests/archives/arrear_prorated/arrear_prorated3.ts index ec414d898..02bf78167 100644 --- a/server/tests/advanced/arrear_prorated/arrear_prorated3.ts +++ b/server/tests/archives/arrear_prorated/arrear_prorated3.ts @@ -275,7 +275,4 @@ describe(`${chalk.yellowBright( startingBalance: balance, }); }); - - // TODO: Test reset at for in arrear prorated with Ent Interval = Lifetime - // TODO: Test in arrear prorated for entitlements with billing units > 1 }); diff --git a/server/tests/archives/coupon1 copy.ts b/server/tests/archives/coupon1 copy.ts new file mode 100644 index 000000000..49ca13452 --- /dev/null +++ b/server/tests/archives/coupon1 copy.ts @@ -0,0 +1,188 @@ +import chalk from "chalk"; +import Stripe from "stripe"; +import { APIVersion, Customer } from "@autumn/shared"; +import { createStripeCli } from "@/external/stripe/utils.js"; +import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js"; +import { getPriceForOverage } from "@/internal/products/prices/priceUtils.js"; +import { expect } from "chai"; +import { addHours, addMonths } from "date-fns"; +import { features, products, rewards } from "tests/global.js"; +import { getFixedPriceAmount, timeout } from "tests/utils/genUtils.js"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { compareMainProduct } from "tests/utils/compare.js"; + +import { + advanceClockForInvoice, + advanceTestClock, + completeCheckoutForm, + getDiscount, +} from "tests/utils/stripeUtils.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { setupBefore } from "tests/before.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; + +const testCase = "coupon1"; + +describe( + chalk.yellow(`${testCase} -- Testing one-off rollover, apply to all`), + () => { + let customerId = "coupon1"; + let stripeCli: Stripe; + let customer: Customer; + let testClockId: string; + let db, org, env; + + let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + + let couponAmount = rewards.rolloverAll.discount_config.discount_value; + + before(async function () { + await setupBefore(this); + db = this.db; + org = this.org; + env = this.env; + autumn = this.autumnJs; + stripeCli = this.stripeCli; + + const res = await initCustomer({ + customerId, + org: this.org, + env: this.env, + db: this.db, + autumn: this.autumnJs, + }); + + testClockId = res.testClockId; + customer = res.customer; + }); + + // CYCLE 0 + it("CYCLE 0: should attach pro with overage (through checkout)", async () => { + couponAmount -= getFixedPriceAmount(products.proWithOverage); + + const res = await AutumnCli.attach({ + customerId, + productId: products.proWithOverage.id, + forceCheckout: true, + }); + + await completeCheckoutForm( + res.checkout_url, + undefined, + rewards.rolloverAll.id, + ); + + await timeout(20000); + + const cusRes = await AutumnCli.getCustomer(customerId); + compareMainProduct({ + sent: products.proWithOverage, + cusRes, + }); + }); + + it("CYCLE 0: should have $0 invoice and correct remaining coupon amount", async () => { + const cusRes = await AutumnCli.getCustomer(customerId); + expect(cusRes.invoices[0].total).to.equal(0); + + const cusDiscount = await getDiscount({ + stripeCli: stripeCli, + customer: cusRes.customer, + }); + + try { + expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal( + rewards.rolloverAll.id, + ); + + // Expect amount to be original amount - pro price + expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100); + } catch (error) { + console.error("--------------------------------"); + console.error( + "Expected stripe cus to have coupon", + rewards.rolloverAll, + ); + console.error("Actual stripe cus discount", cusDiscount); + throw error; + } + }); + + // CYCLE 1 + it("CYCLE 1: should set usage to -100 and advance clock by 1 month", async () => { + const usage = 100; + const res = await AutumnCli.usage({ + customerId, + featureId: features.metered1.id, + value: usage, + }); + + // Price + const price = getPriceForOverage( + products.proWithOverage.prices[1], + -(products.proWithOverage.entitlements.metered1.allowance! - usage), + ); + + couponAmount = + couponAmount - (price + getFixedPriceAmount(products.proWithOverage)); + + await advanceClockForInvoice({ + stripeCli, + testClockId, + waitForMeterUpdate: true, + }); + }); + + it("CYCLE 1: should have $0 invoice and correct new coupon amount", async () => { + const cusRes = await AutumnCli.getCustomer(customerId); + expect(cusRes.invoices[0].total).to.equal(0); + + const cusDiscount = await getDiscount({ + stripeCli: stripeCli, + customer, + }); + + try { + expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal( + rewards.rolloverAll.id, + ); + expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100); + } catch (error) { + console.log("--------------------------------"); + console.log("coupon1, cycle 1 failed"); + console.log("Expected stripe cus to have coupon", rewards.rolloverAll); + console.log("Actual stripe cus discount", cusDiscount); + throw error; + } + }); + + // CYCLE 2 + it("CYCLE 2: should have $0 invoice and correct new coupon amount after 2nd cycle", async () => { + await timeout(20000); + let advanceTo = addHours(addMonths(new Date(), 2), 2); + await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: advanceTo.getTime(), + }); + + const cusDiscount = await getDiscount({ + stripeCli: stripeCli, + customer, + }); + + const newCouponAmount = + couponAmount - getFixedPriceAmount(products.proWithOverage); + + try { + expect(cusDiscount.coupon?.amount_off).to.equal(newCouponAmount * 100); + } catch (error) { + console.log("--------------------------------"); + console.log("coupon1, cycle 2 failed"); + console.log("Expected coupon amount", newCouponAmount * 100); + console.log("Stripe cus discount", cusDiscount); + throw error; + } + }); + }, +); diff --git a/server/tests/archives/entities1.ts b/server/tests/archives/entities1.ts new file mode 100644 index 000000000..1ca584d09 --- /dev/null +++ b/server/tests/archives/entities1.ts @@ -0,0 +1,381 @@ +// import { Autumn } from "@/external/autumn/autumnCli.js"; +// import { setupBefore } from "tests/before.js"; +// import { CusProductStatus, organizations } from "@autumn/shared"; +// import { getFeaturePrice, getUsagePriceTiers } from "tests/utils/genUtils.js"; +// import { entityProducts, features } from "../global.js"; +// import { Stripe } from "stripe"; +// import { checkBalance } from "tests/utils/autumnUtils.js"; +// import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; +// import { advanceTestClock } from "tests/utils/stripeUtils.js"; +// import { addDays, addHours, addMonths } from "date-fns"; +// import { CacheManager } from "@/external/caching/CacheManager.js"; +// import { CacheType } from "@/external/caching/cacheActions.js"; +// import { hashApiKey } from "@/internal/dev/api-keys/apiKeyUtils.js"; +// import { compareMainProduct } from "../utils/compare.js"; +// import { assert, expect } from "chai"; + +// import chalk from "chalk"; +// import { DrizzleCli } from "@/db/initDrizzle.js"; +// import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +// import { eq } from "drizzle-orm"; + +// // Check balance and stripe quantity +// const checkEntAndStripeQuantity = async ({ +// db, +// autumn, +// stripeCli, +// featureId, +// customerId, +// expectedBalance, +// expectedUsage, +// expectedStripeQuantity, +// }: { +// db: DrizzleCli; +// autumn: Autumn; +// stripeCli: Stripe; +// featureId: string; +// customerId: string; +// expectedBalance: number; +// expectedUsage?: number; +// expectedStripeQuantity: number; +// }) => { +// let { customer, entitlements, products } = +// await autumn.customers.get(customerId); + +// let cusProducts = await CusProductService.list({ +// db, +// internalCustomerId: customer.internal_id, +// inStatuses: [CusProductStatus.Active], +// }); + +// let entitlement = entitlements.find((e: any) => e.feature_id == featureId); + +// expect(entitlement.balance).to.equal(expectedBalance); +// if (expectedUsage) { +// expect(entitlement.used).to.equal( +// expectedUsage, +// `Get customer ${customerId} returned incorrect "used" for feature ${featureId}`, +// ); +// } + +// if (products.length == 0) { +// assert.fail(`Get customer ${customerId} returned no products`); +// } + +// // 2. Get stripe quantity +// let mainProduct = products[0]; + +// if (mainProduct.subscription_ids.length == 0) { +// assert.fail(`Get customer ${customerId} returned no subscriptions`); +// } + +// let price = getFeaturePrice({ +// product: mainProduct, +// featureId: featureId, +// cusProducts, +// }); + +// if (!price) { +// assert.fail( +// `Get customer ${customerId} returned no price for feature ${featureId}`, +// ); +// } + +// let stripeSub = await stripeCli.subscriptions.retrieve( +// mainProduct.subscription_ids[0], +// ); +// let subItem = stripeSub.items.data.find( +// (item: any) => item.price.id == price.config!.stripe_price_id, +// ); + +// if (!subItem) { +// assert.fail( +// `Get customer ${customerId} returned no sub item for feature ${featureId}`, +// ); +// } + +// expect(subItem.quantity).to.equal( +// expectedStripeQuantity, +// `Get customer ${customerId} returned incorrect stripe quantity for feature ${featureId}`, +// ); +// }; + +// // UNCOMMENT FROM HERE +// describe(`${chalk.yellowBright("entities1: Testing entities")}`, () => { +// let customerId = "entity1"; +// let autumn: Autumn; +// let stripeCli: Stripe; +// let usageTiers = getUsagePriceTiers({ +// product: entityProducts.entityPro, +// featureId: features.seats.id, +// }); +// let testClockId: string; + +// before(async function () { +// await setupBefore(this); +// autumn = this.autumn; +// stripeCli = this.stripeCli; + +// const { testClockId: testClockId1 } = await initCustomerWithTestClock({ +// customerId, +// db: this.db, +// org: this.org, +// env: this.env, +// }); + +// testClockId = testClockId1; + +// // Update org config +// await this.db +// .update(organizations) +// .set({ +// config: { +// ...this.org.config, +// prorate_unused: false, +// }, +// }) +// .where(eq(organizations.id, this.org.id)); + +// await CacheManager.invalidate({ +// action: CacheType.SecretKey, +// value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!), +// }); +// await CacheManager.disconnect(); +// }); + +// let firstEntityId = "1"; + +// it("should attach entityFree product", async function () { +// await this.autumn.attach({ +// customerId, +// productId: entityProducts.entityFree.id, +// }); + +// await this.autumn.entities.create(customerId, { +// id: firstEntityId, +// name: "1@gmail.com", +// featureId: features.seats.id, +// }); + +// // Check if entity is created +// let res = await this.autumn.entities.list(customerId); +// let entities = res.data; + +// expect(entities).to.have.lengthOf(1); +// expect(entities[0].id).to.equal(firstEntityId); +// }); + +// it("should successfully remove created entity", async function () { +// await this.autumn.entities.delete(customerId, firstEntityId); + +// let res = await this.autumn.entities.list(customerId); +// let entities = res.data; +// expect(entities).to.have.lengthOf(0); +// }); + +// it("should create first entity, then attach entityPro product", async function () { +// await this.autumn.entities.create(customerId, { +// id: firstEntityId, +// name: "1@gmail.com", +// featureId: features.seats.id, +// }); + +// await this.autumn.attach({ +// customerId, +// productId: entityProducts.entityPro.id, +// }); + +// // Check product is attached correctly +// let cusRes = await autumn.customers.get(customerId); +// compareMainProduct({ +// sent: entityProducts.entityPro, +// cusRes: cusRes, +// }); + +// let { invoices } = cusRes; +// let usageTiers = getUsagePriceTiers({ +// product: entityProducts.entityPro, +// featureId: features.seats.id, +// }); + +// // Check invoice is created correctly +// expect(invoices).to.have.lengthOf(1); +// expect(invoices[0].total).to.equal(usageTiers[0].amount); + +// // Check balance and stripe quantity +// await checkEntAndStripeQuantity({ +// db: this.db, +// autumn, +// stripeCli, +// featureId: features.seats.id, +// customerId, +// expectedBalance: -1, +// expectedStripeQuantity: 1, +// }); + +// await checkBalance({ +// autumn, +// featureId: features.metered1.id, +// customerId, +// expectedBalance: +// entityProducts.entityPro.entitlements.metered1.allowance!, +// }); +// }); + +// let newEntities = [ +// { +// id: "2", +// name: "2@gmail.com", +// featureId: features.seats.id, +// }, +// { +// id: "3", +// name: "3@gmail.com", +// featureId: features.seats.id, +// }, +// { +// id: "4", +// name: "4@gmail.com", +// featureId: features.seats.id, +// }, +// ]; + +// it("should create 3 additional entities and be charged immediately", async function () { +// let advanceToDay = addDays(new Date(), 1).getTime(); +// await advanceTestClock({ +// stripeCli, +// testClockId, +// advanceTo: advanceToDay, +// waitForSeconds: 20, +// }); + +// await this.autumn.entities.create(customerId, newEntities); + +// let entitiesRes = await this.autumn.entities.list(customerId); +// let entities = entitiesRes.data; +// expect(entities).to.have.lengthOf(newEntities.length + 1); + +// let cusRes = await autumn.customers.get(customerId); + +// let { invoices } = cusRes; +// expect(invoices[0].total).to.equal( +// usageTiers[0].amount * newEntities.length, +// ); +// }); + +// it("should remove 2 entities, and have correct balance / stripe quantity", async function () { +// await this.autumn.entities.delete(customerId, newEntities[0].id); + +// await checkEntAndStripeQuantity({ +// db: this.db, +// autumn, +// stripeCli, +// featureId: features.seats.id, +// customerId, +// expectedBalance: -(newEntities.length + 1), +// expectedStripeQuantity: newEntities.length + 1 - 1, +// expectedUsage: newEntities.length + 1 - 1, +// }); + +// await this.autumn.entities.delete(customerId, newEntities[1].id); +// await checkEntAndStripeQuantity({ +// db: this.db, +// autumn, +// stripeCli, +// featureId: features.seats.id, +// customerId, +// expectedBalance: -(newEntities.length + 1), +// expectedStripeQuantity: newEntities.length + 1 - 2, +// expectedUsage: newEntities.length + 1 - 2, +// }); +// }); + +// let newEntities2 = [ +// { +// id: "5", +// name: "5@gmail.com", +// featureId: features.seats.id, +// }, +// { +// id: "6", +// name: "6@gmail.com", +// featureId: features.seats.id, +// }, +// { +// id: "7", +// name: "7@gmail.com", +// featureId: features.seats.id, +// }, +// ]; + +// it("should create three additional seats, and be charged for only one", async function () { +// await this.autumn.entities.create(customerId, newEntities2); + +// let totalSeats = newEntities2.length + 2; +// await checkEntAndStripeQuantity({ +// db: this.db, +// autumn, +// stripeCli, +// featureId: features.seats.id, +// customerId, +// expectedBalance: -totalSeats, +// expectedStripeQuantity: totalSeats, +// expectedUsage: totalSeats, +// }); + +// let cusRes = await autumn.customers.get(customerId); +// let { invoices } = cusRes; +// expect(invoices[0].total).to.equal(usageTiers[0].amount); +// }); + +// // return; + +// it("should remove one entity, and have correct balance / stripe quantity after advancing test clock", async function () { +// await this.autumn.entities.delete(customerId, newEntities2[0].id); + +// let totalSeats = newEntities2.length + 1; + +// let advanceTo = addHours(addMonths(new Date(), 1), 4).getTime(); +// await advanceTestClock({ stripeCli, testClockId, advanceTo }); + +// // Get entities +// let { data: entities } = await this.autumn.entities.list(customerId); +// expect(entities).to.have.lengthOf(totalSeats); + +// await checkEntAndStripeQuantity({ +// db: this.db, +// autumn, +// stripeCli, +// featureId: features.seats.id, +// customerId, +// expectedBalance: -totalSeats, +// expectedStripeQuantity: totalSeats, +// expectedUsage: totalSeats, +// }); + +// await checkBalance({ +// autumn, +// featureId: features.metered1.id, +// customerId, +// expectedBalance: +// totalSeats * entityProducts.entityPro.entitlements.metered1.allowance!, +// }); +// }); + +// after(async function () { +// await this.db +// .update(organizations) +// .set({ +// config: { +// ...this.org.config, +// prorate_unused: true, +// }, +// }) +// .where(eq(organizations.id, this.org.id)); + +// void CacheManager.invalidate({ +// action: CacheType.SecretKey, +// value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!), +// }); +// }); +// }); diff --git a/server/tests/archives/multi_interval2.ts b/server/tests/archives/multi_interval2.ts new file mode 100644 index 000000000..4243588c4 --- /dev/null +++ b/server/tests/archives/multi_interval2.ts @@ -0,0 +1,199 @@ +// THIS TEST CASE IS COVERED UNDER UPGRADE2.TS + +// import { Customer } from "@autumn/shared"; +// import chalk from "chalk"; +// import { compareMainProduct } from "../../utils/compare.js"; +// import { AutumnCli } from "../../cli/AutumnCli.js"; +// import { advanceProducts, creditSystems } from "../../global.js"; +// import { timeout } from "../../utils/genUtils.js"; +// import { assert, expect } from "chai"; +// import { createStripeCli } from "@/external/stripe/utils.js"; +// import { +// advanceClockForInvoice, +// advanceMonths, +// advanceTestClock, +// checkBillingMeterEventSummary, +// getUsageInArrearPrice, +// } from "../../utils/stripeUtils.js"; +// import { addMonths } from "date-fns"; +// import { +// sendGPUEvents, +// checkUsageInvoiceAmount, +// } from "../../utils/advancedUsageUtils.js"; +// import { Decimal } from "decimal.js"; +// import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +// import { setupBefore } from "tests/before.js"; +// import Stripe from "stripe"; + +// // FOURTH, TEST GPU STARTER ANNUAL UPGRADE TO GPU PRO + +// const testCase = "usage5"; +// describe(`${chalk.yellowBright("usage5: multi interval upgrade, GPU starter annual -> GPU pro annual")}`, () => { +// const customerId = testCase; +// let testClockId = ""; +// let totalCreditsUsed = 0; +// let customer: Customer; +// let stripeCli: Stripe; + +// let curTime = new Date(); +// before(async function () { +// await setupBefore(this); +// let res = await initCustomer({ +// customerId, +// org: this.org, +// env: this.env, +// db: this.db, +// autumn: this.autumnJs, +// attachPm: "success", +// }); + +// testClockId = res.testClockId; +// customer = res.customer; +// stripeCli = this.stripeCli; +// }); + +// it("should attach GPU starter annual", async function () { +// await AutumnCli.attach({ +// customerId: customerId, +// productId: advanceProducts.gpuStarterAnnual.id, +// }); + +// const res = await AutumnCli.getCustomer(customerId); +// compareMainProduct({ +// sent: advanceProducts.gpuStarterAnnual, +// cusRes: res, +// }); +// }); + +// it(`should advance 1 month and upgrade to GPU pro monthly`, async function () { +// let numberOfMonths = 1; +// await advanceMonths({ +// stripeCli, +// testClockId, +// numberOfMonths, +// }); + +// curTime = addMonths(curTime, numberOfMonths); + +// // Send 20 events +// let eventCount = 20; +// const { creditsUsed } = await sendGPUEvents({ +// customerId, +// eventCount, +// }); + +// totalCreditsUsed = creditsUsed; + +// await AutumnCli.attach({ +// customerId: customerId, +// productId: advanceProducts.gpuProAnnual.id, +// }); + +// await advanceTestClock({ +// stripeCli, +// testClockId, +// numberOfDays: 10, +// startingFrom: curTime, +// }); +// }); + +// it("should have GPU pro annual product and 2 Stripe subscriptions", async function () { +// const res = await AutumnCli.getCustomer(customerId); +// compareMainProduct({ +// sent: advanceProducts.gpuProAnnual, +// cusRes: res, +// }); + +// // Should have 2 subscriptions +// const subs = await stripeCli.subscriptions.list({ +// customer: customer.processor.id, +// }); + +// expect(subs.data.length).to.equal(2); +// }); + +// it("should have correct invoice for GPU starter annual (bill for remaining usages)", async function () { +// const res = await AutumnCli.getCustomer(customerId); +// const invoices = res!.invoices; + +// let invoiceIndex = invoices.findIndex((invoice: any) => +// invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id), +// ); + +// await checkUsageInvoiceAmount({ +// invoices, +// totalUsage: totalCreditsUsed, +// product: advanceProducts.gpuStarterAnnual, +// featureId: creditSystems.gpuCredits.id, +// invoiceIndex, +// includeBase: false, +// }); +// }); + +// it("should send 20 events (on GPU pro annual)", async function () { +// const stripeCli = createStripeCli({ org: this.org, env: this.env }); +// // Send 20 events +// let eventCount = 20; +// const { creditsUsed } = await sendGPUEvents({ +// customerId, +// eventCount, +// }); + +// totalCreditsUsed = creditsUsed; + +// await advanceClockForInvoice({ +// stripeCli, +// testClockId, +// waitForMeterUpdate: true, +// startingFrom: curTime, +// }); +// }); + +// it("should have correct billing meter event summary for GPU pro annual", async function () { +// const res = await AutumnCli.getCustomer(customerId); +// const invoices = res!.invoices; + +// // Think I have to use Stripe metered event summary to check this +// let usagePrice = await getUsageInArrearPrice({ +// org: this.org, +// sb: this.sb, +// env: this.env, +// productId: advanceProducts.gpuProAnnual.id, +// }); + +// let eventSummary = await checkBillingMeterEventSummary({ +// stripeCli, +// startTime: curTime, // Wrong date? +// stripeMeterId: usagePrice?.config?.stripe_meter_id, +// stripeCustomerId: customer.processor.id, +// }); + +// let roundedFirst = Math.ceil( +// new Decimal(totalCreditsUsed) +// .div(usagePrice?.config?.billing_units!) +// .toNumber(), +// ); +// let roundedTotalCreditsUsed = new Decimal(roundedFirst) +// .mul(usagePrice?.config?.billing_units!) +// .toNumber(); +// try { +// assert.exists(eventSummary); +// assert.equal(eventSummary?.aggregated_value, roundedTotalCreditsUsed); +// } catch (error) { +// console.group(); +// console.log(" - Event summary: ", eventSummary); +// console.log(" - Total credits used: ", totalCreditsUsed); +// console.groupEnd(); +// throw error; +// } + +// // await checkUsageInvoiceAmount({ +// // invoices, +// // totalUsage: totalCreditsUsed, +// // product: advanceProducts.gpuProAnnual, +// // featureId: creditSystems.gpuCredits.id, +// // invoiceIndex: 0, +// // includeBase: false, +// // }); +// }); +// }); diff --git a/server/tests/attach/basic/basic1.ts b/server/tests/attach/basic/basic1.ts new file mode 100644 index 000000000..51ec532df --- /dev/null +++ b/server/tests/attach/basic/basic1.ts @@ -0,0 +1,332 @@ +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { assert, expect } from "chai"; +import chalk from "chalk"; +import { setupBefore } from "tests/before.js"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { features, products } from "tests/global.js"; +import { compareMainProduct } from "tests/utils/compare.js"; +import { completeCheckoutForm } from "tests/utils/stripeUtils.js"; +import { timeout } from "tests/utils/genUtils.js"; + +const oneTimeQuantity = 2; +const oneTimePurchaseCount = 2; +const oneTimeOverrideQuantity = 4; +const monthlyQuantity = 2; + +// UNCOMMENT FROM HERE +const testCase = "basic1"; +describe(`${chalk.yellowBright( + "basic1: Testing attach -- free, pro & one-time / monthly add on", +)}`, () => { + let customerId = testCase; + let autumn: AutumnInt = new AutumnInt(); + let db, org, env; + + before(async function () { + await setupBefore(this); + db = this.db; + org = this.org; + env = this.env; + + await initCustomer({ + autumn: this.autumnJs, + customerId, + db, + org, + env, + fingerprint: "test", + }); + }); + + it("should create customer and have default free active", async function () { + const data = await autumn.customers.get(customerId); + + compareMainProduct({ + sent: products.free, + cusRes: data, + }); + }); + + it("should have correct entitlements", async function () { + const data = await autumn.customers.get(customerId); + const expectedEntitlement = products.free.entitlements.metered1; + + const entitled = (await autumn.check({ + customer_id: customerId, + feature_id: features.metered1.id, + })) as any; + + const metered1Balance = entitled.balances.find( + (balance: any) => balance.feature_id === features.metered1.id, + ); + + expect(entitled.allowed).to.be.true; + expect(metered1Balance).to.exist; + expect(metered1Balance.balance).to.equal(expectedEntitlement.allowance); + expect(metered1Balance.unlimited).to.not.exist; + }); + + it("should have correct boolean1 entitlement", async function () { + const entitled = await AutumnCli.entitled(customerId, features.boolean1.id); + expect(entitled!.allowed).to.be.false; + }); +}); + +// describe("Attach pro -- check products & entitlements", () => { +// it("POST /attach -- attaching pro (force checkout)", async function () { +// const { checkout_url } = await AutumnCli.attach({ +// customerId: customerId, +// productId: products.pro.id, +// }); + +// await completeCheckoutForm(checkout_url); +// await timeout(10000); // for webhook to be processed +// console.log(` ${chalk.greenBright("Attached pro")}`); +// }); + +// it("GET /customers/:id -- checking product & entitlements (pro)", async function () { +// const res = await AutumnCli.getCustomer(customerId); +// // console.log("Res: ", res); +// compareMainProduct({ +// sent: products.pro, +// cusRes: res, +// }); +// expect(res.invoices.length).to.be.greaterThan(0); +// }); + +// // return; + +// it("GET /entitled -- checking entitlements for metered1 && boolean1", async function () { +// const proEntitlements = products.pro.entitlements; + +// for (const entitlement of Object.values(proEntitlements)) { +// const allowance = entitlement.allowance; + +// const res: any = await AutumnCli.entitled( +// customerId, +// entitlement.feature_id!, +// ); + +// const entBalance = res!.balances.find( +// (b: any) => b.feature_id === entitlement.feature_id, +// ); + +// try { +// expect(res!.allowed).to.be.true; +// expect(entBalance).to.exist; +// if (entitlement.allowance) { +// expect(entBalance!.balance).to.equal(allowance); +// } +// // console.log(` - ${entitlement.feature_id} -- Passed`); +// } catch (error) { +// console.group(); +// console.group(); +// console.log("Looking for: ", entitlement); +// console.log("Received: ", res); +// console.groupEnd(); +// console.groupEnd(); +// throw error; +// } +// } +// }); +// }); + +// const oneTimeBillingUnits = +// products.oneTimeAddOnMetered1.prices[0].config.billing_units; +// const monthlyBillingUnits = +// products.monthlyAddOnMetered1.prices[0].config.billing_units; + +// describe("One time add on (force checkout)", () => { +// // PURCHASE ONE TIME ADD ON + +// it("POST /attach -- attaching one time add on (force checkout) [no quantity passed in]", async function () { +// try { +// for (let i = 0; i < oneTimePurchaseCount; i++) { +// const res = await AutumnCli.attach({ +// customerId: customerId, +// productId: products.oneTimeAddOnMetered1.id, +// forceCheckout: true, +// }); + +// await completeCheckoutForm(res.checkout_url, oneTimeOverrideQuantity); +// await timeout(10000); // for webhook to be processed +// console.log(` ${chalk.greenBright("Attached one time add on")}`); +// } +// } catch (error) { +// console.group(); +// console.group(); +// console.log("Failed to attach one time add on"); +// console.log("Error data:", error); +// console.groupEnd(); +// console.groupEnd(); +// process.exit(1); +// } +// }); + +// // TODO: Attach one time add on again (with quantity?) + +// it("GET /customers/:id -- checking product & entitlements (one time add on)", async function () { +// const cusRes = await AutumnCli.getCustomer(customerId); + +// // 1. Metered1 balance should be pro + one time add on + +// // Fetch balance +// const addOnBalance = cusRes.entitlements.find( +// (e: any) => +// e.feature_id === features.metered1.id && +// e.interval == +// products.oneTimeAddOnMetered1.entitlements.metered1.interval, +// ); + +// const expectedAmt = +// (oneTimeOverrideQuantity || oneTimeQuantity) * +// oneTimeBillingUnits * +// oneTimePurchaseCount; + +// try { +// assert.equal(addOnBalance!.balance, expectedAmt); +// assert.equal(cusRes.add_ons.length, 1); +// assert.equal(cusRes.add_ons[0].id, products.oneTimeAddOnMetered1.id); +// assert.equal(cusRes.invoices.length, 1 + oneTimePurchaseCount); +// } catch (error) { +// console.group(); +// console.group(); +// console.log("GET customer, balances failed"); +// console.log( +// "Add on entitlement:", +// products.oneTimeAddOnMetered1.entitlements.metered1, +// ); +// console.log("Customer entitlements:", cusRes.entitlements); + +// console.groupEnd(); +// console.groupEnd(); +// throw error; +// } +// }); + +// it("GET /entitled -- checking entitled for metered1", async function () { +// const res: any = await AutumnCli.entitled( +// customerId, +// features.metered1.id, +// ); + +// expect(res!.allowed).to.be.true; + +// // pro metered1 +// const proMetered1Amt = products.pro.entitlements.metered1.allowance; + +// const addOnBalance = res!.balances.find( +// (b: any) => b.feature_id === features.metered1.id, +// ); + +// expect(res!.allowed).to.be.true; +// expect(addOnBalance!.balance).to.equal( +// proMetered1Amt! + +// (oneTimeOverrideQuantity || oneTimeQuantity) * +// oneTimeBillingUnits * +// oneTimePurchaseCount, +// ); +// }); +// }); + +// // PURCHASE MONTHLY ADD ON +// describe("Monthly add on", () => { +// it("POST /attach -- attaching monthly add on", async function () { +// await AutumnCli.attach({ +// customerId: customerId, +// productId: products.monthlyAddOnMetered1.id, +// forceCheckout: false, +// options: [ +// { +// feature_id: features.metered1.id, +// quantity: monthlyQuantity * monthlyBillingUnits, +// }, +// ], +// }); +// await timeout(10000); + +// console.log(` ${chalk.greenBright("Attached monthly top up")}`); +// }); + +// it("GET /customers/:id -- checking product & entitlements (monthly add on)", async function () { +// const cusRes = await AutumnCli.getCustomer(customerId); + +// // 1. Metered1 balance should be pro + one time add on +// const proMetered1 = products.pro.entitlements.metered1.allowance; + +// // Fetch balance +// const monthlyMetered1Balance = cusRes.entitlements.find( +// (e: any) => +// e.feature_id === features.metered1.id && +// e.interval == +// products.monthlyAddOnMetered1.entitlements.metered1.interval, +// ); + +// try { +// assert.equal( +// monthlyMetered1Balance!.balance, +// proMetered1! + monthlyQuantity * monthlyBillingUnits, +// ); + +// assert.equal(cusRes.add_ons.length, 2); +// const monthlyAddOnId = cusRes.add_ons.find( +// (a: any) => a.id === products.monthlyAddOnMetered1.id, +// ); + +// assert.exists(monthlyAddOnId); +// expect(cusRes.invoices.length).to.equal(2 + oneTimePurchaseCount); +// } catch (error) { +// console.group(); +// console.group(); +// console.log("GET customer, balances failed"); +// console.log( +// "Add on entitlement:", +// products.monthlyAddOnMetered1.entitlements.metered1, +// ); +// console.log("Customer entitlements:", cusRes.entitlements); + +// console.groupEnd(); +// console.groupEnd(); +// throw error; +// } +// }); + +// it("GET /entitled -- checking entitlements (monthly add on)", async function () { +// const res: any = await AutumnCli.entitled( +// customerId, +// features.metered1.id, +// ); + +// const metered1Balance = res!.balances.find( +// (b: any) => b.feature_id === features.metered1.id, +// ); + +// const proMetered1Amt = products.pro.entitlements.metered1.allowance; +// const monthlyAddOnMetered1Amt = monthlyQuantity * monthlyBillingUnits; + +// const oneTimeAddOnMetered1Amt = +// (oneTimeOverrideQuantity || oneTimeQuantity) * +// oneTimeBillingUnits * +// oneTimePurchaseCount; + +// try { +// expect(metered1Balance!.balance).to.equal( +// proMetered1Amt! + monthlyAddOnMetered1Amt + oneTimeAddOnMetered1Amt, +// ); +// } catch (error) { +// console.group(); +// console.group(); +// console.log("GET entitled, balances failed"); + +// console.log("/entitled response:", res); +// console.log("Pro metered1 amt:", proMetered1Amt); +// console.log("Monthly add on metered1 amt:", monthlyAddOnMetered1Amt); +// console.log("One time add on metered1 amt:", oneTimeAddOnMetered1Amt); + +// console.groupEnd(); +// console.groupEnd(); +// throw error; +// } +// }); +// }); diff --git a/server/tests/attach_old/attach3.ts b/server/tests/attach/basic/basic10.ts similarity index 79% rename from server/tests/attach_old/attach3.ts rename to server/tests/attach/basic/basic10.ts index d96d67d41..9c65f6107 100644 --- a/server/tests/attach_old/attach3.ts +++ b/server/tests/attach/basic/basic10.ts @@ -14,14 +14,11 @@ import { completeCheckoutForm } from "tests/utils/stripeUtils.js"; import { Decimal } from "decimal.js"; import { expect } from "chai"; -describe(`${chalk.yellowBright("attach3: Multi attach, all one off")}`, () => { - let customerId = "attach3"; +const testCase = "basic10"; - // let billingUnits = - // oneTimeProducts.oneTimeMetered2.prices[0].config.billing_units; - // let quantity = 1000 * billingUnits; +describe(`${chalk.yellowBright("basic10: Multi attach, all one off")}`, () => { + let customerId = testCase; let quantity = 1000; - let options = [ { feature_id: features.metered2.id, @@ -48,7 +45,7 @@ describe(`${chalk.yellowBright("attach3: Multi attach, all one off")}`, () => { }); await completeCheckoutForm(res.checkout_url); - await timeout(15000); + await timeout(20000); }); it("should have correct main product and entitlements", async function () { @@ -65,7 +62,6 @@ describe(`${chalk.yellowBright("attach3: Multi attach, all one off")}`, () => { optionsList: options, }); - // Check invoices const invoices = cusRes.invoices; const metered1Amount = getFixedPriceAmount(oneTimeProducts.oneTimeMetered1); const metered2Tiers = getUsagePriceTiers({ @@ -79,13 +75,11 @@ describe(`${chalk.yellowBright("attach3: Multi attach, all one off")}`, () => { oneTimeProducts.oneTimeMetered2.prices[0].config.billing_units, ); - // console.log("Num billing units: ", numBillingUnits); + const expectedTotal = new Decimal(metered2Amount) + .mul(numBillingUnits) + .add(metered1Amount) + .toNumber(); - expect(invoices[0].total).to.equal( - new Decimal(metered2Amount) - .mul(numBillingUnits) - .add(metered1Amount) - .toNumber(), - ); + expect(invoices[0].total).to.equal(expectedTotal); }); }); diff --git a/server/tests/attach/basic/basic2.ts b/server/tests/attach/basic/basic2.ts new file mode 100644 index 000000000..04be9b43f --- /dev/null +++ b/server/tests/attach/basic/basic2.ts @@ -0,0 +1,295 @@ +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { assert, expect } from "chai"; +import chalk from "chalk"; +import { setupBefore } from "tests/before.js"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { features, products } from "tests/global.js"; +import { compareMainProduct } from "tests/utils/compare.js"; +import { completeCheckoutForm } from "tests/utils/stripeUtils.js"; +import { timeout } from "tests/utils/genUtils.js"; + +const oneTimeQuantity = 2; +const oneTimePurchaseCount = 2; +const oneTimeOverrideQuantity = 4; +const monthlyQuantity = 2; + +// UNCOMMENT FROM HERE +const testCase = "basic2"; +describe(`${chalk.yellowBright("basic2: Testing attach pro")}`, () => { + let customerId = testCase; + let autumn: AutumnInt = new AutumnInt(); + let db, org, env; + + before(async function () { + await setupBefore(this); + db = this.db; + org = this.org; + env = this.env; + + await initCustomer({ + autumn: this.autumnJs, + customerId, + db, + org, + env, + fingerprint: "test", + }); + }); + + it("should attach pro through checkout", async function () { + const { checkout_url } = await autumn.attach({ + customer_id: customerId, + product_id: products.pro.id, + }); + + await completeCheckoutForm(checkout_url); + await timeout(10000); + }); + + it("should have correct product & entitlements", async function () { + const res = await AutumnCli.getCustomer(customerId); + compareMainProduct({ + sent: products.pro, + cusRes: res, + }); + expect(res.invoices.length).to.be.greaterThan(0); + }); + + // return; + + it("should have correct result when calling /check", async function () { + const proEntitlements = products.pro.entitlements; + + for (const entitlement of Object.values(proEntitlements)) { + const allowance = entitlement.allowance; + + const res: any = await AutumnCli.entitled( + customerId, + entitlement.feature_id!, + ); + + const entBalance = res!.balances.find( + (b: any) => b.feature_id === entitlement.feature_id, + ); + + try { + expect(res!.allowed).to.be.true; + expect(entBalance).to.exist; + if (entitlement.allowance) { + expect(entBalance!.balance).to.equal(allowance); + } + // console.log(` - ${entitlement.feature_id} -- Passed`); + } catch (error) { + console.group(); + console.group(); + console.log("Looking for: ", entitlement); + console.log("Received: ", res); + console.groupEnd(); + console.groupEnd(); + throw error; + } + } + }); + + return; + + const oneTimeBillingUnits = + products.oneTimeAddOnMetered1.prices[0].config.billing_units; + const monthlyBillingUnits = + products.monthlyAddOnMetered1.prices[0].config.billing_units; + + describe("One time add on (force checkout)", () => { + // PURCHASE ONE TIME ADD ON + + it("POST /attach -- attaching one time add on (force checkout) [no quantity passed in]", async function () { + try { + for (let i = 0; i < oneTimePurchaseCount; i++) { + const res = await AutumnCli.attach({ + customerId: customerId, + productId: products.oneTimeAddOnMetered1.id, + forceCheckout: true, + }); + + await completeCheckoutForm(res.checkout_url, oneTimeOverrideQuantity); + await timeout(10000); // for webhook to be processed + console.log(` ${chalk.greenBright("Attached one time add on")}`); + } + } catch (error) { + console.group(); + console.group(); + console.log("Failed to attach one time add on"); + console.log("Error data:", error); + console.groupEnd(); + console.groupEnd(); + process.exit(1); + } + }); + + // TODO: Attach one time add on again (with quantity?) + + it("GET /customers/:id -- checking product & entitlements (one time add on)", async function () { + const cusRes = await AutumnCli.getCustomer(customerId); + + // 1. Metered1 balance should be pro + one time add on + + // Fetch balance + const addOnBalance = cusRes.entitlements.find( + (e: any) => + e.feature_id === features.metered1.id && + e.interval == + products.oneTimeAddOnMetered1.entitlements.metered1.interval, + ); + + const expectedAmt = + (oneTimeOverrideQuantity || oneTimeQuantity) * + oneTimeBillingUnits * + oneTimePurchaseCount; + + try { + assert.equal(addOnBalance!.balance, expectedAmt); + assert.equal(cusRes.add_ons.length, 1); + assert.equal(cusRes.add_ons[0].id, products.oneTimeAddOnMetered1.id); + assert.equal(cusRes.invoices.length, 1 + oneTimePurchaseCount); + } catch (error) { + console.group(); + console.group(); + console.log("GET customer, balances failed"); + console.log( + "Add on entitlement:", + products.oneTimeAddOnMetered1.entitlements.metered1, + ); + console.log("Customer entitlements:", cusRes.entitlements); + + console.groupEnd(); + console.groupEnd(); + throw error; + } + }); + + it("GET /entitled -- checking entitled for metered1", async function () { + const res: any = await AutumnCli.entitled( + customerId, + features.metered1.id, + ); + + expect(res!.allowed).to.be.true; + + // pro metered1 + const proMetered1Amt = products.pro.entitlements.metered1.allowance; + + const addOnBalance = res!.balances.find( + (b: any) => b.feature_id === features.metered1.id, + ); + + expect(res!.allowed).to.be.true; + expect(addOnBalance!.balance).to.equal( + proMetered1Amt! + + (oneTimeOverrideQuantity || oneTimeQuantity) * + oneTimeBillingUnits * + oneTimePurchaseCount, + ); + }); + }); + + // PURCHASE MONTHLY ADD ON + describe("Monthly add on", () => { + it("POST /attach -- attaching monthly add on", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: products.monthlyAddOnMetered1.id, + forceCheckout: false, + options: [ + { + feature_id: features.metered1.id, + quantity: monthlyQuantity * monthlyBillingUnits, + }, + ], + }); + await timeout(10000); + + console.log(` ${chalk.greenBright("Attached monthly top up")}`); + }); + + it("GET /customers/:id -- checking product & entitlements (monthly add on)", async function () { + const cusRes = await AutumnCli.getCustomer(customerId); + + // 1. Metered1 balance should be pro + one time add on + const proMetered1 = products.pro.entitlements.metered1.allowance; + + // Fetch balance + const monthlyMetered1Balance = cusRes.entitlements.find( + (e: any) => + e.feature_id === features.metered1.id && + e.interval == + products.monthlyAddOnMetered1.entitlements.metered1.interval, + ); + + try { + assert.equal( + monthlyMetered1Balance!.balance, + proMetered1! + monthlyQuantity * monthlyBillingUnits, + ); + + assert.equal(cusRes.add_ons.length, 2); + const monthlyAddOnId = cusRes.add_ons.find( + (a: any) => a.id === products.monthlyAddOnMetered1.id, + ); + + assert.exists(monthlyAddOnId); + expect(cusRes.invoices.length).to.equal(2 + oneTimePurchaseCount); + } catch (error) { + console.group(); + console.group(); + console.log("GET customer, balances failed"); + console.log( + "Add on entitlement:", + products.monthlyAddOnMetered1.entitlements.metered1, + ); + console.log("Customer entitlements:", cusRes.entitlements); + + console.groupEnd(); + console.groupEnd(); + throw error; + } + }); + + it("GET /entitled -- checking entitlements (monthly add on)", async function () { + const res: any = await AutumnCli.entitled( + customerId, + features.metered1.id, + ); + + const metered1Balance = res!.balances.find( + (b: any) => b.feature_id === features.metered1.id, + ); + + const proMetered1Amt = products.pro.entitlements.metered1.allowance; + const monthlyAddOnMetered1Amt = monthlyQuantity * monthlyBillingUnits; + + const oneTimeAddOnMetered1Amt = + (oneTimeOverrideQuantity || oneTimeQuantity) * + oneTimeBillingUnits * + oneTimePurchaseCount; + + try { + expect(metered1Balance!.balance).to.equal( + proMetered1Amt! + monthlyAddOnMetered1Amt + oneTimeAddOnMetered1Amt, + ); + } catch (error) { + console.group(); + console.group(); + console.log("GET entitled, balances failed"); + + console.log("/entitled response:", res); + console.log("Pro metered1 amt:", proMetered1Amt); + console.log("Monthly add on metered1 amt:", monthlyAddOnMetered1Amt); + console.log("One time add on metered1 amt:", oneTimeAddOnMetered1Amt); + + console.groupEnd(); + console.groupEnd(); + throw error; + } + }); + }); +}); diff --git a/server/tests/attach/basic/basic3.ts b/server/tests/attach/basic/basic3.ts new file mode 100644 index 000000000..e01f082d4 --- /dev/null +++ b/server/tests/attach/basic/basic3.ts @@ -0,0 +1,161 @@ +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { assert, expect } from "chai"; +import chalk from "chalk"; +import { setupBefore } from "tests/before.js"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { features, products } from "tests/global.js"; +import { compareMainProduct } from "tests/utils/compare.js"; +import { completeCheckoutForm } from "tests/utils/stripeUtils.js"; +import { timeout } from "tests/utils/genUtils.js"; +import { + constructProduct, + constructRawProduct, +} from "@/utils/scriptUtils/createTestProducts.js"; +import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; +import { createProducts } from "tests/utils/productUtils.js"; + +// const oneTimeQuantity = 2; +// const oneTimePurchaseCount = 2; +// const oneTimeOverrideQuantity = 4; +// const monthlyQuantity = 2; +let oneTimeItem = constructPrepaidItem({ + featureId: features.metered1.id, + price: 9, + billingUnits: 250, + isOneOff: true, +}); + +let oneTime = constructRawProduct({ + id: "basic3_one_off", + items: [oneTimeItem], + isAddOn: true, +}); + +let monthlyItem = constructPrepaidItem({ + featureId: features.metered1.id, + price: 9, + billingUnits: 250, +}); + +let monthly = constructRawProduct({ + id: "basic3_monthly", + items: [ + constructPrepaidItem({ + featureId: features.metered1.id, + price: 9, + billingUnits: 250, + }), + ], +}); + +// UNCOMMENT FROM HERE +const testCase = "basic3"; +describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add ons")}`, () => { + let customerId = testCase; + let autumn: AutumnInt = new AutumnInt(); + let db, org, env; + + before(async function () { + await setupBefore(this); + db = this.db; + org = this.org; + env = this.env; + + await initCustomer({ + autumn: this.autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + await createProducts({ + autumn: this.autumnJs, + db, + orgId: org.id, + env, + products: [oneTime, monthly], + }); + }); + + it("should attach pro", async function () { + await autumn.attach({ + customer_id: customerId, + product_id: products.pro.id, + }); + + const res = await AutumnCli.getCustomer(customerId); + compareMainProduct({ + sent: products.pro, + cusRes: res, + }); + }); + + const oneTimeQuantity = 500; + const oneTimeBillingUnits = oneTimeItem.billing_units; + const oneTimePurchaseCount = 2; + + it("should attach one time add on twice, force checkout", async function () { + for (let i = 0; i < 2; i++) { + const res = await autumn.attach({ + customer_id: customerId, + product_id: oneTime.id, + force_checkout: true, + }); + + await completeCheckoutForm( + res.checkout_url, + oneTimeQuantity / oneTimeBillingUnits!, + ); + await timeout(20000); + } + }); + + it("should have correct product & entitlements", async function () { + const cusRes = await AutumnCli.getCustomer(customerId); + + const addOnBalance = cusRes.entitlements.find( + (e: any) => + e.feature_id === features.metered1.id && + e.interval == + products.oneTimeAddOnMetered1.entitlements.metered1.interval, + ); + + const expectedAmt = oneTimeQuantity * oneTimePurchaseCount; + + expect(addOnBalance!.balance).to.equal( + expectedAmt, + "add on balance should be correct", + ); + expect(cusRes.add_ons).to.have.lengthOf( + 1, + "should only have one add on product after two purchases (since they combine)", + ); + expect(cusRes.add_ons[0].id).to.equal( + oneTime.id, + "add on product should exist", + ); + expect(cusRes.invoices.length).to.equal( + 1 + oneTimePurchaseCount, + "invoices should be correct", + ); + }); + + it("should have correct /check result for metered1", async function () { + const res: any = await AutumnCli.entitled(customerId, features.metered1.id); + + expect(res!.allowed).to.be.true; + + const proMetered1Amt = products.pro.entitlements.metered1.allowance; + const addOnBalance = res!.balances.find( + (b: any) => b.feature_id === features.metered1.id, + ); + + expect(res!.allowed).to.be.true; + expect(addOnBalance!.balance).to.equal( + proMetered1Amt! + oneTimeQuantity * oneTimePurchaseCount, + ); + }); +}); diff --git a/server/tests/attach/basic/basic4.ts b/server/tests/attach/basic/basic4.ts new file mode 100644 index 000000000..f2b1cd9ce --- /dev/null +++ b/server/tests/attach/basic/basic4.ts @@ -0,0 +1,121 @@ +import chalk from "chalk"; +import { expect } from "chai"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { setupBefore } from "tests/before.js"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { features, products } from "tests/global.js"; +import { compareMainProduct } from "tests/utils/compare.js"; +import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; +import { createProducts } from "tests/utils/productUtils.js"; + +let monthlyItem = constructPrepaidItem({ + featureId: features.metered1.id, + price: 9, + billingUnits: 250, +}); + +let monthly = constructRawProduct({ + id: "basic4_monthly", + items: [monthlyItem], +}); + +const testCase = "basic4"; +describe(`${chalk.yellowBright("basic4: Testing attach monthly add on")}`, () => { + let customerId = testCase; + let autumn: AutumnInt = new AutumnInt(); + let db, org, env; + + before(async function () { + await setupBefore(this); + db = this.db; + org = this.org; + env = this.env; + + await initCustomer({ + autumn: this.autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + await createProducts({ + autumn: this.autumnJs, + db, + orgId: org.id, + env, + products: [monthly], + }); + }); + + it("should attach pro", async function () { + await autumn.attach({ + customer_id: customerId, + product_id: products.pro.id, + }); + + const res = await AutumnCli.getCustomer(customerId); + compareMainProduct({ + sent: products.pro, + cusRes: res, + }); + }); + + const monthlyQuantity = 500; + + it("should attach monthly add on", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: products.monthlyAddOnMetered1.id, + forceCheckout: false, + options: [ + { + feature_id: features.metered1.id, + quantity: monthlyQuantity, + }, + ], + }); + }); + + it("should have correct product & entitlements", async function () { + const cusRes = await AutumnCli.getCustomer(customerId); + const proMetered1 = products.pro.entitlements.metered1.allowance; + + const monthlyMetered1Balance = cusRes.entitlements.find( + (e: any) => + e.feature_id === features.metered1.id && + e.interval == + products.monthlyAddOnMetered1.entitlements.metered1.interval, + ); + + expect(monthlyMetered1Balance!.balance).to.equal( + proMetered1! + monthlyQuantity, + ); + + expect(cusRes.add_ons).to.have.lengthOf(1); + const monthlyAddOnId = cusRes.add_ons.find( + (a: any) => a.id === products.monthlyAddOnMetered1.id, + ); + + expect(monthlyAddOnId).to.exist; + expect(cusRes.invoices.length).to.equal(2); + }); + + it("should have correct /check result for metered1", async function () { + const res: any = await AutumnCli.entitled(customerId, features.metered1.id); + + const metered1Balance = res!.balances.find( + (b: any) => b.feature_id === features.metered1.id, + ); + + const proMetered1Amt = products.pro.entitlements.metered1.allowance; + const monthlyAddOnMetered1Amt = monthlyQuantity; + + expect(metered1Balance!.balance).to.equal( + proMetered1Amt! + monthlyAddOnMetered1Amt, + ); + }); +}); diff --git a/server/tests/attach/basic/basic5.ts b/server/tests/attach/basic/basic5.ts new file mode 100644 index 000000000..3c232d8e7 --- /dev/null +++ b/server/tests/attach/basic/basic5.ts @@ -0,0 +1,98 @@ +import { createStripeCli } from "@/external/stripe/utils.js"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { features, products } from "tests/global.js"; +import chalk from "chalk"; +import { + checkFeatureHasCorrectBalance, + compareMainProduct, +} from "tests/utils/compare.js"; +import { expect } from "chai"; +import { CusProductStatus } from "@autumn/shared"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { setupBefore } from "tests/before.js"; +import Stripe from "stripe"; +import { timeout } from "@/utils/genUtils.js"; + +const testCase = "basic5"; +describe(`${chalk.yellowBright( + "basic5: Testing cancel through Stripe at period end and now", +)}`, () => { + const customerId = testCase; + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + stripeCli = this.stripeCli; + await initCustomer({ + customerId, + db: this.db, + org: this.org, + env: this.env, + autumn: this.autumnJs, + attachPm: "success", + }); + }); + + it("should attach pro product", async function () { + const res: any = await AutumnCli.attach({ + customerId: customerId, + productId: products.pro.id, + }); + }); + + it("should cancel pro product (at period end)", async function () { + const stripeCli = createStripeCli({ org: this.org, env: this.env }); + const cusRes: any = await AutumnCli.getCustomer(customerId); + + const proProduct = cusRes.products.find( + (p: any) => p.id === products.pro.id, + ); + + for (const subId of proProduct.subscription_ids) { + await stripeCli.subscriptions.update(subId, { + cancel_at_period_end: true, + }); + } + await timeout(5000); + }); + + it("should have pro product active, and canceled_at != null, and free scheduled", async function () { + const cusRes: any = await AutumnCli.getCustomer(customerId); + compareMainProduct({ + sent: products.pro, + cusRes: cusRes, + }); + + const proProduct = cusRes.products.find( + (p: any) => p.id === products.pro.id, + ); + expect(proProduct.canceled_at).to.not.equal(null); + expect(proProduct.status).to.equal(CusProductStatus.Active); + + const freeProduct = cusRes.products.find( + (p: any) => p.id === products.free.id, + ); + expect(freeProduct).to.exist; + expect(freeProduct.status).to.equal(CusProductStatus.Scheduled); + }); + + it("should cancel pro product (now)", async function () { + const cusRes: any = await AutumnCli.getCustomer(customerId); + const proProduct = cusRes.products.find( + (p: any) => p.id === products.pro.id, + ); + + for (const subId of proProduct.subscription_ids) { + await stripeCli.subscriptions.cancel(subId); + } + await timeout(5000); + }); + + it("should have free product active, and no pro product", async function () { + const cusRes: any = await AutumnCli.getCustomer(customerId); + compareMainProduct({ + sent: products.free, + cusRes: cusRes, + }); + }); +}); diff --git a/server/tests/attach/basic/basic6.ts b/server/tests/attach/basic/basic6.ts new file mode 100644 index 000000000..90abec520 --- /dev/null +++ b/server/tests/attach/basic/basic6.ts @@ -0,0 +1,74 @@ +import { createStripeCli } from "@/external/stripe/utils.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import chalk from "chalk"; +import { setupBefore } from "tests/before.js"; +import Stripe from "stripe"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { products } from "tests/global.js"; +import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; +import { CusProductStatus, Customer } from "@autumn/shared"; +import { addDays, addHours, addMonths } from "date-fns"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { compareMainProduct } from "tests/utils/compare.js"; +import { expect } from "chai"; + +const testCase = "basic6"; +describe(`${chalk.yellowBright( + "basic6: Testing subscription past_due", +)}`, () => { + const customerId = testCase; + let stripeCli: Stripe; + let testClockId: string; + let customer: Customer; + + before(async function () { + await setupBefore(this); + stripeCli = this.stripeCli; + + const { testClockId: testClockId_, customer: customer_ } = + await initCustomer({ + customerId, + db: this.db, + org: this.org, + env: this.env, + autumn: this.autumnJs, + attachPm: "success", + }); + testClockId = testClockId_; + customer = customer_; + }); + + it("should attach pro product and switch to failed payment method", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: products.pro.id, + }); + + await attachFailedPaymentMethod({ + stripeCli, + customer, + }); + }); + + it("should advance to next cycle", async function () { + await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addHours( + addMonths(new Date(), 1), + hoursToFinalizeInvoice, + ).getTime(), + waitForSeconds: 30, + }); + }); + + it("should have pro product in past due status", async function () { + const cusRes: any = await AutumnCli.getCustomer(customerId); + const proProduct = cusRes.products.find( + (p: any) => p.id === products.pro.id, + ); + expect(proProduct).to.exist; + expect(proProduct.status).to.equal(CusProductStatus.PastDue); + }); +}); diff --git a/server/tests/attach/basic/basic7.ts b/server/tests/attach/basic/basic7.ts new file mode 100644 index 000000000..85849ee75 --- /dev/null +++ b/server/tests/attach/basic/basic7.ts @@ -0,0 +1,80 @@ +import chalk from "chalk"; +import { compareMainProduct } from "tests/utils/compare.js"; +import { products } from "tests/global.js"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { assert, expect } from "chai"; +import { timeout } from "tests/utils/genUtils.js"; +import { CusProductStatus } from "@autumn/shared"; +import { setupBefore } from "tests/before.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; + +const testCase = "basic7"; + +describe(`${chalk.yellowBright("basic7: Testing trial duplicates (same customer)")}`, () => { + const customerId = testCase; + let customerId2 = testCase + "2"; + const autumn = new AutumnInt(); + + before(async function () { + await setupBefore(this); + await initCustomer({ + customerId, + db: this.db, + org: this.org, + env: this.env, + autumn: this.autumnJs, + attachPm: "success", + }); + }); + + it("should attach pro with trial and have correct product & invoice", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: products.proWithTrial.id, + }); + + const customer = await AutumnCli.getCustomer(customerId); + + compareMainProduct({ + sent: products.proWithTrial, + cusRes: customer, + status: CusProductStatus.Trialing, + }); + + const invoices = customer.invoices; + expect(invoices.length).to.equal(1, "Invoice length should be 1"); + expect(invoices[0].total).to.equal(0, "Invoice total should be 0"); + }); + + it("should cancel pro with trial", async function () { + await autumn.cancel({ + customer_id: customerId, + product_id: products.proWithTrial.id, + expire_immediately: true, + }); + await timeout(5000); // for webhook to be processed + }); + + it("should be able to attach pro with trial again (renewal flow)", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: products.proWithTrial.id, + }); + + const customer = await AutumnCli.getCustomer(customerId); + + compareMainProduct({ + sent: products.proWithTrial, + cusRes: customer, + status: CusProductStatus.Trialing, + }); + + const invoices = customer.invoices; + expect(invoices.length).to.equal(1, "Invoice length should be 1"); + expect(invoices[0].amount).to.equal( + products.proWithTrial.prices[0].amount, + "should have paid full amount (trial already used once)", + ); + }); +}); diff --git a/server/tests/attach/basic/basic8.ts b/server/tests/attach/basic/basic8.ts new file mode 100644 index 000000000..2cde7074a --- /dev/null +++ b/server/tests/attach/basic/basic8.ts @@ -0,0 +1,85 @@ +import chalk from "chalk"; +import { setupBefore } from "tests/before.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { products } from "tests/global.js"; +import { compareMainProduct } from "tests/utils/compare.js"; +import { CusProductStatus } from "@autumn/shared"; +import { expect } from "chai"; +import { timeout } from "@/utils/genUtils.js"; + +const testCase = "basic8"; + +describe(`${chalk.yellowBright("basic8: Testing trial duplicates (same fingerprint)")}`, () => { + const customerId = testCase; + let customerId2 = testCase + "2"; + const autumn = new AutumnInt(); + + before(async function () { + const randFingerprint = Math.random().toString(36).substring(2, 15); + await setupBefore(this); + await initCustomer({ + customerId, + db: this.db, + org: this.org, + env: this.env, + autumn: this.autumnJs, + fingerprint: randFingerprint, + attachPm: "success", + }); + + await initCustomer({ + customerId: customerId2, + db: this.db, + org: this.org, + env: this.env, + autumn: this.autumnJs, + fingerprint: randFingerprint, + attachPm: "success", + }); + }); + + it("should attach pro with trial and have correct product & invoice", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: products.proWithTrial.id, + }); + + const customer = await AutumnCli.getCustomer(customerId); + + compareMainProduct({ + sent: products.proWithTrial, + cusRes: customer, + status: CusProductStatus.Trialing, + }); + + const invoices = customer.invoices; + expect(invoices.length).to.equal(1, "Invoice length should be 1"); + expect(invoices[0].total).to.equal(0, "Invoice total should be 0"); + }); + + it("should attach pro with trial to second customer and have correct product & invoice (pro with trial, full price)", async function () { + await autumn.attach({ + customer_id: customerId2, + product_id: products.proWithTrial.id, + }); + + // await timeout(5000); // for webhook to be processed + const customer = await AutumnCli.getCustomer(customerId2); + + compareMainProduct({ + sent: products.proWithTrial, + cusRes: customer, + status: CusProductStatus.Active, + }); + + // Check invoice is equal monthly price + const invoices = customer.invoices; + expect(invoices.length).to.equal(1, "Invoice length should be 1"); + expect(invoices[0].total).to.equal( + 10, + "Invoice total should be full price", + ); + }); +}); diff --git a/server/tests/attach_old/attach2.ts b/server/tests/attach/basic/basic9.ts similarity index 91% rename from server/tests/attach_old/attach2.ts rename to server/tests/attach/basic/basic9.ts index c0ea56060..826c9553a 100644 --- a/server/tests/attach_old/attach2.ts +++ b/server/tests/attach/basic/basic9.ts @@ -7,10 +7,11 @@ import { initCustomer } from "tests/utils/init.js"; import { completeCheckoutForm } from "tests/utils/stripeUtils.js"; import { compareMainProduct } from "tests/utils/compare.js"; +const testCase = "basic9"; describe(`${chalk.yellowBright( - "attach2: Testing monthly with one time prepaid, quantity = 0", + "basic9: attach monthly with one time prepaid, and quantity = 0", )}`, () => { - let customerId = "attach2"; + let customerId = testCase; let options = [ { diff --git a/server/tests/attach/downgrade/downgrade1.ts b/server/tests/attach/downgrade/downgrade1.ts index 2d1599ec4..068fdcb04 100644 --- a/server/tests/attach/downgrade/downgrade1.ts +++ b/server/tests/attach/downgrade/downgrade1.ts @@ -12,7 +12,6 @@ import { addPrefixToProducts, runAttachTest } from "../utils.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { constructFeatureItem } from "@/internal/products/product-items/productItemUtils.js"; import { expectDowngradeCorrect } from "tests/utils/expectUtils/expectScheduleUtils.js"; import { expectNextCycleCorrect } from "tests/utils/expectUtils/expectScheduleUtils.js"; diff --git a/server/tests/attach/downgrade/downgrade5.ts b/server/tests/attach/downgrade/downgrade5.ts new file mode 100644 index 000000000..1e7afa98f --- /dev/null +++ b/server/tests/attach/downgrade/downgrade5.ts @@ -0,0 +1,120 @@ +import chalk from "chalk"; +import Stripe from "stripe"; + +import { CusProductStatus } from "@autumn/shared"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { products } from "tests/global.js"; +import { expect } from "chai"; + +import { compareMainProduct } from "tests/utils/compare.js"; +import { addHours, addMonths } from "date-fns"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; + +import { setupBefore } from "tests/before.js"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; + +const testCase = "downgrade5"; +describe(`${chalk.yellowBright( + "downgrade5: testing basic downgrade (paid to paid)", +)}`, () => { + let customerId = testCase; + let autumn: AutumnInt = new AutumnInt(); + let testClockId: string; + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + stripeCli = this.stripeCli; + + const { customer: customer_, testClockId: testClockId_ } = + await initCustomer({ + customerId, + db: this.db, + org: this.org, + env: this.env, + attachPm: "success", + autumn: autumnJs, + }); + + testClockId = testClockId_; + }); + + it("should attach premium", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: products.premium.id, + }); + }); + + it("should attach pro", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: products.pro.id, + }); + }); + + it("should have correct product and entitlements for scheduled pro", async function () { + const res = await AutumnCli.getCustomer(customerId); + + compareMainProduct({ + sent: products.premium, + cusRes: res, + }); + + const { products: resProducts } = res; + + const resPro = resProducts.find( + (p: any) => + p.id === products.pro.id && p.status === CusProductStatus.Scheduled, + ); + + expect(resPro).to.exist; + }); + + it("should attach premium and remove scheduled pro", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: products.premium.id, + }); + + const res = await AutumnCli.getCustomer(customerId); + const resPro = res.products.find( + (p: any) => + p.id === products.pro.id && p.status === CusProductStatus.Scheduled, + ); + + expect(resPro).to.not.exist; + + compareMainProduct({ + sent: products.premium, + cusRes: res, + }); + }); + + // Advance time 1 month + it("should attach pro, advance stripe clock and have pro is attached", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: products.pro.id, + }); + + await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addHours( + addMonths(new Date(), 1), + hoursToFinalizeInvoice, + ).getTime(), + waitForSeconds: 15, + }); + + const res = await AutumnCli.getCustomer(customerId); + compareMainProduct({ + sent: products.pro, + cusRes: res, + }); + }); +}); diff --git a/server/tests/attach/downgrade/downgrade6.ts b/server/tests/attach/downgrade/downgrade6.ts new file mode 100644 index 000000000..577367d56 --- /dev/null +++ b/server/tests/attach/downgrade/downgrade6.ts @@ -0,0 +1,114 @@ +import { Customer } from "@autumn/shared"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { products } from "tests/global.js"; +import chalk from "chalk"; +import { compareMainProduct } from "tests/utils/compare.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { setupBefore } from "tests/before.js"; +import { getMainCusProduct } from "@/internal/customers/cusProducts/cusProductUtils.js"; + +const testCase = "downgrade6"; +describe(`${chalk.yellowBright("downgrade6: testing expire button")}`, () => { + let customerId = testCase; + let testClockId: string; + let autumn: AutumnInt = new AutumnInt(); + let customer: Customer; + + before(async function () { + await setupBefore(this); + + const { testClockId: testClockId_, customer: customer_ } = + await initCustomer({ + customerId, + db: this.db, + org: this.org, + env: this.env, + autumn: this.autumnJs, + }); + + customer = customer_; + testClockId = testClockId_; + }); + + it("should attach premium", async function () { + await autumn.attach({ + customer_id: customerId, + product_id: products.premium.id, + }); + }); + + it("should expire premium", async function () { + const cusProduct = await getMainCusProduct({ + db: this.db, + internalCustomerId: customer.internal_id, + }); + + await AutumnCli.expire(cusProduct!.id); + }); + + it("should have correct product and entitlements after expiration", async function () { + const res = await AutumnCli.getCustomer(customerId); + + compareMainProduct({ + sent: products.free, + cusRes: res, + }); + }); + + // // 2. Get premium + // it("POST /attach -- attaching premium, then attach pro", async function () { + // this.timeout(30000); + // await AutumnCli.attach({ + // customerId: customerId, + // productId: products.premium.id, + // }); + + // await AutumnCli.attach({ + // customerId: customerId, + // productId: products.pro.id, + // }); + // }); + + // it("Expiring pro product (should re-attach premium)", async function () { + // this.timeout(30000); + + // // Expire pro product + // const customerProduct = await getCusProduct( + // this.sb, + // customer.internal_id, + // products.pro.id, + // ); + // await AutumnCli.expire(customerProduct.id); + // await timeout(5000); + // }); + + // it("GET /customers/:customer_id -- checking product and ents", async function () { + // this.timeout(30000); + // // Check that free is attached + // const res = await AutumnCli.getCustomer(customerId); + // compareMainProduct({ + // sent: products.premium, + // cusRes: res, + // }); + + // // Get stripe subscription (ensure canceled is null) + // const stripeCli = createStripeCli({ + // org: this.org, + // env: this.env, + // }); + + // const premiumCusProduct = await getCusProduct( + // this.sb, + // customer.internal_id, + // products.premium.id, + // ); + + // const stripeSub = await stripeCli.subscriptions.retrieve( + // premiumCusProduct.processor.subscription_id, + // ); + + // // Check that canceled is null + // assert.isNull(stripeSub.canceled_at); + // }); +}); diff --git a/server/tests/attach/downgrade/downgrade7.ts b/server/tests/attach/downgrade/downgrade7.ts new file mode 100644 index 000000000..21ec10db5 --- /dev/null +++ b/server/tests/attach/downgrade/downgrade7.ts @@ -0,0 +1,80 @@ +import { Customer } from "@autumn/shared"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { products } from "tests/global.js"; +import chalk from "chalk"; +import { compareMainProduct } from "tests/utils/compare.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { setupBefore } from "tests/before.js"; +import { findCusProductById } from "@/internal/customers/cusProducts/cusProductUtils/findCusProduct.js"; +import { expect } from "chai"; +import { getSubsFromCusId } from "tests/utils/expectUtils/expectSubUtils.js"; + +const testCase = "downgrade7"; +describe(`${chalk.yellowBright("downgrade7: testing expire scheduled product")}`, () => { + let customerId = testCase; + let testClockId: string; + let customer: Customer; + + before(async function () { + await setupBefore(this); + + const { testClockId: testClockId_, customer: customer_ } = + await initCustomer({ + customerId, + db: this.db, + org: this.org, + env: this.env, + autumn: this.autumnJs, + attachPm: "success", + }); + + customer = customer_; + testClockId = testClockId_; + }); + + // 2. Get premium + it("should attach premium, then attach pro", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: products.premium.id, + }); + + await AutumnCli.attach({ + customerId: customerId, + productId: products.pro.id, + }); + }); + + it("should expire scheduled product (pro)", async function () { + const cusProduct = await findCusProductById({ + db: this.db, + internalCustomerId: customer.internal_id, + productId: products.pro.id, + }); + + expect(cusProduct).to.exist; + await AutumnCli.expire(cusProduct!.id); + }); + + it("should have correct product and entitlements (premium)", async function () { + this.timeout(30000); + // Check that free is attached + const res = await AutumnCli.getCustomer(customerId); + compareMainProduct({ + sent: products.premium, + cusRes: res, + }); + + const { subs } = await getSubsFromCusId({ + stripeCli: this.stripeCli, + customerId: customerId, + productId: products.premium.id, + db: this.db, + org: this.org, + env: this.env, + }); + expect(subs).to.have.lengthOf(1); + expect(subs[0].canceled_at).to.be.null; + }); +}); diff --git a/server/tests/attach/downgrade/downgrade9.ts b/server/tests/attach/downgrade/downgrade9.ts new file mode 100644 index 000000000..045fc9789 --- /dev/null +++ b/server/tests/attach/downgrade/downgrade9.ts @@ -0,0 +1,118 @@ +import { createStripeCli } from "@/external/stripe/utils.js"; +import chalk from "chalk"; +import Stripe from "stripe"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { advanceProducts } from "tests/global.js"; +import { + checkProductIsScheduled, + compareMainProduct, +} from "tests/utils/compare.js"; +import { setupBefore } from "tests/before.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; + +// TEST MULTI INTERVAL DOWNGRADE +// +/* +CASE 1: Annual pro -> Annual starter + - If attach annual starter, should schedule correctly [DONE] + - If advance test clock, should downgrade correctly (to monthly starter) [DONE] + - If cancel active subscription (on Stripe), should remove scheduled correctly [DONE] + - If cancel scheduled subscription (on Stripe), should remove scheduled correctly [DONE] + - If expire on dashboard, should remove scheduled correctly + + - If upgrade back to annual pro, should remove scheduled correctly [DONE] + - If downgrade to monthly pro (switch downgrade), should be correct [DONE] + - If downgrade to free (switch downgrade), should be correct +*/ + +const testCase = "downgrade9"; + +describe(`${chalk.yellowBright("downgrade9: Multi interval downgrade -- Annual pro -> Annual starter")}`, () => { + let customerId = testCase; + + before(async function () { + await setupBefore(this); + await initCustomer({ + customerId, + db: this.db, + org: this.org, + env: this.env, + autumn: this.autumnJs, + attachPm: "success", + withTestClock: false, + }); + }); + + it("should attach annual pro", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: advanceProducts.gpuProAnnual.id, + }); + + let cusRes = await AutumnCli.getCustomer(customerId); + compareMainProduct({ + sent: advanceProducts.gpuProAnnual, + cusRes, + }); + }); + + it("should attach downgrade to annual starter", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: advanceProducts.gpuStarterAnnual.id, + }); + + let cusRes = await AutumnCli.getCustomer(customerId); + checkProductIsScheduled({ + cusRes, + product: advanceProducts.gpuStarterAnnual, + }); + }); +}); + +// describe(`${chalk.yellowBright("downgrade9: Multi interval downgrade -- Quarterly pro -> Monthly pro")}`, () => { +// let customerId = testCase; +// let stripeCli: Stripe; +// let testClockId: string; + +// before(async function () { +// const { testClockId: insertedTestClockId } = +// await initCustomerWithTestClock({ +// customerId, +// org: this.org, +// env: this.env, +// db: this.db, +// }); +// testClockId = insertedTestClockId; +// stripeCli = createStripeCli({ +// org: this.org, +// env: this.env, +// }); +// }); + +// it("should attach quarterly pro", async function () { +// let res = await AutumnCli.attach({ +// customerId: customerId, +// productId: advanceProducts.gpuProQuarter.id, +// }); + +// let cusRes = await AutumnCli.getCustomer(customerId); +// compareMainProduct({ +// sent: advanceProducts.gpuProQuarter, +// cusRes, +// }); +// }); + +// it("should attach downgrade to monthly pro", async function () { +// let res = await AutumnCli.attach({ +// customerId: customerId, +// productId: advanceProducts.gpuSystemPro.id, +// }); + +// let cusRes = await AutumnCli.getCustomer(customerId); +// checkProductIsScheduled({ +// cusRes, +// product: advanceProducts.gpuSystemPro, +// }); +// }); +// }); diff --git a/server/tests/attach/entities/entity2.ts b/server/tests/attach/entities/entity2.ts index bc016291c..739e81c2a 100644 --- a/server/tests/attach/entities/entity2.ts +++ b/server/tests/attach/entities/entity2.ts @@ -147,38 +147,4 @@ describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach pro annual to env, }); }); - - // let nextUsage = 123123912; - // it("should cancel and have a final invoice", async function () { - // await autumn.cancel({ - // customer_id: customerId, - // product_id: proAnnual.id, - // entity_id: entityId, - // }); - - // await timeout(5000); - - // curUnix = await advanceTestClock({ - // stripeCli, - // testClockId, - // advanceTo: addHours( - // addMonths(curUnix, 1), - // hoursToFinalizeInvoice, - // ).getTime(), - // }); - - // await expectInvoiceAfterUsage({ - // autumn, - // customerId, - // entityId, - // featureId: TestFeature.Words, - // product: proAnnual, - // usage: nextUsage, - // stripeCli, - // db, - // org, - // env, - // numInvoices: 3, - // }); - // }); }); diff --git a/server/tests/attach_old/01_multi_product1.ts b/server/tests/attach/multiProduct/multiProduct1.ts similarity index 76% rename from server/tests/attach_old/01_multi_product1.ts rename to server/tests/attach/multiProduct/multiProduct1.ts index 2e9a9102d..e84332eb7 100644 --- a/server/tests/attach_old/01_multi_product1.ts +++ b/server/tests/attach/multiProduct/multiProduct1.ts @@ -1,8 +1,10 @@ import { AutumnCli } from "tests/cli/AutumnCli.js"; import { attachProducts } from "tests/global.js"; import { compareMainProduct } from "tests/utils/compare.js"; -import { initCustomer } from "tests/utils/init.js"; import chalk from "chalk"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { setupBefore } from "tests/before.js"; +import { Customer } from "@autumn/shared"; /* FLOW: @@ -10,24 +12,24 @@ FLOW: 2. Upgrade pro group 1 -> premium group 1 3. Upgrade pro group 2 -> premium group 2 */ + +const testCase = "multiProduct1"; describe( - chalk.yellowBright( - "01_multi_product1: Testing multi product attach, and upgrade", - ), + chalk.yellowBright(`${testCase}: Testing multi product attach, and upgrade`), () => { - let customerId = "multi-product-attach-upgrade"; + let customerId = testCase; + let customer: Customer; before(async function () { - this.customer = await initCustomer({ + await setupBefore(this); + const res = await initCustomer({ + customerId, db: this.db, org: this.org, - customer_data: { - id: customerId, - name: customerId, - email: "multi-product-attach-upgrade@example.com", - }, env: this.env, - attachPm: true, + autumn: this.autumnJs, + attachPm: "success", }); + customer = res.customer; }); it("should attach pro group 1 and pro group 2", async function () { @@ -50,8 +52,6 @@ describe( // 1. Compare main product const cusRes = await AutumnCli.getCustomer(customerId); compareMainProduct({ sent: attachProducts.premiumGroup1, cusRes }); - - // 2. Check latest invoice }); it("should upgrade to premium group 2", async function () { diff --git a/server/tests/attach_old/01_multi_product3.ts b/server/tests/attach/multiProduct/multiProduct2.ts similarity index 72% rename from server/tests/attach_old/01_multi_product3.ts rename to server/tests/attach/multiProduct/multiProduct2.ts index c71948466..0d123d487 100644 --- a/server/tests/attach_old/01_multi_product3.ts +++ b/server/tests/attach/multiProduct/multiProduct2.ts @@ -1,17 +1,20 @@ +import chalk from "chalk"; + +import { Stripe } from "stripe"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { CusProductStatus, Customer } from "@autumn/shared"; import { expect } from "chai"; import { AutumnCli } from "tests/cli/AutumnCli.js"; -import { advanceProducts, attachProducts } from "tests/global.js"; +import { attachProducts } from "tests/global.js"; import { checkProductIsScheduled, compareMainProduct, } from "tests/utils/compare.js"; -import { initCustomer } from "tests/utils/init.js"; -import { searchCusProducts, timeout } from "tests/utils/genUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { Stripe } from "stripe"; + +import { searchCusProducts } from "tests/utils/genUtils.js"; import { checkScheduleContainsProducts } from "tests/utils/scheduleCheckUtils.js"; +import { setupBefore } from "tests/before.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; /* FLOW: @@ -20,26 +23,27 @@ FLOW: 3. Downgrade to starter group 2 4. Change downgrade to pro group 2 */ -describe("Multi Product 3: premium1->starter1, premium2->starter2, then premium2->pro2, then premium2->free", () => { - let customerId = "multi-side-free-downgrade"; + +const testCase = "multiProduct2"; +describe(`${chalk.yellowBright( + "multiProduct2: premium1->starter1, premium2->starter2, then premium2->pro2, then premium2->free", +)}`, () => { + let customerId = testCase; let customer: Customer; let stripeCli: Stripe; + before(async function () { - customer = await initCustomer({ + await setupBefore(this); + stripeCli = this.stripeCli; + const res = await initCustomer({ db: this.db, org: this.org, - customer_data: { - id: customerId, - name: customerId, - email: "multi-side-free-downgrade@example.com", - }, - env: this.env, - attachPm: true, - }); - stripeCli = createStripeCli({ - org: this.org, + customerId, env: this.env, + autumn: this.autumnJs, + attachPm: "success", }); + customer = res.customer; }); it("should attach premium group 1 and premium group 2", async function () { @@ -121,14 +125,6 @@ describe("Multi Product 3: premium1->starter1, premium2->starter2, then premium2 }); }); - // it("should upgrade to premium group 1 again, then downgrade back to starter group 1", async function () { - // await AutumnCli.attach({ - // customerId: customerId, - // productId: attachProducts.premiumGroup1.id, - // }); - - // }); - it("should downgrade to free", async function () { await AutumnCli.attach({ customerId: customerId, @@ -160,15 +156,3 @@ describe("Multi Product 3: premium1->starter1, premium2->starter2, then premium2 }); }); }); - -// Advance test clock -// 1. Premium 1 -> Starter 1, Premium 2 -> Starter 2, Advance Test Clock [OK] -// 2. Premium 1 -> Starter 1, Premium 2, Advance Test Clock -// 3. Premium 1 -> Starter 1, Premium 2 -> Free, Advance Test Clock - -// EXPIRE BUTTON -// 1. Premium 1 -> Starter 1, Premium 2 -> Starter 2, Expire Starter 1, Expire Starter 2 -// 2. Premium 1 -> Starter 1, Premium 2 -> Starter 2, Cancel Premium 1, or 2 immediately... - -// UPGRADES -// 1. Premium 1 -> Starter 1, Premium 2 -> Starter 2, Upgrade to Starter 1 diff --git a/server/tests/attach_old/01_multi_product4.ts b/server/tests/attach/multiProduct/multiProduct3.ts similarity index 80% rename from server/tests/attach_old/01_multi_product4.ts rename to server/tests/attach/multiProduct/multiProduct3.ts index 5478a2b84..f00b5205b 100644 --- a/server/tests/attach_old/01_multi_product4.ts +++ b/server/tests/attach/multiProduct/multiProduct3.ts @@ -1,51 +1,47 @@ -import { createStripeCli } from "@/external/stripe/utils.js"; -import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js"; - -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +import chalk from "chalk"; import { expect } from "chai"; import { AutumnCli } from "tests/cli/AutumnCli.js"; import { attachProducts } from "tests/global.js"; import { compareMainProduct } from "tests/utils/compare.js"; -import { initCustomer } from "tests/utils/init.js"; import { searchCusProducts, timeout } from "tests/utils/genUtils.js"; -import chalk from "chalk"; + +import { createStripeCli } from "@/external/stripe/utils.js"; +import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { setupBefore } from "tests/before.js"; +import Stripe from "stripe"; // TESTING DOWNGRADE DOWNGRADE THEN // 1. UPGRADE FIRST PRODUCT BACK -- SHOULD REPLACE SCHEDULE WITH OLD FIRST PRODUCT // 2. UPGRADE SECOND PRODUCT BACK -- SHOULD CANCEL SCHEDULE +const testCase = "multiProduct3"; describe( - chalk.yellowBright( - "Multi Product 4: double downgrade, double upgrade (back)", - ), + chalk.yellowBright(`${testCase}: double downgrade, double upgrade (back)`), () => { - let customerId = "multi-double-downgrade-upgrade"; + let customerId = testCase; let customer; - - let stripeCli; + let stripeCli: Stripe; before(async function () { - customer = await initCustomer({ + await setupBefore(this); + stripeCli = this.stripeCli; + const res = await initCustomer({ db: this.db, org: this.org, env: this.env, - customer_data: { - id: customerId, - name: customerId, - email: "multi-product-upgrade-test@example.com", - }, - attachPm: true, + customerId, + autumn: this.autumnJs, + attachPm: "success", }); - stripeCli = createStripeCli({ - org: this.org, - env: this.env, - }); + customer = res.customer; }); it("should attach premium group 1 and premium group 2", async function () { - let res = await AutumnCli.attach({ + await AutumnCli.attach({ customerId: customerId, productIds: [ attachProducts.premiumGroup1.id, @@ -59,7 +55,7 @@ describe( }); it("should attach starter group 1, then starter group 2", async function () { - let res = await AutumnCli.attach({ + await AutumnCli.attach({ customerId: customerId, productId: attachProducts.starterGroup1.id, }); @@ -71,20 +67,18 @@ describe( }); it("should reattach premium group 1", async function () { - await timeout(3000); await AutumnCli.attach({ customerId: customerId, productId: attachProducts.premiumGroup1.id, }); - // Check that schedule contains premium group 1, and starter group 2... + await timeout(10000); const cusProducts = await CusProductService.list({ db: this.db, internalCustomerId: customer!.internal_id, }); - // 1. Check that premium group 1 is active, and had scheduled_ids let premiumGroup1 = searchCusProducts({ cusProducts, productId: attachProducts.premiumGroup1.id, @@ -119,7 +113,7 @@ describe( productId: attachProducts.premiumGroup2.id, }); - await timeout(5000); + await timeout(10000); const cusProducts = await CusProductService.list({ db: this.db, @@ -157,12 +151,5 @@ describe( expect(sub.cancel_at).to.equal(null); expect(sub.status).to.equal("active"); }); - - // it("should reattach premium group 2", async function () { - // let res = await AutumnCli.attach({ - // customerId: this.customer.id, - // productId: attachProducts.premiumGroup2.id, - // }); - // }); }, ); diff --git a/server/tests/attach/others/others4.ts b/server/tests/attach/others/others4.ts new file mode 100644 index 000000000..fa90a8341 --- /dev/null +++ b/server/tests/attach/others/others4.ts @@ -0,0 +1,161 @@ +import { assert } from "chai"; +import { features, products } from "tests/global.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { getPublicAxiosInstance } from "tests/utils/setup.js"; +import { completeCheckoutForm } from "tests/utils/stripeUtils.js"; +import { timeout } from "tests/utils/genUtils.js"; +import { ErrCode } from "@autumn/shared"; +import { compareMainProduct } from "tests/utils/compare.js"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import chalk from "chalk"; +import { setupBefore } from "tests/before.js"; + +const testCase = "others4"; +describe(`${chalk.yellowBright("others4: Testing publishable key")}`, () => { + // 1. Initialize customer with card + let customerId = testCase; + const bearerPublicAxios = getPublicAxiosInstance({ + withBearer: true, + }); + + before(async function () { + await setupBefore(this); + await initCustomer({ + customerId, + db: this.db, + org: this.org, + env: this.env, + autumn: this.autumnJs, + }); + }); + + it("should return a 401 if the pkey is invalid", async function () { + this.timeout(30000); + const axiosInstance = getPublicAxiosInstance({ + withBearer: true, + pkey: "am_pk_test_invalid", + }); + + try { + const { data } = await axiosInstance.post("/v1/attach", { + customer_id: customerId, + product_id: products.pro.id, + }); + + throw new Error("Should not be able to attach"); + } catch (error: any) { + assert.equal(error.response.status, 401); + } + }); + + it("should return checkout URL for both bearer key", async function () { + this.timeout(30000); + const axiosInstanceBearer = getPublicAxiosInstance({ + withBearer: true, + }); + + // 1. Should be able to upgrade to pro + const { data } = await axiosInstanceBearer.post("/v1/attach", { + customer_id: customerId, + product_id: products.pro.id, + }); + + assert.exists(data.checkout_url); + + await completeCheckoutForm(data.checkout_url); + + await timeout(5000); + }); + + it("should have customer with product", async function () { + this.timeout(30000); + const res = await AutumnCli.getCustomer(customerId); + compareMainProduct({ + sent: products.pro, + cusRes: res, + }); + }); + + it("should return error if try to upgrade or downgrade without pkey", async function () { + this.timeout(30000); + const axiosInstance = getPublicAxiosInstance({ + withBearer: true, + }); + + try { + await axiosInstance.post("/v1/attach", { + customer_id: customerId, + product_id: products.premium.id, + }); + + throw new Error("Should not be able to attach"); + } catch (error: any) { + assert.equal(error.response.status, 400); + assert.equal(error.response.data.code, ErrCode.InvalidRequest); + } + }); + + it("should return error if try to downgrade to free", async function () { + try { + await bearerPublicAxios.post("/v1/attach", { + customer_id: customerId, + product_id: products.free.id, + }); + + throw new Error("Should not be able to attach"); + } catch (error: any) { + assert.equal(error.response.status, 400); + assert.equal(error.response.data.code, ErrCode.InvalidRequest); + } + }); + + // Next, check entitled for pro + it("should return correct metered1 amount for pro", async function () { + const { data } = await bearerPublicAxios.post("/v1/entitled", { + customer_id: customerId, + feature_id: features.metered1.id, + }); + + assert.equal(data.allowed, true); + const metered1Balance = data.balances.find( + (b: any) => b.feature_id === features.metered1.id, + ); + assert.equal( + metered1Balance.balance, + products.pro.entitlements.metered1.allowance, + ); + }); + + it("should return same balance for entitled with bearer and x-publishable-key", async function () { + const { data } = await bearerPublicAxios.post("/v1/entitled", { + customer_id: customerId, + feature_id: features.metered1.id, + }); + + assert.equal(data.allowed, true); + const metered1Balance = data.balances.find( + (b: any) => b.feature_id === features.metered1.id, + ); + assert.equal( + metered1Balance.balance, + products.pro.entitlements.metered1.allowance, + ); + }); + + it("should return error when try to send event", async function () { + try { + await bearerPublicAxios.post("/v1/events", { + customer_id: customerId, + event_name: features.metered1.id, + properties: { + value: 10, + }, + }); + + throw new Error("Should not be able to send event"); + } catch (error: any) { + assert.equal(error.response.status, 401); + assert.equal(error.response.data.code, ErrCode.EndpointNotPublic); + } + }); +}); diff --git a/server/tests/attach/others/others5.ts b/server/tests/attach/others/others5.ts new file mode 100644 index 000000000..8d1d1347c --- /dev/null +++ b/server/tests/attach/others/others5.ts @@ -0,0 +1,243 @@ +import chalk from "chalk"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { features, products } from "tests/global.js"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { timeout } from "../../utils/genUtils.js"; +import { expect } from "chai"; +import { setupBefore } from "tests/before.js"; + +const checkEntitledOnProduct = async ({ + customerId, + product, + totalAllowance, + finish = false, + usageBased = false, +}: { + customerId: string; + product: any; + totalAllowance?: number; + finish?: boolean; + usageBased?: boolean; +}) => { + // 1. Send events + const allowance = totalAllowance || product.entitlements.metered1.allowance; + // const randomNum = Math.floor(Math.random() * (allowance - 1)); + const randomNum = 3; + + const batchUpdates = []; + for (let i = 0; i < randomNum; i++) { + batchUpdates.push( + AutumnCli.sendEvent({ + customerId: customerId, + eventName: features.metered1.eventName, + }), + ); + } + + await Promise.all(batchUpdates); + await timeout(8000); + let used = randomNum; + + // 2. Check entitled + const { allowed, balanceObj }: any = await AutumnCli.entitled( + customerId, + features.metered1.id, + true, + ); + + try { + expect(allowed).to.be.true; + expect(balanceObj!.balance).to.equal(allowance - randomNum); + + if (!finish) { + return used; + } + } catch (error) { + console.group(); + console.group(); + console.log("Allowance: ", allowance, "Random num: ", randomNum); + console.log("Expected balance to be: ", allowance - randomNum); + console.log("Entitled res: ", { allowed, balanceObj }); + console.groupEnd(); + console.groupEnd(); + throw error; + } + + // Finish up + const batchUpdates2 = []; + for (let i = 0; i < allowance - randomNum; i++) { + batchUpdates2.push( + AutumnCli.sendEvent({ + customerId: customerId, + eventName: features.metered1.eventName, + }), + ); + } + await Promise.all(batchUpdates2); + await timeout(8000); + used += allowance - randomNum; + + // 3. Check entitled again + const { allowed: allowed2, balanceObj: balanceObj2 }: any = + await AutumnCli.entitled(customerId, features.metered1.id, true); + try { + if (usageBased) { + expect(allowed2).to.be.true; + } else { + expect(allowed2).to.be.false; + } + expect(balanceObj2!.balance).to.equal(0); + return used; + } catch (error) { + console.group(); + console.group(); + console.log("Expected balance to be: ", 0); + console.log("Entitled res: ", { allowed2, balanceObj2 }); + console.groupEnd(); + console.groupEnd(); + throw error; + } +}; + +// TODO: Add test case for unlimited feature + +const testCase = "others5"; +describe(`${chalk.yellowBright( + "others5: Testing /events and /entitled, for pro, one time top up", +)}`, () => { + const customerId = testCase; + + let curAllowance = 0; + const oneTimeBillingUnits = + products.oneTimeAddOnMetered1.prices[0].config.billing_units!; + let oneTimeQuantity = 2 * oneTimeBillingUnits; + + before(async function () { + await setupBefore(this); + await initCustomer({ + customerId, + db: this.db, + org: this.org, + env: this.env, + autumn: this.autumnJs, + attachPm: "success", + }); + }); + + it("should have correct entitlements (free)", async function () { + await checkEntitledOnProduct({ + customerId: customerId, + product: products.free, + finish: true, + }); + }); + + it("should attach pro", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: products.pro.id, + }); + }); + + it("should have correct entitlements (pro)", async function () { + const used = await checkEntitledOnProduct({ + customerId: customerId, + product: products.pro, + finish: false, + }); + + curAllowance = products.pro.entitlements.metered1.allowance! - used; + }); + + it("should attach one time top up", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: products.oneTimeAddOnMetered1.id, + options: [ + { + feature_id: features.metered1.id, + quantity: oneTimeQuantity, + }, + ], + }); + }); + + it("should have correct entitlements (one time top up)", async function () { + // const oneTimeAmt = oneTimeBillingUnits * oneTimeQuantity; + + await checkEntitledOnProduct({ + customerId: customerId, + product: products.oneTimeAddOnMetered1, + finish: true, + totalAllowance: curAllowance + oneTimeQuantity, + }); + }); +}); + +describe(`${chalk.yellowBright( + "others5: Testing /entitled & /events, for pro with overage", +)}`, () => { + const customerId = testCase; + + before(async function () { + await setupBefore(this); + await initCustomer({ + customerId, + db: this.db, + org: this.org, + env: this.env, + autumn: this.autumnJs, + attachPm: "success", + }); + }); + + // PRO WITH OVERAGE + it("should attach pro (with overage)", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: products.proWithOverage.id, + }); + }); + + it("should have correct entitlements (pro with overage)", async function () { + await checkEntitledOnProduct({ + customerId: customerId, + product: products.proWithOverage, + finish: true, + totalAllowance: products.proWithOverage.entitlements.metered1.allowance!, + usageBased: true, + }); + }); + + it("should have correct usage-based balance (balance < 0)", async function () { + const { allowed, balanceObj }: any = await AutumnCli.entitled( + customerId, + features.metered1.id, + true, + ); + + expect(allowed).to.be.true; + expect(balanceObj!.balance).to.equal(0); + + // Sent 5 events + const batchUpdates = []; + for (let i = 0; i < 5; i++) { + batchUpdates.push( + AutumnCli.sendEvent({ + customerId: customerId, + eventName: features.metered1.eventName, + }), + ); + } + + await Promise.all(batchUpdates); + await timeout(5000); + + const { allowed: allowed2, balanceObj: balanceObj2 }: any = + await AutumnCli.entitled(customerId, features.metered1.id, true); + + expect(allowed2).to.be.true; + expect(balanceObj2!.balance).to.equal(-5); + expect(balanceObj2!.usage_allowed).to.be.true; + }); +}); diff --git a/server/tests/attach/updateEnts/expectUpdateEnts.ts b/server/tests/attach/updateEnts/expectUpdateEnts.ts index 017ff2bd4..8f8be70e8 100644 --- a/server/tests/attach/updateEnts/expectUpdateEnts.ts +++ b/server/tests/attach/updateEnts/expectUpdateEnts.ts @@ -65,6 +65,7 @@ const runUpdateEntsTest = async ({ expect(preview.branch).to.equal(AttachBranch.NewVersion); } else { expect(preview.branch).to.equal(AttachBranch.SameCustomEnts); + expect(preview.due_today).to.be.undefined; } await autumn.attach({ diff --git a/server/tests/attach/updateEnts/updateEnts1.ts b/server/tests/attach/updateEnts/updateEnts1.ts index 9664ebcf7..6e46aa039 100644 --- a/server/tests/attach/updateEnts/updateEnts1.ts +++ b/server/tests/attach/updateEnts/updateEnts1.ts @@ -30,17 +30,6 @@ export let pro = constructProduct({ type: "pro", }); -/** - * upgrade3: - * Testing upgrades for arrear prorated - * 1. Start with pro monthly plan (usage-based) - * 2. Upgrade to pro annual plan (usage-based) - * 3. Upgrade to premium annual plan (usage-based) - * - * Verifies subscription items and anchors are correct after each upgrade - * with arrear prorated billing - */ - describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing included usage)`)}`, () => { let customerId = testCase; let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); @@ -60,15 +49,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing inclu stripeCli = this.stripeCli; - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - addPrefixToProducts({ products: [pro], prefix: testCase, @@ -83,10 +63,19 @@ describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing inclu customerId, }); + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + testClockId = testClockId1!; }); - it("should attach pro product (prepaid single use)", async function () { + it("should attach pro product", async function () { await runAttachTest({ autumn, customerId, @@ -112,7 +101,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing inclu let usage = 50000; let overage = 50000 - (newItem.included_usage as number); - it("should attach custom pro product", async function () { + it("should update overage item to have new included usage", async function () { const customProduct = { ...pro, items: customItems, @@ -144,7 +133,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing inclu }); }); - it("should have correct invoice usage next cycle", async function () { + it("should have correct invoice next cycle", async function () { const invoiceTotal = await getExpectedInvoiceTotal({ org, env, @@ -175,7 +164,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing inclu }); const customer = await autumn.customers.get(customerId); - const invoice = customer.invoices[0]; + const invoice = customer.invoices![0]; expect(invoice.total).to.equal( invoiceTotal, "invoice total after 1 cycle should be correct", diff --git a/server/tests/attach/updateEnts/updateEnts2.ts b/server/tests/attach/updateEnts/updateEnts2.ts index 97488be37..149354ec9 100644 --- a/server/tests/attach/updateEnts/updateEnts2.ts +++ b/server/tests/attach/updateEnts/updateEnts2.ts @@ -183,7 +183,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing inclu }); const customer = await autumn.customers.get(customerId); - const invoice = customer.invoices[0]; + const invoice = customer.invoices![0]; expect(invoice.total).to.equal( invoiceTotal, "invoice total after 1 cycle should be correct", diff --git a/server/tests/attach/updateQuantity/updateQuantity1.ts b/server/tests/attach/updateQuantity/updateQuantity1.ts index d2e1d0a68..1520121b9 100644 --- a/server/tests/attach/updateQuantity/updateQuantity1.ts +++ b/server/tests/attach/updateQuantity/updateQuantity1.ts @@ -1,5 +1,5 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; + import { APIVersion, AppEnv, @@ -19,6 +19,7 @@ import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { addWeeks } from "date-fns"; import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; import { timeout } from "@/utils/genUtils.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; const testCase = "updateQuantity1"; @@ -33,17 +34,6 @@ export let pro = constructProduct({ type: "pro", }); -/** - * upgrade3: - * Testing upgrades for arrear prorated - * 1. Start with pro monthly plan (usage-based) - * 2. Upgrade to pro annual plan (usage-based) - * 3. Upgrade to premium annual plan (usage-based) - * - * Verifies subscription items and anchors are correct after each upgrade - * with arrear prorated billing - */ - describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid single use`)}`, () => { let customerId = testCase; let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); @@ -80,6 +70,9 @@ describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid singl await createProducts({ autumn, products: [pro], + db, + orgId: org.id, + env, }); testClockId = testClockId1!; @@ -110,40 +103,40 @@ describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid singl errCode: AttachErrCode.ProductAlreadyAttached, func: async () => { await autumn.attach({ - customerId, - productId: pro.id, + customer_id: customerId, + product_id: pro.id, options: proOpts, }); }, }); }); - const newOpts = [ - { - feature_id: TestFeature.Users, - quantity: 1, - }, - ]; - it("should throw error if try to reduce seats to less than current usage", async function () { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: 2, - }); + // const newOpts = [ + // { + // feature_id: TestFeature.Users, + // quantity: 1, + // }, + // ]; + // it("should throw error if try to reduce seats to less than current usage", async function () { + // await autumn.track({ + // customer_id: customerId, + // feature_id: TestFeature.Users, + // value: 2, + // }); - await timeout(1000); + // await timeout(1000); - await expectAutumnError({ - errCode: AttachErrCode.InvalidOptions, - func: async () => { - await autumn.attach({ - customerId, - productId: pro.id, - options: newOpts, - }); - }, - }); - }); + // await expectAutumnError({ + // errCode: AttachErrCode.InvalidOptions, + // func: async () => { + // await autumn.attach({ + // customer_id: customerId, + // product_id: pro.id, + // options: newOpts, + // }); + // }, + // }); + // }); const updatedOpts = [ { diff --git a/server/tests/attach/upgrade/upgrade1.ts b/server/tests/attach/upgrade/upgrade1.ts index 8c3e18192..c5354ca28 100644 --- a/server/tests/attach/upgrade/upgrade1.ts +++ b/server/tests/attach/upgrade/upgrade1.ts @@ -32,7 +32,7 @@ let growth = constructProduct({ type: "growth", }); -describe(`${chalk.yellowBright("attach/upgrade1: Testing usage upgrades")}`, () => { +describe(`${chalk.yellowBright("upgrade1: Testing usage upgrades")}`, () => { let customerId = "upgrade1"; let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); diff --git a/server/tests/attach/upgrade/upgrade2.ts b/server/tests/attach/upgrade/upgrade2.ts index 9b138210b..638caa123 100644 --- a/server/tests/attach/upgrade/upgrade2.ts +++ b/server/tests/attach/upgrade/upgrade2.ts @@ -6,17 +6,11 @@ import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; - import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { - APIVersion, - AppEnv, - FullCusProduct, - Organization, -} from "@autumn/shared"; +import { APIVersion, AppEnv, Organization } from "@autumn/shared"; import { addPrefixToProducts, runAttachTest } from "../utils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { addWeeks, getDate } from "date-fns"; +import { addWeeks } from "date-fns"; import { DrizzleCli } from "@/db/initDrizzle.js"; @@ -51,7 +45,7 @@ export let premiumAnnual = constructProduct({ * Verifies subscription items and anchors are correct after each upgrade */ -describe(`${chalk.yellowBright("attach/upgrade2: Testing usage upgrades with monthly -> annual")}`, () => { +describe(`${chalk.yellowBright("upgrade2: Testing usage upgrades with monthly -> annual")}`, () => { let customerId = "upgrade2"; let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); let stripeCli: Stripe; @@ -153,6 +147,7 @@ describe(`${chalk.yellowBright("attach/upgrade2: Testing usage upgrades with mon db, org, env, + singleInvoice: true, }); }); }); diff --git a/server/tests/attach/upgrade/upgrade5.ts b/server/tests/attach/upgrade/upgrade5.ts index d805b5e11..fc2865759 100644 --- a/server/tests/attach/upgrade/upgrade5.ts +++ b/server/tests/attach/upgrade/upgrade5.ts @@ -29,7 +29,7 @@ export let pro = constructProduct({ export let premium = constructProduct({ items: [ constructPrepaidItem({ - featureId: TestFeature.Users, + featureId: TestFeature.Messages, price: 8, billingUnits: 100, }), @@ -37,29 +37,6 @@ export let premium = constructProduct({ type: "premium", }); -export let proAnnual = constructProduct({ - items: [ - constructPrepaidItem({ - featureId: TestFeature.Users, - price: 12, - billingUnits: 1, - }), - ], - type: "pro", - isAnnual: true, -}); - -/** - * upgrade3: - * Testing upgrades for arrear prorated - * 1. Start with pro monthly plan (usage-based) - * 2. Upgrade to pro annual plan (usage-based) - * 3. Upgrade to premium annual plan (usage-based) - * - * Verifies subscription items and anchors are correct after each upgrade - * with arrear prorated billing - */ - describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid single use`)}`, () => { let customerId = testCase; let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); @@ -89,13 +66,13 @@ describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid singl }); addPrefixToProducts({ - products: [pro, premium, proAnnual], + products: [pro, premium], prefix: testCase, }); await createProducts({ autumn, - products: [pro, premium, proAnnual], + products: [pro, premium], db, orgId: org.id, env, @@ -136,7 +113,9 @@ describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid singl stripeCli, testClockId, advanceTo: addWeeks(curUnix, 1).getTime(), + waitForSeconds: 20, }); + await runAttachTest({ autumn, customerId, diff --git a/server/tests/attach/upgrade/upgrade6.ts b/server/tests/attach/upgrade/upgrade6.ts index b678fee6c..cfb6fcaee 100644 --- a/server/tests/attach/upgrade/upgrade6.ts +++ b/server/tests/attach/upgrade/upgrade6.ts @@ -10,12 +10,10 @@ import { addPrefixToProducts, runAttachTest } from "../utils.js"; import { constructArrearItem, constructArrearProratedItem, - constructPrepaidItem, } from "@/utils/scriptUtils/constructItem.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { addWeeks } from "date-fns"; + import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; import { CusService } from "@/internal/customers/CusService.js"; @@ -55,9 +53,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing failed upgrades`)}`, () => let db: DrizzleCli, org: Organization, env: AppEnv; let stripeCli: Stripe; - let curUnix = new Date().getTime(); - let numUsers = 0; - before(async function () { await setupBefore(this); const { autumnJs } = this; @@ -121,6 +116,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing failed upgrades`)}`, () => }); await attachFailedPaymentMethod({ stripeCli, customer: cus! }); + await timeout(2000); await expectAutumnError({ func: async () => { diff --git a/server/tests/attach/upgradeOld/upgradeOld1.ts b/server/tests/attach/upgradeOld/upgradeOld1.ts new file mode 100644 index 000000000..38a33eb59 --- /dev/null +++ b/server/tests/attach/upgradeOld/upgradeOld1.ts @@ -0,0 +1,75 @@ +import chalk from "chalk"; +import { products } from "tests/global.js"; +import { assert } from "chai"; +import { Customer } from "@autumn/shared"; +import { compareMainProduct } from "tests/utils/compare.js"; +import { addDays } from "date-fns"; +import { setupBefore } from "tests/before.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; +import Stripe from "stripe"; + +describe(`${chalk.yellowBright( + "upgradeOld1: Testing upgrade (trial to paid)", +)}`, () => { + const customerId = "upgradeOld1"; + let testClockId: string; + let customer: Customer; + let autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + stripeCli = this.stripeCli; + const { customer: customer_, testClockId: testClockId_ } = + await initCustomer({ + autumn: this.autumnJs, + customerId, + db: this.db, + org: this.org, + env: this.env, + attachPm: "success", + }); + + customer = customer_; + testClockId = testClockId_; + }); + + it("should attach pro with trial", async function () { + await autumn.attach({ + customer_id: customerId, + product_id: products.proWithTrial.id, + }); + }); + + it("should attach premium", async function () { + await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addDays(new Date(), 3).getTime(), + waitForSeconds: 10, + }); + + await autumn.attach({ + customer_id: customerId, + product_id: products.premium.id, + }); + }); + + it("should check product, ents and invoices", async function () { + const res = await autumn.customers.get(customerId); + compareMainProduct({ + sent: products.premium, + cusRes: res, + }); + + const invoices = await res.invoices; + + assert.equal( + invoices[0].total, + products.premium.prices[0].config.amount, + "Invoice should be for 50.00", + ); + }); +}); diff --git a/server/tests/attach/upgradeOld/upgradeOld2.ts b/server/tests/attach/upgradeOld/upgradeOld2.ts new file mode 100644 index 000000000..421b6ca35 --- /dev/null +++ b/server/tests/attach/upgradeOld/upgradeOld2.ts @@ -0,0 +1,52 @@ +import Stripe from "stripe"; +import chalk from "chalk"; +import { Customer } from "@autumn/shared"; +import { setupBefore } from "tests/before.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { products } from "tests/global.js"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; + +describe(`${chalk.yellowBright( + "upgradeOld2: Testing upgrade (paid to trial)", +)}`, () => { + const customerId = "upgradeOld2"; + let testClockId: string; + let customer: Customer; + let autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + stripeCli = this.stripeCli; + const { customer: customer_, testClockId: testClockId_ } = + await initCustomer({ + autumn: this.autumnJs, + customerId, + db: this.db, + org: this.org, + env: this.env, + attachPm: "success", + }); + + customer = customer_; + testClockId = testClockId_; + }); + + it("should attach pro", async function () { + this.timeout(30000); + await autumn.attach({ + customer_id: customerId, + product_id: products.pro.id, + }); + }); + + it("should attach premium with trial and have trial", async function () { + this.timeout(30000); + + await autumn.attach({ + customer_id: customerId, + product_id: products.premiumWithTrial.id, + }); + }); +}); diff --git a/server/tests/attach/upgradeOld/upgradeOld3.ts b/server/tests/attach/upgradeOld/upgradeOld3.ts new file mode 100644 index 000000000..23d176ed9 --- /dev/null +++ b/server/tests/attach/upgradeOld/upgradeOld3.ts @@ -0,0 +1,72 @@ +import assert from "assert"; +import chalk from "chalk"; +import { CusProductStatus } from "@autumn/shared"; +import { addDays } from "date-fns"; +import { setupBefore } from "tests/before.js"; +import { compareMainProduct } from "tests/utils/compare.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { products } from "tests/global.js"; +import Stripe from "stripe"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; + +describe(`${chalk.yellowBright("upgradeOld3: Testing upgrade (trial to trial)")}`, () => { + const customerId = "upgradeOld3"; + let testClockId: string; + let autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + before(async function () { + await setupBefore(this); + stripeCli = this.stripeCli; + const { customer: customer_, testClockId: testClockId_ } = + await initCustomer({ + autumn: this.autumnJs, + customerId, + db: this.db, + org: this.org, + env: this.env, + attachPm: "success", + }); + + testClockId = testClockId_; + }); + + it("should attach pro with trial", async function () { + this.timeout(30000); + await autumn.attach({ + customer_id: customerId, + product_id: products.proWithTrial.id, + }); + + console.log(` ${chalk.greenBright("Attached pro with trial")}`); + }); + + it("should attach premium with trial", async function () { + const advanceTo = addDays(new Date(), 3).getTime(); + + await advanceTestClock({ + stripeCli, + testClockId, + advanceTo, + waitForSeconds: 10, + }); + + await autumn.attach({ + customer_id: customerId, + product_id: products.premiumWithTrial.id, + }); + }); + + it("should check product and ents", async function () { + const res = await autumn.customers.get(customerId); + compareMainProduct({ + sent: products.premiumWithTrial, + cusRes: res, + status: CusProductStatus.Trialing, + }); + + const invoices = res.invoices; + + assert.equal(invoices![0].total, 0, "Invoice should be 0"); + }); +}); diff --git a/server/tests/attach/upgradeOld/upgradeOld4.ts b/server/tests/attach/upgradeOld/upgradeOld4.ts new file mode 100644 index 000000000..208e4296c --- /dev/null +++ b/server/tests/attach/upgradeOld/upgradeOld4.ts @@ -0,0 +1,116 @@ +// TESTING UPGRADES + +import chalk from "chalk"; +import { AppEnv, Organization } from "@autumn/shared"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { products } from "tests/global.js"; +import { + attachFailedPaymentMethod, + attachPmToCus, +} from "@/external/stripe/stripeCusUtils.js"; +import { Customer } from "@autumn/shared"; +import { compareMainProduct } from "tests/utils/compare.js"; +import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import Stripe from "stripe"; +import { setupBefore } from "tests/before.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; + +const testCase = "upgradeOld4"; +describe(`${chalk.yellowBright("upgradeOld4: Testing upgrade from pro -> premium")}`, () => { + let customer: Customer; + let customerId = testCase; + + let stripeCli: Stripe; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + let autumn: AutumnInt = new AutumnInt(); + + before(async function () { + await setupBefore(this); + + stripeCli = this.stripeCli; + db = this.db; + org = this.org; + env = this.env; + + let { customer: customer_ } = await initCustomer({ + autumn: this.autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + customer = customer_; + }); + + it("should attach pro (trial)", async function () { + await autumn.attach({ + customer_id: customerId, + product_id: products.pro.id, + }); + + let res = await autumn.customers.get(customerId); + compareMainProduct({ + sent: products.pro, + cusRes: res, + }); + }); + + // 1. Try force checkout... + it("should attach premium and not be able to force checkout", async function () { + expectAutumnError({ + func: async () => { + await autumn.attach({ + customer_id: customerId, + product_id: products.premium.id, + force_checkout: true, + }); + }, + }); + }); + + it("should attach premium and not be able to upgrade (without payment method)", async function () { + await attachFailedPaymentMethod({ + stripeCli: stripeCli, + customer: customer, + }); + + await expectAutumnError({ + func: async () => { + await autumn.attach({ + customer_id: customerId, + product_id: products.premium.id, + force_checkout: true, + }); + }, + }); + }); + + // Attach payment method + it("should attach successful payment method", async function () { + await attachPmToCus({ + db: this.db, + customer: customer, + org: this.org, + env: this.env, + }); + }); + + it("should attach premium and have correct product and entitlements", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: products.premium.id, + }); + + const res = await AutumnCli.getCustomer(customerId); + compareMainProduct({ + sent: products.premium, + cusRes: res, + }); + }); +}); diff --git a/server/tests/attach/utils.ts b/server/tests/attach/utils.ts index 17d96eb43..8ad59ffaf 100644 --- a/server/tests/attach/utils.ts +++ b/server/tests/attach/utils.ts @@ -44,6 +44,7 @@ export const runAttachTest = async ({ waitForInvoice = 0, isCanceled = false, skipFeatureCheck = false, + singleInvoice = false, }: { autumn: AutumnInt; customerId: string; @@ -61,6 +62,7 @@ export const runAttachTest = async ({ waitForInvoice?: number; isCanceled?: boolean; skipFeatureCheck?: boolean; + singleInvoice?: boolean; }) => { const preview = await autumn.attachPreview({ customer_id: customerId, @@ -110,10 +112,11 @@ export const runAttachTest = async ({ const freeProduct = isFreeProductV2({ product }); if (!freeProduct) { + let multiInvoice = !singleInvoice && multiInterval; expectInvoicesCorrect({ customer, - first: multiInterval ? undefined : { productId: product.id, total }, - second: multiInterval ? { productId: product.id, total } : undefined, + first: multiInvoice ? undefined : { productId: product.id, total }, + second: multiInvoice ? { productId: product.id, total } : undefined, }); } @@ -141,7 +144,7 @@ export const runAttachTest = async ({ }); const stripeSubs = await stripeCli.subscriptions.list({ - customer: customer.stripe_id, + customer: customer.stripe_id!, }); if (multiInterval) { expect(stripeSubs.data.length).to.equal(2, "should have 2 subscriptions"); diff --git a/server/tests/attach_old/01_multi_interval3.ts b/server/tests/attach_old/01_multi_interval3.ts deleted file mode 100644 index a73fda55f..000000000 --- a/server/tests/attach_old/01_multi_interval3.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { createStripeCli } from "@/external/stripe/utils.js"; -import { addMonths } from "date-fns"; -import Stripe from "stripe"; -import { AutumnCli } from "tests/cli/AutumnCli.js"; -import { advanceProducts } from "tests/global.js"; -import { - checkProductIsScheduled, - compareMainProduct, -} from "tests/utils/compare.js"; -import { initCustomer } from "tests/utils/init.js"; -import { advanceClockForInvoice } from "tests/utils/stripeUtils.js"; -import { advanceMonths } from "tests/utils/stripeUtils.js"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; - -// TEST MULTI INTERVAL DOWNGRADE -// -/* -CASE 1: Annual pro -> Annual starter - - If attach annual starter, should schedule correctly [DONE] - - If advance test clock, should downgrade correctly (to monthly starter) [DONE] - - If cancel active subscription (on Stripe), should remove scheduled correctly [DONE] - - If cancel scheduled subscription (on Stripe), should remove scheduled correctly [DONE] - - If expire on dashboard, should remove scheduled correctly - - - If upgrade back to annual pro, should remove scheduled correctly [DONE] - - If downgrade to monthly pro (switch downgrade), should be correct [DONE] - - If downgrade to free (switch downgrade), should be correct -*/ - -describe.skip("Multi interval downgrade -- Quarterly pro -> Monthly pro", () => { - let customerId = "multi-interval-downgrade"; - let stripeCli: Stripe; - let testClockId: string; - - before(async function () { - const { testClockId: insertedTestClockId } = - await initCustomerWithTestClock({ - customerId, - org: this.org, - env: this.env, - db: this.db, - }); - testClockId = insertedTestClockId; - stripeCli = createStripeCli({ - org: this.org, - env: this.env, - }); - }); - - it("should attach quarterly pro", async function () { - let res = await AutumnCli.attach({ - customerId: customerId, - productId: advanceProducts.gpuProQuarter.id, - }); - - let cusRes = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: advanceProducts.gpuProQuarter, - cusRes, - }); - }); - - it("should attach downgrade to monthly pro", async function () { - let res = await AutumnCli.attach({ - customerId: customerId, - productId: advanceProducts.gpuSystemPro.id, - }); - - let cusRes = await AutumnCli.getCustomer(customerId); - checkProductIsScheduled({ - cusRes, - product: advanceProducts.gpuSystemPro, - }); - }); - - it("should advance clock by a year", async function () { - // let numberOfMonths = 2; - // await advanceMonths({ - // stripeCli, - // testClockId, - // numberOfMonths, - // }); - }); -}); - -describe("Multi interval downgrade -- Annual pro -> Annual starter", () => { - let customerId = "multi-interval-downgrade"; - let customer; - let stripeCli; - let testClockId: string; - - before(async function () { - await initCustomer({ - customer_data: { - id: customerId, - name: customerId, - email: "multi-interval-downgrade@example.com", - }, - attachPm: true, - org: this.org, - env: this.env, - db: this.db, - }); - }); - - it("should attach annual pro", async function () { - let res = await AutumnCli.attach({ - customerId: customerId, - productId: advanceProducts.gpuProAnnual.id, - }); - - let cusRes = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: advanceProducts.gpuProAnnual, - cusRes, - }); - }); - - it("should attach downgrade to annual starter", async function () { - let res = await AutumnCli.attach({ - customerId: customerId, - productId: advanceProducts.gpuStarterAnnual.id, - }); - - let cusRes = await AutumnCli.getCustomer(customerId); - checkProductIsScheduled({ - cusRes, - product: advanceProducts.gpuStarterAnnual, - }); - }); -}); diff --git a/server/tests/basic/03_cancel.ts b/server/tests/basic/03_cancel.ts deleted file mode 100644 index 16e37857c..000000000 --- a/server/tests/basic/03_cancel.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { createStripeCli } from "@/external/stripe/utils.js"; -import { AutumnCli } from "../cli/AutumnCli.js"; -import { features, products } from "../global.js"; -import { initCustomer } from "../utils/init.js"; -import { timeout } from "../utils/genUtils.js"; -import chalk from "chalk"; -import { - checkFeatureHasCorrectBalance, - compareMainProduct, - compareProductEntitlements, -} from "../utils/compare.js"; -import { expect } from "chai"; -import { - advanceTestClock, - completeCheckoutForm, -} from "../utils/stripeUtils.js"; -import { CusProductStatus } from "@autumn/shared"; -import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; -import { addDays, addMonths } from "date-fns"; - -describe(`${chalk.yellowBright( - "03_cancel: Testing cancel (at period end and now)", -)}`, () => { - const customerId = "cancelCustomer"; - - before(async function () { - this.timeout(30000); - - await initCustomer({ - customer_data: { - id: customerId, - name: "Test Customer", - email: "test@test.com", - }, - db: this.db, - org: this.org, - env: this.env, - }); - }); - - it("should attach pro product", async function () { - this.timeout(30000); - - const res: any = await AutumnCli.attach({ - customerId: customerId, - productId: products.pro.id, - }); - - await completeCheckoutForm(res.checkout_url); - await timeout(5000); - console.log(` ${chalk.greenBright("Attached pro product")}`); - }); - - // 1. Cancel pro product - it("should cancel pro product (at period end)", async function () { - this.timeout(10000); - - const stripeCli = createStripeCli({ org: this.org, env: this.env }); - - // 1. Cancel pro product - const cusRes: any = await AutumnCli.getCustomer(customerId); - - const proProduct = cusRes.products.find( - (p: any) => p.id === products.pro.id, - ); - - for (const subId of proProduct.subscription_ids) { - await stripeCli.subscriptions.update(subId, { - cancel_at_period_end: true, - }); - } - - await timeout(3000); - console.log(` ${chalk.greenBright("Cancelled pro product")}`); - }); - - it("should have pro product active, and canceled_at != null", async function () { - this.timeout(10000); - - const cusRes: any = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.pro, - cusRes: cusRes, - }); - - const proProduct = cusRes.products.find( - (p: any) => p.id === products.pro.id, - ); - expect(proProduct.canceled_at).to.not.equal(null); - expect(proProduct.status).to.equal(CusProductStatus.Active); - }); - - // CANCEL SUB NOW, SUBSCRIPTION.DELETED WEBHOOK - it("should cancel pro product (now)", async function () { - this.timeout(10000); - - const stripeCli = createStripeCli({ org: this.org, env: this.env }); - - const cusRes: any = await AutumnCli.getCustomer(customerId); - const proProduct = cusRes.products.find( - (p: any) => p.id === products.pro.id, - ); - - for (const subId of proProduct.subscription_ids) { - await stripeCli.subscriptions.cancel(subId); - } - - await timeout(3000); - console.log(` ${chalk.greenBright("Cancelled pro product now")}`); - }); - - it("should have free product active, and pro product not returned", async function () { - this.timeout(10000); - - const cusRes: any = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.free, - cusRes: cusRes, - }); - }); - - it("should have correct entitlements (for free)", async function () { - for (const entitlement of Object.values(products.free.entitlements)) { - let feature = features[entitlement.feature_id!]; - await checkFeatureHasCorrectBalance({ - customerId, - feature: feature, - entitlement, - expectedBalance: entitlement.allowance || 0, - }); - } - }); -}); - -describe(`${chalk.yellowBright( - "03_cancel: Testing subscription past_due", -)}`, () => { - const customerId = "03_cancel_past_due"; - - before(async function () { - this.timeout(30000); - - const stripeCli = createStripeCli({ org: this.org, env: this.env }); - - const testClock = await stripeCli.testHelpers.testClocks.create({ - frozen_time: Math.round(Date.now() / 1000), - }); - - this.testClockId = testClock.id; - - await initCustomer({ - customer_data: { - id: customerId, - name: "Test Customer", - email: "test@test.com", - }, - db: this.db, - org: this.org, - env: this.env, - attachPm: true, - testClockId: testClock.id, - }); - }); - - it("should attach pro product", async function () { - this.timeout(10000); - - await AutumnCli.attach({ - customerId: customerId, - productId: products.pro.id, - }); - }); - - it("should attach failed payment method and advance to next billing date", async function () { - // 1. Swap customer's card - const stripeCli = createStripeCli({ org: this.org, env: this.env }); - - const cusRes: any = await AutumnCli.getCustomer(customerId); - - await attachFailedPaymentMethod({ - stripeCli, - customer: cusRes.customer, - }); - - // const advanceDate = addDays(addMonths(new Date(), 1), 1); - // await stripeCli.testHelpers.testClocks.advance(this.testClockId, { - // frozen_time: Math.round(advanceDate.getTime() / 1000), - // }); - - await advanceTestClock({ - stripeCli, - testClockId: this.testClockId, - advanceTo: addDays(addMonths(new Date(), 1), 1).getTime(), - }); - }); - - it("should have free product active and correct entitlements", async function () { - const cusRes: any = await AutumnCli.getCustomer(customerId); - // compareMainProduct({ - // sent: products.free, - // cusRes: cusRes, - // }); - - // TODO: Check why this line messes up the test - // compareProductEntitlements({ - // customerId, - // product: products.free, - // features, - // }); - }); -}); diff --git a/server/tests/basic/05_trial.ts b/server/tests/basic/05_trial.ts deleted file mode 100644 index fedbea6d3..000000000 --- a/server/tests/basic/05_trial.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { compareMainProduct } from "../utils/compare.js"; -import { initCustomer } from "../utils/init.js"; -import { features, products } from "../global.js"; -import { AutumnCli } from "../cli/AutumnCli.js"; -import { assert, expect } from "chai"; -import { timeout } from "../utils/genUtils.js"; -import { completeCheckoutForm } from "../utils/stripeUtils.js"; -import { getAxiosInstance } from "../utils/setup.js"; -import chalk from "chalk"; -import { AppEnv, CusProductStatus, Organization } from "@autumn/shared"; -import { createStripeCli } from "@/external/stripe/utils.js"; - -const cancelProduct = async ({ - org, - env, - customerId, - productId, -}: { - org: Organization; - env: AppEnv; - customerId: string; - productId: string; -}) => { - const stripeCli = createStripeCli({ org, env }); - const cusRes: any = await AutumnCli.getCustomer(customerId); - const proProduct = cusRes.products.find((p: any) => p.id === productId); - - // await stripeCli.subscriptions.cancel(proProduct.processor.subscription_id!); - for (const subId of proProduct.subscription_ids) { - await stripeCli.subscriptions.cancel(subId); - } - - await timeout(3000); -}; - -describe(`${chalk.yellowBright("05_trial: Testing free trials")}`, () => { - const customerId = "customerWithTrial"; - let customerId2 = "customerWithTrialSameFingerprint"; - - describe("First customer, attach pro with trial", () => { - before(async function () { - await initCustomer({ - customer_data: { - id: customerId, - name: customerId, - email: "test@test.com", - fingerprint: "fp1", - }, - db: this.db, - org: this.org, - env: this.env, - }); - }); - - it("should attach pro with trial", async function () { - const res = await AutumnCli.attach({ - customerId: customerId, - productId: products.proWithTrial.id, - }); - - await completeCheckoutForm(res.checkout_url); - await timeout(10000); // for webhook to be processed - }); - - // 1. Check if product is attached - it("should have correct product & invoice (pro with trial)", async function () { - const customer = await AutumnCli.getCustomer(customerId); - - compareMainProduct({ - sent: products.proWithTrial, - cusRes: customer, - status: CusProductStatus.Trialing, - }); - - // Check invoice is 0 - try { - const invoices = customer.invoices; - assert.equal(invoices.length, 1); - assert.equal(invoices[0].total, 0); - } catch (error) { - console.group(); - console.group(); - console.log("GET customer, balances failed"); - console.log("Expected invoice amount 0"); - console.log("Customer invoices received:", customer.invoices); - console.groupEnd(); - console.groupEnd(); - throw error; - } - }); - - // 2. Cancel product and attach again - it("should cancel pro with trial", async function () { - await cancelProduct({ - org: this.org, - env: this.env, - customerId: customerId, - productId: products.proWithTrial.id, - }); - }); - it("should attach pro with trial again", async function () { - await AutumnCli.attach({ - customerId: customerId, - productId: products.proWithTrial.id, - }); - - await timeout(5000); // for webhook to be processed - }); - }); - - describe("Second customer (same fingerprint), attach pro with trial", () => { - // 3. Check if product is attached - it("should have correct product & invoice (pro with trial, full price)", async function () { - const customer = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.proWithTrial, - cusRes: customer, - status: CusProductStatus.Active, - }); - - // Check invoice is equal monthly price - const invoices = customer.invoices; - try { - assert.equal( - invoices[0].amount, - products.proWithTrial.prices[0].amount, - ); - } catch (error) { - console.group(); - console.group(); - console.log("GET customer, balances failed"); - console.log( - "Expected invoice amount:", - products.proWithTrial.prices[0].amount, - ); - console.log("Customer invoices received:", invoices); - console.groupEnd(); - console.groupEnd(); - } - }); - - it("should create new customer and attach pro with trial (same fingerprint)", async function () { - await initCustomer({ - customer_data: { - id: customerId2, - name: customerId2, - email: "test2@test.com", - fingerprint: "fp1", - }, - db: this.db, - org: this.org, - env: this.env, - attachPm: true, - }); - - await AutumnCli.attach({ - customerId: customerId2, - productId: products.proWithTrial.id, - }); - - await timeout(8000); // for webhook to be processed - }); - - it("should have correct product & invoice (pro with trial, full price)", async function () { - const customer = await AutumnCli.getCustomer(customerId2); - compareMainProduct({ - sent: products.proWithTrial, - cusRes: customer, - status: CusProductStatus.Active, - }); - - // Check invoice is equal monthly price - const invoices = customer.invoices; - try { - assert.equal( - invoices[0].amount, - products.proWithTrial.prices[0].amount, - ); - } catch (error) { - console.group(); - console.group(); - console.log("GET customer, balances failed"); - console.log( - "Expected invoice amount:", - products.proWithTrial.prices[0].amount, - ); - console.log("Customer invoices received:", invoices); - console.groupEnd(); - console.groupEnd(); - } - }); - }); -}); diff --git a/server/tests/basic/06_upgrade.ts b/server/tests/basic/06_upgrade.ts deleted file mode 100644 index f9bbc2e9b..000000000 --- a/server/tests/basic/06_upgrade.ts +++ /dev/null @@ -1,370 +0,0 @@ -// TESTING UPGRADES - -import chalk from "chalk"; -import { initCustomer } from "../utils/init.js"; -import { CusProductStatus } from "@autumn/shared"; -import { AutumnCli } from "../cli/AutumnCli.js"; -import { products } from "../global.js"; -import { assert } from "chai"; -import { - attachFailedPaymentMethod, - attachPmToCus, -} from "@/external/stripe/stripeCusUtils.js"; -import { Customer } from "@autumn/shared"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { compareMainProduct } from "../utils/compare.js"; -import { addDays } from "date-fns"; -import { timeout } from "../utils/genUtils.js"; -import { InvoiceService } from "@/internal/customers/invoices/InvoiceService.js"; - -describe(`${chalk.yellowBright("06_upgrade: Testing upgrades")}`, () => { - let customer: Customer; - let customerId = "upgrade"; - - before(async function () { - this.timeout(30000); - customer = await initCustomer({ - customer_data: { - id: customerId, - name: "Test Customer", - email: "test@test.com", - }, - db: this.db, - org: this.org, - env: this.env, - attachPm: true, - }); - }); - - it("should attach pro (first time, trial)", async function () { - this.timeout(30000); - const res = await AutumnCli.attach({ - customerId: customerId, - productId: products.pro.id, - }); - - console.log(` ${chalk.greenBright("Attached pro")}`); - }); - - it("should have correct product and entitlements", async function () { - this.timeout(30000); - const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.pro, - cusRes: res, - }); - }); - - // 1. Try force checkout... - it("should attach premium and not be able to force checkout", async function () { - this.timeout(30000); - try { - const res = await AutumnCli.attach({ - customerId: customerId, - productId: products.premium.id, - forceCheckout: true, - }); - - throw new Error("Should not reach here"); - } catch (error: any) { - assert.equal( - error.message, - "Either payment method not found, or force_checkout is true: unable to perform upgrade / downgrade", - ); - assert.equal(error.code, "invalid_request"); - } - }); - - it("should attach premium and not be able to upgrade (without payment method)", async function () { - this.timeout(30000); - try { - const stripeCli = createStripeCli({ - org: this.org, - env: this.env, - }); - await attachFailedPaymentMethod({ - stripeCli: stripeCli, - customer: customer, - }); - - const res = await AutumnCli.attach({ - customerId: customerId, - productId: products.premium.id, - forceCheckout: true, - }); - - throw new Error("Should not reach here"); - } catch (error: any) { - try { - assert.equal( - error.message, - "Either payment method not found, or force_checkout is true: unable to perform upgrade / downgrade", - ); - assert.equal(error.code, "invalid_request"); - } catch (error) { - console.group(); - console.log( - "Expected recase error for force checkout / no payment method", - ); - console.log("Got:", error); - console.groupEnd(); - throw error; - } - } - }); - - // Attach payment method - it("should attach successful payment method", async function () { - this.timeout(30000); - await attachPmToCus({ - db: this.db, - customer: customer, - org: this.org, - env: this.env, - }); - }); - - it("should attach premium", async function () { - this.timeout(30000); - const res = await AutumnCli.attach({ - customerId: customerId, - productId: products.premium.id, - }); - - console.log(` ${chalk.greenBright("Attached premium")}`); - }); - - it("GET /customers/:customer_id -- checking product and ents", async function () { - this.timeout(30000); - const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.premium, - cusRes: res, - }); - }); -}); - -describe(`${chalk.yellowBright( - "06_upgrade: Testing upgrade (paid to trial)", -)}`, () => { - const customerId = "paid_to_trial"; - let testClockId: string; - let customer: any; - - before(async function () { - this.timeout(30000); - const stripeCli = createStripeCli({ - org: this.org, - env: this.env, - }); - const testClock = await stripeCli.testHelpers.testClocks.create({ - frozen_time: Math.floor(Date.now() / 1000), - }); - testClockId = testClock.id; - customer = await initCustomer({ - customer_data: { - id: customerId, - name: "Paid to trial customer", - email: "paid@trial.com", - }, - db: this.db, - org: this.org, - env: this.env, - attachPm: true, - testClockId, - }); - }); - - it("POST /attach -- attaching pro", async function () { - this.timeout(30000); - await AutumnCli.attach({ - customerId: customerId, - productId: products.pro.id, - }); - - console.log(` ${chalk.greenBright("Attached pro")}`); - }); - - it("POST /attach -- attaching premium with trial", async function () { - this.timeout(30000); - - await AutumnCli.attach({ - customerId: customerId, - productId: products.premiumWithTrial.id, - }); - - console.log(` ${chalk.greenBright("Attached premium")}`); - }); -}); - -describe(`${chalk.yellowBright( - "06_upgrade: Testing upgrade (trial to paid)", -)}`, () => { - const customerId = "trial_to_paid"; - let testClockId: string; - let customer: any; - - before(async function () { - const stripeCli = createStripeCli({ - org: this.org, - env: this.env, - }); - const testClock = await stripeCli.testHelpers.testClocks.create({ - frozen_time: Math.floor(Date.now() / 1000), - }); - testClockId = testClock.id; - customer = await initCustomer({ - customer_data: { - id: customerId, - name: "Trial to paid customer", - email: "trial@paid.com", - }, - db: this.db, - org: this.org, - env: this.env, - attachPm: true, - testClockId, - }); - }); - - it("POST /attach -- attaching pro with trial", async function () { - await AutumnCli.attach({ - customerId: customerId, - productId: products.proWithTrial.id, - }); - - console.log(` ${chalk.greenBright("Attached pro with trial")}`); - }); - - it("POST /attach -- attaching premium", async function () { - const advanceTo = addDays(new Date(), 3).getTime() / 1000; - const stripeCli = createStripeCli({ - org: this.org, - env: this.env, - }); - await stripeCli.testHelpers.testClocks.advance(testClockId, { - frozen_time: Math.floor(advanceTo), - }); - - await timeout(10000); - - await AutumnCli.attach({ - customerId: customerId, - productId: products.premium.id, - }); - - await timeout(10000); - - console.log( - ` ${chalk.greenBright("Advanced 3 days and attached premium")}`, - ); - }); - - it("GET /customers/:customer_id -- checking product and ents", async function () { - this.timeout(30000); - const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.premium, - cusRes: res, - }); - - const invoices = await res.invoices; - - try { - assert.equal(invoices[0].total, products.premium.prices[0].config.amount); - } catch (error) { - console.group(); - console.log("Expected invoice to be for 50.00"); - console.log("Got:", res.invoices); - console.groupEnd(); - throw error; - } - }); -}); - -describe(`${chalk.yellowBright("Testing upgrade (trial to trial)")}`, () => { - const customerId = "trialToTrial"; - let testClockId: string; - let customer: any; - - before(async function () { - console.log(" - Running initCustomer"); - this.timeout(30000); - const stripeCli = createStripeCli({ - org: this.org, - env: this.env, - }); - const testClock = await stripeCli.testHelpers.testClocks.create({ - frozen_time: Math.floor(Date.now() / 1000), - }); - testClockId = testClock.id; - customer = await initCustomer({ - customer_data: { - id: customerId, - name: "Trial to trial customer", - email: "trial@trial.com", - }, - db: this.db, - org: this.org, - env: this.env, - attachPm: true, - testClockId, - }); - }); - - it("POST /attach -- attaching pro with trial", async function () { - this.timeout(30000); - await AutumnCli.attach({ - customerId: customerId, - productId: products.proWithTrial.id, - }); - - console.log(` ${chalk.greenBright("Attached pro with trial")}`); - }); - - it("POST /attach -- attaching premium with trial", async function () { - this.timeout(30000); - - const advanceTo = addDays(new Date(), 3).getTime() / 1000; - const stripeCli = createStripeCli({ - org: this.org, - env: this.env, - }); - await stripeCli.testHelpers.testClocks.advance(testClockId, { - frozen_time: Math.floor(advanceTo), - }); - - await timeout(10000); - - await AutumnCli.attach({ - customerId: customerId, - productId: products.premiumWithTrial.id, - }); - - console.log( - ` ${chalk.greenBright("Advanced 3 days and attached premium")}`, - ); - }); - - it("GET /customers/:customer_id -- checking product and ents", async function () { - this.timeout(30000); - const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.premiumWithTrial, - cusRes: res, - status: CusProductStatus.Trialing, - }); - - const invoices = await res.invoices; - - try { - assert.equal(invoices[0].total, 0); - } catch (error) { - console.group(); - console.log("Expected invoice to be 0"); - console.log("Got:", res.invoices); - console.groupEnd(); - throw error; - } - }); -}); diff --git a/server/tests/basic/07_downgrade.ts b/server/tests/basic/07_downgrade.ts deleted file mode 100644 index 000f04372..000000000 --- a/server/tests/basic/07_downgrade.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { CusProductStatus, Customer } from "@autumn/shared"; -import { initCustomer } from "../utils/init.js"; -import { AutumnCli } from "../cli/AutumnCli.js"; -import { products } from "../global.js"; -import { assert } from "chai"; -import chalk from "chalk"; -import { compareMainProduct } from "../utils/compare.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { addDays } from "date-fns"; -import { timeout } from "../utils/genUtils.js"; -import { SupabaseClient } from "@supabase/supabase-js"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; -import { Autumn } from "@/external/autumn/autumnCli.js"; -import { setupBefore } from "tests/before.js"; - -export const getCusProduct = async ( - sb: SupabaseClient, - internalCustomerId: string, - productId: string, -) => { - const { data, error } = await sb - .from("customer_products") - .select("*") - .eq("internal_customer_id", internalCustomerId) - .eq("product_id", productId) - .order("created_at", { ascending: false }); - - if (!data || data.length === 0) { - return null; - } - - return data[0]; -}; - -describe(`${chalk.yellowBright( - "07_downgrade: testing downgrade (paid to paid)", -)}`, () => { - let customer: Customer; - let customerId = "downgrade"; - let testClockId: string; - before(async function () { - this.timeout(30000); - const stripeCli = createStripeCli({ - org: this.org, - env: this.env, - }); - const testClock = await stripeCli.testHelpers.testClocks.create({ - frozen_time: Math.floor(Date.now() / 1000), - }); - - customer = await initCustomer({ - customerId, - db: this.db, - org: this.org, - env: this.env, - attachPm: true, - testClockId: testClock.id, - }); - testClockId = testClock.id; - }); - - it("POST /attach -- attaching premium", async function () { - this.timeout(30000); - const res = await AutumnCli.attach({ - customerId: customerId, - productId: products.premium.id, - }); - - console.log(` ${chalk.greenBright("Attached premium")}`); - }); - - // 1. Try force checkout... - it("POST /attach -- attaching pro", async function () { - this.timeout(30000); - await AutumnCli.attach({ - customerId: customerId, - productId: products.pro.id, - }); - }); - - // Check that pro is scheduled - - it("GET /customers/:customer_id -- checking product and ents", async function () { - this.timeout(30000); - const res = await AutumnCli.getCustomer(customerId); - - compareMainProduct({ - sent: products.premium, - cusRes: res, - }); - - const { products: resProducts } = res; - - const resPro = resProducts.find( - (p: any) => - p.id === products.pro.id && p.status === CusProductStatus.Scheduled, - ); - assert.isNotNull(resPro); - }); - - // Attach premium to see if scheduled product is removed - it("checking if attach premium will remove scheduled product", async function () { - this.timeout(30000); - await AutumnCli.attach({ - customerId: customerId, - productId: products.premium.id, - }); - - const res = await AutumnCli.getCustomer(customerId); - const resPro = res.products.find( - (p: any) => - p.id === products.pro.id && p.status === CusProductStatus.Scheduled, - ); - assert.isUndefined(resPro); - - compareMainProduct({ - sent: products.premium, - cusRes: res, - }); - }); - - // Advance time 1 month - it("Attaching pro, advancing stripe clock and seeing if pro is attached", async function () { - this.timeout(30000); - await AutumnCli.attach({ - customerId: customerId, - productId: products.pro.id, - }); - - const stripeCli = createStripeCli({ - org: this.org, - env: this.env, - }); - - const advanceTo = addDays(new Date(), 32).getTime() / 1000; - await stripeCli.testHelpers.testClocks.advance(testClockId, { - frozen_time: Math.floor(advanceTo), - }); - - await timeout(20000); - - const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.pro, - cusRes: res, - }); - }); -}); - -describe(`${chalk.yellowBright("07_downgrade: testing expire button")}`, () => { - let customer: Customer; - let customerId = "expire"; - let testClockId: string; - - let autumn: Autumn; - before(async function () { - await setupBefore(this); - autumn = this.autumn; - - const stripeCli = createStripeCli({ - org: this.org, - env: this.env, - }); - - const { testClockId: testClockId_, customer: customer_ } = - await initCustomerWithTestClock({ - customerId, - org: this.org, - env: this.env, - db: this.db, - }); - - customer = customer_; - testClockId = testClockId_; - }); - - it("POST /attach -- attaching premium", async function () { - await autumn.attach({ - customerId: customerId, - productId: products.premium.id, - }); - }); - - it("POST /expire -- expiring premium", async function () { - const customerProduct = await getCusProduct( - this.sb, - customer.internal_id, - products.premium.id, - ); - - await AutumnCli.expire(customerProduct.id); - await timeout(5000); - }); - - // Check that active product is free - it("GET /customers/:customer_id -- checking product and ents", async function () { - const res = await AutumnCli.getCustomer(customerId); - - compareMainProduct({ - sent: products.free, - cusRes: res, - }); - }); - - // 2. Get premium - it("POST /attach -- attaching premium, then attach pro", async function () { - this.timeout(30000); - await AutumnCli.attach({ - customerId: customerId, - productId: products.premium.id, - }); - - await AutumnCli.attach({ - customerId: customerId, - productId: products.pro.id, - }); - }); - - it("Expiring pro product (should re-attach premium)", async function () { - this.timeout(30000); - - // Expire pro product - const customerProduct = await getCusProduct( - this.sb, - customer.internal_id, - products.pro.id, - ); - await AutumnCli.expire(customerProduct.id); - await timeout(5000); - }); - - it("GET /customers/:customer_id -- checking product and ents", async function () { - this.timeout(30000); - // Check that free is attached - const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.premium, - cusRes: res, - }); - - // Get stripe subscription (ensure canceled is null) - const stripeCli = createStripeCli({ - org: this.org, - env: this.env, - }); - - const premiumCusProduct = await getCusProduct( - this.sb, - customer.internal_id, - products.premium.id, - ); - - const stripeSub = await stripeCli.subscriptions.retrieve( - premiumCusProduct.processor.subscription_id, - ); - - // Check that canceled is null - assert.isNull(stripeSub.canceled_at); - }); - - // TEST TODO: Expire add-on -}); diff --git a/server/tests/basic/entities/entities1.ts b/server/tests/basic/entities/entities1.ts deleted file mode 100644 index 0a7c63217..000000000 --- a/server/tests/basic/entities/entities1.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { Autumn } from "@/external/autumn/autumnCli.js"; -import { setupBefore } from "tests/before.js"; -import { CusProductStatus, organizations } from "@autumn/shared"; -import { getFeaturePrice, getUsagePriceTiers } from "tests/utils/genUtils.js"; -import { entityProducts, features } from "../../global.js"; -import { Stripe } from "stripe"; -import { checkBalance } from "tests/utils/autumnUtils.js"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { addDays, addHours, addMonths } from "date-fns"; -import { CacheManager } from "@/external/caching/CacheManager.js"; -import { CacheType } from "@/external/caching/cacheActions.js"; -import { hashApiKey } from "@/internal/dev/api-keys/apiKeyUtils.js"; -import { compareMainProduct } from "../../utils/compare.js"; -import { assert, expect } from "chai"; - -import chalk from "chalk"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; -import { eq } from "drizzle-orm"; - -// Check balance and stripe quantity -const checkEntAndStripeQuantity = async ({ - db, - autumn, - stripeCli, - featureId, - customerId, - expectedBalance, - expectedUsage, - expectedStripeQuantity, -}: { - db: DrizzleCli; - autumn: Autumn; - stripeCli: Stripe; - featureId: string; - customerId: string; - expectedBalance: number; - expectedUsage?: number; - expectedStripeQuantity: number; -}) => { - let { customer, entitlements, products } = - await autumn.customers.get(customerId); - - let cusProducts = await CusProductService.list({ - db, - internalCustomerId: customer.internal_id, - inStatuses: [CusProductStatus.Active], - }); - - let entitlement = entitlements.find((e: any) => e.feature_id == featureId); - - expect(entitlement.balance).to.equal(expectedBalance); - if (expectedUsage) { - expect(entitlement.used).to.equal( - expectedUsage, - `Get customer ${customerId} returned incorrect "used" for feature ${featureId}`, - ); - } - - if (products.length == 0) { - assert.fail(`Get customer ${customerId} returned no products`); - } - - // 2. Get stripe quantity - let mainProduct = products[0]; - - if (mainProduct.subscription_ids.length == 0) { - assert.fail(`Get customer ${customerId} returned no subscriptions`); - } - - let price = getFeaturePrice({ - product: mainProduct, - featureId: featureId, - cusProducts, - }); - - if (!price) { - assert.fail( - `Get customer ${customerId} returned no price for feature ${featureId}`, - ); - } - - let stripeSub = await stripeCli.subscriptions.retrieve( - mainProduct.subscription_ids[0], - ); - let subItem = stripeSub.items.data.find( - (item: any) => item.price.id == price.config!.stripe_price_id, - ); - - if (!subItem) { - assert.fail( - `Get customer ${customerId} returned no sub item for feature ${featureId}`, - ); - } - - expect(subItem.quantity).to.equal( - expectedStripeQuantity, - `Get customer ${customerId} returned incorrect stripe quantity for feature ${featureId}`, - ); -}; - -// UNCOMMENT FROM HERE -describe(`${chalk.yellowBright("entities1: Testing entities")}`, () => { - let customerId = "entity1"; - let autumn: Autumn; - let stripeCli: Stripe; - let usageTiers = getUsagePriceTiers({ - product: entityProducts.entityPro, - featureId: features.seats.id, - }); - let testClockId: string; - - before(async function () { - await setupBefore(this); - autumn = this.autumn; - stripeCli = this.stripeCli; - - const { testClockId: testClockId1 } = await initCustomerWithTestClock({ - customerId, - db: this.db, - org: this.org, - env: this.env, - }); - - testClockId = testClockId1; - - // Update org config - await this.db - .update(organizations) - .set({ - config: { - ...this.org.config, - prorate_unused: false, - }, - }) - .where(eq(organizations.id, this.org.id)); - - await CacheManager.invalidate({ - action: CacheType.SecretKey, - value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!), - }); - await CacheManager.disconnect(); - }); - - let firstEntityId = "1"; - - it("should attach entityFree product", async function () { - await this.autumn.attach({ - customerId, - productId: entityProducts.entityFree.id, - }); - - await this.autumn.entities.create(customerId, { - id: firstEntityId, - name: "1@gmail.com", - featureId: features.seats.id, - }); - - // Check if entity is created - let res = await this.autumn.entities.list(customerId); - let entities = res.data; - - expect(entities).to.have.lengthOf(1); - expect(entities[0].id).to.equal(firstEntityId); - }); - - it("should successfully remove created entity", async function () { - await this.autumn.entities.delete(customerId, firstEntityId); - - let res = await this.autumn.entities.list(customerId); - let entities = res.data; - expect(entities).to.have.lengthOf(0); - }); - - it("should create first entity, then attach entityPro product", async function () { - await this.autumn.entities.create(customerId, { - id: firstEntityId, - name: "1@gmail.com", - featureId: features.seats.id, - }); - - await this.autumn.attach({ - customerId, - productId: entityProducts.entityPro.id, - }); - - // Check product is attached correctly - let cusRes = await autumn.customers.get(customerId); - compareMainProduct({ - sent: entityProducts.entityPro, - cusRes: cusRes, - }); - - let { invoices } = cusRes; - let usageTiers = getUsagePriceTiers({ - product: entityProducts.entityPro, - featureId: features.seats.id, - }); - - // Check invoice is created correctly - expect(invoices).to.have.lengthOf(1); - expect(invoices[0].total).to.equal(usageTiers[0].amount); - - // Check balance and stripe quantity - await checkEntAndStripeQuantity({ - db: this.db, - autumn, - stripeCli, - featureId: features.seats.id, - customerId, - expectedBalance: -1, - expectedStripeQuantity: 1, - }); - - await checkBalance({ - autumn, - featureId: features.metered1.id, - customerId, - expectedBalance: - entityProducts.entityPro.entitlements.metered1.allowance!, - }); - }); - - let newEntities = [ - { - id: "2", - name: "2@gmail.com", - featureId: features.seats.id, - }, - { - id: "3", - name: "3@gmail.com", - featureId: features.seats.id, - }, - { - id: "4", - name: "4@gmail.com", - featureId: features.seats.id, - }, - ]; - - it("should create 3 additional entities and be charged immediately", async function () { - let advanceToDay = addDays(new Date(), 1).getTime(); - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: advanceToDay, - waitForSeconds: 20, - }); - - await this.autumn.entities.create(customerId, newEntities); - - let entitiesRes = await this.autumn.entities.list(customerId); - let entities = entitiesRes.data; - expect(entities).to.have.lengthOf(newEntities.length + 1); - - let cusRes = await autumn.customers.get(customerId); - - let { invoices } = cusRes; - expect(invoices[0].total).to.equal( - usageTiers[0].amount * newEntities.length, - ); - }); - - it("should remove 2 entities, and have correct balance / stripe quantity", async function () { - await this.autumn.entities.delete(customerId, newEntities[0].id); - - await checkEntAndStripeQuantity({ - db: this.db, - autumn, - stripeCli, - featureId: features.seats.id, - customerId, - expectedBalance: -(newEntities.length + 1), - expectedStripeQuantity: newEntities.length + 1 - 1, - expectedUsage: newEntities.length + 1 - 1, - }); - - await this.autumn.entities.delete(customerId, newEntities[1].id); - await checkEntAndStripeQuantity({ - db: this.db, - autumn, - stripeCli, - featureId: features.seats.id, - customerId, - expectedBalance: -(newEntities.length + 1), - expectedStripeQuantity: newEntities.length + 1 - 2, - expectedUsage: newEntities.length + 1 - 2, - }); - }); - - let newEntities2 = [ - { - id: "5", - name: "5@gmail.com", - featureId: features.seats.id, - }, - { - id: "6", - name: "6@gmail.com", - featureId: features.seats.id, - }, - { - id: "7", - name: "7@gmail.com", - featureId: features.seats.id, - }, - ]; - - it("should create three additional seats, and be charged for only one", async function () { - await this.autumn.entities.create(customerId, newEntities2); - - let totalSeats = newEntities2.length + 2; - await checkEntAndStripeQuantity({ - db: this.db, - autumn, - stripeCli, - featureId: features.seats.id, - customerId, - expectedBalance: -totalSeats, - expectedStripeQuantity: totalSeats, - expectedUsage: totalSeats, - }); - - let cusRes = await autumn.customers.get(customerId); - let { invoices } = cusRes; - expect(invoices[0].total).to.equal(usageTiers[0].amount); - }); - - // return; - - it("should remove one entity, and have correct balance / stripe quantity after advancing test clock", async function () { - await this.autumn.entities.delete(customerId, newEntities2[0].id); - - let totalSeats = newEntities2.length + 1; - - let advanceTo = addHours(addMonths(new Date(), 1), 4).getTime(); - await advanceTestClock({ stripeCli, testClockId, advanceTo }); - - // Get entities - let { data: entities } = await this.autumn.entities.list(customerId); - expect(entities).to.have.lengthOf(totalSeats); - - await checkEntAndStripeQuantity({ - db: this.db, - autumn, - stripeCli, - featureId: features.seats.id, - customerId, - expectedBalance: -totalSeats, - expectedStripeQuantity: totalSeats, - expectedUsage: totalSeats, - }); - - await checkBalance({ - autumn, - featureId: features.metered1.id, - customerId, - expectedBalance: - totalSeats * entityProducts.entityPro.entitlements.metered1.allowance!, - }); - }); - - after(async function () { - await this.db - .update(organizations) - .set({ - config: { - ...this.org.config, - prorate_unused: true, - }, - }) - .where(eq(organizations.id, this.org.id)); - - void CacheManager.invalidate({ - action: CacheType.SecretKey, - value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!), - }); - }); -}); diff --git a/server/tests/basic/entities/entities2.ts b/server/tests/basic/entities/entities2.ts deleted file mode 100644 index 91482bd06..000000000 --- a/server/tests/basic/entities/entities2.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { compareMainProduct } from "../../utils/compare.js"; - -import { entityProducts, features } from "../../global.js"; - -import { assert, expect } from "chai"; -import chalk from "chalk"; -import { Autumn } from "@/external/autumn/autumnCli.js"; -import { setupBefore } from "tests/before.js"; -import { - BillingInterval, - CusProductStatus, - EntInterval, - ErrCode, - ProductItemFeatureType, - UsageModel, -} from "@autumn/shared"; -import { getFeaturePrice, getUsagePriceTiers } from "tests/utils/genUtils.js"; - -import { Stripe } from "stripe"; -import { CusService } from "@/internal/customers/CusService.js"; -import { SupabaseClient } from "@supabase/supabase-js"; -import { checkBalance } from "tests/utils/autumnUtils.js"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { addHours, addMonths } from "date-fns"; -import { CacheManager } from "@/external/caching/CacheManager.js"; -import { CacheType } from "@/external/caching/cacheActions.js"; -import { hashApiKey } from "@/internal/dev/api-keys/apiKeyUtils.js"; -import { - constructFeatureItem, - constructFeaturePriceItem, -} from "@/internal/products/product-items/productItemUtils.js"; -import { createProduct } from "tests/utils/productUtils.js"; -import { OrgService } from "@/internal/orgs/OrgService.js"; - -// UNCOMMENT FROM HERE -let entity2Pro = { - id: "entity2Pro", - name: "Entity 2 Pro", - items: { - seats: constructFeaturePriceItem({ - feature_id: features.seats.id, - included_usage: 0, - price: 150, - interval: BillingInterval.Month, - usage_model: UsageModel.PayPerUse, - }), - metered1: constructFeaturePriceItem({ - feature_id: features.metered1.id, - included_usage: 50_000, - billing_units: 50_000, - price: 10, - interval: BillingInterval.Month, - entity_feature_id: features.seats.id, - }), - metered2: constructFeatureItem({ - feature_id: features.metered2.id, - included_usage: 4000, - interval: EntInterval.Month, - entity_feature_id: features.seats.id, - }), - }, -}; - -describe(`${chalk.yellowBright( - "entities2: Testing entities with prorate_unused: true", -)}`, () => { - let customerId = "entity2"; - let autumn: Autumn; - let stripeCli: Stripe; - let testClockId: string; - - before(async function () { - await setupBefore(this); - autumn = this.autumn; - stripeCli = this.stripeCli; - - const { testClockId: testClockId1 } = await initCustomerWithTestClock({ - customerId, - db: this.db, - org: this.org, - env: this.env, - }); - - await createProduct({ - autumn: this.autumn, - product: entity2Pro, - }); - - testClockId = testClockId1; - - await OrgService.update({ - db: this.db, - orgId: this.org.id, - updates: { - config: { ...this.org.config, prorate_unused: true }, - }, - }); - - await CacheManager.invalidate({ - action: CacheType.SecretKey, - value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!), - }); - await CacheManager.disconnect(); - }); - - it("should create entity, then attach pro product", async function () { - await autumn.entities.create(customerId, { - id: "1", - name: "seat_1", - featureId: features.seats.id, - }); - - await autumn.attach({ - customerId, - productId: entity2Pro.id, - }); - - let { customer, invoices } = await autumn.customers.get(customerId); - - expect(invoices.length).to.equal(1); - expect(invoices[0].total).to.equal(entity2Pro.items.seats.price); - }); - - after(async function () { - await OrgService.update({ - db: this.db, - orgId: this.org.id, - updates: { - config: { ...this.org.config, prorate_unused: false }, - }, - }); - - void CacheManager.invalidate({ - action: CacheType.SecretKey, - value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!), - }); - }); -}); diff --git a/server/tests/basic/product/product1.ts b/server/tests/basic/product/product1.ts deleted file mode 100644 index b5e05f969..000000000 --- a/server/tests/basic/product/product1.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { features } from "../../global.js"; -import { assert } from "chai"; -import chalk from "chalk"; -import { Autumn } from "@/external/autumn/autumnCli.js"; -import { setupBefore } from "tests/before.js"; -import { BillingInterval, EntInterval, Infinite } from "@autumn/shared"; - -import { initCustomer } from "tests/utils/init.js"; -import { - constructFeatureItem, - constructPriceItem, -} from "@/internal/products/product-items/productItemUtils.js"; - -// UNCOMMENT FROM HERE -describe(`${chalk.yellowBright( - "product1: Testing create and update product", -)}`, () => { - let autumn: Autumn; - - before(async function () { - await setupBefore(this); - autumn = this.autumn; - }); - - it("should create product", async function () { - try { - await autumn.products.delete("product-1"); - } catch (error) {} - - await autumn.products.create({ - id: "product-1", - name: "Product 1", - }); - - let product = await autumn.products.get("product-1"); - - assert.equal(product.name, "Product 1"); - assert.equal(product.id, "product-1"); - assert.equal(product.is_add_on, false); - assert.equal(product.is_default, false); - assert.equal(product.version, 1); - assert.equal(product.group, ""); - }); - - let items = [ - // 1. Boolean feature - constructFeatureItem({ - feature_id: features.boolean1.id, - }), - - // 2. Limited feature - constructFeatureItem({ - feature_id: features.metered1.id, - included_usage: 100, - interval: EntInterval.Month, - }), - - // 3. Unlimited feature - constructFeatureItem({ - feature_id: features.infinite1.id, - included_usage: Infinite, - }), - - // 4. Fixed Price - constructPriceItem({ - price: 10, - interval: BillingInterval.Month, - }), - - // 5. Fixed one off price - constructPriceItem({ - price: 50, - interval: BillingInterval.OneOff, - }), - ]; - - it("should create free price and free feature", async function () { - await autumn.products.update("product-1", { - items: items, - }); - - let product = await autumn.products.get("product-1", { - v1Schema: false, - }); - - items = product.items; - - assert.equal(items.length, 5); - }); - - // 1. Update metered feature and price - it("should update metered feature and fixed price correctly", async function () { - items[1].included_usage = 200; - - items[3].price = 20; - items[3].interval = BillingInterval.OneOff as any; - - await autumn.products.update("product-1", { - items: items, - }); - - let product = await autumn.products.get("product-1", { - v1Schema: true, - }); - - let metered1Ent = product.entitlements.find( - (ent: any) => ent.id === items[1].entitlement_id, - ); - - assert.equal(metered1Ent.allowance, 200); - - let price = product.prices.find( - (price: any) => price.id === items[3].price_id, - ); - - assert.equal(price.config.amount, 20); - assert.equal(price.config.interval, BillingInterval.OneOff); - }); -}); - -describe(`${chalk.yellowBright( - "product1: Testing attach and update product", -)}`, () => { - let autumn: Autumn; - let customerId = "product-1-customer"; - before(async function () { - await setupBefore(this); - autumn = this.autumn; - - await initCustomer({ - customerId, - db: this.db, - org: this.org, - env: this.env, - attachPm: true, - }); - - await autumn.attach({ - customerId, - productId: "product-1", - }); - }); -}); diff --git a/server/tests/contUse/entities/entity1.ts b/server/tests/contUse/entities/entity1.ts index c2aaa9cfa..78e8a2aa0 100644 --- a/server/tests/contUse/entities/entity1.ts +++ b/server/tests/contUse/entities/entity1.ts @@ -41,7 +41,7 @@ const testCase = "entity1"; // Pro is $20 / month, Seat is $50 / user -describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing create / delete entities`)}`, () => { +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing create / delete entities`)}`, () => { let customerId = testCase; let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); let testClockId: string; @@ -150,7 +150,7 @@ describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing create / de }); let customer = await autumn.customers.get(customerId); - let invoices = customer.invoices; + let invoices = customer.invoices!; expect(invoices.length).to.equal(2); expect(invoices[0].total).to.equal(userItem.price! * entities.length); }); @@ -159,7 +159,7 @@ describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing create / de await autumn.entities.delete(customerId, entities[0].id); let customer = await autumn.customers.get(customerId); - let invoices = customer.invoices; + let invoices = customer.invoices!; expect(invoices.length).to.equal(2); await expectSubQuantityCorrect({ @@ -194,7 +194,7 @@ describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing create / de usage += 1; let customer = await autumn.customers.get(customerId); - let invoices = customer.invoices; + let invoices = customer.invoices!; expect(invoices.length).to.equal(3); expect(invoices[0].total).to.equal(userItem.price!); diff --git a/server/tests/contUse/entities/entity2.ts b/server/tests/contUse/entities/entity2.ts index 85755850b..b387fd451 100644 --- a/server/tests/contUse/entities/entity2.ts +++ b/server/tests/contUse/entities/entity2.ts @@ -41,7 +41,7 @@ export let pro = constructProduct({ const testCase = "entity2"; -describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing entities, prorate now`)}`, () => { +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing entities, prorate now`)}`, () => { let customerId = testCase; let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); let testClockId: string; diff --git a/server/tests/contUse/entities/entity3.ts b/server/tests/contUse/entities/entity3.ts index f4d397eac..9888535e5 100644 --- a/server/tests/contUse/entities/entity3.ts +++ b/server/tests/contUse/entities/entity3.ts @@ -17,11 +17,11 @@ import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { expect } from "chai"; -import { expectSubQuantityCorrect } from "../../attach/entities/expectEntity.js"; import { addHours, addMonths, addWeeks } from "date-fns"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; +import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; let userItem = constructArrearProratedItem({ featureId: TestFeature.Users, @@ -40,7 +40,7 @@ export let pro = constructProduct({ const testCase = "entity3"; -describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing replaceables deleted at end of cycle`)}`, () => { +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing replaceables deleted at end of cycle`)}`, () => { let customerId = testCase; let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); let testClockId: string; @@ -128,7 +128,7 @@ describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing replaceable stripeCli, testClockId, advanceTo: addWeeks(new Date(), 2).getTime(), - waitForSeconds: 10, + waitForSeconds: 30, }); await autumn.entities.delete(customerId, firstEntities[0].id); @@ -148,7 +148,7 @@ describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing replaceable }); let customer = await autumn.customers.get(customerId); - let invoices = customer.invoices; + let invoices = customer.invoices!; expect(invoices.length).to.equal(1); }); diff --git a/server/tests/contUse/entities/entity4.ts b/server/tests/contUse/entities/entity4.ts index 94ce7353c..fe5b51421 100644 --- a/server/tests/contUse/entities/entity4.ts +++ b/server/tests/contUse/entities/entity4.ts @@ -49,7 +49,7 @@ export let pro = constructProduct({ const testCase = "entity4"; -describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing per entity features`)}`, () => { +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing per entity features`)}`, () => { let customerId = testCase; let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); let testClockId: string; @@ -152,6 +152,7 @@ describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing per entity (perEntityItem.included_usage as number) * usage, ); + // @ts-ignore for (const entity of customer.entities) { let entRes = await autumn.check({ customer_id: customerId, diff --git a/server/tests/contUse/track/track1.ts b/server/tests/contUse/track/track1.ts index 746738b7c..5316a3160 100644 --- a/server/tests/contUse/track/track1.ts +++ b/server/tests/contUse/track/track1.ts @@ -22,7 +22,7 @@ import { timeout } from "@/utils/genUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { addPrefixToProducts } from "tests/attach/utils.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectSubQuantityCorrect } from "tests/attach/entities/expectEntity.js"; +import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; let userItem = constructArrearProratedItem({ featureId: TestFeature.Users, @@ -153,6 +153,7 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing track usage for con customerId, usage, numReplaceables: 3, + itemQuantity: usage - 3, }); }); diff --git a/server/tests/contUse/track/track2.ts b/server/tests/contUse/track/track2.ts index 593a0ba37..f71521f09 100644 --- a/server/tests/contUse/track/track2.ts +++ b/server/tests/contUse/track/track2.ts @@ -3,7 +3,6 @@ import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; import { APIVersion, AppEnv, - entities, OnDecrease, OnIncrease, Organization, @@ -17,12 +16,10 @@ import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { expect } from "chai"; -import { addWeeks } from "date-fns"; -import { timeout } from "@/utils/genUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; + import { addPrefixToProducts } from "tests/attach/utils.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectSubQuantityCorrect } from "tests/attach/entities/expectEntity.js"; +import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; let userItem = constructArrearProratedItem({ featureId: TestFeature.Users, diff --git a/server/tests/contUse/track/track4.ts b/server/tests/contUse/track/track4.ts index eb8c7511d..8a5cece07 100644 --- a/server/tests/contUse/track/track4.ts +++ b/server/tests/contUse/track/track4.ts @@ -23,7 +23,7 @@ import { timeout } from "@/utils/genUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { addPrefixToProducts } from "tests/attach/utils.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectSubQuantityCorrect } from "tests/attach/entities/expectEntity.js"; +import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; import { expectUpcomingItemsCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; let userItem = constructArrearProratedItem({ diff --git a/server/tests/contUse/track/track5.ts b/server/tests/contUse/track/track5.ts new file mode 100644 index 000000000..6ac56d791 --- /dev/null +++ b/server/tests/contUse/track/track5.ts @@ -0,0 +1,258 @@ +import chalk from "chalk"; +import Stripe from "stripe"; + +import { expect } from "chai"; +import { features } from "tests/global.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; + +import { addDays, addHours } from "date-fns"; + +import { Decimal } from "decimal.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { setupBefore } from "tests/before.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { AppEnv, OnDecrease, OnIncrease, Organization } from "@autumn/shared"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; +import { + addPrefixToProducts, + getBasePrice, +} from "tests/utils/testProductUtils/testProductUtils.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { defaultApiVersion } from "tests/constants.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { getSubsFromCusId } from "tests/utils/expectUtils/expectSubUtils.js"; +import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; + +const seatsItem = constructArrearProratedItem({ + featureId: features.seats.id, + pricePerUnit: 20, + includedUsage: 3, + config: { + on_increase: OnIncrease.ProrateNextCycle, + on_decrease: OnDecrease.ProrateNextCycle, + }, +}); + +const seatsProduct = constructProduct({ + type: "pro", + items: [seatsItem], +}); + +const testCase = "track5"; +const includedUsage = seatsItem.included_usage as number; + +const simulateOneCycle = async ({ + customerId, + db, + org, + env, + stripeCli, + curUnix, + usageValues, + autumn, + testClockId, +}: { + customerId: string; + db: DrizzleCli; + org: Organization; + env: AppEnv; + stripeCli: Stripe; + curUnix: number; + usageValues: number[]; + autumn: AutumnInt; + testClockId: string; +}) => { + const { subs } = await getSubsFromCusId({ + customerId, + db, + org, + env, + stripeCli, + productId: seatsProduct.id, + }); + + let sub = subs[0]; + + let accruedPrice = 0; + for (const usageValue of usageValues) { + let daysToAdvance = Math.round(Math.random() * 10) + 1; + curUnix = await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addDays(curUnix, daysToAdvance).getTime(), + waitForSeconds: 10, + }); + + let customer = await autumn.customers.get(customerId); + let prevBalance = customer.features[seatsItem.feature_id!].balance!; + let prevUsage = includedUsage - prevBalance; + + let usageDiff = usageValue - prevUsage; + + let value1 = Math.floor(usageDiff / 2); + let value2 = usageDiff - value1; + + await autumn.track({ + customer_id: customerId, + feature_id: seatsItem.feature_id!, + value: value1, + }); + + await autumn.track({ + customer_id: customerId, + feature_id: seatsItem.feature_id!, + value: value2, + }); + + let newBalance = includedUsage - usageValue; + let prevOverage = Math.max(0, -prevBalance); + let newOverage = Math.max(0, -newBalance); + + let newPrice = (newOverage - prevOverage) * seatsItem.price!; + + let proratedPrice = calculateProrationAmount({ + periodStart: sub.current_period_start * 1000, + periodEnd: sub.current_period_end * 1000, + now: curUnix, + amount: newPrice, + allowNegative: true, + }); + + accruedPrice = new Decimal(accruedPrice).plus(proratedPrice).toNumber(); + } + + let customer = await autumn.customers.get(customerId); + let balance = customer.features[seatsItem.feature_id!].balance!; + + let overage = Math.min(0, includedUsage - balance); + let usagePrice = overage * seatsItem.price!; + let basePrice = getBasePrice({ product: seatsProduct }); + + const totalPrice = new Decimal(accruedPrice) + .plus(usagePrice) + .plus(basePrice) + .toDecimalPlaces(2) + .toNumber(); + + curUnix = await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addHours( + sub.current_period_end * 1000, + hoursToFinalizeInvoice, + ).getTime(), + waitForSeconds: 30, + }); + + let cusAfter = await autumn.customers.get(customerId); + let invoices = cusAfter.invoices; + let invoice = invoices[0]; + + expect(invoice.total).to.approximately( + totalPrice, + 0.01, + `Invoice total should be ${totalPrice} +/- 0.01`, + ); + + return { + curUnix, + }; +}; + +describe(`${chalk.yellowBright("conUse/track5: Testing update cont use through /usage")}`, () => { + const customerId = testCase; + + let stripeCli: Stripe; + + let testClockId = ""; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + let autumn = new AutumnInt({ version: defaultApiVersion }); + let curUnix = Date.now(); + + before(async function () { + await setupBefore(this); + org = this.org; + env = this.env; + db = this.db; + + let res = await initCustomer({ + customerId, + org, + env, + db, + autumn: this.autumnJs, + attachPm: "success", + }); + + addPrefixToProducts({ + products: [seatsProduct], + prefix: testCase, + }); + + await createProducts({ + products: [seatsProduct], + orgId: org.id, + env, + db, + autumn, + }); + + testClockId = res.testClockId; + + db = this.db; + org = this.org; + env = this.env; + stripeCli = this.stripeCli; + }); + + it("should attach in arrear prorated seats", async () => { + await attachAndExpectCorrect({ + customerId, + product: seatsProduct, + db, + org, + env, + autumn, + stripeCli, + }); + }); + + // return; + + it("simulate first cycle and have correct invoice / balance", async () => { + let res = await simulateOneCycle({ + customerId, + db, + org, + env, + stripeCli, + curUnix, + usageValues: [8, 2], + autumn, + testClockId, + }); + + curUnix = res.curUnix; + }); + + it("simulate second cycle and have correct invoice / balance", async () => { + let res = await simulateOneCycle({ + customerId, + db, + org, + env, + stripeCli, + curUnix, + usageValues: [12, 3], + autumn, + testClockId, + }); + + curUnix = res.curUnix; + }); +}); diff --git a/server/tests/contUse/update/updateContUse1.ts b/server/tests/contUse/update/updateContUse1.ts index 8dfc42d40..4c6cec033 100644 --- a/server/tests/contUse/update/updateContUse1.ts +++ b/server/tests/contUse/update/updateContUse1.ts @@ -20,7 +20,7 @@ import { addWeeks } from "date-fns"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { addPrefixToProducts, replaceItems } from "tests/attach/utils.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectSubQuantityCorrect } from "tests/attach/entities/expectEntity.js"; +import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; let userItem = constructArrearProratedItem({ featureId: TestFeature.Users, diff --git a/server/tests/contUse/update/updateContUse2.ts b/server/tests/contUse/update/updateContUse2.ts index b6fb8c7d6..a0ca62769 100644 --- a/server/tests/contUse/update/updateContUse2.ts +++ b/server/tests/contUse/update/updateContUse2.ts @@ -20,7 +20,7 @@ import { addWeeks } from "date-fns"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { addPrefixToProducts, replaceItems } from "tests/attach/utils.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { expectSubQuantityCorrect } from "tests/attach/entities/expectEntity.js"; +import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; let userItem = constructArrearProratedItem({ featureId: TestFeature.Users, diff --git a/server/tests/utils/advancedUsageUtils.ts b/server/tests/utils/advancedUsageUtils.ts index 4fa5d1db8..8c76d9e4a 100644 --- a/server/tests/utils/advancedUsageUtils.ts +++ b/server/tests/utils/advancedUsageUtils.ts @@ -81,7 +81,6 @@ export const checkUsageInvoiceAmount = async ({ ); let meteredPrice = product.prices[product.prices.length - 1]; - // let overage = Math.round(totalUsage - featureEntitlement.allowance); let overage = new Decimal(totalUsage) .minus(featureEntitlement.allowance) .toNumber(); @@ -99,15 +98,8 @@ export const checkUsageInvoiceAmount = async ({ try { for (let i = 0; i < invoices.length; i++) { let invoice = invoices[i]; - // console.log( - // "Invoice total: ", - // invoice.total, - // "Product id: ", - // invoice.product_ids[0] - // ); if (invoice.total == totalPrice) { invoiceIndex = i; - // console.log(" - Found correct total price at index: ", invoiceIndex); assert.equal(invoice.product_ids[0], product.id); return; } diff --git a/server/tests/utils/compare.ts b/server/tests/utils/compare.ts index f498ffa78..eae49607c 100644 --- a/server/tests/utils/compare.ts +++ b/server/tests/utils/compare.ts @@ -47,7 +47,7 @@ export const compareMainProduct = ({ }) => { const { products, add_ons, entitlements } = cusRes; const prod = products.find( - (p: any) => p.id === sent.id && p.status == status && !sent.is_add_on + (p: any) => p.id === sent.id && p.status == status && !sent.is_add_on, ); try { @@ -76,14 +76,14 @@ export const compareMainProduct = ({ // If options list provideed, and feature let options = optionsList.find( - (o: any) => o.feature_id === entitlement.feature_id + (o: any) => o.feature_id === entitlement.feature_id, ); let expectedBalance = entitlement.allowance; if (options?.quantity) { // Get price from sent const price = sent.prices.find( - (p: any) => p.config.feature_id === entitlement.feature_id + (p: any) => p.config.feature_id === entitlement.feature_id, ); const config = price.config as UsagePriceConfig; expectedBalance = new Decimal(expectedBalance || 0) @@ -102,7 +102,7 @@ export const compareMainProduct = ({ } } catch (error) { console.log( - `Failed to compare main product (entitlements) ${entitlement.feature_id}` + `Failed to compare main product (entitlements) ${entitlement.feature_id}`, ); console.log("Looking for entitlement: ", entitlement); console.log("Received entitlements: ", entitlements); @@ -134,20 +134,20 @@ export const checkFeatureHasCorrectBalance = async ({ return; } - console.log( - ` - Checking entitlement ${feature.id} has ${ - entitlement.allowance_type == AllowanceType.Unlimited - ? "unlimited balance" - : `balance of ${expectedBalance}` - }` - ); + // console.log( + // ` - Checking entitlement ${feature.id} has ${ + // entitlement.allowance_type == AllowanceType.Unlimited + // ? "unlimited balance" + // : `balance of ${expectedBalance}` + // }` + // ); // Get ent from cusRes const { entitlements: cusEnts }: any = cusRes; const { allowed, balanceObj }: any = entitledRes; const cusEnt = cusEnts.find( (e: any) => - e.feature_id === feature.id && e.interval == entitlement.interval + e.feature_id === feature.id && e.interval == entitlement.interval, ); expect(cusEnt).to.exist; @@ -188,7 +188,7 @@ export const compareProductEntitlements = ({ quantity?: number; }) => { for (const entitlement of Object.values( - product.entitlements + product.entitlements, ) as Entitlement[]) { let feature = features[entitlement.feature_id!] || diff --git a/server/tests/utils/cusProductUtils/cusProductUtils.ts b/server/tests/utils/cusProductUtils/cusProductUtils.ts index e0f2c1595..40af28069 100644 --- a/server/tests/utils/cusProductUtils/cusProductUtils.ts +++ b/server/tests/utils/cusProductUtils/cusProductUtils.ts @@ -1,8 +1,6 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; -import { Autumn } from "@/external/autumn/autumnCli.js"; import { CusService } from "@/internal/customers/CusService.js"; import { AppEnv, CusProductStatus, FullCusProduct } from "@autumn/shared"; -import { SupabaseClient } from "@supabase/supabase-js"; export const getMainCusProduct = async ({ db, diff --git a/server/tests/utils/expectUtils/expectAttach.ts b/server/tests/utils/expectUtils/expectAttach.ts index 1cc39f358..51d92c527 100644 --- a/server/tests/utils/expectUtils/expectAttach.ts +++ b/server/tests/utils/expectUtils/expectAttach.ts @@ -33,7 +33,7 @@ export const attachAndExpectCorrect = async ({ waitForInvoice = 0, isCanceled = false, skipFeatureCheck = false, - numSubs = 1, + numSubs, }: { autumn: AutumnInt; customerId: string; @@ -141,7 +141,7 @@ export const attachAndExpectCorrect = async ({ let cus = await autumn.customers.get(customerId); const stripeSubs = await stripeCli.subscriptions.list({ - customer: cus.stripe_id, + customer: cus.stripe_id!, }); if (numSubs) { diff --git a/server/tests/utils/expectUtils/expectContUseUtils.ts b/server/tests/utils/expectUtils/expectContUseUtils.ts index e7acf910e..43fd3ff0a 100644 --- a/server/tests/utils/expectUtils/expectContUseUtils.ts +++ b/server/tests/utils/expectUtils/expectContUseUtils.ts @@ -10,6 +10,7 @@ import { expect } from "chai"; import { TestFeature } from "tests/setup/v2Features.js"; import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { notNullish } from "@/utils/genUtils.js"; export const expectSubQuantityCorrect = async ({ stripeCli, @@ -59,7 +60,10 @@ export const expectSubQuantityCorrect = async ({ }); expect(subItem).to.exist; - expect(subItem!.quantity).to.equal(itemQuantity || usage); + + expect(subItem!.quantity).to.equal( + notNullish(itemQuantity) ? itemQuantity : usage, + ); // Check num replaceables correct let cusEnts = cusProduct?.customer_entitlements; @@ -111,13 +115,13 @@ export const expectUpcomingItemsCorrect = async ({ allowNegative: true, }); - console.group(); - console.group("Upcoming lines"); - for (const line of lines) { - console.log(line.description, line.amount / 100); - } - console.groupEnd(); - console.groupEnd(); + // console.group(); + // console.group("Upcoming lines"); + // for (const line of lines) { + // console.log(line.description, line.amount / 100); + // } + // console.groupEnd(); + // console.groupEnd(); expect(lines[0].amount).to.equal(Math.round(proratedAmount * 100)); }; diff --git a/server/tests/utils/productUtils.ts b/server/tests/utils/productUtils.ts index 4a21c8dec..8d478e675 100644 --- a/server/tests/utils/productUtils.ts +++ b/server/tests/utils/productUtils.ts @@ -1,7 +1,8 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { ProductService } from "@/internal/products/ProductService.js"; -import { AppEnv, Product, ProductV2 } from "@autumn/shared"; +import { AppEnv, CreateReward, Product, ProductV2 } from "@autumn/shared"; import { DrizzleCli } from "@/db/initDrizzle.js"; +import { isUsagePrice } from "@/internal/products/prices/priceUtils/usagePriceUtils.js"; export const createProduct = async ({ db, @@ -54,6 +55,7 @@ export const createProduct = async ({ await autumn.products.create(clone); }; + export const createProducts = async ({ db, orgId, @@ -86,3 +88,42 @@ export const createProducts = async ({ await Promise.all(batchCreate); }; + +export const createReward = async ({ + db, + orgId, + env, + autumn, + reward, + productId, + onlyUsage = false, +}: { + db: DrizzleCli; + orgId: string; + env: AppEnv; + autumn: AutumnInt; + reward: CreateReward; + productId: string; + onlyUsage?: boolean; +}) => { + let fullProduct = await ProductService.getFull({ + db, + orgId, + env, + idOrInternalId: productId!, + }); + + let usagePrices = fullProduct.prices.filter((price) => + isUsagePrice({ price }), + ); + + if (onlyUsage) { + reward.discount_config!.price_ids = usagePrices.map((price) => price.id); + } + + try { + await autumn.rewards.delete(reward.id); + } catch (error) {} + + await autumn.rewards.create(reward); +}; diff --git a/server/tests/utils/setup.ts b/server/tests/utils/setup.ts index 93e810a35..69ee51660 100644 --- a/server/tests/utils/setup.ts +++ b/server/tests/utils/setup.ts @@ -30,6 +30,7 @@ import { ProductService } from "@/internal/products/ProductService.js"; import { RewardService } from "@/internal/rewards/RewardService.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; import { features as v2Features } from "tests/setup/v2Features.js"; +import { timeout } from "./genUtils.js"; export const getAxiosInstance = ( apiKey: string = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!, @@ -143,8 +144,7 @@ export const clearOrg = async ({ active: true, }); - const batchSize = 5; - + const batchSize = 15; const removeStripeProduct = async (product: Stripe.Product) => { try { await stripeCli.products.del(product.id); @@ -162,6 +162,7 @@ export const clearOrg = async ({ batchDeleteProducts.push(removeStripeProduct(product)); } await Promise.all(batchDeleteProducts); + await timeout(800); console.log( ` ✅ Deleted ${i + batch.length}/${ stripeProducts.data.length diff --git a/server/tests/utils/stripeUtils.ts b/server/tests/utils/stripeUtils.ts index 193f56666..9bf901f0f 100644 --- a/server/tests/utils/stripeUtils.ts +++ b/server/tests/utils/stripeUtils.ts @@ -425,13 +425,19 @@ export const getUsageInArrearPrice = async ({ export const getDiscount = async ({ stripeCli, customer, + stripeId, }: { stripeCli: Stripe; - customer: Customer; + customer?: Customer; + stripeId?: string; }) => { const stripeCustomer: any = await stripeCli.customers.retrieve( - customer.processor!.id, + stripeId || customer!.processor!.id, + { + expand: ["discount.coupon"], + }, ); + return stripeCustomer.discount; }; diff --git a/server/tests/utils/testProductUtils/testProductUtils.ts b/server/tests/utils/testProductUtils/testProductUtils.ts index ab3355f5b..39f66f79d 100644 --- a/server/tests/utils/testProductUtils/testProductUtils.ts +++ b/server/tests/utils/testProductUtils/testProductUtils.ts @@ -1,6 +1,14 @@ +import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils.js"; import { isPriceItem } from "@/internal/products/product-items/productItemUtils/getItemType.js"; import { nullish } from "@/utils/genUtils.js"; -import { BillingInterval, ProductItem, ProductV2 } from "@autumn/shared"; +import { + BillingInterval, + FixedPriceConfig, + FullProduct, + Price, + ProductItem, + ProductV2, +} from "@autumn/shared"; export const addPrefixToProducts = ({ products, @@ -54,3 +62,10 @@ export const replaceItems = ({ export const getBasePrice = ({ product }: { product: ProductV2 }) => { return product.items.find((item) => isPriceItem(item))?.price || 0; }; + +export const v1ProductToBasePrice = ({ prices }: { prices: Price[] }) => { + let fixedPrice = prices.find((price) => isFixedPrice({ price })); + if (fixedPrice) { + return (fixedPrice.config as FixedPriceConfig).amount; + } else return 0; +}; diff --git a/shared/models/rewardModels/rewardModels/rewardModels.ts b/shared/models/rewardModels/rewardModels/rewardModels.ts index 2feb6ba39..646b5873f 100644 --- a/shared/models/rewardModels/rewardModels/rewardModels.ts +++ b/shared/models/rewardModels/rewardModels/rewardModels.ts @@ -33,7 +33,7 @@ const RewardSchema = z.object({ export const CreateRewardSchema = z.object({ name: z.string(), promo_codes: z.array(PromoCodeSchema), - id: z.string().nullish(), + id: z.string(), type: z.nativeEnum(RewardType).nullish(), discount_config: DiscountConfigSchema.nullish(), free_product_id: z.string().nullish(),