diff --git a/server/shell/g4.sh b/server/shell/g4.sh index 654df5cae..75aa8212b 100755 --- a/server/shell/g4.sh +++ b/server/shell/g4.sh @@ -12,7 +12,8 @@ $MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \ 'tests/advanced/coupons/*.ts' \ 'tests/attach/updateQuantity/*.ts' \ 'tests/advanced/referrals/*.ts' \ - 'tests/advanced/rollovers/*.ts' + 'tests/advanced/rollovers/*.ts' \ + 'tests/advanced/customInterval/*.ts' # $MOCHA_CMD 'tests/attach/multiProduct/*.ts' \ # 'tests/advanced/usageLimit/*.ts' diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts index 92922a3ed..0557c6ad2 100644 --- a/server/src/external/autumn/autumnCli.ts +++ b/server/src/external/autumn/autumnCli.ts @@ -12,6 +12,8 @@ import { } from "@autumn/shared"; import { CancelParams, + CheckoutParams, + CheckoutResult, CheckParams, CheckResult, Customer, @@ -168,6 +170,16 @@ export class AutumnInt { return data; } + async checkout(params: CheckoutParams) { + // const data = await this.post(`/attach`, { + // customer_id: customerId, + // product_id: productId, + // options: toSnakeCase(options), + // }); + const data = await this.post(`/checkout`, params); + + return data as CheckoutResult; + } async sendEvent({ customerId, diff --git a/server/src/external/stripe/stripePriceUtils.ts b/server/src/external/stripe/stripePriceUtils.ts index 4a7f5e3d5..ccbc811bf 100644 --- a/server/src/external/stripe/stripePriceUtils.ts +++ b/server/src/external/stripe/stripePriceUtils.ts @@ -32,6 +32,11 @@ export const billingIntervalToStripe = ({ }) => { const finalCount = intervalCount ?? 1; switch (interval) { + case BillingInterval.Week: + return { + interval: "week", + interval_count: finalCount, + }; case BillingInterval.Month: return { interval: "month", diff --git a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts index d40ca6fe0..526745120 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts @@ -273,6 +273,7 @@ const getChangeProductBranch = async ({ // } let isUpgrade = isProductUpgrade({ prices1: curPrices, prices2: newPrices }); + if (isUpgrade) { if (isTrialing(curMainProduct!)) { return AttachBranch.MainIsTrial; diff --git a/server/src/internal/customers/change-product/billRemainingUsages.ts b/server/src/internal/customers/change-product/billRemainingUsages.ts deleted file mode 100644 index ae69888b5..000000000 --- a/server/src/internal/customers/change-product/billRemainingUsages.ts +++ /dev/null @@ -1,410 +0,0 @@ -import { Stripe } from "stripe"; -import { AttachParams } from "../cusProducts/AttachParams.js"; -import { FullCusProduct, InvoiceItem } from "@autumn/shared"; -import { BillingInterval, BillingType, UsagePriceConfig } from "@autumn/shared"; - -import { - getBillingType, - getPriceEntitlement, - getPriceForOverage, -} from "@/internal/products/prices/priceUtils.js"; -import { - createStripeCli, - subToAutumnInterval, -} from "@/external/stripe/utils.js"; -import { CusEntService } from "../cusProducts/cusEnts/CusEntitlementService.js"; -import { payForInvoice } from "@/external/stripe/stripeInvoiceUtils.js"; -import { getResetBalancesUpdate } from "../cusProducts/cusEnts/groupByUtils.js"; -import { - getCusPriceUsage, - getRelatedCusEnt, -} from "../cusProducts/cusPrices/cusPriceUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js"; -import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js"; -import { - cusProductsToCusPrices, - cusProductToEnts, -} from "../cusProducts/cusProductUtils/convertCusProduct.js"; -import { getUsageBasedSub } from "@/external/stripe/stripeSubUtils.js"; - -// Add usage to end of cycle -// const addUsageToNextInvoice = async ({ -// db, -// intervalToInvoiceItems, -// intervalToSub, -// customer, -// org, -// logger, -// attachParams, -// }: { -// db: DrizzleCli; -// intervalToInvoiceItems: any; -// intervalToSub: any; -// customer: any; -// org: any; -// logger: any; -// attachParams: AttachParams; -// }) => { -// for (const interval in intervalToInvoiceItems) { -// const itemsToInvoice = intervalToInvoiceItems[interval]; - -// if (itemsToInvoice.length === 0) { -// continue; -// } - -// // Add items to invoice -// const stripeCli = createStripeCli({ -// org: org, -// env: customer.env, -// }); - -// for (const item of itemsToInvoice) { -// const { amount, description } = item; - -// logger.info( -// ` feature: ${item.feature.id}, overage: ${item.overage}, amount: ${amount}`, -// ); - -// let relatedSub = intervalToSub[interval]; -// if (!relatedSub) { -// continue; -// } - -// // Create invoice item -// let invoiceItem = { -// customer: customer.processor.id, -// currency: org.default_currency, -// description, -// price_data: { -// product: (item.price.config! as UsagePriceConfig).stripe_product_id!, -// unit_amount: Math.round(amount * 100), -// currency: org.default_currency, -// }, -// subscription: relatedSub.id, -// period: { -// start: item.periodStart, -// end: item.periodEnd, -// }, -// }; - -// await stripeCli.invoiceItems.create(invoiceItem); - -// // Update cus ent to 0 -// await CusEntService.update({ -// db, -// id: item.relatedCusEnt!.id, -// updates: getResetBalancesUpdate({ -// cusEnt: item.relatedCusEnt!, -// allowance: 0, -// }), -// }); - -// // Update existing cusEnt in attachParams -// let cusProducts = attachParams.cusProducts; -// for (const cusProduct of cusProducts!) { -// for (let i = 0; i < cusProduct.customer_entitlements.length; i++) { -// let cusEnt = cusProduct.customer_entitlements[i]; -// if (cusEnt.id === item.relatedCusEnt!.id) { -// let balancesUpdate = getResetBalancesUpdate({ -// cusEnt, -// allowance: 0, -// }); -// cusProduct.customer_entitlements[i] = { -// ...cusEnt, -// ...balancesUpdate, -// }; -// } -// } -// } -// } -// } -// }; - -// const invoiceForUsageImmediately = async ({ -// db, -// intervalToInvoiceItems, -// customer, -// org, -// logger, -// curCusProduct, -// attachParams, -// newSubs, -// }: { -// db: DrizzleCli; -// intervalToInvoiceItems: any; -// customer: any; -// org: any; -// logger: any; -// curCusProduct: FullCusProduct; -// attachParams: AttachParams; -// newSubs: Stripe.Subscription[]; -// }) => { -// // 1. Create invoice -// const stripeCli = createStripeCli({ -// org: org, -// env: customer.env, -// }); -// const product = curCusProduct.product; - -// let invoiceItems = Object.values(intervalToInvoiceItems).flat() as any[]; -// if (invoiceItems.length === 0) { -// return; -// } - -// let invoice: Stripe.Invoice; -// let newInvoice = false; - -// if (attachParams.invoiceOnly && newSubs.length > 0) { -// invoice = await stripeCli.invoices.retrieve( -// newSubs[0].latest_invoice as string, -// ); - -// if (invoice.status !== "draft") { -// newInvoice = true; -// invoice = await stripeCli.invoices.create({ -// customer: customer.processor.id, -// auto_advance: true, -// }); -// } -// } else { -// newInvoice = true; - -// invoice = await stripeCli.invoices.create({ -// customer: customer.processor.id, -// auto_advance: true, -// }); -// } - -// let autumnInvoiceItems: InvoiceItem[] = []; - -// for (const item of invoiceItems) { -// // const amount = getPriceForOverage(item.price, item.overage); -// const { amount, description } = item; -// let config = item.price.config! as UsagePriceConfig; -// // let stripePrice = await stripeCli.prices.retrieve(config.stripe_price_id!); -// let stripeProdId = config.stripe_product_id; -// if (!stripeProdId) { -// try { -// let stripePrice = await stripeCli.prices.retrieve( -// config.stripe_price_id!, -// ); -// stripeProdId = stripePrice.product as string; -// } catch (error) {} -// } - -// if (!stripeProdId) { -// stripeProdId = product.processor?.id; -// } - -// logger.info( -// `🌟🌟🌟 (Bill remaining) created invoice item: ${description} -- ${amount}`, -// ); - -// let invoiceItem = { -// customer: customer.processor.id, -// invoice: invoice.id, -// currency: org.default_currency, -// description, -// price_data: { -// product: stripeProdId!, -// unit_amount: Math.round(amount * 100), -// currency: org.default_currency, -// }, -// period: { -// start: item.periodStart, -// end: item.periodEnd, -// }, -// }; - -// let stripeInvoiceItem = await stripeCli.invoiceItems.create(invoiceItem); - -// autumnInvoiceItems.push({ -// price_id: item.price.id!, -// internal_feature_id: item.feature.internal_id || null, -// description: description, -// period_start: item.periodStart * 1000, -// period_end: item.periodEnd * 1000, -// stripe_id: stripeInvoiceItem.id, -// }); - -// await CusEntService.update({ -// db, -// id: item.relatedCusEnt!.id, -// updates: { -// balance: 0, -// }, -// }); -// let index = curCusProduct.customer_entitlements.findIndex( -// (ce) => ce.id === item.relatedCusEnt!.id, -// ); - -// curCusProduct.customer_entitlements[index] = { -// ...curCusProduct.customer_entitlements[index], -// balance: 0, -// }; -// } - -// if (newInvoice) { -// await stripeCli.invoices.finalizeInvoice(invoice.id); - -// const { paid, error } = await payForInvoice({ -// stripeCli, -// paymentMethod: null, -// invoiceId: invoice.id, -// logger, -// }); - -// if (!paid) { -// logger.warn("Failed to pay invoice for remaining usages", { -// stripeInvoice: newInvoice, -// paymentError: error, -// }); -// } -// } - -// await insertInvoiceFromAttach({ -// db, -// attachParams, -// invoiceId: invoice.id, -// logger, -// }); -// }; - -// const getRemainingUsagesPreview = async ({ -// intervalToInvoiceItems, -// curCusProduct, -// }: { -// intervalToInvoiceItems: any; -// curCusProduct: FullCusProduct; -// }) => { -// let invoiceItems = Object.values(intervalToInvoiceItems).flat() as any[]; -// if (invoiceItems.length === 0) { -// return; -// } - -// let items = []; -// for (const item of invoiceItems) { -// const amount = getPriceForOverage(item.price, item.overage); -// const description = `${curCusProduct.product.name} - ${ -// item.feature.name -// } x ${Math.round(item.usage)}`; - -// items.push({ -// amount, -// description, -// }); -// } - -// return items; -// }; - -// export const billForRemainingUsages = async ({ -// db, -// logger, -// attachParams, -// curCusProduct, -// newSubs, -// shouldPreview = false, -// billImmediately = false, -// }: { -// db: DrizzleCli; -// logger: any; -// attachParams: AttachParams; -// curCusProduct: FullCusProduct; -// newSubs: Stripe.Subscription[]; -// shouldPreview?: boolean; -// billImmediately?: boolean; -// }) => { -// const { customer_prices, customer_entitlements } = curCusProduct; -// const { customer, org } = attachParams; - -// const intervalToSub: any = {}; - -// for (const sub of newSubs) { -// const interval = subToAutumnInterval(sub); -// if (interval) { -// intervalToSub[interval] = sub; -// } -// } - -// const intervalToInvoiceItems: any = {}; -// const stripeCli = createStripeCli({ -// org: org, -// env: customer.env, -// }); - -// for (const cp of customer_prices) { -// const config = cp.price.config! as UsagePriceConfig; -// const relatedCusEnt = getRelatedCusEnt({ -// cusPrice: cp, -// cusEnts: customer_entitlements, -// }); -// const billingType = getBillingType(config); - -// if (billingType !== BillingType.UsageInArrear) continue; - -// const { usage, overage, description, amount } = getCusPriceUsage({ -// cusPrice: cp, -// cusProduct: curCusProduct, -// logger, -// }); - -// if (overage <= 0) continue; // no overage, no need to bill... - -// let interval = config.interval as BillingInterval; -// if (!intervalToInvoiceItems[interval]) { -// intervalToInvoiceItems[interval] = []; -// } - -// let sub = intervalToSub[interval]; - -// const stripeNow = await getStripeNow({ -// stripeCli, -// stripeSub: sub, -// }); - -// intervalToInvoiceItems[interval].push({ -// overage, -// usage, -// description, -// amount, - -// feature: relatedCusEnt?.entitlement.feature, -// price: cp.price, -// relatedCusEnt, -// periodStart: sub?.current_period_start, -// periodEnd: stripeNow, -// }); -// } - -// if (shouldPreview) { -// return getRemainingUsagesPreview({ -// intervalToInvoiceItems, -// curCusProduct, -// }); -// } - -// if (billImmediately) { -// await invoiceForUsageImmediately({ -// db, -// intervalToInvoiceItems, -// customer, -// org, -// logger, -// curCusProduct, -// attachParams, -// newSubs, -// }); -// } else { -// await addUsageToNextInvoice({ -// db, -// intervalToInvoiceItems, -// intervalToSub, -// customer, -// org, -// logger, -// attachParams, -// }); -// } -// }; diff --git a/server/src/internal/customers/cusUtils/createNewCustomer.ts b/server/src/internal/customers/cusUtils/createNewCustomer.ts index 471c3ef83..537a17620 100644 --- a/server/src/internal/customers/cusUtils/createNewCustomer.ts +++ b/server/src/internal/customers/cusUtils/createNewCustomer.ts @@ -41,7 +41,7 @@ export const createNewCustomer = async ({ const { db, org, env, logger } = req; logger.info( - `Creating customer: ${customer.email || customer.id}, org: ${org.slug}`, + `Creating customer: ${customer.email || customer.id}, org: ${org.slug}` ); const defaultProds = await ProductService.listDefault({ @@ -149,7 +149,10 @@ export const createNewCustomer = async ({ }), nextResetAt, anchorToUnix: org.config.anchor_start_of_month - ? getNextStartOfMonthUnix(BillingInterval.Month) + ? getNextStartOfMonthUnix({ + interval: BillingInterval.Month, + intervalCount: 1, + }) : undefined, scenario: AttachScenario.New, logger, diff --git a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.ts b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.ts index 35c8274ea..3d974736b 100644 --- a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.ts +++ b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.ts @@ -87,11 +87,14 @@ export const featuresToObject = ({ next_reset_at: getEarliestNextResetAt(relatedEnts), interval: relatedEnts.length == 1 ? relatedEnts[0].interval : "multiple", + interval_count: + relatedEnts.length == 1 ? relatedEnts[0].interval_count : null, overage_allowed: relatedEnts.some((e) => e.overage_allowed), breakdown: !unlimited && relatedEnts.length > 1 ? relatedEnts.map((e) => ({ interval: e.interval!, + interval_count: e.interval_count, balance: e.balance, usage: e.usage, included_usage: e.included_usage, diff --git a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts index 4d327e8ca..ca30d90d8 100644 --- a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts +++ b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts @@ -257,6 +257,7 @@ export const getCusBalances = async ({ isBoolean || unlimited ? undefined : cusEnt.next_reset_at; data[key].allowance = isBoolean || unlimited ? undefined : 0; data[key].usage_limit = isBoolean || unlimited ? undefined : 0; + data[key].interval_count = ent.interval_count || 1; } } } diff --git a/server/src/internal/customers/handlers/handleUpdateBalances.ts b/server/src/internal/customers/handlers/handleUpdateBalances.ts index 23ec8dfdc..bef4248e4 100644 --- a/server/src/internal/customers/handlers/handleUpdateBalances.ts +++ b/server/src/internal/customers/handlers/handleUpdateBalances.ts @@ -143,18 +143,21 @@ export const handleUpdateBalances = async (req: any, res: any) => { delete properties.balance; for (const cusEnt of cusEnts) { - if (cusEnt.internal_feature_id !== feature!.internal_id!) { - continue; - } + let cusEntIntCount = cusEnt.entitlement.interval_count || 1; + let deductionIntCount = balance.interval_count || 1; - let intervalCount = cusEnt.entitlement.interval_count || 1; - let intervalCountMatch = - intervalCount > 1 ? balance.interval_count === intervalCount : true; + let intCountMatch = notNullish(balance.interval_count) + ? cusEntIntCount === deductionIntCount + : true; + + let intMatch = notNullish(balance.interval) + ? balance.interval === cusEnt.entitlement.interval + : true; if ( - notNullish(balance.interval) && - balance.interval !== cusEnt.entitlement.interval && - intervalCountMatch + cusEnt.internal_feature_id !== feature!.internal_id! || + !intMatch || + !intCountMatch ) { continue; } @@ -230,9 +233,22 @@ export const handleUpdateBalances = async (req: any, res: any) => { } for (const cusEnt of cusEnts) { + let cusEntIntCount = cusEnt.entitlement.interval_count || 1; + let deductionIntCount = featureDeduction.intervalCount || 1; + + let intCountMatch = notNullish(featureDeduction.intervalCount) + ? cusEntIntCount === deductionIntCount + : true; + + let intMatch = notNullish(featureDeduction.interval) + ? featureDeduction.interval === cusEnt.entitlement.interval + : true; + if ( cusEnt.internal_feature_id !== - featureDeduction.feature!.internal_id! + featureDeduction.feature!.internal_id! || + !intMatch || + !intCountMatch ) { continue; } diff --git a/server/src/internal/products/prices/billingIntervalUtils.ts b/server/src/internal/products/prices/billingIntervalUtils.ts index 030de798b..115412d9b 100644 --- a/server/src/internal/products/prices/billingIntervalUtils.ts +++ b/server/src/internal/products/prices/billingIntervalUtils.ts @@ -3,6 +3,7 @@ import { addMinutes, addMonths, addSeconds, + addWeeks, addYears, differenceInSeconds, getDate, @@ -16,6 +17,7 @@ import { setSeconds, startOfMonth, subMonths, + subWeeks, subYears, } from "date-fns"; import { UTCDate } from "@date-fns/utc"; @@ -33,6 +35,9 @@ export const subtractBillingIntervalUnix = ({ const date = new UTCDate(unixTimestamp); let subtractedDate = date; switch (interval) { + case BillingInterval.Week: + subtractedDate = subWeeks(date, 1 * intervalCount); + break; case BillingInterval.Month: subtractedDate = subMonths(date, 1 * intervalCount); break; @@ -63,6 +68,9 @@ export const addBillingIntervalUnix = ({ const date = new UTCDate(unixTimestamp); let addedDate = date; switch (interval) { + case BillingInterval.Week: + addedDate = addWeeks(date, 1 * intervalCount); + break; case BillingInterval.Month: addedDate = addMonths(date, intervalCount); break; diff --git a/server/src/internal/products/prices/priceUtils.ts b/server/src/internal/products/prices/priceUtils.ts index 38c932c12..d0b902b04 100644 --- a/server/src/internal/products/prices/priceUtils.ts +++ b/server/src/internal/products/prices/priceUtils.ts @@ -131,6 +131,14 @@ export const getBillingInterval = (prices: Price[]) => { throw error; } + // console.log( + // "pricesCopy", + // pricesCopy.map((p) => ({ + // interval: p.config!.interval, + // intervalCount: p.config!.interval_count, + // })) + // ); + if (pricesCopy.length == 0) { throw new RecaseError({ message: "No prices found, can't get billing interval", @@ -140,10 +148,8 @@ export const getBillingInterval = (prices: Price[]) => { } return { - interval: pricesCopy[pricesCopy.length - 1].config! - .interval as BillingInterval, - intervalCount: - pricesCopy[pricesCopy.length - 1].config!.interval_count || 1, + interval: pricesCopy[0].config!.interval as BillingInterval, + intervalCount: pricesCopy[0].config!.interval_count || 1, }; // return pricesCopy[pricesCopy.length - 1].config!.interval as BillingInterval; }; diff --git a/server/src/internal/products/prices/priceUtils/convertPrice.ts b/server/src/internal/products/prices/priceUtils/convertPrice.ts index 617c49a0f..e40104b9d 100644 --- a/server/src/internal/products/prices/priceUtils/convertPrice.ts +++ b/server/src/internal/products/prices/priceUtils/convertPrice.ts @@ -33,8 +33,12 @@ export const toIntervalKey = ({ } else if (interval == BillingInterval.SemiAnnual) { let finalCount = (intervalCount ?? 1) * 6; return `${BillingInterval.Month}-${finalCount}`; + } + + if (interval == BillingInterval.Week) { + return `${BillingInterval.Week}-${intervalCount}`; } else if (interval == BillingInterval.Year) { - return BillingInterval.Year; + return `${BillingInterval.Year}-${intervalCount}`; } return `${interval}-${intervalCount}`; }; diff --git a/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts b/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts index bb74b82b8..bd42e19b7 100644 --- a/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts +++ b/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts @@ -36,6 +36,7 @@ const intervalToValue = ( ) => { const intervalToBaseVal: Record = { [BillingInterval.OneOff]: 0, + [BillingInterval.Week]: 0.25, [BillingInterval.Month]: 1, [BillingInterval.Quarter]: 3, [BillingInterval.SemiAnnual]: 6, diff --git a/server/src/internal/products/productUtils.ts b/server/src/internal/products/productUtils.ts index e6d03166b..cdd1f04b3 100644 --- a/server/src/internal/products/productUtils.ts +++ b/server/src/internal/products/productUtils.ts @@ -125,8 +125,8 @@ export const isProductUpgrade = ({ return true; } - let billingInterval1 = getBillingInterval(prices1); - let billingInterval2 = getBillingInterval(prices2); + let billingInterval1 = getBillingInterval(prices1); // pro quarter + let billingInterval2 = getBillingInterval(prices2); // premium // 2. Get total price for each product const getTotalPrice = (prices: Price[]) => { @@ -142,6 +142,7 @@ export const isProductUpgrade = ({ }; // 3. Compare prices + if ( intervalsSame({ intervalA: billingInterval1, diff --git a/server/src/utils/importUtils/addProductFromSubs.ts b/server/src/utils/importUtils/addProductFromSubs.ts index 56e58fa03..fb38378f9 100644 --- a/server/src/utils/importUtils/addProductFromSubs.ts +++ b/server/src/utils/importUtils/addProductFromSubs.ts @@ -51,9 +51,7 @@ export const addProductFromSubs = async ({ (cp) => !cp.product.is_add_on && cp.product_id == autumnProduct.id && - (notNullish(entity) - ? cp.internal_entity_id == entity!.internal_id - : true), + (notNullish(entity) ? cp.internal_entity_id == entity!.internal_id : true) ); if (mainCusProduct && !force) { @@ -66,7 +64,7 @@ export const addProductFromSubs = async ({ autumnCus.id || autumnCus.email } already has non-free free product: ${ mainCusProduct.product.name - }, skipping...`, + }, skipping...` ); return mainCusProduct; } @@ -112,7 +110,7 @@ export const addProductFromSubs = async ({ anchorToUnix: anchorToUnix || stripeSubs[0].current_period_end * 1000, subscriptionStatus: stripeToAutumnSubStatus( - stripeSubs[0].status, + stripeSubs[0].status ) as CusProductStatus, canceledAt: stripeSubs[0].canceled_at @@ -124,7 +122,7 @@ export const addProductFromSubs = async ({ }); logger.info( - `Added product ${autumnProduct.name} to customer ${autumnCus.name}`, + `Added product ${autumnProduct.name} to customer ${autumnCus.name}` ); // Create sub @@ -146,7 +144,7 @@ export const addProductFromSubs = async ({ sub: constructSub({ stripeId: sub.id, usageFeatures: - subInterval == BillingInterval.Month ? usageFeatures : [], + subInterval.interval == BillingInterval.Month ? usageFeatures : [], orgId: org.id, env, currentPeriodStart: sub.current_period_start, diff --git a/server/src/utils/scriptUtils/constructItem.ts b/server/src/utils/scriptUtils/constructItem.ts index 0a7373221..e02b06f10 100644 --- a/server/src/utils/scriptUtils/constructItem.ts +++ b/server/src/utils/scriptUtils/constructItem.ts @@ -61,7 +61,7 @@ export const constructPrepaidItem = ({ }, rolloverConfig, usageLimit, - intervalCount = 2, + intervalCount = 1, }: { featureId: string; price?: number; diff --git a/server/tests/advanced/customInterval/customInterval1 copy.ts b/server/tests/advanced/customInterval/customInterval1 copy.ts deleted file mode 100644 index 6d2a4e673..000000000 --- a/server/tests/advanced/customInterval/customInterval1 copy.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { APIVersion, AppEnv, Organization } from "@autumn/shared"; -import chalk from "chalk"; -import Stripe from "stripe"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { setupBefore } from "tests/before.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js"; -import { - constructArrearItem, - constructArrearProratedItem, -} from "@/utils/scriptUtils/constructItem.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; - -import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; -import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { timeout } from "@/utils/genUtils.js"; -import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { expectSubItemsCorrect } from "tests/utils/expectUtils/expectSubUtils.js"; -import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js"; - -const testCase = "upgrade6"; - -export let pro = constructProduct({ - items: [ - constructArrearItem({ featureId: TestFeature.Words }), - constructArrearProratedItem({ - featureId: TestFeature.Users, - pricePerUnit: 20, - }), - ], - type: "pro", -}); - -export let premium = constructProduct({ - items: [ - constructArrearItem({ featureId: TestFeature.Words }), - constructArrearProratedItem({ - featureId: TestFeature.Users, - pricePerUnit: 30, - }), - ], - type: "premium", -}); - -describe(`${chalk.yellowBright(`${testCase}: Testing failed upgrades`)}`, () => { - let customerId = testCase; - let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - addPrefixToProducts({ - products: [pro, premium], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro, premium], - db, - orgId: org.id, - env, - }); - - testClockId = testClockId1!; - }); - - it("should attach pro product", async function () { - await runAttachTest({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - - let usage = 100012; - it("should upgrade to premium product and fail", async function () { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Words, - value: usage, - }); - await timeout(4000); - - let cus = await CusService.get({ - db, - orgId: org.id, - idOrInternalId: customerId, - env, - }); - - await attachFailedPaymentMethod({ stripeCli, customer: cus! }); - await timeout(2000); - - await expectAutumnError({ - func: async () => { - await runAttachTest({ - autumn, - customerId, - product: premium, - stripeCli, - db, - org, - env, - }); - }, - errMessage: "Failed to update subscription. Your card was declined.", - }); - - await timeout(4000); - let customer = await autumn.customers.get(customerId); - - expectProductAttached({ - customer, - product: pro, - }); - - expectFeaturesCorrect({ - customer, - product: pro, - usage: [ - { - featureId: TestFeature.Words, - value: usage, - }, - ], - }); - - await expectSubItemsCorrect({ - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); -}); diff --git a/server/tests/advanced/customInterval/customInterval1.ts b/server/tests/advanced/customInterval/customInterval1.ts index 360d9e161..8cf7c3f01 100644 --- a/server/tests/advanced/customInterval/customInterval1.ts +++ b/server/tests/advanced/customInterval/customInterval1.ts @@ -1,27 +1,16 @@ +import chalk from "chalk"; +import Stripe from "stripe"; + import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; import { APIVersion, AppEnv, Organization } from "@autumn/shared"; -import chalk from "chalk"; -import Stripe from "stripe"; + import { DrizzleCli } from "@/db/initDrizzle.js"; import { setupBefore } from "tests/before.js"; import { createProducts } from "tests/utils/productUtils.js"; - -import { - constructArrearItem, - constructArrearProratedItem, - constructFeatureItem, -} from "@/utils/scriptUtils/constructItem.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; - -import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; -import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { timeout } from "@/utils/genUtils.js"; -import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { expectSubItemsCorrect } from "tests/utils/expectUtils/expectSubUtils.js"; -import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js"; import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { addHours, addMonths } from "date-fns"; @@ -35,13 +24,9 @@ export let pro = constructProduct({ items: [ constructFeatureItem({ featureId: TestFeature.Words, - intervalCount: 200, + intervalCount: 2, + includedUsage: 500, }), - // constructArrearItem({ featureId: TestFeature.Words }), - // constructArrearProratedItem({ - // featureId: TestFeature.Users, - // pricePerUnit: 20, - // }), ], intervalCount: 2, type: "pro", @@ -51,7 +36,7 @@ export let premium = constructProduct({ items: [ constructFeatureItem({ featureId: TestFeature.Words, - intervalCount: 500, + intervalCount: 2, }), // constructArrearItem({ featureId: TestFeature.Words }), // constructArrearProratedItem({ @@ -63,7 +48,7 @@ export let premium = constructProduct({ type: "premium", }); -describe(`${chalk.yellowBright(`${testCase}: Testing failed upgrades`)}`, () => { +describe(`${chalk.yellowBright(`${testCase}: Testing custom interval and interval count`)}`, () => { let customerId = testCase; let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); let testClockId: string; @@ -153,5 +138,9 @@ describe(`${chalk.yellowBright(`${testCase}: Testing failed upgrades`)}`, () => expect(invoices.length).to.equal(3); expect(invoices[0].product_ids).to.include(premium.id); expect(invoices[0].total).to.equal(getBasePrice({ product: premium })); + + const wordsFeature = customer2.features[TestFeature.Words]; + // @ts-ignore + expect(wordsFeature.interval_count).to.equal(2); }); }); diff --git a/server/tests/advanced/customInterval/customInteral2.ts b/server/tests/advanced/customInterval/customInterval2.ts similarity index 100% rename from server/tests/advanced/customInterval/customInteral2.ts rename to server/tests/advanced/customInterval/customInterval2.ts diff --git a/server/tests/advanced/customInterval/customInterval3.ts b/server/tests/advanced/customInterval/customInterval3.ts index ea89242f8..b7f4c2148 100644 --- a/server/tests/advanced/customInterval/customInterval3.ts +++ b/server/tests/advanced/customInterval/customInterval3.ts @@ -54,6 +54,7 @@ const prepaidWordsItem = constructPrepaidItem({ price: 10, billingUnits: 1, includedUsage: 0, + intervalCount: 2, }); export const addOn = constructRawProduct({ @@ -62,7 +63,7 @@ export const addOn = constructRawProduct({ isAddOn: true, }); -describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on arrear prorated price`)}`, () => { +describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on add on merged product`)}`, () => { let customerId = testCase; let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); let testClockId: string; @@ -115,7 +116,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on arrear p }); }); - it("should upgrade to premium product and have correct invoice next cycle", async function () { + it("should upgrade to attached add on and have correct invoice next cycle", async function () { const curUnix = await advanceTestClock({ stripeCli, testClockId, @@ -150,22 +151,21 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on arrear p }); let expectedPrice = wordsBillingUnits * prepaidWordsItem.price!; - expect(invoices[0].product_ids).to.include(addOn.id); - expect(invoices[0].total).to.approximately( - calculateProrationAmount({ - amount: expectedPrice, - periodStart: curUnix!, - periodEnd: addMonths(curUnix!, 1).getTime(), - now: curUnix!, - }), - 0.1 - ); + const proratedPrice = calculateProrationAmount({ + amount: expectedPrice, + periodStart: new Date().getTime(), + periodEnd: addMonths(new Date(), 2).getTime(), + now: curUnix!, + }); - const expectedAddonEnd = addMonths(curUnix, 1); + expect(invoices[0].product_ids).to.include(addOn.id); + expect(invoices[0].total).to.approximately(proratedPrice, 0.1); + + const expectedAddonEnd = addMonths(new Date(), 2); const approximate = 1000 * 60 * 60 * 24; // +- 1 day const addOnProduct = customer.products.find((p) => p.id === addOn.id); - expect(addOnProduct?.current_period_end).to.be.closeTo( + expect(addOnProduct?.current_period_end).to.be.approximately( expectedAddonEnd.getTime(), approximate ); diff --git a/server/tests/advanced/customInterval/customInterval4.ts b/server/tests/advanced/customInterval/customInterval4.ts new file mode 100644 index 000000000..056ee8893 --- /dev/null +++ b/server/tests/advanced/customInterval/customInterval4.ts @@ -0,0 +1,150 @@ +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { APIVersion, AppEnv, Organization } from "@autumn/shared"; +import chalk from "chalk"; +import Stripe from "stripe"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { setupBefore } from "tests/before.js"; +import { createProducts } from "tests/utils/productUtils.js"; + +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; + +import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js"; +import { addMonths } from "date-fns"; +import { expect } from "chai"; +import { + expectDowngradeCorrect, + expectNextCycleCorrect, +} from "tests/utils/expectUtils/expectScheduleUtils.js"; +import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; + +const testCase = "customInterval4"; + +export let pro = constructProduct({ + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage: 500, + }), + ], + intervalCount: 2, + type: "pro", +}); + +export let premium = constructProduct({ + id: "premium", + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage: 500, + }), + ], + intervalCount: 2, + type: "premium", +}); + +describe(`${chalk.yellowBright(`${testCase}: Testing downgrades for custom intervals`)}`, () => { + let customerId = testCase; + let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + let testClockId: string; + let db: DrizzleCli, org: Organization, env: AppEnv; + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + addPrefixToProducts({ + products: [pro, premium], + prefix: testCase, + }); + + await createProducts({ + autumn, + products: [pro, premium], + db, + orgId: org.id, + env, + }); + + testClockId = testClockId1!; + }); + + it("should attach premium product", async function () { + await runAttachTest({ + autumn, + customerId, + product: premium, + stripeCli, + db, + org, + env, + }); + }); + + it("should have correct next cycle at on checkout", async function () { + const checkout = await autumn.checkout({ + customer_id: customerId, + product_id: pro.id, + }); + + let expectedNextCycle = addMonths(new Date(), 2); + expect(checkout.next_cycle?.starts_at).to.be.approximately( + expectedNextCycle.getTime(), + 1000 * 60 * 60 * 24 + ); + + expect(checkout.total).to.equal(0); + }); + + let preview: any; + it("should downgrade to pro", async function () { + const { preview: preview_ } = await expectDowngradeCorrect({ + autumn, + customerId, + curProduct: premium, + newProduct: pro, + stripeCli, + db, + org, + env, + }); + + preview = preview_; + }); + + it("should have pro attached on next cycle", async function () { + await expectNextCycleCorrect({ + preview: preview!, + autumn, + stripeCli, + customerId, + testClockId, + product: pro, + db, + org, + env, + }); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices; + expect(invoices.length).to.equal(2); + expect(invoices[0].total).to.equal(getBasePrice({ product: pro })); + }); +}); diff --git a/server/tests/advanced/customInterval/customInterval5.ts b/server/tests/advanced/customInterval/customInterval5.ts new file mode 100644 index 000000000..5fab46f4f --- /dev/null +++ b/server/tests/advanced/customInterval/customInterval5.ts @@ -0,0 +1,163 @@ +import chalk from "chalk"; +import Stripe from "stripe"; + +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { APIVersion, AppEnv, FullCustomer, Organization } from "@autumn/shared"; + +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { setupBefore } from "tests/before.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js"; +import { expect } from "chai"; +import { Customer } from "autumn-js"; +import { timeout } from "@/utils/genUtils.js"; + +const testCase = "customInterval5"; + +const includedUsage = 500; +const monthlyWords = constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage, +}); + +const biMonthlyWords = constructFeatureItem({ + featureId: TestFeature.Words, + intervalCount: 2, + includedUsage, +}); + +export let pro = constructProduct({ + items: [monthlyWords, biMonthlyWords], + intervalCount: 2, + type: "pro", +}); + +const getBreakdown = ({ + customer, + intervalCount, +}: { + customer: Customer; + intervalCount: number; +}) => { + const wordsFeature = customer.features[TestFeature.Words]; + // @ts-ignore + return wordsFeature.breakdown?.find( + (b: any) => b.interval_count == intervalCount + ); +}; + +describe(`${chalk.yellowBright(`${testCase}: Testing multi interval features with custom intervals`)}`, () => { + let customerId = testCase; + let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + let testClockId: string; + let db: DrizzleCli, org: Organization, env: AppEnv; + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + addPrefixToProducts({ + products: [pro], + prefix: testCase, + }); + + await createProducts({ + autumn, + products: [pro], + db, + orgId: org.id, + env, + }); + + testClockId = testClockId1!; + }); + + it("should attach pro product", async function () { + await runAttachTest({ + autumn, + customerId, + product: pro, + stripeCli, + db, + org, + env, + }); + + const customer = await autumn.customers.get(customerId); + const wordsFeature = customer.features[TestFeature.Words]; + // @ts-ignore + expect(wordsFeature.interval_count).to.equal(null); + expect(wordsFeature.breakdown?.length).to.equal(2); + + expect( + wordsFeature.breakdown?.some( + (b: any) => b.interval_count == 1 && b.interval == "month" + ) + ).to.equal(true); + expect( + wordsFeature.breakdown?.some( + (b: any) => b.interval_count == 2 && b.interval == "month" + ) + ).to.equal(true); + }); + + const trackVal = 300; + it("should have correct breakdown after usage", async function () { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Words, + value: trackVal, + }); + + await timeout(3000); + + const customer = await autumn.customers.get(customerId); + + // Should deduct + const monthlyBreakdown = getBreakdown({ customer, intervalCount: 1 }); + const biMonthlyBreakdown = getBreakdown({ customer, intervalCount: 2 }); + + expect(monthlyBreakdown?.balance).to.equal(includedUsage - trackVal); + expect(biMonthlyBreakdown?.balance).to.equal(includedUsage); + + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Words, + value: trackVal, + }); + + await timeout(3000); + + const customer2 = await autumn.customers.get(customerId); + const monthlyBreakdown2 = getBreakdown({ + customer: customer2, + intervalCount: 1, + }); + const biMonthlyBreakdown2 = getBreakdown({ + customer: customer2, + intervalCount: 2, + }); + + expect(monthlyBreakdown2?.balance).to.equal(0); + expect(biMonthlyBreakdown2?.balance).to.equal(includedUsage - 100); + }); +}); diff --git a/server/tests/advanced/customInterval/customInterval6.ts b/server/tests/advanced/customInterval/customInterval6.ts new file mode 100644 index 000000000..5f0899982 --- /dev/null +++ b/server/tests/advanced/customInterval/customInterval6.ts @@ -0,0 +1,165 @@ +import chalk from "chalk"; +import Stripe from "stripe"; + +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { APIVersion, AppEnv, FullCustomer, Organization } from "@autumn/shared"; + +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { setupBefore } from "tests/before.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js"; +import { expect } from "chai"; +import { Customer } from "autumn-js"; +import { timeout } from "@/utils/genUtils.js"; + +const testCase = "customInterval6"; + +// Update balances! + +// const includedUsage = 500; +// const monthlyWords = constructFeatureItem({ +// featureId: TestFeature.Words, +// includedUsage, +// }); + +// const biMonthlyWords = constructFeatureItem({ +// featureId: TestFeature.Words, +// intervalCount: 2, +// includedUsage, +// }); + +// export let pro = constructProduct({ +// items: [monthlyWords, biMonthlyWords], +// intervalCount: 2, +// type: "pro", +// }); + +// const getBreakdown = ({ +// customer, +// intervalCount, +// }: { +// customer: Customer; +// intervalCount: number; +// }) => { +// const wordsFeature = customer.features[TestFeature.Words]; +// // @ts-ignore +// return wordsFeature.breakdown?.find( +// (b: any) => b.interval_count == intervalCount +// ); +// }; + +// describe(`${chalk.yellowBright(`${testCase}: Testing multi interval features with custom intervals`)}`, () => { +// let customerId = testCase; +// let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); +// let testClockId: string; +// let db: DrizzleCli, org: Organization, env: AppEnv; +// let stripeCli: Stripe; + +// before(async function () { +// await setupBefore(this); +// const { autumnJs } = this; +// db = this.db; +// org = this.org; +// env = this.env; + +// stripeCli = this.stripeCli; + +// const { testClockId: testClockId1 } = await initCustomer({ +// autumn: autumnJs, +// customerId, +// db, +// org, +// env, +// attachPm: "success", +// }); + +// addPrefixToProducts({ +// products: [pro], +// prefix: testCase, +// }); + +// await createProducts({ +// autumn, +// products: [pro], +// db, +// orgId: org.id, +// env, +// }); + +// testClockId = testClockId1!; +// }); + +// it("should attach pro product", async function () { +// await runAttachTest({ +// autumn, +// customerId, +// product: pro, +// stripeCli, +// db, +// org, +// env, +// }); + +// const customer = await autumn.customers.get(customerId); +// const wordsFeature = customer.features[TestFeature.Words]; +// // @ts-ignore +// expect(wordsFeature.interval_count).to.equal(null); +// expect(wordsFeature.breakdown?.length).to.equal(2); + +// expect( +// wordsFeature.breakdown?.some( +// (b: any) => b.interval_count == 1 && b.interval == "month" +// ) +// ).to.equal(true); +// expect( +// wordsFeature.breakdown?.some( +// (b: any) => b.interval_count == 2 && b.interval == "month" +// ) +// ).to.equal(true); +// }); + +// const trackVal = 300; +// it("should have correct breakdown after usage", async function () { +// await autumn.track({ +// customer_id: customerId, +// feature_id: TestFeature.Words, +// value: trackVal, +// }); + +// await timeout(3000); + +// const customer = await autumn.customers.get(customerId); + +// // Should deduct +// const monthlyBreakdown = getBreakdown({ customer, intervalCount: 1 }); +// const biMonthlyBreakdown = getBreakdown({ customer, intervalCount: 2 }); + +// expect(monthlyBreakdown?.balance).to.equal(includedUsage - trackVal); +// expect(biMonthlyBreakdown?.balance).to.equal(includedUsage); + +// await autumn.track({ +// customer_id: customerId, +// feature_id: TestFeature.Words, +// value: trackVal, +// }); + +// await timeout(3000); + +// const customer2 = await autumn.customers.get(customerId); +// const monthlyBreakdown2 = getBreakdown({ +// customer: customer2, +// intervalCount: 1, +// }); +// const biMonthlyBreakdown2 = getBreakdown({ +// customer: customer2, +// intervalCount: 2, +// }); + +// expect(monthlyBreakdown2?.balance).to.equal(0); +// expect(biMonthlyBreakdown2?.balance).to.equal(includedUsage - 100); +// }); +// }); diff --git a/server/tests/utils/productUtils.ts b/server/tests/utils/productUtils.ts index 2a81a92a1..eab69f507 100644 --- a/server/tests/utils/productUtils.ts +++ b/server/tests/utils/productUtils.ts @@ -36,7 +36,7 @@ export const createProduct = async ({ internalId: prod.internal_id, orgId, env, - }), + }) ); } @@ -82,7 +82,7 @@ export const createProducts = async ({ const batchCreate = []; for (const product of products) { batchCreate.push( - createProduct({ db, orgId, env, autumn, product, prefix }), + createProduct({ db, orgId, env, autumn, product, prefix }) ); } @@ -113,12 +113,12 @@ export const createReward = async ({ idOrInternalId: productId!, }); - let usagePrices = fullProduct.prices.filter((price) => - isUsagePrice({ price }), + let usagePrices = fullProduct?.prices.filter((price) => + isUsagePrice({ price }) ); if (onlyUsage) { - reward.discount_config!.price_ids = usagePrices.map((price) => price.id); + reward.discount_config!.price_ids = usagePrices?.map((price) => price.id); } try { diff --git a/shared/models/productModels/priceModels/priceEnums.ts b/shared/models/productModels/priceModels/priceEnums.ts index fbc36b181..e9b7b1a2d 100644 --- a/shared/models/productModels/priceModels/priceEnums.ts +++ b/shared/models/productModels/priceModels/priceEnums.ts @@ -1,5 +1,6 @@ export enum BillingInterval { OneOff = "one_off", + Week = "week", Month = "month", Quarter = "quarter", SemiAnnual = "semi_annual", diff --git a/shared/utils/intervalUtils.ts b/shared/utils/intervalUtils.ts index fb88ce05f..c029082e5 100644 --- a/shared/utils/intervalUtils.ts +++ b/shared/utils/intervalUtils.ts @@ -8,6 +8,7 @@ export const intervalToValue = ( ) => { const intervalToBaseVal: Record = { [BillingInterval.OneOff]: 0, + [BillingInterval.Week]: 0.25, [BillingInterval.Month]: 1, [BillingInterval.Quarter]: 3, [BillingInterval.SemiAnnual]: 6,