fix: tests

This commit is contained in:
John Yeo
2025-08-27 17:13:58 -07:00
parent c146ae1f06
commit ad8e9dfca1
57 changed files with 1081 additions and 605 deletions

View File

@@ -13,7 +13,8 @@ $MOCHA_CMD \
'tests/attach/basic/*.ts' \
'tests/attach/upgrade/*.ts' \
'tests/attach/downgrade/*.ts' \
'tests/attach/addOn/*.ts'
'tests/attach/addOn/*.ts' \
'tests/attach/cancel/*.ts'
$MOCHA_CMD \
'tests/attach/checkout/*.ts' \

View File

@@ -8,19 +8,23 @@ if [[ "$1" == *"setup"* ]]; then
MOCHA_PARALLEL=true $MOCHA_SETUP
fi
$MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \
'tests/advanced/coupons/*.ts' \
'tests/attach/updateQuantity/*.ts' \
'tests/advanced/referrals/*.ts' \
'tests/advanced/rollovers/*.ts' \
'tests/advanced/customInterval/*.ts'
$MOCHA_CMD 'tests/advanced/referrals/*.ts' \
'tests/advanced/rollovers/*.ts' \
'tests/advanced/customInterval/*.ts'
$MOCHA_CMD 'tests/attach/multiProduct/*.ts' \
'tests/advanced/usageLimit/*.ts'
$MOCHA_CMD 'tests/advanced/usage/*.ts'
# $MOCHA_CMD 'tests/merged/add/*.ts' \
# 'tests/merged/downgrade/*.ts' \
# 'tests/merged/prepaid/*.ts' \
# 'tests/merged/separate/*.ts' \
# 'tests/merged/upgrade/*.ts' \
# 'tests/merged/trial/*.ts'
$MOCHA_CMD 'tests/merged/addOn/*.ts' \
'tests/core/cancel/*.ts' \
'tests/core/multiAttach/*.ts' \
'tests/core/multiAttach/multiInvoice/*.ts' \
'tests/core/multiAttach/multiUpgrade/*.ts'
# $MOCHA_CMD 'tests/core/multiAttach/multiReward/multiReward1.test.ts'
# $MOCHA_CMD 'tests/core/multiAttach/multiReward/multiReward2.test.ts'
# $MOCHA_CMD 'tests/core/multiAttach/multiReward/multiReward3.test.ts'

View File

@@ -1,6 +1,28 @@
# npx mocha 'tests/alex/00_setup.ts' --timeout 10000000
#!/bin/bash
MOCHA_PARALLEL=true npx mocha --parallel --timeout 10000000 \
'tests/alex/01_free.ts' 'tests/alex/02_pro.ts' 'tests/alex/03_premium.ts' \
'tests/alex/04_topups.ts' 'tests/alex/05_cancel.ts' 'tests/alex/06_switch.ts' \
--ignore 'tests/alex/00_setup.ts'
# Source shared configuration
source "$(dirname "$0")/config.sh"
# MOCHA_PARALLEL=true $MOCHA_SETUP
if [[ "$1" == *"setup"* ]]; then
MOCHA_PARALLEL=true $MOCHA_SETUP
fi
$MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \
'tests/advanced/coupons/*.ts' \
'tests/attach/updateQuantity/*.ts' \
'tests/advanced/referrals/*.ts' \
'tests/advanced/rollovers/*.ts' \
'tests/advanced/customInterval/*.ts'
$MOCHA_CMD 'tests/advanced/referrals/*.ts' \
'tests/advanced/rollovers/*.ts' \
'tests/advanced/customInterval/*.ts'
$MOCHA_CMD 'tests/attach/multiProduct/*.ts' \
'tests/advanced/usageLimit/*.ts'
$MOCHA_CMD 'tests/advanced/usage/*.ts'

6
server/shell/g6.sh Executable file
View File

@@ -0,0 +1,6 @@
# npx mocha 'tests/alex/00_setup.ts' --timeout 10000000
MOCHA_PARALLEL=true npx mocha --parallel --timeout 10000000 \
'tests/alex/01_free.ts' 'tests/alex/02_pro.ts' 'tests/alex/03_premium.ts' \
'tests/alex/04_topups.ts' 'tests/alex/05_cancel.ts' 'tests/alex/06_switch.ts' \
--ignore 'tests/alex/00_setup.ts'

View File

@@ -352,6 +352,11 @@ export class AutumnInt {
};
rewards = {
get: async (rewardId: string) => {
const data = await this.get(`/rewards/${rewardId}`);
return data;
},
create: async (reward: any) => {
const data = await this.post(`/rewards?legacyStripe=true`, reward);
return data;

View File

@@ -13,6 +13,7 @@ import { differenceInSeconds } from "date-fns";
import { SubService } from "@/internal/subscriptions/SubService.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getEarliestPeriodEnd } from "./stripeSubUtils/convertSubUtils.js";
import { notNullish } from "@/utils/genUtils.js";
export const getFullStripeSub = async ({
stripeCli,
@@ -293,3 +294,11 @@ export const getStripeProrationBehavior = ({
? behaviourMap[ProrationBehavior.Immediately]
: behaviourMap[ProrationBehavior.NextBilling];
};
export const subIsCanceled = ({ sub }: { sub: Stripe.Subscription }) => {
return (
notNullish(sub.canceled_at) ||
notNullish(sub.cancel_at) ||
sub.cancel_at_period_end
);
};

View File

@@ -64,37 +64,11 @@ export const createProrationInvoice = async ({
pending: true,
});
console.log("Items:", items.data);
if (items.data.length == 0) {
logger.info(`No items to prorate, skipping invoice creation`);
return null;
}
// console.log(
// "Upcoming invoice:",
// items.data.map((item) => item.description)
// );
// throw new Error("Not implemented");
// const proratedItems = items.data.filter(
// (item) => item.proration || item.parent?.type === "invoiceitem"
// );
// console.log("Preview invoice items:", items.lines.data);
// let items = await stripeCli.invoices.listUpcomingLines({
// subscription: curSub.id,
// });
// let proratedItems = items.data.filter(
// (item) => item.proration || item.type === "invoiceitem",
// );
// if (proratedItems.length == 0) {
// logger.info(`No items to prorate, skipping invoice creation`);
// return null;
// }
let invoice = await stripeCli.invoices.create({
customer: customer.processor.id,
subscription: curSub.id,

View File

@@ -24,10 +24,7 @@ import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
import { handleInvoicePaidDiscount } from "./handleInvoicePaidDiscount.js";
import { handleInvoiceCheckoutPaid } from "@/internal/customers/attach/attachFunctions/invoiceCheckoutPaid/handleInvoiceCheckoutPaid.js";
import {
lineItemInCusProduct,
subItemInCusProduct,
} from "../stripeSubUtils/stripeSubItemUtils.js";
import { lineItemInCusProduct } from "../stripeSubUtils/stripeSubItemUtils.js";
const handleOneOffInvoicePaid = async ({
db,
@@ -214,11 +211,10 @@ export const handleInvoicePaid = async ({
});
}
// let updated = await updateInvoiceIfExists({
// db,
// invoice,
// });
const updated = false;
const updated = await updateInvoiceIfExists({
db,
invoice,
});
if (!updated) {
let invoiceItems = await getInvoiceItems({

View File

@@ -79,8 +79,8 @@ export const handleSubscriptionUpdated = async ({
stripeSubId: subscription.id,
updates: {
status: subStatusMap[subscription.status] || CusProductStatus.Unknown,
canceled_at: canceled ? canceledAt : null,
collection_method: fullSub.collection_method as CollectionMethod,
// canceled_at: canceled ? canceledAt : null,
// trial_ends_at:
// previousAttributes.status === "trialing" &&
// subscription.status === "active"
@@ -89,6 +89,8 @@ export const handleSubscriptionUpdated = async ({
},
});
// 2. Update canceled & canceled_at IF sub has no schedule...?
if (updatedCusProducts.length > 0) {
logger.info(
`✅ Updated ${updatedCusProducts.length} customer product${updatedCusProducts.length === 1 ? "" : "s"} (${updatedCusProducts.map((cp) => cp.id).join(", ")}) - Status: ${updatedCusProducts[0].status}${updatedCusProducts[0].canceled_at ? `, Canceled: ${new Date(updatedCusProducts[0].canceled_at).toISOString()}` : ""}`

View File

@@ -12,6 +12,8 @@ import {
getLatestPeriodEnd,
subToPeriodStartEnd,
} from "../../stripeSubUtils/convertSubUtils.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const isSubCanceled = ({
previousAttributes,
@@ -43,6 +45,33 @@ export const isSubCanceled = ({
};
};
const updateCusProductCanceled = async ({
db,
sub,
canceledAt,
logger,
}: {
db: DrizzleCli;
sub: Stripe.Subscription;
canceledAt?: number | null;
logger: any;
}) => {
// 1. Check if sub has schedule
if (sub.schedule) {
return;
}
logger.info(
`Updating cus products for sub ${sub.id} to canceled | canceled_at: ${canceledAt}`
);
await CusProductService.updateByStripeSubId({
db,
stripeSubId: sub.id,
updates: { canceled_at: canceledAt || Date.now(), canceled: true },
});
};
export const handleSubCanceled = async ({
req,
previousAttributes,
@@ -74,6 +103,24 @@ export const handleSubCanceled = async ({
return;
}
await updateCusProductCanceled({
db,
sub,
canceledAt,
logger,
});
// 2. Update canceled & canceled_at IF sub has no schedule...?
// await CusProductService.updateByStripeSubId({
// db,
// stripeSubId: sub.id,
// updates: {
// canceled_at: canceled ? canceledAt : null,
// canceled: true,
// },
// });
let allDefaultProducts = await ProductService.listDefault({
db,
orgId: org.id,

View File

@@ -8,6 +8,7 @@ import Stripe from "stripe";
import { createStripeCli } from "../../utils.js";
import { cancelFutureProductSchedule } from "@/internal/customers/change-product/scheduleUtils.js";
import { isMultiProductSub } from "@/internal/customers/attach/mergeUtils/mergeUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
const isSubRenewed = ({
previousAttributes,
sub,
@@ -30,6 +31,27 @@ const isSubRenewed = ({
renewedAt: Date.now(),
};
};
const updateCusProductRenewed = async ({
db,
sub,
}: {
db: DrizzleCli;
sub: Stripe.Subscription;
}) => {
if (sub.schedule) {
return;
}
await CusProductService.updateByStripeSubId({
db,
stripeSubId: sub.id,
updates: { canceled_at: null, canceled: false },
});
return;
};
export const handleSubRenewed = async ({
req,
prevAttributes,
@@ -43,7 +65,7 @@ export const handleSubRenewed = async ({
}) => {
const { db, org, env, logtail: logger } = req;
const { renewed, renewedAt } = isSubRenewed({
const { renewed } = isSubRenewed({
previousAttributes: prevAttributes,
sub,
});
@@ -51,19 +73,18 @@ export const handleSubRenewed = async ({
if (!renewed || updatedCusProducts.length == 0) return;
const customer = updatedCusProducts[0].customer;
let cusProducts = await CusProductService.list({
db,
internalCustomerId: customer!.internal_id,
});
if (isMultiProductSub({ sub, cusProducts })) return;
if (isMultiProductSub({ sub, cusProducts }) || sub.schedule) return;
// Sub renewed... if multi sub flow
// console.log(
// `Checking sub renewed: ${sub.id}, Is multi sub: ${isMultiProductSub({ sub, cusProducts })}`
// );
// console.log("Cus products:", cusProducts.map((cp) => `${cp.product.name}`));
await CusProductService.updateByStripeSubId({
db,
stripeSubId: sub.id,
updates: { canceled_at: null, canceled: false },
});
let { curScheduledProduct } = getExistingCusProducts({
product: updatedCusProducts[0].product,

View File

@@ -210,4 +210,27 @@ rewardRouter.post("/:internalId", async (req: any, res: any) => {
}
});
rewardRouter.get("/:id", async (req: any, res: any) => {
try {
const { id } = req.params;
const { orgId, env, db } = req;
const reward = await RewardService.get({
db,
idOrInternalId: id,
orgId,
env,
});
res.status(200).json(reward);
} catch (error) {
handleRequestError({
error,
res,
req,
action: "get reward",
});
}
});
export default rewardRouter;

View File

@@ -42,7 +42,7 @@ export const handleOneOffFunction = async ({
prices,
entitlements,
optionsList,
reward,
rewards,
} = attachParams;
const { invoiceOnly } = config;
@@ -112,13 +112,7 @@ export const handleOneOffFunction = async ({
customer: customer.processor.id!,
auto_advance: false,
currency: org.default_currency!,
discounts: reward
? [
{
coupon: reward.id,
},
]
: undefined,
discounts: rewards ? rewards.map((r) => ({ coupon: r.id })) : undefined,
collection_method: attachParams.invoiceOnly ? "send_invoice" : undefined,
days_until_due: attachParams.invoiceOnly ? 30 : undefined,
});

View File

@@ -46,14 +46,10 @@ export const handleScheduleFunction2 = async ({
}) => {
const logger = req.logtail;
const product = attachParams.products[0];
const { stripeCli, customer: fullCus, prices } = attachParams;
const { stripeCli } = attachParams;
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
const curSub = await paramsToCurSub({ attachParams });
const curPrices = curCusProduct
? cusProductToPrices({ cusProduct: curCusProduct })
: [];
const subItems = curSub?.items.data.filter((item) =>
subItemInCusProduct({ cusProduct: curCusProduct!, subItem: item })
);
@@ -66,12 +62,6 @@ export const handleScheduleFunction2 = async ({
const newProductFree = isFreeProduct(attachParams.prices);
if (schedule) {
console.log("CURRENT SCHEDULE ITEMS:");
await logPhases({
phases: schedule.phases as any,
db: req.db,
});
const newItems = await paramsToScheduleItems({
req,
schedule: schedule!,
@@ -80,20 +70,15 @@ export const handleScheduleFunction2 = async ({
billingPeriodEnd: expectedEnd!,
});
console.log("NEW SCHEDULE ITEMS:");
await logPhases({
phases: newItems.phases as any,
db: req.db,
});
// Should release schedule...
const currentPhaseIndex = getCurrentPhaseIndex({
schedule: { phases: newItems.phases } as any,
now: attachParams.now,
});
if (currentPhaseIndex == newItems.phases.length - 1) {
console.log(`NO SUBSEQUENT PHASES, RELEASING SCHEDULE`);
logger.info(
`SCHEDULE FLOW: no subsequent phases, releasing schedule ${schedule?.id}`
);
await stripeCli.subscriptionSchedules.release(schedule!.id);
await CusProductService.updateByStripeScheduledId({
db: req.db,
@@ -112,6 +97,7 @@ export const handleScheduleFunction2 = async ({
});
schedule = undefined;
} else {
logger.info(`SCHEDULE FLOW: updating schedule ${schedule?.id}`);
schedule = await updateCurSchedule({
req,
attachParams,
@@ -132,6 +118,7 @@ export const handleScheduleFunction2 = async ({
});
}
} else {
logger.info(`SCHEDULE FLOW: no schedule, creating new schedule`);
schedule = await subToNewSchedule({
req,
sub: curSub!,
@@ -152,6 +139,7 @@ export const handleScheduleFunction2 = async ({
}
if (!schedule) {
logger.info(`SCHEDULE FLOW: no schedule, canceling sub ${curSub?.id}`);
await stripeCli.subscriptions.update(curSub!.id, {
cancel_at: expectedEnd!,
});

View File

@@ -25,6 +25,7 @@ import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils
import { cusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js";
import { notNullish } from "@/utils/genUtils.js";
const onDecreaseToStripeProration: Record<OnDecrease, string> = {
[OnDecrease.ProrateImmediately]: "always_invoice",
@@ -67,12 +68,20 @@ export const handleQuantityDowngrade = async ({
.minus(oldOptions.quantity)
.toNumber();
const subItemDifference = new Decimal(newOptions.quantity)
.minus(
notNullish(oldOptions.upcoming_quantity)
? oldOptions.upcoming_quantity!
: oldOptions.quantity
)
.toNumber();
const diffWithBillingUnits = new Decimal(difference)
.mul((cusPrice.price.config as UsagePriceConfig).billing_units || 1)
.toNumber();
const newSubItemQuantity = new Decimal(subItem.quantity || 0)
.plus(difference)
.plus(subItemDifference)
.toNumber();
const stripeProration = onDecreaseToStripeProration[

View File

@@ -25,6 +25,7 @@ import { Decimal } from "decimal.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
import { notNullish } from "@/utils/genUtils.js";
export const handleQuantityUpgrade = async ({
req,
@@ -55,6 +56,14 @@ export const handleQuantityUpgrade = async ({
.minus(oldOptions.quantity)
.toNumber();
const subItemDifference = new Decimal(newOptions.quantity)
.minus(
notNullish(oldOptions.upcoming_quantity)
? oldOptions.upcoming_quantity!
: oldOptions.quantity
)
.toNumber();
const onIncrease =
cusPrice.price.proration_config?.on_increase ||
OnIncrease.ProrateImmediately;
@@ -146,7 +155,7 @@ export const handleQuantityUpgrade = async ({
await stripeCli.subscriptionItems.update(subItem.id, {
// quantity: newOptions.quantity,
quantity: (subItem.quantity || 0) + difference,
quantity: (subItem.quantity || 0) + subItemDifference,
proration_behavior: "none",
});

View File

@@ -88,11 +88,11 @@ export const handleUpgradeFlow = async ({
let canceled = false;
// SCENARIO 1, NO SUB:
if (!curSub) {
console.log("UPGRADE FLOW, NO SUB (FROM CANCEL MAYBE...?)");
logger.info("UPGRADE FLOW: no sub (from cancel maybe...?)");
// Do something about current sub...
} else if (shouldCancelSub({ sub: curSub!, newSubItems: subItems })) {
console.log(
`UPGRADE FLOW, CANCELLING SUB ${curSub!.id}, PRORATE: ${config.proration}`
logger.info(
`UPGRADE FLOW: canceling sub ${curSub!.id}, proration: ${config.proration}`
);
canceled = true;
const { stripeCli } = attachParams;
@@ -104,7 +104,7 @@ export const handleUpgradeFlow = async ({
},
});
} else if (subItems.length > 0) {
console.log(`UPGRADE FLOW, UPDATING SUB ${curSub!.id}`);
logger.info(`UPGRADE FLOW, updating sub ${curSub!.id}`);
itemSet.subItems = subItems;
const res = await updateStripeSub2({
@@ -116,11 +116,22 @@ export const handleUpgradeFlow = async ({
fromCreate: attachParams.products.length === 0, // just for now, if no products, it comes from cancel product...
});
if (res?.latestInvoice) {
logger.info(`UPGRADE FLOW: inserting invoice ${res.latestInvoice.id}`);
await insertInvoiceFromAttach({
db: req.db,
attachParams,
stripeInvoice: res.latestInvoice,
logger,
});
}
const schedule = await paramsToCurSubSchedule({ attachParams });
if (schedule) {
await handleUpgradeFlowSchedule({
req,
logger,
attachParams,
config,
schedule,
@@ -133,7 +144,7 @@ export const handleUpgradeFlow = async ({
latestInvoice = res.latestInvoice;
}
logger.info(`2. Expiring previous cus product`);
logger.info(`UPGRADE FLOW: expiring previous cus product`);
await CusProductService.update({
db: req.db,
cusProductId: curCusProduct!.id,
@@ -143,17 +154,8 @@ export const handleUpgradeFlow = async ({
},
});
if (latestInvoice) {
await insertInvoiceFromAttach({
db: req.db,
attachParams,
stripeInvoice: latestInvoice,
logger,
});
}
if (attachParams.products.length > 0) {
logger.info(`3. Creating new cus product`);
logger.info(`UPGRADE FLOW: creating new cus product`);
const anchorToUnix = sub ? getEarliestPeriodEnd({ sub }) * 1000 : undefined;
await createFullCusProduct({
db: req.db,

View File

@@ -23,6 +23,7 @@ export const handleUpgradeFlowSchedule = async ({
schedule,
curSub,
removeCusProducts,
logger,
}: {
req: ExtendedRequest;
attachParams: AttachParams;
@@ -30,8 +31,9 @@ export const handleUpgradeFlowSchedule = async ({
schedule: Stripe.SubscriptionSchedule;
curSub: Stripe.Subscription;
removeCusProducts?: FullCusProduct[];
logger: any;
}) => {
console.log(`UPGRADE FLOW, UPDATING SCHEDULE ${schedule.id}`);
logger.info(`UPGRADE FLOW, updating schedule ${schedule?.id}`);
const { stripeCli, customer, prices } = attachParams;
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
@@ -70,7 +72,9 @@ export const handleUpgradeFlowSchedule = async ({
// Example: mergedUpgrade4.test.ts, mergedCancel2.test.ts
// pro, pro -> free, pro -> premium, pro (need to cancel initial schedule)
if (newCurPhaseIndex == newItems.phases.length - 1) {
console.log(`NO SUBSEQUENT PHASES, RELEASING SCHEDULE`);
logger.info(
`UPGRADE FLOW: no subsequent phases, releasing schedule ${schedule?.id}`
);
await stripeCli.subscriptionSchedules.release(schedule!.id);
await CusProductService.updateByStripeScheduledId({
db: req.db,
@@ -91,7 +95,7 @@ export const handleUpgradeFlowSchedule = async ({
.every((cp) => cp.canceled) && isFreeProduct(prices);
if (shouldCancelSub) {
console.log(`UPGRADE FLOW, CANCELING SUB: ${curSub.id}`);
logger.info(`UPGRADE FLOW: canceling sub ${curSub?.id}`);
await stripeCli.subscriptions.update(curSub.id, {
cancel_at_period_end: true,
});
@@ -100,10 +104,10 @@ export const handleUpgradeFlowSchedule = async ({
return;
}
await logPhases({
phases: newItems.phases,
db: req.db,
});
// await logPhases({
// phases: newItems.phases,
// db: req.db,
// });
await updateCurSchedule({
req,

View File

@@ -54,12 +54,13 @@ export const updateStripeSub2 = async ({
let updatedSub = await stripeCli.subscriptions.update(curSub.id, {
items: sanitizeSubItems(itemSet.subItems),
// proration_behavior:
// proration == ProrationBehavior.None
// ? "none"
// : fromCreate
// ? "always_invoice"
// : "create_prorations",
proration_behavior:
proration == ProrationBehavior.None
? "none"
: fromCreate
? "always_invoice"
: "create_prorations",
// proration_behavior: "create_prorations",
trial_end: trialEnd,
// default_payment_method: paymentMethod?.id,
add_invoice_items: itemSet.invoiceItems,

View File

@@ -64,7 +64,7 @@ export const priceToUnusedPreviewItem = ({
now?: number;
org?: Organization;
subDiscounts?: Stripe.Discount[];
latestInvoice: Stripe.Invoice;
latestInvoice?: Stripe.Invoice;
}) => {
now = now || Date.now();
const onTrial = isTrialing({ cusProduct, now });
@@ -77,7 +77,7 @@ export const priceToUnusedPreviewItem = ({
const invoiceItem = findStripeItemForPrice({
price,
invoiceLineItems: latestInvoice.lines.data,
invoiceLineItems: latestInvoice?.lines.data || [],
stripeProdId: cusProduct?.product.processor?.id,
}) as Stripe.InvoiceLineItem | undefined;
@@ -115,24 +115,10 @@ export const priceToUnusedPreviewItem = ({
now,
});
console.log("Invoice item qty: ", invoiceItem?.quantity);
// console.log(
// "Sub discounts: ",
// subDiscounts?.map((d) => d.id)
// );
// const discountsApplied = getDiscountsApplied({
// invoiceItem,
// subDiscounts,
// });
// console.log("Discounts applied: ", discountsApplied);
const ratio = new Decimal(quantity)
.div(invoiceItem?.quantity || 1)
.toNumber();
console.log("Ratio: ", ratio);
console.log("Discount amounts: ", invoiceItem?.discount_amounts);
amount = -getUnusedAmountAfterDiscount({
amount,
discountAmounts: invoiceItem?.discount_amounts || [],

View File

@@ -174,7 +174,11 @@ export const paramsToCurSub = async ({
}
const sub = await stripeCli.subscriptions.retrieve(subIds[0], {
expand: ["items.data.price.tiers"],
expand: [
"items.data.price.tiers",
"latest_invoice",
"discounts.coupon.applies_to",
],
});
return sub;

View File

@@ -23,6 +23,7 @@ import {
AttachConfig,
UsagePriceConfig,
OnDecrease,
OnIncrease,
} from "@autumn/shared";
import {
addBillingIntervalUnix,
@@ -128,6 +129,18 @@ const filterNoProratePrepaidItems = ({
);
filteredItems = items.filter((item) => item.price_id !== curPrice?.id);
}
const onIncrease = curPrice?.proration_config?.on_increase;
if (
onIncrease == OnIncrease.ProrateNextCycle &&
prevQuantity &&
quantity > prevQuantity
) {
console.log(
`Quantity for ${feature_id} increased from ${prevQuantity} to ${quantity}, Removing price: ${curPrice?.id}`
);
filteredItems = items.filter((item) => item.price_id !== curPrice?.id);
}
}
return filteredItems;
};
@@ -200,6 +213,8 @@ export const getUpgradeProductPreview = async ({
freeTrial = curCusProduct.free_trial;
}
// console.log("Disable tiral:", config?.disableTrial);
// console.log("Free trial:", freeTrial);
const newPreviewItems = await getItemsForNewProduct({
newProduct,
attachParams,

View File

@@ -12,7 +12,10 @@ import Stripe from "stripe";
import { getExistingUsageFromCusProducts } from "../../cusProducts/cusEnts/cusEntUtils.js";
import { cusProductToEnts } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { AttachParams } from "../../cusProducts/AttachParams.js";
import { getCustomerSub } from "../attachUtils/convertAttachParams.js";
import {
attachParamsToCurCusProduct,
getCustomerSub,
} from "../attachUtils/convertAttachParams.js";
export const isMultiProductSub = ({
sub,
@@ -83,6 +86,21 @@ export const willMergeSub = async ({
return false;
}
const cusProducts = attachParams.customer.customer_products;
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
// Case where upgrading to free trial...
if (
subId &&
curCusProduct?.subscription_ids?.includes(subId!) &&
!cusProducts.some(
(cp) => cp.subscription_ids?.includes(subId!) && cp.id != curCusProduct.id
) &&
attachParams.freeTrial
) {
return false;
}
if (subId) return true;
return false;

View File

@@ -197,10 +197,10 @@ const computeUpdatedScheduleItems = async ({
removeCusProducts ||
getCusProductsToRemove({ attachParams, includeScheduled: true });
console.log(
"REMOVING CUS PRODUCTS:",
cusProductsToRemove?.map((cp) => `${cp.product.id} (E: ${cp.entity_id})`)
);
// console.log(
// "REMOVING CUS PRODUCTS:",
// cusProductsToRemove?.map((cp) => `${cp.product.id} (E: ${cp.entity_id})`)
// );
const allCusProducts = attachParams.customer.customer_products;
@@ -347,18 +347,6 @@ export const paramsToScheduleItems = async ({
const mergedPhases = mergeAdjacentPhasesWithSameItems(newPhases as any);
// console.log(`Merged Phases:`);
// for (const phase of mergedPhases) {
// console.log(
// `Phase ${formatUnixToDateTime(Number(phase.start_date || 0) * 1000)}:`
// );
// await logPhaseItems({
// db: req.db,
// items: phase.items,
// });
// }
// console.log("--------------------------------");
return {
phases: mergedPhases,
invoiceItems: itemSet.invoiceItems,
@@ -366,29 +354,3 @@ export const paramsToScheduleItems = async ({
};
}
};
// let newScheduleItems: any[] = mergeNewScheduleItems({
// itemSet,
// curScheduleItems,
// });
// let cusProductsToRemove =
// removeCusProducts || getCusProductsToRemove({ attachParams });
// const allCusProducts = attachParams.customer.customer_products;
// for (const cusProduct of cusProductsToRemove) {
// newScheduleItems = removeCusProductFromScheduleItems({
// curScheduleItems,
// updateScheduleItems: newScheduleItems,
// allCusProducts,
// cusProduct,
// itemSet,
// });
// }
// return {
// items: newScheduleItems,
// invoiceItems: itemSet.invoiceItems,
// usageFeatures: itemSet.usageFeatures,
// };

View File

@@ -124,11 +124,10 @@ export const paramsToSubItems = async ({
curSubItems,
});
console.log("New sub items:", newSubItems);
const allCusProducts = attachParams.customer.customer_products;
// 3. Remove items related to cus products to remove
const printRemoveLogs = true;
const printRemoveLogs = false;
for (const cusProduct of cusProductsToRemove) {
const prices = cusProductToPrices({ cusProduct });
@@ -141,7 +140,7 @@ export const paramsToSubItems = async ({
price,
stripeItems: curSubItems,
stripeProdId: cusProduct.product.processor?.id,
});
}) as Stripe.SubscriptionItem | undefined;
if (printRemoveLogs) {
console.log("Price:", formatPrice({ price }));

View File

@@ -41,6 +41,16 @@ export const insertInvoiceFromAttach = async ({
});
if (invoice) {
// console.log("UPDATING INVOICE FROM ATTACH:");
// console.log(
// "Product IDs:",
// attachParams.products.map((p) => p.id)
// );
// console.log(
// "Internal Product IDs:",
// attachParams.products.map((p) => p.internal_id)
// );
await InvoiceService.updateByStripeId({
db,
stripeId: stripeInvoice.id!,
@@ -50,6 +60,16 @@ export const insertInvoiceFromAttach = async ({
},
});
} else {
// console.log("INSERTING INVOICE FROM ATTACH:");
// console.log(
// "Product IDs:",
// attachParams.products.map((p) => p.id)
// );
// console.log(
// "Internal Product IDs:",
// attachParams.products.map((p) => p.internal_id)
// );
await InvoiceService.createInvoiceFromStripe({
db,
stripeInvoice,

View File

@@ -62,6 +62,8 @@ export const getItemsForCurProduct = async ({
cusProduct: curCusProduct,
org: attachParams.org,
now,
latestInvoice: sub?.latest_invoice as Stripe.Invoice,
subDiscounts: sub?.discounts as Stripe.Discount[],
});
if (!previewLineItem) continue;

View File

@@ -1,7 +1,5 @@
import { handleAddProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.js";
import { handleUpgradeDiffInterval } from "@/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/handleUpgradeDiffInt.js";
import { handleUpgradeFlow } from "@/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.js";
import { handleUpgradeSameInterval } from "@/internal/customers/attach/attachFunctions/upgradeSameIntFlow/handleUpgradeSameInt.js";
import { intervalsAreSame } from "@/internal/customers/attach/attachUtils/getAttachConfig.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
@@ -54,6 +52,7 @@ export const runMigrationAttach = async ({
sameIntervals,
carryTrial: true,
invoiceCheckout: false,
finalizeInvoice: true,
};
let attachFunction = await getAttachFunction({ attachParams });

View File

@@ -1,20 +1,17 @@
import chalk from "chalk";
import Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { APIVersion, AppEnv, Organization } from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { setupBefore } from "tests/before.js";
import { createProducts, createReward } from "tests/utils/productUtils.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../utils.js";
import {
constructCoupon,
constructProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
import { timeout } from "@/utils/genUtils.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { expect } from "chai";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";

View File

@@ -4,11 +4,7 @@ import {
APIVersion,
AppEnv,
BillingInterval,
CreateFreeTrialSchema,
CusProductStatus,
FreeTrialDuration,
Organization,
organizations,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -19,11 +15,7 @@ import { addPrefixToProducts } from "../utils.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { addDays } from "date-fns";
import { expect } from "chai";
import { eq } from "drizzle-orm";
import { CacheManager } from "@/external/caching/CacheManager.js";
import { clearOrgCache } from "@/internal/orgs/orgUtils/clearOrgCache.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";

View File

@@ -83,7 +83,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro -> pro wi
testClockId = testClockId1!;
});
it("should attach free product", async function () {
it("should attach pro product", async function () {
await attachAndExpectCorrect({
autumn,
customerId,

View File

@@ -14,6 +14,7 @@ import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduc
import { expectTrialEndsAtCorrect } from "tests/utils/expectUtils/expectAttach/expectTrialEndsAt.js";
import { timeout } from "@/utils/genUtils.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
export const expectSubsSame = ({
subsBefore,
@@ -111,6 +112,15 @@ export const runMigrationTest = async ({
env,
});
if (!isFreeProductV2({ product: toProduct })) {
await expectSubToBeCorrect({
db,
customerId,
org,
env,
});
}
// if (!isFreeProductV2({ product: toProduct })) {
// expect(cusAfter.invoices.length).to.equal(numInvoices);
// }

View File

@@ -150,7 +150,7 @@ describe(`${chalk.yellowBright(`attach/${testCase}: upgrade quantity, prorate ne
testClockId,
advanceTo: addHours(
addMonths(new Date(), 1),
hoursToFinalizeInvoice,
hoursToFinalizeInvoice
).getTime(),
waitForSeconds: 10,
});

View File

@@ -0,0 +1,276 @@
// import chalk from "chalk";
// import { setupBefore } from "tests/before.js";
// import { Stripe } from "stripe";
// import { createProducts } from "tests/utils/productUtils.js";
// import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
// import { TestFeature } from "tests/setup/v2Features.js";
// import { AutumnInt } from "@/external/autumn/autumnCli.js";
// import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
// import {
// APIVersion,
// AppEnv,
// CusProductStatus,
// Organization,
// priceToInvoiceAmount,
// Proration,
// } from "@autumn/shared";
// import {
// constructArrearItem,
// constructArrearProratedItem,
// constructPrepaidItem,
// } from "@/utils/scriptUtils/constructItem.js";
// import { DrizzleCli } from "@/db/initDrizzle.js";
// import {
// addPrefixToProducts,
// getBasePrice,
// } from "tests/utils/testProductUtils/testProductUtils.js";
// import { expect } from "chai";
// import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
// import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
// import { advanceTestClock } from "tests/utils/stripeUtils.js";
// import { addWeeks } from "date-fns";
// import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
// import { formatUnixToDate, timeout } from "@/utils/genUtils.js";
// import { CusService } from "@/internal/customers/CusService.js";
// import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
// import { isPrepaidPrice } from "@shared/utils/productUtils/priceUtils.js";
// import { isContUsePrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
// import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
// import { Decimal } from "decimal.js";
// let premium = constructProduct({
// id: "premium",
// items: [
// constructArrearItem({ featureId: TestFeature.Words }),
// constructPrepaidItem({ featureId: TestFeature.Messages }),
// constructArrearProratedItem({ featureId: TestFeature.Users }),
// ],
// type: "premium",
// });
// const creditsQuantity = 500;
// const usersOverage = 1;
// const wordsUsage = 300000;
// const ops = [
// {
// entityId: "1",
// product: premium,
// results: [{ product: premium, status: CusProductStatus.Active }],
// options: [
// {
// feature_id: TestFeature.Messages,
// quantity: creditsQuantity,
// },
// ],
// usage: [
// {
// featureId: TestFeature.Users,
// value: usersOverage + 1,
// },
// ],
// },
// ];
// const testCase = "cancel1";
// describe(`${chalk.yellowBright("cancel1: Testing cancelling singular product")}`, () => {
// let customerId = testCase;
// let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
// let stripeCli: Stripe;
// let testClockId: string;
// let curUnix: number;
// let db: DrizzleCli;
// let org: Organization;
// let env: AppEnv;
// before(async function () {
// await setupBefore(this);
// const { autumnJs } = this;
// db = this.db;
// org = this.org;
// env = this.env;
// stripeCli = this.stripeCli;
// addPrefixToProducts({
// products: [premium],
// prefix: testCase,
// });
// await createProducts({
// autumn: autumnJs,
// products: [premium],
// db,
// orgId: org.id,
// env,
// customerId,
// });
// const { testClockId: testClockId1 } = await initCustomer({
// autumn: autumnJs,
// customerId,
// db,
// org,
// env,
// attachPm: "success",
// });
// testClockId = testClockId1!;
// });
// const entities = [
// {
// id: "1",
// name: "Entity 1",
// feature_id: TestFeature.Users,
// },
// {
// id: "2",
// name: "Entity 2",
// feature_id: TestFeature.Users,
// },
// ];
// it("should run operations", async function () {
// await autumn.entities.create(customerId, entities);
// for (let index = 0; index < ops.length; index++) {
// const op = ops[index];
// try {
// await attachAndExpectCorrect({
// autumn,
// customerId,
// product: op.product,
// stripeCli,
// db,
// org,
// env,
// options: op.options,
// usage: op.usage,
// });
// } catch (error) {
// console.log(
// `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`
// );
// throw error;
// }
// }
// });
// it("should advance test clock and upgrade entity 1 to premium, and have correct invoice", async function () {
// const cus1 = await autumn.customers.get(customerId);
// const prod = cus1.products.find((p) => p.id === premium.id);
// const proration = {
// start: prod?.current_period_start!,
// end: prod?.current_period_end!,
// };
// await autumn.track({
// customer_id: customerId,
// feature_id: TestFeature.Words,
// value: wordsUsage,
// });
// await timeout(3000);
// curUnix = await advanceTestClock({
// stripeCli,
// testClockId,
// advanceTo: addWeeks(Date.now(), 2).getTime(),
// waitForSeconds: 30,
// });
// await autumn.cancel({
// customer_id: customerId,
// product_id: premium.id,
// cancel_immediately: true,
// // @ts-ignore
// prorate: true,
// });
// // 1. Get full customer
// const fullCus = await CusService.getFull({
// db,
// orgId: org.id,
// env,
// idOrInternalId: customerId,
// inStatuses: [CusProductStatus.Expired, CusProductStatus.Active],
// });
// // 2. Calculate base price proration
// const basePrice = getBasePrice({ product: premium });
// const baseProration = calculateProrationAmount({
// periodStart: proration.start,
// periodEnd: proration.end,
// now: curUnix,
// amount: basePrice,
// allowNegative: true,
// });
// const cusProduct = fullCus.customer_products.find(
// (cusProduct) => cusProduct.product.id === premium.id
// );
// // 3. Calculate prepaid and cont use prices
// const prices = cusProductToPrices({ cusProduct: cusProduct! });
// const creditsPrice = prices.find((price) => isPrepaidPrice({ price }));
// const usersPrice = prices.find((price) => isContUsePrice({ price }));
// const creditsPriceAmount = priceToInvoiceAmount({
// price: creditsPrice!,
// quantity: creditsQuantity,
// proration,
// now: curUnix,
// });
// const usersPriceAmount = priceToInvoiceAmount({
// price: usersPrice!,
// overage: usersOverage,
// proration,
// now: curUnix,
// });
// // 4. Calculate words amount
// const wordsAmount = await getExpectedInvoiceTotal({
// db,
// org,
// env,
// onlyIncludeArrear: true,
// usage: [
// {
// featureId: TestFeature.Words,
// value: wordsUsage,
// },
// ],
// stripeCli,
// customerId,
// productId: premium.id,
// expectExpired: true,
// });
// const totalPrice = new Decimal(wordsAmount)
// .minus(baseProration)
// .minus(creditsPriceAmount)
// .minus(usersPriceAmount)
// .toDecimalPlaces(2)
// .toNumber();
// // console.log("BASE PRORATION", baseProration);
// // console.log("CREDITS PRORATION", creditsPriceAmount);
// // console.log("USERS PRORATION", usersPriceAmount);
// // console.log("WORDS AMOUNT", wordsAmount);
// // console.log("TOTAL PRICE", totalPrice);
// // Get upcoming invoice
// await timeout(5000); // for webhook to trigger
// const upcomingInvoices = await stripeCli.invoices.list({
// customer: fullCus.processor?.id,
// limit: 1,
// status: "draft",
// });
// // console.log("INVOICE TOTAL", upcomingInvoices.data[0].total);
// // console.log("INVOICE ID", upcomingInvoices.data[0].id);
// expect(upcomingInvoices.data[0].total).to.equal(totalPrice * 100);
// });
// });

View File

@@ -1,276 +0,0 @@
import chalk from "chalk";
import { setupBefore } from "tests/before.js";
import { Stripe } from "stripe";
import { createProducts } from "tests/utils/productUtils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
APIVersion,
AppEnv,
CusProductStatus,
Organization,
priceToInvoiceAmount,
Proration,
} from "@autumn/shared";
import {
constructArrearItem,
constructArrearProratedItem,
constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
addPrefixToProducts,
getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addWeeks } from "date-fns";
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
import { formatUnixToDate, timeout } from "@/utils/genUtils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { isPrepaidPrice } from "@shared/utils/productUtils/priceUtils.js";
import { isContUsePrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
import { Decimal } from "decimal.js";
let premium = constructProduct({
id: "premium",
items: [
constructArrearItem({ featureId: TestFeature.Words }),
constructPrepaidItem({ featureId: TestFeature.Messages }),
constructArrearProratedItem({ featureId: TestFeature.Users }),
],
type: "premium",
});
const creditsQuantity = 500;
const usersOverage = 1;
const wordsUsage = 300000;
const ops = [
{
entityId: "1",
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
options: [
{
feature_id: TestFeature.Messages,
quantity: creditsQuantity,
},
],
usage: [
{
featureId: TestFeature.Users,
value: usersOverage + 1,
},
],
},
];
const testCase = "cancel1";
describe(`${chalk.yellowBright("cancel1: Testing cancelling singular product")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
let stripeCli: Stripe;
let testClockId: string;
let curUnix: number;
let db: DrizzleCli;
let org: Organization;
let env: AppEnv;
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
addPrefixToProducts({
products: [premium],
prefix: testCase,
});
await createProducts({
autumn: autumnJs,
products: [premium],
db,
orgId: org.id,
env,
customerId,
});
const { testClockId: testClockId1 } = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
testClockId = testClockId1!;
});
const entities = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
{
id: "2",
name: "Entity 2",
feature_id: TestFeature.Users,
},
];
it("should run operations", async function () {
await autumn.entities.create(customerId, entities);
for (let index = 0; index < ops.length; index++) {
const op = ops[index];
try {
await attachAndExpectCorrect({
autumn,
customerId,
product: op.product,
stripeCli,
db,
org,
env,
options: op.options,
usage: op.usage,
});
} catch (error) {
console.log(
`Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`
);
throw error;
}
}
});
it("should advance test clock and upgrade entity 1 to premium, and have correct invoice", async function () {
const cus1 = await autumn.customers.get(customerId);
const prod = cus1.products.find((p) => p.id === premium.id);
const proration = {
start: prod?.current_period_start!,
end: prod?.current_period_end!,
};
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Words,
value: wordsUsage,
});
await timeout(3000);
curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addWeeks(Date.now(), 2).getTime(),
waitForSeconds: 30,
});
await autumn.cancel({
customer_id: customerId,
product_id: premium.id,
cancel_immediately: true,
// @ts-ignore
prorate: true,
});
// 1. Get full customer
const fullCus = await CusService.getFull({
db,
orgId: org.id,
env,
idOrInternalId: customerId,
inStatuses: [CusProductStatus.Expired, CusProductStatus.Active],
});
// 2. Calculate base price proration
const basePrice = getBasePrice({ product: premium });
const baseProration = calculateProrationAmount({
periodStart: proration.start,
periodEnd: proration.end,
now: curUnix,
amount: basePrice,
allowNegative: true,
});
const cusProduct = fullCus.customer_products.find(
(cusProduct) => cusProduct.product.id === premium.id
);
// 3. Calculate prepaid and cont use prices
const prices = cusProductToPrices({ cusProduct: cusProduct! });
const creditsPrice = prices.find((price) => isPrepaidPrice({ price }));
const usersPrice = prices.find((price) => isContUsePrice({ price }));
const creditsPriceAmount = priceToInvoiceAmount({
price: creditsPrice!,
quantity: creditsQuantity,
proration,
now: curUnix,
});
const usersPriceAmount = priceToInvoiceAmount({
price: usersPrice!,
overage: usersOverage,
proration,
now: curUnix,
});
// 4. Calculate words amount
const wordsAmount = await getExpectedInvoiceTotal({
db,
org,
env,
onlyIncludeArrear: true,
usage: [
{
featureId: TestFeature.Words,
value: wordsUsage,
},
],
stripeCli,
customerId,
productId: premium.id,
expectExpired: true,
});
const totalPrice = new Decimal(wordsAmount)
.minus(baseProration)
.minus(creditsPriceAmount)
.minus(usersPriceAmount)
.toDecimalPlaces(2)
.toNumber();
// console.log("BASE PRORATION", baseProration);
// console.log("CREDITS PRORATION", creditsPriceAmount);
// console.log("USERS PRORATION", usersPriceAmount);
// console.log("WORDS AMOUNT", wordsAmount);
// console.log("TOTAL PRICE", totalPrice);
// Get upcoming invoice
await timeout(5000); // for webhook to trigger
const upcomingInvoices = await stripeCli.invoices.list({
customer: fullCus.processor?.id,
limit: 1,
status: "draft",
});
// console.log("INVOICE TOTAL", upcomingInvoices.data[0].total);
// console.log("INVOICE ID", upcomingInvoices.data[0].id);
expect(upcomingInvoices.data[0].total).to.equal(totalPrice * 100);
});
});

View File

@@ -0,0 +1,125 @@
import chalk from "chalk";
import { setupBefore } from "tests/before.js";
import { Stripe } from "stripe";
import { createProducts } from "tests/utils/productUtils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
APIVersion,
AppEnv,
CusProductStatus,
Organization,
} from "@autumn/shared";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addDays } from "date-fns";
import { expectMultiAttachCorrect } from "tests/utils/expectUtils/expectMultiAttach.js";
import { products } from "tests/global.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { CusService } from "@/internal/customers/CusService.js";
import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { timeout } from "@/utils/genUtils.js";
import { expect } from "chai";
const testCase = "cancel1";
describe(`${chalk.yellowBright("cancel1: Testing cancel for trial products")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
let stripeCli: Stripe;
let testClockId: string;
let curUnix: number;
let db: DrizzleCli;
let org: Organization;
let env: AppEnv;
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
const { testClockId: testClockId1 } = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
testClockId = testClockId1!;
});
it("should attach pro", async function () {
await autumn.attach({
customer_id: customerId,
product_id: products.pro.id,
});
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
productId: products.pro.id,
});
});
let sub: Stripe.Subscription | undefined;
it("should cancel pro product through stripe CLI", async function () {
const fullCus = await CusService.getFull({
db,
idOrInternalId: customerId,
orgId: org.id,
env,
});
sub = await cusProductToSub({
cusProduct: fullCus.customer_products?.[0],
stripeCli,
});
await stripeCli.subscriptions.update(sub!.id, {
cancel_at_period_end: true,
});
await timeout(4000);
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
productId: products.pro.id,
isCanceled: true,
});
expectProductAttached({
customer,
productId: products.free.id,
status: CusProductStatus.Scheduled,
});
});
it("should renew pro produce through stripe CLI and have it update correctly", async function () {
await stripeCli.subscriptions.update(sub!.id, {
cancel_at_period_end: false,
});
await timeout(4000);
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
productId: products.pro.id,
status: CusProductStatus.Active,
});
expect(customer.products.length).to.equal(1);
});
});

View File

@@ -12,24 +12,11 @@ import {
CusProductStatus,
Organization,
} from "@autumn/shared";
import {
constructArrearItem,
constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
addPrefixToProducts,
getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import {
advanceTestClock,
completeCheckoutForm,
} from "tests/utils/stripeUtils.js";
import { addDays, addWeeks } from "date-fns";
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addDays } from "date-fns";
import { expectMultiAttachCorrect } from "tests/utils/expectUtils/expectMultiAttach.js";
let growth = constructProduct({

View File

@@ -0,0 +1,180 @@
import chalk from "chalk";
import { setupBefore } from "tests/before.js";
import { Stripe } from "stripe";
import { createProducts } from "tests/utils/productUtils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
APIVersion,
AppEnv,
CusProductStatus,
Organization,
} from "@autumn/shared";
import {
constructArrearItem,
constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
addPrefixToProducts,
getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import {
advanceTestClock,
completeCheckoutForm,
} from "tests/utils/stripeUtils.js";
import { addDays, addWeeks } from "date-fns";
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
import { expectMultiAttachCorrect } from "tests/utils/expectUtils/expectMultiAttach.js";
let premium = constructProduct({
id: "premium",
items: [
constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
],
type: "premium",
trial: true,
});
let pro = constructProduct({
id: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Words,
includedUsage: 300,
}),
],
type: "pro",
trial: true,
});
const ops = [
{
entityId: "1",
product: pro,
results: [{ product: pro, status: CusProductStatus.Active }],
},
{
entityId: "2",
product: pro,
results: [{ product: pro, status: CusProductStatus.Active }],
},
];
const testCase = "multiInvoice1";
describe(`${chalk.yellowBright("multiInvoice1: Testing multi attach through invoice flow")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
let stripeCli: Stripe;
let testClockId: string;
let curUnix: number;
let db: DrizzleCli;
let org: Organization;
let env: AppEnv;
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
addPrefixToProducts({
products: [pro, premium],
prefix: testCase,
});
await createProducts({
autumn: autumnJs,
products: [pro, premium],
db,
orgId: org.id,
env,
customerId,
});
const { testClockId: testClockId1 } = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
// attachPm: "success",
});
testClockId = testClockId1!;
});
it("should run multi attach through checkout and have correct sub", async function () {
const productsList = [
{
product_id: pro.id,
quantity: 3,
product: pro,
status: CusProductStatus.Trialing,
},
{
product_id: premium.id,
quantity: 3,
product: premium,
status: CusProductStatus.Trialing,
},
];
await expectMultiAttachCorrect({
customerId,
products: productsList,
results: productsList,
attachParams: {
invoice: true,
enable_product_immediately: true,
},
db,
org,
env,
});
});
it("should update premium & pro while trialing", async function () {
const newProducts = [
{
product_id: premium.id,
quantity: 2,
},
{
product_id: pro.id,
quantity: 4,
},
];
const results = [
{
product: pro,
quantity: 4,
status: CusProductStatus.Trialing,
},
{
product: premium,
quantity: 2,
status: CusProductStatus.Trialing,
},
];
await expectMultiAttachCorrect({
customerId,
products: newProducts,
results,
db,
org,
env,
});
});
});

View File

@@ -141,6 +141,7 @@ describe(`${chalk.yellowBright("multiReward3: Testing multi attach with rewards
stripeCli,
testClockId,
advanceTo: addDays(new Date(), 12).getTime(),
waitForSeconds: 30,
});
await expectSubToBeCorrect({

View File

@@ -98,6 +98,12 @@ export const setupMultiRewardBefore = async ({
]) {
// let res = await autumn.products.get(product.id);
// if (res.code === "product_not_found") {
// try {
// await autumn.products.create(product);
// } catch (error) {}
// }
try {
await autumn.products.delete(product.id);
} catch (error) {
@@ -128,17 +134,26 @@ export const setupMultiRewardBefore = async ({
];
for (const reward of [proReward, premiumReward]) {
let rewardRes = null;
// try {
// rewardRes = await autumn.rewards.get(reward.id);
// } catch (error) {}
try {
await autumn.rewards.delete(reward.id);
} catch (error) {}
try {
await autumn.rewards.create({
...reward,
discount_config: {
...reward.discount_config,
price_ids: reward.id == proReward.id ? proPriceIds : premiumPriceIds,
},
});
} catch (error) {}
if (!rewardRes) {
try {
await autumn.rewards.create({
...reward,
discount_config: {
...reward.discount_config,
price_ids:
reward.id == proReward.id ? proPriceIds : premiumPriceIds,
},
});
} catch (error) {}
}
}
};

View File

@@ -14,7 +14,7 @@ import {
getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { expectSubToBeCorrect } from "./mergeUtils.test.js";
import { expectSubToBeCorrect } from "../mergeUtils.test.js";
import { getAttachPreviewTotal } from "tests/utils/testAttachUtils/getAttachPreviewTotal.js";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";

View File

@@ -14,7 +14,7 @@ import {
getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { expectSubToBeCorrect } from "./mergeUtils.test.js";
import { expectSubToBeCorrect } from "../mergeUtils.test.js";
import { getAttachPreviewTotal } from "tests/utils/testAttachUtils/getAttachPreviewTotal.js";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";

View File

@@ -14,7 +14,6 @@ import {
AppEnv,
CusProductStatus,
Organization,
ProductItemInterval,
} from "@autumn/shared";
import {
constructArrearItem,
@@ -223,6 +222,6 @@ describe(`${chalk.yellowBright("mergedAddOn1: Adding an add on")}`, () => {
status: CusProductStatus.Active,
});
const products = customer.products.filter((p) => p.group === addOn.group);
expect(products.length).to.equal(0);
expect(products.length).to.equal(1);
});
});

View File

@@ -187,6 +187,7 @@ describe(`${chalk.yellowBright("mergedAddOn3: testing add ons between multiple e
await autumn.cancel({
customer_id: customerId,
product_id: addOn.id,
entity_id: "1",
cancel_immediately: true,
});

View File

@@ -181,7 +181,7 @@ describe(`${chalk.yellowBright("mergedDowngrade4: Testing advance clock, schedul
product: premiumAnnual,
status: CusProductStatus.Active,
},
{ entityId: "2", product: premium, status: CusProductStatus.Active },
{ entityId: "2", product: pro, status: CusProductStatus.Active },
];
for (const result of results) {

View File

@@ -36,6 +36,8 @@ import {
} from "@/internal/customers/attach/mergeUtils/phaseUtils/phaseUtils.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { subIsCanceled } from "@/external/stripe/stripeSubUtils.js";
import { defaultApiVersion } from "tests/constants.js";
const compareActualItems = async ({
actualItems,
@@ -131,6 +133,7 @@ const compareActualItems = async ({
export const expectSubToBeCorrect = async ({
db,
customerId,
entityId,
org,
env,
shouldBeCanceled = false,
@@ -141,6 +144,7 @@ export const expectSubToBeCorrect = async ({
}: {
db: DrizzleCli;
customerId: string;
entityId?: string;
org: Organization;
env: AppEnv;
shouldBeCanceled?: boolean;
@@ -200,6 +204,7 @@ export const expectSubToBeCorrect = async ({
// Add to schedules
const scheduleIndexes: number[] = [];
const apiVersion = cusProduct.api_version || defaultApiVersion;
if (isFreeProduct(product.prices)) {
expect(cusProduct.subscription_ids, "free product should have no subs").to
@@ -292,9 +297,9 @@ export const expectSubToBeCorrect = async ({
org,
options,
existingUsage,
withEntity: true,
withEntity: !!entityId,
isCheckout: false,
apiVersion: APIVersion.v1_4,
apiVersion,
productOptions: cusProduct.quantity
? {
product_id: product.id,
@@ -303,6 +308,12 @@ export const expectSubToBeCorrect = async ({
: undefined,
});
if (res?.lineItem && nullish(res.lineItem.quantity)) {
res.lineItem.quantity = 0;
}
// console.log("API VERSION:", apiVersion);
// console.log("LINE ITEM:", res?.lineItem);
if (options?.upcoming_quantity && res?.lineItem) {
res.lineItem.quantity = options.upcoming_quantity;
}
@@ -316,7 +327,7 @@ export const expectSubToBeCorrect = async ({
if (existingIndex !== -1) {
// @ts-ignore
supposedSubItems[existingIndex].quantity += lineItem.quantity!;
supposedSubItems[existingIndex].quantity += lineItem.quantity;
} else {
supposedSubItems.push({
...res.lineItem,
@@ -384,7 +395,8 @@ export const expectSubToBeCorrect = async ({
if (shouldBeCanceled) {
expect(sub.schedule, "sub should NOT have a schedule").to.be.null;
expect(sub.cancel_at, "sub should be canceled").to.exist;
// expect(sub.cancel_at, "sub should be canceled").to.exist;
expect(subIsCanceled({ sub }), "sub should be canceled").to.be.true;
return;
}
@@ -395,40 +407,24 @@ export const expectSubToBeCorrect = async ({
})
: null;
console.log("--------------------------------");
console.log("Supposed phases:");
await logPhases({
phases: supposedPhases,
db,
});
// for (const phase of supposedPhases) {
// console.log(`Phase ${formatUnixToDateTime(phase.start_date)}:`);
// await logPhaseItems({
// db,
// items: phase.items,
// });
// }
console.log("--------------------------------");
console.log("Actual phases:");
// console.log("--------------------------------");
// console.log("Supposed phases:");
// await logPhases({
// phases: supposedPhases,
// db,
// });
await logPhases({
phases: (schedule?.phases as any) || [],
db,
});
// for (const phase of schedule?.phases || []) {
// console.log(`Phase ${formatUnixToDateTime(phase.start_date * 1000)}:`);
// await logPhaseItems({
// db,
// items: phase.items.map((item) => ({
// price: (item.price as Stripe.Price).id,
// quantity: item.quantity,
// })),
// });
// }
// console.log("--------------------------------");
// console.log("Actual phases:");
// await logPhases({
// phases: (schedule?.phases as any) || [],
// db,
// });
for (let i = 0; i < supposedPhases.length; i++) {
const supposedPhase = supposedPhases[i];
console.log("Supposed phase items:", supposedPhase.items);
if (supposedPhase.items.length === 0) continue;
const actualPhase = schedule?.phases?.[i + 1];

View File

@@ -23,6 +23,8 @@ import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.j
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addDays } from "date-fns";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { timeout } from "@/utils/genUtils.js";
import { Decimal } from "decimal.js";
// Pro Trial
// Trial Finishes
@@ -101,7 +103,7 @@ describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from
testClockId = testClockId1!;
});
it("should attach first trial, and advance clock past trial", async function () {
it("should attach first trial", async function () {
for (const op of ops) {
await attachAndExpectCorrect({
autumn,
@@ -122,7 +124,7 @@ describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from
});
});
it("should advance test clock to before trial ends and attach premium", async function () {
it("should advance test clock to past trial ends and attach premium", async function () {
curUnix = await advanceTestClock({
stripeCli,
testClockId,
@@ -134,6 +136,11 @@ describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from
product_id: premium.id,
});
const checkoutRes = await autumn.checkout({
customer_id: customerId,
product_id: premium.id,
});
expect(attachPreview?.branch).to.equal(AttachBranch.Upgrade);
await autumn.attach({
@@ -141,6 +148,8 @@ describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from
product_id: premium.id,
});
await timeout(5000);
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
@@ -153,6 +162,10 @@ describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from
1000 * 60 * 30 // 30 minutes
);
expect(customer.invoices[0].total).to.equal(
new Decimal(checkoutRes.total).toDP(2).toNumber()
);
await expectSubToBeCorrect({
db,
customerId,

View File

@@ -75,6 +75,7 @@ const ops = [
entityId: "2",
product: growth,
results: [{ product: growth, status: CusProductStatus.Active }],
// waitForInvoice: 5000,
},
];

View File

@@ -89,7 +89,7 @@ export const attachAndExpectCorrect = async ({
...attachParams,
});
const logCheckoutRes = true;
const logCheckoutRes = false;
if (logCheckoutRes) {
console.log("Checkout res:");
for (const line of checkoutRes.lines) {
@@ -210,6 +210,7 @@ export const attachAndExpectCorrect = async ({
flags: {
checkNotTrialing,
},
entityId,
});
// await expectSubItemsCorrect({

View File

@@ -30,6 +30,7 @@ import { AttachParams, Customer } from "autumn-js";
import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
import { Decimal } from "decimal.js";
import { completeInvoiceCheckout } from "../stripeUtils/completeInvoiceCheckout.js";
export const expectMultiAttachCorrect = async ({
customerId,
@@ -38,6 +39,7 @@ export const expectMultiAttachCorrect = async ({
results,
rewards,
expectedRewards,
attachParams,
db,
org,
env,
@@ -52,6 +54,7 @@ export const expectMultiAttachCorrect = async ({
}[];
rewards?: string[];
expectedRewards?: string[];
attachParams?: any;
db: DrizzleCli;
org: Organization;
env: AppEnv;
@@ -63,6 +66,7 @@ export const expectMultiAttachCorrect = async ({
entity_id: entityId,
// @ts-ignore
reward: rewards,
...attachParams,
});
const attachRes = await autumn.attach({
@@ -71,9 +75,16 @@ export const expectMultiAttachCorrect = async ({
entity_id: entityId,
// @ts-ignore
reward: rewards,
...attachParams,
});
if (attachRes.checkout_url) {
if (attachParams?.invoice) {
await completeInvoiceCheckout({
url: attachRes.checkout_url,
isLocal: true,
});
}
await completeCheckoutForm(attachRes.checkout_url);
await timeout(5000);
}

View File

@@ -10,12 +10,14 @@ export const expectProductAttached = ({
productId,
status,
entityId,
isCanceled = false,
}: {
customer: Customer;
product?: ProductV2;
productId?: string;
status?: CusProductStatus;
entityId?: string;
isCanceled?: boolean;
}) => {
const cusProducts = customer.products;
const finalProductId = productId || product?.id;
@@ -47,6 +49,12 @@ export const expectProductAttached = ({
// @ts-ignore
expect(productAttached?.entity_id).to.equal(entityId);
}
if (isCanceled) {
expect(productAttached?.canceled_at).to.exist;
// @ts-ignore
expect(productAttached?.canceled).to.be.true;
}
};
export const expectInvoicesCorrect = ({

View File

@@ -9,6 +9,7 @@ import {
AppEnv,
AttachBranch,
AttachPreview,
CusProductStatus,
FullCusProduct,
FullCustomer,
Organization,
@@ -22,6 +23,7 @@ import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduc
import { advanceTestClock } from "../stripeUtils.js";
import { hoursToFinalizeInvoice } from "../constants.js";
import { addHours } from "date-fns";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
export const expectNextCycleCorrect = async ({
autumn,
@@ -119,42 +121,50 @@ export const expectDowngradeCorrect = async ({
expectProductAttached({
customer,
product: curProduct,
});
const { fullCus } = await expectSubItemsCorrect({
stripeCli,
customerId,
product: curProduct,
db,
org,
env,
isCanceled: true,
});
// const { fullCus } = await expectSubItemsCorrect({
// stripeCli,
// customerId,
// product: curProduct,
// db,
// org,
// env,
// subCanceled: isFreeProductV2({ product: newProduct }),
// isCanceled: true,
// });
const newProductIsFree = isFreeProductV2({ product: newProduct });
if (newProductIsFree) {
let res = await stripeCli.subscriptionSchedules.list({
customer: fullCus.processor?.id,
});
let data = res.data.filter((s) => s.status != "canceled");
expect(data.length, "should have no sub schedules").to.equal(0);
await expectSubScheduleCorrect({
stripeCli,
customerId,
productId: newProduct.id,
db,
org,
env,
});
expectProductAttached({
customer,
product: newProduct,
});
// let res = await stripeCli.subscriptionSchedules.list({
// customer: fullCus.processor?.id,
// });
// let data = res.data.filter((s) => s.status != "canceled");
// expect(data.length, "should have no sub schedules").to.equal(0);
// await expectSubScheduleCorrect({
// stripeCli,
// customerId,
// productId: newProduct.id,
// db,
// org,
// env,
// });
}
expectProductAttached({
customer,
product: newProduct,
status: CusProductStatus.Scheduled,
});
await expectSubToBeCorrect({
db,
customerId,
org,
env,
shouldBeCanceled: newProductIsFree,
});
expect(preview.branch).to.equal(AttachBranch.Downgrade);

View File

@@ -12,7 +12,7 @@ import { CusService } from "@/internal/customers/CusService.js";
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import { isV4Usage } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js";
import { nullish } from "@/utils/genUtils.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
import {
AppEnv,
BillingType,
@@ -128,6 +128,14 @@ export const getSubsFromCusId = async ({
// };
// };
const subIsCanceled = ({ sub }: { sub: Stripe.Subscription }) => {
return (
notNullish(sub.canceled_at) ||
notNullish(sub.cancel_at) ||
sub.cancel_at_period_end
);
};
export const expectSubItemsCorrect = async ({
stripeCli,
customerId,
@@ -135,6 +143,7 @@ export const expectSubItemsCorrect = async ({
db,
org,
env,
subCanceled = false,
isCanceled = false,
entityId,
}: {
@@ -144,6 +153,7 @@ export const expectSubItemsCorrect = async ({
db: DrizzleCli;
org: Organization;
env: AppEnv;
subCanceled?: boolean;
isCanceled?: boolean;
entityId?: string;
}) => {
@@ -167,13 +177,21 @@ export const expectSubItemsCorrect = async ({
if (isCanceled) {
expect(
cusProduct.canceled_at,
`cus product ${cusProduct.product.id} should be canceled`
`cus product ${cusProduct.product.id} have field 'canceled_at' set`
).to.exist;
expect(
cusProduct.canceled,
`cus product ${cusProduct.product.id} have field 'canceled' set to true`
).to.be.true;
} else {
expect(
cusProduct.canceled_at,
`cus product ${cusProduct.product.id} should not be canceled`
`cus product ${cusProduct.product.id} should not have field 'canceled_at' set`
).to.not.exist;
expect(
cusProduct.canceled,
`cus product ${cusProduct.product.id} should not have field 'canceled' set`
).to.be.false;
}
if (isFreeProductV2({ product })) {
@@ -192,10 +210,10 @@ export const expectSubItemsCorrect = async ({
});
for (const sub of subs) {
if (isCanceled) {
expect(sub.canceled_at, "sub should be canceled").to.exist;
if (subCanceled) {
expect(subIsCanceled({ sub }), "sub should be canceled").to.be.true;
} else {
expect(sub.canceled_at, "sub should not be canceled").to.be.null;
expect(subIsCanceled({ sub }), "sub should not be canceled").to.be.false;
}
}
@@ -218,9 +236,6 @@ export const expectSubItemsCorrect = async ({
const usagePriceConfig = price.config as UsagePriceConfig;
// console.log("Sub item:", subItem);
// console.log("Usage price config:", usagePriceConfig);
expect(
nullish(subItem) ||
(subItem?.quantity === 0 &&

View File

@@ -4,20 +4,11 @@ import {
CustomDialogFooter,
} from "@/components/general/modal-components/DialogContentWrapper";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { MainDialogBodyWrapper } from "@/views/products/product/product-item/product-item-config/AdvancedConfigSidebar";
import { Dialog, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { InvoiceCustomerButton } from "../components/InvoiceCustomerButton";
import FieldLabel from "@/components/general/modal-components/FieldLabel";
import { ArrowUpRightFromSquare, Loader2, Minus, Plus, X } from "lucide-react";
import {
DropdownMenu,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ArrowUpRightFromSquare, Loader2, Plus, X } from "lucide-react";
import { useCustomerContext } from "../../CustomerContext";
import {
Select,
@@ -58,6 +49,7 @@ export const MultiAttachDialog = ({
const [checkoutResult, setCheckoutResult] = useState<CheckoutResult | null>(
null
);
const [attachLoading, setAttachLoading] = useState(false);
const defaultCurrency = org?.default_currency || "usd";
@@ -97,7 +89,6 @@ export const MultiAttachDialog = ({
}
}
console.log("attachRewards", attachRewards);
for (const reward of attachRewards) {
if (!reward.reward_id) {
return false;
@@ -333,20 +324,21 @@ export const MultiAttachDialog = ({
<InvoiceCustomerButton
handleAttachClicked={handleAttachClicked}
disabled={checkoutLoading}
checkoutAllowed={!!checkoutResult?.url}
checkoutAllowed={!!checkoutResult?.url && checkoutResult.total > 0}
/>
<Button
variant="add"
isLoading={attachLoading}
onClick={() => {
handleAttachClicked({
useInvoice: false,
enableProductImmediately: false,
setLoading: setCheckoutLoading,
setLoading: setAttachLoading,
});
}}
endIcon={<ArrowUpRightFromSquare size={12} />}
disabled={checkoutLoading}
disabled={checkoutLoading || attachLoading}
disableStartIcon
>
{checkoutResult?.url ? "Checkout Page" : "Attach Products"}