ran tests, updated new product preview and upgrade preview to just message, returning options in no product preview

This commit is contained in:
John Yeo
2025-05-01 19:59:43 +01:00
parent e556ead6cd
commit 0eb5afa906
10 changed files with 164 additions and 149 deletions

View File

@@ -309,6 +309,8 @@ entitledRouter.post("", async (req: any, res: any) => {
entity_id,
} = req.body;
const { logtail: logger } = req;
if (!customer_id) {
throw new RecaseError({
message: "Customer ID is required",
@@ -408,16 +410,21 @@ entitledRouter.post("", async (req: any, res: any) => {
// 3. If with preview, get preview
let preview = undefined;
if (req.body.with_preview) {
let featureToUse = creditSystems.length > 0 ? creditSystems[0] : feature;
preview = await getCheckPreview({
allowed,
balance: balances.find(
(balance: any) => balance.feature_id === featureToUse.id
)?.balance,
feature: featureToUse,
sb,
cusProducts,
});
try {
let featureToUse =
creditSystems.length > 0 ? creditSystems[0] : feature;
preview = await getCheckPreview({
allowed,
balance: balances.find(
(balance: any) => balance.feature_id === featureToUse.id
)?.balance,
feature: featureToUse,
sb,
cusProducts,
});
} catch (error) {
logger.error("Failed to get check preview", error);
}
}
if (org.api_version == APIVersion.v1_1) {

View File

@@ -20,7 +20,6 @@ import {
APIVersion,
} from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import { StatusCodes } from "http-status-codes";
import Stripe from "stripe";
import { createFullCusProduct } from "../add-product/createFullCusProduct.js";

View File

@@ -6,14 +6,53 @@ import {
isFeatureItem,
isPriceItem,
} from "@/internal/products/product-items/getItemType.js";
import { itemToPriceOrTiers } from "@/internal/products/product-items/productItemUtils.js";
import {
Feature,
FullProduct,
Organization,
ProductItem,
ProductV2,
} from "@autumn/shared";
import { Feature, Organization, ProductItem, ProductV2 } from "@autumn/shared";
import { formatCurrency, formatTiers } from "./previewUtils.js";
import { isFeaturePriceItem } from "@/internal/products/product-items/productItemUtils.js";
export const getProductChargeText = ({
product,
org,
features,
}: {
product: ProductV2;
org: Organization;
features: Feature[];
}) => {
let basePrices = product.items.filter((i) => isPriceItem(i));
let total = basePrices.reduce((acc, curr) => acc + curr.price!, 0);
let itemStrs = [];
if (total > 0) {
itemStrs.push(
formatCurrency({
amount: total,
defaultCurrency: org.default_currency,
})
);
}
let prepaidPrices = product.items.filter(
(i) => isFeaturePriceItem(i) && i.usage_model == "prepaid"
);
let prepaidStrings = prepaidPrices.map((i) => {
let feature = features.find((f) => f.id === i.feature_id);
let priceStr = formatTiers({
tiers: i.tiers!,
org,
});
let featureStr =
i.billing_units && i.billing_units > 1
? `${i.billing_units} ${feature?.name}`
: feature?.name;
return `${priceStr} / ${featureStr}`;
});
return [...itemStrs, ...prepaidStrings];
};
export const getItemDescription = ({
item,
@@ -28,7 +67,6 @@ export const getItemDescription = ({
}) => {
let prices = product.items.filter((i) => !isFeatureItem(i));
// let { price, tiers } = itemToPriceOrTiers(item);
let priceStr = getPriceText({
item,
org,

View File

@@ -1,33 +1,20 @@
import { createStripeCli } from "@/external/stripe/utils.js";
import {
BillingType,
CheckProdItemSchema,
Feature,
FullCusProduct,
FullProduct,
Organization,
UsageModel,
} from "@autumn/shared";
import { AppEnv } from "@autumn/shared";
import { Customer } from "@autumn/shared";
import {
handleBillNowPrices,
handleOneOffPrices,
} from "../add-product/handleAddProduct.js";
import { formatCurrency, getItemsHtml, itemsToHtml } from "./previewUtils.js";
import {
mapToProductItems,
mapToProductV2,
} from "@/internal/products/productV2Utils.js";
import {
getBillingType,
getPriceEntitlement,
} from "@/internal/prices/priceUtils.js";
import { getEntRelatedPrice } from "@/internal/products/entitlements/entitlementUtils.js";
import { mapToProductV2 } from "@/internal/products/productV2Utils.js";
import { isOneOff } from "@/internal/products/productUtils.js";
import { itemToPriceOrTiers } from "@/internal/products/product-items/productItemUtils.js";
import { getItemDescription } from "./checkProductUtils.js";
import { isFeaturePriceItem } from "@/internal/products/product-items/productItemUtils.js";
import { getProductChargeText } from "./checkProductUtils.js";
import { isFeatureItem } from "@/internal/products/product-items/getItemType.js";
import { sortProductItems } from "@/internal/products/pricecn/pricecnUtils.js";
@@ -58,95 +45,39 @@ export const getNewProductPreview = async ({
});
let sortedItems = sortProductItems(productV2.items, features);
let items = sortedItems
.filter((i) => !isFeatureItem(i))
.map((item) => {
let items = sortedItems.filter((i) => !isFeatureItem(i));
let itemStrs = getProductChargeText({
product: productV2,
org,
features,
});
let message = `By clicking confirm, you will subscribe to ${product.name} and the following amount will be charged:\n`;
for (let item of itemStrs) {
message += `\n${item}`;
}
let title = "";
if (isOneOff(product.prices)) {
title = `Purchase ${product.name}`;
} else {
title = `Subscribe to ${product.name}`;
}
let options = items
.filter((i) => isFeaturePriceItem(i) && i.usage_model == UsageModel.Prepaid)
.map((i) => {
return {
description: getItemDescription({
item,
features,
product: productV2,
org,
}),
feature_id: i.feature_id,
feature_name: features.find((f) => f.id == i.feature_id)?.name,
billing_units: i.billing_units,
};
});
console.log("items", items);
throw new Error("Not implemented");
// if (isOneOff(product.prices)) {
// let invoiceItems = await handleOneOffPrices({
// sb: null,
// attachParams,
// req: {
// logtail: console,
// },
// res: null,
// fromRequest: false,
// shouldPreview: true,
// }) || [];
// for (let item of invoiceItems) {
// delete item.description;
// }
// throw new Error("Not implemented");
// // let items = res?.lines?.data.map((line: any) => {
// // return {
// // name: line.description,
// // amount: line.amount / 100,
// // currency: line.currency,
// // };
// // });
// } else {
// res = (await handleBillNowPrices({
// sb: null,
// attachParams,
// req: {
// logtail: console,
// },
// res: null,
// fromRequest: false,
// shouldPreview: true,
// })) as any;
// let items = res?.lines?.data.map((line: any) => {
// let price = product.prices.find(
// (p: any) => p.config.stripe_price_id === line.price.id
// );
// let tiers = (price?.config as any)?.usage_tiers;
// let entitlement;
// if (price) {
// entitlement = getPriceEntitlement(price, product.entitlements);
// }
// return {
// name: entitlement
// ? `${product.name} (${entitlement?.feature.name})`
// : `${product.name} (Base)`,
// // description: line.description,
// amount: line.amount / 100,
// currency: line.currency,
// tiers: tiers,
// };
// });
// }
// let html = `<p>By clicking confirm, you will subscribe to ${product.name} and the following amount will be charged immediately:</p>`;
// html += getItemsHtml({ items: items, org: org });
// ${formatCurrency({
// amount: totalAmount,
// defaultCurrency: items?.[0]?.currency,
// })}
let message = `By clicking confirm, you will subscribe to ${product.name} and the following amount will be charged immediately:`;
// console.log("items", items);
return {
title: `Upgrade to ${product.name}`,
title,
message,
line_items: items,
due_when: "immediately",
options,
};
};

View File

@@ -78,25 +78,28 @@ const formatMessage = ({
? "will be charged to your card immediately"
: "will be added to your next bill";
let html = `<p>By clicking confirm, you will upgrade your plan to ${product.name} and the following amount ${addString}.</p>`;
html += `<br/><ul>${itemsToHtml({ items: baseLineItems })}${itemsToHtml({
items: usageLineItems,
})}</ul><br/>`;
html += `<p><strong style="font-size: 1.1em;">Total: ${formatCurrency({
amount: totalAmount,
defaultCurrency: org.default_currency,
})}</strong></p>`;
let message = `By clicking confirm, you will upgrade your plan to ${
product.name
} and ${formatCurrency({
amount: totalAmount,
defaultCurrency: org.default_currency,
})} ${addString}.`;
})} ${addString}:\n`;
return { html, message };
for (let item of baseLineItems) {
message += `\n${item.description}: ${formatCurrency({
amount: item.amount,
defaultCurrency: org.default_currency,
})}`;
}
for (let item of usageLineItems) {
message += `\n${item.description}: ${formatCurrency({
amount: item.amount,
defaultCurrency: org.default_currency,
})}`;
}
return { message };
};
export const getUpgradePreview = async ({
@@ -180,7 +183,6 @@ export const getUpgradePreview = async ({
return {
title: `Upgrade to ${product.name}`,
message: formattedMessage.message,
html: formattedMessage.html,
amount_due: Number(totalAmount.toFixed(2)),
due_when: org.config.bill_upgrade_immediately
? "immediately"

View File

@@ -1,4 +1,4 @@
import { Organization } from "@autumn/shared";
import { Organization, PriceTier } from "@autumn/shared";
export const formatCurrency = ({
amount,
@@ -11,7 +11,35 @@ export const formatCurrency = ({
style: "currency",
currency: defaultCurrency || "usd",
});
return formatter.format(Math.abs(amount));
return formatter.format(amount);
};
export const formatTiers = ({
tiers,
org,
}: {
tiers: PriceTier[];
org: Organization;
}) => {
if (tiers.length == 1) {
return formatCurrency({
amount: tiers[0].amount,
defaultCurrency: org.default_currency,
});
}
let tiersStart = formatCurrency({
amount: tiers[0].amount,
defaultCurrency: org.default_currency,
});
let tiersEnd = formatCurrency({
amount: tiers[tiers.length - 1].amount,
defaultCurrency: org.default_currency,
});
return `${tiersStart} - ${tiersEnd}`;
};
export const getItemsHtml = ({

View File

@@ -3,7 +3,7 @@ import chalk from "chalk";
import { Autumn } from "@/external/autumn/autumnCli.js";
import { features } from "tests/global.js";
import { setupBefore } from "tests/before.js";
import { initCustomer } from "tests/utils/init.js";
import {
AppEnv,
BillingInterval,
@@ -24,12 +24,9 @@ import {
} from "@/internal/products/product-items/productItemUtils.js";
import { SupabaseClient } from "@supabase/supabase-js";
import { timeout } from "@/utils/genUtils.js";
import {
advanceClockForInvoice,
advanceTestClock,
} from "tests/utils/stripeUtils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js";
import { addDays } from "date-fns";
import { addDays, addMonths } from "date-fns";
// Scenario 1: prepaid + pay per use monthly -> prepaid + pay per use monthly
let pro = {
@@ -163,16 +160,13 @@ describe(`${chalk.yellowBright(
});
it("cycle 1:should have correct usage after first cycle", async function () {
let advanceTo = addDays(new Date(), 30).getTime();
let advanceTo = addMonths(new Date(), 1).getTime();
await advanceTestClock({
stripeCli: this.stripeCli,
testClockId,
advanceTo,
});
// let { invoices } = await autumn.customers.get(customerId);
// expect(invoices[0].total).to.equal(overageValue * (pro.items.payPerUse.amount ?? 0));
let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
customerId,
sb: this.sb,

View File

@@ -70,3 +70,4 @@ export const ProductItemSchema = z.object({
});
export type ProductItem = z.infer<typeof ProductItemSchema>;
export type PriceTier = z.infer<typeof PriceTierSchema>;

View File

@@ -0,0 +1,15 @@
import { z } from "zod";
import { UsageModel } from "../productItemModels.js";
export const CheckProdItemSchema = z.object({
feature_id: z.string().nullish(),
feature_name: z.string().nullish(),
usage_model: z.nativeEnum(UsageModel).nullish(),
description: z.string(),
price: z.number().nullish(),
billing_units: z.number().nullish(),
});
export type CheckProdItem = z.infer<typeof CheckProdItemSchema>;

View File

@@ -15,7 +15,7 @@ export enum BillWhen {
BelowThreshold = "below_threshold",
}
export const UsageTier = z.object({
export const UsageTierSchema = z.object({
// from: z.number(),
to: z.number().or(z.literal(TierInfinite)),
amount: z.number(),
@@ -30,7 +30,7 @@ export const UsagePriceConfigSchema = z.object({
internal_feature_id: z.string(),
feature_id: z.string(),
usage_tiers: z.array(UsageTier),
usage_tiers: z.array(UsageTierSchema),
interval: z.nativeEnum(BillingInterval).optional(),
// For usage in arrear