tests: custom intervals
This commit is contained in:
@@ -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'
|
||||
|
||||
12
server/src/external/autumn/autumnCli.ts
vendored
12
server/src/external/autumn/autumnCli.ts
vendored
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -273,6 +273,7 @@ const getChangeProductBranch = async ({
|
||||
// }
|
||||
|
||||
let isUpgrade = isProductUpgrade({ prices1: curPrices, prices2: newPrices });
|
||||
|
||||
if (isUpgrade) {
|
||||
if (isTrialing(curMainProduct!)) {
|
||||
return AttachBranch.MainIsTrial;
|
||||
|
||||
@@ -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,
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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}`;
|
||||
};
|
||||
|
||||
@@ -36,6 +36,7 @@ const intervalToValue = (
|
||||
) => {
|
||||
const intervalToBaseVal: Record<BillingInterval, number> = {
|
||||
[BillingInterval.OneOff]: 0,
|
||||
[BillingInterval.Week]: 0.25,
|
||||
[BillingInterval.Month]: 1,
|
||||
[BillingInterval.Quarter]: 3,
|
||||
[BillingInterval.SemiAnnual]: 6,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -61,7 +61,7 @@ export const constructPrepaidItem = ({
|
||||
},
|
||||
rolloverConfig,
|
||||
usageLimit,
|
||||
intervalCount = 2,
|
||||
intervalCount = 1,
|
||||
}: {
|
||||
featureId: string;
|
||||
price?: number;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
150
server/tests/advanced/customInterval/customInterval4.ts
Normal file
150
server/tests/advanced/customInterval/customInterval4.ts
Normal file
@@ -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 }));
|
||||
});
|
||||
});
|
||||
163
server/tests/advanced/customInterval/customInterval5.ts
Normal file
163
server/tests/advanced/customInterval/customInterval5.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
165
server/tests/advanced/customInterval/customInterval6.ts
Normal file
165
server/tests/advanced/customInterval/customInterval6.ts
Normal file
@@ -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);
|
||||
// });
|
||||
// });
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export enum BillingInterval {
|
||||
OneOff = "one_off",
|
||||
Week = "week",
|
||||
Month = "month",
|
||||
Quarter = "quarter",
|
||||
SemiAnnual = "semi_annual",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const intervalToValue = (
|
||||
) => {
|
||||
const intervalToBaseVal: Record<BillingInterval, number> = {
|
||||
[BillingInterval.OneOff]: 0,
|
||||
[BillingInterval.Week]: 0.25,
|
||||
[BillingInterval.Month]: 1,
|
||||
[BillingInterval.Quarter]: 3,
|
||||
[BillingInterval.SemiAnnual]: 6,
|
||||
|
||||
Reference in New Issue
Block a user