ran tests, updated new product preview and upgrade preview to just message, returning options in no product preview
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -70,3 +70,4 @@ export const ProductItemSchema = z.object({
|
||||
});
|
||||
|
||||
export type ProductItem = z.infer<typeof ProductItemSchema>;
|
||||
export type PriceTier = z.infer<typeof PriceTierSchema>;
|
||||
|
||||
@@ -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>;
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user