fix: updating product with free features

This commit is contained in:
John Yeo
2025-06-11 19:44:09 +01:00
parent afddbe4d3b
commit 5c6c2e603b
7 changed files with 54 additions and 39 deletions

View File

@@ -67,7 +67,10 @@ export const handleUpgradeSameInterval = async ({
disableFreeTrial: config.disableTrial, disableFreeTrial: config.disableTrial,
carryExistingUsages: config.carryUsage, carryExistingUsages: config.carryUsage,
carryOverTrial: config.carryTrial, carryOverTrial: config.carryTrial,
anchorToUnix: stripeSubs[0].current_period_end * 1000, anchorToUnix:
stripeSubs.length > 0
? stripeSubs[0].current_period_end * 1000
: undefined,
logger, logger,
}); });

View File

@@ -26,6 +26,7 @@ import {
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
import { Decimal } from "decimal.js"; import { Decimal } from "decimal.js";
import { intervalsAreSame } from "../attachUtils/getAttachConfig.js"; import { intervalsAreSame } from "../attachUtils/getAttachConfig.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
const getNextCycleAt = ({ const getNextCycleAt = ({
prices, prices,
@@ -107,9 +108,10 @@ export const getUpgradeProductPreview = async ({
// Get prorated amounts for new product // Get prorated amounts for new product
const newProduct = attachParamsToProduct({ attachParams }); const newProduct = attachParamsToProduct({ attachParams });
const intervalsSame = intervalsAreSame({ attachParams }); const intervalsSame = intervalsAreSame({ attachParams });
const anchorToUnix = intervalsSame const anchorToUnix =
? stripeSubs[0].current_period_end * 1000 intervalsSame && stripeSubs.length > 0
: undefined; ? stripeSubs[0].current_period_end * 1000
: undefined;
const newPreviewItems = await getItemsForNewProduct({ const newPreviewItems = await getItemsForNewProduct({
newProduct, newProduct,
@@ -122,21 +124,30 @@ export const getUpgradeProductPreview = async ({
}); });
const lastInterval = getLastInterval({ prices: newProduct.prices }); const lastInterval = getLastInterval({ prices: newProduct.prices });
const nextCycleAt = getNextCycleAt({
prices: newProduct.prices,
stripeSubs,
willCycleReset: !intervalsSame,
interval: lastInterval,
now,
freeTrial: attachParams.freeTrial,
});
let nextCycleItems = await getItemsForNewProduct({ let dueNextCycle = undefined;
newProduct, if (!isFreeProduct(newProduct.prices)) {
attachParams, const nextCycleAt = getNextCycleAt({
interval: attachParams.freeTrial ? undefined : lastInterval, prices: newProduct.prices,
logger, stripeSubs,
}); willCycleReset: !intervalsSame,
interval: lastInterval,
now,
freeTrial: attachParams.freeTrial,
});
let nextCycleItems = await getItemsForNewProduct({
newProduct,
attachParams,
interval: attachParams.freeTrial ? undefined : lastInterval,
logger,
});
dueNextCycle = {
line_items: nextCycleItems,
due_at: nextCycleAt.next_cycle_at,
};
}
let items = [...curPreviewItems, ...newPreviewItems]; let items = [...curPreviewItems, ...newPreviewItems];
@@ -175,7 +186,7 @@ export const getUpgradeProductPreview = async ({
if (branch == AttachBranch.UpdatePrepaidQuantity) { if (branch == AttachBranch.UpdatePrepaidQuantity) {
items = items.filter((item) => item.usage_model == UsageModel.Prepaid); items = items.filter((item) => item.usage_model == UsageModel.Prepaid);
nextCycleItems = nextCycleItems.filter( dueNextCycle!.line_items = dueNextCycle!.line_items.filter(
(item) => item.usage_model == UsageModel.Prepaid, (item) => item.usage_model == UsageModel.Prepaid,
); );
} }
@@ -197,10 +208,7 @@ export const getUpgradeProductPreview = async ({
return { return {
currency: attachParams.org.default_currency, currency: attachParams.org.default_currency,
due_today: dueToday, due_today: dueToday,
due_next_cycle: { due_next_cycle: dueNextCycle,
line_items: nextCycleItems,
due_at: nextCycleAt.next_cycle_at,
},
options, options,
}; };

View File

@@ -2,11 +2,7 @@ import { handleRequestError } from "@/utils/errorUtils.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import RecaseError from "@/utils/errorUtils.js"; import RecaseError from "@/utils/errorUtils.js";
import { import { ErrCode, FullCustomerEntitlement } from "@autumn/shared";
ErrCode,
FullCustomerEntitlement,
FullCusEntWithProduct,
} from "@autumn/shared";
import { Decimal } from "decimal.js"; import { Decimal } from "decimal.js";
import { StatusCodes } from "http-status-codes"; import { StatusCodes } from "http-status-codes";
import { adjustAllowance } from "@/trigger/adjustAllowance.js"; import { adjustAllowance } from "@/trigger/adjustAllowance.js";

View File

@@ -173,15 +173,30 @@ export const itemsAreSame = ({
// 1. If feature item // 1. If feature item
let same = false; let same = false;
let pricesChanged = false; let pricesChanged = false;
if (isFeatureItem(item1)) { if (isFeatureItem(item1)) {
if (!isFeatureItem(item2)) {
return {
same: false,
pricesChanged: true,
};
}
same = featureItemsAreSame({ same = featureItemsAreSame({
item1: FeatureItemSchema.parse(item1), item1: FeatureItemSchema.parse(item1),
item2: FeatureItemSchema.parse(item2), item2: item2 as FeatureItem,
}); });
pricesChanged = false; pricesChanged = false;
} }
if (isFeaturePriceItem(item1)) { if (isFeaturePriceItem(item1)) {
if (!isFeaturePriceItem(item2)) {
return {
same: false,
pricesChanged: true,
};
}
const { same: same_, pricesChanged: pricesChanged_ } = const { same: same_, pricesChanged: pricesChanged_ } =
featurePriceItemsAreSame({ featurePriceItemsAreSame({
item1: FeaturePriceItemSchema.parse(item1), item1: FeaturePriceItemSchema.parse(item1),

View File

@@ -76,6 +76,7 @@ export const toFeaturePriceItem = ({
ent.feature.config?.usage_type || ProductItemFeatureType.SingleUse, ent.feature.config?.usage_type || ProductItemFeatureType.SingleUse,
included_usage: ent.allowance, included_usage: ent.allowance,
interval: billingToItemInterval(config.interval!), interval: billingToItemInterval(config.interval!),
price: null, price: null,

View File

@@ -91,7 +91,7 @@ export const getFeatureInvoiceDescription = ({
}) => { }) => {
const { singular, plural } = getSingularAndPlural({ feature }); const { singular, plural } = getSingularAndPlural({ feature });
const usageStr = numberWithCommas(usage); const usageStr = numberWithCommas(Math.ceil(usage));
let result = ""; let result = "";

View File

@@ -21,11 +21,10 @@ import { AutumnProvider } from "autumn-js/react";
import { useAuth } from "@clerk/clerk-react"; import { useAuth } from "@clerk/clerk-react";
export function MainLayout() { export function MainLayout() {
const env = useEnv();
const { isLoaded: isUserLoaded, user } = useUser(); const { isLoaded: isUserLoaded, user } = useUser();
const { organization: org } = useOrganization(); const { organization: org } = useOrganization();
const { setActive } = useOrganizationList();
const { getToken } = useAuth(); const { getToken } = useAuth();
const env = useEnv();
const { pathname } = useLocation(); const { pathname } = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -85,13 +84,6 @@ export function MainLayout() {
return <RedirectToSignIn />; return <RedirectToSignIn />;
} }
// // 1. If not org, and memberships > 0, set org active
// if (!org && user.organizationMemberships.length > 0 && setActive) {
// setActive({
// organization: user.organizationMemberships[0].organization.id,
// });
// }
if (!org && !pathname.includes("/onboarding")) { if (!org && !pathname.includes("/onboarding")) {
return ( return (
<Navigate <Navigate