fix: tests
This commit is contained in:
@@ -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' \
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
6
server/shell/g6.sh
Executable 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'
|
||||
5
server/src/external/autumn/autumnCli.ts
vendored
5
server/src/external/autumn/autumnCli.ts
vendored
@@ -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;
|
||||
|
||||
9
server/src/external/stripe/stripeSubUtils.ts
vendored
9
server/src/external/stripe/stripeSubUtils.ts
vendored
@@ -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
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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()}` : ""}`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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!,
|
||||
});
|
||||
|
||||
@@ -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[
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 || [],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
// };
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
276
server/tests/core/cancel/cancel1.test.ts
Normal file
276
server/tests/core/cancel/cancel1.test.ts
Normal 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);
|
||||
// });
|
||||
// });
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
125
server/tests/core/cancel/cancel5.test.ts
Normal file
125
server/tests/core/cancel/cancel5.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
180
server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts
Normal file
180
server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -75,6 +75,7 @@ const ops = [
|
||||
entityId: "2",
|
||||
product: growth,
|
||||
results: [{ product: growth, status: CusProductStatus.Active }],
|
||||
// waitForInvoice: 5000,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user