fix: returning trial available in getProductCheckPreview

This commit is contained in:
John Yeo
2025-06-26 13:57:36 +01:00
parent 98739d1277
commit cd905d266d
9 changed files with 148 additions and 88 deletions

View File

@@ -28,9 +28,9 @@ componentRouter.get("/pricing_table", async (req: any, res) =>
ProductService.listFull({ db, orgId, env }),
(async () => {
if (!customerId) {
return null;
return undefined;
}
return await CusService.get({
return await CusService.getFull({
db,
orgId,
env,
@@ -59,48 +59,66 @@ componentRouter.get("/pricing_table", async (req: any, res) =>
}
});
let cusProducts: FullCusProduct[] | null = null;
let batchResponse = [];
for (let p of products) {
let prod = getProductResponse({ product: p, features });
let curMainProduct, curScheduledProduct;
if (customer) {
cusProducts = await CusProductService.list({
db,
internalCustomerId: customer.internal_id,
inStatuses: [
CusProductStatus.Active,
CusProductStatus.PastDue,
CusProductStatus.Scheduled,
],
});
if (customer) {
let res = getExistingCusProducts({
product: p,
cusProducts: customer.customer_products,
});
curMainProduct = res.curMainProduct;
curScheduledProduct = res.curScheduledProduct;
}
batchResponse.push(
toPricecnProduct({
db,
org,
product: prod as ProductV2,
fullProduct: p,
features,
curMainProduct,
curScheduledProduct,
otherProducts: products.filter((other) => other.id != p.id),
fullCus: customer,
}),
);
}
let pricecnProds = await Promise.all(
products
// .filter((p) => !p.is_add_on)
.map(async (p) => {
let prod = getProductResponse({ product: p, features });
let curMainProduct, curScheduledProduct;
let pricecnProds = await Promise.all(batchResponse);
if (cusProducts) {
let res = getExistingCusProducts({
product: p,
cusProducts: cusProducts,
});
// let pricecnProds = await Promise.all(
// products
// // .filter((p) => !p.is_add_on)
// .map(async (p) => {
// let prod = getProductResponse({ product: p, features });
// let curMainProduct, curScheduledProduct;
curMainProduct = res.curMainProduct;
curScheduledProduct = res.curScheduledProduct;
}
// if (cusProducts) {
// let res = getExistingCusProducts({
// product: p,
// cusProducts: cusProducts,
// });
return toPricecnProduct({
org,
product: prod as ProductV2,
fullProduct: p,
features,
curMainProduct,
curScheduledProduct,
otherProducts: products.filter((other) => other.id != p.id),
});
}),
);
// curMainProduct = res.curMainProduct;
// curScheduledProduct = res.curScheduledProduct;
// }
// return toPricecnProduct({
// org,
// product: prod as ProductV2,
// fullProduct: p,
// features,
// curMainProduct,
// curScheduledProduct,
// otherProducts: products.filter((other) => other.id != p.id),
// });
// }),
// );
res.status(200).json({
list: pricecnProds,

View File

@@ -18,6 +18,7 @@ import { isOneOff } from "@/internal/products/productUtils.js";
import { formatAmount } from "@/utils/formatUtils.js";
import { Decimal } from "decimal.js";
import { notNullish } from "@/utils/genUtils.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
const getNextCycle = (preview: AttachPreview) => {
if (!preview.due_next_cycle && !preview.due_today) {
@@ -29,11 +30,13 @@ const getNextCycle = (preview: AttachPreview) => {
};
export const attachToCheckPreview = async ({
preview,
params,
product,
org,
features,
}: {
preview: AttachPreview;
params: AttachParams;
product: FullProduct;
org: Organization;
features: Feature[];
@@ -112,7 +115,11 @@ export const attachToCheckPreview = async ({
items,
due_today,
due_next_cycle,
product: getProductResponse({ product, features }),
product: getProductResponse({
product,
features,
trialAvailable: notNullish(params.freeTrial) ? true : false,
}),
};
return checkPreview;
};
@@ -153,6 +160,7 @@ export const getProductCheckPreview = async ({
const checkPreview = await attachToCheckPreview({
preview,
params: attachParams,
product,
org,
features,

View File

@@ -84,16 +84,6 @@ export const handleProductCheck = async ({
// })
// : undefined;
if (preview) {
preview = {
...preview,
product: getProductResponse({
product: product!,
features,
}),
};
}
if (!cusProduct) {
res.status(200).json({
customer_id,

View File

@@ -14,6 +14,7 @@ import { AttachConfig } from "@autumn/shared";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js";
import { Decimal } from "decimal.js";
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
export const handleOneOffFunction = async ({
req,
@@ -56,32 +57,46 @@ export const handleOneOffFunction = async ({
.toNumber();
}
const amount = priceToInvoiceAmount({
price,
quantity,
});
let invoiceItemData = {};
if (isFixedPrice({ price })) {
quantity = 1;
const product = priceToProduct({
price,
products,
});
invoiceItemData = {
price: price.config.stripe_price_id,
quantity: 1,
};
} else {
const amount = priceToInvoiceAmount({
price,
quantity,
});
const description = newPriceToInvoiceDescription({
org,
price,
product: product!,
ents: entitlements,
quantity: options?.quantity,
withProductPrefix: true,
});
const product = priceToProduct({
price,
products,
});
const description = newPriceToInvoiceDescription({
org,
price,
product: product!,
ents: entitlements,
quantity: options?.quantity,
withProductPrefix: true,
});
invoiceItemData = {
description,
price_data: {
unit_amount: new Decimal(amount).mul(100).round().toNumber(),
currency: org.default_currency,
product: price.config?.stripe_product_id || product?.processor?.id!,
},
};
}
invoiceItems.push({
description,
price_data: {
unit_amount: new Decimal(amount).mul(100).round().toNumber(),
currency: org.default_currency,
product: price.config?.stripe_product_id || product?.processor?.id!,
},
...invoiceItemData,
quantity: 1,
});
}

View File

@@ -5,18 +5,8 @@ import {
CreateFreeTrialSchema,
FreeTrial,
FreeTrialDuration,
FullProduct,
} from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import {
addDays,
addMinutes,
addMonths,
addSeconds,
addWeeks,
addYears,
getTime,
} from "date-fns";
import { addDays, addMinutes, addMonths, addYears } from "date-fns";
import { FreeTrialService } from "./FreeTrialService.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";

View File

@@ -15,12 +15,16 @@ import {
numberWithCommas,
AttachScenario,
FullProduct,
FullCustomer,
FreeTrialResponseSchema,
} from "@autumn/shared";
import { isPriceItem } from "../product-items/productItemUtils/getItemType.js";
import { isFeaturePriceItem } from "../product-items/productItemUtils/getItemType.js";
import { cusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { isProductUpgrade } from "../productUtils.js";
import { getFirstInterval } from "../prices/priceUtils/priceIntervalUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getFreeTrialAfterFingerprint } from "../free-trials/freeTrialUtils.js";
export const sortProductItems = (items: ProductItem[], features: Feature[]) => {
items.sort((a, b) => {
@@ -287,7 +291,8 @@ export const getAttachScenario = ({
return isUpgrade ? AttachScenario.Upgrade : AttachScenario.Downgrade;
};
export const toPricecnProduct = ({
export const toPricecnProduct = async ({
db,
org,
product,
fullProduct,
@@ -295,7 +300,9 @@ export const toPricecnProduct = ({
features,
curMainProduct,
curScheduledProduct,
fullCus,
}: {
db: DrizzleCli;
org: Organization;
product: ProductV2;
fullProduct: FullProduct;
@@ -303,6 +310,7 @@ export const toPricecnProduct = ({
features: Feature[];
curMainProduct?: FullCusProduct | null;
curScheduledProduct?: FullCusProduct | null;
fullCus?: FullCustomer;
}) => {
let items = structuredClone(product.items);
@@ -389,6 +397,21 @@ export const toPricecnProduct = ({
intervalGroup = getFirstInterval({ prices: fullProduct.prices });
}
let trialAvailable = false;
if (product.free_trial && fullCus) {
let trial = await getFreeTrialAfterFingerprint({
db,
freeTrial: product.free_trial,
fingerprint: fullCus.fingerprint,
internalCustomerId: fullCus.internal_id,
multipleAllowed: org.config.multiple_trials,
productId: product.id,
});
if (scenario == AttachScenario.Downgrade) trial = null;
trialAvailable = notNullish(trial) ? true : false;
}
return {
id: product.id,
name,
@@ -406,10 +429,10 @@ export const toPricecnProduct = ({
scenario,
button_text: buttonText,
free_trial: freeTrial
? {
length: freeTrial,
interval: freeTrial.duration,
}
? FreeTrialResponseSchema.parse({
...freeTrial,
trial_available: trialAvailable,
})
: null,
interval_group: intervalGroup,

View File

@@ -59,7 +59,10 @@ export const detectBaseVariant = async ({
nullish(p.base_variant_id) &&
!p.is_add_on &&
p.prices.length > 0 &&
p.prices.every((price) => price.config.interval == BillingInterval.Month),
p.prices.every(
(price) => price.config.interval == BillingInterval.Month,
) &&
p.group == curProduct.group,
);
if (filteredExistingProducts.length == 0) return null;

View File

@@ -2,6 +2,8 @@ import {
EntitlementWithFeature,
Feature,
FeatureType,
FreeTrial,
FreeTrialResponseSchema,
FullProduct,
Price,
ProductItem,
@@ -109,9 +111,11 @@ export const mapToProductV2 = ({
export const getProductResponse = ({
product,
features,
trialAvailable,
}: {
product: FullProduct;
features: Feature[];
trialAvailable?: boolean;
}) => {
let items = mapToProductItems({
prices: product.prices,
@@ -134,5 +138,13 @@ export const getProductResponse = ({
name: product.name || null,
group: product.group || null,
items: items,
free_trial: product.free_trial
? FreeTrialResponseSchema.parse({
duration: product.free_trial?.duration,
length: product.free_trial?.length,
unique_fingerprint: product.free_trial?.unique_fingerprint,
trial_available: trialAvailable,
})
: null,
});
};

View File

@@ -26,6 +26,7 @@ export const FreeTrialResponseSchema = z.object({
duration: z.nativeEnum(FreeTrialDuration),
length: z.number(),
unique_fingerprint: z.boolean(),
trial_available: z.boolean().nullish().default(true),
});
export type FreeTrial = z.infer<typeof FreeTrialSchema>;