feat: checkout endpoint
This commit is contained in:
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -410,6 +410,9 @@ importers:
|
||||
ag-charts-community:
|
||||
specifier: ^12.0.2
|
||||
version: 12.0.2
|
||||
ag-grid-community:
|
||||
specifier: ^34.0.2
|
||||
version: 34.0.2
|
||||
ag-grid-react:
|
||||
specifier: ^34.0.2
|
||||
version: 34.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
} from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
import assert from "assert";
|
||||
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import {
|
||||
cusProductsToCusEnts,
|
||||
cusProductToPrices,
|
||||
} from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import {
|
||||
findStripeItemForPrice,
|
||||
isLicenseItem,
|
||||
@@ -30,6 +33,7 @@ import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { createSupabaseClient } from "@/external/supabaseUtils.js";
|
||||
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
|
||||
import { getRelatedCusPrice } from "./internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
|
||||
const { db, client } = initDrizzle({ maxConnections: 5 });
|
||||
let orgSlugs = process.env.ORG_SLUGS!.split(",");
|
||||
@@ -183,12 +187,20 @@ const checkCustomerCorrect = async ({
|
||||
}
|
||||
}
|
||||
|
||||
// console.log("prices:", prices);
|
||||
// console.log("subItems:", subItems);
|
||||
assert(
|
||||
prices.length - missingUsageCount === subItems.length,
|
||||
`(${cusProduct.product.name}) number of sub items equivalent to number of prices`
|
||||
);
|
||||
|
||||
for (const cusEnt of cusProduct.customer_entitlements) {
|
||||
let cusPrice = getRelatedCusPrice(cusEnt, cusProduct.customer_prices);
|
||||
|
||||
if (cusEnt.usage_allowed && !cusPrice) {
|
||||
assert.fail(
|
||||
`Feature ${cusEnt.feature_id} has usage allowed but no related cus price`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Other checks to perform
|
||||
|
||||
@@ -72,11 +72,11 @@ const checkSubAnchor = async ({
|
||||
console.log("Checking billing cycle anchor");
|
||||
console.log(
|
||||
"Next reset at ",
|
||||
format(new UTCDate(nextResetAt), "dd MMM yyyy HH:mm:ss"),
|
||||
format(new UTCDate(nextResetAt), "dd MMM yyyy HH:mm:ss")
|
||||
);
|
||||
console.log(
|
||||
"Billing cycle anchor",
|
||||
format(new UTCDate(billingCycleAnchor), "dd MMM yyyy HH:mm:ss"),
|
||||
format(new UTCDate(billingCycleAnchor), "dd MMM yyyy HH:mm:ss")
|
||||
);
|
||||
|
||||
const billingCycleDay = getDate(new UTCDate(billingCycleAnchor));
|
||||
@@ -116,7 +116,7 @@ const resetCustomerEntitlement = async ({
|
||||
|
||||
const entOptions = getEntOptions(
|
||||
cusEnt.customer_product.options,
|
||||
cusEnt.entitlement,
|
||||
cusEnt.entitlement
|
||||
);
|
||||
|
||||
const resetBalance = getResetBalance({
|
||||
@@ -140,10 +140,10 @@ const resetCustomerEntitlement = async ({
|
||||
|
||||
console.log(
|
||||
`Reset ${cusEnt.id} | customer: ${chalk.yellow(
|
||||
cusEnt.customer_id,
|
||||
cusEnt.customer_id
|
||||
)} | feature: ${chalk.yellow(
|
||||
cusEnt.feature_id,
|
||||
)} | new balance: unlimited`,
|
||||
cusEnt.feature_id
|
||||
)} | new balance: unlimited`
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -159,16 +159,16 @@ const resetCustomerEntitlement = async ({
|
||||
|
||||
console.log(
|
||||
`Reset ${cusEnt.id} | customer: ${chalk.yellow(
|
||||
cusEnt.customer_id,
|
||||
cusEnt.customer_id
|
||||
)} | feature: ${chalk.yellow(
|
||||
cusEnt.feature_id,
|
||||
)} | reset to lifetime (next_reset_at: null)`,
|
||||
cusEnt.feature_id
|
||||
)} | reset to lifetime (next_reset_at: null)`
|
||||
);
|
||||
return;
|
||||
}
|
||||
let nextResetAt = getNextResetAt(
|
||||
new UTCDate(cusEnt.next_reset_at!),
|
||||
cusEnt.entitlement.interval as EntInterval,
|
||||
cusEnt.entitlement.interval as EntInterval
|
||||
);
|
||||
|
||||
let resetBalanceUpdate = getResetBalancesUpdate({
|
||||
@@ -199,18 +199,18 @@ const resetCustomerEntitlement = async ({
|
||||
|
||||
console.log(
|
||||
`Reset ${cusEnt.id} | customer: ${chalk.yellow(
|
||||
cusEnt.customer_id,
|
||||
cusEnt.customer_id
|
||||
)} | feature: ${chalk.yellow(
|
||||
cusEnt.feature_id,
|
||||
cusEnt.feature_id
|
||||
)} | new balance: ${chalk.green(
|
||||
resetBalance,
|
||||
resetBalance
|
||||
)} | new next_reset_at: ${chalk.green(
|
||||
format(new UTCDate(nextResetAt), "dd MMM yyyy HH:mm:ss"),
|
||||
)}`,
|
||||
format(new UTCDate(nextResetAt), "dd MMM yyyy HH:mm:ss")
|
||||
)}`
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.log(
|
||||
`Failed to reset ${cusEnt.id} | ${cusEnt.customer_id} | ${cusEnt.feature_id}, error: ${error}`,
|
||||
`Failed to reset ${cusEnt.id} | ${cusEnt.customer_id} | ${cusEnt.feature_id}, error: ${error}`
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -218,16 +218,16 @@ const resetCustomerEntitlement = async ({
|
||||
export const cronTask = async () => {
|
||||
console.log(
|
||||
"\n----------------------------------\nRUNNING RESET CRON:",
|
||||
format(new UTCDate(), "yyyy-MM-dd HH:mm:ss"),
|
||||
format(new UTCDate(), "yyyy-MM-dd HH:mm:ss")
|
||||
);
|
||||
|
||||
const { db, client } = initDrizzle();
|
||||
|
||||
try {
|
||||
let cusEnts: FullCusEntWithProduct[] =
|
||||
await CusEntService.getActiveResetPassed({ db });
|
||||
await CusEntService.getActiveResetPassed({ db, batchSize: 500 });
|
||||
|
||||
const batchSize = 20;
|
||||
const batchSize = 50;
|
||||
for (let i = 0; i < cusEnts.length; i += batchSize) {
|
||||
const batch = cusEnts.slice(i, i + batchSize);
|
||||
const batchResets = [];
|
||||
@@ -236,7 +236,7 @@ export const cronTask = async () => {
|
||||
resetCustomerEntitlement({
|
||||
db,
|
||||
cusEnt: cusEnt as FullCusEntWithProduct,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ export const cronTask = async () => {
|
||||
|
||||
console.log(
|
||||
"FINISHED RESET CRON:",
|
||||
format(new UTCDate(), "yyyy-MM-dd HH:mm:ss"),
|
||||
format(new UTCDate(), "yyyy-MM-dd HH:mm:ss")
|
||||
);
|
||||
console.log("----------------------------------\n");
|
||||
} catch (error) {
|
||||
@@ -263,7 +263,7 @@ const job = new CronJob(
|
||||
},
|
||||
null, // onComplete
|
||||
true, // start immediately
|
||||
"UTC", // timezone (adjust as needed)
|
||||
"UTC" // timezone (adjust as needed)
|
||||
);
|
||||
|
||||
// job.start();
|
||||
|
||||
2
server/src/external/stripe/stripeWebhooks.ts
vendored
2
server/src/external/stripe/stripeWebhooks.ts
vendored
@@ -68,8 +68,8 @@ stripeWebhookRouter.post(
|
||||
return;
|
||||
}
|
||||
|
||||
const webhookSecret = getStripeWebhookSecret(org, env);
|
||||
try {
|
||||
const webhookSecret = getStripeWebhookSecret(org, env);
|
||||
event = stripe.webhooks.constructEvent(request.body, sig, webhookSecret);
|
||||
} catch (err: any) {
|
||||
response.status(400).send(`Webhook Error: ${err.message}`);
|
||||
|
||||
@@ -68,7 +68,12 @@ export const mapOptionsList = ({
|
||||
newOption.internal_feature_id === option.internal_feature_id
|
||||
);
|
||||
|
||||
if (!inNewOptions) {
|
||||
const prepaidPriceExists = findPrepaidPrice({
|
||||
prices,
|
||||
internalFeatureId: option.internal_feature_id!,
|
||||
});
|
||||
|
||||
if (!inNewOptions && prepaidPriceExists) {
|
||||
newOptionsList.push(option);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import { AttachBranch } from "@autumn/shared";
|
||||
import {
|
||||
attachParamsToCurCusProduct,
|
||||
attachParamToCusProducts,
|
||||
} from "../attachUtils/convertAttachParams.js";
|
||||
import { cusProductToPrices } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import { isFreeProduct } from "@/internal/products/productUtils.js";
|
||||
|
||||
export const getHasProrations = async ({
|
||||
req,
|
||||
branch,
|
||||
attachParams,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
branch: AttachBranch;
|
||||
attachParams: AttachParams;
|
||||
}) => {
|
||||
let hasProrations = false;
|
||||
|
||||
let { curMainProduct } = attachParamToCusProducts({ attachParams });
|
||||
if (branch == AttachBranch.Upgrade) {
|
||||
let curPrices = cusProductToPrices({ cusProduct: curMainProduct! });
|
||||
|
||||
if (!isFreeProduct(curPrices)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (branch == AttachBranch.UpdatePrepaidQuantity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -1,12 +1,11 @@
|
||||
import {
|
||||
AttachBranch,
|
||||
AttachFunction,
|
||||
AttachScenario,
|
||||
CheckoutResponseSchema,
|
||||
FeatureOptions,
|
||||
FreeTrialResponseSchema,
|
||||
ProductItemResponseSchema,
|
||||
ProductResponseSchema,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { getAttachParams } from "../attachUtils/attachParams/getAttachParams.js";
|
||||
import { AttachBody, AttachBodySchema } from "@autumn/shared";
|
||||
@@ -16,7 +15,6 @@ import { getAttachBranch } from "../attachUtils/getAttachBranch.js";
|
||||
import { getAttachConfig } from "../attachUtils/getAttachConfig.js";
|
||||
import { getAttachFunction } from "../attachUtils/getAttachFunction.js";
|
||||
import { handleCreateCheckout } from "../../add-product/handleCreateCheckout.js";
|
||||
import { z } from "zod";
|
||||
import { checkStripeConnections } from "../attachRouter.js";
|
||||
import { attachParamsToPreview } from "../handleAttachPreview/attachParamsToPreview.js";
|
||||
import { previewToCheckoutRes } from "./previewToCheckoutRes.js";
|
||||
@@ -26,6 +24,7 @@ import { attachParamsToProduct } from "../attachUtils/convertAttachParams.js";
|
||||
import { isPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
import { priceToFeature } from "@/internal/products/prices/priceUtils/convertPrice.js";
|
||||
import { getPriceOptions } from "@/internal/products/prices/priceUtils.js";
|
||||
import { getHasProrations } from "./getHasProrations.js";
|
||||
|
||||
const getAttachVars = async ({
|
||||
req,
|
||||
@@ -167,12 +166,20 @@ export const handleCheckout = (req: any, res: any) =>
|
||||
preview,
|
||||
});
|
||||
|
||||
// Get has prorations
|
||||
const hasProrations = await getHasProrations({
|
||||
req,
|
||||
branch,
|
||||
attachParams,
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
...checkoutRes,
|
||||
options: attachParams.optionsList.map((o) => ({
|
||||
quantity: o.quantity,
|
||||
feature_id: o.feature_id,
|
||||
})),
|
||||
has_prorations: hasProrations,
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
@@ -2,12 +2,11 @@ import {
|
||||
AttachPreview,
|
||||
CheckoutLine,
|
||||
CheckoutResponseSchema,
|
||||
UsageModel,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import { getAttachScenario } from "@/internal/api/entitled/handlers/attachToCheckPreview/getAttachScenario.js";
|
||||
import {
|
||||
attachParamsToCurCusProduct,
|
||||
attachParamsToProduct,
|
||||
attachParamToCusProducts,
|
||||
} from "../attachUtils/convertAttachParams.js";
|
||||
@@ -23,7 +22,9 @@ import {
|
||||
} from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js";
|
||||
import { toProductItem } from "@/internal/products/product-items/mapToItem.js";
|
||||
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { formatUnixToDateTime, notNullish } from "@/utils/genUtils.js";
|
||||
import { isPriceItem } from "@/internal/products/product-items/productItemUtils/getItemType.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
|
||||
export const previewToCheckoutRes = async ({
|
||||
req,
|
||||
@@ -36,7 +37,6 @@ export const previewToCheckoutRes = async ({
|
||||
}) => {
|
||||
const { logger, features, org } = req;
|
||||
const product = attachParamsToProduct({ attachParams });
|
||||
const scenario = await getAttachScenario({ preview, product });
|
||||
|
||||
const { curCusProduct } = attachParamToCusProducts({ attachParams });
|
||||
let curPrices = curCusProduct
|
||||
@@ -83,6 +83,7 @@ export const previewToCheckoutRes = async ({
|
||||
features,
|
||||
currency: org.default_currency,
|
||||
options: attachParams.optionsList,
|
||||
fullCus: attachParams.customer,
|
||||
});
|
||||
|
||||
let curProduct = curCusProduct
|
||||
@@ -96,17 +97,46 @@ export const previewToCheckoutRes = async ({
|
||||
|
||||
const total = lines.reduce((acc, line) => acc + line.amount, 0);
|
||||
|
||||
let nextCycle = undefined;
|
||||
|
||||
if (notNullish(preview.due_next_cycle)) {
|
||||
nextCycle = {
|
||||
starts_at: preview.due_next_cycle.due_at,
|
||||
total: newProduct.items
|
||||
.reduce((acc, item) => {
|
||||
if (item.usage_model == UsageModel.PayPerUse) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// if (item.interval !== newProduct.properties?.interval_group) {
|
||||
// return acc;
|
||||
// }
|
||||
|
||||
if (isPriceItem(item)) {
|
||||
return acc.plus(item.price || 0);
|
||||
}
|
||||
|
||||
let prepaidQuantity =
|
||||
attachParams.optionsList.find(
|
||||
(o) => o.feature_id == item.feature_id
|
||||
)?.quantity || 0;
|
||||
|
||||
return acc.plus(prepaidQuantity * (item.price || 0));
|
||||
}, new Decimal(0))
|
||||
.toNumber(),
|
||||
};
|
||||
}
|
||||
|
||||
return CheckoutResponseSchema.parse({
|
||||
customer_id: attachParams.customer.id,
|
||||
scenario,
|
||||
lines,
|
||||
product: newProduct,
|
||||
current_product: curProduct,
|
||||
total,
|
||||
currency: org.default_currency || "usd",
|
||||
// next_cycle: nextCycle,
|
||||
next_cycle_at: notNullish(preview.due_next_cycle)
|
||||
? preview.due_next_cycle.due_at
|
||||
: null,
|
||||
next_cycle: nextCycle,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -20,10 +20,10 @@ export const getDowngradeProductPreview = async ({
|
||||
}) => {
|
||||
const newProduct = attachParamsToProduct({ attachParams });
|
||||
|
||||
const { curMainProduct } = attachParamToCusProducts({ attachParams });
|
||||
const { curCusProduct } = attachParamToCusProducts({ attachParams });
|
||||
const stripeSubs = await getStripeSubs({
|
||||
stripeCli: attachParams.stripeCli,
|
||||
subIds: curMainProduct?.subscription_ids || [],
|
||||
subIds: curCusProduct?.subscription_ids || [],
|
||||
});
|
||||
|
||||
const anchorToUnix = stripeSubs[0].current_period_end * 1000;
|
||||
@@ -47,11 +47,15 @@ export const getDowngradeProductPreview = async ({
|
||||
// anchorToUnix,
|
||||
});
|
||||
|
||||
let nextCycleAt = curCusProduct?.trial_ends_at
|
||||
? curCusProduct.trial_ends_at
|
||||
: anchorToUnix;
|
||||
|
||||
return {
|
||||
currency: attachParams.org.default_currency,
|
||||
due_next_cycle: {
|
||||
line_items: items,
|
||||
due_at: anchorToUnix,
|
||||
due_at: nextCycleAt,
|
||||
},
|
||||
|
||||
options,
|
||||
|
||||
@@ -13,7 +13,12 @@ import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/free
|
||||
import { getLastInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
|
||||
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
|
||||
import { getMergeCusProduct } from "../attachFunctions/addProductFlow/getMergeCusProduct.js";
|
||||
import { formatUnixToDate, notNullish, nullish } from "@/utils/genUtils.js";
|
||||
import {
|
||||
formatUnixToDate,
|
||||
formatUnixToDateTime,
|
||||
notNullish,
|
||||
nullish,
|
||||
} from "@/utils/genUtils.js";
|
||||
|
||||
export const getNewProductPreview = async ({
|
||||
branch,
|
||||
|
||||
@@ -30,7 +30,7 @@ import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/free
|
||||
import { Decimal } from "decimal.js";
|
||||
import { intervalsAreSame } from "../attachUtils/getAttachConfig.js";
|
||||
import { isFreeProduct } from "@/internal/products/productUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { formatUnixToDateTime, notNullish } from "@/utils/genUtils.js";
|
||||
|
||||
const getNextCycleAt = ({
|
||||
prices,
|
||||
@@ -60,17 +60,18 @@ const getNextCycleAt = ({
|
||||
}
|
||||
|
||||
if (willCycleReset) {
|
||||
const minInterval = getLastInterval({ prices });
|
||||
const firstInterval = getFirstInterval({ prices });
|
||||
return {
|
||||
next_cycle_at: addBillingIntervalUnix(now, minInterval),
|
||||
next_cycle_at: addBillingIntervalUnix(now, firstInterval),
|
||||
};
|
||||
}
|
||||
|
||||
const minInterval = getLastInterval({ prices });
|
||||
const firstInterval = getFirstInterval({ prices });
|
||||
const nextCycleAt = getAlignedIntervalUnix({
|
||||
alignWithUnix: stripeSubs[0].current_period_end * 1000,
|
||||
interval: minInterval,
|
||||
interval: firstInterval,
|
||||
alwaysReturn: true,
|
||||
now,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -52,41 +52,62 @@ export class CusEntService {
|
||||
static async getActiveResetPassed({
|
||||
db,
|
||||
customDateUnix,
|
||||
batchSize = 1000,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
customDateUnix?: number;
|
||||
batchSize?: number;
|
||||
}) {
|
||||
const data = await db
|
||||
.select()
|
||||
.from(customerEntitlements)
|
||||
.innerJoin(
|
||||
customerProducts,
|
||||
eq(customerEntitlements.customer_product_id, customerProducts.id),
|
||||
)
|
||||
.innerJoin(
|
||||
entitlements,
|
||||
eq(customerEntitlements.entitlement_id, entitlements.id),
|
||||
)
|
||||
.innerJoin(
|
||||
features,
|
||||
eq(entitlements.internal_feature_id, features.internal_id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(customerProducts.status, CusProductStatus.Active),
|
||||
lt(customerEntitlements.next_reset_at, customDateUnix ?? Date.now()),
|
||||
),
|
||||
);
|
||||
const allResults: FullCusEntWithProduct[] = [];
|
||||
let offset = 0;
|
||||
let hasMore = true;
|
||||
|
||||
return data.map((item) => ({
|
||||
...item.customer_entitlements,
|
||||
entitlement: {
|
||||
...item.entitlements,
|
||||
feature: item.features,
|
||||
},
|
||||
customer_product: item.customer_products,
|
||||
replaceables: [],
|
||||
})) as FullCusEntWithProduct[];
|
||||
while (hasMore) {
|
||||
const data = await db
|
||||
.select()
|
||||
.from(customerEntitlements)
|
||||
.innerJoin(
|
||||
customerProducts,
|
||||
eq(customerEntitlements.customer_product_id, customerProducts.id)
|
||||
)
|
||||
.innerJoin(
|
||||
entitlements,
|
||||
eq(customerEntitlements.entitlement_id, entitlements.id)
|
||||
)
|
||||
.innerJoin(
|
||||
features,
|
||||
eq(entitlements.internal_feature_id, features.internal_id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(customerProducts.status, CusProductStatus.Active),
|
||||
lt(customerEntitlements.next_reset_at, customDateUnix ?? Date.now())
|
||||
)
|
||||
)
|
||||
.limit(batchSize)
|
||||
.offset(offset);
|
||||
|
||||
if (data.length === 0) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
const mappedData = data.map((item) => ({
|
||||
...item.customer_entitlements,
|
||||
entitlement: {
|
||||
...item.entitlements,
|
||||
feature: item.features,
|
||||
},
|
||||
customer_product: item.customer_products,
|
||||
replaceables: [],
|
||||
})) as FullCusEntWithProduct[];
|
||||
|
||||
allResults.push(...mappedData);
|
||||
offset += batchSize;
|
||||
hasMore = data.length === batchSize;
|
||||
console.log(`Fetched ${allResults.length} entitlements to reset`);
|
||||
}
|
||||
}
|
||||
|
||||
return allResults;
|
||||
}
|
||||
|
||||
static async update({
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
FreeTrial,
|
||||
PreviewLineItem,
|
||||
BillingType,
|
||||
AttachConfig,
|
||||
getFeatureInvoiceDescription,
|
||||
UsagePriceConfig,
|
||||
UsageModel,
|
||||
@@ -232,7 +231,7 @@ export const getItemsForNewProduct = async ({
|
||||
billingUnits: (price.config as UsagePriceConfig).billing_units,
|
||||
prodName: newProduct.name,
|
||||
isPrepaid: true,
|
||||
fromUnix: finalProration?.start,
|
||||
fromUnix: now,
|
||||
}),
|
||||
amount,
|
||||
usage_model: UsageModel.Prepaid,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { cusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import { AttachScenario, FullCustomer, FullProduct } from "@autumn/shared";
|
||||
import { isFreeProduct, isProductUpgrade } from "../../productUtils.js";
|
||||
import {
|
||||
isFreeProduct,
|
||||
isOneOff,
|
||||
isProductUpgrade,
|
||||
} from "../../productUtils.js";
|
||||
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
|
||||
|
||||
export const getAttachScenario = ({
|
||||
@@ -19,6 +23,10 @@ export const getAttachScenario = ({
|
||||
|
||||
if (!curMainProduct || fullProduct.is_add_on) return AttachScenario.New;
|
||||
|
||||
if (isOneOff(fullProduct.prices)) {
|
||||
return AttachScenario.New;
|
||||
}
|
||||
|
||||
// 1. If current product is the same as the product, return active
|
||||
if (curMainProduct?.product.id == fullProduct.id) {
|
||||
if (curMainProduct.canceled_at != null) {
|
||||
|
||||
@@ -6,18 +6,17 @@ import {
|
||||
FreeTrialResponseSchema,
|
||||
FullCustomer,
|
||||
ProductItem,
|
||||
Organization,
|
||||
AttachScenario,
|
||||
ProductPropertiesSchema,
|
||||
BillingInterval,
|
||||
FeatureOptions,
|
||||
UsageModel,
|
||||
FreeTrial,
|
||||
FreeTrialResponse,
|
||||
Price,
|
||||
} from "@autumn/shared";
|
||||
import { sortProductItems } from "../../pricecn/pricecnUtils.js";
|
||||
import {
|
||||
getItemType,
|
||||
isPriceItem,
|
||||
} from "../../product-items/productItemUtils/getItemType.js";
|
||||
import { getItemType } from "../../product-items/productItemUtils/getItemType.js";
|
||||
import { mapToProductItems } from "../../productV2Utils.js";
|
||||
import { getProductItemDisplay } from "./getProductItemDisplay.js";
|
||||
import { getAttachScenario } from "./getAttachScenario.js";
|
||||
@@ -28,6 +27,7 @@ import { isFreeProduct, isOneOff } from "../../productUtils.js";
|
||||
import { getFirstInterval } from "../../prices/priceUtils/priceIntervalUtils.js";
|
||||
import { itemToPriceOrTiers } from "../../product-items/productItemUtils.js";
|
||||
import { toAPIFeature } from "@/internal/features/utils/mapFeatureUtils.js";
|
||||
import { isPrepaidPrice } from "../../prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
|
||||
export const getProductItemResponse = ({
|
||||
item,
|
||||
@@ -123,16 +123,29 @@ export const getFreeTrialResponse = async ({
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getProductProperties = ({ product }: { product: FullProduct }) => {
|
||||
export const getProductProperties = ({
|
||||
product,
|
||||
freeTrial,
|
||||
}: {
|
||||
product: FullProduct;
|
||||
freeTrial?: FreeTrialResponse | null;
|
||||
}) => {
|
||||
let firstInterval: any = getFirstInterval({ prices: product.prices });
|
||||
if (firstInterval == BillingInterval.OneOff) {
|
||||
firstInterval = null;
|
||||
}
|
||||
|
||||
let hasFreeTrial =
|
||||
notNullish(freeTrial) && freeTrial?.trial_available !== false;
|
||||
|
||||
return ProductPropertiesSchema.parse({
|
||||
is_free: isFreeProduct(product.prices) || false,
|
||||
is_one_off: isOneOff(product.prices) || false,
|
||||
interval_group: firstInterval,
|
||||
has_trial: hasFreeTrial,
|
||||
has_prepaid: product.prices.some((p: Price) =>
|
||||
isPrepaidPrice({ price: p })
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -180,12 +193,12 @@ export const getProductResponse = async ({
|
||||
fullProduct: product,
|
||||
});
|
||||
|
||||
let freeTrial = await getFreeTrialResponse({
|
||||
let freeTrial = (await getFreeTrialResponse({
|
||||
db: db as DrizzleCli,
|
||||
product,
|
||||
fullCus,
|
||||
attachScenario,
|
||||
});
|
||||
})) as FreeTrialResponse;
|
||||
|
||||
return ProductResponseSchema.parse({
|
||||
...product,
|
||||
@@ -194,6 +207,6 @@ export const getProductResponse = async ({
|
||||
items: items,
|
||||
free_trial: freeTrial || null,
|
||||
scenario: attachScenario,
|
||||
properties: getProductProperties({ product }),
|
||||
properties: getProductProperties({ product, freeTrial }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import puppeteer from "puppeteer";
|
||||
import { timeout } from "./genUtils.js";
|
||||
import { Stripe } from "stripe";
|
||||
import {
|
||||
AppEnv,
|
||||
BillingInterval,
|
||||
BillingType,
|
||||
Customer,
|
||||
FullProduct,
|
||||
Organization,
|
||||
} from "@autumn/shared";
|
||||
import { BillingInterval, Customer, FullProduct } from "@autumn/shared";
|
||||
import {
|
||||
addDays,
|
||||
addHours,
|
||||
@@ -17,8 +10,6 @@ import {
|
||||
addWeeks,
|
||||
format,
|
||||
} from "date-fns";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
|
||||
|
||||
const STRIPE_TEST_CLOCK_TIMING = 20000; // 30s
|
||||
// const STRIPE_TEST_CLOCK_TIMING = 40000; // 30s
|
||||
@@ -26,7 +17,7 @@ const STRIPE_TEST_CLOCK_TIMING = 20000; // 30s
|
||||
export const completeCheckoutForm = async (
|
||||
url: string,
|
||||
overrideQuantity?: number,
|
||||
promoCode?: string,
|
||||
promoCode?: string
|
||||
) => {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
@@ -106,7 +97,7 @@ export const deleteAllStripeProducts = async ({
|
||||
active: false,
|
||||
});
|
||||
}
|
||||
}),
|
||||
})
|
||||
);
|
||||
console.log("Deleted", i, "of", stripeProds.data.length);
|
||||
}
|
||||
@@ -124,9 +115,7 @@ export const deleteAllStripeTestClocks = async ({
|
||||
for (let i = 0; i < stripeTestClocks.data.length; i += batchSize) {
|
||||
const batch = stripeTestClocks.data.slice(i, i + batchSize);
|
||||
await Promise.all(
|
||||
batch.map(async (clock) =>
|
||||
stripeCli.testHelpers.testClocks.del(clock.id),
|
||||
),
|
||||
batch.map(async (clock) => stripeCli.testHelpers.testClocks.del(clock.id))
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -149,7 +138,7 @@ export const deleteStripeProduct = async ({
|
||||
const config = price.config as any;
|
||||
if (config.stripe_price_id) {
|
||||
const stripePrice = await stripeCli.prices.retrieve(
|
||||
config.stripe_price_id,
|
||||
config.stripe_price_id
|
||||
);
|
||||
|
||||
await stripeCli.prices.update(config.stripe_price_id, {
|
||||
@@ -252,7 +241,7 @@ export const advanceTestClock = async ({
|
||||
});
|
||||
|
||||
await timeout(
|
||||
waitForSeconds ? waitForSeconds * 1000 : STRIPE_TEST_CLOCK_TIMING,
|
||||
waitForSeconds ? waitForSeconds * 1000 : STRIPE_TEST_CLOCK_TIMING
|
||||
);
|
||||
|
||||
return advanceTo;
|
||||
@@ -297,7 +286,7 @@ export const advanceClockForInvoice = async ({
|
||||
|
||||
console.log(
|
||||
" - advanceClockForInvoice (1): ",
|
||||
format(advanceTo, "dd MMM yyyy HH:mm:ss"),
|
||||
format(advanceTo, "dd MMM yyyy HH:mm:ss")
|
||||
);
|
||||
|
||||
if (waitForMeterUpdate) {
|
||||
@@ -318,7 +307,7 @@ export const advanceClockForInvoice = async ({
|
||||
|
||||
console.log(
|
||||
" - advanceClockForInvoice (2): ",
|
||||
format(advanceTo2, "dd MMM yyyy HH:mm:ss"),
|
||||
format(advanceTo2, "dd MMM yyyy HH:mm:ss")
|
||||
);
|
||||
|
||||
await timeout(STRIPE_TEST_CLOCK_TIMING);
|
||||
@@ -337,10 +326,10 @@ export const advanceMonths = async ({
|
||||
let advanceTo = new Date();
|
||||
for (let i = 0; i < numberOfMonths; i += 1) {
|
||||
// let numMonths = Math.min(numberOfMonths - i, 2);
|
||||
(advanceTo = addMonths(advanceTo, 1)), 10;
|
||||
((advanceTo = addMonths(advanceTo, 1)), 10);
|
||||
console.log(
|
||||
" - Advancing to: ",
|
||||
format(advanceTo, "dd MMM yyyy HH:mm:ss"),
|
||||
format(advanceTo, "dd MMM yyyy HH:mm:ss")
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -379,7 +368,7 @@ export const checkBillingMeterEventSummary = async ({
|
||||
start_time: Math.round(startTime.getTime() / 1000),
|
||||
end_time: Math.round(endTime.getTime() / 1000),
|
||||
customer: stripeCustomerId,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (event.data.length === 0) {
|
||||
@@ -402,7 +391,7 @@ export const getDiscount = async ({
|
||||
stripeId || customer!.processor!.id,
|
||||
{
|
||||
expand: ["discount.coupon"],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return stripeCustomer.discount;
|
||||
|
||||
@@ -13,14 +13,20 @@ export const CheckoutLineSchema = z.object({
|
||||
export const CheckoutResponseSchema = z.object({
|
||||
url: z.string().nullish(),
|
||||
customer_id: z.string(),
|
||||
scenario: z.nativeEnum(AttachScenario),
|
||||
lines: z.array(CheckoutLineSchema),
|
||||
product: ProductResponseSchema.nullish(),
|
||||
current_product: ProductResponseSchema.nullish(),
|
||||
options: z.array(FeatureOptionsSchema).nullish(),
|
||||
total: z.number().nullish(),
|
||||
currency: z.string().nullish(),
|
||||
next_cycle_at: z.number().nullish(),
|
||||
has_prorations: z.boolean().nullish(),
|
||||
// next_cycle_at: z.number().nullish(),
|
||||
next_cycle: z
|
||||
.object({
|
||||
starts_at: z.number().nullish(),
|
||||
total: z.number().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export type CheckoutLine = z.infer<typeof CheckoutLineSchema>;
|
||||
|
||||
@@ -8,6 +8,8 @@ export const ProductPropertiesSchema = z.object({
|
||||
is_free: z.boolean(),
|
||||
is_one_off: z.boolean(),
|
||||
interval_group: z.string().nullish(),
|
||||
has_trial: z.boolean().nullish(),
|
||||
has_prepaid: z.boolean().nullish(),
|
||||
});
|
||||
|
||||
export const ProductResponseSchema = z.object({
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"@types/recharts": "^2.0.1",
|
||||
"@wooorm/starry-night": "^3.6.0",
|
||||
"ag-charts-community": "^12.0.2",
|
||||
"ag-grid-community": "^34.0.2",
|
||||
"ag-grid-react": "^34.0.2",
|
||||
"autumn-js": "^0.0.93",
|
||||
"axios": "^1.8.3",
|
||||
|
||||
@@ -32,8 +32,8 @@ export default function PricingTable({
|
||||
|
||||
const intervals = Array.from(
|
||||
new Set(
|
||||
products?.map((p) => p.properties?.interval_group).filter((i) => !!i),
|
||||
),
|
||||
products?.map((p) => p.properties?.interval_group).filter((i) => !!i)
|
||||
)
|
||||
);
|
||||
|
||||
const multiInterval = intervals.length > 1;
|
||||
@@ -151,7 +151,7 @@ export const PricingTableContainer = ({
|
||||
{multiInterval && (
|
||||
<div
|
||||
className={cn(
|
||||
products.some((p) => p.display?.recommend_text) && "mb-8",
|
||||
products.some((p) => p.display?.recommend_text) && "mb-8"
|
||||
)}
|
||||
>
|
||||
<AnnualSwitch
|
||||
@@ -163,7 +163,7 @@ export const PricingTableContainer = ({
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-[repeat(auto-fit,minmax(200px,1fr))] w-full gap-2",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -214,7 +214,7 @@ export const PricingCard = ({
|
||||
" w-full h-full py-6 text-foreground border rounded-lg shadow-sm max-w-xl",
|
||||
isRecommended &&
|
||||
"lg:-translate-y-6 lg:shadow-lg dark:shadow-zinc-800/80 lg:h-[calc(100%+48px)] bg-secondary/40",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{productDisplay?.recommend_text && (
|
||||
@@ -223,7 +223,7 @@ export const PricingCard = ({
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col h-full flex-grow",
|
||||
isRecommended && "lg:translate-y-6",
|
||||
isRecommended && "lg:translate-y-6"
|
||||
)}
|
||||
>
|
||||
<div className="h-full">
|
||||
@@ -339,7 +339,7 @@ export const PricingCardButton = React.forwardRef<
|
||||
<Button
|
||||
className={cn(
|
||||
"w-full py-3 px-4 group overflow-hidden relative transition-all duration-300 hover:brightness-90 border rounded-lg",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
variant={recommended ? "default" : "secondary"}
|
||||
|
||||
Reference in New Issue
Block a user